release - #42
Merged
Merged
Conversation
feat(site): give every home section a consequence line, and the Phases band its numbers
Picks up antgrid-ai/dart_terminal#8. Two independent paths in the terminal view dropped precision scrolling, and a two-finger scroll hits both: the wheel path rounded each event to whole lines and discarded the remainder (a mouse notch survives that, a touchpad's few-pixel events all round to zero), and the pan-zoom path — what a trackpad actually delivers — returned early whenever the program had mouse reporting on, which a full-screen agent holds for its entire run.
…supervisor (#33) A conversation that backgrounds itself exits the PTY, leaves the slot resuming a session id a job outside this bridge still holds, and relocates its cwd out of the session's checkout — measured on a live job whose fork had moved from a managed worktree to the main repo while the slot still read checkoutKind=managed-worktree. Pin CLAUDE_CODE_DISABLE_AGENT_VIEW on both claude launch paths: AgentSpec.env for the PTY path, buildClaudeEnv for the SDK path (chat mode surfaces claude's slash commands, so /background is reachable there too). Closes /background, --bg, --routine and claude agents. Does NOT close the two-press left-arrow gesture, whose only guard is the machine-wide leftArrowOpensAgents global-config key — measured against the 2.1.247 binary, the fleet gate never reaches the REPL keymap.
) A remote machine band and an advertised project row painted bgHover on pointer-enter while the local project row beside them never did, so one drawer with one row grammar had two rest states. Both drop the opt-in — AbListRow is flat by default, so this deletes hoverable rather than suppressing it. The hover AFFORDANCES are untouched: the chevron swap, the trash and the + still appear. The dots move to the end of the machine band. Its trash is revealed out of a reserved slot, so a dot placed before it sat a button's width in from the row edge, while LocalMachineBand's host dot — which has no trash — sat at the edge; the two bands could not line up. Liveness dot goes in the final slot, since that is the one sharing a column with the local host dot. The cost is the band's trash now ending 20px in while a project row's ends flush: only one of the two can own the edge. _BandDotSlot replaces three ad-hoc right paddings with one cell that reserves its width whether or not a dot renders. A cell that collapsed would drag the hover-revealed trash leftward whenever a socket resolved or an agent asked a question — out from under the pointer that was reaching for it. The width is a floor, not a cap, so a larger dot overflows where it can be seen instead of painting as a squashed circle in an off-centre cell. Two further changes came out of review. A Blocked machine's dot no longer pulses: the condition is now a whitelist over the sealed SupervisorStatus, so a fifth state has to opt in rather than inherit an animation nothing stops. And _MachineOnlineDot carries the Semantics label connectionDisplayInfo was already computing and discarding — the dot is colour-only and is the drawer's only report that a machine is unreachable. AbListRow now wires onShowHoverHighlight only when hoverable. _hovered feeds nothing but that fill, so on a flat row it was a setState per pointer crossing to repaint identical pixels.
…own, POSIX tree kill, honest announcements (#35) Windows Store updates left the app dead and the bridge force-killed: RegisterApplicationRestart now brings it back (flags 0 on purpose), the install sequence drains and seals the host before handing over, the row reports real progress, and the channel returns what the Store actually said instead of inventing success. The mandatory tier runs the same sequence minus the dialog rather than self-installing over a live bridge. Two defects that were never Windows-specific: terminateTree signalled a process group the host does not lead, so every call reached the host and nothing under it — now SIGTERM-first with a grace and a liveness re-probe before escalating; and the quit-time drain had no ceiling, which on macOS stalls the Sparkle install standing behind the quit. The post-update announcement no longer keys off Windows' --after-update argument (macOS and Linux never pass one). The version recorded at hand-off is the evidence on every platform, the cost line moved onto the strategy, a mark newer than the running build is treated as a rollback, and Sparkle can now retract a row our own appcast read lit. Review of the branch closed eight more gaps: the seal's in-flight spawn window, retryNow racing it, a non-durable retraction, a leaked Sparkle listener, unbounded hand-off writes, an unchecked final grace step, dead --after-update plumbing, and two test spies reaching the process-global launcher.
…37) No Android build has ever contained google-services.json, so build.gradle.kts never applied the google-services plugin, the app shipped with no google_app_id resource, FirebaseMessaging.getToken() threw on every launch, and the phone never sent push:register. The bridge then dropped every notification with "no eligible phone ... need a registered phone with a push token" — 119 times in one host.log, against three paired phones and not one recorded registration. Nothing in any build log said so. deploy-android writes the file from GOOGLE_SERVICES_JSON_BASE64 and hard-fails when the secret is absent. The job is already gated on RELEASE_REPO, so a missing secret there is a misconfiguration rather than a fork — and a release AAB that silently ships without push is the exact failure being fixed, so this path must not inherit the Gradle guard's tolerance. ci-android writes it too, but tolerates absence: a pull_request from a fork gets no secrets, and build.gradle.kts skips the plugin when the file is missing, so those builds still pass. It echoes which of the two builds it produced — a silent difference between CI and the release build is how this shipped broken. Writing it here is also what puts the plugin's resource generation and manifest merge under PR-time verification instead of the nightly deploy. No content validation in either: the google-services plugin already refuses a malformed file or one with no client for the applicationId, and it reads that id from Gradle, so a check here would be a second copy that can drift. The relay's FCM_* credentials are the server half and are configured out-of-band (see relay/.env.example).
…command (#38) * A headless model call is a capability an agent declares, not a title command The judge spec and titleCmd collapse into one AgentSpec.headless, keyed by reach (sealed | readonly | transcript) and selected by what the caller needs (none | repo). Title generation and the Handler's judge become two consumers of one runner instead of two hard-coded spawn paths, so a new agent is named and judged by filling in a record rather than by editing either caller. judgeCapable stays "declares a non-sealed entry", never "declares any entry": being able to answer a question is not the same as being a vetted supervisor over the working tree. For the same reason a repo call is never borrowed from another installed agent, while a naming call may be — its whole input is in the prompt, so nothing about it is specific to the agent whose work is being named. noHistory is required per entry because no passing test can detect a spawn that persisted a session, and every mechanism here was verified against the real CLI: claude --no-session-persistence, codex --ephemeral, opencode and kilo redirect their SQLite store to :memory:, cursor redirects CURSOR_DATA_DIR (conversations) and not CURSOR_CONFIG_DIR (credentials), copilot redirects COPILOT_HOME, and vibe sets session_logging.enabled=false through its config layer. Copilot and vibe look alike and are opposites: copilot authenticates fine against an empty home, vibe keeps its credentials in one. antigravity and kimi declare nothing — no mechanism was found, and absence is the honest answer. A naming spawn now runs in an empty throwaway directory rather than the session's checkout. These CLIs write a session unconditionally, but the resume surfaces that matter are scoped to the working directory, so this keeps an auxiliary call out of the picker the user actually opens; it also leaves the agent no repo to read and no project-tier hook config to fire. The judge keeps the checkout, because reading the tree is its work, and that split is now pinned by tests rather than left to convention. Chat sessions are named from the bridge's own prompt tap rather than from anything the agent generates, which also moves codex chat naming from turn-end to the first message. * A headless spawn that outlives its kill must still settle The timeout killed the process HANDLE, but every argv here is reached through a launcher shim: the handle is a cmd.exe/sh wrapper and the agent is its child holding the inherited stdout pipe. Killing the wrapper left that pipe open, so the stdout read never reached EOF and runHeadless never settled — and the judge awaits it with no outer deadline, wedging a supervised session in "handling" for the life of the bridge. It now kills the tree and races an abandon deadline, so the budget is a real bound even where the tree kill cannot reach. The naming attempt was keyed by a flat "terminalId:conversationId" string, whose space collides with real terminal ids (a checkout's setup PTY is "<checkoutId>:setup"), so releasing one terminal reached another's entries by prefix. It is a nested map now, which also makes the release O(1) rather than a scan. Four things that are not a new conversation may no longer spend the one naming attempt or rename a session: a supervisor's injected auto-reply, the first message of a RESUMED chat, a terminal/chat mode flip whose carry-over exemption the key change had defeated, and a slash command whose text is only its arguments. The claude hook also stops sending an empty transcriptPath on session start/stop, which overwrote a path the session had already reported. Tests that could not fail are replaced rather than kept: judgeCapable was pinned by recomputing the predicate from the same table, so it passed for any registry and stayed green through this branch's own widening, and two env assertions probed process.env enumeration order. Coverage that opencode's session.updated is dropped is restored, for the root session and for a subtask. buildJudgeCommand had no production caller while handler/judge.ts hand-rolled the same selection; it becomes pickJudge and the live path calls it. The api-server's unread title field goes, along with a comment claiming a wire-compatibility reason that a z.object (which strips unknown keys) never needed.
Adds `session:fork`: a new session that picks the conversation up where an existing one left it, running either in a workspace cut from that session's last commit or in the one it is already using. The bridge resolves the source transcript, the native fork argv and the checkout, so the app sends only a session id and a workspace choice. Agents with a native fork (claude-code, codex, opencode) launch through it; the rest get a normalized transcript handoff. The fork is named after its source and records `forkedFromSessionId`, so its provenance survives a rename on either side, and the kebab lands the user in it the way the New Session canvas does. The menu item is greyed with a reason for a session that has nothing to fork yet, and every refusal the bridge can answer with now reaches the user. A shared workspace is marked wherever a session is named — drawer row, Recent row, breadcrumb — with the count in the tooltip, and the delete flow stops promising to remove a directory other sessions are still working in.
…#40) * fix: let the Handler send the command it decided on An armed Handler could produce zero auto-replies for a whole session while looking healthy. Every "handle" it reached was converted to an escalation by harness guards the judge was never told about: a slash_command value had to be a single whitespace-free token, so "/code-review --fix" was refused for its argument, and a reply carrying a line break was refused as a control character. buildRetryPrompt only ever fired on JSON schema failures, so the judge could not learn either rule and repeated both. The shape rules now live in one module, bridge/src/handler/reply-shape.ts. The engine calls it as its final gate and hands the same function to the judge as a retry trigger, so the rule the retry teaches and the rule the gate enforces cannot drift. action.value splits on its first whitespace run. The verb keeps the token rule; the argument tail joins the destructive floor's pathCheckText while the verb stays out of it, since a "/"-shaped verb reads to ABS_PATH as a path start. A reply is flattened to the one line injectReply will submit. Setting both reply and action is refused rather than silently resolved, and the refused text still reaches the user as the escalation's draftReply. The retry sits at judge.ts's parse seam, not in the engine. The destructive floor and the runaway guard both live above runDecisionFn and need per-terminal state judge.ts cannot see, so a retry there is structurally incapable of wrapping a safety verdict; it also avoids re-applying absorbTransitions, which feeds RunawayGuard.recordProgress. A parsed-but-refused decision now always beats null on the way out: null means judge outage to HandlerEngine, which parks the session, and parking would swallow a decision the caller's own gate would have escalated with its text attached. The prompt states the contract and names the agent under supervision, which is not the judge tool and not the routing key. Chat sessions carry a real command catalog, so it is rendered when non-empty and replaced by an explicit "no catalog available" sentence when not: an empty list is indistinguishable from a failed or not-yet-landed discovery, and announcing it as a complete set would tell the judge nothing exists. Membership is required only where a catalog is present. A PTY has none, and an invented command typed at a terminal is visible and recoverable. Chat no longer refuses slash commands. The engine resolves the verb against the catalog it already holds and passes the hit through injectReply's new optional command argument; the structured adapter sends it as a commandId with the argument tail as text, and falls back to the whole string as a plain prompt on a miss. Matching is on CapCommand.name, never an id prefix, because codex emits no cmd: ids. supportsSlashCommands is retired. * fix: report what the Handler could not send, and verify what it calls done Four supervision gaps, found while tracing a session where the Handler sent nothing all morning and then reported the work as finished. A guard rejection is a report, not a question. Escalations raised by the shape check, the hard floor and the runaway guard now carry kind "guard_blocked": they survive a typed line and an id-less retraction, and retire only on an explicit handler:dismiss. A report is excluded from every "one unanswered escalation is enough" suppression site, so it can neither mute a later escalation nor block wrap-up, and it gets no one-tap chip - the chip would re-send the text a guard just refused. Capped at five per session, oldest dropped; the wrap-up push carries the count, since disarm takes the rows off the app with it. The post-completion idle nudge no longer costs a judge call. Claude Code fires one "notification" hook for both a genuine mid-turn block and its generic idle ping, so the hook now classifies its own post and the api-server drops it only when the hook says "nudge" AND the session's own turn has already ended. Both answers come from one reading, because the paired /notify races the handler event and cannot be relied on to have landed. Every hop fails toward forwarding: a genuine block dropped here leaves a blocked agent unsupervised with no event left to raise it. The floor no longer reads a slash command as a path. ABS_PATH now requires an interior separator, so /etc/passwd warns and /code-review does not - it was feeding a false "absolute path outside project" warning back into the very prompt section that exists to teach the judge which of its own proposals were dangerous. The cost is real and deliberate: a bare top-level path (/tmp, /opt) is neither flagged nor grantable by instruction, and the other four tiers carry it. Evidence must be a citation. A terminal transition's evidence is now graded against the context the judge was actually shown - normalised for quote style, whitespace and ellipsis - must clear a length floor, must not merely echo the item's own text, and a done on a command-shaped item must name that command. Refusals feed back into the next prompt and surface as an activity row instead of vanishing into a log line; the anchor is waived after three anchor-only refusals on one item, grounding never is. This cannot tell a correct attribution from a real quote about the wrong subject - that judgement needs the reader role the design collapsed - and the command anchor is the one narrow substitute available without one. * fix: resolve the api-server port file live, not at module load The path was captured in a module-level const, so it answered for whatever ANTGRID_DIR held when the first module in the import graph loaded. Every other reader honours the override live — resolveAbDir throughout agent-core, and hook-runner's own fallback — so a test that pins ANTGRID_DIR the way the bridge suite's convention does was silently writing the developer's real port file instead, and the new stale-nudge wire test read that file to find its core's port. On a clean CI runner ~/.antgrid does not exist, the best-effort write failed into a log.warn, and the read was ENOENT. Creating the directory before the write is the other half: a first launch that starts the api server before anything has made the abDir loses hook discovery with nothing but a warning to say so.
* An agent judges only on an argv someone has actually run
cursor-agent and mistral-vibe declared headless argvs that had never been executed — both CLIs refuse every invocation on this machine without CURSOR_API_KEY / MISTRAL_API_KEY, so their reach was inferred from help output rather than measured. AgentSpec.headless says outright that an entry goes in only after running it, and the cost of breaking that is not a mislabel: any non-sealed reach makes the agent judgeCapable, arming a supervisor over the user's working tree on an argv nobody has run. Both entries are gone; naming still works by borrowing an installed agent. Copilot's readonly entry stays — writes were re-tested through three vectors (plain create, an explicit shell call, and git init's side effect) and the system blocked all three.
A failed naming spawn no longer costs the session its name. The single 'attempted' flag answered three questions with one bit and got two wrong: a signed-out CLI or a timeout marked the conversation spent, so it could never be named even after the cause was fixed. TitleAttempts splits them — an in-flight claim released in a finally, a bounded failure count, and a terminal done — and generateTitleFromContext now returns why it failed, so 'nothing installed can serve this' ends the budget while a spawn that merely failed does not.
State a CLI offers no switch to skip is redirected per spawn rather than to a fixed path. COPILOT_HOME pointed at one directory per tool, which kept every session it was ever handed: ~51KB apiece plus a 352KB uncheckpointed WAL after two calls, in a %TEMP% Windows does not reclaim. HeadlessCommand.scratchEnv names the vars, and the runner creates the directory and deletes it with the spawn.
* vibe's ask profile gates on approval, it does not restrict writes
Read against mistralai/mistral-vibe v2.24.5 after the entry was removed for being unrun. The removed argv was wrong in a sharper way than that: `ask` is the approval-gated profile ('Requires approval for tool executions'), while `plan` is the read-only one and the only builtin pinning write_file and edit to permission 'never'. Programmatic mode denies every callback it is handed, which is what made ask look read-only — a write fails closed on an approval nothing can answer.
That safety is config-level, never argv-level. An agent profile is only another config layer; ask contributes no bypass_tool_permissions key, so a user's own config survives it, and the loop returns EXECUTE before consulting any permission once that is set. Nothing raises an approval, so nothing is denied — and the same switch defeats `--agent plan`. Recorded on the entry so the argv is not reintroduced as 'readonly'.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.