Repository navigation
cockpit v3: level views and lenses, Needs you and Find, last-look marks (S8, S9, S10) - #213
Merged
Merged
Conversation
6 of 13 tasks
…ks (S8, S9, S10)
S8. GET …/n/{ref}/level/{level} (grouped under parents, all-done groups
folded, a cursor per group) now filters by `state` and `q`. node_lenses.py
adds the lenses the node page does not carry: GET …/n/{ref}/lineage
(ancestors top to bottom with their one line, a need's evidence, the
proposal that approved a feature, the levels below and the results),
GET …/n/{ref}/timeline?from=&to= (one lane per child from the recorded
spans: stages and task sessions as work bars; queued and ready stages,
merges, open asks and agents parked on a person as waits, amber only when a
person is the cause; the wait a lane is in now is its status line; gaps are
waits; server-closed alarms never count; lanes never join) and
GET …/n/{ref}/money (children's cost against budget, most first, the total
and what no child holds).
S9. node_sheets.py: GET /factories/{fid}/needs-you?tree= ranks every ask by
the work it holds (the blocker paths' weight), with its breadcrumb and what
it holds by name; merges on one human-merged base are one item; asks that
block nothing come apart; closed alarms with their causes; the other tree's
count. GET /factories/{fid}/find?q= finds nodes by ref (T-16, omnigent-ai#9876, a
closed alarm), by what a decision or proposal is about (D-45, C-16) and by
title, each with its breadcrumb.
S10. factory_node_reads (workspace_id, factory_id, user_id, node_ref,
seen_at; migration fz8c9d0e1f2a, verified on PostgreSQL up/down/up) holds
one last look per person per node, only moving forward. PUT …/n/{ref}/seen
(people only) and GET …/n/{ref}?seen=1 (never from a runner) mark it; the
node page then carries rollup.since (the three most significant changes in
the subtree since that look, or the nearest ancestor's) and `changed` on
each child, from the event log placed on the nodes events are about.
The server writes nodeLenses.server.json for the web's lenses, sheets and
since lines (W7, W8, W10).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… tree's rule, no empty level groups (review)
- An answered proposal's ask is amber only when the proposal holds work by
the tree's rule (Tree.proposal_holds_work, shared with blocks): D-43 on
connectors-core, done, no longer cuts its done bar short.
- A lane's current wait, and its recorded waits still open, begin no earlier
than the lane's first bar and its last recorded work: an ask made while
T-15 ran leaves T-15 visible, and a merge is amber from when its PRs
became ready (the sample's ready PRs now carry ready_at 12:00). A person
is the cause of a leaf lane's wait only from their own recorded waits.
- A past queue names the project it waited on ("waits on assistant-core").
- Timelines sweep each lane once (a heap, not every bar per point), read the
last day by default instead of 7 days, and a roll-up lane's unbroken work
is one bar; 5000 spans in one lane take milliseconds.
- from/to outside 2000 .. a week from now answer 400 bad_window (no int64
overflow in SQL).
- Level views: state=later and state=ghost list those nodes as the items; a
group with nothing to show is left out.
- Needs you names the PRs a merge holds (omnigent-ai#9856, omnigent-ai#9857).
- Tests pin that PUT …/seen refuses runners (403 human_only) and that GET
?seen=1 from a runner marks nothing; the write-route walk covers the node
routes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LittleChimera
force-pushed
the
factory/v3-m2-views
branch
from
November 4, 2026 03:51
acf669e to
a4d3008
Compare
This was referenced Nov 4, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related issue
Closes # (none: cockpit v3 milestone 2, plan.md S8, S9 and S10)
Summary
This PR adds the server side of three steering aids for cockpit v3: level views and lenses (S8), Needs you and Find (S9), and "since your last look" marks (S10). Everything lives under
/v1/factorywith the usual factory-reader auth. v1 and v2 are not touched. Each route reads the same 5-second, per-viewer tree as the node page, so a viewer sees only the projects, decisions and cost they are allowed to read.ELI5: the cockpit can now answer four questions about any node. Where did this come from (lineage)? What happened under it today (timeline)? Where did the money go (money)? What changed since I last looked? Two factory-wide questions get answers too: what do I owe, most important first (Needs you), and where is T-16 (Find).
Plan bullet → what was done
GET …/n/{ref}/level/{level}: grouped under parents, all-done groups folded, cursors per groupstateandqfilters that api.md §3.6 listed as "not read yet" (400bad_state). A group the filters leave empty is dropped. A cursor pages the filtered group.GET …/lineagenode_lenses.lineage. Lists the ancestors root first. Folded implicit ancestors are flagged as on the breadcrumb. Every row has its compact status line. The root also has its why (repo → base), a need its evidence summary, a feature itsapproved by C-nlink (from C-nfor an implicit feature), and any row with progress shows it. Then come the node itself (here), the levels below it (levels_below) and its results (product tree only).GET …/timeline: spans become bars, waits become hatched gaps, amber when a person is the causenode_lenses.timelinegives one lane per shown child; runs and Later children are left out. Work comes fromstagespans for projects andtaskspans for task sessions. Waits come from queued and ready stages,ready_waitspans (amber unless the server merges into an auto-merge target), open or blocking decision spans, and agentwaitspans parked on a person. The wait a lane is in now is its compact status line, starting when it began. It is amber only when the lane's own path ends at a person; a path that hops to another node is grey. Gaps between bars are greywaiting. Spans of server-closed alarms (loopanddone_check) never count. Done lanes draw their work asdone. The window comes fromfrom/to(epoch seconds or ISO, at most 7 days; otherwise 400bad_window). By default it is the last day, starting at the hour of the first bar.last_lookis this viewer's mark.GET …/moneynode_lenses.money. Children are sorted by cost (the same numbers as their roll-ups), with budget and burn. The response also has the total, the node's own budget, andrest: what the node spent that no child holds, such as leads and agents.test_node_lenses.py:test_tasks_under_the_product_are_grouped_by_project,test_the_lineage_of_t16_reads_product_need_feature_project,test_the_timeline_of_agent_assistant_shows_the_panel_waiting_all_day. The panel lane is06:01–13:40 wait "Waits on assistant-core · held for C-16". assistant-core is planning → T-15 → T-16, then amber from 07:46. There are also tests for filters, a closed alarm never being a person's wait, auto-merge targets waiting on the server, windows, no lane joining another, money, and timing at scale.GET /factories/{fid}/needs-you: ranked by held weight, merges on one human-merged base grouped, block-nothing asks grouped, closed alarms with causes, the other tree's countnode_sheets.needs_you. Items are ranked byPaths.weight, the same held weight the blocker paths use, then by age. Each item has its breadcrumb (root first, ending at the node the ask sits on),holdsby name (newPaths.holds: the tasks whose own wait ends at the ask, then the projects waiting on where it sits) and itsweight. Ready PRs into one human-merged base are onemerge:<base>item.blocks_nothingis listed apart, oldest first.closed_alarms(product tree) carry their causes and real asks.other_treeis a count (400bad_tree).GET /factories/{fid}/findover refs and titles, with breadcrumbsnode_sheets.findmatches, in this order: refs (T-16,t-16,#9876,PR-9876, a slug, a closed alarm'sD-n), what a decision or proposal is about (D-45andC-16go to assistant-core;C-11goes to I-1 and then its projects, via newTree.proposal_nodes), titles that start with the words, then titles that contain them (higher levels first). An empty query lists the work that holds an ask. Results are capped atlimit(default 20, max 50). Done work never leaves the tree, so it is still found after 30 days.test_node_sheets.py:test_needs_you_ranks_by_what_each_ask_holds(D-45 holds T-16 and assistant-panel-ux),test_find_reads_task_pr_and_proposal_refs. Further tests cover merges as one item, closed alarms and the other tree, needs-you after D-45 is answered (4 left, as in api.md §8), viewer cuts, titles and breadcrumbs, done work still found, and the cap at scale.factory_node_reads (user_id, node_ref, seen_at)with a Postgres-verified migrationfz8c9d0e1f2a. The primary key is(workspace_id, factory_id, user_id, node_ref);seen_atis epoch seconds and only moves forward. The table is created only when missing, so CockroachDB can replay it. Also added:SqlFactoryNodeRead,FactoryNodeRead, andmark_node_seen/list_node_readson the abstract, in-memory and SQL stores (racing first marks replay on IntegrityError).PUT …/n/{ref}/seenGET …/n/{ref}?seen=1, which the page'sseenparam promised, now marks after building the response, and only for a person's request (a runner-origin GET does not mark).sinceitems in roll-ups andchangedon childrenlast_look.py. The mark used is the node's own, or else that of its nearest looked-at ancestor. The events since then (500 newest of 10 kinds) are placed on the node each is about. Ranking: a new ask that holds work > a merge or failure > work added > a change applied > an answer > a task done > a stage moved. Three merges or more read as one ("6 merged").rollup.since = {at, items[:3], more}. Each child getschanged. Not counted as changes: server-closed alarms, asks that block nothing, a builder going idle on an unfinished task, and work the viewer cannot read.test_nodes_api.py::test_last_looks_are_per_person_and_name_what_changed(alice getssince; carol has no mark, thenseen=1marks hers).test_last_look.py::test_the_root_names_the_three_most_significant_changesgives["D-49 new", "6 merged", "T-17 done"]withmore: 1. Further tests cover subtree scoping, noise, viewer cuts, ancestor marks and events before the mark. Store tests:tests/stores/test_factory_store_node_reads.py. Migration test:tests/db/test_migration_factory_node_reads.py.Response models are in
node_views.py:LineageView,TimelineView,MoneyView,NeedsYouView,FindViewandSeenView(extra keys forbidden).nodeLenses.server.json(new, besidenodePages.server.json) is the server's output for the web's W7, W8 and W10, kept in sync bytest_node_lens_wire.py. docs/FACTORY.md describes the lenses, sheets and last looks.Deviations from api.md
needs-you.blocks_nothingitems have the same shape asitems({decision, breadcrumb, holds: [], weight: 0}) rather than bareDecisionRefs, so the sheet can say where each one sits ("Open assistant-core").closed_alarmsis listed for the product tree only (in the factory tree they are Health's items already), and every sheet carriesversion.Crumbplusline(its compactStatusLine), with the optionalwhy_html(root),evidence(need),links(feature:approved by/fromC-n) andprogress. There is no "informed by" study link yet, because studies (R-n) are S14.factory_cost_daily, so the lens and the roll-up never disagree. It addsstate,tag, the node'sbudgetandrest.lanesis empty.stateandq.groupandlensstay client-side choices.fz8c9d0e1f2arevisesfz7b8c9d0e1f. If another M2 migration lands first, this one'sdown_revisionneeds a rebase (test_single_alembic_headwill say so).Test Plan
uv run --frozen pytest -q -n 8 tests/server/factory tests/stores tests/db tests/examples/test_factory_sample_tokens.py -p no:cacheprovider(SQLite, final commit): 5890 passed, 15 skipped. The only failures were the 11test_databricks_volumes_artifact_store.pycases, which need the optionaldatabricksextra that is not installed here; they are deselected on the standard lanes.upgrade head→downgrade -1→upgrade head: the table and its primary key appear, are dropped, and reappear.compare_metadatashows no diff forfactory_node_reads.OMNIGENT_TEST_DB_URIset,tests/stores/test_factory_store_node_reads.py,tests/stores/test_factory_store_round3.py,tests/server/factory/test_nodes_api.pyandtests/server/factory/test_cockpit_read.pyall passed: 132 passed.test_nodes.py,test_node_status.py(the held-weight change keeps every existing ranking),test_nodes_api.py(p95 < 300 ms with the extra mark read), the new lens, sheet, last-look and wire tests,test_factory_sample_tokens.py(FORBIDDEN_SAMPLE_TOKENS green), and the single-head and replay migration tests.pre-commit run --files <changed>: ruff format and ruff check pass. pyrefly reports only the 28 errors already on staging (runtime/telemetry.py,onboarding/sandboxes/gensee.py,background/reconcile.py), none in the files this PR changes.omnigent/runner,omnigent/tools,omnigent/policies,omnigent/factory.py) is untouched.Demo
N/A (server routes only). For a quick look, run a local server with a factory and try
curl …/v1/factory/factories/<fid>/needs-you,…/find?q=T-1,…/n/<feature ref>/timeline,…/n/<ref>/lineage,…/n/<ref>/money. ThenPUT …/n/product/seen, let something merge, andGET …/n/product:rollup.sincenames the merge.Type of change
Test coverage
Coverage notes
The unit tests run on the 13:40 sample factory plus
spans_1340(), a new fixture with the day's stages, task sessions, asks and ready PRs, and on the synthetic 13/45/360 factory. The integration tests drive the routes over a real store on SQLite and PostgreSQL. The web pieces (W7 lenses, W8 sheets, W10 since) are separate PRs and render againstnodeLenses.server.json.Review fixes
Tree.proposal_holds_work(shared withblocks). D-43 on connectors-core, which is done, no longer cuts the done bar short.ready_at12:00, so the merge ask reads1h 40m. In a lane that is not a roll-up, a person counts as the cause only when the open wait is theirs, never a queue.waits on assistant-core: the first project the lane depends on that had not finished when the queue began.+n). 5000 spans in one lane now take milliseconds instead of 2.1 s and give one bar instead of 5001 (test_a_timeline_at_the_span_cap_is_fast_and_small).bad_windowinstead of a 500 from an int64 overflow.state=laterandstate=ghostlist those nodes as the items. A group with nothing to show is left out, filtered or not.#9856,#9857.PUT …/n/{ref}/seenis in the human-origin route table (runner bearer and tunnel header get 403human_only).GET ?seen=1from a runner leaves no mark. The write-route walk now covers the node routes too.Rebased on staging after #214 (web only; the fixtures it reads were regenerated and pass). The migration is still the single head
fz8c9d0e1f2a, checked again on PostgreSQL 16 (upgrade, downgrade -1, upgrade, no model diff).🤖 Generated with Claude Code