Commit Graph
137 Commits
Author SHA1 Message Date
Daniel Kim 2dd67bc2ca UI design-consistency pass: token scales, sentence case, sans-first, rounded everywhere (v0.1.68) (#103)
* ui: add type/weight/radius token scales, drop dead CSS rules

* ui: drop all uppercase transforms and tracking, sentence-case status strings

* ui: reserve mono for code, paths, ids and terminal output

* ui: primary content uses --text; grays reserved for true secondary text

* ui: normalize font weights onto the --fw-* scale

* ui: normalize radii and type sizes onto token scales, round callouts and inputs

* ui: pin badges to sans in mono contexts, un-mono instance durations; rebuild dist

* ui: sentence-case remaining status strings, fix stale ModelPicker comment

* ui: promote composer reasoning pill to --text

* ui: shared Wordmark component — logo mark + all-black OpenResearch text

* ui: match home wordmark weight to the landing page (800 -> 700)

* chore: bump version to 0.1.68
v0.1.68
2026-07-20 14:59:21 -07:00
Daniel Kim 0a42fb8b28 Onboarding tour: spotlight walkthrough of the workspace (#102)
* Onboarding tour: spotlight walkthrough of the workspace

Seven-step tour (welcome, composer, model picker, Files, Compute,
experiments panel, new session) shown once after first-run setup and
replayable from a help button in the chat header. Ported from the
openresearch.sh onboarding system: data-onboarding attribute targeting,
ResizeObserver/MutationObserver bounds tracking, box-shadow spotlight
dim, viewport-clamped card with pointer arrow. Completion persists to
localStorage (orx:tour-done). Escape is captured so it can't interrupt
a running agent turn.

* Tour polish: exact-primary ring, data-tip tooltip on replay button

The ring's 45%-alpha glow plus a 1.5px border read as a lighter red than
the theme primary; a crisp 2px border with no glow matches it exactly.
The header help button now uses the app's data-tip tooltip instead of a
native title.
2026-07-20 14:24:24 -07:00
Daniel Kim 726ce566b4 Local-mode skill variants, slimmer playbook, no-merge/rebase rule, first-run setup questions (v0.1.67) (#101)
* Local-mode bodies for compute + experiment-tree skills; slim and extend the playbook

- agent-skills/orx-compute/SKILL.local.md and orx-experiment-tree/SKILL.local.md:
  local-mode variants (same public names) that drop managed-SKU compute,
  cache-dir clone flow, and locally-unavailable commands (orx experiments,
  exp cmd, artifacts, query, chart); orx-compute also dedupes the per-backend
  shared-contract boilerplate into one section.
- src/local/agent_skills.rs: serve the local bodies in SkillSet::Local via
  _LOCAL/_CLOUD consts sharing one description each.
- SYSTEM_PROMPT.md: fold 'Where runs execute' into cardinal rule 5, drop
  content now covered by skills (baseline para, lit para, report layout,
  supervise note); add cardinal rule 6 (never merge/rebase a branch with a
  completed non-failed run; child + merge commit; never rebase) and a
  'Learn how the user runs their code' section (ask about env/deps before the
  first launch on a fresh project; two env failures -> stop and ask). The
  no-merge/rebase rule is echoed in orx-git's rules list.

* chore: bump version to 0.1.67
v0.1.67
2026-07-20 10:40:55 -07:00
Daniel Kim aba90b6298 Files tab redesign: split explorer with inline preview (#100)
Replace the pill-header list + full-page report swap with a master-detail
split: a resizable file tree on the left (folders open by default, report
folders titled by their report, dir path demoted to a copyable footer) and
a persistent preview pane that renders everything inline — reports and
markdown as GFM docs with a view-source toggle, images and PDFs directly,
other text as highlighted code via the new shared CodeView (also adopted
by FileViewer), with an Open-raw fallback for binary/oversize files.

Icon-only controls get a CSS data-tip tooltip primitive; empty states for
no-files and nothing-selected; FilesTab keyed by project and PreviewPane
keyed by path so state can't leak across switches.
2026-07-20 10:36:50 -07:00
Daniel Kim a72c4cdc2b feat(codex): native plan mode as a selectable permission mode (v0.1.66) (#98)
* feat(codex): native plan mode as a selectable permission mode

Codex ≥ 0.144 supports plan mode as an app-server collaboration mode.
Advertise Plan + Auto + Bypass for codex and wire the full flow:

- initialize sends capabilities.experimentalApi (unlocks collaborationMode)
- conditional per-turn collaborationMode mask: plan on plan turns; default
  on a non-plan turn only when the thread may be sticky-planned (DB
  prev_permission_mode threaded via TurnCtx + in-memory last-sent tracking);
  otherwise no mask (a mask always injects a template)
- render streamed plan items (item/plan/delta + completed plan item) and
  synthesize an end-turn plan card / PlanStrip
- requestUserInput -> question card, answerable inline or via composer;
  per-kind server-request settle shapes (approval/userInput/other)
- resume_from_prompt: plan (send-message), permission/question (inline reply)
- store: keep 'plan' off the codex retire-list; reuse claude.rs
  synthesize_resume / should_synthesize_plan via harness re-export
- UI: PlanStrip agentLabel; ChatPanel pendingQuestion gate includes codex

Legacy exec path (< 0.144) keeps Plan->read-only, with an updated agent note.

* fix(chat): keep prompt-resume scaffolding out of the transcript

Plan/permission card resumes send synthesized text the user never typed
("Implement the plan.", "The user approved that action. Continue.").
Persisting it as a user message misattributes it in the UI. Split the
transcript text from the harness text in send_message (the slash-skills
precedent): plan/permission resumes now record only the user's own note
— nothing at all when there isn't one; the resolved card tells the rest.
Question resumes still record the answer, which the user did choose.

* chore: bump version to 0.1.66
v0.1.66
2026-07-19 20:22:35 -07:00
Daniel Kim 9be46cd4dd Telemetry: delineate human vs automated installs; kill consent phantom persons (#99)
* Delineate human vs automated installs in telemetry; kill consent phantoms

- Every event now carries an install_kind base property: "human" by
  default, or the machine context set via ORX_TELEMETRY_CONTEXT / the new
  `orx telemetry context <value>` (persisted in settings.json), so
  first-party automation (cloud agent boxes) can be excluded from human
  usage metrics centrally via PostHog's internal-users filter.
- cli_telemetry_consent no longer mints a fresh UUID person per event:
  agrees use the persisted install id (joinable with the active-install
  population), declines collapse into one fixed sentinel id.

* Reattach record_consent docs after helper extraction; doc nits
2026-07-19 20:11:27 -07:00
Daniel Kim 087fde79af Contain file serving to the files dir even through symlinks (#97)
* Contain file serving to the files dir even through symlinks

is_safe_rel_path only blocks lexical escapes (..); a symlink inside the
files dir pointing outside it was followed by read_file,
read_report_markdown, and the report-title scan, so anything reachable
on disk could be served to whatever can reach the local API.

Resolve every served path with canonicalize() and require it to stay
under the canonicalized files dir. Internal symlinks keep working and
now also appear in listings (DirEntry::metadata never follows links, so
they were silently dropped before); escaping ones are skipped from the
listing and fingerprint, and refused by the serve endpoints. Delete
resolves parent segments the same way but removes a final-component
symlink literally, never its target.

* Review nits: shared followed_metadata helper, drop redundant pre-check

Extract the symlink-aware metadata lookup shared by collect_tree and
hash_dir, let resolve_contained handle read_report_markdown's path
validation, and document the containment guarantee (and the accepted
check-vs-use gap) at module level.
2026-07-19 17:41:14 -07:00
Daniel Kim 66c8159511 Match the web app's favicon and retitle the tab to OpenResearch CLI (#96)
Ports favicon.svg from openresearch.sh so the embedded UI's browser tab
shows the same brand mark, and retitles the tab "OpenResearch CLI" to
distinguish it from the hosted web app. favicon.svg joins index.html in
the no-cache branch since it's served under a fixed (unhashed) name.
2026-07-19 17:29:38 -07:00
Rehaan Ahmad 67190285ab chore: support automated reproductions (#95) 2026-07-19 13:56:43 -07:00
Rehaan Ahmad 2c72297604 chore: add marimo reproduction notebooks (#94) v0.1.65 2026-07-19 13:51:16 -07:00
Rehaan Ahmad 99f358c5fe chore: neutral reproduction assessments (#93)
* chore: neutral reproduction assessments

* chore: bump version to 0.1.64

* chore: plan reproductions to compute budget
v0.1.64
2026-07-18 18:12:58 -07:00
Rehaan AhmadandClaude Fable 5 87df17ce0f chore: Report md heading styles (#91)
* Report pane: restore Markdown heading hierarchy in report-md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Bump version to 0.1.63

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:24:55 -07:00
Rehaan Ahmad 3010c8cea6 Guide reproduce-paper toward visual reports (#92)
* Guide reproduce-paper toward visual reports

* Encourage evidence-rich report figures
2026-07-18 13:24:46 -07:00
Daniel Kim 1526d78a14 ControlPlane trait: plane-agnostic dual-mode commands (v0.1.63) (#90)
* ControlPlane trait: hoist dual-mode command bodies behind two planes

Phase 3 (final) of the control-plane separation. Adds src/plane.rs — a
#[async_trait(?Send)] ControlPlane trait with 14 verbs derived from the
six dual-mode commands' paired arms — with ServerPlane (wraps client.rs,
connects via require_credentials on the server arm only, with a sync
ServerPlaceholder so pre-login guards keep firing before the login
check) and LocalPlane (owns the Store; ?Send because rusqlite is !Sync —
planes are driven inline under block_on and never spawned).

The six commands collapse to resolve-once-then-verb; their local/server
bodies move verbatim into the plane impls. Printing stays in the impl
where the two planes differ (status blocks, launch recaps) and in the
command where shared (runs table, logs footer).

Domain type plane::Run (+RunLog/RunOrigin) merges the two
run_failure_detail twins — output.rs no longer depends on client::Run.
CLI-facing only: local::model (the up-API wire), the SQLite schema, and
the client.rs DTOs are untouched, verified by byte-identical golden
snapshots of the affected commands on a seeded isolated store.

The plane lives outside src/local/ because local/mod.rs's invariant
(nothing there calls client.rs) would otherwise break. up.rs and
supervise.rs stay off the trait as the deliberate cloud-mirror bridges.

* Bump version to 0.1.63
v0.1.63
2026-07-17 20:22:20 -07:00
Daniel Kim a0b6f3ea5f Persistent cross-session agent memory (user + project memory files, {memory} playbook token) (#89)
* Persistent cross-session agent memory: user + project memory files inlined via {memory} playbook token

* Mention files/ + memory/ in datadir module doc
2026-07-17 19:53:25 -07:00
Daniel Kim 8d07767b2d Control-plane resolver: centralize local-vs-server dispatch (phases 1+2) (#88)
* Centralize local-vs-server control-plane dispatch in a resolver

Commands used to hand-roll the "is this id a local project/run or a
server one?" decision as a copy-pasted get_local_project / local_run
fork across 14 files. Add local::resolve — ProjectRef/resolve_project
and RunRef/resolve_run (which reuses local::local_run, since store
membership alone is not enough: server HF runs also live in the runs
table) — and route every dispatch site through it. Reject-guard sites
use the is_local() guard form; dual-mode sites carry the fetched
project through the Local arm, so no second store lookup.

up.rs and supervise.rs keep their direct store calls deliberately:
those are lookups and the cloud-mirror bridge, not ownership dispatch.

Store::open_at(dir) lets the resolver's tests run against a throwaway
temp-dir store without mutating the process-global ORX_DATA_DIR, which
the localbox lifecycle test owns under the parallel test runner.

Phase 1 of the control-plane separation plan; a later phase abstracts
the per-arm bodies behind the resolver.

* Phase 2: total-match dispatch + ExperimentRef for dual-mode commands

Convert the six dual-mode commands (exp, runs, logs, project, report,
create-experiment) from local-early-return guards to a uniform total
match over the resolver, with server bodies extracted into named
run_server-style fns — the mechanical hoist target for the later
ControlPlane trait. Adds ExperimentRef/resolve_experiment (keyed on
local_experiments) and routes exp.rs's five experiment-scoped dispatch
sites plus both wait branches through it.

Bodies are unchanged; credentials stay inside the server arms so local
mode keeps working logged-out. Verified end-to-end against an isolated
data dir + config (local arms serve from the store; server arms stop at
the login check without touching the api).
2026-07-17 19:16:52 -07:00
Daniel Kim f633b80f9c Native modular agent skills: per-session worktree install + orx skill modules (v0.1.62) (#87)
* Native modular agent skills: per-session worktree install + orx skill modules (v0.1.62)

- Factor the 66KB SKILL.md into 10 embedded modules under agent-skills/
- Write the Local set as native SKILL.md skills into each orx up session
  worktree (harness-specific dir), fresh every turn beside the playbook
- Slim the playbook: compute/k8s/report detail moves to skills, generated
  skills index added; cardinal rules and command surface stay inline
- orx skill <name> serves bundled modules (API fallback unchanged);
  no-arg prints the slim root + module index
- orx install-skills --full writes the Full set globally (cloud box);
  Codex shim migrates to ~/.agents/skills (native, model-discoverable),
  legacy prompt kept

* agent-skills: store modules as literal SKILL.md files, installed verbatim

Each module is now a complete, GitHub-readable skill at
agent-skills/<name>/SKILL.md (frontmatter included); local-mode body
variants live as SKILL.local.md siblings. The render() step is gone —
files are embedded and installed byte-for-byte, and the frontmatter
drift test now validates the files against the code's name/description.

* Revert incidental ui/dist rebuild (no UI source changes in this branch)

* Drop leftover rebuilt dist assets — ui/dist now matches origin/main exactly

* agent-skills: trigger-rich descriptions with explicit liberal 'Use when' cues

Descriptions are the model's trigger surface; err toward false-positive
loads. Bound raised 150 -> 400 chars (Codex ambient budget is ~8k across
the set).

* Playbook: dedupe against skills + extract to literal SYSTEM_PROMPT.md

The system prompt is now a GitHub-readable template at repo root; playbook_md
strips the leading doc comment and substitutes {tokens} (project facts,
compute default, skills index). Content duplicated by the installed skills is
trimmed to pointers: the five per-backend exp-run table rows collapse to one,
the loop keeps only its skeleton, and the sizing/analysis sections fold into
one-liners. The prompt now explicitly tells the agent to load the relevant
skill before acting in its area (long-horizon staleness guard). Placeholder
test extended to all ten tokens + comment-stripping.

* orx-compute skill: add missing slurm + openresearch backend sections

The playbook trim removed the slurm/openresearch table rows, but orx-compute's
body (extracted from the old SKILL.md) never had sections for them — the
detail existed only in the playbook. Content moved from the pre-PR playbook
rows in the same per-backend section style. Also restore the shell-tool
time-limit rationale on the wait loop step in SYSTEM_PROMPT.md.
v0.1.62
2026-07-17 17:47:24 -07:00
Daniel Kim 012672958d Plan-mode chat: AskUserQuestion double-card/hang fix + desktop-parity plan/question cards (v0.1.61) (#84)
* Fix harness prompt bugs: AskUserQuestion double-card + hang, resolved-card rendering

- plan_auto_policy auto-allows AskUserQuestion: the bridge no longer fronts
  the question card with a redundant permission card whose held request also
  deadlocked the end-turn resume (answer rejected as busy until interrupt).
- respond() resolves a card only after its answer is delivered, so a rejected
  send leaves the card actionable instead of resolved-with-the-answer-dropped.
- Resolved cards mirror Claude Code: permission cards vanish, plan/question
  cards collapse to compact inline expandable rows with an answer echo
  (answers/approved/note stamped on resolve, carried through turn flushes).

* Review round 1 fixes: doc split, echo-preserving adopt, neutral outcome, read-only class rename

* Review round 2 fixes: PendingGuard doc, best-effort resolve folded into helper, neutral question echo

* Post-review nits: doc wording, dedupe question text in collapsed body, word-break on collapsed rows

* Plan-mode AskUserQuestion truly blocks: bridge holds the call as the question card

Auto-allowing AskUserQuestion was wrong: headless the tool returns no
answer, so the model guessed and kept planning instead of waiting. The
bridge now surfaces the QUESTION itself as a held mid-turn card (like
ExitPlanMode's plan card); the answer settles the held call as a denial
whose message is the answer, so the model continues the same turn with
the user's actual choice. The turn-status line says 'Waiting for your
input…' instead of 'Working…' while a card holds the turn open.

* Post-review nits: deny-message hardening, question cards don't suppress plan-synth, empty-options fallback, held-card status via nativeId, comment accuracy

* Polish: no tool row for bridge-held calls (deny-with-answer painted them red), plan pane gets padding + plain background

The bridged AskUserQuestion/ExitPlanMode tool_use now renders nothing —
the held card is the representation, and the denial that carries the
user's answer back made the CLI report the tool as errored, painting a
successful Q&A as a red error row. The right-pane plan tab had no CSS
at all: flush text on the panel tint; it's a markdown document, so it
gets a padded, plain reading surface.

* PlanStrip: Claude-desktop shape — title line, real buttons in a row below

The strip's buttons used .btn-primary/.btn-ghost, which are only styled
inside .prompt-actions — in the strip they rendered as bare text crammed
next to the title. The actions row now reuses .prompt-actions (same
buttons as every card) and sits on its own line under the title.

* PlanStrip buttons: monochrome — black text/borders, no brand red

* PlanStrip split button: single internal divider (caret drops its own left border)

* PlanStrip split button: flat inner corners (radius reset must out-rank .prompt-actions shorthand)

* Composer: attention bell instead of Stop while a held card awaits the user

A blocked-on-input turn is busy, so the composer showed the Stop X — a
loading/cancel affordance while the agent is actually waiting on an
answer. When the session is busy AND a held card (nativeId) is
unresolved, show a pulsing bell that scrolls to the pending card; Esc
still interrupts. The transcript status line shares the same
awaitingInput signal.

* Composer: two states only — Send by default, Stop X only while actively streaming

Drop the bell experiment. A turn held on an unanswered card shows the
idle Send button; the card and the status line are the waiting
affordance. Esc still interrupts a held turn.

* Composer answers the pending card (Claude-desktop): typed text = plan revision feedback / custom question answer

- send() routes plain typed text to the newest unresolved plan/question
  card (respond with note) instead of racing the held turn with a new
  message the busy guard would drop; placeholder says so. Questions
  route only on Claude sessions (opencode rejects note-only replies).
- Rail busy-dot stops pulsing when the turn is held waiting on the user
  (steady dot; same awaitingInput signal as the status line).
- Note-only question answers echo the note in the collapsed row.

* PlanStrip: Claude-desktop parity actions + popovers above the strip

Reject / Revise… / Accept / Accept-and-auto-mode (+ bypass in the caret),
Open plan as a title-row link. Reject is a true rejection: the note-less
plan denial now tells the model to stop and wait (was 'keep refining',
which made it improvise revisions/questions); collapsed rows echo
'Rejected' vs 'Revision requested' by note presence. Accept resumes
under accept-edits (guarded), Accept-and-auto under Auto — the primary
is the solid dark button, not brand red. The strip moved inside
.composer so the mode/model picker popovers (z 50 in that stacking
context) layer above it — as a sibling, .composer's z-index:4 capped
them below the strip.

* PlanStrip: single Accept-and-auto-mode primary, drop the plan excerpt

Matches the desktop reference exactly: [Reject] [Revise…] on the left,
one primary button on the right whose caret menu holds the two guarded
tiers (Accept = accept-edits, Accept and bypass all). The separate
outline Accept button and the plan-title excerpt next to the header
are gone — plan.excerpt/plan prop was fully unused after removal.

* PlanStrip: self-contained Revise mode (Claude-desktop parity)

Swaps the strip into its own textarea + [Back] [Revise ↵] footer
instead of routing through the main composer — matches the desktop
reference exactly. Enter submits, Shift+Enter newlines, Esc cancels
back to the action row. The main-composer typed-text routing
(pendingPrompt) is now question-only; plans have their own dedicated
UI and no longer need it.

* PlanStrip: drop the plain accept-edits Accept option

We have no partial-approval story (edits-only, everything else still
gated) — the caret now holds only Accept and bypass all.

* Plan revision: visible feedback + never silently dead-end

- Submitting a revision now swaps the strip to a 'Revising the plan…'
  placeholder until the revised card arrives (or the turn ends) —
  previously the strip snapped back to an identical-looking action row
  and the submit read as a no-op.
- An empty revise note maps to a generic 'Please revise the plan.'
  instead of riding the note-less wire shape, which the backend reads
  as a hard rejection ('stop and wait') — the '(optional)' field was
  accidentally a Reject.
- respond() reconciles messages AND busy state from the server after
  every answer: a stale card (e.g. resolved by the bridge timeout while
  the tab's SSE was dropped) previously stayed actionable forever and
  swallowed every answer.

* PlanStrip: drop the 'Revising the plan…' placeholder

The transcript's Working… spinner already communicates it. The strip
still hides while a submitted revision is in flight (the outgoing card
must not look actionable) and the revised card swaps in on arrival.

* Review round 1 fixes: zombie-card recovery, honest composer button, reject-loop fix

Blockers:
- A held card orphaned by a process restart (PendingGuard never ran)
  permanently captured typed input: the composer routed every message to
  it and errors were swallowed. Now the backend collapses the zombie on
  the first answer attempt, and the composer only routes typed text to a
  question card that is actually answerable (live turn for native cards).
- The Send/Stop swap keyed on awaitingInput while send() could only
  service question cards — a held plan/permission card showed a lying
  Send (silent no-op) with no Stop control. Now keyed on pendingQuestion.

Also: note-less Reject on an end-turn/synthesized plan card resolves
with no resume (resuming just to say 'stop' re-synthesized a card — an
un-dismissable strip); the bridge plan-deny arm reuses synthesize_resume
verbatim; respond() reconciles busy for its own session only and reports
failure so the question route can restore a consumed draft; revising
clear gated to its session; un-docked plan card fallback matches the
strip's semantics; Rejected gets its own outcome class; note-only
answers echo once; split-button CSS consolidated; dead CSS/union
removed; test pins the reject wording.

* Post-clean-round nits: keep optimistic busy on failed reconcile fetch, sweep zombie held cards at claude turn entry

- respond()'s busy reconcile no longer clears busy when the sessions
  fetch fails — during a Handled resume nothing re-asserts busy
  mid-stream, so a transient fetch failure would hide Working…/Stop for
  the rest of the turn.
- claude run_turn sweeps unresolved native_id cards at turn entry
  (resolve_stale_prompts gains a native_only mode; codex keeps sweeping
  everything). A held card can never outlive its process, so one left
  unresolved is a crash artifact — previously it could capture the
  composer's typed-text routing once the new turn made the session busy.

* Bump version to 0.1.61
v0.1.61
2026-07-17 00:15:45 -07:00
Daniel Kim 7450249cec Compute tab redesign: unified target list + default compute target (#86)
* Settings: persist default compute target (backend + flavor) with pure resolver

* Launch paths: resolve the default compute target; widen HTTP run to all 7 backends

* API: compute-targets summary, default-target POST, local hardware + openresearch status endpoints

* Agent guidance: reveal the default compute target in the playbook, SKILL.md, and skill templates

* Compute tab: unified target accordion with This-machine + OpenResearch rows and default picker

* cargo fmt

* Polish: summary casing, a11y-clean row controls, instances subtitle

* Review round 1: coherent no-flavor default prompt, 500 on settings io errors, UI dedup + summary refresh, doc fixes

* Clean-round nits: refetch race guard, aligned token-source labels, flavor-required const, doc accuracy

* Quiet the agent-disclosure footnote: muted text instead of amber callout

* Info icon on the agent-disclosure footnote
2026-07-16 23:38:04 -07:00
Daniel Kim 866ccd4ec0 Session code browser + GitHub branch links (#85)
* Session code browser + GitHub branch links

Backend: GET /api/projects/{id}/code-tree?sessionId= lists the chat
session's worktree via git ls-files (branch name included); factor the
worktree/clone root resolution shared with project_file into
resolve_checkout_root. Add current_branch + list_worktree_files git helpers.

UI: per-session Code right-panel tab (CodeTab) with a collapsible file
tree that opens files in the existing FileViewer; a Code button in the
chat header; GitHub tree/<branch> links (per-segment URL-encoded) on
experiment cards (TreeView), the Changes header (DetailDrawer), and the
Code tab header. Rebuild committed ui/dist.

* Code tab: browse experiment branches, not just the live worktree

Sessions usually sit detached on the baseline tip, so the worktree-only
view showed baseline code instead of the experiment's. The tab now has a
source select: live session worktree or any experiment branch's committed
tree (default: the branch of the latest run's experiment).

Backend: code-tree and file endpoints take an optional ref= param —
resolve_branch_commit (local ref, then origin; option-injection guarded)
+ list_tree_files (ls-tree) / file_at (git show); root:"branch" in
responses; 404 when the branch doesn't exist. Live mode unchanged.

UI: getCodeTree/getProjectFile take {sessionId?, ref?}; FileViewDef
carries the ref through FileViewer (branch badge, ref-aware not-found
copy); polling only in live mode; GitHub link follows the selected ref.
Rebuild committed ui/dist.

* Code tab review round 2: lift tab state, stream branch-file reads

- CodeTab source + expansion state move onto the App-side tab def
  (CodeTabDef.sel/selPicked/toggled): the component unmounts whenever a
  file tab fronts it, and remount must not rederive the default against
  newer runs or lose expansion. Default computed once in openCodeTab.
- file_at_capped: cat-file -e existence check (exit code, no stderr
  parsing) + streamed cat-file blob read capped at limit+1 bytes — a
  committed multi-GB blob no longer means a full-blob allocation, and
  content is byte-exact (no trim). Used by project_file's ref arm;
  file_at kept for its small-file callers.
- resolve_branch_commit rejects rev-suffix expressions (@{, ^, ~, :,
  whitespace); doc comments cross-reference branch_head_sha's opposite
  local/remote preference.
- CodeTab load lifecycle: request-id guard covers stale sources,
  superseded polls and post-unmount responses; manual Refresh surfaces
  errors (inline note when a tree is showing); a seeded default branch
  that doesn't exist yet falls back to live once, user picks keep the
  error. split_nul helper dedups the NUL-split decode. Copy/comment
  alignment. Rebuild committed ui/dist.

* Code tab post-review polish

- file_at_capped: reap the child before propagating read errors (no
  zombies); after a complete read, a failing cat-file exit (tree path
  via crafted request, vanished object) is an error, not a fake empty
  file. Kill only on read error/truncation — after EOF git exits on its
  own, and killing then could race a bogus signal-death status.
- list_worktree_files drops dir entries ('nested/' from a nested git
  repo under --others) server-side.
- openCodeTab's updater self-guards against duplicate defs.
- Seeded default branch selection falls back to live on any fetch
  failure (best-effort), not just 'branch not found'; user picks keep
  errors. Doc/copy alignment; drop unreferenced tree row classNames.
  Rebuild committed ui/dist.

* Tree cards + file viewer UX: Code shortcut, labels, line numbers

- Cards: labeled Changes/Logs/Code actions (Terminal renamed), GitHub-mark
  icon link at the card edge; width 240->264 so the row fits; no phantom
  selection outline or pointer cursor (selection state was write-only and
  is removed; RF nodes stay selectable so pointer-events keep the buttons
  alive, visuals suppressed in CSS)
- Card Code button opens the project-level Code tab pinned to that
  experiment's branch (CodeTabDef.sessionId nullable); chat-header Code
  button removed along with the session-tab opener it fed
- Changes/Code headers: BRANCH/COMMIT/SOURCE micro-labels; shared GitHubMark
  (lucide has no brand icons) used by pill, cards, and code-tab link
- File viewer: sticky line-number gutter (1:1 with unwrapped rows) and a
  slimmer header
- ui/dist rebuilt

* Code tab: collapse the dead session path

The chat-header opener was removed deliberately (45f0b11), leaving the
code tab's session plumbing writable by no one. Delete it end-to-end:

- CodeTabDef is { code, sel, toggled }; codeTabs array + null-keyed
  update/close machinery replaced by a single codeTab state. Card
  shortcut (openCodeTabForBranch) uses a functional updater — stable
  identity, so TreeView's layout memo no longer churns per tab change.
- CodeTab drops sessionId/selPicked/seeded-fallback/poll/inFlight; the
  "" source is the project clone; fetch on source change + Refresh.
- Backend: CodeTreeQuery loses session_id (endpoint is new in this PR);
  code-tree always resolves the hub clone. project_file's sessionId
  stays (chat file tabs use it). Docs rewritten as current truth.
- GitHubMark moves to BackendLogos (brand-SVG home); CodeTab header uses
  it instead of ExternalLink. TreeView: 'Open logs' title, comment fix,
  nodesFocusable=false (redundant focus/selected CSS suppression rules
  dropped — RF's own CSS covers focus, .selected only styles built-in
  node types). Code tab header matches file-view-header padding; gutter/
  code shared font metrics grouped into one rule; empty files skip the
  gutter. Rebuild committed ui/dist.
2026-07-16 23:15:07 -07:00
Daniel Kim 874e84080c Plan UI: docked approval strip + right-pane plan tab (#83)
* Plan gate: read-only git verbs + pipelines into pure consumers + 2>&1

Planning agents read experiment code that lives only on git branches
(git show <ref>:<file>, git ls-tree) and habitually pipe output through
head/grep with a stderr-merge — all of which the gate rejected outright
via its whole-command metachar blacklist. Rework classification to be
per-pipeline-stage:

- segments (;, &&) split into pipe stages; stage 0 must be a read-only
  producer (orx read, git read, glue, or pure consumer), later stages
  pure consumers (head/tail/grep/wc/cat/sort/uniq/cut/tr/nl/column —
  tee/xargs/sed/awk deliberately excluded)
- standalone 2>&1 tokens dropped; any other token carrying > or & (or
  backticks/$(/</newlines anywhere) still gates the whole line
- read-only git allowlist: whole-verb reads (status/log/show/diff/grep/
  ls-tree/...) with --output and -O/--open-files-in-pager escape-hatch
  guards; conditional forms for branch/tag (list-query only), stash
  (list/show), remote (bare/show/get-url), worktree (list), reflog;
  global flags limited to -P/--no-pager/-C (rejecting -c config
  injection); git config excluded wholesale
- command_is_readonly exported for reuse by the upcoming MCP permission
  bridge auto-policy

Fail-safe allowlist property preserved: unknown programs, verbs, or
unparseable forms gate the entire command.

* Plan mode: synthesize a plan card when ExitPlanMode never fires + accept-edits option

Observed failure: a plan-mode session ended with the plan as plain chat
text and no ExitPlanMode call — and since only a plan-card answer
switches the resume mode, the user's chat 'yes' resumed still in plan
mode, stranding the session read-only. Root cause of the missing call:
the shared playbook still said 'Never call an interactive
question/elicitation tool — it will hang', guidance that predates the
prompt-card work in #37/#40.

- run_turn tracks (plan_mode, saw_prompt, turn_errored, last_text) and
  synthesizes a WirePrompt{kind:plan, synthesized:true} from the final
  text when a plan-mode turn ends card-less and error-free; the pure
  condition fn is unit-tested
- WirePrompt/ChatPrompt gain ; the UI softens the header to
  'Plan mode — ready to proceed?' for synthesized cards
- playbook: interactive tools surface as cards now; in plan mode, always
  present the finished plan via ExitPlanMode
- plan card gains 'Approve & accept edits' (resumeMode accept-edits —
  synthesize_resume already honors it)

* chore: drop stray npm lockfile (ui uses pnpm)

* Plan mode: MCP permission bridge — desktop-style mid-turn approvals

Headless plan mode had no approval loop: gray-area tools hard-denied
with nobody to ask, WebFetch was denied (verified on claude 2.1.197),
and — worst — ExitPlanMode SELF-APPROVED headless ("User has approved
exiting plan mode", nobody asked), letting the model exit plan mode and
edit files without the user's say.

The fix is a permission bridge on Claude Code's (hidden but functional)
--permission-prompt-tool flag:

  claude ── --mcp-config ──▶ orx mcp-gate (stdio MCP server, hidden cmd)
                                 │ HTTP long-poll, 127.0.0.1:<up port>
                                 ▼
  orx up: POST /api/internal/permissions ─ oneshot ─▶ card (SSE)
          user answers /respond ─▶ ResumeAction::Handled ─▶ claude unblocks

- src/commands/mcp_gate.rs: newline JSON-RPC stdio server (initialize /
  tools/list / tools/call), concurrent calls, single stdout writer; any
  transport failure answers deny — never hangs claude
- ChatHost: request_permission (token gate -> tier-1 auto-policy ->
  card + held oneshot, 55-min timeout under MCP_TOOL_TIMEOUT=60min),
  settle_permission, per-session drain on turn end/interrupt, drop-guard
  so an abandoned request always resolves its card
- auto-policy (pure fn, tested): read-only Bash allow (shared
  plan_gate classifier), WebFetch/WebSearch allow, file edits DENY —
  with a permission tool configured the CLI delegates plan mode's edit
  block to it (verified: an allow creates files mid-plan), so that deny
  IS the safety
- plan_gate hook now also matches ExitPlanMode and answers "ask",
  routing plan approval to the bridge card instead of self-approval;
  without a bridge, ask denies headless — still better than self-approve
- claude.rs: write_mcp_config + flags wired per plan-mode turn
  (best-effort; older CLIs silently ignore the flag and degrade to
  today's gating), ExitPlanMode tool_use renders as a plain tool row
  while the bridge card is authoritative, bridge cards count as
  "saw a prompt" for the synthesized-card fallback
- resume_from_prompt: native (mid-turn) branch — permission allow/deny
  and plan keep-planning settle the held request in place (Handled);
  plan approve interrupts the paused plan turn and resumes via the
  proven --resume + mode-flip machinery (respond()'s SendMessage arm
  now interrupts first for native cards only)

Empirical grounding (claude 2.1.197 spikes): denials don't fail headless
runs (permission_denials + subtype success); WebFetch denied natively in
plan mode; hook allow bypasses the permission tool (no card spam);
bridge tool timeout = tool error, turn continues; hook "ask" on
ExitPlanMode routes it through the bridge and an allow resumes the same
turn.

* Plan UI: docked approval strip + right-pane plan tab

The plan card scrolls away with the transcript and renders the whole
plan inline. Desktop-style flow instead:

- PlanStrip (new): docked between the thread and the composer while a
  plan awaits a decision — "View plan" / "Keep planning" / "Approve &
  run" with a split-menu for accept-edits / bypass. Keep-planning
  consumes the composer draft as the refinement note, so typing
  feedback then clicking the button is the natural gesture. Solid card
  floated over the composer's gradient fade; disappears on resolve via
  the SSE re-emit.
- Plan tab: "View plan" opens the markdown as a right-pane ClosableTab
  (PlanViewDef in the rightTab union, one tab per prompt id, no fetch —
  the text is already client-side); file links inside the plan resolve
  against the plan's session worktree.
- The inline plan card goes compact while a strip host exists: clamped
  to ~9.5em with a fade, "View full plan" link, no buttons (the strip
  owns the actions; single source of truth). Without a strip host the
  card keeps its full button row.

* Store: stop normalizing claude-code plan sessions to auto

The retired-modes migration runs on every Store::open and still listed
'plan' for claude-code — written when plan mode was dropped (pre-#75).
Re-adding plan to options() without updating it meant picking Plan
silently degraded to Auto on the next request; the composer masked it by
re-sending the mode with every message, but any turn without an override
(API callers, resumes) ran in auto. Caught live in the plan-mode E2E.

* chore: bump version to 0.1.60
v0.1.60
2026-07-16 11:17:35 -07:00
Daniel Kim 81a255be74 Plan mode: MCP permission bridge — desktop-style mid-turn approvals (#82)
* Plan gate: read-only git verbs + pipelines into pure consumers + 2>&1

Planning agents read experiment code that lives only on git branches
(git show <ref>:<file>, git ls-tree) and habitually pipe output through
head/grep with a stderr-merge — all of which the gate rejected outright
via its whole-command metachar blacklist. Rework classification to be
per-pipeline-stage:

- segments (;, &&) split into pipe stages; stage 0 must be a read-only
  producer (orx read, git read, glue, or pure consumer), later stages
  pure consumers (head/tail/grep/wc/cat/sort/uniq/cut/tr/nl/column —
  tee/xargs/sed/awk deliberately excluded)
- standalone 2>&1 tokens dropped; any other token carrying > or & (or
  backticks/$(/</newlines anywhere) still gates the whole line
- read-only git allowlist: whole-verb reads (status/log/show/diff/grep/
  ls-tree/...) with --output and -O/--open-files-in-pager escape-hatch
  guards; conditional forms for branch/tag (list-query only), stash
  (list/show), remote (bare/show/get-url), worktree (list), reflog;
  global flags limited to -P/--no-pager/-C (rejecting -c config
  injection); git config excluded wholesale
- command_is_readonly exported for reuse by the upcoming MCP permission
  bridge auto-policy

Fail-safe allowlist property preserved: unknown programs, verbs, or
unparseable forms gate the entire command.

* Plan mode: synthesize a plan card when ExitPlanMode never fires + accept-edits option

Observed failure: a plan-mode session ended with the plan as plain chat
text and no ExitPlanMode call — and since only a plan-card answer
switches the resume mode, the user's chat 'yes' resumed still in plan
mode, stranding the session read-only. Root cause of the missing call:
the shared playbook still said 'Never call an interactive
question/elicitation tool — it will hang', guidance that predates the
prompt-card work in #37/#40.

- run_turn tracks (plan_mode, saw_prompt, turn_errored, last_text) and
  synthesizes a WirePrompt{kind:plan, synthesized:true} from the final
  text when a plan-mode turn ends card-less and error-free; the pure
  condition fn is unit-tested
- WirePrompt/ChatPrompt gain ; the UI softens the header to
  'Plan mode — ready to proceed?' for synthesized cards
- playbook: interactive tools surface as cards now; in plan mode, always
  present the finished plan via ExitPlanMode
- plan card gains 'Approve & accept edits' (resumeMode accept-edits —
  synthesize_resume already honors it)

* chore: drop stray npm lockfile (ui uses pnpm)

* Plan mode: MCP permission bridge — desktop-style mid-turn approvals

Headless plan mode had no approval loop: gray-area tools hard-denied
with nobody to ask, WebFetch was denied (verified on claude 2.1.197),
and — worst — ExitPlanMode SELF-APPROVED headless ("User has approved
exiting plan mode", nobody asked), letting the model exit plan mode and
edit files without the user's say.

The fix is a permission bridge on Claude Code's (hidden but functional)
--permission-prompt-tool flag:

  claude ── --mcp-config ──▶ orx mcp-gate (stdio MCP server, hidden cmd)
                                 │ HTTP long-poll, 127.0.0.1:<up port>
                                 ▼
  orx up: POST /api/internal/permissions ─ oneshot ─▶ card (SSE)
          user answers /respond ─▶ ResumeAction::Handled ─▶ claude unblocks

- src/commands/mcp_gate.rs: newline JSON-RPC stdio server (initialize /
  tools/list / tools/call), concurrent calls, single stdout writer; any
  transport failure answers deny — never hangs claude
- ChatHost: request_permission (token gate -> tier-1 auto-policy ->
  card + held oneshot, 55-min timeout under MCP_TOOL_TIMEOUT=60min),
  settle_permission, per-session drain on turn end/interrupt, drop-guard
  so an abandoned request always resolves its card
- auto-policy (pure fn, tested): read-only Bash allow (shared
  plan_gate classifier), WebFetch/WebSearch allow, file edits DENY —
  with a permission tool configured the CLI delegates plan mode's edit
  block to it (verified: an allow creates files mid-plan), so that deny
  IS the safety
- plan_gate hook now also matches ExitPlanMode and answers "ask",
  routing plan approval to the bridge card instead of self-approval;
  without a bridge, ask denies headless — still better than self-approve
- claude.rs: write_mcp_config + flags wired per plan-mode turn
  (best-effort; older CLIs silently ignore the flag and degrade to
  today's gating), ExitPlanMode tool_use renders as a plain tool row
  while the bridge card is authoritative, bridge cards count as
  "saw a prompt" for the synthesized-card fallback
- resume_from_prompt: native (mid-turn) branch — permission allow/deny
  and plan keep-planning settle the held request in place (Handled);
  plan approve interrupts the paused plan turn and resumes via the
  proven --resume + mode-flip machinery (respond()'s SendMessage arm
  now interrupts first for native cards only)

Empirical grounding (claude 2.1.197 spikes): denials don't fail headless
runs (permission_denials + subtype success); WebFetch denied natively in
plan mode; hook allow bypasses the permission tool (no card spam);
bridge tool timeout = tool error, turn continues; hook "ask" on
ExitPlanMode routes it through the bridge and an allow resumes the same
turn.

* Store: stop normalizing claude-code plan sessions to auto

The retired-modes migration runs on every Store::open and still listed
'plan' for claude-code — written when plan mode was dropped (pre-#75).
Re-adding plan to options() without updating it meant picking Plan
silently degraded to Auto on the next request; the composer masked it by
re-sending the mode with every message, but any turn without an override
(API callers, resumes) ran in auto. Caught live in the plan-mode E2E.
2026-07-16 11:13:18 -07:00
Rehaan Ahmad 352e574860 readme update 2026-07-16 11:09:35 -07:00
Rehaan Ahmad 436994224c README 2026-07-16 11:06:27 -07:00
Daniel Kim 7d1aed6f7d Plan mode: synthesized plan card fallback + accept-edits option (#81)
* Plan mode: synthesize a plan card when ExitPlanMode never fires + accept-edits option

Observed failure: a plan-mode session ended with the plan as plain chat
text and no ExitPlanMode call — and since only a plan-card answer
switches the resume mode, the user's chat 'yes' resumed still in plan
mode, stranding the session read-only. Root cause of the missing call:
the shared playbook still said 'Never call an interactive
question/elicitation tool — it will hang', guidance that predates the
prompt-card work in #37/#40.

- run_turn tracks (plan_mode, saw_prompt, turn_errored, last_text) and
  synthesizes a WirePrompt{kind:plan, synthesized:true} from the final
  text when a plan-mode turn ends card-less and error-free; the pure
  condition fn is unit-tested
- WirePrompt/ChatPrompt gain ; the UI softens the header to
  'Plan mode — ready to proceed?' for synthesized cards
- playbook: interactive tools surface as cards now; in plan mode, always
  present the finished plan via ExitPlanMode
- plan card gains 'Approve & accept edits' (resumeMode accept-edits —
  synthesize_resume already honors it)

* chore: drop stray npm lockfile (ui uses pnpm)
2026-07-16 11:05:40 -07:00
Daniel Kim 203f63b900 Configurable data directory: choose, change, and move with progress (#80)
Let users pick where orx stores everything (orx.db + WAL, run-logs/,
artifacts/, chat-attachments/, agent-*.log) instead of assuming
~/.local/share/openresearch. The default is unchanged; the location is now
choosable from the product.

Resolution order in store::data_dir(): $ORX_DATA_DIR (forced override) →
persisted user choice → $XDG_DATA_HOME → default. An explicit UI choice beats
the ambient XDG base; the env var stays authoritative for sandboxes/tests.
Empty env vars are treated as unset.

- Persisted path lives in config_dir()/settings.json, OWNED by telemetry's
  locked+atomic mutate_settings (in-process mutex + cross-process flock + temp
  -and-rename + corrupt-file refusal). data_dir is a field on telemetry::Settings
  so a write can't clobber installId/telemetryDisabled and vice-versa.
- Move engine (src/local/datadir.rs): validate → WAL checkpoint → same-fs rename
  (atomic, consumes old) or cross-fs copy-with-progress (verify, then KEEP the
  old dir, reported as oldPathLeft — never deletes data a stray writer might
  still be using) → swap the persisted path. Free-space pre-check via statvfs;
  EXDEV falls back to copy.
- Endpoints (up.rs): GET/POST /api/settings/data-dir, /validate, /move. Move
  streams datadir.move.progress/done/error over the existing SSE broadcast and
  returns 202. A move-in-progress flag (claimed via compare_exchange) makes every
  store-mutating handler back off with 409 for the duration.
- After a move, harness children are restarted so any that pinned the old
  $ORX_DATA_DIR (Codex) respawn on the new path.

UI:
- Settings → Storage card: current path + source, validate, move with a new
  byte-level progress bar, env-forced read-only state, in-flight 409 handling.
- Onboarding gains a "Storage" step (default pre-filled, skippable).
- Files view links "Change storage location →" to Settings → Storage.
- Chat send surfaces the move-in-progress 409 and restores the draft instead of
  dropping it.

human_bytes shared via store::human_bytes; fmtBytes shared via api.ts.
2026-07-16 11:04:49 -07:00
Daniel Kim 1b1a9453df Plan gate: read-only git verbs + pipelines into pure consumers + 2>&1 (#79)
Planning agents read experiment code that lives only on git branches
(git show <ref>:<file>, git ls-tree) and habitually pipe output through
head/grep with a stderr-merge — all of which the gate rejected outright
via its whole-command metachar blacklist. Rework classification to be
per-pipeline-stage:

- segments (;, &&) split into pipe stages; stage 0 must be a read-only
  producer (orx read, git read, glue, or pure consumer), later stages
  pure consumers (head/tail/grep/wc/cat/sort/uniq/cut/tr/nl/column —
  tee/xargs/sed/awk deliberately excluded)
- standalone 2>&1 tokens dropped; any other token carrying > or & (or
  backticks/$(/</newlines anywhere) still gates the whole line
- read-only git allowlist: whole-verb reads (status/log/show/diff/grep/
  ls-tree/...) with --output and -O/--open-files-in-pager escape-hatch
  guards; conditional forms for branch/tag (list-query only), stash
  (list/show), remote (bare/show/get-url), worktree (list), reflog;
  global flags limited to -P/--no-pager/-C (rejecting -c config
  injection); git config excluded wholesale
- command_is_readonly exported for reuse by the upcoming MCP permission
  bridge auto-policy

Fail-safe allowlist property preserved: unknown programs, verbs, or
unparseable forms gate the entire command.
2026-07-16 11:04:37 -07:00
Daniel Kim 55a3ac73d7 Plan gate: allow batched read-only orx commands (; and && sequences) (#78)
The plan-mode PreToolUse hook only auto-allowed a *single* orx read
command; any compound line was hard-rejected (contained ';'/'&&'/'|'/...).
The planning agent routinely batches read-only lookups into one Bash call
punctuated by echo separators (orx exp desc A; echo ====; orx exp desc B),
so those calls fell through to plan mode's gate and failed under headless
--print.

Split the line on ';' and '&&' and allow it only if *every* segment is
independently a read-only orx command or harmless glue (echo/true/:). One
write, non-orx, or unclassifiable segment gates the whole line, preserving
the allowlist-only security property: a read-only prefix can never smuggle
a write through as a later segment. Pipes, redirection, backticks, $(...),
background &, and newlines remain rejected outright.
2026-07-15 23:50:59 -07:00
Daniel Kim 106eceb864 orx up --remote: run remote orx under POSIX sh -c on any login shell + PATH/CARGO_HOME (v0.1.59) (#77)
* up --remote: find orx via ~/.cargo/bin on non-interactive PATH; bump 0.1.59

The remote preflight and launch run orx over a non-interactive ssh shell,
which skips ~/.bashrc/~/.profile — so on a minimal image (RunPod's Ubuntu)
~/.cargo/bin isn't on PATH and 'command -v orx' fails even though 'which orx'
works when logged in. Wrap both remote commands in a PATH prelude that
prepends ~/.cargo/bin and ~/.local/bin (the two common installer targets),
with $HOME left for the remote shell to expand so it works for root or the
runpod user. The 'not installed' error now names those paths and the
install command.

Tests: remote_shell prepends both dirs and keeps $HOME literal; remote_up_cmd
still carries the port behind the prelude.

* up --remote: run remote orx under POSIX sh -c; cover CARGO_HOME

Harden the non-interactive-PATH fix so `orx up --remote` works on any box,
not just RunPod:

- Force POSIX sh. ssh runs the preflight and launch under the remote user's
  *login* shell, which may be csh/tcsh/fish — none understand `export VAR=val`,
  so the previous `export PATH=...; cmd` was a syntax error there and got
  misreported as "unreachable"/"not installed". Wrap the POSIX body in
  `sh -c <sh_quote(body)>` so /bin/sh interprets it whatever the login shell is
  (mirrors how the job backends run their payload through an explicit shell).
- Cover a relocated CARGO_HOME. The installer honors $CARGO_HOME
  (install-path = CARGO_HOME); search ${CARGO_HOME:-$HOME/.cargo}/bin instead
  of a hardcoded ~/.cargo/bin so a box that relocates cargo still resolves orx.
- Split the preflight outcome: ORX_MISSING -> clear "not installed"; neither
  marker (ssh exited 0 but no answer) -> a distinct message that echoes what we
  saw, instead of lumping it into "not installed".

Verified end-to-end that the generated command resolves orx under /bin/sh,
bash, and a tcsh login shell, with orx only in ~/.cargo/bin, with a relocated
CARGO_HOME, and reports ORX_MISSING when genuinely absent.
v0.1.59
2026-07-15 23:50:40 -07:00
Daniel Kim af8eed1724 orx up --remote: custom SSH port support + readable, honest banner (#74)
* orx up remote banner: make the SSH commands the readable part

The remote-access banner dimmed the very things you need to read and
copy: the whole `ssh -N -L …` command was faint gray, and the
`<ssh-alias>` placeholder in both commands was dimmed too — so the
prose asides and the commands were nearly the same weight, and the one
line you'd paste was the hardest to see.

Invert the emphasis. Both commands are now bold with a `$` prompt so
they read as "type this", the `<ssh-alias>` fill-in-the-blank is cyan
(legible, obviously a placeholder) instead of dimmed, and only the prose
(intro, the alias note, the trailing `# then open …` comment) is dim.
Reworded the intro/option headings so the two paths scan as 1) and 2).

Plain-text output (pipes/CI/NO_COLOR) is unchanged in spirit and still
carries no escape codes.

* orx up remote banner: adapt to boxes with no ~/.ssh/config alias

The banner assumed a `~/.ssh/config` alias existed and told the user to
type `<ssh-alias>`. On a fresh RunPod / openresearch dev node you connect
with a raw `root@<ip> -p <port>` and have no alias at all, so the primary
instruction was a dead end.

Make the connection target adaptive:

- Anchor on `<user@host>` — the one thing every reader has, alias or not —
  with a concrete example, and say plainly no config alias is needed.
- When `SSH_CONNECTION` gives a usable *public* server IP, splice it
  straight into both commands so they're nearly copy-paste.
- Custom SSH ports can't be recovered here (`SSH_CONNECTION` carries sshd's
  listen port, not the client-dialed one — e.g. RunPod maps :38455 onto
  sshd :22), and `orx up --remote` has no `-p` flag. So the `[-p PORT]`
  slot goes only on the manual `ssh` line (which accepts it), and a caveat
  tells the reader to use option 2 on a non-default port.

Tests cover both branches (placeholder vs inlined public IP) and assert
the port slot never leaks onto the `orx up --remote` line.

* up --remote: security-aware host-key policy + IPv6/port hardening

Addresses multi-agent review of the custom-port support:

- HostKeyPolicy enum (jobs/ssh.rs) centralizes host-key handling behind
  SshTarget::host_port, so the '-p'/'-o' opt vector and its rationale live
  in one place. openresearch_ssh_target now uses it (Ephemeral), replacing
  its inline vec — behavior byte-identical (guarded by an exact-vector test).

- Don't silently disable host-key verification for a user-typed HOSTNAME
  with a port: a name may be one the user has pinned. Only a raw IP (the
  freshly-provisioned-box signal) gets accept-new TOFU against the real
  known_hosts; a hostname gets '-p' only (UserConfig). Neither uses
  /dev/null — that stays reserved for provider-managed boxes.

- Fix raw-IPv6 mis-parse: '2001:db8::5' was split into host + '-p 5'. And
  fix 'user@[::1]:2222' silently dropping its port (the bracket check now
  looks past the user@ prefix). Reject port 0.

- Be honest in the banner/help/errors: 'orx up --remote' reconstructs only
  user@host + port, so a custom key / jump host must come from ~/.ssh/config;
  the reachability error now hints at ssh-keygen -R for a reused-IP key clash.

Tests cover the policy dispatch, IPv6 (raw/bracketed/user-prefixed), port 0,
>u16, host_is_ip_literal forms, and the exact Ephemeral opt vector.

* chore: bump version to 0.1.58

Ships the orx up --remote custom-port support + readable/honest SSH banner.
v0.1.58
2026-07-15 19:35:41 -07:00
Rehaan Ahmad 2c50f99a21 report skill (#76) 2026-07-15 19:30:41 -07:00
Daniel Kim 9cfc2e9cfa Claude plan mode: read-only orx allowed via PreToolUse hook (#75)
Re-enable plan mode for the Claude Code chat harness (headless
`--print --permission-mode plan`). Plan mode gates `Bash(orx …)`, which
broke the read-only `orx` inspection the agent plans from. A PreToolUse
hook (`orx plan-gate`) returns an allow decision for read-only `orx`
verbs, which overrides plan mode's Bash gate while file edits and
launches stay blocked until the user approves the plan.

- New hidden `orx plan-gate` command: the hook body (reads Claude's hook
  JSON on stdin, emits allow for read-only orx invocations).
- Allowlist-only classifier in local/harness/plan_gate.rs; unknown verbs
  gate. `exp desc`/`cmd` are reads only without --set/--stdin; `exp wait`
  and whole-verb reads (runs/logs/query/…) allowed; launches gated.
- Claude harness advertises Plan and writes the hook --settings file only
  in plan mode (reuses jobs::ssh::sh_quote).
- Drift-guard test + pointer comment keep the allowlist synced with the
  Command enum in main.rs.

Codex stays out (headless collaboration_mode=plan is a no-op). OpenCode
already has a native read-only plan agent.

Verified live against claude 2.1.197: read-only orx executes under plan
mode with the hook, is denied without it, and orx exp run stays denied.
2026-07-15 19:16:14 -07:00
Daniel Kim 08d30a8e61 Add anonymous, opt-out usage telemetry (PostHog) (#70)
* Add anonymous, opt-out usage telemetry (PostHog)

`orx` shipped with no telemetry, so there was no way to measure installs,
DAU/WAU, retention, or command usage. This adds anonymous, opt-out analytics
sent to a dedicated PostHog project.

What's collected: a per-invocation `cli_command` event and explicit
`experiment_started` events (create + run, with a coarse compute target),
carrying only a random per-install UUID, CLI version, os/arch, and CI flag.
Never any code, prompts, file contents, ids, names, paths, or tokens.

Design:
- New `src/telemetry.rs`: settings (install id + opt-out) at
  `config_dir()/settings.json`, opt-out policy, event send, and a
  fire-and-forget session modeled on `updates::UpdateWarning`.
- Opt-out: `DO_NOT_TRACK`, `ORX_TELEMETRY=off`, `CI`, a `--no-telemetry`
  global flag, and a persistent `orx telemetry on|off|status` subcommand.
  A one-time disclosure prints after the first `orx login` (shown even when
  disabled, so it's never permanently suppressed).
- Never blocks or crashes the CLI: events are spawned and flushed once at
  exit within a bounded window, so success output is never delayed; all
  errors are swallowed.
- Cross-process safe: settings writes use an `fd_lock` advisory lock plus
  atomic temp+rename, so a concurrent write can't revert a persisted opt-out.
- musl-safe: reuses the rustls `reqwest` client; no new TLS/C dependency.

Events land at PostHog `/i/v0/e/` with a public write-only `phc_` key and a
client-side ISO-8601 timestamp. `$process_person_profile:false` keeps events
anonymous. Unit tests cover opt-out precedence, id stability, payload shape
(PII-free), timestamp math, corrupt-file fail-safe, and the non-blocking
flush path.

* Prefix all CLI events with `cli_` to separate from website analytics

This PostHog project is shared with the website/cloud-agent analytics, so CLI
events must be distinguishable by event name alone — relying only on the
`source: "cli"` property is fragile (one forgotten dashboard filter leaks CLI
events into website metrics).

`build_payload` now stamps a `cli_` prefix onto every event name centrally, so
every current and future CLI event is `cli_*` by construction. Wire names:
- `cli_command`  (was `cli_command`)
- `cli_experiment_started`  (was `experiment_started`)

Added `every_event_name_is_cli_prefixed` to lock the guarantee.

* Telemetry: trim opt-out surface to --no-telemetry + `orx telemetry off`

Drop the `DO_NOT_TRACK`, `ORX_TELEMETRY`, and `CI` environment-variable
opt-out/disable checks. They were redundant with the two mechanisms we keep:
the `--no-telemetry` per-run flag and the persistent `orx telemetry off`.

The `CI` auto-disable in particular guarded a scenario that's mostly
hypothetical for an interactive research CLI, and it only ever fired for
hosted CI providers that set `CI` (missing Jenkins, agent boxes, and ad-hoc
scripts — the automation `orx` actually runs under). Every event still carries
a `ci` property, so automated runs can be filtered at query time in analytics
without being dropped up front.

Updates the opt-out policy, first-login notice text, `--no-telemetry` help,
README, and tests accordingly.

* orx up: add usage-analytics step to onboarding

Adds a third onboarding step ("Usage analytics") to the `orx up` dashboard
first-run walkthrough, after the coding-agents and git/GitHub steps. It shows
what is and isn't collected and lets the user toggle anonymous analytics On/Off
before creating their first project. On by default (matches the CLI's opt-out
model); the choice persists via the same `telemetry_disabled` setting as
`orx telemetry off`.

- Backend: `GET/POST /api/settings/telemetry` in up.rs, reusing
  telemetry::disabled_reason / set_persisted_disabled (mirrors the git-settings
  handler pattern). Returns `{ enabled, reason }`.
- UI: getTelemetry/setTelemetry in api.ts; Onboarding.tsx grows to a 3-step
  flow with a TelemetryCard (On/Off segmented control).

Verified end-to-end in orx up: toggling Off in the wizard persists
`telemetryDisabled: true` to settings.json; toggling On clears it.

* Telemetry: drop the CLI login stderr disclosure

The one-time `orx login` stderr notice (show_notice_once) is removed now that
the disclosure lives in the `orx up` onboarding step, which is the right place
for it. Also removes the now-unused `notice_shown` settings field and updates
the module doc + README to point at the onboarding step instead of a login
notice.

* Telemetry: record consent decisions (cli_telemetry_consent)

Adds one event, `cli_telemetry_consent { agreed: bool }`, that records whether
the user opts into or out of analytics — so we can count agrees vs rejects.

Unlike every other telemetry event, this one is UNCONDITIONAL: it bypasses the
opt-out check and the persisted install id, and uses a fresh anonymous
distinct_id per call. If it honored the opt-out, every rejection would be
invisible and the ratio would be meaningless.

Fired once when the user leaves the analytics choice:
- `orx up` onboarding: on "Create your first project" (records the final On/Off
  state, so users who accept the default On are counted too) via a dedicated
  POST /api/settings/telemetry/consent endpoint.
- CLI: on `orx telemetry on` / `orx telemetry off`.

Tests: consent payload shape + name prefix, and that record_consent sends even
when persisted-off without generating an install id.

* Bump version to 0.1.57

Ships the anonymous opt-out telemetry subsystem, the orx up onboarding
usage-analytics step, and the cli_telemetry_consent event. Merging this PR
cuts release v0.1.57.
v0.1.57
2026-07-15 16:35:44 -07:00
Daniel Kim 843e5ed14b orx up: remote access over SSH (#73)
* orx up: remote access over SSH

Running `orx up` while SSH'd into a remote box was broken: the server
binds 127.0.0.1 (no auth, by design), so its loopback URL is unreachable
from the laptop and there's no browser on the box to open. Add two
complementary paths, keeping the server on loopback (never 0.0.0.0).

Part A — guidance from the remote side (src/remote.rs):
  When `orx up` runs inside an SSH session (detected via SSH_CONNECTION/
  SSH_TTY/SSH_CLIENT), skip the futile browser-open and print exact
  forwarding instructions — the `orx up --remote` one-liner plus a manual
  `ssh -L` fallback, both framed as "run on your laptop, not this session."
  A public SSH_CONNECTION server IP is offered as a best-effort hint;
  private/loopback/link-local/CGNAT addresses are filtered out.

Part B — automatic forwarding from the laptop (src/commands/up_remote.rs):
  `orx up --remote <host>` owns the SSH client, so it can forward the port
  itself: check the host is reachable and has `orx`, start `orx up` there,
  bring up an `ssh -L 127.0.0.1:port:localhost:port` tunnel
  (ExitOnForwardFailure so a busy local port fails loudly instead of
  attaching to the wrong server), wait for the forwarded /api/health, open
  the browser, and hold the tunnel until Ctrl-C.

Also: `orx up` now shuts down on SIGTERM/SIGHUP as well as SIGINT, so the
remote server started over the tunnel exits when the tunnel closes instead
of leaking.

The remote dashboard is unauthenticated on the remote's loopback — noted
in `--remote` help, since co-tenants on that host can reach it.

* remote: clarify <ssh-alias> placeholder is the alias, not the IP

The old <the-host-you-ssh'd-into> placeholder was ambiguous — a user could
paste the box's IP, which usually won't work (the ~/.ssh/config alias carries
the key/port/user the connection needs). Rename to <ssh-alias>, state plainly
it's the name given to ssh (not the IP), and reframe the IP hint as a fallback
that may need the usual -i/-p options.

* remote: make the SSH forwarding guidance concise and scannable

The startup message was a wall of equal-weight text. Tighten it and add
light terminal styling: bold the header + the one command that matters
(orx up --remote), dim the manual fallback and asides so the eye lands on
the primary path. Fold the 'then open localhost' step onto the ssh -L line
as an inline comment. Styling mirrors the updates.rs bold() idiom and is
stripped on pipes/CI/NO_COLOR. Refactored into a testable
render_instructions(port, ansi) core.
2026-07-15 16:23:21 -07:00
Daniel Kim eaace241d6 jobs: default PYTHONUNBUFFERED=1 so streamed logs never stall (#71)
Every job backend surfaces logs by tailing a pipe or a redirected `log`
file. When the job's Python block-buffers stdout (the default when stdout
isn't a TTY), those logs appear frozen until the buffer fills or the
process exits — the classic "frozen then a wall of text" symptom.

Default PYTHONUNBUFFERED=1 into the job environment across all five
backends so a job's prints stream live, while letting an explicit
author value win:

- modal: the sandbox env, plus the local launcher subprocess whose stdout
  orx reads line-by-line (separate layer, same buffering problem)
- kubernetes: the container env in the Job manifest
- ssh / localbox: the exported env in the generated run.sh
- slurm: the sbatch payload's exports (not the login-node clone script)

Shared via jobs::default_unbuffered for the HashMap-env backends;
kubernetes open-codes the equivalent because its env is a JSON
[{name,value}] array. Inert for non-Python jobs and inherited by child
processes (unlike the -u flag).
2026-07-15 15:38:13 -07:00
Daniel Kim 36b7058f98 Chat: Escape stops the streaming turn (all harnesses) (#69)
Press Escape to interrupt an in-flight chat turn and drop focus back
into the composer, mirroring the Claude Code desktop app. Works for
every harness — a chat-view-scoped, busy-only document keydown listener
calls the existing stop() -> interruptChat, which the backend routes to
the active session's harness (Claude, Codex, OpenCode, ...).

Also hardens the shared usePopover() hook to own Escape while open
(capture phase + stopPropagation) — a pre-existing gap the new listener
surfaced: without it, closing a composer picker with Escape would also
interrupt the turn. The slash menu keeps its bubble-phase preventDefault
path, gated by the listener's defaultPrevented check.

Rebuilt ui/dist (tracked; debug reads from disk, release embeds it).
2026-07-15 01:12:01 -07:00
Daniel Kim bd9f438501 Chat: rename sessions from the Recents kebab menu (#68)
Add a "Rename" item to the per-row three-dot menu in the chat Recents
list (alongside Archive/Delete). Selecting it turns the row title into an
inline input — Enter/blur saves, Esc cancels — with an optimistic update
that reconciles with the server's chat.session broadcast.

Backend: extend the existing PATCH /api/chat/sessions/{id} handler to
accept a `title` field (trimmed, empty rejected) and add ChatHost::set_title,
mirroring set_archived (re-read after write + SSE emit).

Also guard harness auto-titling: TurnCtx::set_title (opencode's
session.updated path) now sets the title only when the session has none yet,
via an atomic conditional UPDATE, so it can't clobber a title the user set
via Rename.

Incidental: make the vite dev proxy target overridable with ORX_BACKEND
(defaults to the standard 4791) for testing against a non-default backend.
2026-07-15 00:55:13 -07:00
Daniel Kim 2c08a60fed Add OpenResearch compute as a backend: --backend openresearch (#67)
* Add OpenResearch compute as a backend: --backend openresearch

An ephemeral org-billed platform box per run, symmetric with the other
external backends. Submit validates flags, resolves the org, preflights the
user's registered SSH keys, pushes the branch, provisions via POST /sandboxes
(402 -> top-up message), records the run as starting, and detaches the
supervisor. The supervisor polls GET /sandboxes/{id} until the box is online,
records the SSH endpoint on the descriptor (restart-safe), launches the
clone-and-run payload over ssh wrapped in a wall-clock timeout guard, runs
the shared ssh watch loop, and deletes the box on every exit path — logs are
drained off the box before teardown, and a failed teardown appends an
'orx instance delete' hint to the run instead of changing its status.

Supporting changes:
- jobs/ssh.rs: SshTarget {dest, extra_opts} so the transport reaches
  explicit user@host:port endpoints (openresearch) as well as config
  aliases (ssh/slurm); ControlPath hash folds in the opts.
- supervise.rs: run_ssh's two-half loop extracted into watch_ssh_job,
  shared by ssh and openresearch.
- Descriptor: sshHost/sshPort/sshUser + timeoutSecs optional fields
  (fixed field list — unknown keys do not survive a round-trip).
- store: set_backend_json (narrow setter; upsert would clobber
  status/markdown and race the cancel flag).
- client: get_sandbox / delete_sandbox / list_sandboxes / list_ssh_keys.
- orx instance list/delete: the manual cleanup path for a leaked box.
- UI: OpenResearch badge in BackendLogos; startRun type widened.

Requires the platform's companion PR (GET /sandboxes/{id} + PAT auth on
sandbox list/delete and ssh-keys list); against an older API the ssh-key
preflight degrades to a warning and provisioning-wait fails the run with
the API error after teardown retries.

* Bump version to 0.1.56 — releases --backend openresearch on merge
v0.1.56
2026-07-14 17:48:05 -07:00
Rehaan Ahmad 18102cf0a9 message default (#66) v0.1.55 2026-07-14 00:08:53 -07:00
Rehaan Ahmad a70206655b chore: prompt improvements (#65)
* improvements

* skill

* cargo

* prompt improvements
v0.1.54
2026-07-14 00:02:58 -07:00
Rehaan Ahmad b1cab32b48 chore: local runs (#64)
* open runs

* open runs

* ci
v0.1.53
2026-07-13 23:52:03 -07:00
Daniel Kim f1df93d4a3 Codex Auto: sandbox escalations become permission cards (PR2 re-land, v0.1.52) (#63)
* Codex auto mode: grant the orx data dir as a sandbox writable root

workspace-write denies writes outside the workspace, and the orx SQLite
store lives under ~/.local/share (WAL + read-write open), so every
store-touching orx command died with 'unable to open database file'
under Auto mode. Add store::data_dir() to the turn's writable_roots
alongside the shared git dir.

* Review fixes: pin ORX_DATA_DIR to the granted root, ensure_ naming

- Pin ORX_DATA_DIR on the codex child (after prepare_env) so the store
  the sandboxed orx resolves is the exact path granted as a writable
  root — synced env or a relative override could otherwise diverge.
- Rename orx_data_dir -> ensure_orx_data_dir (codebase ensure_ prefix
  for create-if-missing helpers); note the grant is dir-wide.
- Dedent the '-c replaces user roots' note: it covers both roots.

* Review round 2: match-expression pin, stale options() doc, asymmetry note

* Codex chat: migrate to app-server protocol with exec fallback

One long-lived 'codex app-server' child per session (CodexHost, the
AgentHost pattern), JSON-RPC over stdio. Threads persist as the
session's native id and resume across orx up restarts; the playbook
rides developerInstructions; the sandbox policy travels per turn as a
first-class sandboxPolicy param (writable roots + network) instead of
-c config overrides that clobbered user config.

approvalPolicy stays 'never' on every mode — this is a pure transport
swap; the follow-up flips Auto to on-request and surfaces approval
requests as permission cards (validated live against codex-cli 0.144:
sandbox escalations DO arrive as item/commandExecution/requestApproval,
upstream #21982 does not reproduce).

codex < 0.144 keeps the legacy exec path for one release
(ORX_CODEX_EXEC=1 forces it); detect() notes the upgrade.

* Review round 1 fixes: turn-id filter, drop race, bounded interrupt

Majors:
- Filter turn events by the turn/start response's turn id so an aborted
  predecessor's tail (fatally, its turn/completed) can't leak into a
  successor turn's transcript.
- TurnRoute::drop only detaches its own channel (same_channel guard) —
  an aborted task's late drop can no longer clobber a successor's
  registration.
- interrupt_active_turn bounded to 5s so a wedged child can't hold the
  user-facing interrupt/delete paths hostage; reader EOF now also kills
  a half-dead child instead of leaving a zombie in the registry.
- thread/resume falls back to a fresh thread only on a server-reported
  error (try_request); transport failures propagate so a resumable
  thread is never discarded over a timeout.
- delete_project now kills codex children too (was opencode-only).

Nits: options()/sandbox_policy_json doc rewrites, tool_part/tool_status/
part_exists/find_codex_required/respond_decline helpers, HashSet for
unanswered ids, REQUEST_TIMEOUT const, outputDelta creates its part,
turn/completed(inProgress) guard, double-error dedup, truthy
ORX_CODEX_EXEC, softer agent_note, import cleanups.

* Review round 2 fixes: abort-proof child bring-up, shared-helpers section

Major: spawn + register + handshake now run in a detached task, so an
aborted turn task (interrupt/delete during the first message) can no
longer orphan an unregistered app-server child; a no-displace insert
keeps a raced successor's live client intact, and handshake failure
kills + deregisters only its own entry.

Nits: shared helpers moved out of the legacy section under their own
banner; set_active_turn armed from the turn/start response (deletes the
turn/started special case and closes the early-interrupt no-op window);
parse_codex_version takes the first parseable token; resume-rejection
logged; HANDSHAKE_TIMEOUT const; TurnEnd/apply_notification docs;
has_error_part test; fold-test guard tightened; comment accuracy.

* Round 3 polish: doc word order, eprintln prefix, mid-handshake reuse note

* fmt

* Codex Auto: on-request approvals surfaced as permission cards

Auto now runs approvalPolicy on-request: the writable roots keep
routine orx traffic prompt-free, and anything escalating past the
sandbox arrives as item/*/requestApproval, surfaced as a permission
card (native_id = JSON-RPC request id) and answered inline via
resume_from_prompt -> {"decision": accept|decline}.

Invariant: no unresolved codex card outlives its turn — every turn
exit sweeps open cards (resolve + decline with codex); interrupt paths
cancel pending approvals natively before turn/interrupt;
serverRequest/resolved reconciles codex-side settlement (its ~5min
approval deadline). Stale-turn requests are declined, never surfaced;
Bypass auto-accepts; unknown request types get a JSON-RPC error
instead of blocking codex. A 10-minute no-event watchdog (suspended
while a card is pending) interrupts wedged turns.

* PR2 review round 1 fixes: stale-card defense, Bypass reply schema, card content

- Consent integrity: codex request ids restart per child, so a stale
  unresolved card could deliver a click to a different live request.
  Turn-entry resolve_stale_prompts sweep (store-side, codex-only —
  end-turn harnesses keep their cross-turn cards) + turn-scoped part
  ids (appr-{turn}-{id}) close it from both ends.
- Bypass now auto-accepts only card-able approval methods; unknown
  request types get the JSON-RPC error reply in every mode (a
  {decision} reply to item/tool/requestUserInput would fail to parse).
- Permission cards show the request's human-readable reason (fileChange
  cards carry nothing else); UI dist rebuilt.
- serverRequest/resolved also clears the id from the client's pending
  set (stale-answer guard stays truthful after codex self-settles).
- Reconciliation computes the part id directly (approval_part_id)
  instead of a reverse scan; resolve_card returns (); sweep declines
  unconditionally (respond's pending set is the single arbiter);
  watchdog bumped to 30min with an honest false-positive note;
  resume_from_prompt gains a kind guard.

* PR2 round 2 polish: schema-aware fallback replies, immediate card flush

Non-approval request methods now get the JSON-RPC error reply in every
fallback path (read_loop unrouted, stale-turn arm) via a shared
is_approval_request; codex-settled cards flush immediately instead of
waiting for the next event; message_json reuse; Handled doc drift;
watchdog comment dedup.

* Bump version to 0.1.52
v0.1.52
2026-07-13 15:27:04 -07:00
Daniel Kim 34fd606402 Codex chat: migrate to app-server protocol (PR1 — transport swap) (#61)
* Codex auto mode: grant the orx data dir as a sandbox writable root

workspace-write denies writes outside the workspace, and the orx SQLite
store lives under ~/.local/share (WAL + read-write open), so every
store-touching orx command died with 'unable to open database file'
under Auto mode. Add store::data_dir() to the turn's writable_roots
alongside the shared git dir.

* Review fixes: pin ORX_DATA_DIR to the granted root, ensure_ naming

- Pin ORX_DATA_DIR on the codex child (after prepare_env) so the store
  the sandboxed orx resolves is the exact path granted as a writable
  root — synced env or a relative override could otherwise diverge.
- Rename orx_data_dir -> ensure_orx_data_dir (codebase ensure_ prefix
  for create-if-missing helpers); note the grant is dir-wide.
- Dedent the '-c replaces user roots' note: it covers both roots.

* Review round 2: match-expression pin, stale options() doc, asymmetry note

* Codex chat: migrate to app-server protocol with exec fallback

One long-lived 'codex app-server' child per session (CodexHost, the
AgentHost pattern), JSON-RPC over stdio. Threads persist as the
session's native id and resume across orx up restarts; the playbook
rides developerInstructions; the sandbox policy travels per turn as a
first-class sandboxPolicy param (writable roots + network) instead of
-c config overrides that clobbered user config.

approvalPolicy stays 'never' on every mode — this is a pure transport
swap; the follow-up flips Auto to on-request and surfaces approval
requests as permission cards (validated live against codex-cli 0.144:
sandbox escalations DO arrive as item/commandExecution/requestApproval,
upstream #21982 does not reproduce).

codex < 0.144 keeps the legacy exec path for one release
(ORX_CODEX_EXEC=1 forces it); detect() notes the upgrade.

* Review round 1 fixes: turn-id filter, drop race, bounded interrupt

Majors:
- Filter turn events by the turn/start response's turn id so an aborted
  predecessor's tail (fatally, its turn/completed) can't leak into a
  successor turn's transcript.
- TurnRoute::drop only detaches its own channel (same_channel guard) —
  an aborted task's late drop can no longer clobber a successor's
  registration.
- interrupt_active_turn bounded to 5s so a wedged child can't hold the
  user-facing interrupt/delete paths hostage; reader EOF now also kills
  a half-dead child instead of leaving a zombie in the registry.
- thread/resume falls back to a fresh thread only on a server-reported
  error (try_request); transport failures propagate so a resumable
  thread is never discarded over a timeout.
- delete_project now kills codex children too (was opencode-only).

Nits: options()/sandbox_policy_json doc rewrites, tool_part/tool_status/
part_exists/find_codex_required/respond_decline helpers, HashSet for
unanswered ids, REQUEST_TIMEOUT const, outputDelta creates its part,
turn/completed(inProgress) guard, double-error dedup, truthy
ORX_CODEX_EXEC, softer agent_note, import cleanups.

* Review round 2 fixes: abort-proof child bring-up, shared-helpers section

Major: spawn + register + handshake now run in a detached task, so an
aborted turn task (interrupt/delete during the first message) can no
longer orphan an unregistered app-server child; a no-displace insert
keeps a raced successor's live client intact, and handshake failure
kills + deregisters only its own entry.

Nits: shared helpers moved out of the legacy section under their own
banner; set_active_turn armed from the turn/start response (deletes the
turn/started special case and closes the early-interrupt no-op window);
parse_codex_version takes the first parseable token; resume-rejection
logged; HANDSHAKE_TIMEOUT const; TurnEnd/apply_notification docs;
has_error_part test; fold-test guard tightened; comment accuracy.

* Round 3 polish: doc word order, eprintln prefix, mid-handshake reuse note

* fmt
2026-07-13 15:11:50 -07:00
Daniel Kim 433c91c479 Codex auto mode: grant the orx data dir as a sandbox writable root (#60)
* Codex auto mode: grant the orx data dir as a sandbox writable root

workspace-write denies writes outside the workspace, and the orx SQLite
store lives under ~/.local/share (WAL + read-write open), so every
store-touching orx command died with 'unable to open database file'
under Auto mode. Add store::data_dir() to the turn's writable_roots
alongside the shared git dir.

* Review fixes: pin ORX_DATA_DIR to the granted root, ensure_ naming

- Pin ORX_DATA_DIR on the codex child (after prepare_env) so the store
  the sandboxed orx resolves is the exact path granted as a writable
  root — synced env or a relative override could otherwise diverge.
- Rename orx_data_dir -> ensure_orx_data_dir (codebase ensure_ prefix
  for create-if-missing helpers); note the grant is dir-wide.
- Dedent the '-c replaces user roots' note: it covers both roots.

* Review round 2: match-expression pin, stale options() doc, asymmetry note
2026-07-13 15:06:31 -07:00
Daniel Kim 6636e8e8cc Files tab redesign: minimalist tree + proper report rendering (v0.1.51) (#58)
* Files tab redesign: minimalist tree, centered column, GFM report tables

* Bump version to 0.1.51

* Review round 1 fixes: GFM link/table edge cases, shared code blocks, class rename sweep

* Rebuild ui/dist
v0.1.51
2026-07-12 23:14:29 -07:00
Daniel Kim 9799d21d9f fix: append ellipsis to truncated chat session titles (#59) 2026-07-12 23:12:08 -07:00
Daniel Kim a0e67cd210 File viewer: open files from the chat session's worktree (#57)
Clicking a file chip in chat failed with "path must be repo-relative":
agents work in per-session worktrees, and the viewer only knew how to
strip the hub-clone prefix — and only served files from the hub clone,
where agent-created files don't exist.

- App.tsx parseFilePath maps agent-reported paths (relative, repoPath-
  prefixed, worktrees/ and repos/ cache layouts) to a repo-relative path
  plus the session id; relative paths inherit the click context's
  session (chat's active session, or the viewed file's).
- project_file gains a sessionId query param: the session is validated
  against the store (must belong to the project), its worktree is
  served when present, and a missing worktree degrades to the hub
  clone. The response's root field says which checkout answered so the
  viewer can flag the fallback.
- File tabs are keyed by (path, sessionId) so the same path from
  different sessions opens distinct tabs.
2026-07-12 22:15:50 -07:00
Daniel Kim 10eaa5a25e Recents: hover kebab menu (archive/delete) + Active/Archived/All filter (#56)
* Recents: hover kebab menu (archive/delete) + Active/Archived/All filter

Claude-desktop-style session management in the chat rail:
- three-dot menu on session-row hover with Archive/Unarchive and Delete
  (confirm-guarded; delete already existed server-side)
- filter button beside the Recents label: Active (default) / Archived / All
- new archived column on chat_sessions + PATCH /api/chat/sessions/{id};
  archive keeps updated_at so sessions keep their recency slot

* Review round 1: delete broadcast, keyboard access, rollback + polish

- delete_session now emits chat.session.deleted; dashboards remove the row
  (and its cached transcript/busy state) instead of keeping ghosts
- set_archived re-reads after the write so the broadcast can't clobber a
  concurrent title change or resurrect a deleted session; sending into an
  archived session unarchives it (activity unarchives)
- kebab reveal uses :focus-within (row-only :focus-visible dropped focus to
  body mid-Tab); row keydown ignores events from descendant buttons
- archive rollback restores the pre-request snapshot, not the negation;
  removeSession alerts on failure and uses functional setActiveId
- Escape closes all popovers; dead .rail-filter-btn.active rule removed;
  danger color uses --accent-red; copy/labels tidied

* Review round 2 polish: delete tombstones, keyboard menu dismiss, error surfacing

- deletedIds tombstone set ignores chat.session upserts racing a delete
  (a naturally-finishing turn could resurrect a ghost row)
- row Enter/Space mirrors the click branch: dismiss an open menu instead of
  switching sessions underneath it
- deleteChatSession goes through the shared json helper so the alert shows
  the real server error; alert copy names the session
- one filter reset in send() covers new sessions and unarchive-on-activity
- SESSION_FILTERS carries railLabel (single table); comment placement fix
2026-07-12 22:07:02 -07:00
Rehaan Ahmad 7844718961 skills (#55) 2026-07-12 19:28:41 -07:00
Daniel Kim 55692ef239 Codex harness: inject GitHub token so sandboxed git/gh can authenticate (v0.1.50) (#53)
* Codex harness: inject GitHub token so sandboxed git/gh can authenticate

Codex's workspace-write Seatbelt sandbox blocks the macOS keyring, so gh
inside the runner reports its stored token invalid and git's credential
helper can't authenticate — private-repo fetch/push fail even with the
network access granted in #51. Resolve the token in the parent process
(GITHUB_TOKEN env, synced env, gh auth token) and pass it to the codex
child as GH_TOKEN/GITHUB_TOKEN, which gh and its credential helper
prefer over the keyring.

* Bump version to 0.1.50
v0.1.50
2026-07-12 17:36:18 -07:00