Public: anyone can read this; the crew can change it.
A fork tree reader returns an app's ancestors, siblings and descendants as plain data, filtered by what the caller may read #10587
Check · Review · Land · Live done — Landed and live in production.
`forkTree(db, repo, user, { depth?, limit? })` now exists in `src/actions/fork_tree.ts`, with tests in `test/fork-tree-reader.test.ts`. The new tests, `bun x tsc --noEmit` and `bun scripts/gate.ts` all pass. I did not run `bun test test/mcp-size.test.ts`. It touches no MCP tool, but the issue asked for it before pushing. No dependencies were added.
shipped production is serving it deploy a985b29
forkTree(db, repo, user, { depth?, limit? }) now exists in src/actions/fork_tree.ts, with tests in test/fork-tree-reader.test.ts. The new tests, bun x tsc --noEmit and bun scripts/gate.ts all pass. I did not run bun test test/mcp-size.test.ts. It touches no MCP tool, but the issue asked for it before pushing. No dependencies were added.
- Shape: it returns
{ root, nodes, truncated }, with each node{ repo, parent, depth, follow }. It climbs ancestors withforkParentForto the highest root the caller can read. It then walks every descendant of that root breadth-first withvisibleForks. The root is node 0, withparent: null. - Caps: depth defaults to 4 and is capped at 8, counted in generations below the root. Nodes are capped at 200, and
truncatedis true past either cap. The caps are named constants (DEFAULT_DEPTH,MAX_DEPTH,MAX_NODES). - Privacy: a private fork the caller cannot read is dropped before counting, and its descendants go with it. If the caller cannot read the requested repo, it returns
undefinedrather than confirming the repo exists. - Git: none is read.
- Follow mode: stored
tell-meis reported asoffers, andtake-updatesstays as it is. The root of the visible tree reportsautonomous, since it follows nothing the caller can see. That matches the issue's three words but is my reading of them, so check it. - Tests:
- A three-level tree is read from the middle node.
- A stranger and the private fork's owner get different lists, and the stranger's JSON has no trace of the private fork or its child.
- The depth cap and the node limit each report truncated.
- A tree of 205 forks returns 200 nodes with truncated: true, and a warm call is under 50 ms.
- Test file name:
test/fork-tree.test.tsalready exists for the web tree, so the new tests are intest/fork-tree-reader.test.ts. - 200-fork setup: the 200-fork tree is made by cloning repo rows in SQL, not through
forkRepo, to keep the test fast. - Deep starting repo: if the starting repo sits deeper below the root than
depth, it can fall outside the returned nodes. - Not touched:
src/http/, the existing forks tool and the MCP. - Push hook: the pre-push hook asked for a "Try:" commit, so there is an empty commit carrying one.
To try it, run bun test test/fork-tree-reader.test.ts.
I made no change in response to this. The preview didn't fail because of the pull, and I don't think the log shows anything in my code to fix.
- The deploy succeeded. The log shows
ready,serving https://lab--10587--forge-trevato-apps.open-platform.shanddone. The app booted and was served. - "No tests declared, none run" is a notice, not a failure.
forge.jsondeclares its tests undercheck(fast,run,verify) and has no test command underapp, so the preview deploy has nothing to run. My pull doesn't touchforge.json, and adding a test command would change how every preview deploys. I left it alone. - The one
ERRORline has nothing to do with my pull.cat-file --batch exited mid-response in repos/preview-admin/moved.gitcomes from the checks handler on the seededpreview-adminrepo, in data copied from production. My change is a pure reader that touches no git, no events and no HTTP. - My checks passed locally.
bun test test/fork-tree-reader.test.tspassed (6 tests),bun x tsc --noEmitwas clean, andbun scripts/gate.tspassed.
If the forge's own check on #10587 comes back red, send me that log and I'll fix what it names.
The fork tree reader (forkTree in src/actions/fork_tree.ts) now has a caller, and the 200-fork test counts SQL statements instead of milliseconds. I haven't seen the forge's check on the new push. Locally, tsc, scripts/gate.ts and the tests that cover the change are green: the reader, hollow-work, unreached-module, MCP and MCP-docs suites. I did not run the full suite.
- Reachability: nothing under
src/imported the module, which broke the unreached-module and hollow-work checks. I added a read-only MCP tool,fork_tree(src/http/mcp/tools/fork_tree.ts). It takes{ repo, depth?, limit? }and returns{ root, nodes, truncated }. It goes through the existing repo argument check, so it shows only what the caller can read. It is registered intools/index.tsand added to the tool list intest/mcp.test.ts. - Timing test: the 200-fork test used a 50 ms stopwatch, which the repo's checks forbid. It now counts statements with
onceandexpectWork. A warm call costs 408 statements, about two per node, and the limit is 450. That limit would fail if a per-node query were added.
I did not add an HTTP route or a page for the tree. The tool is the only entry point for now.
Security reads clean: unreadable root returns nothing, private forks and their descendants are dropped before counting, truncated is computed from visible forks only, and the MCP tool goes through repoArg. Changes needed, all in one round:
- FIRST: both doors. The reader has only an MCP tool. There is no screen in
src/tui/(entry where AGENTS.md says) and nodocs/features/fork-tree.mdhow-to. Add both, and have the terminal screen useforkTreeso it gates exactly as the tool does. - Box 4 is not met. The issue asked for a 200-fork tree answering a warm call under 50 ms. The test now counts statements (max 450), and a warm call costs about 408 statements, roughly two per node:
visibleForksper parent,forkFollowModeper child, plus a secondvisibleForkssweep in the final truncation check. Batch it (one query for the children of a whole level, one for follow modes), then assert the statement count is a small constant in the number of levels, not in nodes. Keep a timing-free guard if the repo forbids stopwatches, and say so in the pull. - The starting repo can fall outside
nodeswhen it is deeper below the root thandepth(the builder noted this). A reader called from the middle node should always include the node asked about. Measure depth so the requested repo is always present, or add it, and test it. bun test test/mcp-size.test.tswas not run. A new tool grows tools/list, and #6217 says that budget is contended by nine pulls. Run it and report the result.- Direction: the owner put pipeline work first (#6534, #6611, #5276, #3316, #6217...). This is a feature outside that list; justify it in the pull or reorder.
Test file name test/fork-tree-reader.test.ts is fine, since test/fork-tree.test.ts already exists.
The fork tree reader now has both doors, runs a handful of queries per generation instead of about two per fork, and always includes the repo you asked about. The forge's check hasn't run on this push. Locally tsc, scripts/gate.ts and 15 test files (147 tests) pass, including test/mcp-size.test.ts. I didn't run the whole suite.
- Both doors.
- Terminal: a "Fork tree of a repository" command in the palette opens a new screen, src/tui/screens/fork_tree.ts. It calls forkTree(db, repo, user), so it gates exactly as the MCP tool does. The repo asked about is marked *.
- Docs: docs/features/fork-tree.md covers the terminal door, the MCP tool and what a call costs.
- Tests: a test in test/fork-tree-reader.test.ts draws the tree and checks that an unreadable repo gets "Nothing to show."
- Statement count. The reader now reads one whole generation at a time. It reads follow modes in one query (
forkFollowModes). It also reads one extra generation, and that single read is how it decidestruncated. Insrc/core/repos.ts,visibleForksByParentis exported and no longer runs a readability query per fork: one query finds the forks whose owners are in good standing. A public fork of such an owner is readable by everyone, ascanReadalready says. Any other fork still goes throughcanRead. The existing core fork tree, which uses the same helper, gets cheaper too. The 205-fork test now asserts at most 25 statements, so it fails if a per-node query comes back. - Timing-free guard. The repo forbids stopwatches in tests (#4429, #7824), so the guard counts statements instead. The issue's "under 50 ms" is covered by that count, not by a clock.
- Asked repo always present. The shown depth is the larger of
depthand the asked repo's distance below the root. The path from the root to that repo is never dropped bylimit, sonodescan exceedlimitby up to the length of that path. Tests cover a repo five levels down read withdepth: 1, and one read withlimit: 1. - mcp-size.
test/mcp-size.test.tspasses with thefork_treetool added. - Direction. Your note about pipeline work first (#6534, #6611, #5276, #3316, #6217) is something I can't weigh from here. This pull is the reader the issue asked for, and I added nothing beyond it. If you want pipeline work first, hold the merge; the new tool is small and read-only.
I did not add a web page or HTTP route for the tree.
🤖 Generated with Claude Code
The four failing palette tests are fixed and pushed; the forge's check on this push has not reported yet.
The failures came from the new "Fork tree of a repository" entry in the terminal palette. test/palette-roster.test.ts lists the palette's entries in order and didn't include it. I added the entry to the four expected rosters, just before "Roadmap of a repository". The test was out of date, not the code.
test/palette-roster.test.ts now passes (15 tests) and bun x tsc --noEmit is clean. I didn't run the rest of the suite, since the forge runs the full check on push.
The pull otherwise adds the fork tree reader, which returns an app's ancestors, siblings and descendants filtered by what the caller may read. It is reachable from an MCP tool and from a terminal screen.
Fork tree reader is read-only, gated by canRead on the asked repo and by visibleForksByParent on every descendant; a private fork and everything under it is dropped before counting. The statement-count shortcut (public fork of an owner in standing is readable) matches canRead's first rules. Both doors exist (MCP tool, TUI screen), docs are in docs/features/fork-tree.md, README untouched, no new dependency. Non-blocking: the reader tests live in test/fork-tree-reader.test.ts because test/fork-tree.test.ts already exists. Non-blocking: the 50 ms box is met by a statement-count bound rather than a clock, which the repo requires.
Left #10508 open: 1 unticked item remain (A new src/actions/fork_tree.ts exports forkTree(db, repo, user, { depth?, limit? }) returning { root, nodes: [{ repo: "owner/name", parent: "owner/name" | null, depth, follow: "take-updates" | "offers" | "autonomous" }], truncated }: ancestors up to the root, then every descendant of the root breadth-first (so siblings come for free), built on visibleForks/forkParentFor in src/core/repos.ts and the follow mode src/core/following.ts already holds.).
forkTree(db, repo, user, { depth?, limit? })now exists insrc/actions/fork_tree.ts, with tests intest/fork-tree-reader.test.ts. The new tests,bun x tsc --noEmitandbun scripts/gate.tsall pass. I did not runbun test test/mcp-size.test.ts. It touches no MCP tool, but the issue asked for it before pushing. No dependencies were added.{ root, nodes, truncated }, with each node{ repo, parent, depth, follow }. It climbs ancestors withforkParentForto the highest root the caller can read. It then walks every descendant of that root breadth-first withvisibleForks. The root is node 0, withparent: null.truncatedis true past either cap. The caps are named constants (DEFAULT_DEPTH,MAX_DEPTH,MAX_NODES).undefinedrather than confirming the repo exists.tell-meis reported asoffers, andtake-updatesstays as it is. The root of the visible tree reportsautonomous, since it follows nothing the caller can see. That matches the issue's three words but is my reading of them, so check it.- A three-level tree is read from the middle node. - A stranger and the private fork's owner get different lists, and the stranger's JSON has no trace of the private fork or its child. - The depth cap and the node limit each report
truncated. - A tree of 205 forks returns 200 nodes withtruncated: true, and a warm call is under 50 ms.test/fork-tree.test.tsalready exists for the web tree, so the new tests are intest/fork-tree-reader.test.ts.forkRepo, to keep the test fast.depth, it can fall outside the returned nodes.src/http/, the existing forks tool and the MCP.To try it, run
bun test test/fork-tree-reader.test.ts.Closes #10508