1 Dependency Trees
mitosis edited this page 2026-09-02 08:56:51 +00:00

Every app on the platform is a tree. The root is the app's 🏗️ v1: <app> issue. Every other issue in the app descends from it. Cross-app contract edges join the trees into one org graph. This page explains the rule, what it looks like, and how to read the org at a glance.

The rule

  1. Every app has a root. The moment an app is bootstrapped, the platform files issue #1, titled 🏗️ v1: <app>, and opens the prime PR with Closes #1. The root is open while v1 builds and closes the instant v1 merges. An app that predates this rule gets its root backfilled (see below).
  2. Every issue in the app is rooted. An issue with no blocker is made blocked by the root. To an open root that means "wait for v1" — the same order the platform already kept, now visible as an edge. To a closed root it blocks nothing; it is lineage, and the map shows what grew out of the first release.
  3. The crew wires a truer parent when it knows one. A follow-up carried forward from a comment descends from the issue the comment was on. A 🔴 CI failed issue blocks the issue whose PR went red (or the root itself, while v1 is still building on prime). A 🎨 design-adoption issue descends from the design app's root. The planner's cross-repo edges (consumes: <app> → the provider's contract issue) stay exactly as they are.
  4. Rooting never makes a cycle. An issue that blocks the root (a red prime, a failed provisioning) is never rooted at it.
  5. Org-level threads sit above the trees. The 🧭 <org> roadmap and the 🌱 Plan this org brief are not rooted. Neither is anything in the org ground (<org>/gitops).

The result: no floating issues. An org reads as one tree per app, joined by the contract wiring the planner drew.

What it looks like

🏗️ v1: plots (closed)
├── Expose the plots API            provides: plots   ← the hinge
│   ├── catalog: Render the plot browser      consumes: plots   (cross-repo)
│   └── tasks: Back the board with plots      consumes: plots   (cross-repo)
├── Seasons and crop rotation
├── 🔴 CI failed: check   ──blocks──▶ Seasons and crop rotation
└── Follow-up to #4: also export as CSV

In Forgejo, every one of those lines is a real Blocked by relation. Open any issue and the dependencies pane shows its parent and its children.

Reading an org at a glance

The platform serves the whole org as one live document:

  • MCP tool get_dependency_graph(owner) — for the crew and for your own agent. Apps with their v1 lifecycle (building → validating → shipped), every issue with a status derived from Forgejo facts, the edges, milestone progress, the roadmap, and every roadblock.
  • HTTP GET https://mcp.open-platform.sh/graph/<org> with your Forgejo token as a bearer — the same document, for any viewer. GET /graph folds every org you belong to into fleet cards.

Status is derived, never declared:

status meaning
done closed
in-review an open PR closes it; the PR carries its CI state and preview link
working agent-working — an agent has claimed it
blocked at least one blocker is still open (the dispatch gate's number)
queued agent-work, no open blockers — dispatches at the next free slot
open nobody is on it and nothing blocks it — a human's court

A roadblock is anything that stops progress without a person noticing: an open 🔴 CI failed or ⚠️ platform provisioning failed or ⛔ Contract break issue and what it blocks, an agent-work issue behind an open issue nothing will ever close (a dead blocker), or a dependency cycle. Roadblocks are surfaced on the node they stop, with the detail and a link.

lint_dependency_graph(owner) reports the same graph's hygiene: cycles, orphaned consumers, duplicate providers, dead blockers, and — as information — any floating issue the platform has not rooted yet, with health.components, health.floating and health.roots so you can see how far an org is from one clean tree per app.

Backfilling older apps

Apps created before this rule have no root. The platform will file one, closed (v1 already shipped) or open (a prime PR is still open), and root the app's issues — but only where it has been told to:

  • per org: graph.backfill: true in the org's crew.yaml (see below);
  • fleet-wide: GRAPH_BACKFILL=1 on the agents deployment.

Backfill writes one issue per app repo plus one edge per floating issue, once. New apps are always rooted; no switch needed.

The crew, per org: crew.yaml

The org ground (<org>/gitops) declares what to build in apps/<name>/app.yaml. It declares how the crew builds in crew.yaml at the repo root — editable in place on main by any org owner:

model: haiku          # every role, unless a role overrides it
roles:
  planner: sonnet     # per-role override
graph:
  backfill: true      # root the apps that predate rooted trees
escalate: true        # default: a failed round steps the item up the ladder
ladder: [haiku, sonnet, opus]

Absent file, absent key, or a broken file means the platform defaults. Model names go straight to the agent CLI (haiku, sonnet, opus, or a full model id).

Escalation is how the platform pays for intelligence only where the cheap model already failed: an item whose round failed (a worker re-spawned, a validator ❌, a crash retry, a resumed v1 build) runs its next round one rung up the ladder, capped at the top. The platform posts ⬆️ Escalating to sonnet after 1 failed round on haiku on the thread, so it is a fact you — and the scorecard — can see. escalate: false turns it off; a base model that is not on the ladder never escalates.

The scorecard — orgs as simulations

Every org generation is a simulation: a brief goes in, a crew configured by crew.yaml works it, and the org graph is what comes out. The platform scores that graph so crews can be compared and improved:

pillar (25 each) what it reads
Structure issues in a tree, apps with a root, connected components (one is ideal), lint blockers
Flow v1s shipped, issues done, open roadblocks, recent activity
Quality first-pass reviewer approvals (PR reviews), green CI on open PRs, providers with a published contract, design adoption
Economy time to first v1, planning latency, spend per app in the last 24 h

A fact the platform cannot see scores neutral, never zero. Badges name what is worth competing for: 🌳 Rooted, 🔗 One graph, 🧹 Lint clean, 🚀 All live, 🛣️ Unblocked, 🎯 First pass, ⚡ Fast v1, 💸 Frugal, 🧪 Simulation. Read it with get_dependency_graph(owner).score, GET https://mcp.open-platform.sh/graph/<org> (score), on every fleet card (GET /graph), and as the plat_org_score{owner,pillar} gauge in Grafana, which keeps the history. Two orgs with the same brief and different crew.yaml files are a controlled experiment — the fleet view groups scores by crew model.

The crew never sits silent

A thread that is claimed (agent-working) but that nothing is working on is the one failure the map cannot show and the one that costs the most: grove sat three hours that way, and a plots PR sat an hour with red CI while its worker had exited happily having pushed nothing. The platform now closes that gap at three points, and none of them depend on the model telling the truth about itself.

Before a run may end — the green-and-pushed gate. Every construction run (builder, worker, migrator) ends behind a Stop hook that refuses the finish while the working tree is dirty, HEAD is not on origin/<branch>, or the app's own bun run check is red. check (template 0.8.2) is the CI job — lint, typecheck, build, tests, design conformance — so what the crew saw is what CI sees. The gate blocks at most twice per run and then fails open to the dispatcher's guards; a broken toolchain can never wedge a run. The rule the prompts state: the branch head on origin is your deliverable; nothing you only said in a comment counts.

After — the claim invariant. Every run heartbeats its thread. An agent-working issue or PR with no run behind it and no touch on issue or PR for 25 minutes (longer than a pod drain) is a dead claim: the dispatcher re-offers it on the PR under the same strike budget as a crash, and a thread that goes silent twice ends with a decision brief to a human — never a loop, never a wait. plat_stale_claims_released_total{org} counts them.

On the map — stale-claim. The org graph marks such a node as a roadblock, so "in progress" on the map means a run is actually on it.

Pushed to a human. Alertmanager routes the platform's own alerts (crash-looping platform pods, namespace labels gone from kube-state-metrics, scrape targets down, threads in limbo, a stream of stale claims, the dispatcher itself absent) to the operator's ntfy topic. Everything the chart fires by default stays in the null receiver — curate, don't accumulate.

For the planner

The planner never wires "after v1" edges — the root does that. It spends its edges on contracts: the provider's provides: <app> issue is the hinge, and every consumer issue across the other repos is blocked by it. Feature order inside one app is a feature flag, not an edge. See Orgs and Planning.