Commit Graph
82 Commits
Author SHA1 Message Date
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.
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.
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.
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 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
2026-07-14 17:48:05 -07:00
Rehaan Ahmad a70206655b chore: prompt improvements (#65)
* improvements

* skill

* cargo

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

* open runs

* ci
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
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 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
2026-07-12 17:36:18 -07:00
Daniel Kim 31120adda1 Sort projects by last visited; open the most recent project on startup (#52)
Opening a project now POSTs /api/projects/{id}/open, which bumps updated_at.
The store lists projects updated_at DESC, so the projects page sorts by
recency and the app boots into the last project you were in. The bump also
fires the existing project.updated SSE diff, keeping open lists in sync.
2026-07-12 17:36:00 -07:00
Daniel Kim eff59cd1a5 Codex harness: resolve bin symlinks, grant git dir + network in Auto sandbox (v0.1.49) (#51)
* Codex harness: resolve bin symlinks, grant git dir + network in Auto sandbox

Two fixes for headless codex-cli 0.144 under orx:

1. find_codex() now canonicalizes the PATH hit. Codex >= 0.144 locates its
   codex-code-mode-host helper as a sibling of the *invoked* path without
   dereferencing symlinks, and installers drop only a lone `codex` symlink
   into ~/.local/bin — so every spawn via the symlink died with "No such
   file or directory" before running a single command.

2. Auto mode (sandbox_mode=workspace-write) now also passes
   sandbox_workspace_write.network_access=true and declares the repo's
   `git rev-parse --git-common-dir` as a writable root. Codex's sandbox
   blocks network by default (DNS included — git fetch/push, package
   installs, and the orx CLI's localhost API all fail) and denies every
   git metadata write (in-workspace .git is marked read-only; a worktree's
   real metadata lives in the hub clone, outside the workspace). The
   interactive TUI escalates both denials to approval prompts; codex exec
   has no approval channel, so they were hard failures. All behavior
   verified against codex-cli 0.144 via `codex sandbox` (no model calls)
   in plain clones and worktrees.

* Bump version to 0.1.49
2026-07-12 15:19:24 -07:00
Daniel Kim efb6db6644 Files tab: rename from Artifacts + experiment-keyed folders (v0.1.48) (#47)
* Files tab: rename from Artifacts, experiment-keyed folders, project/ namespace

Reapplies the Files-tab rework onto main. (The original PR #44 was
mistakenly based on the #43 branch instead of main, so its content never
reached main when #43 merged.)

The middle-pane tab becomes "Files" and its layout mirrors the experiment
tree: every top-level folder is named for an experiment slug, with the
reserved project/ namespace for cross-experiment syntheses.

- Backend module artifacts.rs -> files.rs; on-disk root <data>/artifacts/
  migrates to <data>/files/ in place on first touch.
- Listing decorates top-level folders whose name matches an experiment
  slug with that experiment (id, slug, title, branch, latest run status).
- Experiment slug generation refuses `project`, so the reserved namespace
  can never collide ("Project" -> project-2).
- Endpoints /api/projects/{id}/artifacts* -> /files*; SSE event
  artifacts.updated -> files.updated.
- Playbook: files-dir wording plus the layout convention (per-experiment
  folders by slug; project/<topic>/ for project-wide reports).
- UI: FilesTab groups top-level entries project/-first, then experiment
  folders (report title + slug/status chip), then a "not linked to an
  experiment" section.
- orx report local guidance points at the files dir.

Cloud-mode CLI commands (orx artifacts / orx artifact) are untouched.
No version bump: main is already at 0.1.47.

* Bump version to 0.1.48
2026-07-10 16:22:31 -07:00
Daniel Kim 768ad11b70 Add Slurm as a compute backend (local experiments) (v0.1.47) (#46)
* Add Slurm as a compute backend (local experiments)

New --backend slurm: submit runs as batch jobs on a Slurm cluster via
ssh to its login node (sbatch --parsable / squeue / sacct -P / scancel),
reusing the ssh backend's multiplexed transport. The repo is cloned on
the login node at submit time (compute nodes often lack internet) with
the GitHub token fed through an inline credential helper so it never
hits argv or .git/config on the shared filesystem. Job state reads
exit_code-first, then squeue, then sacct, with scheduler-unknown (GONE)
debounced over a minute to ride out slurmctld restarts and NFS lag.

Cluster defaults (host/partition/account/time limit) live in slurm.json
with a dashboard settings card (host picker from ~/.ssh/config,
preflight showing detected partitions); the Instances tab shows slurm
runs. --flavor maps to --gres GPU requests.

Also fixes the ssh ControlMaster socket path overflowing macOS's
104-byte sun_path limit (%C hash was too long; ssh failed outright).

Includes an opt-in live E2E test (ORX_SLURM_TEST_HOST) exercised
against a dockerized single-node cluster: submit/complete/logs, cancel,
supervisor reattach, GRES scheduling.

* Bump version to 0.1.47
2026-07-10 15:21:50 -07:00
Daniel Kim c31b80e46b Per-session agent worktrees: parallel chat agents stop sharing one checkout (#43)
Each chat session now gets its own git worktree of the project's hub clone
(~/.cache/openresearch/worktrees/<owner>/<repo>/<session-id>), created
detached on the baseline tip. Worktrees share the hub's object store and
refs, so agents see each other's experiment branches (even unpushed) with
plain `git branch -a`, and git itself refuses to check out a branch another
worktree holds — the old failure mode (one agent switching the shared
clone's branch under another mid-edit) becomes impossible.

- git: ensure_session_worktree / remove_session_worktree + path helpers;
  stale registrations pruned, orphaned worktrees (wiped hub) rebuilt.
- ensure_playbook takes the session id and returns the worktree; the shared
  .git/info/exclude is written on the hub (a worktree's .git is a file).
- Claude/Codex turns spawn with cwd = the session worktree.
- AgentHost (opencode serve) goes from one child per project to one child
  per session, keyed by orx session id, each in its session's worktree;
  interrupt/inline replies resolve the port per session.
- Session delete and project delete kill the serve child and remove the
  worktree (best-effort, off the request path).
- Playbook: new "Working alongside other agents" section — orient from
  sibling branches / orx runs / exp desc, keep exp desc current, one branch
  one owner; wording updated from "clone" to "worktree".
2026-07-10 14:14:39 -07:00
Daniel Kim 9cd79abf8e Retire eager baseline: projects start with an empty experiment tree (#42)
* Retire eager baseline: projects start with an empty experiment tree

Creating/importing a project no longer auto-creates the baseline. The first
no-parent create-experiment becomes the baseline (the control); a partial
unique index (SQLite) hardens one-root-per-project against races. The hosted
client now calls POST /projects/{id}/baseline-experiment (renamed from
import-baseline; server keeps a deprecated alias) and can seed --run-command.
Playbooks, SKILL.md, --help text, and the dashboard empty states now teach the
create-the-baseline-first flow.

* Allow multiple baselines per project

Drop the one-root-per-project unique index (plus a DROP INDEX guard for dbs
that ran earlier builds of this branch) and the create_experiment pre-check.
New --baseline flag on orx create-experiment (and a baseline field on the
local HTTP body) forces another root when one already exists; plain no-parent
creates keep attaching under the oldest root. Playbooks/SKILL.md now describe
multi-root trees (each root is the control for its own subtree).
2026-07-10 11:26:06 -07:00
Rehaan Ahmad e8972bb042 chore: gpt 5 6 (#45)
* gpt 5 6

* ok
2026-07-10 10:51:47 -07:00
Daniel Kim de1aeac999 SSH host test history, chat scrollbar fix, quiet buttons (v0.1.45) (#41)
* Settings/SSH: fix hosts table overflow, persist last preflight per host

The ssh hosts table reused .flavor-table's auto layout, so unbreakable
key paths forced it wider than the card and pushed the Test column
outside it. Fixed layout + anywhere-wrapping pins every column inside
the card at any width.

Preflight results now land in a ssh_host_tests table in orx.db and come
back with GET /api/settings/ssh, so the Status column shows the last
result and how long ago it ran ("never tested" until then) across
restarts.

* UI: drop headline/bold weight from 700 to 600

Chat title, sidebar project title, and markdown strong/headings all
render SF Pro semibold now instead of the browser-default 700; the
markdown weights were previously implicit (UA bold).

* Chat: move the thread scrollbar to the pane's right edge

The 760px readable cap lived on .chat-pane, so the thread's scrollbar
hugged the text column instead of the pane. The cap now lives on the
header, the composer, and a new .chat-thread-inner wrapper inside the
scroller — same pattern .settings-view-scroll already used (whose
.settings-pane un-cap override is now redundant and removed).

* UI: retire the neo-brutalist button style

.btn drops the 2px text-colored border, square corners, hard offset
shadow, and hover translate for the quiet idiom modals/home already
used: 1px hairline border, 8px radius (6px for .sm), flat background
that tints on hover. Filled primary/success variants darken slightly on
hover via color-mix; the now-redundant :is(.modal, .home) overrides are
gone.

* Review round 1 fixes + bump version to 0.1.45

- ssh_settings degrades to 'never tested' on a store error instead of
  failing the host listing (matches the preflight write's best-effort
  intent)
- SshHostTest serializes via serde (host skipped) so the wire shape
  lives in one place instead of three hand-built json! maps
- SSH settings UI derives display state at render (tests[host] ??
  lastTest) instead of copying server state into client state
- drop the dead .btn.success variant; give primary/danger distinct
  :active tints
- shared --readable-col var replaces five hardcoded 760px caps
- fix stale comments (ChatPanel settings-pane, styles.css neo-brutalist
  header), label ssh-table column widths, project-card name 700->600

* Review round 2 nits

- rename record_ssh_host_test -> upsert_ssh_host_test (store convention)
- 700->600 for .term-title and .onb-card-name (same title role as the
  sweep's other targets)
- fix two stale comments (neo square send button; hardcoded 760px)
2026-07-09 17:59:44 -07:00
Daniel Kim 0e2eff44fd Codex + OpenCode parity: permission modes, reasoning, interactive prompts (#40)
* Codex + OpenCode parity for permission modes, reasoning, and interactive prompts

PR #37 wired the Harness trait's composer toggles + interactive prompt cards for
Claude Code only. This extends them to Codex and OpenCode, addressing the two
Claude-shaped seams the review flagged.

Empirically pinned each CLI's contract by spawning the real binary (codex-cli
0.143, opencode 1.17.15) and capturing its event/reply protocol before coding —
not by guessing. Key findings that shaped the design:

* `codex exec --json` has NO inline approval channel. With approval_policy=
  on-request it either runs the command or the sandbox denies it and the model
  reports failure — it never emits an approval-request event or pauses (those
  live only in codex's app-server/TUI protocol). So Codex permission modes map
  onto the *sandbox policy*, not prompt cards; codex emits no WirePrompt.
* `codex exec resume <id>` rejects `-s`/`--sandbox` ("unexpected argument") but
  accepts `-c sandbox_mode=` on both fresh and resume — verified on both paths.
  So the sandbox is set via `-c`, one form for the whole session lifecycle.
* OpenCode genuinely approves *inline*: serve emits permission.asked /
  question.asked while the session.prompt POST stays open (turn paused), and the
  reply is POSTed back to that live process. Reply bodies verified against live
  serve (permission {response: once|always|reject}; question {answers:[[label]]}).

Changes per seam:

1. resume_from_prompt trait method + ResumeAction enum (SendMessage | Handled |
   Nothing). ChatHost::respond orchestrates (locate card, mark resolved,
   broadcast) but the harness decides HOW an answer flows back: Claude ends its
   turn and resumes with a new message; OpenCode replies inline over serve and
   returns Handled (busy stays set — the turn is still running). Inline harnesses
   are never routed through the new-message resume path.

2. Neutralized PermissionMode wire ids off Claude's --permission-mode spelling
   (default/acceptEdits/bypassPermissions) to harness-agnostic ids (ask/
   accept-edits/bypass; plan/auto unchanged). Claude maps enum→CLI in claude.rs;
   a store data-migration rewrites old rows; the React resumeMode strings and the
   composer (already data-driven from advertised options) follow.

Per harness:
* Codex: sandbox-mapped modes (Plan→read-only, Auto→workspace-write, Bypass→
  full-access + bypass flag) + reasoning low/medium/high via -c
  model_reasoning_effort.
* OpenCode: inline permission/question cards from serve SSE events, held open and
  replied over the live session; permission modes Ask/Auto/Bypass (Auto/Bypass
  auto-approve; Ask surfaces & waits); no reasoning axis (model property).

Correctness hardening (from the review loop): per-session respond serialization
(no double-fire on double-submit), flush reconciliation so a concurrent answer
isn't clobbered by the paused turn's next flush, a liveness guard so a stale
answer to a dead turn is rejected instead of lost, and auto-approve/parse-failure
fallbacks so one flaky reply never loses the whole turn.

52 tests (14 new): wire-id round-trips, sandbox/reasoning mappers, resume
synthesis, and the advertised per-harness option vocabulary. fmt/clippy/test
clean; verified the live /api/harnesses payload advertises the neutralized ids.

* Style the interactive prompt cards (plan / permission / question)

PR #37 shipped the prompt-card JSX in ChatPanel.tsx but never added any CSS for
its classes — so plan/permission/question cards rendered as an unstyled text
wall. The AskUserQuestion card was the worst: the option label and its
description are separate inline <span>s with no styling, so they glued together
("modalModal Sandboxes…", "hfHugging Face Jobs…") and the whole card dumped as
flowing text instead of clickable choices.

Add the missing styles for all 11 prompt-* classes plus btn-primary/btn-ghost
(used only by these cards), matching the app's neo-brutalist .btn idiom (2px text
borders, hard offset shadow, square corners) with a per-kind left accent stripe.
The fix for the glue bug: each option is a real full-width button laying its
label and description out as block elements in a column, so they never run
together. Resolved cards render muted and read-only.

CSS-only; UI build clean.

* Quiet the prompt cards — drop the shadow and heavy borders

The first pass matched the app's neo-brutalist .btn look (2px black borders,
hard bottom/right offset shadow, square corners), which made the plan/permission/
question cards shout for attention in the transcript. Tone them down to read as
inline asides: hairline 1px border, a thin 3px per-kind left accent stripe,
6px rounded corners, and no shadow anywhere (card and buttons). Options and
action buttons get the same lighter treatment (hairline borders, subtle hover,
weight 600 instead of 700).

CSS-only; UI build clean.

* Fix: approving a permission card must resume under bypass, not acceptEdits

Clicking "Allow" on a Claude permission card wasn't actually granting the
command — it looped, re-asking for the same permission even after approval.

Root cause: Claude Code's headless `--print` can't prompt interactively, so a
tool needing approval is *denied* (recorded in permission_denials) and surfaces
the card. "Allow" resumed the session under `acceptEdits`, but that mode only
auto-approves file edits — it does NOT grant Bash (or any non-edit tool). So the
resumed turn re-attempted the blocked `orx …` command, got denied again, and the
card re-appeared indefinitely.

Verified against the real CLI (same denied session, same resume message, only
the mode differs): `acceptEdits` → permission_denials still ['Bash'];
`bypassPermissions` → denials cleared, the tool runs. Claude's --permission-mode
is coarse — there's no per-tool "allow once" in headless — so approving means
bypassing for the continuation.

Fix: synthesize_resume("permission") defaults an approval to `bypass` (an
explicit resume_mode still overrides); the permission card's "Allow" now sends
just {approve:true} and lets the harness pick the mode. Pinned by a new test.

* Claude: offer only the permission modes headless actually honors (plan/auto/bypass)

Headless `claude --print` has no interactive approval channel — verified against
the real CLI on a state-changing Bash command (git/curl/touch):
  default(Ask) → denied   acceptEdits → denied (edits only)   plan → denied (r/o)
  auto → runs             bypassPermissions → runs
So "Ask" doesn't prompt the user; it just denies what it would ask about. A
blocked tool can only run by re-running the turn under a more permissive mode.

Offering `ask` / `accept-edits` for Claude was therefore a leaky simulation of a
capability the CLI lacks (the earlier "Allow re-runs under bypass" workaround).
Drop them: Claude now advertises only `plan` / `auto` / `bypass`.

- Claude stops turning permission_denials into approve-me cards. Of the offered
  modes only Plan denies, and that's expected (read-only) — an "Allow" that
  re-ran under bypass would silently defeat plan mode. Removed the now-dead
  permission_prompt.
- Plan card's approve buttons → Auto ("Approve & run") / Bypass, the modes that
  actually run the work after leaving plan mode.
- Store migration moves existing Claude sessions off ask/accept-edits → auto.
- OpenCode is unaffected: its serve has a genuine inline approval channel
  (permission.asked → reply), so it legitimately keeps `ask` and real cards.

fmt/clippy/test clean (53 tests; advertised-options test updated to [plan,auto,bypass]).

* Let read-only orx run in plan mode via a PreToolUse hook

Problem: in Claude plan mode the agent couldn't run `orx projects`/`runs`/`logs`
to inform a plan — plan mode is a hard read-only gate that blocks any Bash it
can't verify as read-only, and `--allowedTools` does NOT pierce it. Verified
against the CLI: a PreToolUse hook returning permissionDecision:"allow" DOES.

Add a hidden `orx hook-approve` subcommand that acts as that hook: it reads the
tool call on stdin and approves ONLY read-only orx inspection commands, so they
run in every mode including plan; everything else defers to the mode's gate. The
Claude adapter registers it via an inline `--settings` JSON pointing at the
absolute (single-quoted, space-safe) path of the current orx binary. Verified
end-to-end: `orx version` runs in plan mode; `orx up` still gates.

The read-only classification lives in Rust as the single source of truth
(READONLY_SUBCOMMANDS + per-verb gating for mixed commands like `project view`
vs `project edit`), kept in lockstep with the Command enum.

Security (adversarial multi-agent review):
- The classifier refuses any shell metacharacter, so an approved `orx …` can
  never chain a second command (the tokenizer/bash-IFS mismatch was checked and
  yields no bypass in either direction).
- Excluded every write primitive a read-only gate must not open: `report
  download` (writes report.md to an arbitrary, traversal-capable dir), `chart`
  (writes a PNG), `exp cmd/desc --set` / `exp cancel`, plus all top-level
  mutating commands. `orx query` is a server-side read endpoint (no local write).
- Path-with-spaces in the hook command is single-quoted (would otherwise break).

fmt/clippy/test clean (61 tests; 8 new for the classifier + hook settings).

* Drop plan mode for Claude, and remove the plan-mode approval hook

Plan mode fought the orx workflow rather than helping it: it's a hard read-only
gate that blocks the read-only `orx` inspection the agent needs to build a plan
*and* the launches that are the whole point. The PreToolUse hook added to prop it
up (`orx hook-approve`) was also janky in practice — the agent's natural
`orx … 2>&1 | head` got rejected by the anti-command-chaining guard, so the hook
rarely fired. Net: a lot of machinery to make a mode limp along that isn't suited
to this workflow.

So Claude now offers only `auto` + `bypass`, and the whole hook feature is
removed (`orx hook-approve` subcommand, `claude_hook_settings`/`shell_single_quote`,
the `--settings` wiring, and their tests). "Propose a plan then approve" happens
in conversation instead: the agent describes its plan and asks before running
`orx exp run`.

The plan CARD stays — Claude emits `ExitPlanMode` even in auto mode when asked to
plan (verified), independent of the plan permission mode; its approve buttons
resume under auto/bypass. Store migration moves existing Claude sessions off the
retired modes (ask/accept-edits/plan) → auto. OpenCode is unaffected (real inline
approval).

fmt/clippy/test clean (53 tests); verified /api/harnesses advertises only
auto/bypass for Claude.

* Codex: drop plan mode too (match Claude — auto/bypass only)

`codex exec` has no usable plan mode, for the same reason it has no approval
channel: Codex's first-class Plan mode lives in the app-server/TUI layer, not
exec. Verified `codex exec -c collaboration_mode="plan"` is accepted but ignored
(the command ran anyway). The `Plan`→`-c sandbox_mode="read-only"` mapping we'd
offered had the identical defect that got plan dropped for Claude — read-only
blocks the `orx` inspection the agent needs AND the launches that are the point.

So Codex now advertises only `auto` + `bypass`, consistent with Claude. The
`codex_sandbox` mapper keeps its Plan→read-only arm defensively; only the
advertised list changed. Store migration extended to move Codex sessions off
retired modes (ask/accept-edits/plan) → auto. OpenCode unchanged (real inline
approval keeps `ask`).

fmt/clippy/test clean (53 tests); verified /api/harnesses advertises [auto,bypass]
for both claude-code and codex, [ask,auto,bypass] for opencode.

* OpenCode: add plan mode via its native `plan` agent

Unlike Claude/Codex (whose exec CLIs have no usable plan mode), OpenCode ships a
real, clean one: a built-in `plan` agent (verified live — "Plan mode. Disallows
all edit tools") whose permission ruleset denies edits but ALLOWS everything else
— so read-only inspection like `orx projects`/`runs`/`logs` runs fine while
planning, then you switch to `build` to execute. That's exactly the behavior
Claude/Codex's coarse read-only gates couldn't give.

Wire it through the message body's `agent` field (the v1 session.prompt endpoint
takes it directly — verified — so no separate switch call): `Plan` → agent
`plan`, everything else → `build`. OpenCode now advertises [plan, ask, auto,
bypass]. Plan and the permission-reply axis (surface card vs auto-approve) are
orthogonal; Plan surfaces cards like Ask, and its agent self-restricts edits.

Each harness now offers exactly what its CLI natively supports: Claude/Codex
auto+bypass, OpenCode plan+ask+auto+bypass.

fmt/clippy/test clean (54 tests); verified /api/harnesses advertises the plan
option for opencode and the agent field reaches serve.

* OpenCode: drop the hollow `ask` mode — offer Plan + Auto + Bypass

OpenCode's native default is permissive (`allow *`; it only prompts on a few
risky cases — runaway loops, out-of-workspace writes, `.env` reads), verified
against an empty user config. So a dedicated "ask for every permission" mode was
hollow: cards almost never fired, and ask/auto/bypass barely differed. Drop it.

OpenCode now offers Plan (its native plan agent) + Auto (the permissive default —
still surfaces the rare card / any question) + Bypass (auto-approves even those),
default Auto. Only Bypass auto-replies; Auto/Plan surface the cards that do fire.
The inline card/question machinery stays fully exercised (Plan mode + questions).

Store migration moves existing opencode `ask` sessions → auto (keeps `plan`).

Net across harnesses, each offers exactly its native surface:
  claude-code / codex → [auto, bypass]
  opencode            → [plan, auto, bypass]

fmt/clippy/test clean (54 tests); verified /api/harnesses.

* Composer pickers follow the open session, not a stale global preference

Bug: the composer had a split brain. The placeholder read the OPEN session's
locked harness ("Message Codex"), but the model/mode/reasoning pickers read a
sticky GLOBAL selection from localStorage. So with a Codex session open but the
picker last set to OpenCode, it showed "Message Codex" + "Plan mode" (a mode
Codex doesn't even have) + an opencode model — contradicting each other. And
picking e.g. Plan mode on the Codex session silently did nothing (send()'s
sameHarness guard dropped mismatched overrides).

Fix: when a session is open, the pickers reflect and edit THAT session's
settings (harness/model/mode/reasoning — already stored on the session), with any
unsent tweak layered on and applied (persisted server-side) on the next send.
The harness is locked once a session exists (its harness is immutable), so the
model picker offers only that harness. The sticky global preference now only
seeds NEW sessions. Unsent tweaks clear when switching sessions.

Verified against the real DB + live API: with the codex session (gpt-5.5, auto)
open, the composer now shows "Message Codex" + "Auto mode" — placeholder and
toggles agree. UI build clean.

* Composer placeholder follows the picker for a new session

After the previous fix tied the placeholder to the open session, a NEW session
(none open yet) fell back to the generic "Ask the research agent…" and no longer
tracked the harness picker — switching to OpenCode/Codex left the placeholder
unchanged. Point it at `composerSelection` instead of `activeSession`: that
resolves to the picked harness for a new session and the open session's harness
once one exists, so "Message OpenCode" / "Message Codex" now updates with the
picker in both cases.

UI build clean.

* Review fixes: close the opencode resolve/flush race + polish

Multi-agent review of the full branch surfaced these:

- **BLOCKER (fixed): lost-update race on opencode cards.** A still-running
  opencode turn's `flush` (which carries a concurrently-resolved card's flag
  forward via `adopt_resolved_prompts`, then upserts the message) could interleave
  with `respond`'s `mark_prompt_resolved` (read parts → set resolved → upsert the
  same message) and clobber the resolve back to false — reviving an already-
  answered card. SQLite's WAL serializes the two writes but not the logical
  read-modify-write. Add a process-wide `msg_write` sync mutex on ChatHost;
  `flush` (when the message has a prompt part) and `mark_prompt_resolved` each
  hold it across their whole RMW, so the two can't interleave. Never held across
  an `.await`; writes are brief and already WAL-serialized, so no real contention.

- Double-submit of one card: the loser now gets a clean `Ok(())` no-op instead of
  `Err`, so the UI doesn't clear `busy` on a still-streaming turn.

- Stale doc comments fixed: claude.rs module header (permissions are mode-driven
  now, and questions ARE surfaced) and handle_prompt_event (only Bypass
  auto-replies; Auto/Plan surface cards).

- Added opencode `permission_card`/`question_card` fixture tests (the parsing was
  the one uncovered spot — pins field names incl. `multiple` vs Claude's
  `multiSelect`).

fmt/clippy/test clean (56 tests).
2026-07-09 17:41:08 -07:00
Daniel Kim 4f413431dc Add Instances tab: all compute across projects, running vs past (#38)
New sidebar tab showing every run across all projects as compute
instances, split into a Running section (live, with a count badge) and a
Past section. Rows show the backend with its real brand logo (Modal
green cube, Hugging Face smiley, Kubernetes helm wheel; neutral glyph
for ssh), project name, status, started time, and runtime — elapsed and
ticking for live instances, total duration for finished ones. HF rows
link out to their job page (the dashboard URL is already stored on the
run); Modal stores no URL so those rows stay plain.

Backend: GET /api/instances aggregates store.list_runs(500) across all
projects and enriches each run with its project name; reuses the ApiRun
wire type (serialized to a Value, projectName inserted) so the shared
type stays free of instances-only concerns.

UI plumbing worth noting:
- backendKind/backendDetail extractors move to api.ts, shared by the
  runs table's text label and the new BackendBadge (logo + flavor).
- The shared .empty-state and .runs-table-wrap classes are
  position:absolute panel-fillers; the tab uses in-flow equivalents
  (.instances-empty, .instances-table-wrap) instead — reusing them
  overlaid the sidebar and swallowed scrolling.
- Settings views un-cap the 760px chat pane (.settings-pane) so the
  scroll container reaches the pane edge; .settings-view re-applies the
  readable width inside the scroller, centered.
2026-07-09 14:42:31 -07:00
Daniel Kim 428905b689 Harness compat layer: trait + registry, permission/reasoning toggles, interactive prompt cards (#37)
* refactor: unify claude/codex/opencode/cursor behind a Harness trait + registry

Collapse the three parallel per-harness surfaces (detection in
harness.rs, chat run_turn in chat/*, skill install in install_skills.rs)
into one Harness trait with per-harness impls under src/local/harness/.

- Detection, chat-turn, and skill-install are now optional capabilities
  on one trait; each consumer iterates a single registry() and filters
  by capability (supports_chat / skill_target).
- Removes the stringly-typed dispatch match and the triple-duplicated
  harness id lists (HARNESS_IDS, detect assembly, install Agent enum).
- Cursor becomes a first-class install-only harness impl.
- Adding a harness is one new file + one registry() line.

No behavior change: detection order, wire output, and install paths are
preserved.

* feat(chat): per-session permission mode + reasoning level toggles

Add two composer controls, wired end to end and driven by the Harness
trait so Codex/OpenCode can adopt them by implementing one method.

- New harness/options.rs: PermissionMode (auto/plan/default/bypass) and
  ReasoningLevel (low/medium/high) as harness-agnostic wire ids, plus
  HarnessOptions the harness advertises via Harness::options().
- Claude Code maps mode -> --permission-mode (auto=acceptEdits, etc.,
  replacing the hardcoded bypassPermissions) and reasoning -> a thinking
  directive prepended to the prompt. Defaults: Auto + High.
- Store: chat_sessions gains permission_mode/reasoning_level columns
  (best-effort ALTER migration) + setters; sticky like model.
- Chat: send_message takes a TurnOverrides struct; TurnCtx carries the
  effective mode/level; overrides persist per-session.
- API: create/send accept permissionMode/reasoningLevel; session_json
  and /api/harnesses (options) expose them.
- UI: composer redesign — permission mode bottom-left, model + reasoning
  bottom-right (matches Claude Code desktop). OptionPicker renders only
  the axes a harness advertises, so Codex/OpenCode show nothing yet.

Verified end to end: create+send round-trips the values, they persist
across reload, per-turn overrides are sticky, and the spawned claude
process receives the mapped --permission-mode.

* fix(chat): match Claude Code's real mode + effort values

Correct the toggles to Claude Code's actual CLI vocabulary (verified
against claude 2.1.145 --help), addressing two errors in the first cut:

Permission modes — 5, not 4. `auto` is a distinct --permission-mode value,
not an alias for acceptEdits. Wire ids now equal the CLI values so no
per-harness remap is needed:
  Ask permissions    -> default
  Accept edits       -> acceptEdits
  Plan mode          -> plan
  Auto mode (default)-> auto
  Bypass permissions -> bypassPermissions

Reasoning — use Claude's own --effort tiers (low/medium/high/xhigh/max,
default high), passed straight through as --effort, replacing both the
generic low/medium/high list and the prompt-injected 'think hard' hack.
Reasoning vocabulary is now per-harness (ReasoningLevel enum removed):
Claude owns its tiers; Codex will own reasoning_effort. Permission mode
stays a shared enum since the concept maps across harnesses.

Composer menu redesigned to mirror the Claude Code mode menu: a header,
the default row pinned on top with a '· Default' note + separator, and
1-based number hints.

Verified: /api/harnesses serves the 5 modes + 5 effort tiers with the
right labels/defaults, and a plan+xhigh session spawns
`claude --print --permission-mode plan --effort xhigh`.

* feat(chat): plan approval + permission/question prompt cards

Surface Claude Code's interactive moments in the UI and let the user act
on them, using the CLI's resume model (headless --print can't answer
inline, so a turn ends on the prompt and the answer resumes the session).

Shared, harness-agnostic wire type WirePart{kind:"prompt"} carries three
prompt kinds — plan / permission / question — so Codex's approvals map
onto the same cards later:
- ExitPlanMode -> plan card (renders the plan markdown) with Approve &
  auto-accept / Approve & ask-each-step / Keep planning. Approve resumes
  the session in the chosen permission mode; keep-planning stays in plan.
- AskUserQuestion -> question card with the options; picking resumes with
  the choice as the reply. No longer disallowed.
- permission_denials -> permission card (Allow / Deny). Allow resumes
  with a more permissive mode.

New POST /api/chat/sessions/{id}/respond + ChatHost::respond marks the
prompt resolved in the transcript and resumes via send_message with a
synthesized follow-up + mode override. Prompt parts render read-only once
resolved.

Verified end to end against claude 2.1.145: plan approve resumes in
acceptEdits, a question surfaces its options and answering resumes with
the pick, denials surface as permission cards.

* feat(ui): compact, coalesced tool-call rendering (Claude-desktop style)

Replace the bulky per-tool bordered boxes with minimal gray one-liners
that blend in, like the Claude desktop app:

- Consecutive tool parts coalesce into one ToolGroup. Collapsed it shows
  a single muted line — the tool's verb phrase for one call, "Used N
  tools" for several. Click to expand all rows; a running tool
  auto-expands.
- toolLine() renders human summaries from the tool + input: "Ran <cmd>",
  "Read <file>", "Edited <file>", "Searched <pattern>", etc., instead of
  the raw tool name + full JSON.
- Each row expands to show the command + output inline in subtle boxes;
  file tools get an "open" link to the viewer.
- Status dot: amber (running) / red (error) / muted (done). Chevron
  rotates on expand.

Verified by rendering the real components' markup against the app's
actual styles.css and screenshotting — matches the reference.

* style(ui): darker, larger, roomier assistant transcript text

Assistant prose was rendering in muted gray (--subtext), small and
tight. Match the desired look:
- .md text color --subtext -> --text (near-black); tool-call lines stay
  gray (they're styled separately).
- Body size 13px -> 14px with line-height 1.62 on .msg-assistant / .md.
- Paragraph spacing 6px -> 10px.

Also improves report readability in the file viewer (shares .md).
Verified by screenshotting the real .md markup against styles.css.

* feat(ui): render markdown tables + syntax-highlighted code blocks

Chat markdown was rendering tables as raw pipe-text and code blocks as
plain unhighlighted <pre>. Fix both:

- Add remark-gfm so GFM tables (and their column alignment) render.
  Restyle .md tables: header background, full border + radius, roomier
  cells, row hover, right-align honored from the `---:` syntax.
- Fenced code blocks now highlight via refractor (same tokenizer +
  .token theme the file viewer uses) through a custom code renderer, and
  get a hover copy button. Inline code stays a plain chip.

Verified by rendering real markdown through the Md component and
screenshotting — matches the reference.

* fix(review): address blockers from multi-agent review

Correctness:
- respond(): 'Keep planning' (plan reject) was dead — the early-return
  fired for every non-question reject, so keep-planning silently stopped
  the session. Now only 'permission' denials / bare rejects close the
  card; plan-reject and question answers resume.
- respond() now broadcasts the mutated assistant message (chat.message
  with resolved:true) so prompt cards render read-only on all clients
  immediately, instead of staying clickable and re-firing a duplicate
  resume after the turn ends.
- send_message: atomic turn-slot reservation (TurnGuard) closes the
  check-then-insert race where two rapid /respond or /message calls could
  both spawn a turn against the same session and orphan an abort handle.
  turns map now holds Option<AbortHandle> (None = reserved-not-spawned).
- claude.rs result handler: drop the  guard that swallowed
  genuine failures whenever any denial was present. A plan/permission
  pause reports subtype:success/is_error:false (verified), so drive the
  error path off is_error alone.
- respond() rejects an empty question answer before marking resolved.

Quality:
- Extract shared refractor highlighting into ui/src/syntaxHighlight.tsx;
  Md.tsx and FileViewer.tsx both use it (was duplicated).
- Soften over-promising doc comments (WirePrompt/PermissionMode/options)
  to mark the interactive-prompt vocabulary Claude-shaped-until-Codex,
  and note synthesize_resume encodes Claude's resume-by-new-message model.

Verified end to end: keep-planning resumes, resolved card broadcasts over
SSE, unknown prompt id -> 400. fmt/clippy/37 tests/tsc green.

* fix(review r2): close interrupt-during-setup turn-slot race

Round-2 review found a real orphaned-turn bug: TurnGuard reserves the
slot under the lock, but send_message's prologue then runs lock-free
across many awaits before upgrading None->Some(handle). An interrupt in
that window removed the reservation and emitted idle, yet send_message
still unconditionally re-inserted the handle and spawned a live turn no
one could abort — busy desynced (UI idle, server busy) and the next
send/respond was wrongly rejected.

Fix: upgrade None->Some under the lock with an atomic re-check. If the
reservation is gone (interrupt won), abort the freshly-spawned task and
don't re-insert — honoring the interrupt. Verified with a send+interrupt
race stress test: slot never wedges, a fresh send stays accepted, no
panics.

Also from round 2 (cleanups): collapse respond's redundant match arms to
a single 'denied permission closes the card' condition, and document why
TurnGuard::drop's try_lock can't leak (the lock is always free when an
armed guard drops).

* fix(review r3): defuse TurnGuard in both upgrade branches

Round-3 review found a residual narrow race in the r2 fix: the abort
(interrupt-wins) branch left the guard armed, so its synchronous Drop
still ran after the locked block. A same-session send that claimed a
fresh reservation in the gap before Drop could have it clobbered by the
stale guard — silently dropping that second turn (a lost turn, not a
busy-desync, but still wrong).

Defuse the guard in BOTH branches while still holding the lock, so its
Drop can never run post-upgrade. Corrected the misleading comment.

Both round-3 reviewers confirmed mergeable; this closes the last
refinement. Re-ran the send+interrupt race stress test: not wedged, no
panics. fmt/clippy/37 tests/tsc green.
2026-07-08 18:52:13 -07:00
Rehaan Ahmad 81eb77616e chore: icml skill (#36)
* icml skill

* tab title
2026-07-08 18:23:16 -07:00
Rehaan Ahmad 372e549ea6 chore: hf (#34)
* hf

* cargo
2026-07-07 22:53:23 -07:00
Rehaan Ahmad 14bebd2ddf chore: artifacts cleanup (#33)
* artifacts cleanup

* cargo build

* cargo
2026-07-07 22:38:32 -07:00
Rehaan Ahmad 5434264f2b redesign (#32) 2026-07-07 17:50:19 -07:00
Rehaan Ahmad 064655647f chore: bump this (#31)
* bump this

* format
2026-07-07 14:50:35 -07:00
Rehaan Ahmad a46d6034bb chore: repo (#30)
* repo

* repo version
2026-07-07 11:03:58 -07:00
Rehaan Ahmad 02b575a09c chore: ui (#29)
* ui

* proper runs

* fmt

* ui?

* yeah

* onboarding flow

* fix this
2026-07-06 20:05:41 -07:00
Rehaan Ahmad 3371de32d7 ok 2026-07-05 17:56:52 -07:00
Rehaan Ahmad aeec0dc151 fmt (#28) 2026-07-05 16:56:11 -07:00
Rehaan Ahmad 34cfc74d73 live status 2026-07-05 16:53:46 -07:00
Rehaan Ahmad 01b2334841 chore: hf job sfixes (#27)
* fixes

* buidl
2026-07-05 15:02:51 -07:00
Rehaan Ahmad f544babf08 chore: decoupled runds 2 (#26)
* decoupled runs

* stream

* fmt

* ok

* cargo
2026-07-05 14:41:47 -07:00
Rehaan Ahmad f3207e6bd3 chore: decoupled runs (#25)
* decoupled runs

* stream

* fmt
2026-07-05 13:31:45 -07:00
Rehaan Ahmad c23a1caa38 chore: set public (#24)
* set public

* cargo fmt
2026-06-26 23:29:03 -07:00
Rehaan Ahmad 9c68a0a7be continue errors (#23) 2026-06-21 22:16:38 -07:00
Rehaan Ahmad 9db692cf69 chore: download report (#22)
* empty repo seeding

* download report

* cargo lock

* fmt
2026-06-19 20:39:02 -07:00
Daniel Kim 0ebd96ae19 feat: warn on every command when the CLI is outdated (#20)
* feat: warn on every command when the CLI is outdated

Every orx command now prints a warning to stderr when the running version
is behind the latest release, because the backend API is versioned and a
stale client can hit breaking changes. The message escalates with the gap:
a breaking (pre-1.0 minor / post-1.0 major) gap warns that commands can
start failing now; a smaller gap is a gentler upgrade nudge.

This reworks the existing interactive-only update nudge:
- Shows on every command (piped/scripted/CI too), not just interactive
  terminals; ORX_NO_UPDATE_CHECK / NO_UPDATE_NOTIFIER /
  OPENRESEARCH_CLI_DISABLE_UPDATE=1 silence it.
- Prints in start() before the command runs, so it shows even for commands
  that std::process::exit on their own (e.g. the not-logged-in path).
- Latest version still comes from the cached prior check (instant, zero
  latency); a fire-and-forget background task refreshes the cache, given a
  brief grace window in finish() to commit before the runtime tears down.
- Cache write is now atomic (temp + rename) so concurrent orx processes
  never read a torn file.
- Version comparison ignores build metadata (semver's Ord compares it, the
  spec says it shouldn't) to avoid a false 'outdated' on local builds.

Excludes the version/update commands, which manage updates themselves.

* feat: bold the warning label on a TTY and set it off in its own block

The outdated-version warning now prints with a blank line above and below
so it reads as its own block, and bolds just the `Warning:` label
(gh/cargo-style) when stderr is an interactive terminal. Pipes, files, and
CI logs stay plain text — the ANSI is gated on `stderr().is_terminal()`
and honors NO_COLOR — so non-interactive output never gets raw escape codes.

Hand-rolled \x1b[1m/\x1b[22m (no color dependency, matching the codebase's
plain-text style); `render`/`bold` are pure and unit-tested.

* refactor: apply review findings (CI latency, label const, doc cleanup)

From the multi-agent code review:
- finish() now skips the 250ms refresh wait when stderr isn't a terminal.
  In ephemeral CI (fresh config dir per run) the cache is always cold, so
  the wait warmed a cache that's immediately discarded — pure per-call cost
  across a pipeline. The refresh is still spawned; we just don't wait. The
  warning still prints on every command. Interactive runs keep the wait so
  the cache warms for the next command.
- Hoist a shared WARNING_LABEL const used by both warning_for (builder) and
  render (styler), so the bolded label can't silently drift from the emitted
  prefix on a copy edit.
- Drop the now-inaccurate "passive" framing from the module header, section
  banner, struct doc, and main.rs wiring comment (the warning is always-on).
- s/nudge/update check/ in the version/update command comments.
- Add prerelease-axis tests for warning_for (rc-behind-final is soft, a
  cross-minor prerelease is breaking, a local dev prerelease ahead is silent).

* simplify: drop the breaking/soft warning tiers, always show one message

Per request, remove the two-tier distinction entirely. The warning no longer
classifies the version gap as breaking vs. non-breaking — every outdated client
gets the same calmer message:

    Warning: orx X is outdated (latest Y). A newer release is available; upgrade
    to stay compatible with the API. Run `orx update` to upgrade.

Deletes `is_breaking_gap` (and its RE-CONFIRM-AT-1.0 note), the second message
arm in `warning_for`, and the tier-specific tests; collapses the gap-coverage
tests into one that asserts the single message fires for every kind of gap.
`precedence` (the build-metadata comparison guard) is unrelated and stays.
2026-06-19 00:24:51 -07:00