Commit Graph
41 Commits
Author SHA1 Message Date
Brennan Benson 0a853a5c0d fix(agent-hooks): stop backgrounded Claude sessions posting a stale pane key (STA-4769) (#15304)
* fix(agent-hooks): stop backgrounded Claude sessions posting a stale pane key (#9236)

A session started with `claude --bg` or `/background` runs in a worker under
the shared daemon, and that worker inherits the environment of whichever pane
first started the daemon — not the pane that dispatched it. ORCA_PANE_KEY there
names an unrelated pane, so the session's hooks overwrite that pane's sidebar
row, from any worktree.

It fails silently and successfully: the script re-sources the endpoint file, so
port and token self-heal and the POST lands, while the pane key has no refresh
path and stays wrong.

Measured on the wire against a throwaway listener: three sessions on one daemon,
and the one dispatched from pane B posted pane A's key and pane A's worktree on
SessionStart, UserPromptSubmit and Stop.

CLAUDE_JOB_DIR is set only in those workers — absent from all 68 live foreground
sessions on this machine — so it is the signal to decline. Declining is the only
option that exists: normalizeHookPayload rejects an absent paneKey outright and
AgentHookEventPayload.paneKey is a required string, so there is no "session with
no pane" representation to post instead. A backgrounded session genuinely has no
pane; attributing it to nothing is correct.

The Windows guard exits rather than jumping to the stdin drain: the drain parks
in more.com and a daemon worker is outside an Orca pane, which is exactly the
abandoned-stdin hang #11549 guards against.

* fix(agent-hooks): guard the Claude statusline against the same stale pane key

The statusline command IS invoked inside a backgrounded worker — measured, with
no client ever attached, and its ancestry terminates at the daemon rather than
at any pane:

  statusline pid=41746 <- claude bg-spare <- claude bg-pty-host <- claude daemon
  CLAUDE_JOB_DIR=/tmp/.../jobs/f1f9edd2

A second session dispatched from a different pane saw the first pane's
ORCA_PANE_KEY with its own correct session id, so this script is a live second
producer of the same misattribution the hook guard closes.

Windows uses exit /b 0 before stdin is owned, per the #11549 contract; POSIX
places the guard after capture, since exiting mid-write there surfaces as EPIPE
the agent can see (#8110).
2026-08-19 15:22:33 -07:00
Brennan Benson 0ac2e77db1 fix(agent-hooks): default-form managed hook vars so a static precheck cannot reject them (#14994)
The managed hook command embedded a bare $SYSTEMROOT. Grok loads Claude's
settings.json hooks and statically prechecks env vars across the whole command
string, so the reference inside the never-taken Windows branch made it refuse
the hook on macOS on every event:

  hook not executed: required env var(s) not set: ${SYSTEMROOT}

Grok fails these open, and Orca installs Grok's native hook separately, so no
status was lost -- the symptom is a swallowed failure line per tool call.

$VAR and ${VAR-} expand identically in POSIX shells absent set -u, so this has
no execution-time effect; only the static precheck observes it. Verified in Git
Bash on Windows that both guard forms resolve identically ($SYSTEMROOT is set
there and uppercase is the correct spelling -- $SystemRoot is undefined).

Also converts the three bare $HOME references so the regression test can assert
zero bare variable references with no exemption. A $SYSTEMROOT-specific check
would not have caught this class of bug being introduced elsewhere.
2026-08-16 21:58:28 -07:00
OrcaWinandOrcaWin 02ba70a847 fix(agent-hooks): make the Windows managed hook survive Claude-hooks-compat consumers (#14825)
* fix(agent-hooks): make the Windows managed hook survive Claude-hooks-compat consumers

`~/.claude/settings.json` is not read only by Claude Code. Third-party
Claude-hooks-compat layers (cursor-agent, Devin) import the same file and
reimplement hook execution, so Orca's entry has to survive consumers that
support strictly less than the documented schema. Three separate defects
came from assuming otherwise.

1. The entry depended on `args`, which a compat consumer ignores.
   `args` is valid Claude Code syntax, but cursor-agent spawns `command`
   alone -- so `conhost.exe` ran bare, which opens an interactive console
   that never closes. Hook payloads were typed into those stranded shells
   (#14815). The entry is now one self-contained `command` string that
   depends on nothing optional.

2. `conhost.exe --headless` never relayed anything. It implements the
   ConPTY server protocol, not a generic no-window wrapper: it does not
   wait for the hosted process and relays neither exit code nor stdout.
   Measured directly -- `conhost --headless cmd /c "echo X& exit /b 42"`
   yields empty stdout and no exit code, while the replacement returns
   both and waits. So every hook was fire-and-forget, and whatever it
   printed was discarded. Replaced with `-WindowStyle Hidden`, which
   suppresses the window and keeps wait/exit-code/stdout intact.

3. The hook never wrote anything to stdout. Guards exited silently and
   curl's output went to nul. Claude Code documents empty stdout as "no
   decision", but cursor-agent treats PreToolUse as a permission gate,
   fails to parse empty stdout as JSON, and blocks the tool call -- so
   every shell command in every cursor-agent session on Windows failed
   (#14818). The script now writes `{}` first, on both the Windows and
   POSIX branches, which is documented to be identical to writing nothing
   for real Claude Code. Gemini and Antigravity already did this.

Defects 2 and 3 are causally linked: `{}` cannot reach any consumer while
conhost is swallowing stdout, so neither fix works without the other.

Also fixed while establishing the contract:

- The launcher's own missing-script fallback returned empty stdout,
  reproducing #14818 whenever `~/.orca` was cleaned or an install was
  half-finished. It now emits `{}` too.
- PowerShell serializes progress records to stderr as CLIXML when stderr
  is redirected; a consumer merging stderr into stdout would see those
  bytes before the JSON. Every encoded payload now silences progress.
- `runtime-home-hook-command.ts` built its own launcher without window
  suppression -- exactly the drift #14815 asks to prevent. All launcher
  construction now goes through `windows-powershell-hook-launcher.ts`, so
  the switch list cannot be present in one installer and missing in
  another.
- Renamed `usesWindowsHeadlessHook` to `usesWindowsPowerShellLauncher`;
  nothing is headless anymore, and the flag selects a launcher.

Testing: the new regression test asserts the effect a consumer observes
-- it runs the exact `command` string from settings.json through both
cmd.exe and Git Bash, across the guard-exit, reached-curl, and
missing-script paths, and parses stdout. Verified it fails when
`conhost --headless` is reintroduced. The previous tests all asserted
installer intent, which is why they passed through all three defects.

* fix(agent-hooks): close hook launcher review gaps

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-16 20:48:26 -07:00
OrcaWinandBrennan Benson f226fcfc4b fix(claude): make managed hook paths portable (STA-3348) (#13442)
* fix(claude): make managed hook paths portable

* perf(claude): keep portable hooks shell-native

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-11 14:03:16 -07:00
OrcaWinandBrennan Benson f70b52e30a STA-3608: hide managed Claude hook consoles on Windows (#13443)
* fix(claude): hide managed hook consoles on Windows

* fix(claude): harden managed hook invocation

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-11 13:30:55 -07:00
Brennan Benson 2ee43bfc0d fix(agent-hooks): refresh existing Orca launchers when agent CLIs are unavailable (#13378)
* fix(agent-hooks): refresh existing shared hook scripts when the CLI is no longer detected

A CLI that falls off PATH (moved npm prefix, relocated shim) keeps its user-wide
config invoking Orca's launcher script under ~/.orca/agent-hooks, but the
presence gate skips install() with no removal — freezing the script at whatever
Orca generated last. Anyone in that state kept the pre-#11568 more.com-leaking
.cmd forever, because no launcher script is ever deleted and Windows startup
deliberately skips shell PATH hydration.

Reconcile before gating: every existing shared launcher/statusline script is
rewritten to the current template on each install pass. Creating scripts stays
behind the presence gate — an existing file is proof of a prior install; a
missing one means the gate did its job. Amp and Hermes are deliberately absent:
they write provider-native plugin code with its own install lifecycle, not
shared launchers.

- refreshManagedScriptIfPresent() in installer-utils (no-op unless the file exists)
- refreshManagedScripts() on the 11 launcher-writing services (openclaude via
  the shared Claude class)
- reconcile pass in installManagedAgentHooks before presence detection,
  filtered by the agents option, best-effort per agent
- coverage gate: a launcher written to ~/.orca/agent-hooks without a matching
  refresher entry fails the suite, in both directions

* perf(agent-hooks): refresh launchers off the main thread

* test(agent-hooks): keep refresh mode assertion POSIX-only
2026-08-10 16:34:15 -07:00
Wooseong KimandBrennan Benson 9a6c5cddc9 fix(agent-hooks): exit Windows hooks when Orca env is missing (#11568)
* fix(agent-hooks): exit Windows hooks when Orca env is missing

* test(agent-hooks): pin the Windows missing-env exit as a deliberate stdin handoff

The Windows-only executable matrix still asserted every managed .cmd drains
its writer, which the missing-env fast exit intentionally stops doing. That
assertion never runs in CI (vitest is ubuntu-only), so it would only fail on
a Windows dev machine.

- Accept a broken writer for .cmd hooks on the missing-env path; keep the
  no-error contract for the PowerShell and Git Bash hooks that still read
  stdin first.
- Correct the drain-epilogue comment: claude-hook.cmd is the remaining
  in-script jump to it.

* docs(agent-hooks): cite #11549 and the stdin-ownership trade in the guard comment

* test(agent-hooks): stop overstating what the live matrix pins

runHookProcess always closes stdin, so the executable case cannot reproduce
the abandoned-pipe hang; say so and name the guard strings as the real gate.
Also record that the Devin skip guard still routes to the drain above these
guards, so #11549 stays reachable for Devin outside an Orca pane.

* docs(agent-hooks): record why Windows batch diverges from the POSIX stdin policy

The two templates now answer the same question opposite ways and nothing states
the rule, so name the constraint (batch has no bounded stdin reader) and the
evidence (POSIX callers have not shown the abandon; Windows callers have).

* test(agent-hooks): stop the live matrix name and comment from over-claiming

The renamed case covers two missing-script launchers that still read to EOF, so
'without stranding a reader' was false for them. Payload size was also the wrong
reason for the .cmd exit — the statusline script buffers the same size through
more.com; the real constraint is that any read-to-EOF hangs on an abandoned pipe.

Also record that the broken-writer relaxation is only sound because
hookEnvironment() strips every ORCA_* var, since the branch keys on .cmd.

* test(agent-hooks): gate the rule, not the three guards that exist today

The revert pin was three exact string denials, so a fourth ORCA_* guard — copied
from the nearest in-repo pattern, which still routes to the drain — would have
reintroduced #11549 with the suite green. Assert instead that no ORCA_* guard in
a generated .cmd routes to the more.com drain.

Verified both directions: reverting the source fails it, and adding a simulated
ORCA_TAB_ID guard that gotos the drain fails it too. Does not match the Devin
skip, which is not an ORCA_* guard and whose caller is a live Orca pane.

* docs(agent-hooks): name the exceptions so the stated rule is not self-contradicting

The previous wording set a rule that claude/statusline-script.ts breaks 25 lines
away in the same export surface, and said nothing about the Windows .ps1 and Git
Bash hooks. State the rule for generated .cmd, then name every hook that still
reads first and why — the read is the payload capture, not a drain. Batch is the
exception because it streams to curl, so owning stdin buys nothing there.

* test(agent-hooks): close the if-not-defined bypass in the drain gate

The guard-spelling regex only covered `if "%VAR%"==""`, but `if not defined VAR`
is equally house idiom here — every Windows hook opens with `if defined
ORCA_AGENT_HOOK_ENDPOINT`. A fourth guard written that way routed to the drain
with the suite green. Match the whole line instead of one spelling, which also
covers `goto` without its colon.

Probed all eight cases: the five bypass spellings are caught, and the Devin skip,
the endpoint refresh, and the fixed guard are correctly exempt.

* docs(agent-hooks): separate the two reasons a hook is exempt from the .cmd rule

The list conflated them. POSIX, copilot .ps1 and kimi .sh read first because the
read is the payload capture. claude/statusline-script.ts does route its guard to
the drain — it is exempt because its caller closes stdin, which the in-pane
capture already proves. Grouping them under 'the read is the capture' would let a
future author reach the right conclusion from the wrong premise.

* fix(agent-hooks): let the missing-Orca-env exit outrank the Claude/Devin skip

claude-hook.cmd checked DEVIN_PROJECT_DIR before the Orca env guards, and that
check jumps to the more.com drain. Outside an Orca pane the caller is not Orca's
PTY, so it can abandon stdin and leave more.com waiting forever — the #11549 hang
this guard reordering is meant to close.

Both branches end the hook, so the only behavior that changes is Devin outside an
Orca pane: it now exits 0 instead of parking in more.com. In-pane Devin still
reaches the skip and still owns stdin.

Depends on #11568.

* refactor(agent-hooks): cut the review additions back to house idiom

The three-line guard fix needed a three-line comment, not a thirteen-line
taxonomy adjudicating three other files. Swap the hand-rolled drain gate and
broken-writer set for the combinators already used repo-wide: not.toMatch
(94 uses) and toContain on a literal.

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-09 14:00:46 -07:00
Brennan Benson cd8c66551a fix(agent-hooks): resumed Claude Code session gets its sidebar agent row at SessionStart (STA-3386) (#12859)
* fix(agent-hooks): give resumed Claude sessions a sidebar row at SessionStart (STA-3386)

Claude's hook set never registered SessionStart and normalizeClaudeEvent
dropped it at ingest, so a resumed session that idled produced zero hook
traffic and earned no sidebar agent row until the first prompt.

- Register SessionStart in CLAUDE_EVENTS (local + remote installs).
- Map lead SessionStart (startup/resume/clear) to an idle 'done' row,
  resetting stale roster/task/cron/tool/prompt state like the Codex path;
  compact restarts and child-attributed SessionStart stay dropped.
- Thread hookEventName through the agent-status IPC payload so the
  completion coordinator can tell a session connect from a turn result;
  a SessionStart 'done' no longer raises agent-task-complete.

* fix(agent-hooks): mark SessionStart rows as session boundaries, not completions (STA-3386)

Review follow-up: represent the idle connect as a first-class
sessionBoundary flag on the status payload instead of gating one
renderer consumer on hookEventName.

- sessionBoundary rides AgentStatusPayload/AgentStatusEntry (done-only,
  clamped like interrupted); drops the hookEventName IPC threading.
- Completion-reactive consumers ignore session boundaries: the
  completion coordinator (task-complete notifications), automation
  dispatch observers (a connecting agent no longer completes the run
  and closes its tab), activity unread counts, and the dashboard
  finished timestamp; the status slice keeps boundaries out of
  stateHistory and preserves the flag across done->done repaints.
- SessionStart sources are allowlisted (startup/resume/clear) so
  compact restarts or unknown sources fail closed mid-turn.
- A live SessionStart now un-retires a reusable pane like a fresh
  prompt, so resume-in-reused-pane earns its row too.

* fix(agent-hooks): keep session-boundary dones out of teardown and completion history (STA-3386)

Review round 2:
- A boundary done no longer deletes the pane's launch-config registry
  entry, so a resumed idle TUI keeps its registered-launch-agent
  identity evidence.
- A boundary landing on a REAL done pushes that completion into
  stateHistory so the finished timestamp and unread badge survive a
  resume//clear right after a finish.
- The done->done flag carry yields to turn evidence (assistant message
  or changed prompt) so a genuine completion can never be suppressed.
- Star-nag value-moment observer and the server's OSC-equivalence
  dedupe now discriminate the flag.

* fix(agent-hooks): keep a displaced completion unread in the sidebar badge (STA-3386)

Review round 3: sidebar-badge mode counts only the live entry, so a
session boundary landing on an unacknowledged completion silently
dropped the sidebar badge while the agent-events count kept it. Count
the displaced completion from history for boundary rows, and pin the
behavior with countActivityUnread tests.

* fix(agent-hooks): prevent SessionStart completion side effects (STA-3386)

* fix(agent-hooks): preserve SessionStart through renderer IPC (STA-3386)
2026-08-05 22:06:36 -07:00
Jinjing 05206046f6 chore: condense code comments (#12008)
* chore: condense code comments

* chore: shorten more code comments

* clarify PTY agent session descendant cleanup behavior

Refine the comment on ptyAgentSessionIds to more accurately describe
when agent sessions sweep their descendant process trees and note the
exception on immediate Windows shutdown.
2026-08-01 14:24:31 -07:00
Brennan Benson c4d903ff21 fix(agent-status): keep Claude in-process teammates visible as idle sidebar rows (#9850)
* fix(agent-status): keep Claude in-process teammates visible as idle sidebar rows

Claude Code 2.1.21x runs named Agent-tool agents as turn-based in-process
teammates: SubagentStop and TeammateIdle fire at every TURN end while the
teammate stays alive awaiting mail (verified live on 2.1.217). Treating
those events as finish signals deleted the child row seconds after each
burst, so the sidebar showed no subagents for most of a teammate's life.

Root-cause fix: the roster now tracks a working/idle state per child.

- One-shot children (hyphen-free ids) keep remove-on-stop: their
  SubagentStop is a true finish.
- Teammate-shaped rows park as idle on SubagentStop/TeammateIdle and
  revive to working via the next SubagentStart (same lifecycle id,
  first-observed startedAt preserved).
- Idle rows never gate the pane 'working' (#8825's done-gate rule).
- Only TeammateIdle-confirmed idle rows survive a complete lead-Stop
  fold; a stopped workflow lane wearing a teammate-shaped id is reaped
  there (or immediately, once a fold tagged it listedAsSubagentTask), so
  the pre-#8825 idle pile cannot rebuild.
- At the wire cap, the oldest idle row is evicted to admit a working
  spawn; working children are never displaced.
- Hydrate keeps pruning idle snapshots: idle-teammate liveness cannot be
  proven across a restart, and a live teammate re-earns its row.

* fix(agent-status): restore inventory-confirmed workflow lanes
2026-07-22 11:42:34 -07:00
Brennan Benson 586cf781ca perf(rate-limits): throttle statusline usage posts to one curl per pane per 15s (#9829)
* perf(rate-limits): throttle statusline usage posts to one curl per pane per 15s

The managed Claude statusline script posted on every statusLine tick
carrying rate_limits — ~3 curl spawns/sec per streaming pane, multiplied
across concurrent panes. The service drops same-value posts inside its
30s dedupe window anyway, so most spawns bought nothing.

Gate the post on a per-pane stamp file: POSIX compares date +%s against
the stamp; Windows uses an all-builtin seconds-of-day parse of %TIME%
(octal-safe, no extra process). Both fail open — unparseable time,
garbage stamp, or midnight wrap posts rather than darkening the live
usage feed — and the stamp only advances when a post actually fires, so
skipped ticks never defer the next one.

Measured: 30 rapid rate-limit ticks spawn 1 curl (was 30).

* perf(rate-limits): keep throttled statusline ticks process-free

Use Claude's monotonic session duration for the POSIX throttle so skipped ticks do not replace curl churn with date churn, while retaining a fail-open date fallback. Key temp files by the stable leaf UUID so path-like or long host tab IDs cannot disable the throttle.

* fix(rate-limits): preserve throttle isolation across upgrades

* fix(rate-limits): reject leading-zero stamp values before arithmetic

All-digits validation still admitted values like 008, which are invalid
octal inside $(( )) — and dash treats that expansion error as fatal, so
the script died before rewriting the stamp and the pane's live usage
feed stayed dark until the file was deleted. Allow-list canonical
decimals (same pattern as the duration parse) on both the stamp and the
computed clock so malformed values fail open to posting.

Verified under dash: the old digits-only check aborts at the arithmetic
(Illegal number: 008); the allow-list survives and posts.

* perf(rate-limits): eliminate POSIX statusline cat churn

* test(rate-limits): exercise overlapping statusline ticks

The stamp check/write is deliberately lock-free (a lock could wedge the
feed closed; fail-open is the contract), so a truly concurrent burst may
post more than once, bounded by overlap width — an exact at-most-one
assertion would be flaky by design. Assert the invariants that do hold:
every overlapping run exits 0, the raced stamp lands valid, and it
throttles the following ticks.
2026-07-21 20:59:33 -07:00
c24ebcade5 fix(rate-limits): unstick Claude "Limited" usage — respect Retry-After and feed live usage from session statuslines (#9617)
* fix(rate-limits): unstick Claude "Limited" usage and feed live usage from session statuslines

The OAuth usage endpoint's 429 Retry-After (~50 min) was ignored, so the
30s-15min automated retry lanes kept landing inside the throttle window and
the status bar stayed on a bare "Limited" indefinitely while Claude itself
worked fine.

- Respect Retry-After on 429: carry it through usageMetadata.retryAtMs and
  gate automated refetches (activation lane, poll cycles) until it expires;
  user-directed refreshes still bypass.
- Keep the last-known usage snapshot visible through rate-limited windows
  (24h) instead of dropping it after the generic 30-minute stale threshold.
- Add a managed Claude statusLine command that forwards each session's
  rate_limits (Claude Code >=2.1.80) to a new /statusline/claude loopback
  route, feeding live usage windows with zero usage-endpoint calls; OAuth
  polling pauses while the live feed is fresh. User-owned statusLine
  settings are never overwritten.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(rate-limits): keep last-known window when a statusline post carries only one

Statusline payloads may report five_hour and seven_day independently; a
partial post must not wipe the other bar to null. Also document the
seconds-vs-ms epoch heuristic.

Addresses CodeRabbit review on #9617.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(rate-limits): unstick Claude usage with live statusline feed

The OAuth polling endpoint is rate-limited; Claude's status often shows
"Limited" until the next poll cycle, even when quota remains. Live posts
from the statusline command update usage within 100ms, eliminating false
"Limited" displays during active sessions.

Manages install lifecycle via marker to respect user deletions. Handles
Windows payload buffering and guards before curl spawn. Protects against
live-post/OAuth-fetch races and cross-attribution during account switches.
Gracefully tolerates schema drift in statusline parsing.

* test(rate-limits): assert stale outgoing post doesn't affect incoming

Capture usedPercent before ingesting and assert it remains unchanged,
rather than checking for a specific value. This is more precise and less
brittle when testing session switch isolation.

---------

Co-authored-by: Dzmitry Bachko <dbachko@users.noreply.github.com>
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
2026-07-21 00:36:42 -07:00
NeilandOrca 190de8223e refactor(comments): slim verbose comments in main integrations (git/providers/…) (#9543)
Collapse multi-line explanatory comment blocks into single-line "why" statements
per AGENTS.md ("Document the Why, Briefly"): drop restatements of the code and
mechanism narration; keep the non-obvious reason, external refs, and directives.

Comments-only — verified no code changed via a Babel/esbuild comment-strip
token-equality gate against origin/main; typecheck and oxlint clean.

Area: main — git, source-control, providers & integrations. 40 files changed, 1432 insertions(+), 4473 deletions(-).

Co-authored-by: Orca <help@stably.ai>
2026-07-20 03:18:28 -07:00
Brennan Benson c8986ca52e fix(agent-status): reap finished Claude named agents/teammates from the sidebar roster (#8825)
* fix(agent-status): reap finished Claude named agents/teammates from the sidebar roster

#8522 stopped one-shot subagents from squatting as idle rows, but named
background agents (Workflow/orchestration/ultracode lanes and agent-teams
teammates) still piled up permanent "Idle - <type>" child rows for the rest
of the session — the reported regression (11 idle rows under an
"Orchestration Messages" pane, all idle 3-6h after finishing).

Root cause, confirmed against live hook captures (claude 2.1.210): named
agents get teammate-shaped ids (a<name>-<hex>) AND now appear in Stop's
`background_tasks` as `type: "teammate"` entries whose status stays "running"
forever — even after the agent finished. The old code read that shape as a
"resumable teammate", so SubagentStop only marked it idle and the fold never
reaped it (a present teammate task kept hasTeammateTypedTask true). The rows
never left.

Fix: the roster now tracks only WORKING children.
- SubagentStop removes the child outright (teammate-shaped or not) — it is
  the reliable finish signal; the teammate task's "running" status is not.
- TeammateIdle removes by name as the fallback when a SubagentStop is lost.
- A lead Stop's background_tasks still reaps unlisted children: hyphen-free
  one-shots always, and teammate-shaped rows once a complete inventory shows
  no teammate-typed task at all. A live named agent whose id never appears is
  kept only while a teammate-typed task is still present (the done-gate).
- Hydration drops persisted idle snapshots so a restart can't re-pile them.

Verified live in a dev Electron instance driving a real Claude TUI that spawns
four named background agents: pre-fix the pane resolves to done with four
persistent "Idle - <probe>" rows; post-fix each row disappears the instant its
agent finishes and the roster drains to empty (done, zero child rows).

Tests: roster + row-lifecycle + hook-listener suites rewritten to the
working-only semantics, grounded in the captured 2.1.210 hook stream
(126 passing). Typecheck + oxlint clean.

* fix(agent-status): prune persisted idle Claude children

* fix(agent-status): persist Claude idle-row hydration cleanup

* fix(agent-status): avoid ambiguous teammate idle cleanup

* fix(agent-status): reconcile replacement children at roster cap
2026-07-15 12:09:30 -07:00
Kaynan Sampaio de CamargoandJinjing c3ab805d12 fix(agent-hooks): drain stdin before hook script early exits so agents never hit EPIPE (#8430)
* Fix hook scripts to drain stdin before any early-exit path

Generated agent hook scripts and missing-script launchers could exit
successfully before consuming the payload written to their stdin,
leaving the writer with a broken pipe (EPIPE/ERROR_BROKEN_PIPE) once
the reader closed early. Capture stdin (or drain it via a shared
epilogue/fast-path guard) before any whole-script success exit across
all POSIX, batch, PowerShell, and Git Bash launcher variants, and add
a cross-agent lifecycle test suite plus a live Electron verification
script to guard the contract going forward.

* Harden hook scripts against unreadable managed scripts and add a Claude/

- Extend the POSIX launcher guard to also require `[ -r ]`, not just `-f`/`-x`,
  so an executable-but-unreadable managed script still drains stdin instead of
  erroring or silently misbehaving.
- Add a verifier case (`verifyClaudeDevinSkip`) that spins up a local HTTP
  server and confirms the Claude hook never forwards a request that Devin
  already imported, catching accidental double-forwarding.
- Update installer-utils tests and stdin-lifecycle docs to match the new
  readable-file guard and the added verification case.

* Fix hook-launcher verification to derive script paths from the installed

Extract the quoted path from the launcher's `if [ -f '...'` clause instead of
reconstructing it via join(home, ...), so missing/failing-script test cases
can't silently fall through to the real script if the install layout changes.

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
2026-07-13 00:26:22 -07:00
Brennan BensonandBrennan Benson 31cff75a7b feat(agent-status): show Claude subagent child rows and gate premature done (#8211)
* feat(agent-status): show Claude subagent child rows and gate premature done

A Claude pane that spawned background subagents/teammates showed a green
done check the moment the lead's turn ended, even while a background
review loop was still running. Orca now tracks the pane's live children
from Claude hook events and:

- keeps the pane 'working' while at least one child is working (Stop is
  gated; Claude wakes the lead when a child finishes, so the pane
  resolves to done on the follow-up Stop with an empty roster)
- renders the children as indented child rows under the pane's sidebar
  row (name/type + working/idle dot), reusing the existing lineage UI

Tracking is lifecycle-primary: SubagentStart/SubagentStop/TeammateIdle
(newly registered hooks) plus child-origin tool events (they carry
agent_id) own the roster. Stop's background_tasks is folded only where
unambiguous — verified live on Claude Code 2.1.207 that teammates report
status "running" while idle-alive and their task ids never match
lifecycle agent_ids, so the list cannot decide teammate working-ness.

Child-origin events no longer overwrite the lead's tool/prompt caches
(a live AskUserQuestion card survives child churn); a child's own
PermissionRequest records waitingAgentId so only that child's progress
or death clears the wait. The interrupted flag survives the gated
window, inferred interrupts sync the lead record and refuse while a
child works, and hydration reseeds the roster after a restart.

* fix(agent-status): drop identity icon on subagent child rows

The child's agentType carries its NAME (e.g. "pr-reviewer"), which is not
an iconable agent and rendered the unknown "?" glyph. Nesting under the
parent row already conveys identity.

* fix(agent-status): restore displaced lead state and reconcile phantom subagents

Four review findings from the adversarial pass on the subagent child-row
feature:

- Stash the lead state a child-induced wait displaces
  (ClaudeLeadTurnState.stateBeforeWait) and restore it when the wait
  clears, instead of inventing 'working' — a lead that had already
  stopped left the pane spinning forever after the roster drained,
  since the done-gate only ever downgrades done → working.
- Tag snapshot-seeded and background_tasks-recreated roster entries
  (backgroundTasksAuthoritative) and demote them when a PRESENT
  background_tasks list omits their id. A phantom child seeded before a
  restart could otherwise gate the pane 'working' indefinitely in teams
  sessions, whose task list is never empty. Live activity clears the
  tag so lifecycle-tracked teammates keep their state.
- Match teammate ids with a hyphen-free suffix after `a<name>-` so
  TeammateIdle for "lane" cannot idle "lane-hooks"'s rows or clear its
  pending permission wait.
- Route turn-boundary events (Stop/StopFailure/UserPromptSubmit) that
  carry a KNOWN child agent_id through the child-driven re-emit instead
  of adopting them as lead state, and tie the prompt-cache new-turn
  reset to lead-origin events so child refreshes can't blank the
  prompt label.

---------

Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
2026-07-10 23:34:49 -07:00
4797cf81a4 fix(agent-hooks): stream hook payload to a temp file instead of inlining it on the curl command line (#4475)
The POSIX agent-hook script for every curl-based agent inlined the full
event payload via `curl --data-urlencode "payload=${payload}"`. Tool
output can be tens of KB, so the resulting process command line could be
multi-KB — which endpoint security tools (e.g. Microsoft Defender for
Endpoint) flag as an oversized/suspicious command line. That produced a
false-positive detection on Orca's own loopback (127.0.0.1) telemetry POST.

Stream the payload to an mktemp file and post it with
`--data-urlencode "payload@$payload_file"` instead. The urlencoded body on
the wire is byte-identical, so the agent-hook receiver is unchanged; the
payload simply never appears on a process command line. `trap ... EXIT`
removes the temp file on every exit path. Small bounded metadata fields
(paneKey/tabId/worktreeId/env/version) stay inline.

Applied to all curl-based agents: claude, codex, command-code, copilot,
cursor, droid, gemini, grok, antigravity. (amp/hermes/opencode post via
the HTTP request body and were never affected.) The Windows post-command
shares the same latent pattern and is flagged as follow-up.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-03 16:26:53 -07:00
Eren Çakar 801effa68e fix(claude): use Git Bash-safe hook fast path
Fixes #7116.

Use a forward-slash bare .cmd path for Claude/OpenClaude Windows hooks when the managed script path is Git-Bash-safe, avoiding the per-hook PowerShell startup overhead in the common case. Unsafe paths with spaces or shell metacharacters continue to use the encoded PowerShell launcher from #6078.

Keep Codex on its cmd.exe-safe fast path and leave the other agent integrations on the hardened encoded launcher.

Co-authored-by: Eren Çakar <hey@erencakar.com>
2026-07-02 15:42:01 -07:00
NeilandOrca 46646d7ff1 chore(lint): upgrade oxlint to 1.71 + enable 7 new rules (autofixed backlog) (#6841)
* chore(lint): upgrade oxlint to 1.71 and enable 7 new rules

Upgrade oxlint 1.67.0 -> 1.71.0 (1.72 was blocked by the repo's 3-day
minimum-release-age supply-chain guard; nothing here needs it). The
bump is a no-op on the existing config.

Enable 3 error rules (backlog autofixed to zero in this commit) and
4 warn rules (surface signal without gating CI):

error (autofixed, behavior-preserving):
- unicorn/prefer-node-protocol        (~1531 sites: bare builtin -> node:)
- typescript/no-import-type-side-effects (~36: all-inline-type -> import type)
- unicorn/no-array-reverse            (19: copy-then-reverse -> toReversed)

warn (real signal, current fires are test-only/correct):
- unicorn/no-array-fill-with-reference-type  (aliasing footgun guard)
- typescript/no-unsafe-function-type         (bans bare Function type)
- unicorn/prefer-array-flat-map              (map().flat() -> flatMap())
- unicorn/prefer-regexp-test                 (.match() in bool ctx -> .test())

mobile/.oxlintrc.json extends root, so it inherits all 7; the autofix
ran from root and covered mobile/ too.

Verification (all green): oxlint 0 errors (root+mobile+aux configs),
oxfmt clean, typecheck (node+cli+web), vitest 22795 passed / 0 failed,
builds (electron-vite + web + cli) succeed. node: rewrites confirmed to
skip embedded SSH/CLI string payloads (AST-only); all toReversed sites
verified to operate on fresh copies or write-once locals.

* chore(lint): bump mobile oxlint to 1.71 so inherited rules parse

mobile/ is a standalone pnpm project pinning its own oxlint@1.67, which
lacks unicorn/no-array-fill-with-reference-type (needs >=1.70). Since
mobile/.oxlintrc.json extends the root config, mobile CI's 'cd mobile &&
oxlint' failed to parse the new rule. Bump mobile to match root (1.71).

Verified in mobile/: oxlint 0 errors, oxfmt --check clean, tsc --noEmit
pass, vitest 978 passed / 0 failed.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-06-29 22:38:29 -07:00
Brennan BensonandNeil b6ac7b9c92 Harden Windows hook command resolution (#6409)
* Keep spaced Windows Codex hook paths fast

* Harden Windows hook command resolution

---------

Co-authored-by: Neil <neil@stably.ai>
2026-06-25 22:43:25 -07:00
d0d257189c Speed up Windows Codex/Claude hooks without dropping events (#6402)
Windows runs managed hooks through a shell, and #6078 wrapped the
launcher in `powershell -EncodedCommand` to survive spaces in profile
paths. Combined with the inner PowerShell Invoke-WebRequest post, every
hook spawned two PowerShell processes (~300ms startup each), so a hook
took ~650ms+ and fired up to 6x per turn. Codex 0.140 renders that as
lingering "Running <event> hook" rows. The earlier RC worked around it
by deleting SessionStart/UserPromptSubmit/Stop, which loses lifecycle
status fidelity.

Keep all six Codex events and make them fast instead:

- Codex runs hooks as `cmd.exe /C <command>` and forwards our string
  verbatim when it has no spaces/quotes, so emit the bare .cmd path for
  cmd-safe profiles (zero shell startup) and fall back to the encoded
  PowerShell launcher only for spaced/metachar paths (#6078 robustness).
- Replace the inner PowerShell post with curl.exe (Windows 10 1803+),
  posting the same form fields as the POSIX hook and reading the raw
  payload from stdin via `--data-urlencode payload@-` so UTF-8 (e.g. CJK)
  survives without code-page translation.

Result: the common Codex case is 0 PowerShell (~70-150ms vs ~650ms);
spaced-path profiles drop to 1.

Claude runs hooks through Git Bash, so its launcher must stay
PowerShell-encoded (a bare path is what breaks it), but its inner post
moves to curl.exe too, cutting Claude from two PowerShell startups to
one.

Validated against the real Codex 0.140 binary (interactive TUI + exec):
all managed hooks fire, render briefly, and clear with no lingering
rows; the listener receives every post in 1-6ms.

Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 20:53:58 -07:00
Brennan Benson e03a1cf769 Prevent managed agent hooks from hanging (#6148) 2026-06-23 17:22:10 -07:00
Jorge SilvaandJinwoo Hong 1fde7f553c fix(agent-hooks): wrap Windows hook commands in cmd.exe to survive spaces in profile path (#6078) (#6083)
* fix(codex): wrap Windows hook command in cmd.exe to survive spaces in profile path (#6078)

Windows splits raw hook commands on whitespace, so a user profile path
like `C:\Users\Jane Doe` made Codex hooks exit with code 1. Add a
wrapWindowsHookCommand helper that invokes the .cmd through
`cmd.exe /d /c call "..."` and use it in getManagedCommand.

* fix(agent-hooks): wrap Windows hook command in cmd.exe for all agents with raw .cmd path (#6078)

Apply the wrapWindowsHookCommand helper to cursor, command-code, gemini,
grok, and droid, which shared the same raw-scriptPath-on-Windows pattern
as codex. A user profile path with a space (e.g. `C:\Users\Jane Doe`)
used to split at the space and fail with exit code 1.

Agents that already handle spaces correctly are left untouched:
- claude/openclaude (Git Bash + forward slashes)
- copilot (PowerShell with quoted path)
- kimi (Git Bash + forward slashes)
- antigravity (event-specific wrapper .cmd files)
- devin (already wraps via `cmd /d /s /c ""...""`)

Each fixed agent gets a Windows-only test asserting the cmd.exe wrapping
survives spaces in the profile path.

* fix(claude): wrap Windows hook command in cmd.exe to survive spaces in profile path (#6078)

Claude Code runs hooks through Git Bash on Windows. The previous
forward-slash trick only works when the path has no spaces — Git Bash
splits `C:/Users/Jane Doe/...` at the space and tries to execute
`C:/Users/Jane` as a command. Use wrapWindowsHookCommand so the .cmd is
invoked through `cmd.exe /d /c call "..."`, which Git Bash treats as one
argument. Applies to both Claude and OpenClaude (shared getManagedCommand).

* Harden Windows agent hook launcher

---------

Co-authored-by: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com>
2026-06-22 16:04:01 -07:00
Brennan BensonandOrca fe8fe16c83 Preserve agent settings when sleeping sessions resume (#5916)
Co-authored-by: Orca <help@stably.ai>
2026-06-21 16:44:58 -07:00
slashdevcorpse f80704cc60 [perf] Speed up CLI launch through Orca
Speed up Windows CLI launch by delivering short startup commands through shell args, falling back to stdin for oversized commands, and moving Codex historical session bridging into an incremental background task. Includes review fixes for Windows symlink and WSL test reliability.
2026-06-18 22:21:48 -07:00
08ba730d8e feat(devin): managed hooks, sleeping resume, AI Vault (#5380)
* Revert "fix(terminal): add proportional scroll fallback for sidebar resize" (#937)

* fix(sidebar): smoothly animate off-screen worktree reveal on click (#1302)

Clicking a worktree card whose row lies outside the sidebar viewport
caused an instant jump when scrolling it into view. Switching
`scrollToIndex` to `behavior: 'smooth'` turns that minimum-distance
scroll into an animated slide while keeping `align: 'auto'` so visible
cards still no-op (no re-centering).

Co-authored-by: Orca <help@stably.ai>

* Avoid local scrollback serialization on shutdown (#1821)

* Fix PR refresh coordinator test arguments (#2545)

* release: v1.4.31

* release: v1.4.31

* release: v1.4.31

* release: v1.4.31

* release: v1.4.31

* release: v1.4.31

* release: v1.4.31

* release: v1.4.36-rc.6

* release: v1.4.36-rc.6

* release: v1.4.36-rc.6

* ci: gate release-cut to the canonical repo so it skips forks (#4815)

The cut job checks out main, bumps package.json's version, and
fast-forwards main. On a fork with Actions enabled, the scheduled RC
cut runs against the fork's main and diverges it on the version line
every slot, so that contributor's PRs back to upstream conflict on
package.json even when their change never touches it.

Gate the job to github.repository == 'stablyai/orca' so it (and the
jobs that depend on it) no-op on forks. Canonical scheduled and manual
cuts are unaffected.

* feat(hooks): install Devin managed status hooks

* feat(devin): address hook review, resume, and UI polish

- Parse Devin config.json as JSONC; warn on read_config_from overlap
- Windows hook command uses forward slashes; APPDATA fallback
- Add devin to sleeping-agent resume and UI registries (plan 003/004)
- Add hook-service and hook-config-json tests

Closes follow-up for plans 002–004 on feat/add-devin-agent.

* feat(devin): scan ATIF transcripts for AI Vault

Register devin in AI_VAULT_AGENTS, discover ~/.local/share/devin/cli/transcripts
(or DEVIN_HOME), parse ATIF JSON sessions, and build devin --resume commands.

* docs(devin): clarify stdin-after-start vs bracketed paste

* fix(devin): use JSONC for remote install, add partial+APPDATA tests

- installRemote: replace readHooksJsonRemote (JSON.parse) with
  readTextFileRemote + parseJsonc for JSONC compatibility on SSH
- Add partial status test (some hooks missing → state:'partial')
- Add Windows APPDATA config path test with fallback

* fix(devin): address CodeRabbit review — sessionId fallback, parseJsonc errors, comment, i18n

* Fix Devin integration edge cases

Co-authored-by: Orca <help@stably.ai>

* Package Devin JSONC parser dependency

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Trevin Chow <trevin@trevinchow.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
2026-06-17 20:49:29 -07:00
0ec3882cb8 Add project Windows runtime selection (#5519)
* Add project Windows runtime selection

* Fix project Windows runtime selection

Co-authored-by: Orca <help@stably.ai>

* fix: preserve WSL shell variables

---------

Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Neil <neil@stably.ai>
2026-06-17 16:08:14 -07:00
Neil d1b9392b49 perf: cap generated hook posts (#4166)
Generated agent hook POSTs now use short curl and PowerShell request timeouts so best-effort status hooks cannot hold agent subprocesses open on a stalled local listener.
2026-05-31 06:40:02 -07:00
Rajvardhan Patil a00c96ff03 Add OpenClaude agent support
Adds OpenClaude as a distinct CLI agent across detection, launch, settings, status, hooks, orchestration, telemetry identifiers, notifications, and README badges. Installs OpenClaude hooks under its own ~/.openclaude config root, handles StopFailure API/model-error events so statuses clear correctly, and keeps OpenClaude tab/status icons distinct from Claude.
2026-05-30 02:46:28 -07:00
Brennan BensonandOrca add481978e Revert Claude scoped settings launch path (#2809)
Co-authored-by: Orca <help@stably.ai>
2026-05-25 19:03:14 -07:00
Neil d4703bd1ab Restore agent hook opt-out controls (#2778) 2026-05-25 10:37:22 -07:00
Neil 28ba9151a5 Revert "Add agent hook opt-out controls" (#2739) 2026-05-23 23:04:57 -07:00
Neil 4f81953d10 feat: add agent hook opt-out controls (#2714) 2026-05-23 21:16:49 -07:00
Neil 675b1b0cb6 Fix Windows agent hook Unicode prompts (#2100) 2026-05-16 13:32:23 -07:00
Brennan BensonandOrca 2cadcbc4e1 Support agent status hooks over SSH (#1865)
Co-authored-by: Orca <help@stably.ai>
2026-05-14 16:06:23 -07:00
Brennan BensonandOrca 727226fed9 fix(agent-hooks): write managed hook script to shared ~/.orca path (#1546)
Co-authored-by: Orca <help@stably.ai>
2026-05-14 11:57:48 -07:00
Brennan BensonandOrca c375f403ef fix(agent-hooks): silence exit-127 spam from stale managed hook entries (#1536)
Co-authored-by: Orca <help@stably.ai>
2026-05-07 13:05:30 -07:00
2c7dddbbc4 fix(terminal): dirty-file close prompts for split, bulk, and quit flows (#1338)
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: shamounY <shamoun.yousuf04@gmail.com>
2026-05-01 21:39:15 -07:00
Leynier Gutiérrez GonzálezandAmp 6faf406511 fix(claude-hook): use forward slashes in Windows managed hook command path (#1329)
Co-authored-by: Amp <amp@ampcode.com>
2026-05-01 18:50:22 -07:00
Brennan BensonandOrca 4279fc8075 feat(agent-hooks): on-disk endpoint discovery for surviving PTYs (v2) (#1196)
Co-authored-by: Orca <help@stably.ai>
2026-04-27 21:49:18 -07:00
Brennan Benson 6675138d80 feat(agent-hooks): shared types, renderer store, local hook server + Claude/Codex/Gemini/OpenCode wiring (gated) (#1019) 2026-04-24 15:13:58 -07:00