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.
Forge evolution PART 2 (v2, supersedes #449): EVOLUTION TIMELINE from git dates + containment retirement gate + 118-blob orphan worklist. OMNI IS BLACK-BOXED — it is not a git (every git cmd fails) and covers 11% of forge; config/forge/remotes is the only authority.
goal
PART 2 OF 2. Part 1 (#445) LANDED on main: tools/forge_family_tree.py clusters 264 bare repos into
109 lineages, classifies each, and emits --json. It deliberately never retires or merges anything.
THIS ticket adds the two things Part 1 does not do: the EVOLUTION TIMELINE and the RETIREMENT GATE.
DELIVERABLE: checkouts/current/congruency/tools/forge_evolution.py — standalone, stdlib + git only.
CONSUME Part 1, do not re-derive it: run `forge_family_tree.py --json -` (or accept --family-json PATH)
and build on its lineages/members/tips/classification. Part 1 already solved tip resolution (183 of
264 repos have an UNRESOLVABLE HEAD — a symref to a refs/heads/master that does not exist; `rev-parse
HEAD` prints the literal string "HEAD" and EXITS 0). Never re-resolve tips yourself; never trust HEAD.
1. EVOLUTION IN STAGES — the temporal dimension. Part 1 carries NO time data at all (verified: zero
ts/date keys in its JSON). Git already has every date for free. For each lineage emit a TIMELINE:
* root commit date (`log -1 --format=%ai <root>`)
* FORK POINT date per member (`log -1 --format=%ai $(merge-base <canonical-tip> <member-tip>)`)
* tip date per member
* the fork WIDTH — tip date minus fork-point date — i.e. how long the fork ran alone.
Order members by fork-point date so the tree reads as stages, oldest first.
WORKED EXAMPLE to reproduce (measured, agent_tools_suite lineage, root 379ac05319):
2026-07-08 20:38 root 379ac05 "agent_tools_suite: crankimg ..."
2026-07-11 02:23 FORK POINT = agent_tools_suite tip 888d3447c6 ("add deploy_test loop")
2026-07-12 14:07 jail tip 38e95ad16d -> jail ran 4 days alone, adding 6 files
2. OMNI IS BLACK-BOXED — DO NOT OPEN IT. omni.git is NOT a git; it is a TOOL MADE OF GITS (an
archive of many repos' refs). Every ordinary git command against it FAILS — measured:
git -C omni.git rev-parse --abbrev-ref HEAD -> "HEAD" (the literal string)
git -C omni.git ls-tree HEAD -> fatal: Not a valid object name HEAD
git -C omni.git log -1 -> fatal: your current branch 'master' has no commit
It also does not cover the forge: measured 264 forge bare repos vs 145 repos in omni, of which
only 29 overlap — omni COVERS 11% AND MISSES 235 OF THE FORGE REPOS. (The "~86%" figure the
ticketer cites is BLOB coverage in `occ`, not repo coverage. Do not conflate them.)
THEREFORE: do not walk refs/archives/*, do not read omni.git, do not treat it as a source of
dates or provenance, and do not confuse it with forge. config/forge/remotes/*.git is the ONLY
authority. If a future omni interface is wanted it is a separate tool with its own contract —
NOT this ticket, and NOT by an agent reaching into omni's internals.
3. RETIREMENT GATE — CONTAINMENT PROOF (mechanical, not a judgement). A fork is SAFE TO OMIT IF AND
ONLY IF every file at its tip resolves, by CONTENT IDENTITY, to a blob present in the home repo(s).
Use the SAME module-sha the ticketer computes:
sha256("raw:" + bytes)[:16] — non-Python, or Python that does not parse
sha256("ast:" + codebean._ast_norm(src))[:16] — Python that parses
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 surfaces as work.
* every blob resolves -> report "SAFE TO OMIT (proven)" — proven, never assumed.
* any blob unresolved -> emit the exact file list. The fork MUST NOT be dropped while that list
is non-empty. THE UNRESOLVED LIST *IS* THE MERGE-HOME WORKLIST; it
shrinks to empty exactly when the fork is safe to drop.
4. THE ORPHAN SET. Measured across the forge: 118 blobs (2.1% of 5,741 unique code blobs) exist ONLY
in non-canonical tips — content that would be SILENTLY LOST by naively unifying on canonical tips.
Emit them as a first-class report: blob sha, path, which member holds it, which lineage. This is
small enough to review by hand and is the highest-value output of this tool.
5. DIRECTION IS NOT YOURS TO DECIDE. Part 1 already accepts --override / --override-file for the
operator's per-PACKAGE home mapping (a grab-bag fork like jail has packages belonging to SEVERAL
different homes). Honour that mapping; PROPOSE only; never infer direction from topology. LINEAR
and FORK-CAME-HOME are the same git shape and differ only in intent, which is not in the DAG.
NOTE: for LINEAR the direction cannot lose code (canonical supersets the rest) — it is a naming
decision. Only DIVERGED/SPLIT-STORE put content at risk. Say so in the report.
6. READ-ONLY, ALWAYS. Never write to config/forge/remotes/*.git — the bare repos are the archive.
Use `git show`/`cat-file`/`ls-tree`/`log`/`merge-base` plumbing only. Never write to the
governance db. This tool PLANS retirement; it never performs it.
7. --selftest on THROWAWAY git repos in a temp dir: a fork fully contained in home (-> SAFE TO OMIT),
a fork with one un-merged file (-> worklist of exactly 1, NOT safe), whitespace/comment-only drift
(-> contained via ast identity), a semantic change (-> correctly NOT contained), and a fork-width
timeline assertion. No network, no live db, no writes outside the temp dir.
ACCEPTANCE: --selftest green; a live read-only run reproduces the measured 118-blob orphan set and
prints the agent_tools_suite timeline above (root 2026-07-08 20:38 -> fork point 2026-07-11 02:23 ->
jail tip 2026-07-12 14:07, width 4 days, 6 files added); jail reports NOT-safe-to-omit with its 6
files as the worklist (they have not come home yet); no writes anywhere. RUN ON CLAUDE.
patch
none
approval
unapproved
runs
no runs recorded
mandate (clauses)
not yet recorded — lands with the ticket-class build
manifestations
not yet recorded — lands with the ticket-class build
guarantees
not yet recorded — lands with the ticket-class build
journey (blunders & successes)
not yet recorded — lands with the ticket-class build
artifacts (forge)
not yet recorded — lands with the ticket-class build