Auspex 0.1.0-dev.2 (Dev)
Pre-releaseChanged
-
The board's unit is the task, not the session. A session is a process,
and half the processes on a busy machine are subagents — a step inside
somebody else's job, spawned and finished within one turn. Drawing each as a
peer of the thing that spawned it gave a wall where a delegation of four read
as four independent pieces of work, and the reader reassembled the family in
their head every time they looked.Every surface now folds them. The Ledger draws one card per piece of work
with its sessions as a strip of member dots and a↳ 3; the sidebar lists
project → checkout → task, with sessions only under a task somebody opened;
the Aviary gives one desk per task with the task's title on the nameplate;
the Flock draws one bird per task with its members as small avatars under it;
the Flight gains a Task scope that merges every member's lane into one
waterfall. The header, the menu bar and the sidebar count units, so a
family of three is one working task. A single global switch — "Show
subagents", in the grouping menu, persisted — lists the sessions on every
card at once for people who want the old density, and a chevron per card
does it for one.Work nobody filed a task for gets exactly the same card, derived from the
root of its delegation and markedauto, with "Promote to task…" beside it.
The task protocol stays enrichment rather than a dependency. -
Finishing a task asks for a review; it does not close it.
tasks.complete
andauspex.notify(done)move a task to a new Review state — still open,
still counted, still on the wall, wearing the sentence the worker wrote — and
only a person's close moves it todone. An agent saying it is done is a
claim about its own work, and the one thing a board full of agents must not
let any of them do is mark their own homework. The chip that used to say
✓ N donesays✓ N in review. -
The views are named for the birds. Ledger, Aviary, Flock, Flight, and the
Tasks page is the Roost.--viewstill takesboard,scene,crewand
trajectory, because they are in people's shell histories and in settings
files already written. -
A flock bird's body is its session, and its colour is its harness. The
wall gave each harness a silhouette; on a board of ninety that is one bird
drawn forty times. Bodies now come from a ten-strong plump family seeded by
the session key, every one of them at least 0.72 as narrow as it is wide —
asserted over the catalogue, because a shape with a point in it reads as
spiky at 56 points and vanishes at 22. The wall itself is half the size it
was.
Added
-
Tasks carry what they are and what they wait for. A kind
(feature/fix/chore/research), labels, an importance in words as well as a
number, and dependencies.tasks.listtakesready_onlyandlabel, so
"find me something to pick up" is one call rather than a list plus a
judgement. A task waiting on unfinished work sayswaits on AUX-…and is not
ready. -
Notes have kinds and refs.
tasks.logtakesdecision,evidence,
riskornote, and aref— a commit, a URL, a path. That is the whole
difference between a work log and a chat transcript: an agent that writes
"checked, it holds" has said nothing anybody can verify. -
A task has a page. Its handle, status, importance, labels, project and
milestone; the claim and every session on it with per-member state and
freshness; what it waits on; its history, with a place to write into it; and
Close, Reopen, Release claim and Promote. Reached from a card's double click
or ⌘↩. -
⌘K. A palette over the frame in hand: a handle out of a brief, a project
you have not scrolled to, a session by name, and the two or three actions
that otherwise need a right-click on a card you have to find first. -
Filters. Importance, label, harness, ready-only, claimed/unclaimed and
orphaned claims, offered only where the wall can answer them, with a bar that
says what is on and one click per facet to take it off. -
Orphaned claims. A claim whose session ended without finishing is marked
amber on its card, filterable, and has Release beside it. Its own marker
rather than a bucket: debris is not the same thing as work that is stuck. -
What each harness finishes. The Harnesses page gains claimed, finished
and median claim-to-finish, because "how many of its sessions are open" is
mostly history on a machine that has run agents all week. -
Projects contain tasks. Plans and projects used to be two roots side by
side, so a task an agent filed over MCP without a plan landed in a lane called
"Unfiled" — which meant "nobody asked where this belongs", next to the project
it obviously belonged to. There is one hierarchy now: project ⊃ task ⊃
sessions.tasks.createresolves the caller's project from the session on
the other end of the socket, using the sameBoardSnapshot.projectKey(for:)
the wall groups cards by, and a task inherits its project from the milestone
it is filed under or from the session that first claims it.A plan is demoted to a milestone: an optional heading inside a project.
Theplans.*tools keep their names — briefs already in flight carry them —
and describe themselves as milestones. The Tasks page draws one lane per
project with milestone sub-headers inside, hides projects with nothing in them
unless the window is bound to one, and says "Nothing to do" once instead of
drawing a dash in each of four columns. The Projects page gains a column for
the work each project is carrying, including projects whose sessions have all
gone home.Schema v5 adds
tasks.project_keyandplans.project_keyand backfills every
task that was already there from its claimer, then its filer, then its
milestone; what is left lands in a Scratch project a person can empty
rather than aNULLnobody can find. -
Attention is something said, not something inferred. A card is counted as
wanting a person, or as having finished, only when something explicit said so
— an agent callingauspex.notify, aPermissionRequesthook, a harness's
own permission wait, ortasks.complete. The olddone unseenbucket was
inferred from a closed turn, which on a machine that has been running agents
all week is true of several hundred sessions at once; a count nobody can act
on takes the counts beside it down with it. The inference survives as a faint
dot on a card, counted nowhere and notified never.Activity and attention are now orthogonal: an agent that reports finishing
while aswift buildis still running isworkinganddoneat once. The
buckets areneedsYou · doneReported · working · idle · ended, and every
surface reads the one map the frame carries — the board's ring and banner, the
header's chips, the sidebar's dots and per-project counts, the menu bar, the
crew wall's badges, the Trajectory's banner and timeline marker, and which
column a task is drawn in.Both loud buckets clear themselves: opening the card, typing into that
session's own terminal, the agent going back to work, "Dismiss", the header's
new Mark all as seen, or a day going by.session_viewsgrows
acknowledged_atandack_reasonso the answer survives a relaunch. -
A session waiting on a person now walks to the garden's front row instead
of keeping its desk, and shares that row with anything that reported
finishing — a red!and a green✓by the path. A raised hand among forty
desks is something you have to find. The back lawn keeps the resting and the
dozing, and is still the half that gives way when a busy repository fills the
map; nothing on the front row is ever bounded away. -
Settings → Agents gains one switch: whether a reported finish raises a
macOS notification (on by default). A session blocked on a person always
raises one, and deliberately has no switch.
Added
-
Every card says how full its session's context window is. The
/context
gauge, for the three harnesses whose stores record it: a thin bar and
898.8k / 1M · 90 %under the counters, coloured by the board's own state
language — quiet ink under 70 %, tool-amber to 90 %, needs-you red above it,
because a session about to compact is closer to "somebody should look" than
to anything new. A tiny⟲ 2after it counts the compactions it has already
been through.One definition of "used" on every row, so the gauge means the same thing
wherever it appears: the tokens that were in the model's context when it
was last called — the whole prompt, cached prefix included, and the reply
just generated excluded, because that lands in the next call's input. Claude
Code'sinput_tokensplus both cache counters; Codex's
last_token_usage.input_tokens; Grok Build'scontextTokensUsed. Cursor,
AntiGravity, Grok Bot, Claude Cowork and Gemini CLI record nothing that
answers it and get no gauge at all — a bar at zero would say something none
of them said.The dotted half is the part worth knowing about. Codex and Grok write their
window size into their own logs; Claude Code computes it in-process and never
writes it down, so Auspex looks it up from the model id. What is uncertain is
therefore the denominator, never the fill — so the fill stays solid and the
unfilled remainder goes dotted, which says "this is roughly where the wall is"
rather than "this measurement is soft". A model the lookup table has never
heard of still reports a fill, with no bar:421kis worth showing, and a
guessed denominator is not.And the gauge never prints more than 100 %. A fill past the window is not a
session at 425 % — no harness overruns its own window by four times — it is
Auspex holding the wrong denominator, which is what a model bigger than the
lookup table knows about looks like. So the gauge stops dividing:850.1k · window ?, in the quietest ink, with a tooltip saying the model's window is
not on record. Not red, because the one wrong answer there is a card sending
somebody to wrap up a session that has plenty of room. The trajectory lane
and the composition bar refuse the same reading for the same reason, and the
popover's ledger still prints what was recorded so the figure can be checked. -
The trace header opens what is actually in the window.
context 96.4k / 200k (48 %)beside the token totals it is most often confused with, and a
popover behind it with the exact counts, the cached share, the compaction
count, and an estimated composition — messages, tool results, everything
else, free.The composition is Auspex's own estimate and every line of the panel says so.
Claude Code's/contextis exact because Claude Code is the thing holding
the window; it knows its system prompt, its tool schemas and the skills it
loaded, and none of that reaches disk. What does reach disk is the
conversation, which Auspex already indexes, so messages and tool results are
estimated at four characters to the token and the rest is inferred by
subtraction. Four bands rather than Claude's seven: fabricating "System tools
1.9 %" out of a number nobody wrote down would be a worse answer than one
band honestly labelled everything else. When almost nothing could be
attributed — a session Auspex met after it started — the panel says that too,
rather than letting the remainder read as a claim about a system prompt.The scan runs when the panel opens, never on a frame: two index seeks from
the newest compaction forward, capped, off the main actor. -
A Context lane on the trajectory. A fourth row under Input, Model and
Tools: a step line of how full the window was, with a rule wherever the
harness threw the window away. Readings are anchored to a step index rather
than to a timestamp, so they land in the right place under all three scales —
the x axis is a clock under Duration and a count under Turns and Calls. A
step line rather than a smooth one, because nothing was measured between two
readings, and each run is coloured by the level it is at rather than the
one it ends in, so a session that sat at 48 % for a minute is drawn calm for
that minute. The lane appears only for a session that has readings; five of
the nine harnesses record none, and a lane that is empty on most of the board
is a lane a reader learns to ignore. -
The Codex row on the Harnesses page names its plan window.
used 43 % · resets in 2 h 10 m · plan pro, read out of a rollout that was already being
tailed. Codex writes arate_limitsblock beside its token counters and no
other store on this Mac writes one at all, so this is one line on one row.
Nothing here asks a network what anybody's quota is; the tooltip says so, and
says how old the claim is. -
--render-context, which draws that popover on its own.
ImageRendererhas no window and therefore no popover, so--render-board
can draw the header control and never what is behind it. The renderer runs
the real demo pipeline and the real estimate query, because a picture of an
estimate that skipped the query proves nothing. -
Auspex updates itself, on a stable channel or a dev one. Tagged builds go
out as GitHub Releases and an installed copy keeps itself current: Settings →
Updates, or Auspex → Check for Updates…. One signed feed serves both streams,
and Dev is additive — a preview build still receives every stable release,
because Sparkle always considers its untagged items. Choosing to try a
preview should never mean missing the next fix.Every archive is signed with the project's EdDSA key and verified against the
key compiled into the running copy before a byte is unpacked, so the check
does not depend on Apple's — which matters while builds are ad-hoc signed.
Nothing installs without being asked: this is a window people leave open for
days, and an app that replaced itself under a running session would take the
session's window with it.The channel lives in
~/.auspex/settings.jsonbeside every other setting;
an absent or unrecognised value means stable, because the failure direction
of this one setting has to be the one that installs less. -
A release process, in two scripts and three workflows.
Scripts/release_app.shcuts a release — version bump, changelog section,
branch, tag — and refuses a build number Sparkle would treat as a downgrade.
Scripts/generate_update_feed.shadds one signed item to the feed and
asserts that it dropped nothing already in it. CI builds from the tag into a
draft release, and publishing that draft is what rebuilds the feed. See
RELEASING.md. -
Light and dark, and the Mac decides. Auspex forced dark for as long as it
had one palette. Every token is a pair now — declared in one totalswitch,
so a colour cannot exist in one appearance and not the other — and the window
follows the appearance the Mac is set to, including a scheduled switch at
sunset. Settings → Appearance overrides that with System · Light · Dark,
switches the sidebar between the system's own material and the board's flat
ground, and shows which accent, background and foreground the choice resolved
to. Both are persisted in~/.auspex/settings.json; an absent key means
"follow", so nobody has to go and ask for the behaviour the setting exists to
give them.Both columns are retuned around one anchor pair —
#2D2D2Bground,#F9F9F7
paper — so the dark side moves off near-black onto a warm charcoal and the
two are the same room lit differently. A single app accent, terracotta
#CC7D5E, arrives for selection, keyboard focus and every system control's
tint, and "this control is on" stops being a surface step, because on a white
panel there is nowhere lighter to go. State colours keep their hue and clear
3:1 on their own pill in both; harness accents keep their hue exactly and
only lose brightness, by the least that brings each to 3:1 on a white ground.
AuspexPaletteTestscomputes every WCAG ratio from the table on each build,
so the numbers in the palette's documentation cannot rot.Nothing has to be relaunched. The three surfaces that hold bytes rather than a
dynamic colour — the board's tiled grid, the activity strips'CALayers, and
the office's textures — each rebake on an appearance change, and the tests
drive them through a real one and check what they are holding afterwards. The
office gets a daylit column of its own rather than the dark one inverted, and
its glows change how they meet the floor: additive light on a white floor
can only make white, so in light a glow paints the state's hue instead.--appearance system|light|darkdraws one launch in an appearance without
writing it down, which is how the performance budget is measured against
both;appearance=lighton--render-board,--render-scene,
--render-crewand--render-trajectorygives every screenshot in
docs/screenshots/a-lighttwin. -
The scene is a place, not a room: a meeting room and a garden, and people
who walk to them. The map is one continuous plan — the office as it was,
with two annexes under it — and a session's position is now the first thing
a glance reads. A session that is delegating walks to a long table with the
subagents it spawned down the sides, one table per family, a screen at the
far end carrying the parent's state colour. Anything resting goes to the
garden: idle on a bench, gone-quiet dozing, finished-while-you-were-
elsewhere holding a note, and over walking out through the gate. A session
waiting on a person never leaves its desk, whatever else is true of it.
Both annexes are on by default and switch off in Settings → Scene
(persisted in~/.auspex/settings.json), and off means everybody stays at
their desks — the office exactly as it was, verified by rendering it both
ways and diffing the pixels. -
SceneZoning,SceneRouteand the annex half ofSceneLayoutin
AuspexCore— all of it pure and tested. Placement is a function of the
board, the delegation edges it admits to, and the set of sessions the task
ledger calls unread; a table is a family in one project, the same rule a bay
follows. A desk is held while its occupant is away rather than freed, which
is what keeps the office's geometry identical, sits a returning session back
down where it was, and gives a walk somewhere to walk from. A route is three
straight legs — out to the walkway, along it, back in — with a gutter down
the left joining the strips; between two strips it is those with a trip down
the gutter in the middle. No pathfinder, and every leg axis-aligned so it is
oneSKAction.movewith one walk strip over it. -
Walking, at no per-frame cost. A walk is a node of its own that lives
exactly as long as the walk: both ends draw themselves empty and it carries
the sprite either would have. It uses the character package'swalkDown/
walkUp/walkRightstrips, mirrored for left, and falls back to the
procedural rig for a package that has none. A walk whose whole path is
outside the cull is skipped, a session that changes its mind halfway turns
round from where it had got to, Reduce Motion has no walking in it at all,
and a render lands everybody before the shutter opens. -
The demo covers the whole map.
delegateManyfans a parent out to two
subagents at once (twodelegatebeats in a row are two delegations, so a
family of three never existed to photograph); the renderer refreshes
staleness against the instant being drawn, against the demo's own compressed
threshold, so a still can show a dozing session; andDemoScriptdeclares
which of its sessions nobody has read, because whether a person has looked
is the one fact about a board no harness store holds. -
--render-scene … --office-onlydraws the map with both annexes off, so
"the annexes changed nothing about the office" is a picture two commands
apart rather than a claim. -
Harness hooks, so "waiting for you" stops being a guess. A permission
prompt is the one state no harness writes to disk: from outside, an agent
waiting for a person and an agent thinking hard are the same silence. The
Settings → Agents page (and the first-launch sheet) now installs hook entries
for Claude Code, Grok Build and Cursor, and wraps Codex's singlenotify
slot, each naming its file and its events before anything is ticked and each
removable from the row that installed it.Auspex --hook <harness>is what
the entries run: it reads the harness's JSON, writes one line to
~/.auspex/mcp.sock, and exits 0 within 200 ms whatever happens — a hook
that can hang is a harness that can hang. Tool calls are deliberately not
reported; the tailer already describes those, and counting them twice would
be worse than late. -
Vendor marks, not initials.
HarnessLogoloads each vendor's own
single-colour SVG fromSources/AuspexApp/Resources/ProviderIcons, draws it
as a template tinted with the harness accent, and caches it per
(harness, size). Every surface that identified a harness with two condensed
condensed capitals now wears the mark on the same tinted tile: the board card, the
sidebar, the trace header, the search results, the Harnesses page, the empty
state, the menu bar, and the desk fronts in the scene. The two-letter
HarnessStyleproperty they came from is gone, so nothing can reuse it, and
the SF Symbols survive only as
HarnessLogo.fallback(for:)for a mark that failed to load. -
Claude Cowork and ChatGPT Work are first-class harnesses.
AuspexAdapters.allrunsClaudeCoworkLiveAdapter, andfeaturedis now the
seven a person can actually be running: Claude Code, Claude Cowork, Codex,
ChatGPT Work, Cursor, Grok Build, AntiGravity. The Harnesses page shows all
seven, and the two rows that name one directory say why on the row —
~/.codex/sessionscarries both Codex and ChatGPT Work rollouts, told apart
by each rollout'soriginator. The demo board gained a Claude Cowork session
and a ChatGPT Work session, so all seven are visible in--demo. -
Full harness names everywhere. A menu bar row now carries the vendor mark and
the harness's full name rather than a two-letter code, and no UI string
abbreviates a harness. -
SwiftPM package scaffold:
AuspexCorelibrary on GRDB 7 and
agent-session-kit, and theAuspexAppSwiftUI executable, both in
Swift 6 language mode. -
AuspexPaths— single source of truth for the~/.auspex/tree, created
lazily with mode 0700, with an injectable home directory for tests and a
containment check that refuses to create anything outside its base. -
AuspexStore— GRDB store on a WALDatabasePool, whose append-only
v1_initialmigration creates the whole schema: projects and worktrees,
sessions, the event log, the tool-call ledger, indexed message text with
an FTS5 index over it, the task board, per-source tailing cursors, and
meta. A session row carries the reducer's snapshot verbatim plus the
columns the board sorts and filters on, projected out of it in one place
so the two cannot drift. -
Full-text search across every harness at once, using FTS5's
trigram
tokenizer so a Chinese substring and an identifier glued inside a longer
one are both found — neither of which a word tokenizer can do. -
SessionRepository— snapshot upsert and fetch, batched event append and
recent-event windows, the tool-call ledger, and search.
SourceCursorRepositorypersists how far each tailer has read, so a
relaunch resumes instead of re-reading gigabytes of history. -
RetentionPolicyandRetentionJob— 2000 events per session, 14 days
measured from when Auspex observed an event rather than from the source's
own timestamp, 30 days of searchable text, and a per-harness exclusion
list for the index. Stored inmeta; not scheduled yet. -
SessionRegistry— an actor that bootstraps from the store, folds the
AgentEventstream throughSessionStateReducer, batches writes into one
transaction per 250 ms, and re-evaluates staleness on a one-second tick. -
BoardSnapshot— the immutable frame the UI renders, published coalesced
to at most 20 Hz, sorted alive-first and then by which session most needs
a person, and grouped by harness and by project. -
The live board. A wall of session cards in a
NavigationSplitView:
sidebar, board, session trace. Each card carries its harness accent rail
and vendor mark, a state pill, the current tool or target, an elapsed-in-state
stopwatch, turn / tool-call / token counters, and the project, pid, and
model. Group by nothing, by harness, or by project, with sticky section
headers carrying live counts; search every transcript from the toolbar. -
A state language built out of colour and motion. Every card ends in one
2 pt pulse line whose rhythm is its state: a slow breath for thinking, a
travelling segment while a tool is open, one tick per child while
delegating, and a hard strobe plus a red glow for a session blocked on a
permission prompt. OnerepeatForeveranimation per animating card and
none at all for idle or ended ones; every rhythm collapses to a static bar
under Reduce Motion. -
HarnessStyleandStateStyle— the fixed accent hue and vendor mark
for each of the eight harnesses, and the colour, label, and motion
for each session state, defined once so the board, the trace, the menu bar,
and M2's scene view cannot disagree. Colours are dynamicNSColors, so
light mode works without an asset catalog. -
SessionTraceView— a session's identity, its parent link, and a trace
waterfall of its events on a continuous spine: timestamps to a tenth of a
second, a coloured node per event kind, turn separators, filter chips, and
tail-following that yields the moment the reader scrolls away. A tool call
is one row carrying its duration rather than two; clicking a row opens the
full text and the pretty-printed payload. -
BoardGroupingandTraceEntryinAuspexCore— the pure grouping,
sorting, and event-summarising the views render, testable without a window. -
LiveBoardModel— the single consumer ofSessionRegistry's frame
stream. The trace stays live by re-readingrecentEventswhenever a frame
moves the selected session, debounced so a burst is one query. -
AppEnvironmentwires the pipeline: store → registry → merged event
stream, fed byIngestCoordinatorand aLivenessResolverloop, with
SourceCursorRepositorynow conforming to the kit'sSourceCursorStoreso
a relaunch resumes where the tailers stopped.AuspexAdapters.allis empty
until the Claude Code and Codex adapters land, and the empty board says so
rather than looking broken. -
Menu bar extra showing live, delegating, and blocked counts, and a menu
of live sessions that opens the window onto the one you pick. -
--demo(orAUSPEX_DEMO=1) replays a fabricated board — eight
sessions across all five harnesses, seeded and reproducible — out of an
in-memory store, so the UI can be developed and demonstrated before any
adapter exists. It reads no harness store and writes nothing to disk. -
The scene view. The same board read as a room: every session is a pixel
agent at a desk, every project is a room they share, and a sub-agent sits at
a smaller desk beside its parent with a dotted tether back to it. A
Board / Scene control above the grid switches between them, and the
choice is remembered. The two answer different questions — the wall says what
one session is doing, the office says what the whole machine is doing without
being read — so the loudest channel here is light: a monitor's colour is its
session's state, its rhythm is that state's motion, and the spill lands on
the desk and the agent. Blocked sessions strobe red, raise a hand, and put an
exclamation over the desk; everything else stays quiet. The canvas is an
NSScrollView, so two fingers pan and pinch at once with the platform's own
momentum and elastic edges; ⌘-scroll zooms, a two-finger double tap frames the
room under the pointer, and Fit frames the whole building. Clicking a
desk sets the same selection clicking a card does, in
both directions. Every rhythm collapses to a static pose under Reduce Motion. -
SceneLayoutinAuspexCore— the pure, tested seating plan behind it.
Keeps an allocation table rather than laying out from the board's own order,
which sorts by urgency: a desk is held for as long as its session is on the
board, a newcomer takes the lowest free slot, and nothing already seated
moves when somebody arrives or leaves. Vacated desks in the middle of a row
stay on the plan as empty workstations and are reused; trailing ones are
trimmed. Rooms are shelved left to right and wrap, so four small projects
read as one building rather than a column four screens tall. -
SceneDirectordiffs the SpriteKit graph against each frame instead of
rebuilding it — a node recreated twenty times a second is a node whose
repeatForeverrestarts twenty times a second — and the view runs at 30 fps
and pauses itself when its window is occluded or hidden. -
Procedural placeholder sprites and
SpriteLibrary. The agents are drawn
in code today, in their harness's own accent hue, from small RGBA buffers
rendered with nearest-neighbour filtering. Real frame strips drop in per
harness, variant, and pose from~/.auspex/sprites/or the app bundle, with
the procedural rig as the fallback for anything nobody has drawn yet.
docs/SPRITES.mdspecifies the atlas. -
--render-scene <path> [seconds]renders the office to a PNG offscreen
from the demo board, so the README's screenshot is a reproducible build
artefact containing no real session, path, or name.
a person, and grouped by harness and by project. Carries the delegation
forest astree, and groups a child that recorded no directory of its own
under its nearest ancestor's project rather than leaving it homeless. -
ProjectResolver— turns a working directory into aProjectPlacementby
reading git's own files (.git,commondir,HEAD) and never shelling out
togit. A linked worktree resolves to the repository it was branched
from, so three agents in three worktrees of one repo are one project with
three checkouts. Recognises the agent-worktree conventions —
.agents/worktrees/<task>and the.claude,.codex,.cursor,.grok
variants — and reports the task name. Cached per directory, invalidated by
HEAD's mtime, with a 30-second TTL for directories that have noHEADto
watch. -
PlacementService— resolves each(session, directory)pair once, which is
what keeps a harness that re-reports its cwd on every transcript line from
costing a filesystem walk per line. -
ProjectRepository— upsertsprojectsandworktreesfrom placements,
pointssessions.project_id/worktree_idat them, writesroot_keyfrom
the session tree, and answersfetchProjects(withCounts:),
sessions(inProject:), andsessions(inTreeRootedAt:). A later resolution
that learned less never erases what an earlier one knew. -
SessionTree/SessionTreeBuilder— the cross-harness delegation forest
built fromidentity.parent, withrootKey(for:),descendants(of:), and
per-node depth. Total by construction: an orphan whose parent is not on the
board becomes a root, and a contradictory pair of stored parents is broken
rather than followed. -
SessionRegistry.applyPlacements(_:)andapplyLinks(_:)— the entry
points that turn resolved placements and inferred parent links into ordinary
identityUpdatedevents, so the snapshot, the event log, the store, and the
board learn them the same way everything else is learned. A link is refused
for a session that acquired a recorded parent in the meantime, and one
naming a parent the board does not have is dropped. -
GroupingCoordinator— the thin three-second driver that runs both off the
registry's actor, becausesysctlandstatdo not belong on the path of
every event. Sessions that are no longer running keep their session id but
lose their pid, so a recycled pid cannot be mistaken for a live parent.
AppEnvironmentstarts it alongside the liveness loop, sharing one
ProcessTablebetween them so a tick costs one process-table read rather
than two — in demo mode as well as live, so both exercise the same path. -
Projects sidebar — a live tree of every project on the board, built from
the frame rather than from the database so it cannot disagree with the wall
next to it:project → checkout → session, with a dot per harness at work,
a count of what is running, and the agent worktree's task as the checkout's
label wherever the path follows the convention. A project opens itself the
first time something in it goes live, and never re-opens after that, so it
does not fight a reader who closed it. Directories in no repository are
listed too, marked "no git". Selecting a project filters the wall to it;
selecting a session selects its card.ProjectTreedoes the building, in
Core, where it is tested. -
Group by: Tree — the wall as a delegation forest. Each root that
delegated gets a section with its children nested behind a rail; roots that
delegated to nobody share one trailing section, because a tree of one is not
a tree. A card carries a "↳ N children" badge for what is below it and a
chip naming its parent, which selects the parent when clicked. The forest is
rebuilt from the filtered sessions, so a child whose parent a harness
filter removed becomes a root rather than vanishing. -
Project filter — applied on every grouping axis and answered against the
frame, so a subagent with no directory of its own stays with the project it
inherited. A bar above the wall says which project is showing and clears it. -
Trace header now shows the project, the branch, the agent worktree's
task, the parent, the children, and — the point of the row — how the
parent link was established: a spawn the parent's own log recorded, an
inherited environment variable, a process ancestry, or a person's decision.
Those are claims of very different strength, and a header that showed a
parent without saying which invites a reader to trust a guess as a record. -
Harnesses page — one rack row per harness: whether its store exists on
this Mac, live / idle / total sessions from the board, last activity, and
the MCP servers it is configured with. The three come from three different
places on three different schedules and are kept apart, because "no sessions
on the board" reading as "not installed" would be the most misleading thing
the page could say. -
HarnessMCPConfigStore— read-only parsing of each harness's own MCP
configuration:mcpServersin~/.claude.json(global and per-project
scopes kept apart) and~/.cursor/mcp.jsonand
~/.gemini/config/mcp_config.json, and[mcp_servers.<name>]tables in
~/.codex/config.tomland~/.grok/config.toml. The TOML side is a
tolerant section scanner rather than a parser: a sub-table is not a server, a
quoted name is unquoted, and a construct it does not understand costs it that
table rather than the file. It never writes, never creates a missing file,
and reports "no config file", "could not be read", and "no servers" as three
different answers. Whether Auspex's own server is registered is shown as an
empty socket — it arrives in M3. -
Main window (
NavigationSplitView) and a menu bar extra with Open and Quit.
The sidebar's destinations are Live / Tasks / Harnesses / Settings; Projects
is not one of them, because the tree below them is the projects section. -
--mcp-stdioand--hookcommand-line placeholders, dispatched before
AppKit starts; both exit 2 until M3. -
Scripts/build_app.sh— packages and ad-hoc signs.build/Auspex.app, and
fails the build if the signed bundle claims the app-sandbox entitlement. -
Open-source scaffolding: README (English and Chinese), architecture notes,
contributor and security policies, issue and PR templates, and CI.
Changed
- The scene's canvas is the platform's. The office now hangs on an
NSScrollViewwhose document view is empty, flipped, world-sized and never
drawn; theSKViewstays the size of the window underneath it and renders
whatever rectangle the clip view is showing, read onto theSKCameraNode
once per frame. Panning, momentum, elastic edges, the scroll-direction
preference and the live pinch are the platform's, which is what makes two
fingers scale and move the map at the same time — the thing a hand-rolled
magnify handler cannot do, because zooming around a centroid does not
translate when the centroid travels. What stayed ours is the travel of such a
pinch (applied only when the system is not already scrolling for the same
fingers), a zoom on ⌘-scroll, a smart zoom that frames the room under the
pointer, and the landing on the crisp zoom ladder when the fingers lift. The
SKViewis deliberately not the document view: a document is scaled by the
magnification, so a Metal-backed one would need a drawable the size of the
building times the zoom — 559 MB for a forty-project office at 4× on a Retina
display, and 3.5 GB for a six-hundred-session one, to draw a picture 900
points wide. - Hovering costs a rectangle test rather than a walk of the office. Every
mouse-moved event used to callSKScene.nodes(at:), which visits every node
in the scene and allocates; measured on a 600-session office that is 5.0 ms
per hit test, and a trackpad asks several times per frame. The pointer is now
placed against the floor plan — the room first, then that room's desks — at
0.14 µs per hit test, and at most once per drawn frame however often the
pointer moved between them. AuspexAdapters.installedandwatchRoots(home:)index by each adapter's
handledHarnessesrather than its primaryharness, so ChatGPT Work is not
reported as unwatched while its sessions are on the board.HarnessMCPConfigStoreno longer answers~/.claude.jsonfor Claude Cowork.
Cowork's MCP servers come from Claude.app's own settings inside the app
container, so the location isniland the Harnesses page says managed by
Claude.app rather than reporting the CLI's servers as Cowork's.