Open Cargento, a local agent-cartography dashboard for Claude Code, Codex, Pi, Gemini, Antigravity, Copilot, OpenCode, Cursor, Goose, and Droid sessions, with subagents, task progress, token rate, ETAs, and Claude input-wait notifications. Use for “open cargento” or “monitor agent progress”.
Resources
3Install
npx skillscat add spacedock-dev/cargento/cargento Install via the SkillsCat registry.
Cargento
Cargento is an agnostic agent cartography and visualization tool: a local web dashboard mapping live coding-agent activity across ten harnesses on this machine — Claude Code, Codex, Pi, Gemini CLI, Antigravity CLI, GitHub Copilot CLI, OpenCode, Cursor CLI, Goose, Factory Droid — each row badged with its harness. A "Discovered harnesses" strip at the top shows all supported harnesses; ones with local session data are green/enabled, others gray/disabled. A harness's sessions only appear if its data is discovered. Transcript-backed sessions appear even if they never called TaskCreate; Claude task files may also surface a task-only session when its transcript is unavailable. Per session: a live state badge, what it's doing right now, running subagents (named pills), a current-turn elapsed/ETA estimate with progress bar, a ⚠️ warning (with tooltip) when a request runs or is estimated ≥15 min, one row per tracked task, and the recent token output rate. Fires desktop notifications when a session is blocked waiting on the human — Claude, Codex, Copilot, Cursor, OpenCode and Pi can report waits, within their stated coverage — and when any session registers a question through the ask lane. Sessions, Projects and Attention render the same data three ways — see Dashboard views below.
Store locations are resolved per platform, and the documented relocation variables are honored: CLAUDE_CONFIG_DIR, CODEX_HOME, GEMINI_CLI_HOME (the CLI creates .gemini inside it; relocates the Antigravity store with it), COPILOT_HOME, PI_CODING_AGENT_DIR, and PI_CODING_AGENT_SESSION_DIR. When one is set it is authoritative — no fallback to the default location. Run --diagnose to see every path searched.
Data sources (read-only, no external calls by default; the observer model is the one path that can send session content off this machine through the installed Codex CLI, goal summaries require --observer-model and their own consent, while reader-requested readings require remembered first-press consent; --no-observer-model refuses it for the run; all parsing is defensive — a broken harness store is skipped, never fatal):
~/.claude/projects/*/<session>.jsonl— Claude transcript tails: session discovery (a file holding only generated-title, agent-name or PR-link records is not a session), titles, token usage, pending AskUserQuestion detection and the text of the question it is asking- Claude subagents, two generations: modern harnesses write each subagent as a top-level
~/.claude/projects/*/<uuid>.jsonlwhose records carryagentName+teamName: "session-<parent prefix>"— these fold into the parent session (named pill, its own start and elapsed, freshness, output rate) and never appear as standalone sessions. A teammate dispatched into its own pane stays on that row once it goes quiet, marked as no longer running rather than dropped, and its own subagents are walked and listed beside it, each attributed to the teammate that spawned it; legacy<session-uuid>/subagents/agent-*.jsonl+.meta.jsonfiles are still recognized (fresh mtime = running, and one the lead ran itself stays listed as no longer running once quiet, on the same display window as a teammate), including thesubagents/workflows/<run-id>/directory a workflow fan-out nests its agents in. A bareagentNamefrom a top-level--agentlaunch has no parent relation and remains a standalone session;agentSetting, notagentName, supplies any Spacedock role. Agent writes count as parent activity in their own right, so a session parked on a long background workflow reads Working rather than Idle - Spacedock workflows: a Claude session launched by Spacedock carries an
agentSettingofspacedock:first-officerorspacedock:ensignin its first transcript records, which is how its role badge appears. Pi and Codex write no such setting, so a supported boot result earns a first-officer badge instead, and never an ensign one. Codex also accepts the captured fulfilled-result wrapper only when its integer exit code is zero and its output is one complete boot JSON object. A first officer also records itsspacedock status --bootoutput — counted only when it arrives as command output, never as ordinary conversation text — which names each workflow directory and each entity-state directory absolutely. The ordered stage list comes from the workflowREADME.mdfrontmatter, and each entity's current stage from thestatusin its own state-file frontmatter; boot'sdispatchablelist is only a snapshot of what was ready to move at boot, so it fills in behind the state directory rather than standing in for it. Those two kinds of frontmatter are the only project files the stage-strip reader reads; the cockpit's separate project-context prototype also reads bounded dispatch and workflow evidence — see the repository's SECURITY.md for the contract, and--no-spacedockto disable it ~/.claude/teams/session-<id>/config.json— the Claude teams registry: the roster of members a lead session has dispatched. A member is listed here the moment it is spawned, which is before it has written a transcript byte, so a member on the roster with no transcript anywhere is one that has not started — the shape an agent parked on a startup permission prompt takes on disk — and the session reports Needs input and names it. The roster is a roster and not a heartbeat: one file stamp covers every member and moves only on a join or a leave, so a member is read this way only once it has been registered longer than a healthy agent takes to start and only while the session is still inside the display window; running subagents keep their own transcript freshness. Older harnesses pruned a member as it finished; Claude Code 2.1.259 keeps it and marks itisActive: falseinstead, so what stops finished work from reading as running is transcript freshness, and that flag is only ever allowed to confirm a member has stopped, never to claim one is still going. A session directory with noconfig.json(an older layout) is skipped rather than treated as broken. A gate that opens mid-run is not detected: nothing in the store distinguishes a blocked member from a busy one once it has started~/.claude/tasks/<session-id>/N.json— tracked task state (subject, status, activeForm); current bare-UUID and oldersession-<id>directories are supported~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl— Codex. The newest instruction, the title and the agent's turn-start statement of intent come from a walk backward from the end of the file rather than from its tail, because Codex pads a rollout with large encrypted reasoning blobs and the prompt is usually well outside any bounded tail read. Line 1 (session_meta) gives identity + cwd;thread_source: "subagent"files are subagent threads (named byagent_nickname), grouped throughsource.subagent.thread_spawn.parent_thread_id; resumes dedupe to newest file per session. Codex's own plan is read from the newestupdate_planrecord and becomes that session's task rows — the same backward walk and for the same reason, since a session that has done any work since writing its plan has pushed it out of tail range. Two wire shapes are read, because a given Codex build writes one or the other: an older tool call whose arguments are JSON, and the current one that carries the plan as JavaScript source. The walk does not stop at a compaction boundary the way the prompt walk does, since the CLI keeps showing the plan across one. A step carries no timestamps of its own, so a Codex session gets task rows and a progress count but no age per row and no "est. remaining"~/.pi/agent/sessions/--<encoded-cwd>--/<timestamp>_<uuid>.jsonl— Pi v3. The default store is nested; a custom session store is flat and contains JSONL files directly. The last persisted leaf's ancestor path is active, so sibling branches are excluded from prompts, tools, output usage, and turns. The latest globalsession_infoname is the title, including a later clear;parentSessionmeans fork or clone, never a subagent. Assistant, tool-result, compaction, and branch-summary output usage feed the rate. Pi has no allowance of its own, so each row also names the authority it is spending, read from the newest entry on the active branch that carries one (an assistant message, or amodel_changethe user has switched to but not spent yet): it renders asvia Codex · gpt-5.6-sol, and a provider with no harness of its own keeps its own name. A session that names no provider makes no claim rather than guessing from Pi's current default. Pi has no passive needs-input or core-subagent signal. Its Working/Idle reading comes off the active-branch leaf record'sstopReasonrather than recency alone, and it is the only harness whose store records how a turn ended: an assistant leaf stampedtoolUseis a tool call whose result has not been written, so it reads Working —running <tool>— past the 90s window and up to 15 minutes, because recency cannot tell a longbashfrom a parked session; past that it reads Idle, because a transcript can record that a tool started and can never record that the process died, so a Pi hard-killed mid-tool would otherwise hold the top of the board for the whole display window. A finished turn (stop,abortedorerror) reads Idle the second it lands however fresh it is. AuserortoolResultleaf has handed the turn back to the model and readsthinkingwhile fresh. An unrecognisedstopReasonclaims nothing and stays on recency. None of this distinguishes a finished turn from an unanswered wait, which still needs a turn-end event reaching the board: Pi documentsturn_endandagent_settled, the second one for status integrations by name, but both are handed to an in-process extension handler and Cargento ships no Pi adapter to receive one. It does get a Spacedock first-officer badge and stage strips: with noagentSettingto read, a Pi session is taken to be a first officer when its transcript carries aspacedock status --bootenvelope, written as atoolResultmessage. There is no Pi ensign role, and a Pi strip never bolds an entity, because Pi reports no workers to attribute one to.~/.gemini/tmp/<project>/chats/session-*.jsonl— Gemini CLI main sessions. Gemini CLI stopped serving consumer accounts in June 2026 and Antigravity CLI succeeds it there, but enterprise Code Assist and API-key use still write this store, so it is historical on a consumer machine — sessions read as idle and need?all=1to show — and live on an enterprise or API-key one (line 1:sessionId/kind/directories);chats/<parentSessionId>/*.jsonlare subagent recordings (linked by directory name). Flat messages and resumed-session$set.messagessnapshots supporttype: user|geminiand per-messagetokens.output. Legacy single-.jsonchat files are not parsed~/.gemini/antigravity-cli/conversations/<session-id>.db+cache/last_conversations.json+log/cli-*.log— Antigravity CLI (agy) sessions. Per-conversation DB/WAL activity provides discovery and working/idle state; trajectory-metadata protobuf fields fold fresh subagents at every nesting depth into the root card with role/type labels, and descendant activity and output usage feed the root state and token rate; stable step-metadata fields provide prompt boundaries and tool action/summary; the cache supplies the primary workspace, with CLI logs as its fallback and as the latest-prompt source. Conversation content is not decoded~/.copilot/session-state/<uuid>/events.jsonl(+ legacyhistory-session-state/) — Copilot CLI typed events:user.message,session.start(data.context.cwd),tool.execution_start,subagent.started/completed/failed,session.task_complete,permission.requested/completed. The permission pair joins ondata.requestIdand is what makes a Copilot row report Needs input; only the kind of prompt is read from it, never the command or the URL. A subagent's model comes off the samesubagent.startedobject its name does, so the two are paired without a join, and all three subagent events are keyed on theagentIdthey share. Eventdatafield names are de-facto, parsed defensively~/.local/share/opencode/opencode*.db(SQLite, read-only) — OpenCode:sessiontable (parent_idlinks subagent/child sessions,directory,title,time_updatedin epoch ms; archived sessions filtered), plusmessagejoined topartfor turns and prompts.messagehas notypecolumn — the role is insidemessage.data— and the prompt text is insidepart.data, on the parts a message typedtext, so an attached filename never publishes as the prompt someone typed. Measured on 1.18.20:session_messageexists, is what an earlier reading went to, and no build measured writes a row into it. The model is theidinsidesession.model, a JSON object OpenCode writes on the session row; the column is optional there, so a row carrying none falls back to the newest assistant message's ownmodelID, and a child session's model is read from its own row rather than inherited. Busy/idle is not persisted — inferred fromtime_updatedfreshness~/.cursor/chats/*/<agent-id>/store.db(SQLite, read-only) + the siblingmeta.json— Cursor CLI: themetatable holds hex-encoded JSON (session name,latestRootBlobId, andsubagentInfoon a child), and the sibling file holdscwd. Minimal support: discovery + working/idle via db/WAL mtime + title + project + the model of the newest message + subagents + a standing permission gate; no turn ETA (content is hex blobs). The gate ispendingToolExecutionContractson the newest blob byrowid— non-empty means somebody is being asked, andpendingToolCallStartedAtMsbeside it is when they started waiting. Newest byrowidand not simply anywhere, because the store is append-only and content-addressed: the blob that stood at the gate is still there long after the answer, androwidis the only order the schema keeps. The key is matched on bytes, since the carrier is a length-prefixed binary frame rather than JSON, and the wait is published only while the row is fresh — an abandoned session stays pending in the file forever, so without that the board would carry a red row and a notification for a session nobody is in. A reading is also dropped when its stamp is too old to have been written by the store's own last write, since a real gate is what froze that file: that is what keeps a stamp belonging to some neighbouring record, or a message that merely quotes the field, from turning a row red with a wait nobody believes. The model isproviderOptions.cursor.modelName, found by following the chat's root blob to the message blobs it lists and reading a bounded slice of the newest few. Cursor writes its own codename there and it is published verbatim, since mapping it to a marketing name is a guess that would silently mislabel the next codename. Blobs are plaintext on every store measured, but the store carries an encryption key it is never asked for: where the bytes do not read, the session reports no model. The workspace iscwdin themeta.jsonbeside the store — no spelling of it appears in themetapayload at all — so the key spellings that payload was read for stay on as a fallback beneath it. Either source is accepted, bare path orfile://, only when it resolves to a directory that exists, since a wrong key would label the row confidently and wrongly; when neither does, the row readscursor, and a store with nometa.json(a subagent's has none) is a session without a workspace rather than a broken store. A subagent keeps its own store under the same workspace hash and names its agent insubagentInfo.rootParentAgentId— another store's directory name, which is exactly the session id another row carries — so children fold onto that row as pills labelled bytypeName(their own name is the genericNew Agent), each with the model read from its own store; a child whose named agent is not among the rows stays a row of its own rather than disappearing~/.local/share/goose/sessions/sessions.db(SQLite, read-only) — Goose v1.10.0+:sessions(working_dir,updated_atUTC,session_type='subagent'+parent_session_id; archived + infrastructure typeshidden/terminal/gateway/acpfiltered),messages(role,created_timestamp) for turns/prompts,usage_ledgerfor the token rate (messages.tokensis never written by goose). Per-session activity =updated_atcolumn, not file mtime (shared DB) — a long single-message generation can briefly read Idle sinceupdated_atonly bumps on message insert. Legacy per-session.jsonland theGOOSE_PATH_ROOToverride are not supported~/.factory/sessions/<slugified-cwd>/<session-id>.jsonl— Factory Droid 0.202.0 (the older~/.factory/projects/<project>/<session-id>.jsonlroot is still searched): line 1session_start(id,sessionTitle,cwd); messages are Anthropic-style blocks (role,content[],timestamp). The sibling<session-id>.settings.jsonis not read
Pi relocation: PI_CODING_AGENT_SESSION_DIR is an authoritative direct session-store override and takes precedence over PI_CODING_AGENT_DIR, Pi's global sessionDir setting, and the default. PI_CODING_AGENT_DIR relocates the configuration root; a relative global sessionDir resolves below it. A one-off Pi --session-dir and a project-local .pi/settings.json are not discoverable from this separate process. Set PI_CODING_AGENT_SESSION_DIR to the effective directory in either case.
Session states
| State | Meaning | Derived from |
|---|---|---|
| Needs input (red, popup) | An agent is blocked on the human | Best effort, never guaranteed, and six harnesses can report it at all. Claude has four sources that cover different things rather than ranking cleanly: the bundled PermissionRequest hook is the main one for tool gates, seen directly for ExitPlanMode and in the wild for AskUserQuestion; an actionable Notification-hook POST is the only source for an MCP elicitation or a worker's permission or network request; and a pending input tool seen in the transcript is an opportunistic extra, because Claude Code flushes that record on its own schedule and sometimes not until the gate has been answered; the fourth is about a subagent rather than the session, and is a member on the teams roster that was dispatched and never wrote a transcript, described on the teams-registry bullet above. Codex has one, its own bundled PermissionRequest hook, measured firing interactively with the prompt still on screen. A Codex row says a gate is open and cannot say which, because the event envelope drops the tool name at the hook; it raises a popup on the same terms a Claude one does. Copilot has one too, and it is the only one that needs nothing installed: the CLI writes permission.requested to its own events.jsonl when the dialog opens and permission.completed only once the human answers, so the collector reads a request with no answer behind it as a standing gate. A Copilot row says whether a command or a URL is being asked about, and never which command or which URL. Cursor takes the same route on a different store: a standing tool-call gate leaves pendingToolExecutionContracts non-empty on the newest blob of the chat's SQLite store, and answering it — either way — appends a blob with the map emptied. Its store closes nothing on its own, so the wait is published only while the row is fresh enough to be trusted; the reason is on the Cursor store bullet above. A Cursor row says a permission request is open and how long it has stood, and nothing about what was asked. OpenCode reports parent permission waits through its per-project passive plugin. Pi reports extension UI prompts in persisted sessions through its adapter on 0.85.1; startup trust is excluded. Both require events enabled and lose standing observations on restart. The other four harnesses have no gate detection; Attention reports that coverage gap so a quiet row cannot be read as an all-clear |
| Working (blue) | Actively generating | transcript/subagent/DB activity within the last 90s; detail = in-progress task's activeForm, else running subagents, else thinking where the harness can see the model holding the turn with no tool call open, else last tool, else generating…. Pi is the one exemption from recency, in both directions — see its store bullet above |
| Idle (gray) | Turn ended | anything else — "awaiting your message" from the harness, which the board replaces with how long ago the session was last active. Two situations wear the one word: a turn that ended and nobody read the result, and a session still waiting on a reply that never came. Only a turn-end event tells them apart, so only the four harnesses with turn-end hooks can — Claude Code, Codex, Gemini CLI and Antigravity, and only with their hooks installed. On the other six the row says the answer cannot be known there, rather than guessing at it: it carries Read by scanning: no turn end can be observed here, in the session cell and again on the session page, so a quiet row is read as nothing observed recently and not as a turn that finished |
Dashboard views
The dark-only dashboard opens on Sessions, which keeps active work first and recent history
below it. Above the rows it carries at most one sentence: while the annotation store is on and no
session has been checked for drift, it says so, with no count. Projects, one click away, groups
sessions by the label their harness publishes. Intent log lists every
current board session and retained annotation/discard record once, including sessions without a
project or a goal. It labels the typed goal and each expected outcome line, cached deterministic goal, and each
published workflow goal separately. A missing workflow title supplies no goal. Saved deterministic
evidence is bounded and scrubbed; every line says its currentness has not been checked and gives
its observation time or says that time is unknown. Opening the log never derives a new goal.
Annotation off/loading/error withholds typed evidence while board sessions and other sources stay
visible. Retained words, revisions and readings remain reachable after the session leaves the
board. Discard removes your saved words, typed or adopted, and readings without removing independently labeled sources.
An open log invalidates withdrawn words before loading their replacement, including a discard
made in another tab. The annotation store keeps at most 256 records and sixteen revisions each;
discard records are evicted before retained words, then oldest first. These limits do not apply
to board-only rows. A present session without a project links to its own page, which has no
project to return to.
Each project opens its cockpit:
a left Scope rail selects the project or one exact session. Each row leads with the session
title, with its harness, state and age as a caption beneath; a row whose title was never published says
so in place of the title rather than falling back to an id. Where two sessions of one harness would
otherwise render identically, the row carries the session key so you are not picking blind. A
persistent briefing shows
ASSIGNMENT / EXECUTION / COMMAND, latest evidence, and direction. Assignment retains the
stated goal, its source, and the reason when no goal is available. Command keeps a project session
waiting on you visible above the tabs, with raise and copy-resume controls where supported.
Now pairs current activity with observed session endings, their outcome glyphs and git readings,
plus workflow evidence. Course holds observed state changes, semantic history, and completed
tasks. Decisions shows recorded decisions and their application state; it does not approve
them. Console leads with the operations rail, Delegation, Waiting on you, Capacity and
Tripwires, and keeps how the server was started in one disclosure beneath it, holding the selected
session's optional read-only terminal, the observer-model controls and the status line. The summary
names the state of both capabilities. A capability that is on renders expanded and outside the
disclosure, so nothing you can act on is behind a click. What you typed for a session, and
everything read against it, lives on that session's own page (below), not on a tab here.
Browser-local
human context and old tripwire notes do not instruct an agent and remain inert.
Each tab names its own panel in a line beneath the strip, because two of the four open onto a
heading that does not repeat the label. Course and Decisions also carry a cue on the tab
itself: the number of observed state changes or decisions. A cue reads … while
the record is still loading and · where nothing published the figure, and those two are never the
same claim as a zero. Every cue is spelled out for a screen reader beside its mark.
The composer for a note to yourself sits in the project header, above the tabs, rather than inside
Tripwires. It is the same control wherever you reach it, and it still writes nothing into a session.
Workflow stage conditions are separate typed controls in Course and Projects. Save one declared
stage per exact workflow; the first current entity observed entering it trips the condition once.
First sight, restart and evidence gaps establish a baseline rather than an entry. The card reports
the current coverage and unavailable sources. Rearm permits another alert; saving the unchanged
stage preserves its latch. Projects retains Remove when no source session remains. Up to 64 rules
are saved without eviction, and --diagnose reports their store path and any read failure. No rule
writes into an agent. Notification outcomes report service acceptance or failure, never that a
person saw a banner.
The state-change timeline and delegation figure use the server's local session history, survive a
restart or fresh tab, and caption the window actually observed. The timeline carries its own filter
again: active, all, or decisions. The choice is remembered per session in this browser, so picking
one on a session does not pick it for the next. The cockpit's semantic history is
a separate prototype store; --forget does not delete it. The terminal is also a prototype: it
requires both interaction flags and registration from inside the selected session's tmux pane,
and accepts no input. Its xterm assets are vendored and served from loopback.
Exact session detail is reachable from Sessions, cockpit session links and the Intent log. Under
the session's name and its state (working, needs input or idle), a session waiting on you shows its
question first. Below that the page has two columns, one above the other on a narrow screen. The
Intent and drift panel comes first: an Intent section with the goal and the expected
outcome lines you typed, then a Drift section with the Analyze drift control, what an
analysis reads and sends under it, the reading, any direction you gave after you saved those words
(labelled "Conflict to settle", which asks a question and records no finding), and every departure
on record, asked for or not. The Session activity column beside it holds the agent's current
activity, the recorded request, tasks, subagents, token measurements, how the session landed and
the observed record. On a harness other than Claude Code or Pi the Drift section says "Cargento
can't read work from this harness" where a drift level will go; Analyze drift stays wherever a
reader is named. With no reader on this machine, the reason stands where the control would be. The
page has at most one primary control, Analyze drift. No answer option is ever emphasised: a session blocked on
you gives the primary to the terminal raise when one is offered, and otherwise nothing is primary
while its question is open.
Beside COPY ID, COPY LINK copies the page's own address, which reopens the same session after
a reload. A link to a session the board no longer holds names that session and says it is not in
the current payload.
The route lives in the URL fragment: #n=sessions, #n=projects, #n=attention, #n=intent,#n=project:<encoded-project> with the selected session and then the open tab appended when either
is set, or the full project, harness, and session identity for session
detail, where the project part is empty for a session that published no project label. A project link that ends in the retired held-to tab opens that session's page. Reload, pasted links, and browser back therefore preserve the selected view. Old fragments
that belonged to the retired dashboard normalize to Sessions, as the bare URL does. Open the dashboard at its bare URL;
the retired next query is no longer a dashboard route.
The header reports event-backed running sessions and all observed subagents. When work needs intervention, a button counting
the reported blocks opens Attention. Keyboard shortcuts a, p, and s open Attention,
Projects, and Sessions unless focus is in a form control or Meta, Control, or Alt is held.Escape returns from a session to its project (to Sessions when it has none) and otherwise to
Sessions, under the same focus and modifier rules. Inside a tripwire draft it cancels the draft. Breadcrumbs return through the same
project hierarchy.
MCP tools appear under the service being called rather than their wire name, for exampleLinear · list issues. The full recorded string remains available in the row tooltip.
Attention
Attention is a triage view. At risk names session evidence alongside NEEDS YOU NOW,
CLOSE THE LOOP, and COMING NEXT. The opening brief counts the sessions these categories
claim and describes the remainder as moving, quiet, ended, or without a counted state.
Also at risk, off the session count holds quota pressure, shared display labels and requests
whose session ownership is not established. Those subjects never inflate the session denominator.
Not on this board yet names the capabilities no source supports. Coverage details explain
missing observations instead of treating an unmeasured harness as an all-clear.
NEEDS YOU NOW combines native harness gates with questions registered through ask_operator.
Native permission prompts, plan approvals, and harness questions must still be answered in that
session's terminal; Cargento does not mark them answered on the session's behalf. A Claude Code or
Codex row therefore carries a control that copies the command that harness's own CLI takes to
re-enter that session, so reaching the terminal is a paste rather than a hunt. The other eight
harnesses publish no session id their CLI would accept, and their rows show no control rather than a
guessed command. Where Cargento can reach the terminal a session is running in, the row carries a
second control that raises it, so the hunt across tabs ends in a click rather than a paste. That
reach is narrow: macOS, tmux, a session that started while this dashboard has been up, and exactly
one terminal attached to it. Rows outside it carry no raise control at all, and the queue's coverage
details say how far the feature reached rather than repeating the absence on every row. A raise
changes what that terminal displays; it does not bring the window in front of other applications,
and nothing is ever typed into the session. The item leaves when the harness publishes evidence that the wait ended. A
question registered through ask_operator is different: its offered options are buttons in
Attention and in the exact session detail, and choosing one returns that option to the waiting
agent. Free-form replies are not
accepted, unanswered questions expire, and --no-ask disables this lane.
At risk names detected failed-tool loops, long-running turns, uncommitted work and conflicting
completion evidence. Waiting on the reader takes precedence over those categories; the session
still carries its observed details. Quota pressure and shared labels appear in the separate board
group. A quiet row is never promoted into proof that nothing is waiting. Claude, Codex, Copilot, Cursor, OpenCode and Pi have wait-reporting paths; the coverage detail names
conditions. OpenCode covers parent permissions with its project adapter; Pi covers persisted-session
extension UI prompts on 0.85.1. Neither proves installation or reconstructs a wait after restart.
CLOSE THE LOOP identifies observed stops and session ends after waiting and risk take
precedence. The outcome vocabulary has six readings: stop or end, each with uncommitted work,
clean git state, or unmeasured git state. A stop is not a session end, and neither tells Cargento
whether you read the result, whether commits reached a remote, or why the process ended.
COMING NEXT groups the strongest available next action by project. Both are advisory views of
observed records, not commands sent to a harness.
Usage and rate limits
The capacity strip sits on Session operations below the two session groups, so the first screen
carries values rather than its consent and budget sentences. Each readable window shows
how much of its allowance is spent against how much of its own time is gone, so the two can be
compared: a window a third spent with an eighth of its time gone runs out long before one that is
nine-tenths spent with nine-tenths of its time gone. Rows are ordered by when the budget runs out at
the pace measured so far, which is not the same order as by percentage. Beneath them, what the
remaining budget buys in minutes, and how long sessions have actually worked in the project that
consumed the most measured working time, counting the intervals that opened working and were
seen to close.
Cargento never states which of the two times arrives first: the budget's end and the window's reset
sit side by side and the comparison is yours.
Quota evidence also appears in Attention, raised either by a level at or above 70 percent or by a
pace whose projected end falls before the reset. Codex reads rate-limit snapshots from its own
session files. Claude and Cursor use the harness credential only after the first-run disclosure in
the page is answered, poll their vendor endpoint at most once per five minutes while a page is open,
and never refresh, write, log, or serve the token. Until that disclosure is answered no credential
is read and no request is made, and the strip carries a switch that changes the answer later.
Cursor fetching is macOS-only, and because Cursor publishes a cycle end with no start, its row shows
a level and no clock.
Copilot contributes per-session AI Units from disk but no percentage because its entitlement is not
stored locally. Antigravity can forward quota from its status-line payload:
"statusLine": {"command": "python3 <skill-dir>/notify_hook.py http://127.0.0.1:4553/api/usage", "enabled": true}--no-usage disables credential-backed vendor fetching for a run whatever the page has stored; disk-read evidence remains.
Expired, rejected, missing, or stale quota is withheld rather than rendered as zero. Claude, Codex,
and Antigravity may publish five-hour and weekly windows, while Cursor publishes its monthly billing
cycle. Capacity bars use accent below the window's sustainable pace, amber from 1× pace, clay from 1.5×, and neutral ink when no clock supports a pace.
Model-specific Claude weekly limits remain separate because the tightest model allowance can stop
work before the account-wide window does.
Start
Stdlib-only, Python 3.11+, no dependencies. Resolve server.py relative to this SKILL.md in the
installed plugin and start it detached, so it keeps running after this session ends:
python3 "<skill-dir>/server.py" --port 4553 --daemonOne line on every platform and in every shell — --daemon does the detaching itself, so no
backgrounding operator is involved. It prints the URL, the pid and the log path, and returns.
On native Windows python3 is not a reliable spelling; use python (or py -3). Whichever
interpreter starts the server, reuse it for the commands below.
Drop --daemon to run it in the foreground instead, which is the easier shape for debugging: log
output goes to your terminal rather than to the log file.
Then open the UI:
python3 -m webbrowser -t http://127.0.0.1:4553/Use 127.0.0.1, not localhost: the server listens on IPv4 only, and on some systems localhost
resolves to ::1 first.
Tell the user the URL, that the page updates itself as sessions change, that it keeps running until
stopped, and that popups require the server to be running. Completed-task ages/estimates degrade
where the filesystem exposes no birthtime (Linux, and Windows before Python 3.12).
If the port is busy the server explains that instead of dumping a traceback, and exits non-zero —
under --daemon too. Check whether a dashboard is already there before killing anything:
python3 "<skill-dir>/server.py" --port 4553 --status--status reports one of three things, and never guesses: running (with pid and start time), not
running, or that the port belongs to some other process — in which case it changes nothing.
The server writes ten files, all under ~/.cargento (relocatable with CARGENTO_HOME):cargento-<port>.json, which records the running instance; cargento-<port>.log, where a
detached server's output goes; cargento-dismissals.json, the sessions marked handled;cargento-annotations.json, the goal and expected outcome lines you typed against a session, plus a
text-free record of any session you discarded everything for;cargento-deliveries.json, what became of each notification the board raised;cargento-departures.json, what an unasked reading raised, written only with --unasked-readings;cargento-ends.json, the session ends the board observed, so a session that finished still reads as
ended after a restart;semantic-work-history.json, the project cockpit's own record of what a project's sessions were
observed doing, bounded to a 24-hour window across at most 20 projects;observer/<harness>_<sid>.json, the sidecar an observer panel records when a reader opens one; andcargento-history.json, the history of what the server observed, kept for up to 14 days. A store
that cannot be read is discarded rather than repaired, the board starts empty, and the header names
which reset it was. If you
wire up Antigravity's status line, statusline_hook.py keeps one small memo per conversation in the
same directory so a status line that fires many times a turn posts once.
Marking a session handled
The dismissal API remains available for integrations, but the dashboard does not currently expose
handled controls. A mark made through POST /api/dismiss removes that session from published
counts until anything in it writes again; a subagent write counts. Marks live incargento-dismissals.json. Delete that file to clear them all, call the endpoint with{"clear": false} to restore one, or run with --no-dismiss to leave the store unread for a run.
Recording what a session should achieve
You can record a goal and an expected outcome against one session, in your own words. The
expected outcome is a checklist of up to six lines, each one line of at most 240 characters, and
the goal is one line of at most 240. Both are optional and set independently, and saving one
leaves the other alone. Open the session's page: the fields are the first thing in its drift
block, for a session with or without a project. A line above them says what typing buys: a check
happens when you ask for one, and Cargento never writes into the session. Each box shows how many
of its 240 characters you have used as you type. The goal offers clear only when there is text;
each outcome line has remove, and add a line adds another until there are six, where it stays
on the page, refuses a seventh and says why (replace or merge a line instead). save appears only
when what you typed differs from what is stored, the list saves as a whole, and Escape puts the
stored words back. Each saved line shows where it came from: typed, or added from an entry. A
reading reads each line on its own and shows one row per line.
POST /api/annotate with a harness, a session id and goal or lines (a list of strings, the
whole checklist) does the same thing without the page; expected_revision refuses a list saved
from a view of an older revision, and a seventh line or a line over 240 characters is refused
rather than clipped. It writes a numbered revision; an earlier revision is never edited, so anything citing
revision 1 still means what it meant. Send {"clear": true} to forget a session's words entirely,
or settle_through with a timestamp to mark the directions given up to that moment as settled
against the current baseline. The reply says what happened: persisted is whether the words are
on disk, and outcome is which of six things the store did — stored (a new revision),unchanged (the same words again, so no revision was minted and persisted is still true),refused (the request named nothing the store would take, such as a settle on a session with no
words, or a list saved without the revision it was drafted against, and nothing was written),unwritable (the write failed, so the words are held for this run only; or the entry could not
fit the store even alone, so nothing was written and nothing is held), untrusted (the store
file exists and could not be read whole, so nothing was written over it, and the page says so
in place of "nothing typed" until the file is moved or repaired) or unreadable (this session's
own entry was saved by a build that can read more than this one, so it is kept as it was and not
saved over). A save may send origins, the saved position each line came from, so two lines that
share text each keep their own source. The page draws a different sentence for each, and
re-saving the same words never mints a revision.
They are held in cargento-annotations.json, bounded by how many sessions carry words and how many
revisions each keeps rather than by age, because a session still on the board should not lose what
you asked of it because time passed. Delete that file to clear them all, or run with--no-annotations to leave the store unread and unwritten for a run.
Binding is per session, and the board says when it is not exact. Where a harness publishes only a
short identity prefix, another session sharing that prefix would share these words, and the row says
so rather than leaving you to assume otherwise.
Below the two fields the Drift section runs in reading order: Analyze drift, the reading, a
later direction and the notes about your saved words, and the departures on record. The agent's
current activity leads the Session activity column beside it. The departures section appears only with --unasked-readings on, or once a reading or a
departure is on record. How it landed and the observed record follow
the session's own facts further down. The record is last because it is the longest block here and
its absence sentence is one of four that used to arrive before you reached the reading. It lists every observed entry naming that session with its own type and the source that
published it, and states the limit under it: demonstrated work results are read on Pi, and on
Claude Code the record also lists the checks a session ran and the files it wrote, each result as
the tool reported it and counted from the whole transcript read. A reading sends those checks, with
the last 180 characters each printed and the paths of the files written, only after you allow tool output for the destination named
beside the button, and sends none where Cargento cannot name that destination. On every other harness those
entries are instructions, dispatches and gate decisions and never an inspected file, test or
deliverable. Your words also appear beside the goal the harness published,
in the project view's stated goal block, each on its own row so the two claims are never merged, and
the derived row says when the directive was observed.
Long caveats keep their claim on the page and put the rest behind a summary you can open, on both
the session page's drift block and Console. Nothing is removed by that: an opened one stays open when the board redraws,
and a closed one still holds every word it was written with.
A later direction gets its own block, labelled "Conflict to settle". Every instruction you gave after your newest save is listed
there with its age, and nothing there decides whether it changes what you asked for: that is yours,
and Cargento does not write into the session either way. Marking the baseline as still applying
settles them through the newest one shown, which is what settle_through records. The retype
control only moves the caret to the goal field, because Cargento cannot author your words. While a
later direction is unsettled a reading states no departure at all.
DEPARTURES RAISED TO YOU is where a raise is reviewed, and the one place on the session page it
appears; it keeps two collections apart. One
holds what a reading you asked for raised; the other holds what the checks run while you were away
raised, with the annotation revision each read against and where its evidence stopped. Where the
words have since moved to a later revision the row says so, in the same wording the reading block
uses, and a raise whose revision was never recorded is not given today's number. Under both sits
what became of that lane's raise, and a raise with no delivery record on file says that rather
than showing nothing. Each raised constraint carries what a later check found, derived from later
checks rather than from a second reading: the usual answer is that no later check has read the
session, and where one has, the sentence says what it found and never that the raise caused it. The
figures beneath are one line per collection — the departures the reading raised and the departures
the away checks raised, each counted from the rows shown beside it — then the board's raises,
attempts and hand-overs, with no arithmetic between any of them. A count identifies a session worth
reading and settles nothing else, and where the away checks have never read the session the figure
says it was not measured rather than reading zero. The clear beside each box empties that box, and
the save after it keeps every earlier revision, so the raises quoting those words stay. Discarding
the whole annotation withdraws them: the quotations go from the departure record too, and what
stays is the fact that a check ran and a record of the discard itself, holding when it happened
and no text at all. That record is why a discarded session still has a line in the Intent log,
and why the board never describes it as a session nobody typed against. That act is discard everything, under the two boxes and offered only
where a revision is stored; it takes two presses, and between them the board names what it will
delete and what it will withdraw. That sentence is the control's own description, and it is
written to the page's live region when the first press arms it, so it reaches a reader who is not
looking at the screen. Beside each departure sit the command that resumes the session, on Claude
Code and Codex, and the raise that selects its tmux pane, whatever state the session is in; the
raise does not bring the window forward. Where either is missing the section says why once. Neither
sends anything to the session. The Intent log carries the same raises a line at a time, and keeps
them after the session leaves the board.
Sessions labels your typed goal beside NOW, or shows your latest prompt from Claude Code or
Codex when its source permits that; other harnesses show typed words only. A slot without a typed
goal opens the session page with the cursor in its goal field. Showing a prompt does not save it.
A recorded departure adds a Drift mark with its age and moves the session after blocked sessions
and before working ones, even if it is idle; it does not add to the Active now figure. Earlier
revisions say the words have moved, and legacy readings without a reading time say the age is
unknown. Consistent readings add no mark or rank, and there is no drift total. With annotations
off the slot has no link and there is no Drift mark; the screen explains that once.
Nothing here evaluates on a cadence. Drift marks show departures already on record, from a
reader-requested reading or the optional unasked lane. With nothing typed the block says there is nothing to read against; with the observer
model off it gives that reason; otherwise it states what a reading may and may not read and offers
one control, Analyze drift. When it cannot run it stays on the page, refuses the press, and
names one next step, except where no reader is available on this machine: then the reason stands
where the control would be. The accepted case review enables that control; the evidence stays readable
whether or not you ask for a reading. A press starts an analysis the server runs in the background,
and while it runs the control is replaced by an "Analyzing drift" box listing its real steps
(preparing what is sent, waiting for the named provider, checking the reply), the one under way
marked, with the attempt count and disclosure still beside it. The box comes from the published
board, so a reload or another tab shows the same analysis at the same step, and a second press
starts nothing. The result, or why there is none, replaces the box when it is stored; an analysis
a stop or restart cut short is recorded as an interrupted attempt. The box offers Cancel, which stops the call and records a cancelled attempt that still counts, or one that spent nothing when it landed before anything was sent. A failed request says its result could
not be confirmed and is never retried automatically. The counter reports recorded model requests, so a refusal before the model runs
does not increase it.
Notifications
Three paths manage Claude's needs-input state, and a question registered through the ask lane is a
fourth alert rather than needs-input state. On macOS the server can deliver native notifications
through osascript, even with no dashboard tab open. Gate alerts have a 60-second per-session
cooldown and a 15-second lane-wide floor; questions have their own 15-second floor. Linux and
Windows have no native backend in this release; with a dashboard tab open, the page can deliver
browser notifications after permission is granted, for a session that starts waiting on the human
and for one that was working and has gone quiet. The dashboard shows every published item in
Attention on all platforms.
Native alerts fire on the transition into needs-input, not on every refresh. Questions notify on
arrival. Idle nudges (idle_prompt) can notify without marking the session blocked. The browser's
quiet nudge is close to that but not the same event: it has no hook of its own, so it fires on the
published row ceasing to look busy, and a row reaches that two ways. Where the harness's lifecycle
hooks are installed its own turn-stop reaches the row within a few seconds; everywhere else the row
turns over only once its writes have paused for as long as the working threshold. Its wording says
the session has gone quiet rather than that it is waiting on you, which is the honest reading of
either path.
Notification delivery is best effort; the dashboard's observed state remains the source to inspect.
Cargento can also check an annotated session against what you asked for without being asked, and
raise a departure while you are away. Off by default, behind --unasked-readings, refused by--no-observer-model whatever else is set, and it is the only thing here that spends your model
capacity with nobody watching. It checks on an observed state
change rather than every turn, it raises a departure and never a reassurance, and it stops at a
per-session and a per-day limit. It reads your typed goal alone, never your expected outcome
lines, and a session with lines and no goal is not checked this way. A spent limit is said out loud: a session nobody checked and a
session checked and found clean are different sentences on the board, because you were not there to
know which one you are looking at. Turning it off stops new checks and retracts nothing: what was
already raised still shows on the session, in the review and in the Intent log, under a line saying
nothing new is being checked.
What became of an alert about a session is now recorded, one entry per raise, and that session's
panel prints it. There are five outcomes and they mean different things: handed to this machine's
notification service, the service returned an error, the command could not be started at all, the
call did not return inside its time limit, and this platform has no backend in this build. A raise
whose subject carries no session id records nothing, since the record is keyed by session. None of them is "seen". osascript exits zero under Do Not
Disturb and with the hosting application's notifications switched off, so a hand-over says the
service accepted the request and nothing about whether a banner was drawn or anyone was at the desk.
A raise on a platform with no backend spends no cooldown, so the next real transition is still
eligible. Beside the outcome, the panel says whether a dashboard tab has reported a notification lane
of its own and how long before the raise it did so. That is a statement about a tab, not about a
raise: a tab that opens reports and a tab that closes does not, so its absence never means the alert
missed you.
- Transcript detection — an open
AskUserQuestionorExitPlanModeflips the session to Needs input on the next collection, when the record has reached disk. Claude Code buffers it and may not write it until the gate is answered, so treat this as an opportunistic early signal rather than a source to rely on (an open dashboard tab is what drives collections, so keep one open). When the record is there, the row shows the question itself, or a plan's first line, rather than the tool's name; when it is not, the row still says a question is open but cannot say which. Both readings are normal for the same session. There is also a window of up to 90 seconds after a turn starts where a live event overlay reports Working and the question does not show at all, even though it was parsed. - Lifecycle hooks —
NotificationandSessionEndhooks in user settings (~/.claude/settings.json) POSTing their payloads tohttp://127.0.0.1:4553/api/notify. Notifications cover permission prompts and idle waits, even with no browser tab open. The structurednotification_typedecides whether a notification is actionable. Idle nudges (idle_prompt, message "Claude is waiting for your input") pop once but never mark the session blocked; authentication, completion and computer-use status notifications do neither; permission prompts, MCP elicitation dialogs and a worker's permission or network request create Needs-input state. A type not on either list is treated as actionable, so a notification kind added upstream surfaces rather than disappearing.SessionEndclears a standing hook when Claude exits cleanly. These hooks are NOT installed by the plugin — if the user wants path 2, offer to add them to their~/.claude/settings.json:
Use the bundled notify_hook.py (next to server.py) rather than a curl one-liner. The one-liner is POSIX-only end to end — single-quoting, /dev/null, || true, and --data-binary @- all fail in cmd.exe, and Windows PowerShell 5.1 aliases curl to Invoke-WebRequest and has no ||. One interpreter invocation behaves the same in every shell, exits 0 even when the dashboard is not running, and refuses to POST anywhere but loopback.
"hooks": {
"Notification": [
{"matcher": "", "hooks": [{"type": "command", "command": "python3 \"<skill-dir>/notify_hook.py\"", "async": true}]}
],
"SessionEnd": [
{"matcher": "", "hooks": [{"type": "command", "command": "python3 \"<skill-dir>/notify_hook.py\""}]}
]
}On native Windows use python instead of python3, and a Windows path. Pass a URL as the first argument for a non-default port: python3 "<skill-dir>/notify_hook.py" http://127.0.0.1:9999/api/notify.
After adding it (or after any settings change that breaks it), tell the user to open /hooks once or restart Claude Code to reload hook config.
Simulate for testing:
echo '{"session_id":"<id>","message":"test"}' | python3 "<skill-dir>/notify_hook.py"Lifecycle events — the plugin's own bundled hooks at
hooks/hooks.json, so there is nothing to add to a settings file. They forward general lifecycle events to/api/events/claude: session started and ended, prompt submitted, turn stopped, permission requested, a tool run finishing, subagent started or stopped, tasks changed, compaction finished. Where path 2 sets one piece of side state, this drives the session's Working, Needs-input and Idle state directly, so the board reacts to a turn starting instead of waiting for the next scan.Nothing to install: enabling the plugin is enough. Note that a session's overlays are retired when it ends, which is correct and means a one-shot
claude -prun shows no event-driven state by the time it exits. Two things survive that retirement, both held outside those overlays on purpose: the mark that its turn ended, so a run that finished and was never read can still say so, and the mark that the session itself ended, so a session that is over reads as over rather than as one sitting at its prompt waiting for you. A session with no end recorded is not known to be running — only aSIGKILLends a Claude session silently, but a harness with no adapter, a session that started before the server did, and--no-eventsall look the same from here.Only add the hooks by hand if the user runs
event_hook.pyoutside the plugin, for instance against a checkout. In that case:
"hooks": {
"UserPromptSubmit": [
{"matcher": "", "hooks": [{"type": "command", "command": "python3 \"<skill-dir>/event_hook.py\" claude", "async": true}]}
],
"Stop": [
{"matcher": "", "hooks": [{"type": "command", "command": "python3 \"<skill-dir>/event_hook.py\" claude", "async": true}]}
],
"PermissionRequest": [
{"matcher": "", "hooks": [{"type": "command", "command": "python3 \"<skill-dir>/event_hook.py\" claude", "async": true}]}
],
"SessionEnd": [
{"matcher": "", "hooks": [{"type": "command", "command": "python3 \"<skill-dir>/event_hook.py\" claude"}]}
]
}The harness name is the first argument and is required. Pass a port second for a non-default instance: python3 "<skill-dir>/event_hook.py" claude 9999. Note that this differs from notify_hook.py, which takes a whole URL.
Claude Code and Codex after-tool hooks also report fixed command shapes: force push, hard reset,
DROP TABLE in supported SQL-client forms, and recursive delete outside literal temporary prefixes.
Session detail and Attention show reports, not proof that an action succeeded. Missing hooks and
unmatched commands can look the same. Reports stay in memory for this run: newest 1,000 globally,
20 per session, at most 24 hours. Restarting clears them; duplicates and reordered delivery are possible.
Matching uses a nominal 5 ms best-effort wait, separate from startup and transport. Exact literal forms
and exclusions are in the project's security documentation; arbitrary shell syntax is not interpreted.
This path needs no configured secret. Each run of the dashboard generates its own capability and publishes it in its state file, which the hook reads; nothing is stored between runs. With no dashboard running the hook reads one small file and exits 0. A dashboard started with --no-events publishes no capability, so the hook stays silent.
Codex
Codex sessions report through the plugin's own bundled hooks at hooks/codex-hooks.json, so there is nothing to add to a settings file. Codex asks the user to trust each hook once, and silently skips any hook it has not been asked about, so after installing the plugin tell the user to approve Cargento's hooks when Codex prompts. Until they do, Codex rows keep the same scan-only behaviour they have today.
Eight events are registered: SessionStart, UserPromptSubmit, PermissionRequest, PostToolUse, Stop, PostCompact, SubagentStart and SubagentStop. Seven have been seen firing in a real session; PostCompact is the exception and is mapped on Codex accepting Claude's schema rather than on a capture, which is worth knowing before trusting a compaction row. PreToolUse fires too and is deliberately not used, because PostToolUse reports the same turn once the store has actually changed.
PermissionRequest is the one that makes a Codex row go red, and it is worth knowing what it does and does not do. Codex blocks the tool call on the hook and applies whatever the hook returns, so this is the one event where a reporting adapter could gate the user's own work. It cannot here, and this is measured rather than reasoned: a hook on this event that printed nothing and exited 0 let the approval prompt reach the person, who approved it, and the command ran. Empty output is how Codex is told the hook has no opinion. Cargento never answers a Codex gate.
Two honest limits on that. The argument covers what the adapter writes, and a failure before it starts — no interpreter, an unreadable file — exits non-zero instead, which is a channel nothing here has measured. And the hook runs before the prompt is drawn, so its cost lands while the person is still waiting to be asked rather than after; it is one short-lived process, and the same one seven other Codex hooks already run.
Pi
Pi reports extension UI prompts in persisted sessions through the bundledpi_extension.js adapter. Validated on Pi 0.85.1: select, confirm, input, editor
and custom prompts enter the waiting queue; answering or canceling clears the wait.
Sessions shows the elapsed wait without publishing the prompt's title or answer.
Stock project trust and prompts before persistence are outside coverage. A missing
adapter, disabled event lane, or expired reading leaves block state unavailable;
restarting the dashboard does not recover a standing prompt.
Follow Pi setup
to install the adapter.
Copilot
Copilot needs nothing installed at all: its own store records the permission prompt, so the collector reads the gate straight off events.jsonl with no hook, no adapter and no settings file in the way. Cursor is the other harness that works this way — see below.
The CLI writes permission.requested when the dialog opens and permission.completed when the human answers, joined on a request id. A request with no answer behind it is somebody being held up, and that is what turns the row red; the answer clears it on the next refresh, whichever way it went. Measured on 1.0.78: the request was on disk in the first frame the dialog was visible in all four timed arms, and prompts stood 36, 44 and 48 seconds on a shell gate and 23 on a URL one before anyone answered.
A new prompt typed into the session also clears it, and that is the only other thing that does. The answer is never written when the terminal is closed on the dialog or the process is killed at it, so without this a resumed session carries the dead request until it ages out of the activity window — red, and ranked ahead of the work it is visibly doing. A request also has to have stood a couple of seconds before it counts, which no prompt in front of a person ever notices and which keeps a headless run's auto-denial, written and answered a millisecond apart, from being caught half-written and read as a wait.
The row says a command is being asked about, or a URL, and stops there. It never carries the command itself, the URL, or the model's stated reason for wanting either, and a typed refusal does not reach the board.
Four limits worth knowing. A session directory with no events.jsonl is not a Copilot row in any state, gate or no gate: 3 of the 7 directories on the machine this was measured on had none, and the collector cannot see what is not written. A headless run (copilot -p without --allow-all-tools) auto-denies every gated call in about a millisecond and raises no wait, which is correct rather than a gap, since no human was ever going to be asked. Whether an unanswered prompt eventually gives up on its own is not something this has measured; the longest observed gate ran 48.5 seconds and was answered.
The fourth is the one to know before trusting a red Copilot row overnight. A session killed at its dialog looks exactly like a person who has walked away from one — the store records the question, never the process going away, and there is no lock file or pid beside it to ask. So a row can say a prompt has been standing for hours when the terminal it was in is long gone, and the wait ends when the session drops out of the activity window rather than when the prompt does. Answering it or typing to the session is what clears it; nothing else can.
Cursor
Cursor needs nothing installed either, and for the same reason: its own store records the prompt. While a tool call sits in front of you, the chat's store.db carries a non-empty pendingToolExecutionContracts on its newest blob, with pendingToolCallStartedAtMs beside it saying when the wait began. Answering — approving or refusing, it makes no difference — appends a blob with the map emptied, and the row clears on the next refresh. Measured on 2026.08.11-e8db854 across four interactive allowlist-mode sessions, with gates standing 15 to 244 seconds.
The row says a permission request is open and how long it has stood. It never says what was asked. The contract entry beside the map names the tool three different ways, and none of them is read.
The hook route is deliberately not taken. beforeShellExecution runs before Cursor decides whether to ask you, and feeds into that decision, so a hook that reported a wait from there would be reporting a prompt it had just caused — on every command, whether or not anybody was ever asked.
Two limits. The first is the one to know before trusting a red Cursor row: the store is append-only, so a session abandoned at a prompt keeps its pending contract forever, and 2 of the 10 stores on the machine this was measured on still read as waiting 29.4 hours after their process had exited. Nothing in the store records a process going away, so Cargento publishes the wait only while the session is fresh enough to be worth a row at all — the same freshness its title is gated on. That draws the line at the edge of the activity window rather than at the moment the terminal closed, which is as close as a passive reader can get.
The second is subagents. A Cursor child keeps its own store and appears as a pill on its parent's card, so a gate standing in front of a child is not read and does not turn anything red. Only a shell prompt was measured; MCP prompts, plan decisions and question tools were not, and whether they write the same record is unknown here. And where a store's blobs are encrypted — every one measured was plaintext, but the store carries a key for it — the record does not read and the session reports no wait.
OpenCode
Copy {baseDir}/opencode_plugin.js into each project's .opencode/plugin/cargento.js, then
start a fresh OpenCode session. Remove that copy and start a fresh session to uninstall.
The passive plugin reports parent permission.asked and matching permission.replied events;
Cargento never answers them. Use CARGENTO_PORT for a local port other than 4553 and the sameCARGENTO_HOME as the dashboard when its state lives elsewhere. Missing state, disabled events,
transport failure and restart can lose observations. Child permissions and question events remain
unclaimed; no current event means block state is unknown. Measured on official OpenCode 1.18.30.
Antigravity
Antigravity has two paths, and they do different jobs.
Hooks are bundled with the plugin (hooks.json at the plugin root), so nothing needs configuring for them. They fire after a tool step and after a model invocation, and they only tell the dashboard the store probably moved, which keeps an Antigravity row fresh without waiting for the next scan. They deliberately claim nothing about whether the agent is working or idle: Antigravity's hooks can fire several times in one turn, so treating them as turn boundaries would flap the row.
Working and Idle state comes from the status line, which cannot be plugin-bundled, so that half is opt-in. Point it at statusline_hook.py, which forwards both the quota figures and the lifecycle state:
"statusLine": {"command": "python3 \"<skill-dir>/statusline_hook.py\"", "enabled": true}Pass a port as the first argument for a non-default instance. This replaces the older notify_hook.py <url>/api/usage line, which still works and still feeds the quota band; the new script additionally drives Working state and sends the quota block alone rather than the whole status-line document, which carries the account email and the transcript path.
Two honest limits. The conversation id is blank until a conversation exists, which is measured: the return to Idle often cannot be reported, so the Working state carries a deadline and the collector decides again after it lapses. The second one is now settled, and not the way an earlier note here guessed. The status line does report a pending tool confirmation: driven to a real permission prompt, it pushes a confirmation-pending flag beside a tool_use state, and in the one arm that recorded it, every flagged push carried an id that names the session. An Antigravity row still does not report Needs input, for a different reason. The status line is a repeating render rather than an event stream, so the flag arrives in a short burst as the dialog opens — two or three pushes — and then not again until something redraws the pane, and the ordinary Working and Idle pushes that follow would clear the wait rather than leave it stuck. Reporting it needs a precedence rule that does not exist yet. An agent question raises no flag at all.
Paths 2 and 3 are complementary and can both be installed. Keep Notification on notify_hook.py: Claude Code can emit an input-waiting notification for a session that then carries on running, so it stays revocable side state rather than an authoritative state change. SessionEnd is useful on both, which is why it appears twice above.
Options
| Flag / URL | Effect |
|---|---|
--observer-model / --no-observer-model |
Offer optional Codex goal summaries, or refuse every model call for this run (refusal wins, including unasked checks). Goal summaries are off by default and retain their separate browser consent. On a goal-less Claude Code or Codex session, Analyze drift adopts the published latest prompt when its source time is available. The goal field also offers the first prompt and adoption without checking. Adopted goals say "from your prompt" and never enable unasked readings; editing the goal makes typed words. Reader-requested readings need no startup flag: the first Analyze drift presents its disclosure and Allow and analyze. The answer is remembered across tabs and restarts; Turn off readings revokes it. They spend Codex capacity and are capped at twelve reserved or actual attempts per rolling twenty-four hours. Failed attempts are not refunded; off/on and --forget do not reset that budget. Each prompt is bounded to 16,384 redacted bytes, with one call in flight per session and a 60-second timeout. Quota consent authorizes neither path. |
--interaction-origin-session <harness:sid> / --interaction-origin-registration-file <private-file> |
Enable the prototype terminal for one exact session only when both flags are supplied. The generated private file supplies the registration capability; registration must happen inside that session's tmux pane. Output is read-only and bounded; no terminal input is accepted. |
--port N |
Change port (default 4553; valid range 1–65535). If the port is busy, check --status first — a running dashboard may already be there; don't kill it blindly. |
--host A |
Bind address: 127.0.0.1 (default) or 0.0.0.0, IPv4 only. Nothing narrower — a single-interface bind is refused rather than half-supported, because --status, --stop and the hook forwarders all reach the dashboard over loopback and such a bind does not answer there. Nothing authenticates a remote reader: anything that reaches the port reads every session's titles, prompts and paths, and can answer a question a session is waiting on. Prefer ssh -L 4553:127.0.0.1:4553; use --host only on a network the user would hand the transcripts to. |
--daemon |
Detach and keep running after the starting session exits. Prints the URL, pid and log path. |
--stop |
Stop the instance on --port over /api/shutdown. Returns once the port is free, so a restart on the same port works. |
--status |
Report whether Cargento is on --port: running, not running, or the port belongs to another process. Exits 0 only when running. |
--forget |
Clear reading permission without erasing unexpired spend timestamps. Delete the local history store and the session-end store, remove the record of any session you discarded everything for, and exit. It deletes nothing you typed: a discard record holds when the act happened and no text. A one-shot command like --stop and --status, not a switch for a run: what it does is not undone by running the next command without it. Refused while a dashboard answers on --port, and the refusal names all three stores. Two of them a running instance would undo: it would write the deleted history records back, and it would go on publishing the ends it observed. The annotation store it would not — every write there re-reads the file, so a running board picks the sweep up on its next collection — but the command refuses whole rather than in parts, so the discard records are still there too. Stop it with --stop first. |
--window-hours H |
Sessions idle longer than H hours are hidden (default 24) |
--diagnose |
Print where each harness's data was searched for and what was found there, then exit. Use this first whenever a harness the user expects is missing — collectors skip broken or absent stores silently, so a wrong path looks exactly like an idle machine. Add --json for machine-readable output. Reads local paths only; it writes nothing and transmits nothing. |
--no-spacedock |
Do not read Spacedock workflow definitions. Claude role metadata remains, but stage strips do not; saved stage conditions remain readable and suspended. |
--no-tripwires |
Disable workflow stage conditions and all reads or writes of their saved store. |
--no-usage |
For this run, never fetch vendor quota over the network and ignore quota a harness pushes in, regardless of the setting stored in the dashboard. Quota a harness writes into its own store (Codex, Copilot) still shows. |
--no-irreversible |
Disable command-shape matching, report ingress and publication for this run. --no-events also disables it. Ordinary lifecycle hints remain available with only this flag. |
--no-events |
For this run, do not accept lifecycle events: no event overlays, no coarse store probe, no capability published, and the fixed-interval scan keeps the board warm instead. The session-end store is neither read nor written, since the coordinator is its only writer, so a session that ended before this run reads as quiet. The rollback switch if event acquisition misbehaves. |
--no-git |
For this run, do not run the end-of-session git probe in any session's working repository. No git command runs at all, and every row's dirty and changed stay empty. Empty means no reading available: never attempted (including refused), attempted without a usable result, or a reading retired after resumed work. It does not mean a clean tree. |
--no-dismiss |
For this run, do not read or write the store of sessions marked handled: every marked session comes back onto the board. The rollback switch for the dismissal store Cargento writes on your behalf. |
--no-annotations |
For this run, do not read or write the goal and expected outcome lines you typed against a session: nothing is shown, nothing is saved, and the session page's Intent and drift panel offers no field; its Analyze drift control stays on the page, inert, and says why. The rollback switch for the one store holding prose you composed. |
--no-ask |
For this run, do not let a session ask the reader a question: the register, poll and answer routes refuse and the page offers no control. The rollback switch for the ask lane. |
--no-focus |
For this run, do not raise a session's terminal: no focus command runs, no terminal identity is recorded, and the page is handed no capability to ask with, so it offers no raise control. --no-events turns it off as well. The rollback switch for the terminal raise. |
--no-history |
For this run, keep no local history of what the server observed: nothing is written and an existing store is not read back, so the board opens with no memory of earlier sessions. |
--no-reach |
For this run, disable outbound reach nudges: no webhook URL is resolved and no off-machine nudge is posted. |
--reach-url URL |
Webhook URL for off-machine reach nudges when sessions need input or finish unread while away from the desk. Overrides CARGENTO_REACH_URL and ~/.cargento/reach_url. |
--no-quiet-hours |
For this run, disable quiet hours notification suppression: notifications fire regardless of local time or configured window. |
--quiet-hours WINDOW |
Local time window (HH:MM-HH:MM) during which non-urgent notifications (popups, unasked checks, tripwires, reach nudges) are suppressed, except direct questions. Overrides CARGENTO_QUIET_HOURS and ~/.cargento/quiet_hours. |
--history-days N |
How long the local history keeps an observation, in days (default 14). Eviction is age first, so narrowing this drops what falls outside the window and widening it again brings nothing back. Zero or negative is refused. |
--history-max-bytes N |
The size cap on the local history store, in bytes (default 1048576). It is the read cap too: a file larger than it is discarded unread rather than parsed. Zero or negative is refused. |
http://127.0.0.1:4553/?all=1 |
Show all sessions ever, including idle ones |
/api/data |
Raw JSON, same data as the UI |
/api/health |
Liveness and identity (pid, port, start time). Scans nothing, unlike /api/data. |
/api/overlays |
Diagnostic: the live event overlays behind each session's state, with their arrival order and timings, plus a record of every time an event overruled a session the dashboard had read as waiting. Use this when a row's state disagrees with what the agent is actually doing and you need to know whether an event said so or never arrived. Empty is a real answer. Absent under --no-events. |
/api/stream |
Server-sent events, one per new revision. This is what keeps an open page current, so it refetches when something changed rather than on a timer; one tab per browser holds the connection. A browser without EventSource falls back to polling. |
POST /api/shutdown |
Stop the server. Loopback-only, with the same origin checks as /api/notify. |
POST /api/usage |
Receive a harness's own quota, forwarded by its status-line command (see Usage and rate limits). Loopback-only, same origin checks. Stores in memory only. |
POST /api/dismiss |
Mark one session handled, or with {"clear": false} put one back. Body is {"harness", "sid"} and carries no timestamp — the watermark is the server's clock. Answers persisted: false when the store could not be written. 503 under --no-dismiss. |
POST /api/tripwire |
Save, Remove or Rearm one workflow stage condition. Requires action, opaque id, declared stage and expected_revision; stale revisions return 409, failed writes 503. Disabled by --no-tripwires. |
POST /api/annotate |
Record a goal or the expected outcome checklist against one session, clear both, or settle a later direction; the paragraph on what you asked for above has the body. Answers persisted (are the words on disk) and outcome, one of stored, unchanged, refused, unwritable, untrusted or unreadable, so a refused request and a failed write are told apart and re-saving the same words is not reported as a new revision. 503 under --no-annotations. |
/api/cleared |
The sessions marked handled: a harness key, a session id and when each was marked, and nothing else. 503 under --no-dismiss. |
/api/annotations |
Every session you have typed a goal or an expected outcome line against, including sessions no longer on the board, with what an unasked check raised against each. Serves the words themselves, so it is read when the Intent log is opened rather than on the refresh loop. 503 under --no-annotations. |
POST /api/reading |
Ask for one reading against the words typed against a session. Body is {"harness", "sid", "provider", "press": true, "observer_model": 1}, where provider is the one the page's route for that harness named (codex or claude); the first authorized press adds "allow": true, which allows that provider only. {"consent":"off","press":true,"observer_model":1} revokes permission. Bodies are capped at 12,288 bytes, loopback-only and refused on a document navigation. 503 under --no-annotations, the explicit model off switch, a closed reading gate, or unavailable permission storage; 403 without consent; 429 at the rolling cap, with its retry time. 409 while that session has a reading in flight. 409 with "reason": "provider-changed" and the current route when provider is missing or no longer the one that would run: nothing was saved or spent, so read the route's disclosure and press again. 503 with the route and its reason when no provider can read that harness on this machine. 200 with produced: false when no annotated session by that name exists. |
Interpretation notes (share with the user if asked)
- A quiet session is not proof nothing is waiting. Claude, Codex, Copilot, Cursor, OpenCode and Pi can
report waits within their stated coverage. Attention names missing coverage and collector errors rather than turning them into
zero. A question registered throughask_operatoris separate and can belong to any harness. - Age on a task is creation to now, or creation to last update when complete. Claude task files
expose timestamps; Codex plan steps do not, so Codex task rows show no borrowed age. - Output rate is output tokens over the last ten minutes, not billed input or cache tokens.
Claude, Codex, Pi, Gemini CLI, Antigravity, and Goose expose usable generation totals. Missing
harnesses make aggregates a floor, never a measured zero. - Model and consumption come only from records a harness writes. Claude, Codex, Pi,
Antigravity, Copilot, and Cursor can report a model. Copilot alone provides per-session AI Units;
those units are not converted to dollars or summed across harnesses. - Current-turn estimates compare elapsed generation time with completed turns from the same
session. They are withheld when there is no defensible sample. A quiet gap longer than five
minutes re-anchors elapsed time so a permission wait is not counted as generation. - Loop detection reads one request three ways, and the note says which one fired. Four
consecutive failed Claude tool calls is the tight run. Six failed calls in the request however
they were spaced, and more failures than successes, is the total, which no success resets, and it
catches the request that fails three times, succeeds once, then fails three more, where the run
alone reads as clean. Three failed calls with none succeeding at all is a request where nothing
has worked yet. The count alone is not enough for the second of those: six scattered failures
among two hundred successful calls is ordinary work, so the failures have to outnumber the
successes as well. The last two readings are withheld entirely when the scan started partway
through a request, since both describe the whole of it. Other harnesses do not expose a verified
failure field, so Cargento does not infer any of the three there. The pattern is evidence, not proof; iterating on a failing test can look
the same from outside. - Project is the last two path segments when the working directory is known. This keeps sibling
repositories distinguishable without printing an entire path. The fallback harness label is not
treated as proof that two sessions share a directory. - Session identity includes harness, project, and the complete session id. Displayed ids widen
beyond eight characters when nearby rows would otherwise collide. Detail routes carry the full
identity so similarly prefixed sessions cannot alias. - The line beneath a session title is the strongest current instruction Cargento can attribute:
newest operator request, current agent statement, or an earlier substantive request when the
newest prompt is only a continuation such asproceed. It carries its source and age and is
absent when attribution would be a guess. …REDACTEDmeans a credential-shaped value was replaced before it reached the page. The
original still exists in the harness transcript on disk, so rotate a real credential rather than
treating the marker as remediation.- Spacedock evidence is bounded and freshness-gated. Stage, entity, and delegation readings are
omitted when the workflow definition or measured window cannot support them. A missing strip is
not a claim that no workflow exists. - "Source not fully read" on a row means Cargento opened that session's store and could not read
every part of it, and it names which readings are missing. What the row still shows was read
normally; the named readings are absent rather than zero, which matters most for a token rate,
where nothing else separates a missing measurement from a measured zero. The five harnesses whose
store is a database can report it: Antigravity, Copilot, Cursor, Goose, and OpenCode. A row
without the line is not a promise that everything read, only that nothing reported otherwise.
Stop
python3 "<skill-dir>/server.py" --port 4553 --stopThe command uses POST /api/shutdown and waits for the port to come free before reporting success,
so you can start a fresh instance on the same port straight afterwards. It also clears a state file
left behind by a server that was killed,
and exits non-zero without touching anything if the port turns out to belong to another process.
Last resort, for a server wedged badly enough that it no longer answers HTTP. Match only
listening sockets — without that filter these also match connected clients, including the
browser's own network process:
# macOS, Linux, WSL (lsof is absent on many minimal images — fuser is the fallback)
lsof -ti tcp:4553 -sTCP:LISTEN | xargs kill
fuser -k 4553/tcp# Windows PowerShell
Get-NetTCPConnection -LocalPort 4553 -State Listen |
ForEach-Object { Stop-Process -Id $_.OwningProcess -Force }:: Windows cmd, typed at the prompt — inside a .bat file write %%a for %a.
for /f "tokens=5" %a in ('netstat -ano ^| findstr "LISTENING" ^| findstr ":4553"') do taskkill /PID %a /FSubstitute the port the server was actually started on. The cmd form matches any port whose digits
contain the string (:4553 also matches :45530), so prefer the PowerShell form on Windows.
Common mistakes
- Do not widen the bind in code. The default is
127.0.0.1and--host 0.0.0.0is the operator's own opt-in (see the flag table above), never something to set for them: anything that reaches the port reads every session's titles, prompts and paths. - A harness the user expects is missing: run
--diagnosebefore guessing. It distinguishes "no store here", "store present but unreadable", and "store read, no recent sessions" — the collectors cannot, because they skip unreadable stores silently by design. - Headless Linux (no graphical session):
python3 -m webbrowserandnotify-sendboth need a desktop. Reach the dashboard over SSH withssh -L 4553:127.0.0.1:4553 <host>and open it locally. If running it under systemd, use a user unit — a system unit expands~to/rootandProtectHomehides every harness store. - Flatpak- or Snap-installed harnesses write inside their sandbox, and Snap's
homeinterface excludes dotfiles like.claude. Cargento running outside the sandbox will not see them;--diagnoseshows where it looked. - Very long Windows paths: Claude's encoded project directory names are long by construction, and without
LongPathsEnableda store can exceed the 260-character limit. Those transcripts are skipped rather than crashing, so the symptom is missing sessions —--diagnosereports the root it scanned. - On Windows, Cargento briefly holds a transcript open while reading it, and Python cannot request
FILE_SHARE_DELETE. If a harness rotates that exact file in that window it may see a sharing violation. Reads are short and bounded, but the window is not zero; there is no way to close it from Python without native calls. - A session stuck on "Needs input" after you already answered: the state clears on the session's own next transcript write — its own, not a subagent's, since a background agent writing says nothing about whether you have answered.
SessionEndclears it on a clean Claude exit, and a server restart also clears hook-reported notifications (they're in-memory). An abrupt kill cannot be distinguished from a terminal left open at a prompt, so it clears only by a later transcript event or restart. - After two missed refreshes, the live header changes to "stalled" and shows the last successful update — restart the server, no need to reload the page.