Every ticket as the root of its provenance graph — goal, patch, approval, runs and (as the ticket-class build lands) mandate, manifestations, guarantees, journey, artifacts and children. Rendered through the <<<TicketGraphList>>> tag.
goal
Build the FORGE FAMILY TREE / EVOLUTION tool: cluster every forge repo into lineages, classify how
each family forked, name the CANONICAL tip, and produce a stage-by-stage evolution tree plus a
reconciliation plan for the few families that truly diverged.
WHY (measured, not theoretical — a prototype was run on 2026-07-15 over config/forge/remotes/*.git):
* 264 bare repos collapse to 109 DISTINCT LINEAGES (grouped by root-commit set).
* Of those 109: 68 IDENTICAL (pure duplicates, same tip ingested from several paths)
36 LINEAR (one tip supersets the rest; the others are STALE copies)
3 DIVERGED (true forks: jazz.git, notes_db.git, z3_mcp.git)
2 SPLIT-STORE (objects not shared; need a fetch before they can be compared:
big-jazz.git, code__code.git)
So only ~5 families carry real reconciliation work. The other ~104 need NO merge — just pick the
superset tip and drop the aliases. The 264 is largely an ingestion-path illusion (code__X,
code__rebuild__X, gitlab__<group>__X, gitlab__<group>__jazz-forge__X, and bare X are the SAME
project ingested repeatedly).
* THE MOTIVATING CASE — "THE FORK THAT CAME HOME" (operator-defined, do not re-derive):
jail.git (47 commits) is topologically a linear SUPERSET of agent_tools_suite.git (46) — shared
root 379ac05319, ats tip 888d3447c6 is a direct ancestor of jail tip 38e95ad16d, and jail adds 6
files (crankws-mcp/server.py +138, start-crank.py, claude-gui.py, registry.crankws.json,
crank_job_backlog.json, crank_job_congruency.json).
BUT JAIL IS **NOT** CANONICAL AND **NOT** A DESTINATION. Per the operator: jail was an EXPEDIENT —
a name wrapped around some packages, dropped in a folder, followed by cleaning up a large mess of
tests in a hurry. jail is a GRAB-BAG whose packages belong to SEVERAL DIFFERENT home repos.
The correct direction is the REVERSE of what the topology suggests: jail's contents MERGE BACK
into the existing/main repos they belong to; the main repo is UPDATED; jail's code thereby becomes
REDUNDANT (the updated codebase contains it) and jail is then OMITTED.
Do NOT adopt jail as canonical. Do NOT re-scope the agent_tools_suite rebuild onto jail —
agent_tools_suite IS a home repo and #412-436 are correctly scoped.
DELIVERABLE: checkouts/current/congruency/tools/forge_family_tree.py — standalone, no deps beyond
git + stdlib + sqlite3. It MUST:
1. DISCOVER + CLUSTER. Walk config/forge/remotes/*.git. Group by root-commit SET
(`git rev-list --max-parents=0 --all`). Repos sharing a root are one lineage.
2. RESOLVE TIPS CORRECTLY — the prototype's key gotcha: MOST of these bare repos have an
UNRESOLVABLE HEAD (`git rev-parse HEAD` returns the literal string "HEAD"; `--verify` fails and
silently drops the repo, which produced a wrong "0 diverged" answer on the first run). Resolve
tips via `git for-each-ref refs/heads`, preferring main/master, else the ref with most commits.
3 repos have NO refs at all — report them, never skip silently.
3. CLASSIFY each family:
IDENTICAL — all members share one tip.
LINEAR — the max-commit tip is an ancestor of every other tip (superset). Report the
superset tip and the stale aliases.
FORK-CAME-HOME — *** THE SAME GIT SHAPE AS LINEAR. *** A fork whose additions must be merged
BACK into its parent/home, after which the fork is retired. Topology CANNOT
distinguish this from LINEAR — they differ only in INTENT, which is not in the
DAG. Therefore: NEVER auto-decide direction. The tool REPORTS topology,
PROPOSES a direction, and MUST accept an explicit operator-supplied `home`
mapping that overrides the proposal. "home" is per-PATH / per-PACKAGE, not
per-repo: one grab-bag fork (jail) can have packages belonging to several
different home repos.
DIVERGED — some tip is NOT reachable from the candidate: compute `merge-base`, and report
each side's unique commits and file-level diff.
SPLIT-STORE — a member's tip object is absent from the candidate's store. Do NOT guess:
fetch into a scratch object store (or use --alternates) and then re-classify.
4. EVOLUTION IN STAGES. For each family emit the tree: root -> shared spine -> fork points -> tips,
annotated with commit dates (stage boundaries), which repo/alias sits at each node, and what
each fork ADDED vs the spine. Additive-only divergence must be distinguished from conflicting
divergence (both sides modified the same file) — that distinction IS the reconciliation cost.
5. RECONCILIATION PLAN per family:
IDENTICAL -> keep canonical, drop N aliases (zero work).
LINEAR -> fast-forward: canonical already contains everything; drop stale aliases.
ADDITIVE -> union: the merge is mechanical (no conflicting hunks).
CONFLICTING-> flag for synthesis: list the conflicting files; this is the ONLY case needing a
human/competitive-synthesis decision (see #206).
5b. RETIREMENT GATE — CONTAINMENT PROOF (mechanical, not a judgement call). A fork is SAFE TO OMIT
IF AND ONLY IF every file at the fork's tip resolves, by content identity, to a blob present in
the home repo(s) after the merge-home update. Use the SAME module-sha the ticketer computes:
sha256("raw:"+bytes)[:16], or sha256("ast:"+_ast_norm(src))[:16] for Python that parses.
* ALL blobs resolve -> fork is a strict SUBSET of home = redundant BY CONSTRUCTION -> report
"safe to omit (proven)".
* ANY blob unresolved -> that file IS outstanding merge-home work. Emit the exact list. The
fork MUST NOT be dropped while the list is non-empty. The unresolved list IS the worklist;
it shrinks to empty exactly when the fork is safe to drop.
AST-normalized identity means a file that came home with only whitespace/comment drift still
counts as contained (same module), while a semantically changed file correctly FAILS and shows up
as real work. "Prove the fork is a subset, then delete" — never "delete and hope".
6. OUTPUT: (a) machine-readable — a sqlite db or JSON: families, members, tips, classification,
canonical tip, fork edges, reconciliation verdict; (b) human-readable ASCII family tree.
7. --selftest on THROWAWAY git repos built in a temp dir that synthesize each class (identical /
linear / additive-diverged / conflicting-diverged / split-store / no-refs / unresolvable-HEAD),
proving each is classified correctly. No network, no live db, no writes outside the temp dir.
DO NOT USE omni.git AS THE SOURCE OF TRUTH — it is INCOMPLETE. Measured: omni.git is MISSING
agent_tools_suite's tip 888d3447c6 (it holds the common line but not the fork's feature commits).
config/forge/remotes/*.git is authoritative. Optionally REPORT omni coverage gaps as a side output.
DOWNSTREAM VALUE (state it in the tool's own docstring, do not implement here): the canonical-tip
map is what should drive forge2 rebuild scope — ticket modules ONLY from canonical tips, skip stale
variants (a stale fork's older foo.py hashes to a DIFFERENT module-sha than the tip's foo.py, so
today it mints a second ticket and we rebuild an obsolete module), and rebuild one lineage into ONE
forge2 package.
ACCEPTANCE: --selftest green; a real run over config/forge/remotes reproduces the prototype's
headline numbers (264 repos -> 109 lineages; 68 IDENTICAL / 36 LINEAR / 3 DIVERGED / 2 SPLIT-STORE,
with the 2 split-store families RESOLVED after fetch rather than left unknown); correctly classifies the jail.git / agent_tools_suite.git family as FORK-CAME-HOME (a topological
superset whose direction is REVERSED: jail merges home and is then retired) and does NOT name jail
canonical; honours an operator-supplied `home` mapping that overrides its proposal; implements the
containment retirement gate and emits jail's unresolved-blob worklist; prints a readable family tree.
RUN THIS ON CLAUDE (multi-file git reasoning; not a GLM task).