mirror of
https://github.com/stablyai/orca.git
synced 2026-10-07 16:02:29 +00:00
Three fixes, all on paths this PR could not exercise locally. The managed hook command was stored as a bare path. jcode tokenizes that string shell-style before exec'ing it directly (parse_hook_command, crates/jcode-terminal-launch/src/lib.rs): unquoted whitespace splits, and every unquoted backslash is consumed as an escape. So on Windows `C:\Users\me\.orca\agent-hooks\jcode-hook.cmd` reached exec as `C:Usersme.orcaagent-hooksjcode-hook.cmd` and no hook fired at all, and a POSIX home with a space split into two arguments. Store the path single-quoted (verbatim, backslashes included), falling back to double quotes for a path containing a single quote. Existing bare entries are already repointed by the stale-key path, and getStatus accepts both forms so the repair is not reported as a user-owned hook. The quoting helper was previously dead code that only tests called; the three production sites now use it. isJcodeManagedCommand also normalizes separators, since a `/`-only needle never matched a Windows entry. Commit-message generation feeds a staged patch to `jcode run` as the prompt — attacker-influenced text — while jcode's default profile exposes shell, read, write, and MCP. Pass `--tool-profile none`, which resolves to an empty allowed-tool set in jcode's config (base_allowed_tools), matching the read-only posture claude (plan) and codex (read-only) already take. docs/reference/jcode-hook-events.md was never actually in this PR: the repo ignores docs/** and tracks reference docs by allow-list only, so the captured-payload evidence four source comments point at was silently dropped. Allow-list it. Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>
148 lines
8.7 KiB
Markdown
148 lines
8.7 KiB
Markdown
# jcode hook events
|
|
|
|
What jcode actually emits, and why Orca's status mapping is shaped the way it is.
|
|
Everything below was captured from jcode **v0.87.1 (944f747e9)** by pointing every
|
|
`[hooks]` entry in `config.toml` at a script that appends `$JCODE_HOOK_PAYLOAD` to
|
|
a log, then running real turns. Re-capture before changing the mapping; do not
|
|
edit it from memory.
|
|
|
|
## The six events
|
|
|
|
jcode's `[hooks]` table (`crates/jcode-base/src/hooks.rs`) has six lifecycle
|
|
points. Five are **observers** — detached, fire-and-forget, they can never slow
|
|
the agent. One, `pre_tool`, is a **gate**: jcode spawns it, writes the tool input
|
|
to its stdin, and waits for it to exit before the tool runs.
|
|
|
|
| Event | When | Orca state | Notable payload fields |
|
|
| --------------- | -------------------------------------------- | ---------- | --------------------------------------------------------- |
|
|
| `session_start` | TUI open, attach, or `--resume` | none | `source` = `create`/`attach`/`resume`, `model` |
|
|
| `turn_start` | prompt submitted, before the model generates | `working` | `source`, `model` |
|
|
| `pre_tool` | before each tool call (gate) | `working` | `tool_name`, `tool_input` (argument JSON as a string) |
|
|
| `post_tool` | after each tool call | `working` | `tool_name`, `status`, `duration_ms`, `output_bytes`/`error` |
|
|
| `turn_end` | turn finished | `done` | `status`, `duration_ms`, `model`, `last_assistant_text`, `error` |
|
|
| `session_end` | session closed | `done` | `source` = `close` |
|
|
|
|
`session_start` is identity-only. jcode fires it on an idle TUI open, so mapping
|
|
it to `working` would spin before the user has typed anything (same reason Devin
|
|
does not map its `SessionStart`).
|
|
|
|
## Captured payloads
|
|
|
|
A `jcode run` turn that read one file and wrote another:
|
|
|
|
```json
|
|
{"cwd":"/private/tmp/jcode-work","event":"session_start","model":"claude-haiku-4-5","session_id":"session_pawprint_1790149117838_071c2a106812396c","source":"create"}
|
|
{"cwd":"/private/tmp/jcode-work","event":"pre_tool","session_id":"session_pawprint_…","tool_input":"{\"file_path\":\"sample.txt\",\"intent\":\"Read sample.txt to get its contents\"}","tool_name":"read"}
|
|
{"cwd":"/private/tmp/jcode-work","duration_ms":"0","event":"post_tool","output_bytes":"12","session_id":"session_pawprint_…","status":"ok","tool_name":"read"}
|
|
{"cwd":"/private/tmp/jcode-work","event":"pre_tool","session_id":"session_pawprint_…","tool_input":"{\"content\":\"HELLO\",\"file_path\":\"out.txt\",\"intent\":\"Write uppercased contents of sample.txt to out.txt\"}","tool_name":"write"}
|
|
{"cwd":"/private/tmp/jcode-work","duration_ms":"9","event":"post_tool","output_bytes":"137","session_id":"session_pawprint_…","status":"ok","tool_name":"write"}
|
|
```
|
|
|
|
A TUI turn that failed upstream (note `turn_start`, which the `run` path does not emit):
|
|
|
|
```json
|
|
{"cwd":"/private/tmp/jcode-work","event":"session_start","model":"claude-opus-5","session_id":"session_snail_…","source":"create"}
|
|
{"cwd":"/private/tmp/jcode-work","event":"turn_start","model":"claude-opus-5","session_id":"session_snail_…","source":"chat"}
|
|
{"cwd":"/private/tmp/jcode-work","duration_ms":"6868","error":"Anthropic API error (503 Service Unavailable): …","event":"turn_end","model":"claude-opus-5","session_id":"session_snail_…","status":"error"}
|
|
```
|
|
|
|
Three consequences the mapping depends on:
|
|
|
|
- **`turn_start` only fires on the streaming turn path** (TUI, desktop, swarm
|
|
workers, headless sessions), not `jcode run`. It is what fills the otherwise
|
|
blank window between a submitted prompt and the first tool call.
|
|
- **Only `pre_tool` carries `tool_input`.** `post_tool` reports the name and the
|
|
outcome, so the tool preview has to be held from the matching `pre_tool`.
|
|
- **Every jcode tool schema has an `intent` string** the model fills in. It is
|
|
the preview fallback when no tool-specific key (`file_path`, `command`, …)
|
|
matches.
|
|
|
|
## Why Orca subscribes to the gate
|
|
|
|
`pre_tool` is the only event that can report a tool *while it runs*. Without it a
|
|
three-minute `bash` shows no tool at all until it finishes. Two rules keep the
|
|
gate from ever costing the agent anything:
|
|
|
|
1. **The POST is detached.** jcode calls `child.wait_with_output()`, which waits
|
|
for the process *and* reads its stderr to EOF — a backgrounded child that
|
|
inherited stderr would hold the gate open for as long as it ran. The managed
|
|
script runs the POST as `orca_post_jcode_event >/dev/null 2>&1 &`, so the
|
|
inherited pipes are closed and the script exits immediately.
|
|
2. **stdin is drained first.** jcode `write_all`s the full tool input to the
|
|
hook's stdin. A tool input larger than the pipe buffer (a big `write`) would
|
|
block that write until the gate timed out if nobody read it, so the script
|
|
drains stdin before any exit path.
|
|
|
|
Orca never blocks a jcode tool call: the script always exits 0.
|
|
|
|
## Questions and permissions
|
|
|
|
jcode has **no interactive per-tool approval prompt**. Its safety model
|
|
(`crates/jcode-app-core/src/tool/bash_destructive_gate.rs`) either denies a
|
|
command outright or asks the model to justify it — both inside the tool, with no
|
|
human in the loop. There is therefore no hook, and no terminal-title state, for
|
|
"jcode is waiting on you" during ordinary tool use.
|
|
|
|
The one tool a *human* answers is ambient mode's `request_permission`
|
|
(`crates/jcode-app-core/src/tool/ambient.rs`), resolved out of band with
|
|
`jcode permissions`. Orca maps a `pre_tool` for it to `waiting` and publishes the
|
|
tool input as the question card. `post_tool` for the same tool is *not* mapped —
|
|
by then the human has already answered.
|
|
|
|
Matching is by exact tool name. jcode's live tool set is `agentgrep, apply_patch,
|
|
bash, batch, bg, browser, compile_remote, conversation_search, edit, gmail,
|
|
integration_tools, ls, macos_computer_use, maintainer_feedback, mcp, memory,
|
|
multiedit, open, panel, patch, read, schedule, session_search, side_panel,
|
|
skill_manage, swarm, todo, webfetch, websearch, write` plus the ambient tools;
|
|
a substring rule over that set would be matching on coincidence.
|
|
|
|
## Terminal titles
|
|
|
|
jcode paints OSC 0 titles roughly once a second. Captured sequence from one TUI
|
|
session:
|
|
|
|
```
|
|
jcode → 🐍 jcode Snake → 🐍 jcode/creek Snake → 🌐 jcode Snake · work ~0s → … → 🌐 jcode Snake · last ~6s
|
|
```
|
|
|
|
The format is `<emoji> jcode <session-name>[ · +N -M][ · work|last ~<duration>]`
|
|
(`crates/jcode-tui/src/tui/app/terminal_title.rs`). Orca uses it for tab-bar
|
|
identity only — status comes from hooks, never from a parsed title. Note there is
|
|
no "needs input" title state; that is the same gap as above, not an omission in
|
|
the parser.
|
|
|
|
## Per-pane daemons
|
|
|
|
jcode runs one server/client daemon per runtime dir, and lifecycle hooks fire
|
|
*inside the daemon*. Every TUI client connects the daemon the first pane started,
|
|
so without isolation a second jcode pane's events carry the first pane's
|
|
`ORCA_PANE_KEY` and its status lands on the wrong tab.
|
|
|
|
jcode does forward a client's terminal identity to hooks
|
|
(`CLIENT_TERMINAL_ENV_VARS` in `crates/jcode-terminal-launch/src/lib.rs`), but
|
|
that allowlist covers tmux/zellij/herdr and the terminal emulators — not
|
|
`ORCA_PANE_KEY`. Until it does, Orca stamps a per-pane `JCODE_RUNTIME_DIR` so
|
|
each pane gets its own daemon, socket, and lock. The value is a 16-hex hash of
|
|
the pane key because the socket path is capped at `SUN_LEN` (104 bytes) and a
|
|
full pane key never fits.
|
|
|
|
## Config shape
|
|
|
|
`[hooks]` values accept a string or an array of strings (`HookCommands` in
|
|
`crates/jcode-config-types/src/lib.rs`), and jcode re-reads the config on reload,
|
|
so hooks can be added without restarting. jcode parses a hook command line
|
|
shell-style but **executes it directly, not through a shell** — the managed value
|
|
must be the script path, never an `if [ -f … ]` wrapper.
|
|
|
|
That shell-style parse is `parse_hook_command`
|
|
(`crates/jcode-terminal-launch/src/lib.rs`), and it is why Orca stores the path
|
|
**shell-quoted**. The tokenizer splits on unquoted whitespace and consumes every
|
|
unquoted backslash as an escape, so a bare Windows path reaches `exec` as
|
|
`C:Usersme.orcaagent-hooksjcode-hook.cmd` and no hook fires at all; a POSIX home
|
|
with a space splits into two arguments. Single quotes pass a path through
|
|
verbatim — backslashes are literal inside them — so Orca single-quotes by
|
|
default and falls back to double quotes (escaping `\` and `"`) only for a path
|
|
that itself contains a single quote. The value is then TOML-quoted on the way
|
|
into the file, so neither the raw path nor the shell-quoted string appears
|
|
alone.
|