mirror of
https://github.com/alphaXiv/OpenResearch.git
synced 2026-11-12 10:05:24 +08:00
203f63b900d7eb8a973c19a327c1484840c9db31
82
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
2c50f99a21 | report skill (#76) | ||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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).
|
||
|
|
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.
|
||
|
|
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
|
||
|
|
a70206655b |
chore: prompt improvements (#65)
* improvements * skill * cargo * prompt improvements |
||
|
|
b1cab32b48 |
chore: local runs (#64)
* open runs * open runs * ci |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
9799d21d9f | fix: append ellipsis to truncated chat session titles (#59) | ||
|
|
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. |
||
|
|
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
|
||
|
|
7844718961 | skills (#55) | ||
|
|
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 |
||
|
|
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.
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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". |
||
|
|
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).
|
||
|
|
e8972bb042 |
chore: gpt 5 6 (#45)
* gpt 5 6 * ok |
||
|
|
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)
|
||
|
|
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). |
||
|
|
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. |
||
|
|
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.
|
||
|
|
81eb77616e |
chore: icml skill (#36)
* icml skill * tab title |
||
|
|
372e549ea6 |
chore: hf (#34)
* hf * cargo |
||
|
|
14bebd2ddf |
chore: artifacts cleanup (#33)
* artifacts cleanup * cargo build * cargo |
||
|
|
5434264f2b | redesign (#32) | ||
|
|
064655647f |
chore: bump this (#31)
* bump this * format |
||
|
|
a46d6034bb |
chore: repo (#30)
* repo * repo version |
||
|
|
02b575a09c |
chore: ui (#29)
* ui * proper runs * fmt * ui? * yeah * onboarding flow * fix this |
||
|
|
3371de32d7 | ok | ||
|
|
aeec0dc151 | fmt (#28) | ||
|
|
34cfc74d73 | live status | ||
|
|
01b2334841 |
chore: hf job sfixes (#27)
* fixes * buidl |
||
|
|
f544babf08 |
chore: decoupled runds 2 (#26)
* decoupled runs * stream * fmt * ok * cargo |
||
|
|
f3207e6bd3 |
chore: decoupled runs (#25)
* decoupled runs * stream * fmt |
||
|
|
c23a1caa38 |
chore: set public (#24)
* set public * cargo fmt |
||
|
|
9c68a0a7be | continue errors (#23) | ||
|
|
9db692cf69 |
chore: download report (#22)
* empty repo seeding * download report * cargo lock * fmt |
||
|
|
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.
|