Tiller Serve: The Web Console
Scope. This document defines tiller serve, the local console for the human in the loop: what it shows and the boundaries it holds, with explicitly labeled future actions. The CLI command surface and artifact schema remain canonical for commands and stored data. The view sections also retain shared visual and interaction invariants; retired mocks and rollout notes are historical references, not competing current contracts.
Status. Current reference for the read-only console. It renders local project state and copyable CLI commands; it does not perform project writes or host sync. UI writes, watcher-driven updates, and two-tier freshness remain unbuilt designs, explicitly labeled below. The roadmap tracks coverage; the design history records the reasoning.
What the console is
Section titled “What the console is”tiller serve is a local web server that renders the current working tree for the human in the loop. The shipped console is read-only oversight: it shows state, review context, and copyable commands for actions the human runs outside the console.
It rests on a division of labor the rest of Tiller already assumes: the editor edits text, the model reasons, the harness frames the work, and Tiller maps SDLC process over it. The console shows every node visible in this checkout, what changed while you were away, what needs you now, and the corpus to drill and search.
The unbuilt approval design would add human-judgment transitions such as approve, attest, answer, block, and cancel, each mapping one-to-one to a CLI command. Acting at the gates retains that design without implying shipped write endpoints.
What it refuses is as defining as what it does:
- It does not author. No spec, design doc, or eval is written here. Editing prose is the editor’s job. The console’s inputs are choices and short plain-text notes, never a prose surface.
- It does not do the agent’s work. Decomposition, speccing, task breakdown, the work loop, drift fixes: none of it. Those are the agent’s, through skills. The console renders their results and never offers them as buttons.
- It does not review code. It renders Tiller’s own structured artifacts and links out to GitHub for diffs and PRs. It is the review surface for the plan, not for the code.
- It does not orchestrate git or the host. The shipped console never commits, pushes, or merges. An explicit CLI-mapped state commit belongs to the unbuilt write design. The human merges outside the console and
syncobserves it.
There is one caller, the human in the loop, which is what most distinguishes this surface from the CLI’s three. The agent does not use the console; it uses the CLI and the skills. The console is the human’s window and the human’s gate.
Who it serves, and identity
Section titled “Who it serves, and identity”The console serves the human in the loop as product owner, architect, reviewer, and acceptance tester. Two engineers run two local servers, each anchored in its served working tree with bounded local task-source observations. Identity is a convenience lens for “what needs you,” not a permission boundary.
Nothing in Tiller is hidden from the team. Every server shows the whole project, every owner’s streams, every node, and the entire .tiller/ corpus. There is no team permission model. Serve defaults to 127.0.0.1; bare --host opts into 0.0.0.0, while an explicit value selects another supported IP bind. The default port remains 4400 in either mode. Network mode exposes the planning ledger, the .tiller/ Markdown corpus, Markdown documents the ledger cites, and gate evidence files the ledger records, without authentication or TLS. The file and evidence readers serve nothing else from the checkout (cli-surface.md defines their scope). Use a trusted network or private VPN only, never direct public-internet exposure. Wildcards expose all interfaces of the chosen address family, not just a trusted LAN or VPN.
Before reading project files, visit memory, version metadata, or assets, the entire console checks Host against its bind policy. Default and explicit 127.0.0.1 accept exactly localhost, 127.0.0.1, and ::1; other loopback binds also accept the bound IP. IPv4 wildcard accepts localhost, 127.0.0.1, and usable IPv4 interface addresses; IPv6 wildcard accepts localhost, ::1, and usable IPv6 interface addresses. Specific non-loopback binds accept only the bound IP, with no loopback aliases. Wildcard sets are startup snapshots of up interfaces; address changes require a restart. The CLI surface specifies literal restrictions, authority syntax, and listener families.
A present Origin must match the direct connection’s scheme, normalized Host, and effective port. Distinct admitted names or IPs are not interchangeable browser origins. Missing Origin permits ordinary CLI requests and browser navigation. When present, Sec-Fetch-Site accepts only none and same-origin. Malformed or repeated headers are refused. Forwarded headers do not affect these checks; proxy headers do not establish trust.
Non-loopback modes additionally reject a present foreign Referer before any reads. Referer must be one well-formed absolute HTTP(S) URL without credentials or a fragment; paths and queries are allowed, but scheme, normalized host, and effective port must match the request origin. Empty, malformed, or repeated values are refused. All network-mode responses, including errors, send Content-Security-Policy: frame-ancestors 'self' and X-Frame-Options: SAMEORIGIN; existing CSP directives remain intact. Loopback mode retains its existing behavior without the network Referer restriction.
Plain HTTP LAN browsers may omit Origin, Referer, and fetch metadata. Headerless requests remain allowed for direct navigation and CLI reads because their initiating site cannot be identified. Framing policy blocks foreign rendering, not the HTTP request or its reads. Browser same-origin policy limits cross-origin script reads, but these controls are not authentication and do not guarantee refusal of every foreign navigation or localhost-equivalent protection. Any process that can reach the socket can construct an admitted Host.
The console remains read-only. Future write endpoints cannot safely inherit this network read policy; they require a separate security design before network exposure.
The console is not a live team server. It reads the served working tree and bounded task execution observations from other local worktrees or locally available branch tips. It does not fetch, pull, push, merge, subscribe to a remote event feed, or watch another engineer’s checkout. Tracking refs describe the last external fetch, not the remote’s live state. “Live” in this spec means local reads on navigation or manual refresh, not real-time cross-machine presence.
The bounding principles
Section titled “The bounding principles”Five rules size every screen and every interaction. They are the test for whether a proposed feature belongs.
- Whole-team visibility. Everyone can inspect the same corpus. Default browsing omits cancelled epic subtrees, but Explore filters, search, and direct detail links retain access. Any “mine” filter is a convenience lens, never a permission boundary.
- No authoring. Inputs are choices and roomy plain-text notes: a blocker reason, a cleared-blocker note, an answer to a question. The moment a task wants real prose or markdown, the human is in the editor, not the console.
- Render structure, link out for code. The console draws Tiller’s structured artifacts richly, because Tiller owns that data, and links out to GitHub for code diffs and PRs and to the editor for files. It does not rebuild GitHub.
- Mechanical, never narrative. Every view is derived from the working tree. The console never writes prose. A narrated recap or status is reasoning, which is a skill’s job; if one exists, the console displays it as an artifact, it does not generate it.
- Every write is a CLI verb. Each action maps one-to-one to a command in the CLI surface, and to exactly the subset that is the human’s to run. The console adds no write capability the CLI does not already have.
The views
Section titled “The views”The console is a set of lenses over one corpus, the whole .tiller/ tree, varying by altitude and recency. Primary navigation is ordered Home, Explore, Build, Fix, Report, History. Fix opens the combined Bugs, Chores, and Tweaks board at /fix; its page title is Fix. Node and file detail render inside Explore’s shell.
Every view shares a topbar. Its home link pairs a compact official boat-and-wave mark with the Tiller wordmark. The root tiller-logo.svg is fixed navy for light mode; tiller-logo-inverted.svg preserves its geometry in light ink for dark mode. The console selects the visible asset from its Auto/Light/Dark setting, with explicit choices overriding the OS and Auto following live OS changes. It does not depend on media queries inside the SVG. The README and documentation site use the same asset pair. A small, muted version sits beneath the wordmark. It comes from the running binary through GET /api/version, not from the UI bundle, and remains available if project data cannot be read.
Loaded Build, Fix, and Explore omit a visible view-name H1. The active main-navigation highlight and browser title identify the current area. Node and file titles inside Explore remain visible; the shared initial loading shell still names the view while its required reads settle.
Shared rendering conventions. Keyed-node rows lead with title and a muted mono key, or a hover key where space is tight; the id stays in detail metadata and copyable commands rather than repeating on each row. Materialized questions scan as Q-id plus short title. Staged add-question operations have no Q-id and lead with short title, with their full statement as subordinate detail. This changes no CLI addressing rule.
Home, Explore Browse, Build, and Fix share the same initial loading shell: the view name as a heading and a muted “Loading…” status line, with identical page gutters and typography. The topbar stays available. Each view keeps its own data-read prerequisites; independent panels and Explore’s other modes do not wait for unrelated reads.
Person labels use full names from the project roster across owners, contributors, proposers, history actors, attestors, and baseline confirmers. An unlisted person uses a supplied name when available, otherwise the stable person ID. The shared roster refreshes on navigation and manual refresh. IDs remain unchanged in stored state, API payloads, commands, filter values, grouping, and tooltips; authored prose is not rewritten.
In rendered Markdown, standalone epic, feature, and task IDs (E-, F-, or T- followed by six lowercase base36 characters) link to their node pages. This includes formatted prose, lists, tables, and code spans containing only one ID. Commands, code blocks, existing Markdown links, and IDs embedded in longer identifiers or paths remain unchanged. Links retain the original label and resolve by immutable ID when opened; they do not assert that the target exists in this checkout. Unknown IDs use the normal node read error. This is a rendering rule, not a rewrite of stored prose, and does not infer targets from titles or keys.
Markdown reference safety. Block and inline prose share one renderer and link policy. Authored /node/... links navigate within the console; valid absolute https:// links open a new tab with rel="noopener noreferrer" and the console’s small outbound arrow, with a new-tab cue for assistive technology. HTTP, other schemes (including mailto:, javascript:, and data:), protocol-relative URLs, fragments, and arbitrary relative paths remain readable labels without anchors. Malformed HTTPS destinations and destinations containing whitespace or backslashes are inert. Relative file navigation is deliberately not introduced: neither Markdown paths nor /file/... links become clickable through prose. Raw HTML remains escaped, never interpreted. All Markdown images, including reference-style images and local or data URLs, render only their alt text, without image elements or requests; alt text is inert and does not acquire automatic work-ID links. Existing Markdown links are not revisited by work-ID linking, so anchors never nest. Rendering does not verify external destinations or promise that node IDs exist.
The shared seven-color palette is draft, specced, ready, active, in_review, done, and blocked. A raw status word or count caption always accompanies color. proposed folds to specced; in_progress and check to active; question open to in_review and answered to done; cancelled, archived, and unknown statuses to quiet draft slate. One shared fold supplies every page.
The visual contract uses Space Grotesk for identity and titles, Inter for body, and Space Mono for operational text. Auto/Light/Dark persists, with light and dark tokens and Brash Burgundy as the needs-you accent in both themes. The layout uses the full window except reading material such as History, which keeps a centered measure. Supported viewports are laptop-sized or larger; narrow stacked fallbacks are not a supported phone or tablet surface.
Home is the drop-in view: orient, see what needs you, act outside the console, leave. A header band sits above any dependency diagnostics and the action queue, with the pulse beneath. Every part is derived.
A tab rail heads the page. Overview holds the desk described below. Branches lists the task execution sources: the read notes, the visibility diagnostics, and every task observation with its provenance. Its label carries the diagnostics count, in the warning tone, when there is at least one. The choice rides ?tab=branches, absent for Overview, replaced rather than pushed, and independent of ?needs=<kind>. This tab is the only place the console lists execution sources; the shell above every page does not, and a node page shows only its own task’s observed provenance. A third tab, Recovered reviews (?tab=recovered), appears only while an epic or feature carries a recovered review record. Its records come from the epic and feature lists alone, since no ranked move carries one, and its count takes the warning tone while a record leaves historical riders pending or current acceptance unresolved.
- The action queue shows current attention entries from the shared Queue projection: plan approvals, later-added tasks, proposed amendments, due questions, human review gates and eval criteria, and parked feature checks. Rows open review context and carry copyable commands, not write buttons. A feature approval, task-batch approval, or feature check must not offer a command that would refuse under a retired ancestor. Feature checks also suppress for open structural-blocker questions,
active_basisdrift, or a manual blocked flag; the phase remains visible on the node. Proposed amendments lead because their reshape can invalidate later asks, and have no ancestor suppression. Gravity order then runs attest, approve, answer, and parked checks and merges.merge_featureandmerge_epicrequire the coarse reviewed node’s own recordedpr/review_refand nonterminal status. Their command istiller sync, their prose names the PR or review branch, and their labels areMerge · feature PRandMerge · epic PR; merging is the done crossing. Drift is a condition to inspect, not a queue card. It stays on the node with its clearing command where one exists;stale_specandorphan_evalhave no invented command. - Shelf work joins Needs you. The
approve_bug,approve_chore,approve_tweak, andcarrier_cancelledqueue kinds render with the other action rows and their copyable commands. In-review bugs, chores, and tweaks participate in the project-move count, so the all-clear guard stays silent while any of them still waits on a human. - Bursts compress into decision bands. A small kind keeps one card per entry with its command visible. A kind exceeding the console-wide row cap, currently five, folds into a band whose head names the kind, count, and any shared why-line. Every member remains a visible two-line row, title over mono address, with its varying annotations and a node link. Rows carry no command because the node’s attention stack owns the single-node review and act path. The foot offers the CLI’s full multi-node
tiller approve <noun> <node>...only where supported, captioned “approve all N after review.” Answer bands have no batch because every answer is distinct;approve_taskshas none because--featureis exclusive with positionals. Kinds keep gravity order and lay out as auto-fit columns at 360px, folding to one column below 1100px. A lone kind fills the width. The foot’s command stays one horizontally scrolling line. With more than the row cap of total decisions and more than one kind, a tab rail names every kind and count and renders one kind at a time. Selection is?needs=<kind>, not remembered across visits. At or below the cap, or with one kind, there is no rail. Compression never silently drops a queue member. - Amendment approvals and attestations never compress.
approve_amendmentis an exception to the per-kind threshold because its CLI verb accepts exactly one amendment and the amendment page intentionally offers no approval command.attest_gateis another: each entry’s command names one node and one review gate, and there is no batch attestation verb.attest_criterionis a third: each entry’s command names one node and one eval criterion, with no batch verb either. A burst of any of these kinds stays a stack of decision cards past the row cap, preserving every runnable command beside its node and gate or criterion and never inventing a batch foot.workflow_closeoutcards likewise keep their own phase commands. - The header band carries project identity and state counts; the as-of line lives in the topbar beside the theme control. The Drift tile names both candidates and uncovered comparisons and links to the full report, never reading clean over unchecked nodes. Epics and Questions have their own tiles. Features form a six-segment funnel matching Build’s columns, each labeled count linking to Build; zero segments stay dimmed in place. Tasks appear beneath as “N tasks across M features,” with any excluded feature statuses named as a retired count. One combined Fix link-tile shows the open shelf count with the noun split in its caption.
- Dependency diagnostics appear as their own section between the header band and the queue, only when present.
/api/nextsuppliesdependency_diagnosticswith dependency-onlyready,gaps, andadvisories, using the same classifier and messages as CLI readiness. Genuine prerequisite and completion loops render as execution blockers; projected epic grouping cycles render separately as nonblocking advisories. Both name recorded edges and affected features. An advisory explains interleaving and deterministic grouped-map fallback without asking the developer to remove valid prerequisites or changing the ranked moves.
Beneath the queue, Home carries the pulse: three mechanical sections reading past, present, and future left to right, each derived and none narrated.
- Just happened is a fixed-window slice of the event stream, rendered as the same collapsed-burst rows History draws, capped and linking to History for the rest. A fixed window never touches the visit horizon (History’s own rule), so Home states recent motion without spending a digest the human has not opened. The count of events behind the last-visit horizon renders as one quiet line in the queue’s section head, served by the non-marking count read, so the signal and the view still answer against the same instant and neither clears the other.
- In flight groups unfinished task/bug/chore/tweak/tweak assignments by owner, including ready reservations, proposed, blocked, checked, and in-review work, rather than duplicating whole features under historical contributors. Features remain contextual groups. Carried shelves wait on their carriers. The unclaimed rollup is one count of eligible unclaimed ready task candidates, not a per-feature status/held pool; Up next ranks those tasks among the reader’s other moves rather than listing them all. Blocked/held conditions remain distinct from lifecycle.
- Up next is the ranked next list for the served checkout’s contributor, read from
GET /api/next: the same rowstiller nextprints, at most five, in served order. The console never re-sorts, filters, or recomputes it. The reader is the identity the CLI uses,TILLER_CONTRIBUTORand then.tiller/identity.yml, and the header names them by roster display name,Up next · for <name>, falling back to the id. With no identity the header readsUp next, the list falls back to plan order, and a muted line says “Not personalized; runtiller join.” There is no “view as” picker. Each row leads with its actor,Youfor a human move orAgentfor an agent move, then the action, a link to the node it names (a question by its title, which its reason then does not repeat), and the reason as an inline-Markdown sentence, followed by the branch for a move that continues on another branch. Commands stay off the row; Needs you and the CLI carry them. With nothing legal the column says “Caught up: nothing is legal to do right now.” The list’s hint, when it carries one, follows the rows. Up next and Needs you may overlap: Needs you is the complete inventory of decisions grouped by kind, and Up next is the short ranked answer mixing human and agent moves. Home offers no “show all”;tiller next --alllists the rest. A ranked move is a suggestion, not authorization.
All-clear is honest or silent. Next-owner derives from the CLI’s legal-next-verb table, not a console rule or badge. The empty-queue summary counts agent and human moves only after every definition-axis list has loaded, shows a named error if any failed, and still names in-review work awaiting human PR review or merge even where its legal Tiller moves belong to the agent. Empty Queue alone never means nothing needs the human.
Future rail design. Home has no standing rail tenant and reserves no empty column. If a tenant is added, its sections share independently persisted collapse state, an eyebrow, chevron, and optional header extra. Only the body collapses; the header remains reachable.
Build is the state of the work as a board: six fixed columns (Draft, Specced, Ready, Active, In review, Done), features as the cards placed by stored status (a ready feature with a task in progress, in check, or in review, as the console observes it from task branches, sits in Active while its stored status stays ready; features have no stored review state, so a working feature whose non-cancelled tasks are all in review or done, with a task still in review or its own feature PR or review ref recorded, sits in In review, again display-only; a feature whose tasks are all done but whose check or rollup has not run stays in Active; a cancelled feature has no column), and an epic list standing at the left deciding what the board shows. It answers “where is everything” spatially, which is a different question from Home’s “what needs me,” and the division stays exactly there: a card with a queue entry carries the loud left edge and its ask as a small flag, but the queue itself lives on Home.
Build and Fix share a top-right Scroll: Board / Columns control above their boards. Its joined segments match the color-mode selector: the same type size, recessed selected fill, and heading-colored text. Board is the default: the board scrolls vertically and all columns move together. Columns gives each column its own vertical scroll. The board remains the horizontal pan container in either mode. One browser-local scroll preference applies to both pages and survives navigation and reload; invalid or unavailable storage defaults to Board without disabling the control. Every column background fills the available board height, including empty columns. In Board mode, all backgrounds also extend to the tallest column’s content height. Column headings stay in place while cards scroll, and no scrolled card shows above a heading.
The toolbar and sidebar stay outside the board scrollport. Column headings sit in their own row above the scrolling cards rather than sticking inside the scroller, in both modes; the heading row pans horizontally with the cards but never scrolls vertically, so no card can show above a heading. Sticky headings were retired because Safari composited them a few pixels off their layout position during scrolling. Build’s gutter above the columns is outside the scrollport. Below 48rem, each sidebar stacks above its board and scrolls within a 30vh cap; toolbar controls wrap, while both board scroll modes remain available.
- Epics are the lens, not rows on the board. The left panel lists every non-cancelled epic with name, feature count, and child-state mix bar. Cancelled epics and their descendants are excluded even if an old selection URL names them. Multiple epics can be selected, with active epics selected on first visit. Selection rides the URL; panel collapse is persisted client convenience. The board bar reads “Showing N features across M epics,” with singular nouns where appropriate, instead of listing selected epic names. M counts matched epics, ignoring unknown selection keys. Panel badge, showing line, and mix caption count only the six column statuses. The caption separately names excluded feature statuses, such as “+N cancelled,” so visible counts agree with board cards.
- Selection is remembered per project. A bare
/buildarrival restores the saved epic selection, including primary navigation to Build while the page is already mounted. An explicit?epics=selection, including the empty form, takes precedence and becomes the saved selection. Storage is browser-local and keyed by project key; absent or unavailable storage leaves the active-epic default. - The card is the feature. Each card carries its epic as a quiet kicker, the feature title, its Contributors, the task rollup as done-count over total, and an expand control that opens the task list in place. Every expanded task row renders the existing status
Chipwith the raw status word, so status is labeled rather than conveyed by color alone. The card body links to the feature’s node page in Explore, and an expanded task row links to the task’s page, so the board is an entry to the corpus, never a second corpus. A feature with open attached shelf work also carries a quiet linked count. The count includes bugs, chores, and tweaks attached directly to the feature or to one of its child tasks, excludesdoneandcancelledshelf work, and links to/fix?attached=<feature-id>&terminal=all. - Blocked is a condition, never a column. The manual blocked flag, a held ancestor, and a drift finding each render as a flag on the card wherever the card’s stored state puts it, because all three are orthogonal to the lifecycle and a flagged active feature is still active. Cards within a column follow the served sequence: epics in the shared epic order, then each epic’s own features in the shared feature order. The done column runs uncapped, bounded in practice by epic selection.
- The board is read-only. No drag. Most column crossings are agent verbs, while human crossings require gate review, so cards are for reading and drilling, never moving. Future console writes belong to in-context gate reviews, not drag.
Ownership, contribution, and shared review
Section titled “Ownership, contribution, and shared review”Ancestor API fields are contributors: [{id,name}], sorted/deduplicated by owner ID, with empty [] and ID fallback for retired roster entries. The display includes owners of started descendant tasks, including done/cancelled tasks and transferred originals, even under cancelled features. Reservations, reviewers/assembly actors, creators, and attached bug/chore/tweak/tweak owners do not count. Scalar owner remains only execution/question attribution; document ownership and event actors are unchanged.
Explore separates Owner from Contributor filters. contributor=<id> selects ancestor membership, not current assignment; Owner never matches ownerless ancestors. Relationship query filters run after traversal so those ancestors remain bridges. Build stays feature-centered rather than becoming a task board: a card displays Contributors and the task rollup, and its tasks appear only in the card’s expand. A past contributor with no unfinished assignment receives no invented personal work.
Shared aggregate assembly, checks, rubric work, acceptance, and submission appear once per feature/epic, as do task-grain feature checks after all tasks finish. The ranked next list, queue, node advice, and recap use the shared workflow-input classification; ancestor gate arrivals are team asks for identified readers too. Keep human-versus-agent Actor/next_owner distinct from developer ownership. No last-task owner or persistent coordinator is inferred.
Task and shelf details/relationships display reciprocal supersedes/superseded_by transfer links. The original keeps owner, branch, review, and evidence; a transferred shelf has resolution transferred, not duplicate. A ready replacement is unstarted, with fresh identity/basis and no inherited checks/review. Copyable transfer guidance uses tiller transfer task|bug|chore|tweak <ref> --to <contributor-id> --reason <text> only under explicit human authority; the console does not mutate state, Git refs, or host PRs.
Fix is the dedicated read-only shelf-work lens at /fix. Like Build, its frame has no outer page gutter or visible page heading: the filter sidebar and board toolbar start directly beneath the main header, with spacing owned by the sidebar and board. Its board has six columns in task-vocabulary order: Proposed, Ready, In progress, Check, In review, Closed. No card is draggable and the board offers no write interaction.
Fix uses the same scrolling control, full-height backgrounds, and sidebar collapse button as Build. Its filter-sidebar collapse state is remembered separately from Build’s epic panel, defaults to open, and never changes URL filters. Collapsed sidebar controls are hidden from assistive technology and keyboard navigation on both pages. The toolbar reads “Showing N bugs, M chores, and P tweaks,” using singular nouns where appropriate and retaining zero counts. Counts reflect only rendered cards after noun and attachment filters and the Closed history window.
- Open work keeps shelf order. Nonterminal rows preserve the bug shelf sequence followed by the chore and tweak shelf sequences, then render in the column for their raw status.
- Closed is one terminal history. It combines
doneandcancelledwithout replacing either raw status, orders rows by newest terminal event first, and shows the five newest terminal transitions by default. The all-history URL state isterminal=all, toggled by a Show older items switch at the foot of the filter rail that appears only when Closed has more than five rows. - Filters are addresses. Noun and attachment filters ride the URL. Attached to is a searchable multi-select over the nodes that carry attached work, each result labeled Epic, Feature, or Task with its key or ref; picks show as removable chips and the result list scrolls inside a fixed-height control.
?attached=is a comma list (?attached=F-abc,T-def) read as OR, and a single id still parses. Unattached is a separate fixed toggle encoded as thenonemember, so?attached=none,F-abcmatches unattached work or work under that feature. A feature filter matches work attached directly to that feature and to its child tasks; an epic filter matches direct attachments and work attached anywhere under its descendant features and tasks. - Filters are remembered per project. Opening Fix with no noun, attachment, or older-items filter in the URL restores the filters last used in that project, the way Build restores its epic selection. This includes reopening Fix from primary navigation while it is already showing. A URL that names any filter wins and becomes the remembered state, and an explicit return to All through the filter controls is remembered as All. Storage is browser-local and keyed by project key; unavailable storage or an unread project status leaves the default All board.
History
Section titled “History”History answers “where are we, and how did we get here” after you have stepped away.
It mechanically derives a digest of local Tiller events since your last visit, with last-day and last-week overrides, across the whole team in this checkout. Grouping is a runtime URL choice: ?group=epic or the absent parameter uses served epic/stream sections; ?group=time merges rows into a newest-first day-grouped timeline. Both compose with the window parameter. Bursts are collapsed within each served group before time mode merges the resulting rows, so two epics’ same-type, same-actor events never become one invented burst. Each merged row carries its epic label. The server serves one grouped event shape; grouping and collapsing are rendering decisions.
A group is one newest-first timeline, and a run of one kind of thing is one row. A digest is rarely a list of distinct events. One decomposition run is four bursts: ten epics added, sixty-four features added, thirty-two questions asked, sixty-one features linked, one contributor, two minutes. Rendered one row per event that prints the event type, the timestamp and the actor on every line, which are the same three values down the whole burst, so the surface spends hundreds of rows carrying a handful of facts. So consecutive entries of the same type by the same actor collapse into one row that counts them, names what happened once, and expands in place to the entries beneath it. The count is the chart. A collapsed row states one time only when its first and last entry are the same time at the precision it prints; otherwise it states the span, so a row can never claim a week happened at an instant. Collapsing is a rendering decision, not a payload one: the API stays an honest event list.
History states what changed; it never restates what is open. Listing your open asks a second time was one actionable set in two grammars, and this view’s copy was the worse one, because it carried no way to act. What History says about asks is a past-tense count of events, in the one loud ink, on the row it happened in and in the page head, with a link to the surface that owns the live number. The link is an address, not a guarantee that the queue holds every counted event: an ask is an event that arrived at a human gate, a queue row is a move a human can make now, and the two agree on the ordinary cases and part on the ones a project’s own manifest decides. Where they part, Home’s all-clear enumeration still names the node, which is why the link points at Home rather than at a filtered list. An ask is also an ARRIVAL and never a restatement: an event that records evidence without moving the node, an attestation on a gate it is already sitting at, asks nobody, and counting it would make the page’s loudest number a tally of the reader’s own last few keystrokes.
Everything on this view is past tense for the same reason the link is an address: two surfaces making present-tense claims about the same set will contradict each other the moment one of them is acted on.
Every row is an address, and nothing deeper lives here. A row names the node its event touched and links to it, whatever its noun: epics, features, tasks, questions, amendments, bugs, chores, and tweaks all resolve to their noun, id, key, and title. Only an id the tree no longer holds renders as the bare id without a link, and that node page already renders the node’s own history, its files, its commits, and its PR. So the digest hands over the address instead of growing a second, shallower copy of the node surface, which is this document’s “render structure, link out” rule turned on the console’s own views: the recap says what changed, and the page it points at says everything about the thing that changed.
The window is an address too, and the digest is bounded. Each window, your last visit and the last day and the last week, is its own address, so a window a human can switch to is a window they can link to, and a wider one is one click from a digest that came up empty. Bounded means a group carries at most a fixed number of routine entries and states how many it dropped, while an entry that asked something of you is never among the dropped, because losing the reader’s own ask to make room for a routine transition is the one truncation a digest cannot afford. What a cap drops is not recoverable through this view, deliberately: a wider window and the node’s own history hold the full record, and a digest that refuses to truncate is a log.
History owns the recency lens, digest horizon, and full record. Home’s fixed-window Just happened section is only a bounded taste of it, never a second digest.
The horizon is the reader’s, and only this view moves it. Opening History on its default window advances the last-visit mark, and nothing else on the console does, so arriving at Home, drilling a node, or running a search never consumes a digest you have not read. One visit is a burst rather than a single request, so stepping into a node and coming back shows the same digest instead of an emptied one. The window override never touches the mark at all, which makes a link to a fixed window a read with no cost, and is what lets Home render its slice.
The last-visit memory is .tiller/.cache/console.json. Its read and atomic write use the same checkout-anchored, symlink-refusing state operations as the store. An unsafe, absent, or corrupt cache supplies no remembered horizon; a refused write does not prevent the recap from serving and never follows the unsafe cache path.
Only the console’s own History visit moves the mark. Any page or peer that reaches the port can send a plain GET, and neither its headers nor its peer address reliably tell the console apart from a hostile page in the developer’s own browser, so the History view marks its default-window request with an X-Tiller-Visit: 1 header and the server advances the mark only for a request that carries it. A cross-site page cannot attach a custom header without a CORS preflight, and the server answers no preflight and never grants CORS access. Every unmarked request, a CLI curl included, gets the same digest against the current horizon and writes nothing, the way Home’s count read does. The rule is the same on every bind. On a network bind, opening History from any device that reaches the console advances the mark, since the mark belongs to whoever reads the digest through the console.
Explore
Section titled “Explore”Explore is for the “I need to understand the broader context, drill into anything” need. It is the altitude lens: a persistent left navigator beside a reading pane, and it is also the shell for node and file detail. Node-owned roads land on node pages with the navigator opened to that node. The read-only file page serves both unowned Markdown search hits and local source citations from node provenance without inventing another node.
Explore fills the viewport below the topbar, using calc(100vh - var(--topbar-h)). The page itself does not scroll; navigator and reading pane scroll independently. The mode rail starts beneath the topbar without a view-name heading or reserved title gutter, gives its tabs an 8px top inset, and stays above the split. The rail shows Browse, Filter, and Map, then a vertical separator followed by Questions and Amendments links. Search opens from the Browse search box rather than a dedicated tab; direct ?mode=search URLs remain supported. The shared search input and tree controls sit outside the navigator’s scroll area, without separate ledger rows taking tree space. A node’s whole reading pane, header included, scrolls together. Filter and search use the same frame with their own scroll area. Node picks, rendered links, and pasted keys all use /node/<noun>/<id-or-key>; the old /explore/<noun>/<id> form redirects there. The retired /recap and /bugs-chores addresses redirect to /history and /fix, keeping their query strings. A feature with no tasks has no empty chevron.
Browse-to-Browse navigation replaces only the reading pane. The navigator retains its last successful tree, shelf rows, and question counts while background reads run, with selection following the URL immediately. Expanded branches, search input, divider width, focused links, and navigator scroll remain in place when the tree structure is unchanged. A different node starts its reading pane at the top; source citations keep their line-scrolling behavior without moving the navigator. Real links retain Back/Forward, keyboard activation, copying, and opening another tab. Structural changes or explicit collapse may legitimately move rows or clamp the scrollbar.
Across Home, Explore, History, amendment summaries, and every other row or card, a question is a scan reference: id plus short title. The full statement is not used as a row handle. It remains available when the question is the subject, on question detail and amendment operation detail, and may appear as a search snippet when the statement itself matched.
Cancelled epics and their feature/task subtrees do not appear in Browse’s navigator or Map’s containment tree. Filter and search retain the complete corpus, including cancelled work; status=cancelled isolates cancelled epics in Filter. Direct node links still open their detail without restoring the subtree to the navigator. Recorded relationships remain available for explicit inspection. This is console presentation, not removal of history: tiller tree still includes cancelled work. Cancelled features, tasks, and shelf rows beneath live branches do appear, until the reader presses the navigator’s Hide cancelled toggle beside Open all and Close all. The toggle drops them from every tier and shelf, and their counts with them, leaves the epic bars counting the full child mix, and is remembered per browser across reloads.
- Browse the work tree as a real tree, epics to features to tasks with chevrons, indent guides, and a shape-coded status glyph on every row, with type entry points alongside for amendments, decisions, and design docs. Each of the seven chart states draws its own shape so the state reads without color: a dashed ring for draft, a ring for specced, a ring with a center for ready, half filled for active, three-quarters filled for in review, a solid check for done, and a solid bar for blocked. Other statuses take their chart sibling’s shape, and the glyph’s status label (Draft, Specified, Ready, In Review, and so on) is its accessible name. Hovering a glyph with a mouse shows that label in a tooltip after a short delay, above the glyph (below it near the top of the window). Finished rows (done, answered, cancelled, archived) fade so unfinished work stands out, and blocked rows carry a faint wash and a bolder title. Sibling lists follow the CLI’s shared dependency order, refined by recorded soft order where dependencies leave the sequence open.
- Bugs, Chores, and Tweaks are independent shelf branches. Each is a top-level navigator branch below the epics with its own expansion state, and a direct shelf-node address opens the matching branch. Bugs, Chores, and Tweaks are all valid Filter nouns. Search attributes a matching shelf title or shelf prose passage to the owning bug, chore, or tweak node.
- The questions ledger link stays in the mode rail, carrying only an accent badge when questions are open, with no total count. The badge remains available across Browse, Filter, search results, and Map; unknown or zero open counts omit the badge. The link remains usable while counts load or fail. It opens the questions page, with open questions first showing id plus title, due date, what they block, and the copyable answer, cancel, and per-target
unlink --blockscommands, answered questions staying listed with their answers inline, and cancelled questions staying listed with their cancel reason. A status rail above the list offers All and each question status with its count, a fixed Cancelled tab included even at zero; the choice rides?status=, and an unknown value reads as All. The complete statement is one click away on the question page rather than repeated across this scanning surface. - Amendments have a permanent mode-rail link, including when there are none or other navigator reads fail. Its destination is the amendments ledger at
/amendments, which renders inside the Explore layout like the questions ledger, with the navigator standing beside it; inherited status, owner, epic, feature, and search filters do not travel with it. The same status rail narrows it to proposed, approved, or withdrawn through?status=. Amendments also remains a Filter noun. Proposed records precede terminal records, followed by canonical key and immutable ID order. Proposed, approved, and withdrawn rows remain readable, and every detail link uses its A-ID even when terminal records share a reused key. Browser Back restores the list address. - Filter and search share one persistent bar over the navigator. Search matches node fields and project-authored regular, UTF-8, NUL-free lowercase-
.mdfiles beneath the canonical.tiller/root. Corpus validation also reads generatedorientation.md; it is excluded from results but a symlink, non-regular file, invalid UTF-8, NUL byte, or canonical path escape still refuses the whole corpus rather than returning an undestinationable partial result. The retired root.tiller/board.mdcoordinate is excluded before opening or validating it, including any directory subtree; direct prose reads return ordinary not found. Authored paths such as.tiller/design/board.mdremain supported. Search answers with the grouped, ranked resultstiller searchdefines: one row per node and one per unowned document. A node the query names by id, reference, or whole key stands first in an Exact match section with its kind pill. The rest group into one section per kind (Epics, Features, Tasks, Bugs, Chores, Tweaks, Questions, Amendments, Documents), each in rank order and capped at five rows with a link to its kind’s tab. A tab rail carries All and each matched kind with its count; the choice rides?kind=and asks the API for that kind alone, so a kind is never cut short by the others’ share of the page, and an unknown value reads as All. A node row leads with its title, highlighted where it matched, then its key (a question’s Q-id, since it has none), its status, and the lookup that found it when it was an id, key, or key fragment; beneath sit its first three matching lines, each named by file and line or by ledger field (statement,answer), and how many more it holds. A document row links its path to/file/<each-segment-encoded>#L<first-line>and each line to its own#L<line>. The summary states the result total and says when the page holds fewer. Search stays keyword-only, with no query language or semantic search. - Filter fields are faceted pickers. Each noun shows only the axes it answers: epics status and contributor; features status, contributor, and epic; tasks status, owner, epic, and feature; questions, bugs, chores, and tweaks status and owner; amendments status. Options are counted against every other active filter. Zero-count options are hidden except the selected value, which stays visible at 0 with a clear button. Each axis carries one value, applied immediately on pick; typing only narrows options, and Escape or blur restores the selection without applying text. Keyboard arrows highlight options and Enter picks.
Status options use stored statuses in lifecycle order, with unfinished first for tasks, bugs, chores, and tweaks. People display roster names and carry IDs, with the acting contributor pinned first as “You”; unlisted people display their IDs. Parent options display title and key, carry immutable IDs, and follow dependency order with cancelled parents last and dimmed. Old key-based parent URLs still select the resolved parent. Picking an epic clears a selected feature outside it; picking a feature does not fill the epic.
Switching noun keeps axes the new noun answers and drops the rest. Hand-typed inapplicable axes are removed with a history replace. A carried status not legal on the new noun remains selected at 0 rather than disappearing. Unresolved parent URLs show an inline no epic "<value>" or no feature "<value>" note and an empty result list; clearing the field recovers the list. Clear all removes every filter axis. The server owns the axis table and option ordering; the bar retains its data while picks refetch.
- Map draws a balanced horizontal mind map with the project fixed at the center, epics branching to either side in dependency order, features beside epics, tasks beside features, and Bugs, Chores, and Tweaks as three more branches. Quiet connectors show containment at rest; operative question holds remain badges, not conclusions drawn from generic
blocksedges. Selected dependency, attachment, and carrier arcs retain their existing chain highlighting and routing. Branch affinity, sibling order, collapse state, and pan/zoom remain properties of the containment display tree, never of the full semantic graph. Same-column arcs use an outer bracket, same-side cross-column arcs join facing edges, and opposite-side arcs run through the middle. Clicking the selection again, empty canvas, or Escape deselects. A floating detail card links to the selected work node. Containers expand and collapse at every level; the initial map has epics and shelves closed with counts. Expand all opens epics and shelves to features. First visit centers and fits; expand/collapse fits without moving the root through toggles. The address is?mode=map, withsel=for selection; open set and viewport persist per project in the browser. - Relationship inspection belongs in the selected item’s right-hand inspector, not in the corner key. Dependencies separate Depends on from Required by and show counterpart status; containment separates Parent from Children. Other groups name work documents, sources, derivation, authored and generated document mentions, shelf links, questions, amendments, replacements, declared code scope, and preservation context. Inspect on map selects the counterpart. Open follows only a server-granted node or confined-file destination. Why linked? mounts provenance on demand, retaining source file, field, line, criterion, operation, role, scope, and revision evidence. Relationship counts count logical assertions, not wires or source occurrences.
The compact Map key explains line styles and direction, including Prerequisite → dependent and undirected attachments. Its entries select the same kind filter as the toolbar. Document coordinates remain metadata in the inspector and stay available through explicit at_path queries. The All dependencies preset retains dependency, attachment, and carrier overlays. Full-graph display is a deliberate action, not the default way to browse project relationships. Overlay choices remain local to the mounted map.
Project relationship exploration provides searchable pages of at most 25 relationships with explicit scope, direction, kind, total relationship count, and continuation controls. Supported one-hop queries include prerequisites, recorded dependents, work using a source, declared code scope, amendments concerning work, and documents mentioning work. Every explicit query submission performs a fresh read, including unchanged arguments after a failure. Show results on map maps the current page, clears the previous map kind filter, and leaves full-graph scope off; inspecting or paging results does not silently enable a full-graph overlay. Query-derived endpoints remain inspectable while an earlier result page is mapped, and extraction-loss warnings from both contexts remain until the map refreshes. The full catalog remains available, including internal document coordinates. Work-to-work edges use existing cards; other endpoints occupy a contextual region only when their overlay is requested. Containment is never drawn twice, and semantic edges never rearrange the containment tree or change dependency meaning.
Map kind controls, including Work links and the Map key, filter the mapped page without replacing it. Clearing the kind restores that page’s full membership. Full graph and All dependencies explicitly replace the mapped page with project-wide scope. A successful map refresh clears mapped and inspected query snapshots and names that reset; a failed refresh retains them with the last successful graph. This avoids mixing old query facts into a newly read working tree. Clearing the selected item resets selected-item browsing to Whole project, without restoring the old scope when another item is selected.
Reference issues separate the selected item and its owned documents from project-wide issues. Query issues name their narrower scope: reached query subjects plus scan-wide warnings. Cause groups retain each source occurrence, affected subject, original value, and available safe actions, with continuation after 20 occurrences. Unsupported citation syntax is not a missing file; retained historical ambiguity is not an operative blocker. Source links use granted destinations, unavailable citation lines and missing documents stay copyable rather than linked, and suggested commands inspect a subject at depth zero without mutation. No generic repair action or inferred severity is provided.
Extraction-loss warnings remain visible across the scan, using each diagnostic’s server-produced incomplete flag rather than a browser-maintained code list. Missing input elsewhere can conceal incoming relationships. The aggregate complete flag describes extraction, not reference validity or project health. Scrollable relationship content keeps its panel header reachable, with native keyboard-accessible controls, disclosures, and links.
Collapsing an individual branch preserves the selected node and its relationship scope. Relationship filtering, dependency traversal, and arc highlighting use actual node identities; only the resulting endpoints project onto visible ancestor cards. Hidden siblings never contribute their relationships merely because they share a card. Collapse all ends selection, closes the inspector, and fits the collapsed overview using the full frame. Explicit global scope and kind choices remain unchanged. Expand all preserves selection. Both toolbar actions fit the resulting tree together with any active related-endpoint region, reserving inspector space only while a selection remains.
- Endpoint capabilities are explicit. Work, question, and amendment destinations use their immutable node routes; documents and citations use the confined file page and source line. Code and external paths are copyable coordinates, not Markdown links or a general-purpose file server. Unresolved references remain labeled text without fabricated status, owner, title, or live destinations. Coverage states its recorded basis and never proves code correctness or existence. A current-file link does not display pinned historical contents.
Unbuilt map scope. Status and owner filters would ghost nonmatching cards, while type filters would remove them. Client-side image/export and a standalone view remain future work. The current absence of those filters does not discard these designs; relationship-kind selection is separate.
File detail
Section titled “File detail”The file page is the confined reader for both an unowned Markdown search hit and a node’s local provenance citation. Its canonical address is /file/<each-segment-encoded>#L<n>, it renders inside Explore, and ordinary browser Back returns to the exact prior node or search address. The /file/... URL retains the human-authored path spelling. It calls GET /api/prose?path=<repo-relative-lowercase-.md>&line=<positive-1-based-line>, whose success payload is {path, content, line, line_text} and whose path resolution and safety contract are defined by the shared source-citation reader in cli-surface.md.
The page shows the visible file address, an exact source-line locator with id L<n>, and the full file through the same safe Markdown renderer node prose uses. A #L<n>-<m> citation opens at its first line while its stored citation spelling remains unchanged. Raw HTML stays escaped and arbitrary relative links are not enabled. A missing or invalid line fragment does not fetch, and a stale or refused source remains a read error rather than an editor or fallback page. Search still produces only canonically contained .tiller/ Markdown results; direct node provenance may use every local path the shared reader accepts.
Registered as-is docs add optional baseline and drift_detail fields to that same response. baseline is the existing BaselineDocView directly: design-relative path, registered describes, optional derived_from revision refs, and optional confirmed with by, project commit (including observed_at) and fingerprinted surfaces. drift_detail sits beside it and uses NodeDriftView’s candidate and uncovered arrays; the shared drift.ForDesign comparison supplies the facts. Both the design/ citation alias and canonical .tiller/design/ address resolve to the same registration. Registered identity comes from the requested address, never from where a symlink leads: a registered document that has become a symlink is refused as unsafe with readiness’s missing_design_doc wording, and an unregistered path that is a symlink is refused as an unsafe read, never shown with another document’s prose. Unregistered prose has neither field. Safe prose resolution, existing errors and GET/HEAD-only behavior remain unchanged. If drift configuration or comparison fails after a registered file is safely read, prose and confirmation remain visible with an uncovered comparison_error, not a clean assertion.
The registered file page shows confirmation or Unconfirmed, the historical confirming owner, project commit and observation time, described paths, and recorded source-blob provenance, including retired source paths. A confirmation means human review against observed working-tree code; it is neither a cleanliness claim nor proof those bytes are recoverable from the project commit. Each candidate marker gets a neutral disclosure chip. Opening it renders the same detail, affected paths, history anchors, feature/spec references, context and copyable clearing command as the full drift report. Uncovered reasons stay visible even with the chips closed. An unconfirmed doc has explicit human-review guidance and a shell-quoted tiller approve design <doc> command. This is copyable guidance only: no approval endpoint, no design-node route and no console write.
Drift report
Section titled “Drift report”/drift includes stale_design and opt-in shifted_design alongside the existing work-node markers. Design candidates and uncovered rows lead with design/<doc> and link to /file/<encoded .tiller/design/doc>#L1; literal filename segments are encoded independently, including percent and fragment characters. Their empty IDs never become node links or invented references. Candidates show the confirmation since anchor and optional upper until anchor, matching features at their recorded crossing commits, and links to their repo-relative feature specs. The clearing command follows human reconciliation and review, never substitutes for it. Shifted comparison context must retain the dirty-worktree and external-code history limitations.
Coverage keeps its actual grain: compared counts distinct work nodes and compared_design counts distinct design docs, with baseline coverage in doc(s) rather than node units. The footer names both totals; candidates or uncovered rows suppress “nothing moved.” Older node-only reports without the additive fields retain their prior rendering. Disabled checks remain explicit and no unconfirmed or failed comparison is presented as clean.
Node detail
Section titled “Node detail”Node prose uses the same 8 MiB per-file limit as the direct prose route and refuses oversized content with HTTP 413 before serialization. HTTP relationship enrichment bounds corpus and cited-document reads before parsing. An oversized supplemental document produces the existing incomplete-corpus or unavailable-document diagnostic rather than silently claiming complete relationships. CLI corpus reads retain their separate policy.
Every node-owned drill, queue item, History entry, and search result lands on the same node page, so it is one consistent surface rather than a layout per type. The page renders inside Explore’s shell: the browse navigator stands at its left, expanded to the node’s own position in the tree, whichever node surface the reader arrived from.
- A uniform shell: ancestor breadcrumbs with their state chips and separators only between crumbs; a kind line with solid noun pill, hero-size state chip, gate ticks, Drifted, Held, and review-grain chips; a title alone; then the canonical reference and copy affordance. Questions use their short title and Q-id; tasks use the full three-segment key path with id. Epic and feature headers add the children’s state bar and count caption. One label-over-value facts block sits under one divider: Contributors for epics/features, Owner for execution/question nodes, proposed by only when different, Found in and Carried by with target chips, branch, PR, change, touches, accepted or verified basis, and done basis, each only when recorded. Ancestors have no owner or claim field; their Contributors are derived from started descendant tasks. Basis refs show short shas with full tokens on hover; recorded basis commits, branches, and PRs link when the origin is GitHub.
accepted_against.commit,verified_against.commit, serve-filledcommit_urlandbranch_url,done_basis, andproposed_bysupply these facts. Blob pins are not host commit links; a local “spec as approved” blob reader remains unbuilt. A missing task parent gets a dangling-parent caption, never a breadcrumb gap. Feature rollup phases append to stored status as text, such asactive, awaiting check, from the served phase field on node and list reads; they never replace state or add a color. Held names the retired ancestor in blocked colors. Drifted uses blocked colors only for refusing conditions,active_basisor an unreviewed answered assumption at a submittable task, and is neutral otherwise. Epic and feature review-grain chips appear only for non-task grain, neutral rather than asking;review_grain_sourcenames feature, epic, project, or default. Task pages show that review rides the grain node’s PR, not a separate grain chip. Header, attention stack, and drift panel render once above all tabs. - A single source surface: a node with written prose shows its source exactly once in the Spec head. When prose is stub or missing, the same source appears exactly once on the existing compact state line, without restoring section chrome. Questions have no Spec section and keep their source among header facts. The link keeps the recorded citation spelling and opens the local file page at the cited line, or opens an HTTP(S) URL externally. A source reported missing, unreadable, or unresolved by the relationship diagnostics stays copyable rather than advertising an unavailable file destination; the recorded citation is unchanged. When corpus validation is incomplete, local source citations also remain copyable until a successful read can establish their availability; external HTTP(S) sources stay navigable.
- A drift disclosure: one candidate-grain row per finding, with detail, files, since revision, and a copyable clearing command where one exists;
active_basiskeeps its drifted-versus-missing distinction. The same sweep’s uncovered rows stay visible, with a link to/drift. Refusing findings start expanded; neutral findings open on click using transient component state, not a URL parameter. Uncovered-only nodes have no Drifted chip because failure to compare does not assert drift. - An attention stack: node-specific workflow advice names agent checks, rubric work, human acceptance, formal review, sync, or terminal diagnostics alongside the unchanged lifecycle moves. The node’s own Queue entries remain visible except optional
attest_gateandapprove_amendment, whose commands stay Home-only. Each node arrival rereads the queue: the previous node’s entries clear while that read settles, a late response for the previous node is discarded, and a failed read leaves the stack without queue entries rather than erroring the page. A matching workflow and queue command renders once. Workflow advice takes precedence over the generic lifecycle fallback; without workflow or queue entries, the existing fallback remains. The fallback never offerstiller approve feature, whose queue row is hidden exactly when it would refuse, and never offerstiller approve taskunder a retired ancestor or while an open question holds the task; it names that question’s title, id, andtiller answer questioncommand instead. Human decisions use the loud accent; agent work and publication cautions use separate warning boxes. Mixed machine work and pending human acceptance remain visible together. Historical policy violations do not disappear after acceptance or advertise restarting done work. - A type-aware body: epic, feature, and task bodies use the shared underline tab rail, text on a hairline with active underline and muted counts, not data-chip pills. Feature tabs are spec, evals, tasks, dependencies, history; epic tabs are spec, evals, features, dependencies, history; task tabs are spec, gates, dependencies, history. The evals display label is Contract; its wire key and
tabparameter stayevals. Spec is the default, and tab clicks replace rather than push URL state; unknown tab values fall back to spec. Countable tabs show counts and hide at zero, with relationship-only dependencies retained by the rule below. Evals hides without a contract; spec always remains. Questions stay flat with the full statement and answer. Amendments stay flat with narrative, staged operations in order, linked resolvable targets, cancel reasons and supersession, and preconditions labeledvalid,stale, orunknownbeside actual values or explanatory notes, never conveyed by color. Staged questions lead with short title and carry full statement as detail.triggered_byandamends_designare quiet captions; the amendment page offers no approval command. Design-doc file pages are flat too. Gates and evals render recorded statuses, per-criterion results includingneeds_attestation, attestations, policy violations, checked revisions, and PR or offline branch/head provenance. The evals gate state line and its header chip take readiness from the recorded evals gate status, the factsubmitjudges: a recordedpassreads passing (“passed at last check” when a required criterion was added since), and any other recorded status reads not ready, a storedstaleover an all-optional contract included. The per-criterion projection supplies only the label detail and the blocking criterion, reading required criteria alone as the Go acceptance contract does: optional results keep their own rows but never name the blocker. Only when no gate status was ever recorded does that projection stand in for readiness, and then a contract with no required criteria reads nonblocking. The console neither computes nor grades these. Prose renders whole without read-more folds, at a reading measure inside the pane. - A shelf-aware body: epic, feature, and task pages add a countable Fix tab for shelf work attached directly or rolled up from descendants. The tab follows the zero-hidden rule and does not render when its count is zero. Bug, chore, and tweak pages use the task-shaped Spec, Gates, Dependencies, and History tabs. Their header metadata links
Found into the attached epic, feature, or task andCarried byto the resolving carrier. - An amendment-aware body: epic, feature, and task pages add an Amendments tab after Fix, before Dependencies.
?tab=amendmentsfollows the existing tab URL and fallback rules. Its count is unique amendment identities, not operation or descendant counts, and known zero hides the tab. Incomplete relationship history keeps the tab reachable without any count and shows the narrow history warning; it never claims zero. Each row links its title to the immutable A-ID, shows raw state and only event-recorded dates, and summarizes operation, endpoint role, and direct, descendant, or historical addition-parent scope. Relationship operation numbers are already 1-based; the existing forward detail operation index remains 0-based on the wire and displays plus one. The tab is not another review instrument. - Proposed amendment context stays above the tab body, on Spec and Contract as well as the relationship tab. Every known proposal has a title link, with an honest count (a lower bound if history is incomplete), and wording distinguishes proposed effects from approved changes. This notice does not suppress drift, required acceptance, or other diagnostics and introduces neither a write control nor an execution hold. The flat amendment review page also shows provable created-node and addition-parent links and unresolved identity notes without inventing links for unmaterialized additions.
- A relationship hub: every related node is a link to drill. A question’s flat page keeps its durable forward
Blockstargets, including after answer or cancel; while the question is open it carries the copyable cancel and per-targetunlink --blockscommands beside the answer command, and once cancelled it shows a Cancelled section with the reason. Epic, feature, and task pages show the operative open questions blocking them, including inherited holds, alongside both dependency directions and both supersession directions in the Dependencies tab. The tab label counts onlydepends_on + depended_on_by. When supersession or question relationships are the only content, the tab remains visible with no count; it never shows· 0. The parent climb stays in the header and provenance uses the single source surface rather than repeating in the hub. - Backlinks are a distinct tab on tabbed nodes and a section on flat question/amendment pages, served in the same enriched node response. Groups use inverse relationship names and count distinct source identities, not repeated citations. Document sources retain their own identity plus owning work where known, exact file/line citations, and separate generated mentions. Mentions of a node’s owned document also appear through the document relationship. Existing containment, dependency, supersession, attachment/carrier/duplicate, effective-question, and amendment-operation panels keep their responsibilities rather than duplicating those incoming edges in Backlinks. Other incoming provenance/reference relationships remain inspectable. Known zero hides the tab; incomplete extraction keeps it reachable without presenting an unqualified zero.
- A lightweight history timeline: the node’s state transitions, derived from the event stream, with links out to touched commits and the PR where they exist. On a tabbed noun this timeline is the History tab’s own content; a question renders it in its old place, beneath its flat body and the hub. The console keeps the history light and always links out for depth, rather than pulling data in from GitHub.
A correct-eval operation reads like a revise: the target feature as a link, the recorded reason, and a retires line naming each criterion id the approval removes from the contract, absent when it retires none. The console offers no correction or approval button.
Reviewed move-feature operations show the retained feature identity, original and proposed addresses, destination epic, preserved review grain and any explicit override, deterministic order effect, reference warnings, and live move-snapshot validity. A stale snapshot stays visible before approval. After approval, the same feature and task links resolve to their current addresses; source and destination epic relationships retain their historical parent roles. The console offers no move or approval button.
Merged aggregate recovery is visible in Home advice and node detail. An absent review is labeled unrecorded, not merged: sync has not inspected its merge state. Advice distinguishes ordinary submission if unmerged from explicitly authorized historical recovery if already merged, without guessed PR/head/result values.
A recovered node exposes the historical PR/ref, reviewed head, exact integration result, evidence basis, recording contributor/time/reason, frozen riders, and historical policy provenance independently of current gates and acceptance. It recommends sync, not another PR, branch assembly, or awaiting human merge; contained/unwitnessed work instead names the existing human fast-forward decision. Historical warnings preserve source revision, unresolved criterion IDs, and evidence even after an approved contract change/current pass. Unknown policy is not rendered as all-clear. Views remain pure, read-only, and never query a PR host.
Home retains recovered reviews after completion, with classified current acceptance and separately labeled historical-source and crossing-time warning counts. An epic’s provenance renders once with every participating feature’s acceptance and current gates. Recovered console list rows carry these facts from one request-local workflow observation, without per-row node fetches or host calls.
Report
Section titled “Report”Report is the client-facing page, at /report and between Fix and History in the primary navigation. Its reader is a client product owner who wants a short answer to “how is this going over time”: how much is done against what was approved, how fast work ships, how long features take, whether quality is holding, and how many decisions are outstanding. It is metrics only. It shows numbers and charts; it lists no nodes, links to none, and offers no drill-down into Explore. Questions and decisions appear only as open and answered counts. It is presented live from tiller serve and has no export; if export returns, it exports this page.
The page is a header over two tabs on the console’s tab rail, labeled “Report view” for assistive technology as Home’s is: Project, the default, and Agent use, described under Agent use below. The selected tab lives on the URL as ?tab=agent-use; Project leaves the URL bare, and an unknown value reads as Project. The tabs appear only while the manifest’s usage.capture is on; otherwise the page is the Project view alone, with no tab rail, and ?tab=agent-use is ignored. The Project tab is a KPI strip and five charts: a full-width feature burn-up, throughput and bugs reported against fixed side by side, a full-width lead and cycle time chart, and a full-width epic progress chart. Below the console’s narrow breakpoint everything stacks in one column. The Auto, Light, and Dark toggle applies as everywhere else. The header shows the project name, the title “Project report”, and “As of” the read time in local time. The page-wide rules hold on both tabs: no commit ids and no Tiller vocabulary, so no node ids or keys, status names, or command names; nothing that names, counts, or ranks contributors or owners, on the page or in either payload; and series never told apart by color alone.
GET /api/report serves the header and the Project tab and is a thin shell over views.ReportOf, which is the only place a Project metric is defined. It reads no usage file; its one Agent use field is agent_use, the manifest’s usage.capture, which says whether the page offers the tab. GET /api/report/usage reads no usage file either while capture is off and returns an empty report. It replays the event stream through the process event cache and reads the current tree for scope, parentage, and reconciliation. It walks no Git history and never writes. The payload is ready-to-draw series, not events; the UI only draws, with hand-written SVG charts on the console’s tokens and one shared tooltip. Series are never told apart by color alone: each pairs its color with a direct label, a legend swatch with text, a hatch or dash pattern, or a filled versus hollow mark.
The week axis. Every time chart shares one axis of weekly buckets. Weeks start Monday in the local timezone of the tiller serve process, so an event at Sunday 23:30 local belongs to that Sunday’s week even when it is Monday in UTC. The payload retains date-only weeks labels and supplies required week_boundaries: RFC3339 timestamps with offsets, one for each Monday plus the Monday after the last week. Completion dots and median lines interpolate between these absolute boundaries, never browser-local reconstructions or a fixed seven-day duration. This keeps remote readers in the server’s week and handles 167-hour and 169-hour daylight-saving weeks, including the final week. The axis runs from the week of the first event to the current week and spans at least four weeks, padded on the left, so a young project’s bars sit at the right edge. It spans at most 520 weeks, ten years ending at the current week, so one absurd timestamp cannot stretch it; an event dated before the axis start counts at the start. An event with a zero timestamp is malformed and is ignored, both for the axis and for every status. The current week is drawn partial, hatched or lighter, so it does not read as a slump. There are no period controls.
Scope. Scope is the non-cancelled features under approved epics. A feature enters scope when its epic is approved or when it is added under an already approved epic, and leaves when it is cancelled. Approved means the epic is ready, done, or archived. Draft epics and their features are proposals, not scope. The burn-up and the epic progress bars use this one rule; epic progress also shows draft epics as ghost rows, which are not scope.
Dating. Work is dated by the sync event that observed it, not by the merge itself. A weekly bucket absorbs the lag, and reading merge times from Git history would undo the console’s read performance. A bug carried by another item’s merge crosses done through its own sync event and counts once, in the week that sync ran.
Reconciliation. Replayed events can disagree with the tree: a ledger that was migrated or hand-edited, or a rollup reopened by new work with no event recording the reopen. A node with no recorded status starts in its current status at the start of the week holding the earliest event, not in the padding before it, and at the start of the axis when there are no events. A node whose last recorded status differs from the tree is moved to its current status at the read time. An event that only restates a node’s status records nothing: a feature eval correction, a revise, and a re-emitted question add all write the unchanged current status, and none of them is a new completion. An event is a change when it is the node’s first, when it sets a status different from the node’s previous one, or when its own before status differs from the status it sets, as when a rollup reopened with no event and is synced done again. The burn-up’s final week therefore always agrees with the tree and with epic progress. An event that lists several targets changes only those whose id prefix matches the event’s noun, so the amendment id that cancellations and applied amendments list beside the node is left alone. A transfer lists the original and its replacement, which share a prefix, and names each one: the original takes the transfer’s status, cancelled, and the replacement takes its own status, ready. An event stamped after the read time counts at the read time.
The KPI strip has five tiles. Features done of total, by the scope rule. Items shipped in the last 7 days, counting all three throughput kinds over a rolling 7 days ending now. Median cycle time, with “lead N d” and the count of completed features beneath, over all completed features. Open bugs. Open questions, with the answered count beneath and a sparkline of the weekly open count. Only materialized questions count, opened when added and closed when answered; questions staged in an unapproved amendment do not.
The charts.
- Feature burn-up. Two lines on the week axis: approved scope and features done, each measured at the week’s end. With no approved epic it reads “No approved scope yet”.
- Throughput. Weekly stacked bars of items reaching
done, labeled exactly Feature work, Fixes, and Upkeep on the page. The spec maps them to tasks, bugs, and chores and tweaks; those nouns are not page copy. Fixes is hatched as well as colored. With nothing shipped it reads “Nothing shipped yet”. - Bugs reported vs fixed. Paired weekly bars labeled Reported (when the bug was added; a transferred bug’s replacement is not a new report) and Fixed (when it reached
done, carried bugs included), with an Open line counting bugs reported and neither fixed nor cancelled at each week’s end. A cancelled bug leaves the Open line on its cancel date and never appears as Fixed. Chores and tweaks stay out. With no bugs it reads “No bugs reported yet”. - Lead and cycle time. A dumbbell chart with one entry per completed feature, placed at its completion date on the shared axis. A filled dot marks cycle time, from the feature’s first start to its latest completion. A hollow dot marks lead time, from the first time it became ready to the same completion. A thin line joins them, so its length is the time spent approved but waiting. A solid line is the rolling median of cycle time and a dashed line the rolling median of lead time, each over the last five completed features. A feature starts at its earliest
start.taskamong its tasks, or a legacystart.feature, whichever is earlier. A feature returned to specced and re-approved keeps its original approval date, and completion is its latest move todone. Only features with a real approval, start, and completion event are plotted; no duration is invented, and a reconciled status is not evidence. Durations are calendar days, one decimal below 10 days and whole days at 10 and above (6.5 d,14 d). Hover names the feature and both durations; a feature’s title appears nowhere else on the page. With no completed feature it reads “No features completed yet”. - Epic progress. The one snapshot chart: a horizontal bar per approved epic, in the order the Build board uses, with Done, In progress, and Not started segments and a label such as “7 of 12 features”. A feature’s segment is its Build board column, read over the observed tree: the start that activates a feature is recorded on its task’s branch, so a ready feature with a task in flight, on main or observed on a branch, counts as In progress. Every other Report metric reads the integration tree its event replay describes. Cancelled features are excluded, and done and archived epics stay. An approved epic with no features reads “No features yet”. Each draft epic is a ghost row in the same order, so the client sees the planned sequence and the future work: its title in muted ink, an empty track outlined in round dots in place of the bar, and a label with its count of non-cancelled features, such as “2 features planned”, or “Not yet approved” when it has none. The legend gains a dotted “Not yet approved” entry only when a ghost row is present, and the chart’s accessible summary counts the ghost rows. Ghost rows carry no progress and are not scope, so the burn-up and the KPI strip ignore them and their planned features. Cancelled epics are left out. With no approved epic but at least one draft epic the chart draws only ghost rows; with neither it reads “No approved scope yet”. Segment colors are the console’s state palette, and the dotted outline and the label, not hue, mark a ghost row.
Enlarging a chart. Each chart with data, on either tab, has a magnifier button at the top right of its card, labeled “Enlarge” and the chart’s title, and a chart in its empty state has none. It opens the same live chart, tooltips included, in a centered modal dialog 1100 pixels wide, or 92 percent of a narrower window, where a weekly chart draws one and a half times as tall and epic progress keeps its height and gains width; the Close button, Escape, or a press and release outside the panel closes it and returns focus to the button. On the page and in the dialog, the tooltip opens above the pointer in the lower half of a plot so it stays inside the card.
Checkout guard. An event records the status its own branch saw, so a checkout off the integration branch can show numbers that include unmerged work. When the checkout’s branch differs from git.integration_branch, an amber notice reads “Showing branch x, not main. Numbers include unmerged work.” It names the branches, the one place the page does, and never blocks rendering. A detached HEAD reports the branch as HEAD and shows the notice. A checkout that is not a git repository, or a project with no integration branch configured, shows no notice and renders normally.
A project with no events renders every empty state, with four all-zero weeks and no durations, and the SVG never carries a NaN or an infinity.
Agent use
Section titled “Agent use”The Agent use tab shows roughly how many model tokens the project’s coding agents spent and where they went: by stage of the workflow, by model, by finished feature and work item, and over time. Its source is the committed usage files described in the artifact schema, captured by tiller usage and sync; missing usage is unknown, never zero. It reuses the Project tab’s parts rather than lookalikes: the KPI tile, chart cards and frames with the enlarge dialog, the shared tooltip, legend swatches, the week axis with its partial-week hatch, and the empty-state idiom, in the same grammar of full-width charts and side-by-side pairs.
GET /api/report/usage serves it and is a thin shell over views.UsageReportOf, the only place a usage metric is defined. The tab fetches it only when selected. It reads every .tiller/usage/ file in the working tree, reusing a parsed file while the directory listing still reports the same file, size, modification time, and mode, as the event cache does. It reads the same integration tree as the Project tab for node kinds and parentage, and the event stream through the process event cache for start and completion dates, so its weeks and week_boundaries are the Project payload’s. It walks no Git history and never writes. A usage file that does not parse is a 500 naming its path, as a broken event stream is for /api/report; the Project tab is unaffected. The budget on a client platform build is under 500 ms cold and under 150 ms warm. The payload carries counts only; the one name in it is a finished feature’s title, for the per-feature hover.
With no usage at all the tab reads “No agent usage recorded yet” and draws nothing else, whether capture is off or simply has not run yet.
The measured note. Above the KPI strip a line names each harness measured and the local date of its earliest captured call, such as “Measured: Claude Code since Sep 30 · Pi since Sep 5 · Codex since Sep 7”, with harnesses ordered by their token totals. The page cannot know of harnesses it has no files for, so it names what it measured rather than claiming completeness.
Partial coverage. Harnesses keep their logs for different lengths of time, and a project that turns capture on mid-flight backfills a lopsided history: on a client platform build, Claude Code reaches back only to Sep 30 while Pi reaches Sep 5. To keep a short history from reading as low usage, the payload carries a full-coverage date, the latest of the per-harness dates. A week is not measured when it ends on or before the earliest harness date. It is partly measured when any part of it falls before the full-coverage date, so the week holding that date is partial unless the date is its Monday. Every other week is fully measured. On the weekly charts a not-measured week is an empty column outlined in dots, the outline Epic progress uses for “Not yet approved”, with “Not measured” in its tooltip and, only when such a week is present, in the legend; a partly measured week keeps its bar under the hatch the page uses for the current week, with “Partly measured” leading its tooltip and, only when such a week is present, in the legend. Neither is ever drawn as zero. Rows dated before the axis count in its first week. The KPI totals count every captured row, and the measured note makes the per-harness dates visible. The page cannot tell a harness that deleted its logs from one adopted later; both read as partial coverage before its first call, which understates confidence rather than overstating it.
Work items. The per-item figures cover done tasks, bugs, chores, and tweaks that have usage rows of their own and whose first start is on or after the full-coverage date. A done item with rows of its own but an earlier start, or no recorded start of its own, as when another item’s merge carried it, is left out, so older work never looks artificially cheap. A done item with no rows of its own is unmeasured. The work-item chart’s note counts both, and neither is ever placed in the lowest band. In-progress items are not counted at all, since partial totals would pull the figures down. Usage spent on a carried bug or chore under its carrier stays with the carrier. Features and epics are not work items; features have their own chart.
The KPI strip has five tiles. Total tokens. Cache reads, with its share of all tokens beneath. Fresh input and cache writes. Output. Median per work item, with “N items · X% of tokens tied to a work item” beneath; with no work item yet it reads “Not yet” and keeps “X% of tokens tied to a work item” beneath.
The charts.
- Cache reads per week and Fresh tokens per week, side by side. Two charts on their own scales, because cache reads run about thirty times everything else and one axis would flatten the rest. The second stacks fresh input, cache writes, and output, with cache writes hatched as well as colored. They read “No cache reads measured yet” and “No fresh tokens measured yet” when empty.
- Tokens by stage and Model by stage, side by side, with stages in display order: Spec writing, Planning, Implementation, Checks, Acceptance evals, Code review, Publishing, Closeout, and Other. The first has a row for every stage: a horizontal bar with its total and, in a right-hand column, its cache-read share, which reads a muted “n/a” for a stage with no tokens, since its share is unknown rather than zero. The second has a row only for each stage with tokens: a 100 percent bar split by model, naming the top four models by total tokens and folding the rest into Other. A model keeps one color in every chart, assigned once by overall total. A segment at least 12 percent of its bar carries a percent label, the hover spells out each model’s share and tokens, and the legend names every model, so hue is never the only cue. Both read “No stage usage measured yet” when empty.
- Tokens per finished feature, full width on the shared week axis: one dot per completed feature at its completion date, dated as the lead and cycle chart dates completion, with a solid rolling median over the last five features. A feature’s total is its own tokens plus its tasks’. The y axis is logarithmic and labeled “Tokens (log scale)”, because feature totals span orders of magnitude. Only features with usage whose first start is on or after the full-coverage date are plotted; the note beneath counts the earlier ones not shown. Hover names the feature and its total, as the lead and cycle chart does. With none it reads “No features completed yet”.
- Tokens per work item, full width: a histogram of work items by size band (under 100K, 100K to 300K, 300K to 1M, 1M to 3M, 3M to 10M, 10M to 30M, 30M to 100M, over 100M). Its note gives the median and count, the earlier items not shown, and the items with no usage recorded. It shows outliers without naming them. With none it reads “No work items finished yet”.
Each chart keeps its note in its empty state, so a chart the coverage rule leaves empty still says how many items it left out.
Acting at the gates
Section titled “Acting at the gates”Unbuilt write design. The current console offers review context and copyable commands only. The following actions, revision checks, local request protections, and state-commit convenience are future scope. Proposed console writes are limited to human-judgment verbs, plus answer and block/unblock; nothing the agent drives becomes a button.
| Action | What it does |
|---|---|
| approve | A feature plan to ready with its initial tasks, a later-added task to ready, an epic decomposition to ready, an amendment (approve applies it), or an accepted-basis refresh. Approve the plan, the most common gate. |
| return | Send a mistakenly approved feature or task back across its pre-work human gate, with a reason. |
| attest | Sign off an optional review gate, a manual criterion, or a required eval criterion after reviewing its recorded rubric evidence. The target is either a review gate or a specific criterion, matching the CLI’s --gate or --criterion. |
| answer | Resolve an open question. |
| block / unblock | Flag or clear a blocker, with a reason. |
| cancel | Terminate an epic, feature, or task, behind a confirm. |
| withdraw | Drop a proposed amendment, behind a confirm. |
add, spec, start, claim, release, reassign, transfer, check, eval, submit, amend, archive, and sync are not proposed node write actions. Ownership remains CLI-only even in this future write design: the console renders claims and the pool but does not take, release, or hand off work. Archive and generative actions stay on the CLI; future sync belongs to the refresh control described under Liveness.
Review is in context. No gate crossing happens blind. Opening a queue item or a node expands the review surface, which renders exactly what the action changes: a decomposition as the tree of proposed nodes with their change-types, surfaces, and provenance; an amendment as its operations, each showing target, preconditions, and effect; a feature as its spec and eval criteria; a finished node as its gates, policy violations, and evidence. Prose renders read-only, and code and PRs link out. The button sits beside what it approves. Every button submits the content revision rendered in the review; if the working tree changed since the page opened, the CLI refuses the write and the console reloads the review instead of approving unseen content.
There is no merge button. The done crossing is a merge, the merge happens on GitHub, and Tiller only observes it. For an in-review node the console shows the gate status and the PR link, the human merges on GitHub, and sync records the crossing. The console drives the work to the edge of done the same way the agent does, and stops.
Uncommitted state is surfaced. Because an action edits the working tree without committing unless explicitly asked, writes made in the console sit uncommitted until the human commits, and the other stream will not see them until then. The console surfaces a small “N uncommitted Tiller changes” indicator and offers an explicit state-commit action that maps to the CLI’s commit verb and is limited to reviewed .tiller/ changes. It never pushes; broadcasting stays the human’s deliberate act.
Task execution sources
Section titled “Task execution sources”The served tree remains authoritative for approved structure, prose, dependencies, lifecycle eligibility, cancellation, replacement, and terminal state. The console can observe a known task’s execution fields from its task branch without copying machine state into integration. Observations cover execution status, ownership/start activity, gates, evidence, and review records only. They cannot introduce tasks, rewrite approved structure, cancel or replace work, or mark it done.
Source selection uses the task’s recorded branch, or its canonical branch derived from the configured pattern when no branch is recorded. Each source must belong to the same Git repository and project prefix and match the target task’s immutable ID, canonical key, and recorded approval basis. A matching local worktree is preferred, including its uncommitted execution state; without one, the committed local branch takes priority, then the configured remote-tracking tip if no local branch exists. Unequal but related tips retain that priority, and the selected record’s freshness names which tip is behind; that is provenance, not a diagnostic. Divergent local and tracking tips produce a diagnostic and no observation, even when a worktree exists. Ambiguous worktrees also produce a diagnostic rather than an arbitrary selection. Unavailable or invalid sources remain diagnostic, not invented progress.
When the branch and the served tree disagree on a task’s owner, the newest ownership event decides. Ownership events are claim.task, release.task, reassign.task, and start.task. If the served tree’s newest ownership event for the task is absent from the branch and newer than the branch’s own newest ownership event, the served tree’s ownership stands: the task is shown from the served tree with an ownership_superseded diagnostic. This keeps a release or reassign on integration from being hidden by a stale task branch. Otherwise the branch’s record stands, including a release recorded on the branch. A served tree with no ownership event for the task never outranks the branch. If the events cannot be read, the served tree is shown with an ownership_unresolved diagnostic.
State and event reads stay confined to the selected project’s .tiller/ boundary; evidence reads stay within its project root and are limited to the task’s recorded gate references. A project nested beneath the repository root retains that source prefix in worktree and committed-tree reads; a root-level copied plan is not a substitute. Reads never cross symlinks, FIFOs or other special files, or Git gitlinks, including populated submodules and paths marked as gitlinks in HEAD or the staged index. Missing partial-clone objects are unavailable; Git lazy fetching is disabled for these reads. Source boundary checks inspect the staged index without refreshing it. The console performs no automatic fetch, pull, push, merge, Git-index refresh, or project-state write. Deliberate visit memory retains its separate existing behavior.
Every source read in a request is confirmed once, after the request’s projection, together with the served tree. Agents commit in their worktrees while the console reads, so a task source that moved costs only its own observation: that task falls back to its served-tree value with a source_unstable diagnostic, and the projection runs again without it. Only movement in the served tree itself fails the read. Queue, recap, drift, prose, and global History read only the served tree and skip source discovery entirely.
Home, Build, Explore, and node detail can reflect observed task activity. Parent activity reflects those tasks’ observed activity, not readiness to start or complete work on integration. Queue and workflow guidance still use the served tree. Home’s Up next honors observations through the same ranked list tiller next, tiller readout, and tiller guide compute, so the console and the CLI answer alike; eligibility still comes from the served tree. A task observed in flight under the acting contributor is a Continue row naming its branch, and one in flight under anyone else, or under no recorded owner, is no row at all. The in_review merge wait stays a row, and a console served from the task’s own worktree ranks it as ordinary continuing work. A branch-observed task suppresses its node action commands, workflow advice, and unpushed-review warning rather than presenting guidance based on a different checkout; its queued asks, such as an open question, stay visible without their commands. CLI eligibility is unchanged.
GET /api/execution returns observations and diagnostics arrays, including empty arrays. A source record labels its worktree or branch origin, source-qualified opaque revision, observed_at inspection time, optional freshness, and uncommitted, which reports uncommitted changes under the project’s .tiller/ state only, not unrelated code edits in the worktree. Home reads it once when it opens and again on manual refresh, not on a tab switch, and its Branches tab caps the visible diagnostics list at eight, with the rest one click away. The revision is a source pin, not an executable Git revision. The timestamp is neither commit time nor fetch time and does not establish remote freshness.
GET /api/node can include optional execution and execution_history_error. For an observed task, node History merges that target task’s events from the selected source with integration events; other source tasks’ events are excluded. A source history failure is named rather than presented as complete history. Global History and Home’s recent integration events remain integration-only.
Evidence entries can include an optional url or unavailable explanation. A source evidence URL pins the task, opaque source revision, evidence reference, and SHA-256 digest of the displayed artifact. Retrieval rereads that source and refuses a changed source revision or changed artifact bytes with HTTP 409; it never silently substitutes integration evidence. Source evidence shares the 8 MiB served-file cap for working-tree files and committed branch blobs alike: an artifact over it is refused with HTTP 413 before the digest comparison, without reading more than one byte past the cap, and node detail shows it as unavailable rather than hashing it to mint a link. Unavailable artifacts remain visible as unavailable, without a navigable link.
Liveness
Section titled “Liveness”Current behavior. The topbar refresh control reissues every currently mounted read in place, using the same path as a route or filter change. It never calls sync. The shared last-successful-fetch timestamp records a successful read, not proof that every resource is current or evidence of a host sync. Nothing auto-updates without navigation or refresh.
Explore rereads the tree, question counts, and shelf lists whenever the pathname changes. Successful navigator data stays visible during those reads and during manual refresh; the loaded project summary also stays available so refresh does not unmount the split. A fixed, single-line status area above the scrolling rows names pending resources and failed refreshes, with accessible busy indications that do not disable navigation. Additional notices scroll within that line instead of reserving a tall empty band or moving the tree. A failed resource retains its last successful content and a warning until a successful read replaces it, including an empty response. Other resources update independently. Navigation or the existing manual refresh action can retry; there are no automatic retries. Late superseded responses cannot replace current data or warnings. Every console read that is superseded by navigation, a refresh, or an unmount aborts its request, so it gives its browser connection back at once instead of holding it until the server answers. A browser keeps about six connections per console origin, and each Browse pick issues eight reads; without the abort, a few quick picks queued the newest node page behind every abandoned read.
Retention is opt-in and limited to the same non-null API path, with no persistent response cache. Different paths and disabled reads clear immediately. Node and file readers continue to replace old detail with loading or error rather than showing a previous destination under a new URL. Initial navigator reads retain their loading, empty, and error states, and expansion storage restores before ancestry auto-opening. Switching a node’s detail tab does not refetch either pane; leaving Browse keeps the existing mode lifecycle.
Map relationship data retains the last successful graph on a failed same-address refresh and names the failure. Node backlinks belong only to their node-page response: a slow response for an old selection must not appear beneath a new node. Ordinary reference diagnostics are scoped to the node and its owned documents, while index-wide warnings and extraction-loss explanations remain visible. Completeness stays conservative across the whole index; an unrelated extraction failure can hide an incoming reference. An unwritten or owned empty acceptance contract is normal draft state, not incomplete extraction. Indexed dangling references do not by themselves imply incompleteness or successful navigation.
Node and map pages tolerate unsafe or unreadable supplemental corpus files by preserving structural facts and marking relationship extraction incomplete. A failed corpus walk discards all partially scanned documents and names the failure with corpus_unreadable; an unreadable cited document remains unresolved with document_unreadable, attributed to the document and citing nodes. Unexamined files are not reported as missing. Separately read node prose and contracts obey the same confined-read boundary. CLI/API queries still refuse unsafe or unreadable corpus input. Malformed tree state, cancellation, and unstable Tiller generations are not converted into degraded success.
Relationship reads rebuild a request-local index from current tree, confined Markdown, parsed contracts, and historical evidence; there is no persistent index, watcher, event append, identity write, binding execution, or Git-index refresh. A request that starts its own load sees a manual prose edit or branch change without cache reset; one that joins an in-flight load sees state as of that load’s start (Read cost, below). The final generation check protects against Tiller mutations, not arbitrary editors or Git changes racing a read.
Read cost. Parsed event files are reused while the confined directory listing still reports the same file (same file identity, size, modification time, and mode); every read still lists every dated directory, so new, removed, or replaced event files appear on the next read. Parsed commit tree inventories are reused by commit object id. A passing check of a recovered review’s historical evidence is reused while the record and the integration branch tip are unchanged; a refusal is rechecked on every read. Every Markdown corpus walk lists every directory, checks every entry, and reads and validates current confined document bytes. File metadata never substitutes for a content read, so timestamp-preserving edits and newly invalid prose are visible on the next request. Relationship mention and contract parsing reuse bounded content-digest results under the relationship read-lifetime contract. Reads that arrive while an identical tree load and execution observation are already in flight share that load instead of starting another, and nothing from it is kept once it finishes; a read can therefore reflect state as old as the start of the load it joined. Each read still projects over its own copy and closes its own stability boundary. A read that joined a load and finds a Tiller mutation committed under it reloads once, since that load may predate the request; the read that started the load, or a retry that moves again, answers 500.
Unbuilt two-tier design. Local working-tree changes would update automatically, by watcher or polling, without a reload. Review panels retain the revision they rendered so live updates cannot silently change what a human approves. Host-derived state remains explicitly as-of-last-sync rather than continuously polled, with a separate labeled freshness time.
The planned refresh-all control rereads local files and runs sync as one manual sanity action. Watcher-driven updates, host-sync triggering, and the split between local and host freshness are not shipped. The roadmap tracks the gap.
What the console refuses
Section titled “What the console refuses”Consolidated, because the refusals are the design as much as the features.
- It does not author prose. Choices and short notes only. Specs, designs, and evals are edited in the editor.
- It does not do the agent’s work. No decompose, spec, execute, amend, or drift-fix. It renders their output.
- It does not review code. Tiller’s structure is rendered; code and PRs link out.
- It does not orchestrate git or the host. No commit, push, or merge in the shipped console. The unbuilt write design permits only an explicit CLI-mapped state commit.
- It does not hide. No team permission model; the whole team is visible. Future local write endpoints still require loopback plus session/origin protection.
- It does not narrate. Derived views only; written summaries are deterministic templates or skill artifacts it may display.
These mirror the CLI’s and the skills’ refusals, one surface up. The console is the human’s gate, and a gate’s discipline is in what it will not wave through.
How it maps to the CLI
Section titled “How it maps to the CLI”The shipped console reads the same served-tree projections as show, ls, status, search, query, and event-history surfaces, with the bounded console-only task execution observations described above. It shows CLI commands without executing them. Proposed writes would use only the human-judgment verbs above and an explicit state-commit convenience, adding no capability the CLI lacks.
The read API inventory includes GET /api/work-items for ordered shelf cards and attachment options; the ranked list in GET /api/next, the same payload tiller next --json prints with candidates cut to the shortlist, including each candidate’s optional workflow and the tree’s dependency_diagnostics; open_work_items on features in GET /api/build; and node-specific workflow alongside the existing node data in GET /api/node. The node relationship hub includes attached_to, resolved_by, duplicate_of, attached_work, and optional amendments with amendment_history_warning. Amendment browsing reuses GET /api/ls?noun=amendments. GET /api/version supplies the running binary version independently of project reads. These are local read projections, not host-state fetches.
GET /api/ls also carries the filter bar’s ordered facets, with option counts and selection flags; the axes table keyed by singular noun; and unresolved, an object mapping unresolved parent axes to their raw values. An unresolved epic or feature returns 200 with no rows, while a bad noun remains 400. Facets are console-only: CLI ls output and flags are unchanged and its parent resolver still refuses unresolved references.
An unresolved active parent excludes rows from every other axis’s counts, even if its raw value coincides with a stored parent link. Clearing that parent axis still offers valid recovery parents. Missing parents are never offered as unselected picks; a selected stale parent remains raw at count 0.
GET /api/map adds relationships: {vertices, edges, catalog, diagnostics, complete} beside the existing ordered work nodes and display edges. GET /api/node adds backlinks: {groups, count, diagnostics, complete}; each group has an inverse kind and distinct sources, each with its canonical vertex, contributing edges, and optional owner_vertex. These shared projections do not scan prose or resolve relationship identities again in the browser.
GET /api/query (also HEAD) accepts repeated from and where, comma-separated follow, and depth. It returns the CLI query’s data payload, with identical traversal and terminal filters, provenance, diagnostics, and completeness. Syntax errors use 400, missing starts 404, and read/environment failures 500. Relationship queries owns the complete catalog and recorded-versus-effective semantics.
One parity command defines the search seam:
tiller searchis shipped lookup and keyword search behind Explore.
The Report page has no CLI counterpart: it is a console view over GET /api/report and, for its Agent use tab, GET /api/report/usage, and no tiller report command exists. tiller usage writes the files the Agent use tab reads; it is capture, not a report.
Decisions settled so far
Section titled “Decisions settled so far”The reasoning behind the console design is in the design history. The owning sections above retain current invariants and explicitly unbuilt designs.
For the Report’s Agent use tab, settled 2026-10-26 (rationale in agent usage, aggregate and never personal):
- Agent use is client-visible, a second Report tab after Project, under the page’s metrics-only rules. (JC)
- No view, filter, or ranking by person, and no dollar figures; raw tokens with cache reads shown apart. (JC)
- Partial coverage is shown, never read as low usage: per-harness dates in the note, a full-coverage date, partly measured weeks hatched before it, and item-level figures limited to work begun after it. (JC)
- The tab has its own endpoint, fetched only when the tab is selected, so the Project tab and its payload are untouched.
- The tab exists only while
usage.captureis on. Turning capture off removes it even when usage files remain, because usage that is no longer captured is not instructive; the page then shows the Project view with no tab rail, as before the tab existed. (JC, 2026-10-27)
What’s next
Section titled “What’s next”Remaining console scope is UI writes with revision and request-boundary protection, explicit state commit, automatic local updates, two-tier freshness and host-sync refresh, Report export (print, Markdown, static HTML) and an executive summary with period comparison, and a local approved-spec blob reader. These are not implied by the shipped read-only review surfaces. The roadmap tracks implementation coverage.