Commit Graph
172 Commits
Author SHA1 Message Date
Daniel Kim cec874e47c Simplify project files as artifacts (#145) 2026-07-31 16:51:56 -07:00
Daniel Kim 07d3221f3e Increase dashboard typography (#143) 2026-07-31 10:28:57 -07:00
3e934765e1 Add Ray Jobs compute backend (v0.1.88) (#141)
* Add Ray Jobs as a first-class local compute backend.

Submit via the Ray Jobs API with Settings → Compute → Ray, optional
cpu/gpu/mem flavors, and the same supervise/log/cancel path as other backends.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Review fixes: sync cloud skill, reject inapplicable flags, panic-safe log tail, fmt/clippy, dist rebuild (v0.1.87)

* Round-1 review fixes: synced env for ray jobs, server-plane ray arm, help text, honest configured flag, resolver/preflight simplification, NaN guard, address validation

* Round-2 review fixes: GONE debounce for purged ray jobs, cancel re-issue, prefix-guarded log tailer, drop SubmitResponse/Explicit, single URL normalization point

* Round-3 nits: dirty-flag log tailer invariant, slurm-shaped cancel gate on transitions, doc accuracy

* Rebase on v0.1.87; bump to v0.1.88 and rebuild ui/dist

* Use official Ray and OpenResearch marks in BackendLogos

---------

Co-authored-by: Sébastien Fabbro <sebfabbro@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
v0.1.88
2026-07-30 20:43:33 -07:00
Daniel Kim 0ddf5027b4 docs: add OpenResearch account signup 2026-07-30 18:50:54 -07:00
Daniel Kim d5d1e02128 chore: ignore local agent skills (#142)
* chore: add repository-scoped agent skills

* chore: keep agent skills local
2026-07-30 14:01:51 -07:00
Daniel Kim 5e25afaf98 Surface silently-dropped chat turns (v0.1.87) (#139)
* Surface silently-dropped chat turns (v0.1.86)

A sent message could vanish with no response and no error. Evidence from
the live DB: user messages persisted with no assistant reply at all —
the claude turn loop waits on child events with no watchdog, so a wedged
resident child (seen: expired-OAuth refresh hang) spins forever; the
user's only recourse, Stop/Escape, aborted the turn task without leaving
any trace; and a failed send POST was swallowed by the UI catch.

- claude adapter: watchdog the event wait (120s to first event, 30min
  between events, matching the codex TURN_WATCHDOG), exempting turns
  held on a pending mcp-gate approval; on trip, kill the child and
  surface a visible error (next message resumes via --resume).
- ChatHost::interrupt_by_user: the Stop/Escape path now persists an
  'Interrupted' marker row, so a stopped turn no longer reads as 'orx
  did nothing'. Internal interrupts (plan resume, deletes) stay silent.
- UI: a failed send now renders a local error row instead of silently
  clearing busy; chat SSE reconnects re-seed sessions/busy and the
  active transcript (chat frames are edge-only, gap frames are lost).

* Address review: verified send-failure surfacing, watchdog guard arm, shared consts

- send() catch: restore draft/attachments even when session creation
  failed; an explicit 'session is busy' rejection always surfaces (that
  busy is someone else's turn); a network failure probes server-side
  busy first and undoes the restore when the turn is real.
- claude watchdog: permission re-arm is a guard arm that no longer
  promotes the first-event deadline; error reports minutes on the
  mid-turn path and the login hint only on the first-event path.
- TURN_WATCHDOG hoisted to harness/mod.rs, shared with codex.
- interrupt_by_user: timestamp captured before the abort; WirePart::tool
  constructor (also used by push_error).
- Reconnect repair: shared syncSessionList (deletedIds tombstones,
  contextUsage merge, seenTitles baseline), seedBusy scoped by known
  ids, transcript reseed retries once via msgGen when a flush raced it.

* Guard claude turns against --resume replay output (echo boundary)

Folds in (and supersedes) the unmerged daniel/fix-swallowed-messages
branch: a --resume respawn can replay prior-session/startup output
before touching the new message — fatally a stale result event, which
ended the turn instantly with zero parts while the real reply streamed
into a dropped channel. --replay-user-messages makes the child echo the
submitted message; the turn loop now skips everything before the echo.

Review round (2x empirical against claude 2.1.212): echo is byte-exact
and stdin-scoped (no history replay, so no same-text false positive);
pre-echo lines count as watchdog proof-of-life (API backoff after boot
must not be killed at 120s; only total silence keeps the fast wedge
budget + login hint); pre-echo system/init harvests the session id for
first-turn interrupts; module docs pin the echo contract.

* Reconcile deleted sessions on SSE reconnect; repair after a failed first connect

Addresses review findings 2 and 3 on PR #139:

- A session deleted while SSE was down lost its chat.session.deleted
  frame for good: syncSessionList removed the sidebar row but left the
  cached transcript, loadedSessions, activeId, and — via the known-
  scoped seedBusy merge — a stale busy flag. Now the pre-fetch snapshot
  of shown sessions is diffed against the authoritative list and every
  missing id gets the full forgetSession cleanup, before seedBusy so
  the ghost's flag can't be carried forward. The snapshot is taken
  before the fetch (a session created mid-flight can't be tombstoned),
  and the project-change reset clears the snapshot ref synchronously
  (the old project's rows must not read as deleted under the new one).

- 'reconnected' now also fires when the first successful EventSource
  open follows a FAILED first connect (page loaded while the backend
  was briefly down) — the initial fetches likely failed too, so the
  panel repaired itself only on later drops, never on that first
  recovery. A clean first open still emits nothing.

Verified live on the slot-8 dev stack: session row deleted from the
DB while the backend was down; on reconnect (same page, no reload)
the row, ghost transcript, active selection, and busy flag all clear.
v0.1.87
2026-07-29 16:41:13 -07:00
Daniel Kim 286d9681d5 Stamp chatSessionId on server-plane experiment creation (#137)
Server-plane experiment creation now forwards the launching chat session so
openresearch.sh can attribute experiments to the agent conversation that
asked for them.

Splits ORX_LOCAL_SESSION out of ORX_CHAT_SESSION_ID: the cloud box's opencode
plugin exports the latter for attribution, which would otherwise have made
in_local_session() true on every cloud box and served the Local skill bodies
in place of the Full ones.
2026-07-29 13:21:51 -07:00
Daniel Kim bed1b31aaf Resolve chat file links via the files dir; fix \boxed math rendering (v0.1.86) (#140)
- FileViewer: a checkout-sourced tab whose path misses the worktree/clone now
  falls back to the project's files-dir endpoints (agents link files-dir
  reports with checkout-relative paths); renders via ReportMd with a note
  naming the checkout that missed. Branch-ref tabs never fall back.
- Pin katex to ^0.16.22 so the imported stylesheet matches the katex version
  rehype-katex actually renders with (0.16.47); the 0.18 stylesheet lacked
  rules for 0.16 class names, collapsing \boxed{} to a 1px vertical line.
v0.1.86
2026-07-29 13:20:27 -07:00
Daniel Kim 33feb2e1ac Don't mint empty reasoning parts; fix tool-group rendering artifacts (v0.1.85) (#136)
* Don't mint empty reasoning parts; fix tool-group rendering artifacts (v0.1.85)

Fable's encrypted thinking streams empty thinking_delta events (payload in
signature_delta) and per-block assistant events with thinking:"" — orx stored
these as invisible empty reasoning parts, which split consecutive tool calls
into single-row groups; an expanded singleton group then painted the identical
tool line twice (summary + row).

- claude.rs: skip empty stream deltas and empty thinking blocks (main loop and
  sub-agent routing) so empty parts are never created
- ChatPanel renderParts: parts that render nothing no longer break a tool run
  (repairs already-stored transcripts)
- ToolGroup: a single-tool group renders its ToolRow directly — no duplicate
  summary line; has-error color parity via styles.css
- test: encrypted_thinking_mints_no_empty_reasoning_parts (captured live from
  the claude CLI)

* Multi-tool group summary is always the count

A running group is always expanded, so a summary echoing the running tool's
line always sat directly above the identical row. 'Used N tools' never
duplicates. Rebuilt ui/dist.

* Review round: shared visibility predicate, sub-agent per-block offset fix, nits

- ChatPanel: extract partIsVisible as the single invisible-part predicate,
  shared by messageHasVisibleContent and renderParts; renderParts now also
  skips resolved permission cards (Codex inlines them into the turn's parts,
  where they split tool runs the same way empty reasoning parts did)
- SubagentTranscript: gate the empty state on rendered output, not raw part
  count, so stored all-invisible transcripts read as empty
- claude.rs: guard empty text blocks like thinking blocks (both loops);
  comment accuracy passes (delta guard scope, cross-layer back-reference,
  ToolGroup doc, CSS child-combinator note)
- apply_subagent_blocks: fix per-block offset semantics — .skip(start) treated
  the cumulative message offset as an index into this event's (single-element)
  array, silently dropping every sub-agent block after a message's first;
  now mirrors the main loop's start+n id shift. Caught by the new
  subagent_empty_thinking_mints_no_child_parts test; also added offset
  assertion comment
v0.1.85
2026-07-27 19:56:16 -07:00
Daniel Kim 5228228b2d Auto-generate and animate chat session titles (v0.1.84) (#135)
* Auto-generate chat session titles from the first message

Chat sessions were titled with a 64-char truncation of the first user
message. Now the first message also kicks off a short-lived headless
harness child that names the session, in parallel with the turn.

- store: new chat_sessions.title_source column ('fallback' / 'generated'
  / 'user'; NULL on legacy rows) via the best-effort ALTER pattern.
  set_chat_session_title takes a source; set_chat_session_title_if_empty
  becomes set_chat_session_title_if_placeholder — one conditional UPDATE
  that lets a generated title replace the placeholder while a user
  Rename, a legacy title, and an already-generated title all stand.
- harness: defaulted Harness::generate_title, implemented for Claude
  (claude -p --model haiku --max-turns 1) and Codex (codex exec --json
  at low reasoning effort). Both children run tool-less and MCP-less,
  hermetically in a temp cwd, under a shared TITLE_TIMEOUT. Prompt and
  output sanitizer live in the new harness/title.rs so both halves are
  unit-testable; opencode and cursor keep the default.
- opencode fix: TurnCtx::set_title's if-empty check could never fire —
  the placeholder write always beat it — so native opencode titles were
  silently dropped. The provenance check revives them, and the seed
  title opencode stamps on every new session ("New session - <ts>") is
  now skipped so it can't latch and block the real one.
- chat: one emit_session helper for the re-read/resolve-busy/broadcast
  sequence shared by set_archived, set_title and auto-titling; the
  placeholder now takes the first non-empty line.

No UI changes: the existing chat.session SSE upsert re-renders Recents.

* Animate the reveal of auto-generated session titles

An auto-title used to pop into the sidebar with no acknowledgement that
anything happened. Now it materializes character by character.

- session_json gains "titleSource" so the UI can tell a harness-generated
  title from a placeholder or a user rename.
- New TitleReveal component, used by both the Recents row and the chat
  header so the effect is identical in each. Characters are aria-hidden
  behind an aria-label — a screen reader hears one title, not forty
  single-letter spans — and render as a plain string when not animating.
- The reveal plays only on a live chat.session event that changes a title
  we already had on screen, and only for titleSource "generated": renames
  and page loads swap silently. A per-session nonce remounts the spans so
  a second retitle replays rather than sitting on a finished animation.
- CSS: 240ms per character, 14ms stagger capped at 500ms, fading in from
  blurred and slightly below. No color, no scale, no layout shift, and
  @media (prefers-reduced-motion: reduce) turns it off entirely.

* Trim the skills catalog from codex title requests

The plugin/skills catalog injected ~6k prompt tokens (24.3k -> 18k input
measured) into a one-line title request that ignores it.

* Strip the agent system prompt from claude title requests

--system-prompt replaces the ~8.5k-token Claude Code scaffolding with one
sentence (measured 8.5k -> 1.9k input tokens; wall time unchanged at ~3s,
which is node boot + one API round trip).
v0.1.84
2026-07-27 18:22:18 -07:00
Daniel Kim 1b9b62328f Agent | Project scope toggle for the Experiments pane (#134)
* Agent | Project scope toggle for the Experiments pane (v0.1.83)

Attribute experiments to the chat session that created them
(local_experiments.chat_session_id, stamped from ORX_CHAT_SESSION_ID like
runs, immutable after creation) and add an Agent | Project toggle to the
experiments pane. Agent scope filters the tree to the open session's
experiments, collapsing foreign lineage regions into dashed "…" pills
with counts, and scopes the runs table to the same experiments.

* Show the Agent | Project toggle only in projects with attributed experiments

Legacy projects (created before chat_session_id existed) have no attributed
nodes, so the toggle would only ever show an empty Agent view there. Gate it
on the project having at least one attributed experiment — no backfill
needed, and pre-existing projects keep today's UI untouched.

* Require full attribution before showing the Agent | Project toggle

Any unattributed experiment (legacy or pre-feature) makes Agent scope
misrepresent the tree, so the toggle only renders when every experiment
in the project carries a chatSessionId.
2026-07-27 18:04:42 -07:00
Daniel Kim 397dd48815 Stream Codex sub-agent activity into the chat transcript (#117)
* Stream Codex sub-agent activity into the chat transcript

Sub-agents spawned via Codex collaboration now stream their own transcript
live, nested under the spawn row, instead of showing an opaque
collabAgentToolCall/subAgentActivity row.

- WirePart gains a recursive children list (serde-default, no migration)
- apply_item split into a pure item_to_part + thin streaming wrapper
- turn loop classifies foreign-turn events: parent / known sub-agent / stale,
  routing sub-agent items+deltas into the spawn part's children; the
  aborted-predecessor drop is preserved
- UI renders the nested transcript inline (recursive, auto-expands while
  running) plus a right-pane view tab

* Address review: settle orphans on all exits, re-point sendInput, share tool-group CSS, trim spawn fields

* Nits from review round 2 + version bump to 0.1.77

- drop dead .subagent-block class token
- re-point grandchild threads on sendInput (symmetry with top-level)
- fix stale doc comment on subagent_spawn_part
- rebuild ui/dist, bump Cargo version

* Fix un-collapsible running sub-agent row; add live header to view tab

- SubagentBlock: expanded is now a user-toggleable state seeded from running,
  instead of `open || running` which made a running row impossible to collapse
- SubagentTranscript: show a header (spawn label + status dot + "live" badge)
  so the right-pane tab reads as the sub-agent's own live view, not a
  duplicate of the inline block

* Stream Claude and OpenCode sub-agent activity too

Extends live sub-agent streaming (shipped for Codex) to the other two harnesses,
so a Task/task sub-agent's transcript nests under its spawn row in all three.

- Promote find_part_mut / upsert_preserving_children to chat/mod.rs and add
  TurnCtx::upsert_child / append_child_text — shared routing primitives
- Claude: route stream_event/assistant/user events carrying parent_tool_use_id
  (= the Task tool_use's part id) into that Task part's children, instead of
  dropping the deltas and leaking the blocks flat
- OpenCode: link a task-spawned child session (session.created carrying
  parentID) to its task row, and route that child session's parts/deltas into
  the row's children instead of filtering them out by sessionID
- UI: a tool part with children renders as a spawn block regardless of tool
  name (Codex subagent / Claude Task / OpenCode task)
- Tests: per-harness nesting tests; rebuilt ui/dist

* Review fixes: preserve spawn-row children on re-upsert; filter sub-agent user parts

- OpenCode final-message merge and Claude Task tool_use now use
  upsert_part_preserving_children so completing/re-emitting a spawn row doesn't
  wipe the sub-agent transcript that streamed into its children (blocker)
- OpenCode: gate sub-session parts on owned_by_assistant so a child session's
  user prompt echo doesn't nest as sub-agent output; track sub-session assistant
  message ids
- tests: assert Task/task row completes AND keeps children through the merge

* Nits: use imported HashSet; document one-level nesting scope in OpenCode

* Sub-agent row opens transcript in the right panel instead of expanding inline

Per design feedback: the inline sub-agent spawn row is now a single clickable
line (status dot + label + chevron) that opens its full transcript in the
right-side panel — matching the Claude/Codex desktop apps. No inline expansion,
no auto-expand while running (the row just pulses). Nested sub-agents in the
panel are themselves click-to-open rows.

* Size the sub-agent row like the transcript body text

Match the spawn row's font-size (--fs-base) and color (--text) to the
surrounding transcript prose, keeping the trailing chevron.

* Sub-agent row: block-level flex so long labels truncate with ellipsis

Review nit: inline-flex + flex:initial let a long label grow the row and push
the chevron instead of truncating. Use block-level flex + width:100% and keep
the base .tool-line flex:1/min-width:0 so the label ellipsizes and the chevron
pins right (matching .tool-group-summary). Also refresh a stale doc comment.
2026-07-27 17:46:42 -07:00
Daniel Kim 17633be723 Add a hover detail card to experiment tree nodes (v0.1.83) (#133)
* Add a hover detail card to experiment tree nodes (v0.1.83)

Hovering a node in the experiment tree (350ms intent delay) opens a
portal-anchored card with the full title/description, run stats
(counts, backend, duration), a lazily fetched diffstat vs the parent
branch, branch lineage, and the run command. Portaled to <body> so it
neither scales with canvas zoom nor disturbs the fixed tree layout;
dismissed on mouse-out or canvas wheel.

* Review round 1: shared diff parser, viewport-change dismissal, honest counts

- Parse the diffstat with react-diff-view's parseDiff + the Changes tab's
  countChanges/formatDiffFilePath (now exported) instead of a hand-rolled
  scanner; truncated diffs render as lower bounds.
- Replace the global wheel listener with useOnViewportChange dismissal
  (covers drag-pan, pinch, fitView) and re-measure the anchor when tree
  data changes; position via the existing useMeasure/usePopoverPosition.
- Fix duplicate failure text, 'passed'→'done' wording, reconciled run
  counts, live-run duration fallback, mono font-size leak, z-index tier,
  and reuse diff-stat-add/del classes.

* Review round 2: reliable pan/zoom dismissal, truncation-safe parsing, side fallback

- useOnViewportChange stores a single global callback, so per-node
  registrations overwrote each other; dismissal now flows from ReactFlow's
  onMoveStart through a module-level EventTarget every open card subscribes to.
- Byte-capped diffs can cut mid-hunk and crash parseDiff; trim to the last
  complete file (or line) first, try/catch as a backstop, and hide the row
  for empty diffs.
- On windows too narrow for either side placement, anchor above/below
  instead of letting the clamp sit the card on top of its own node.
- Skip rect churn on SSE ticks that don't move the node.

* Review round 3: reset diffstat on run change; label truncated file count

The diffstat effect never cleared state, so when SSE flipped latestRun
while a card was open the old run's counts lingered beside the new run's
status (and a failed refetch kept them forever). Reset at the top of the
effect. Also mark the file count as a lower bound on truncated diffs and
align the cancellation-flag name with the codebase idiom.

* Skip header-only diffstat remnant after truncation trim

* Hover card feedback: expandable description, darker text, drop file names

- Clamped descriptions get a Show more / Show less toggle; expanded text
  scrolls within the card instead of growing past the viewport.
- Title, stats and git rows read in --text instead of --muted (the card
  was mostly grey); the foot row stays muted as tertiary metadata.
- Diffstat keeps branch and +/− counts, drops the changed-file list.

* Drop the description's recessed background for hairline rules

The filled box was the only non-hairline surface on the card and read as
a heavy slab on long results; the description is now plain text between
the same hairline rules the rest of the app uses.

* Post-feedback review nits: toggle hygiene and hairline polish

Reset the expanded state when SSE swaps the description; type=button on
the toggle; trim CSS the global button reset already provides; bottom
padding on the expanded scroll body; comment accuracy.
v0.1.83
2026-07-27 17:37:48 -07:00
Daniel Kim 04cae09219 Auto-grow the chat composer with multi-line input (v0.1.82) (#132)
* Auto-grow the chat composer with multi-line input (v0.1.82)

The composer textarea was fixed at two rows (rows={2}, resize: none), so
multi-line drafts scrolled inside a tiny box instead of expanding it
(issue #130). Grow it to fit content up to the existing 180px max-height
cap, past which it scrolls internally; an empty draft clears the inline
height so the resting two-row look is unchanged (and Chrome's
placeholder-in-scrollHeight quirk can't inflate an unsettled layout). A
width ResizeObserver re-measures on pane resizes since wrapping changes
the needed height.

* Rebind the autosize observer on composer remount; review nits

The composer textarea unmounts when a settings view opens (mainView !=
chat renders a different tree); the effect-based observer kept watching
the detached node and a remounted draft came back at two rows. Replace
the effect with a stable ref callback that rebinds the ResizeObserver
and re-measures on every mount. Also: skip measuring at zero width
(hidden/mid-layout scrollHeight is nonsense), drop the circular
useCallback around autosizeComposer, and document the width-only guard,
the CSS min/max-height coupling, and the field-sizing tradeoff.

* Floor the composer at its rows=2 resting height; comment polish

A one-line draft measured ~33px and clamped to the old 42px min-height,
so the box dipped ~10px on the first keystroke and jumped back when
cleared. Raise the floor to the rows=2 resting size (52px) so empty and
one-line drafts sit at the same height. Also soften the ref-callback
comment (an effect could work with a mount flag) and note the plan-strip
textarea sharing the CSS rule.

* Replace the JS autosize with CSS field-sizing: content

Drops the whole autogrow block from ChatPanel (measure function, layout
effect, ref callback, ResizeObserver) for one CSS declaration. Firefox
doesn't ship field-sizing yet and falls back to the previous fixed
two-row box; min-height stays 52px so the resting look is unchanged, and
the plan strip's revise textarea picks up the same growth for free.
v0.1.82
2026-07-27 15:58:28 -07:00
Daniel Kim 62609c020b Don't treat single dollars as inline math in chat/report markdown (v0.1.81) (#131)
Prose like "cost $8–18 across both nodes ($1.45 each)" was pairing its
dollar signs into a KaTeX inline-math region, italicizing the span and
collapsing spaces. remark-math's default singleDollarTextMath treats any
$...$ as math; disable it and normalize agent-emitted \(...\) to $$...$$
so real math keeps rendering.
2026-07-27 13:05:04 -07:00
Daniel Kim 30e2ab18f4 Make reasoning/effort choices model-aware (#123) (#127)
* Make reasoning/effort choices model-aware (#123)

The composer exposed one static reasoning list per harness, which hid valid
choices and silently overrode the CLI's own configured effort.

Reasoning choices now ride on each ModelInfo, with the harness-wide list kept
only as the fallback for a session with no model selected:

* Codex — per-model tiers from a curated table (codex exposes no queryable
  effort metadata), kept in the same table as the model ids so a model can't be
  added without stating its tiers. Verified live against codex-cli 0.144: all
  three 5.6 models accept `ultra`, and 5.5 rejects `max` with a 400 — issue
  #123's table claims Luna stops at `max`, which did not reproduce. This matters
  because codex forwards the value unvalidated, so a wrong tier kills the turn.
* OpenCode — real per-model `variants` parsed from `opencode models --verbose`,
  sent as a top-level `variant`. A model with an empty variants map hides the
  picker entirely, which is why absent and empty are distinct states. Unknown
  variants are forwarded rather than filtered: the catalog is the authority and
  already ships ids outside our vocabulary (`thinking` on minimax-m3).
* Claude Code — `ultracode` gated on CLI >= 2.1.217. The installed 2.1.197 does
  not error on an unknown effort; it warns and silently runs at the default, so
  advertising it unconditionally would offer a choice that does nothing.

Every list leads with a `default` sentinel that sends no override at all, and
it is the default selection — so a configured `model_reasoning_effort = "max"`
is no longer clobbered by an implicit per-turn `high`. Switching models
reconciles a stored effort the new model doesn't support back to `default`,
both in the composer and again in each harness mapper before it reaches a CLI.

Deliberately no store migration for existing rows: every value older builds
wrote is still a value the picker offers, so a blanket reset would be
indistinguishable from a level the user just chose — and Store::open() is a
per-request constructor, so it would re-run and clobber fresh picks.

* Lower the ultracode gate to 2.1.203, the measured boundary

The constant was 2.1.217 — the version issue #123 happened to report — with a
comment admitting the true cutoff was somewhere in (197, 217] and that lowering
it needed a version actually tested. It has now been tested: every published
release in 2.1.197..=2.1.212 was installed in isolation and probed.

2.1.198-2.1.202 reject (warn on stderr, run at default effort); 2.1.203 and
every version above accept. So a 2.1.212 install was being denied a choice its
CLI supports.

The acceptance is specific to this value rather than newer CLIs dropping the
warning: on 2.1.206, `ultra`, `ultrathink`, and `ultracode123` all still warn
while `ultracode` does not. `--help` lists only the five ordinary tiers on every
version tested, so it can't be used to detect support.

The tests now pin the 2.1.202/2.1.203 pair, so moving the constant without
re-probing fails loudly.

* Discover the Codex catalog from the CLI instead of curating it

The model list and per-model effort tiers were hard-coded. That was wrong on
the facts, not just the principle: the installed codex actually serves seven
models (gpt-5.4, gpt-5.4-mini, gpt-5.3-codex-spark were missing), and the
curated Luna entry advertised `ultra`, which the catalog does not offer —
codex's own picker doesn't show it, so ours shouldn't either. (An earlier live
`codex exec` turn on Luna tolerated `ultra`, which is what the table was built
from; the catalog is authoritative for what to offer.)

Detection now spawns `codex app-server`, does the initialize handshake, and
calls `model/list` — the same data codex's TUI picker renders, with each
model's `supportedReasoningEfforts`. This is the codex analogue of the
opencode `models --verbose` path. The static table survives only as the
fallback for a codex too old to answer (legacy exec era), with Luna corrected
to the catalog's tiers.

Send-time validation follows the opencode precedent for catalog-discovered
models: a model outside the frozen fallback table forwards its level rather
than clamping to the conservative intersection, because the composer only
offered what `model/list` reported — clamping would drop genuinely supported
tiers. A bad value still surfaces as codex's 400 in the chat.

Claude stays gated rather than discovered, now with evidence there is nothing
to query: no models command, a static `--help`, and the "Valid values" warning
on 2.1.212 still omits `ultracode` even though that version accepts it — the
CLI's only enumeration surface is inconsistent with its own parser.

* Discover the Claude catalog and ultracode support from the CLI

Claude Code was the last harness with hard-coded models and effort tiers. It
turns out the CLI can be asked for both, via two surfaces:

* `list_models` — a control request over `--print` stream-json (the channel
  run_turn already speaks for set_model), returning the same catalog the CLI's
  own /model menu renders: each model's alias, and its per-model
  `supportedEffortLevels`. This immediately corrected the static list a third
  time: Haiku supports no effort control at all, yet the table had been
  offering all five tiers on it. Its picker now hides, like a variant-less
  opencode model.

* `--effort ultracode --version` — the CLI's own argument parser as a
  capability probe (~0.2s, no network). `--version` still runs the parser,
  which warns `Unknown --effort value` for a value it doesn't know; absence of
  the warning is acceptance. This replaces the bisected version constant
  entirely: the 2.1.202/2.1.203 boundary is now discovered per install rather
  than pinned. The parser is the only truthful surface — --help, the warning's
  own "Valid values" list, and list_models all omit ultracode on versions that
  accept it, because the CLI models it as a session mode, not an effort level.
  It is appended to models that reach xhigh (the mode is documented as xhigh +
  dynamic workflows), only when the probe passes.

claude_effort now forwards any non-sentinel value, matching the catalog policy
in codex_reasoning and opencode_variant: the composer only offers what the CLI
reported, an allowlist would drop a future catalog tier, and Claude only
warns-and-defaults on an unknown value rather than failing the turn.

The static model/effort tables survive only as the fallback for a CLI too old
to answer list_models, mirroring the codex and opencode fallbacks.

* Carry the catalogs' display names and blurbs into the picker

The Claude aliases the CLI's picker submits (`opus[1m]`, `sonnet`) are
unversioned, so the model menu couldn't say which Opus or Sonnet you'd get.
The catalogs already answer this: list_models has displayName + description
(the description is where the resolved version lives — "Opus 4.8 with 1M
context · …"), codex model/list has the same pair, and opencode's verbose
output has name. ModelInfo now carries display_name/description, and the
picker renders the catalog's own strings — the same text each CLI's native
picker shows — with the raw id demoted to a hover title. Statically-listed
fallback models keep deriving a label from the id.

* Lead with versioned names, drop the Default row, allow free-form model ids

Three picker fixes from using it:

* The Claude rows read "Fable / Fable 5 · Most capable…" — the name repeated
  because the catalog's displayName is unversioned and the version lives in
  the description's first `·` segment. That segment is now promoted to the
  display name ("Fable 5") with the rest as the blurb, so rows lead with the
  resolved version, exactly once.

* The per-harness "Default model · CLI configuration" row is gone wherever the
  CLI advertises a catalog — it was redundant noise next to a discovered list
  that already leads with the CLI's default. It survives only for custom
  providers, which advertise no models and where "let the CLI decide" is the
  only sensible row. New selections seed the catalog's first model instead of
  null.

* Typing an id the catalog doesn't list offers "Use "<id>"". The catalogs are
  curated menus, not the set of accepted ids — `--model claude-opus-5` works
  today on a CLI whose menu still resolves `opus` to 4.8 — so the menu can't
  be the ceiling. A custom id gets no per-model effort list (unknowable), so
  the reasoning picker falls back to the conservative harness-wide tiers.

* Preselect the effort that actually runs instead of a Default sentinel

The reasoning menu led every list with "Default · CLI configuration" — a
pseudo-value that told the user nothing about what would run. Replace it with
the real answer wherever the CLI can give one:

* Codex reports each model's defaultReasoningEffort, and config.toml's
  model_reasoning_effort outranks it (codex's own resolution order — verified:
  this machine's config sets "high" and the picker now preselects High on
  every model, not the catalog's low/medium). The sentinel row is gone for
  catalog models; the resolved tier is preselected and marked "· Default".
  Sending it explicitly is equivalent to sending nothing, by construction.

* Claude reports no default tier because its unset default isn't one: the
  catalog advertises supportsAdaptiveThinking, and with no --effort the CLI
  scales effort per request. Preselecting a fixed tier would pin behavior the
  user never chose, so the sentinel stays — now labeled "Adaptive", which is
  what actually runs, instead of "Default".

* OpenCode reports no per-model default, so its sentinel row stays as is.

reasoningFor resolves in that order: the model's reported concrete default,
then the sentinel when offered, then the harness-wide default. Stale stored
"default" sentinels on codex sessions reconcile to the resolved tier.

* Render a concrete default tier inline instead of pinning it

Pinning above a separator now applies only to the `default` sentinel, which
isn't a point on the tier ramp (Claude's Adaptive, opencode's model default).
A concrete default — codex's resolved tier, a permission mode — stays in its
natural ramp position with just the "· Default" marker, so the codex menu
reads Low → Ultra in order. Side effect on the shared mode menu: Auto renders
inline with the marker and the 1/2/3 number hints cover the full list again.

* Make every picker change update the sticky new-session preference

The mode/model/reasoning selection saved for new sessions only updated when
the pickers were changed with no session open. A change made inside a session
landed as that session's unsent tweak and the global preference never learned
about it — so configuring auto/sonnet/high mid-session and clicking New
session showed the old config.

selectModel now always writes the merged selection to the sticky preference
(state + localStorage), and additionally layers it onto the open session's
override as before. New session therefore opens with whatever was chosen
last, wherever it was chosen — verified across a full page reload.
v0.1.81
2026-07-27 13:00:34 -07:00
Daniel Kim 85c1f7852b Fix duplicated assistant text for thinking models in the Claude harness (v0.1.80) (#129)
* Fix duplicated assistant text for thinking models in the Claude harness

The Claude CLI emits one 'assistant' event per completed content block,
each with a single-element content array. The fold keyed parts as
{mid}-{enumerate index}, so every block keyed to {mid}-0: a text block
following a thinking block clobbered the reasoning part while its own
delta-built part at {mid}-1 survived — every reply rendered twice for
models with visible thinking (Fable 5), and the thinking disappeared.

Track a running per-message block offset so each assistant event's
blocks recover their true index in the message and land on the same
part ids the stream deltas built. A full-content array in a single
event (the shape the old tests assumed) starts at offset 0 and behaves
as before.

* Address review round 1: subagent offset namespace, comments, tests

- Namespace the block-offset map by parent_tool_use_id so a subagent's
  top-level assistant events can never advance the main message's offset
  (their parts still fold — pre-existing behavior, unchanged).
- Document the exactly-once/ascending invariant the counter trusts, and
  why the offset advances by blocks.len() rather than a render count.
- Tests: pin part ids in the per-block regression test; add coverage for
  bridge-suppressed blocks still occupying their position and for
  subagent offset isolation; rename the whole-message test and the
  per-message-id keying test to match what they exercise.
- Nits: hoist HashMap use, unshadow the loop index, avoid the
  unconditional format! allocation in the tool_use id fallback.

* Address review round 2 nits: single parent read, tighter docs, sharper tests

- Bind parent_tool_use_id once and use it for both the offset namespace
  and the usage guard (one read, one null-semantics).
- Trim the field doc to the what + pointer; the assistant arm stays
  canonical for the mechanism; restore a short named pointer from the
  stream_event comment.
- Make the subagent test reuse the main message id so it actually
  exercises the namespace branch; name the cross-test reference; drop
  the CLI version stamp; assert last_text ordering in the per-mid test.

* Bump version to 0.1.80
v0.1.80
2026-07-27 12:12:48 -07:00
Daniel Kim 07a7e209ff Detect Codex and Claude Code behind custom API endpoints (#125)
Follow the active provider in $CODEX_HOME/config.toml so a provider with requires_openai_auth = false authenticates off its declared env_key, and offer the model it configures instead of the first-party catalog. Accept ANTHROPIC_AUTH_TOKEN for Claude Code, letting the env credential win over a stale ~/.claude.json only when ANTHROPIC_BASE_URL is set. Add a "Default model" picker entry that sends model: null so the CLI uses its own configured default.

Closes #121
2026-07-26 22:13:09 -07:00
Daniel Kim 979d8e9362 Rework first-run onboarding and the New project modal (v0.1.79) (#126)
* Tighten the onboarding + new-project flows

Onboarding:
- Gate steps 1 and 2 (a signed-in agent, GitHub access) — both are hard
  requirements for chat and project creation, so Continue is disabled
  until they're met.
- Drop the data-dir step: the default suits ~everyone, it's write-once,
  and Settings > Storage already exposes it with a move flow.
- Split harness notes into not-installed vs not-signed-in, each with the
  real command (claude auth login / codex login / opencode auth login).
- Fix OpenCode reporting Connected while unauthenticated (agent_ready
  ignored authentication).

New project modal:
- Auto-open on the onboarding hand-off.
- Only show the fork choice when the user can actually push (new
  /api/github/repo-access), and say what happens when they can't.
- Name the real GitHub account in repo previews (new /api/github/account).
- Drop the branch picker; resolve the repo's real default branch server
  side instead of assuming "main".
- Describe the copy honestly: it's a single-branch, history-less snapshot,
  not a GitHub fork.
- Top-anchor the modal so the tab row doesn't move between tabs.

* Review round 1: fix onboarding lockouts, trapped name field, stale fork mode

- opencode: treat a non-empty `opencode models` list as authenticated, so
  env-key installs (no auth.json) aren't locked out by the new step-1 gate.
  Claude and Codex already had the equivalent fallback.
- Onboarding: surface a load error. A rejected probe left harnesses/git at
  null forever, which the gates read as not-ready — disabling Continue with
  no hint and no way out, on the screen that is the whole app at first boot.
- NewProjectForm: hoist the project-name field out of repoFields. A stalled
  access check used to unmount it, and `valid` needs a name, so the form
  became unsubmittable with no escape.
- NewProjectForm: reset canPush on every repo change and sync repoMode to
  the answer, so a forced "fork" can't linger and silently copy a repo the
  user can push to.
- github.rs: share one authed_get helper (with a 10s timeout — there was
  none), URL-encode owner/repo, drop the now-dead has_push_access wrapper.
- Drop the unread `known`/`defaultBranch` fields from the repo-access
  response and its TS type.
- ProjectsHome: consume the post-onboarding hand-off so a later remount
  can't re-open the modal.
- Deduplicate the paper-repo hint and the two identical code-chip CSS rules;
  tie the modal offset and max-height together with a --modal-top token.

* Review round 2: correct the opencode fallback, unblock SSH-only users

The round-1 opencode fix rested on a false premise — I claimed `opencode
models` only lists authenticated providers. Tested: under a signed-out HOME
it returns nothing, so the fallback never fired for the users it was meant to
rescue, and had the catalog ever been listed it would have marked signed-out
installs Connected. Replaced with an explicit provider env-key check, matching
claude.rs. Verified both directions: env key set -> ready, none -> gated.

- Onboarding: step 2 gets a "Skip — I use SSH keys" escape. ensure_clone
  tries ssh first, so a token isn't actually required for the use-existing
  path, and git_settings_json can't see SSH keys at all — the hard gate was
  locking out a setup the CLI's own startup note calls supported. Also covers
  the git-not-installed dead end, where the token form is unreachable.
- Onboarding: split the shared loadError into per-probe flags. A telemetry
  failure was putting a connectivity error on unrelated gates and hiding the
  actionable hint behind it.
- NewProjectForm: only force "fork" on denied access; the symmetric sync was
  undoing selectPaper's deliberate copy default for writable paper repos.
  Reset repoMode in onRepoChange instead, which is where the repo changes.
- github.rs: drop the deleted has_push_access's doc comment, left welded onto
  authed_get and describing the wrong thing.
- Bump to v0.1.79 (+ Cargo.lock): ui/dist changes don't ship without it.

* Review round 3: detect synced-env keys, unstick the branch fallback

- Harness detection: resolve API keys through orx's synced env file as well
  as the process env (new detect::api_key, used by claude.rs and opencode.rs).
  A key pasted into Settings > Environment is handed to the harness child, so
  chat works — but detect() couldn't see it, and step 1 is a hard gate.
  Verified: key only in ~/.openresearch/env now reports ready for both.
- opencode: widen the provider key list (Gemini/Groq/xAI/DeepSeek). Measured
  rather than assumed that `opencode models` can't stand in for a credential:
  signed out with no keys it still lists 7 free/bundled models, so keying off
  it would report signed-out installs as Connected.
- baseline_branch: fall back to `git ls-remote --symref` when the GitHub API
  can't answer. Without a token the branch resolved to a hardcoded "main" and
  the clone died on any master/dev repo — and the form no longer has a branch
  field to recover with. git's own ssh/https credentials need no token.
- Step 1 no longer gates on a failed probe: detection fans out to per-binary
  version/model commands that can stall, and there's no way out of onboarding.
- Step 2's skip is always offered and reworded "Skip for now": a stale gh
  token still reads as connected, hiding the escape from the users who need
  it most. Promoted to a real button rather than a muted link.

* Rebuild ui/dist after rebase

* Review round 4: stop 404 forcing a history-losing copy

- repo_meta: a 404 means "invisible to this token", which is also how a
  private repo looks to an SSH-only user who can push to it fine. It was
  reported as can_push=false, which force-forks — and this branch removed the
  "Use this repo" button in that state, so the override was gone too. The
  user would silently get a new repo with their history dropped. Report
  unknown instead, matching the fallback used everywhere else.
- remote_default_branch: bound it with a 15s timeout. ls-remote tries ssh then
  https, and a black-holed host blocks on TCP connect ~75s each — the exact
  hazard authed_get got a timeout for last round, reintroduced on the git path
  of the same request. A stall now degrades to "main" instead of hanging.
- git.rs: the new function had been inserted between ensure_clone's doc
  comment and ensure_clone, so the comment described the wrong function and
  ensure_clone was left undocumented.
- Never show a "Detecting…" spinner next to a fetch error: on a failed probe
  both steps now render the retry copy in place of the card, instead of a
  spinner that claims work is in progress beside an enabled Continue.
- onRepoChange no longer resets repoMode in paper mode, where editing the
  auto-filled repo would quietly retarget pushes to the upstream paper repo.

* Review round 5: make step 2 an honest soft gate, fix stale-state contradictions

- Step 2 no longer pretends to gate. Cloning and pushing work over SSH keys,
  so a disabled Continue beside an always-enabled "Skip for now" was two
  buttons doing the same thing — one greyed out, which reads as a bug. One
  Continue, always enabled, and the hint states what's missing instead of
  blocking on it.
- A rejected probe now clears its state as well as setting the error flag.
  Otherwise a failed *re-check* kept the old cards on screen: stale "Not
  signed in" badges and a "sign in to continue" hint next to a Continue that
  harnessError had just un-gated, with no indication the check had failed.
- Mode switches reset repoMode via a chooseMode helper. Leaving paper mode
  carries the paper's repo into the field but not its copy default — and
  because the field is pre-filled, onRepoChange never fired to reset it, so
  "Private copy" stayed selected on a repo the user never chose it for.
- Deduplicate the project-name field and blank-repo hint, both of which were
  inlined a third time in "New blank repo" (flagged since round 2).

* Review round 6: restore the Existing repo name field, kill the footgun

Round 5's nameField refactor turned it into a function but updated only two of
three call sites. `{parsed && nameField}` passes the function itself as a
React child, which renders nothing — so "Existing repo" had no Project name
input and Create stayed disabled. Both tsc and a grep of the call sites missed
it; three reviewers caught it.

Made the class of bug impossible rather than just fixing the call site: the
autoFocus parameter was the only reason nameField was a function, so it's a
plain element again like blankRepoHint. Verified in a browser this time —
name field present and submit enabled in every mode.

- Onboarding: the docstring still called step 2 a hard gate, which round 5
  made false, and it sat below the RETRY_COPY const so it documented the wrong
  symbol. Rewritten and moved.
- Onboarding: sequence-guard load(). A token save landing mid-refresh was
  overwritten by the pre-save snapshot, leaving the card reading "Not
  connected" until the next check; the save now supersedes in-flight probes.

* Review round 7: fix a Re-check lockout, share renderNote, trim comments

- load()'s .finally was sequence-guarded, so a token save (which bumps loadSeq
  to supersede the in-flight probe) left `checking` true forever and both
  Re-check buttons dead for the session. `checking` is "a load is running",
  not data a stale response can corrupt — it shouldn't be guarded. Introduced
  by round 6's guard; verified fixed by superseding a slow load in a browser.
- renderNote moved to its own module: ModelPicker, a core chat control, was
  importing it from the first-run walkthrough.
- Drop the redundant {parsed && …} guard (repoFields already returns null),
  a doubled CSS comment block, and two stacked rationale comments.
- Document the opencode agent_ready tightening where the behaviour changes:
  installs with no auth.json and no provider key now read "Not signed in".
v0.1.79
2026-07-26 21:48:33 -07:00
Daniel Kim c09ccb6808 Repair a provisional experiment instead of branching a child (#124)
* Repair a provisional experiment instead of branching a child

An agent that hit a dependency error on its root experiment created a
child node to fix it, then another for the next error — a 14-node chain of
pure engineering remediation with no scientific content. The cause was a
missing affordance, not a missing rule: rule 1 forbade editing a baseline,
the loop offered only refill/promote/stop, and `orx create-experiment` was
the only exit from a failed state.

A node is now an evidence contract, not a code revision. It stays
provisional — editable on its own branch — until one of its runs puts the
intended measurement in the log, and only then freezes. Getting code to
execute is setup, and setup happens on the node.

- SYSTEM_PROMPT.md (+5 net lines): rule 1 freezes on measurement and covers
  every node, not just the baseline (the chain reproduced at depth 3 as
  easily as at the root); rule 6 inherits that freeze test instead of
  restating "completed non-failed run", a trigger a `done`-but-diagnostic
  run satisfied with zero evidence; the loop gains a fourth move, Repair;
  the 2-strike env rule becomes the named repair cap and closes the dodge
  of resetting it by creating a node.
- orx-experiment-tree (both variants): the freeze test, "remediation is
  neither width nor depth", and a classification table.
- orx-git: repair-in-place workflow, worktree branch-ownership handling,
  and a safe sync recipe — `checkout -B <b> origin/<b>` silently discards
  unpushed commits, which a repair flow hits when a push has failed.
- orx-compute (both) + k8s: provisioning failures never create nodes.
- orx-create and root SKILL.md: drop "frozen the moment it holds real
  code", which contradicted the new model.

Also fixes `orx skill <name>` serving cloud bodies inside local sessions.
The playbook points there as the fallback, but `find` searched the Full set
only, so a local agent was told to use `orx artifact`/`query`/`chart` —
commands local mode does not have. It now resolves against the set implied
by ORX_CHAT_SESSION_ID.

* Review round 1: close repair loopholes, fix the -B hazard

Four independent reviewers found 15 blockers. The two that mattered most
were mine:

- The old `checkout -B <b> origin/<b>` code fences survived in orx-git and
  orx-create while a new bullet warned against that exact command eight
  lines below. An agent copies the fence, not the prose — so the hazard was
  documented, not fixed. All fences now use `checkout` + `merge --ff-only`,
  which fails loudly instead of discarding unpushed commits.
- The "safe form" I had added (`checkout X 2>/dev/null || checkout -b X`)
  was itself wrong: plain `checkout` already DWIM-creates the tracking
  branch, so the fallback only fired on the worktree-lock error, where the
  redirect masked "already checked out at ..." as "already exists" and made
  the triage block unreachable.

Also:
- orx-git:32 still said "Edit children, never the baseline" — the sentence
  this change exists to remove, in the skill an agent loads to repair.
- The cap read "two consecutive ... the same error twice", so a sequence of
  *distinct* errors (the actual 14-node case) never tripped it. It now says
  different errors still count, and that flavor/provider/backend switches
  are repairs — only a measured run resets the count.
- Freezing's permanence lived only in the on-demand skill; it is now in both
  always-on rule 1 copies, where the anti-tampering defence belongs.
- Dropped "a wrong flag" from the Repair list: it licensed in-place knob
  edits against cardinal rules 2 and 3.
- k8s said a GPU-type change is "a real experimental variable" while the
  tree table called flavor changes "provisioning, not science". Reconciled
  on the freeze boundary.
- Partial measurement (3 of 5 metrics) was undefined; any real measurement
  now freezes, and the node's expected metrics belong in `orx exp desc`.
- Harness shims still enumerated the three-move loop.

Rust: module doc described the old always-Full lookup; the variant test
pinned an English sentence from a file this branch doesn't touch (replaced
with a dispatch check over all four swapping skills); the mode inference
moved to `chat::in_local_session()` next to the env contract it reads, with
a direct test — it was the crux of the fix and untested.

* Review round 2: make the freeze test evidential, not causal

Round 2's four reviewers found 10 blockers. The most serious was a
regression my round-1 compression introduced.

The classification row read "anything that stops the code MEASURING at all
-> Repair". That is a causal test where an evidential one was needed: a run
that diverges to NaN, OOMs on the node whose hypothesis IS "wider MLP", or
times out on the slower config never reaches its eval block — so an agent
would repair the branch and silently delete a real negative result. That is
worse than the bug this branch fixes, because a wrong repair destroys
evidence irrecoverably while a wrong child costs one cheap node.

The freeze test now asks whether the failure came from the node's own
change, with a mechanical discriminator: would this have happened on the
parent too? If no, it is a result — freeze it.

Two reviewers disagreed on partial metrics (3 of 5 printed): one wanted
FROZEN kept, one wanted provisional because it contradicted the definition
above it. Resolved on the same asymmetry — evidence-valid now reads "any of
the numbers this node owes", so partial output freezes and the definition
carries it instead of a patch line below the table.

Also:
- `merge --ff-only` had been added only to orx-git; the orx-create seed
  recipe and both experiment-tree child recipes kept a bare `checkout`,
  which silently resolves to a stale local branch on a reused clone — and
  the seed recipe then rm -rf's the tree and pushes from it. Fixed at all
  four sites. Flagged independently by three of the four reviewers.
- src/local/skills.rs:117, the live `/reproduce` slash template, still said
  "never mutate an experiment root after it has been run" — the old model,
  in a Rust string my markdown-only grep gate never covered. The gate now
  spans src/ too.
- Both orx-compute bodies had zero mentions of frozen/provisional, so an
  agent loading them after a failure got an unconditional repair with the
  freeze concept out of scope.
- orx-compute-k8s cited "the repair cap" without ever stating its value.
- The cap lacked "on the same node" in compute and create, reading as a
  global counter.
- Root SKILL.md had neither the four moves nor the cap.
- in_local_session's doc asserted "only local projects can own a chat
  session" as fact; there is no FK enforcing it and it is not the operative
  reason. Re-grounded on the actual mechanism.
- The variant test's ptr::eq loop was redundant with the assert_ne above
  it; it now pins each body against the embedded const, outside the lookup
  under test.

* Review round 3: stop classifying OOM both ways

Round 3's four reviewers found 17 blockers, converging hard on one: my
round-2 freeze test named "OOM on the bigger model" as the canonical FROZEN
case, while six other sites still listed a bare "OOM" as a repair trigger —
including the Repair bullet 190 lines below it in the same file, the
always-on prompt, and both compute bodies. The compute skill is the one an
agent loads when a job OOMs, and it said "OOM or timeout -> bigger flavor,
same node". So a node whose whole hypothesis was "the bigger model" would
have its result repaired away by weight of repetition. Every bare OOM is now
qualified by whether the node's own change caused it.

Round 2 also overshot on partial output. "Any of the numbers this node owes"
froze a node that printed `step 0 | loss 11.83` and then died on a missing
path — and since a child whose purpose is "finally find the checkpoint" is
explicitly not a node, the agent was deadlocked: forbidden to repair,
forbidden to branch. Evidence-valid is now floored on the metric the node's
`orx exp desc` names; a partial trace is not the measurement.

The counterfactual ("would this have happened on the parent too?") was also
undecidable as written — a claim about a run that never happened, with the
burden falling on proving the parent would have failed, which pushes
uncertainty toward Repair. It now points at the parent's actual run log and
states the tie-break explicitly: uncertainty freezes, because freezing
wrongly costs one node while repairing wrongly destroys a measurement.

Also:
- SYSTEM_PROMPT rule 1 and the freeze table both said an all-failed node is
  provisional, unconditionally — contradicting the freeze test two lines
  later. The always-on copy matters most: an agent that never loads the
  skill only ever saw the permissive rule.
- orx-git:37 still said "Always fetch + reset before editing", the exact
  idiom the -B removal was meant to eliminate, 19 lines above a bullet
  forbidding it.
- The repair recipe itself — the flow explicitly about returning to a branch
  you already left — was the one checkout site still missing
  `merge --ff-only`. I had claimed all four were fixed; this was the fourth.
- legacy_root_warning used "frozen" for branch immutability, now a term of
  art meaning "has produced evidence". Reworded to "immutable".

* Review round 4: a typo is not a hypothesis

Round 4's four reviewers found 17 blockers. The important one is a hole in
the model itself, not a stray sentence.

An agent's own coding bug had no classification. A NameError or a shape
mismatch in the hypothesis edit is literally "caused by the change this node
makes", so round 3's causal clause froze it — and the child inherits the same
typo, and the same reasoning freezes the child. That is the 14-node chain
reintroduced under a different label, and the "freezing wrongly costs one
extra node" accounting was wrong for exactly this case: the cost is a chain.
The freeze test now separates the change's semantics from its spelling —
freeze only when a *correct* implementation of the node's idea would have
failed the same way.

The freeze test also rested on a field nothing produced. Evidence-valid is
floored on "the metric its `orx exp desc` names", but every instruction and
all four worked examples told the agent to write only the *change*, and
orx-create called the description optional. Step 3, the examples, and
orx-create now carry the measurement.

Also:
- `cancelled` was classified two ways: a run cancelled after the metric
  landed matched both the provisional row and the frozen row. Keyed on
  content now, not status.
- A run with no usable log at all (spin-up failure, preemption, empty
  output) fell through "uncertainty freezes" and froze, contradicting
  orx-compute's transient-retry guidance. It is now explicitly not a result.
- The tie-break told the agent to read the parent's run — but a root has no
  parent, so a baseline that OOMs on its first launch (which orx-create says
  to *expect* and repair) would have frozen permanently.
- A seventh unqualified OOM survived round 3's sweep, in orx-compute's
  sizing section: "OOM or hopelessly-slow -> move up a tier. That's expected,
  not a mistake" — an unconditional imperative to do the thing the freeze
  test forbids, 35 lines below the qualified bullet, in the file an agent
  loads when a job OOMs.
- Root SKILL.md rule 1 never got the "own change" qualifier round 3 added to
  SYSTEM_PROMPT.md rule 1 — the same defect in the same slot on the cloud
  side.
- skills.rs:113 still said "Immutable `orx/*` branches hold experiment
  nodes", contradicting line 117 four lines below it.
- `--baseline` was an unguarded cap-evasion route; the cap now says a second
  baseline is a repair wearing a root's hat.
- The cap terminated in "ask the user" with no unattended fallback. The
  agent now records the diagnosis and carries on with other nodes.

* Review round 5: bound the counterfactual to the diff, not the idea

Round 5's four reviewers found 17 blockers. The decisive one: my round-4
defect carve-out reopened round 3's bug through a new door.

The carve-out asked whether "a correct implementation of the same idea"
would have failed. But an idea is a phrase the agent itself wrote, so the
agent controls the abstraction level it reasons against — and for any
resource failure the natural reading favours repair: "a correct
implementation of 'wider MLP' wouldn't have OOMed, it would use gradient
checkpointing." Rounds 1-3 built an evidential wall; round 4 punched a
narrative door through it, and one narrative hatch outweighs four evidential
ones because an agent that wants to keep working needs only one.

The counterfactual is now over **the diff actually written**, not the idea:
ask "is this diff malformed?", never "could a better version have avoided
it?" Resource and dynamics failures — OOM, timeout, NaN, divergence — are
explicitly never defects. A kwarg the callee *rejects* is a typo; a kwarg
value you regret is the hypothesis. A shape mismatch inside the edited lines
is a typo; one that collides with code you didn't touch is the finding that
the idea doesn't fit.

Also:
- The carve-out lived only in the on-demand skill, so it was off by default
  for any agent that hadn't loaded it. Added to the always-on Repair move.
- SYSTEM_PROMPT step 1 (the branch step the agent takes most often) created
  nodes with no --description at all — producing exactly the un-judgeable
  node the freeze test can't classify. Round 4 fixed the skills and missed
  the prompt.
- No fallback existed for a node created without a named metric; now infer
  from title and siblings, write it, and freeze if it still can't be pinned.
- orx-create's seed recipe had no `set -e`: after this branch replaced the
  force-creating `checkout -B` with a fallible `checkout` + `merge --ff-only`,
  a failed sync fell through to `rm -rf` + `cp -a` + push on whatever branch
  the clone was on — usually the project's README/notebook surface. The
  hardening introduced the hazard; it is now guarded.
- k8s never classified a provisional-phase GPU-type or node-count change.

Cuts (quality review): the permanence paragraph stated one rule four times;
orx-compute cross-referenced the freeze test twice in four lines; root
SKILL.md inlined the whole taxonomy it tells the reader to go read; the
always-turn cap paragraph and rule 1 carried gloss the skills already own.
Retired `execution-invalid` (coined once, never reused) and reworded
"nothing ran" for truncated output, which is factually wrong.

The env test now restores via a Drop guard, matching telemetry's EnvGuard,
so a failing assert can't leak the mutated var into the test binary.

* Review round 6: judge the evidence, not the cause

Round 6's four reviewers found 15 blockers, and one of them named the reason
this section has been rewritten in every single round: the test was
adjudicating **causation**. "Did this node's own change cause the failure?"
is not answerable from a log, so each round swung between too-permissive and
too-restrictive, and each swing needed a new qualifier to patch the last.

Round 5's swing was too restrictive. Bounding defects to a closed list of
four syntactic symptoms left every *semantic* self-inflicted defect
unclassified — an added import needing an incompatible torch, a config key
the edit references but never adds, a wrong path inside the edited code.
Those fall through "uncertainty freezes" and freeze; a child to fix them is
"not a node"; the agent is trapped. That is round 4's deadlock re-entering
through round 5's door. Meanwhile "resource and dynamics failures are never
defects" was written as an unconditional universal, so a node that changed
only an LR and OOMed on a pre-existing leak also froze — contradicting
orx-compute's own instruction to retry exactly that.

The freeze test now asks what the log **holds**, not what caused it:

- numbers (the measurement, or a NaN/OOM/timeout of what this node changed)
  -> FROZEN, that is the answer
- an error and no numbers -> provisional, repair in place
- nothing to judge -> provisional, repair or relaunch

Causes are arguable; evidence is on disk. This collapses six contradictory
sites into one rule and kills the "a correct implementation wouldn't have
OOMed" argument structurally rather than by prohibition — the question is
what the log contains, so there is nothing to argue about.

Also fixed this round:
- The cap reset on every sibling, so one setup bug could burn 2N runs across
  a round without ever asking. Added a project-wide backstop.
- A bare relaunch didn't obviously count toward the cap, leaving an
  unbounded retry loop on infrastructure flakiness.
- "A mis-sized flavor" sat unqualified one row above the OOM row — a
  standing licence to reclassify any OOM as a sizing problem.
- The round-5 warning I added to the seed recipe said "the lines below wipe
  the tree"; they are above it. Folded into the `set -e` comment where it
  travels with the code.
- Root SKILL.md had lost its freeze-side clause in round 5's cuts, leaving
  that document with a repair trigger and no freeze rule.
- The "freezing wrongly costs one extra node" accounting was still wrong
  (the cost is a chain when a child inherits the defect) and had become
  load-bearing again.
- Deduped the classify table against the freeze table; trimmed k8s paste.

* Review round 7: delete the causal predicate, don't reword it

Round 7's four reviewers found 17 blockers and one of them landed the
diagnosis that matters: the oscillation was not resolved by round 6, it was
relocated. The freeze test's numbers bucket still read "a quantitative
failure of *the thing this node changed*" — so an OOM landed in FROZEN or
Repair on exactly the causal judgement round 6 declared unanswerable, and
the compute skills' complementary clause ("OOM the node's own change didn't
cause -> bigger flavor, same node") made the pair a coin flip. Round 6 said
the argument was unavailable, then handed it back two lines up.

The causal predicate is now deleted rather than reworded, at all six sites.
Numbers freeze a node **whatever ended the run**; the only question is
whether the run got far enough to emit them. "Numbers" is defined as the
run's own measurement stream — byte counts inside an allocator error are not
numbers, digits in an error string are part of the error. That was the other
half of the problem: every CUDA OOM message carries a watermark, so on a
literal reading every OOM froze.

Two escape hatches this opened, both closed:
- The cap creates a standing incentive to declare "the OOM is numbers" to
  escape a hard debug and avoid asking the user. Named explicitly: if that
  reading only became attractive after a repair, it is the wrong reading.
- "Not the measurement I owed" let an agent repair a node that produced real
  metrics. Numbers that aren't the ones you named still freeze it; a missing
  eval block is a reporting defect, fixed on a child.

Also:
- orx-evidence — the skill whose entire job is judging a finished run, loaded
  at exactly the repair-vs-child decision point — had no mention of the rule,
  not even a cross-reference. Added to both variants.
- orx-create's copy-paste examples omitted --description entirely, so the doc
  that most often creates nodes created unjudgeable ones; its own rule sat 25
  lines below the code an agent copies.
- The "nothing to judge" class existed in one skill pair only. A preempted
  run had no documented classification for an agent on the always-on docs.
  Added to SYSTEM_PROMPT and root SKILL.md.
- SYSTEM_PROMPT step 0 (baseline creation) named no metric and implied the
  first launch succeeds — the local-mode gap, since orx-create is Full-only.
- Dropped the freeze table: after round 6 it restated the prose buckets
  almost verbatim. Shipping both was the restructure half-applied.

* Review round 8: one bright line instead of two soft ones

Round 8's four reviewers found 9 blockers, converging from two directions on
the same root problem: the rule still had two discretionary predicates, and
each was one paraphrase away from the causation test round 7 deleted.

"the **intended** numbers" (cardinal rule 1 in both top-level docs, the
always-on prompt, orx-git, and a Rust prompt string) restores exactly the
adjudication the tree skill kills — were these the numbers I meant? no ->
repair. "any **substantive** metric" (root SKILL.md and both orx-evidence
variants, the skill loaded at the moment a log is judged) invites the mirror
abuse: rule real metrics insubstantial and repair a node that produced
results. The tree skill neutralised both locally; none of that reached the
five other sites.

Both are now one bright line, stated identically at all six: **two or more
metric lines this run computed**. One banner, one config echo, or a single
step-0 line is not numbers; two logged intervals are, whatever they say. The
agent cannot tune that boundary after reading the log.

That also fixed a hole round 7 opened: "a throughput line" counted as
numbers, and a tqdm bar emits `3.6it/s` — so a *preempted* run froze
permanently on a progress bar, which bucket 3 is supposed to route to
repair. Progress bars, ETAs, and figures carried over from a resumed
checkpoint are now explicitly excluded, along with allocator byte counts.

Deleted the partial-trace carve-out: it contradicted the bullet three lines
above it ("losses past warmup" freeze vs "a warmup value" doesn't), left
"warmup" undefined, and was the last place the agent picked a boundary
post-hoc.

Also:
- The over-freeze is now acknowledged rather than left implicit. A node that
  OOMed on a pre-existing leak after emitting losses is frozen even though
  its change was innocent; saying so stops an agent reading it as an
  oversight to reason around.
- Stated the asymmetry that seven rounds kept re-deriving: a spurious freeze
  costs one node, a spurious repair rewrites the branch a recorded
  commit_sha points at. When ambiguous, freeze.
- "If the only available repair would change what the node is testing, it is
  not a repair" — a 2x-wider-MLP node that OOMs at allocation cannot be
  fixed by shrinking the width.
- The hard-debug guard asked the agent to date its own beliefs. Now
  mechanical and two-directional: the count of metric lines does not change
  between readings.
- The cap's reset clause was dead text (measuring something freezes the
  node, so repair ends there anyway).
- k8s said a GPU-type change is a repair while cardinal rule 2 calls the
  environment a fixed contract; reconciled.

* Review round 9: kind, not count — one metric freezes

Round 9's reviewers found that round 8's bright line broke the normal
success path, and both correctness reviewers landed on it independently.

"Two or more metric lines" made a clean eval that prints exactly one
`accuracy: 0.83` **provisional and editable in place** — as would a single
summary block carrying five metrics, or a run that completed one epoch.
Those are successful, complete runs. Worse, orx-evidence explicitly tells
agents to "print final metrics and a compact summary block at the end,
not just scattered mid-training lines" — so following the repo's own advice
produced exactly the log shape the freeze test then refused to protect. The
better a run's output hygiene, the less protected it was.

That inverted the whole risk gradient: a crashed run emitting two loss lines
froze, while a clean run emitting one final accuracy did not. It was also
the first version in nine rounds where a run that fully answered its node's
question could be repaired — the one harm the asymmetry paragraph exists to
prevent, contradicted by the rule it was attached to.

The threshold is now **one**, and the discrimination moved to kind: any
metric the run computed freezes the node, one line or many. The exclusion
list carries the weight — banners, config echoes, progress bars and it/s,
allocator byte counts, timeout durations, resumed-checkpoint figures are
never numbers however often they appear. That also dissolves three ways the
count was gameable (summary block vs lines, redrawn tqdm lines in the byte
stream, PTY wrapping) and retires a false axiom that claimed line counts
don't vary between readings — they do.

Also:
- "If the only available repair would change what the node is testing" was a
  general-purpose freeze excuse: pinning a dep, fixing a driver, or
  correcting a shape error all "change what it tests", so an agent could
  freeze any node it didn't want to debug and branch a child — the 14-node
  chain rebuilt with each link relabelled "frozen". It now applies only to a
  node the freeze test already froze.
- Reconciled the over-freeze paragraph with "remediation is neither width
  nor depth": a confound-fix child is depth only because the parent holds a
  measurement.
- The always-on prompt asserted the rule with no definition at all, so the
  default rule an agent ran under was undefined. Inlined it.
- The cloud variant judged the PTY log only, but cloud runs put metrics in
  EVAL.md and W&B — a documented best practice would have left a node
  provisional with its measurement sitting in the artifact store.
- Dropped the last two causal qualifiers ("of what this node changed").

* Review round 9 (quality): retire the dead metric-naming machinery

The two quality reviewers reviewed the committed state before the kind-not-
count fix landed, so most of their findings were already resolved. Two
survived, and both are real.

**orx-compute contradicted the exclusion list.** It said "if the run put
numbers there — an OOM at a known memory watermark, a timeout at a known
step rate — that *is* the node's result", while the freeze test says byte
counts inside an allocator error and durations in a timeout message are
*never* numbers. Same log, opposite verdicts, in the skill an agent loads
when a job OOMs — the OOM-both-ways loophole reopened at the one site most
likely to be read alone. It now defers to the freeze test instead of
restating a rival criterion.

**"The freeze test reads that metric" is false.** Round 4 added the metric-
naming chain when bucket 1 genuinely read the named metric. Round 8 made the
test metric-agnostic ("numbers that aren't the ones you named still freeze
it"), so six surfaces were left asserting a lookup that no longer happens —
including "a node without one cannot be judged", which is flatly untrue: a
node with an empty description is judged exactly like any other. Naming the
measurement is still worth doing, so the guidance stays with an honest
reason (a later reader needs to know what the node owed); the false
justification is gone.

Two clauses died with it:
- The "No metric named?" fallback resolved a lookup that no longer occurs,
  and its own last sentence just restated the bright line.
- The reporting-defect guard duplicated "Numbers that aren't the ones you
  named still freeze it — record what actually landed" one bucket above.

Neither deletion reopens the semantic-defect case; that bucket-1 sentence
closes it on its own.

* Review round 10: close the inclusion list, not the exclusion list

Round 10's four reviewers found 23 blockers. The structural one is a flaw in
how I had been writing the rule for three rounds.

The exclusion list was closed and the inclusion side open ("a loss, an eval
figure, a score" — plus anything else you'd call a metric). That is backwards,
because the tie-breaker already pushes toward freezing: the enumeration guarded
the disfavoured direction while the open list guarded the favoured one. So
`lr=3e-5`, gradient norm, `tokens/s`, `epoch took 42.1s`, and dataset counts
were all undecidable, and an agent that didn't want to debug an OOM could call
the LR trace "the metric my node owes" and freeze. That is the fifth home for
the same discretion: idea -> cause -> evidence class -> line count -> kind.

The inclusion side is now the closed one. Numbers means a **quality**
measurement — how good the model or its output is: loss, perplexity,
accuracy/F1/reward/win-rate, eval score. Nothing else, whatever it is called.
Operational figures are never numbers however central to the node's idea, and
a node whose stated `Measures:` is operational is mis-specified. That resolves
the LR-node scenario and every case the enumeration had missed.

Round 9's own regression, found here: I widened bucket 1 to read artifacts and
W&B but left buckets 2 and 3 saying "the log". On the normal cloud path — where
the run command writes EVAL.md — a run whose metrics landed in an artifact
satisfied bucket 1 (frozen) *and* bucket 2 (provisional, "repair it in place").
The destructive verb was on the wrong branch. Worse, widening where evidence
counts without widening where you must *look* made "I read the log, only a
traceback" fully compliant while EVAL.md held the metrics — a repair licence.
Buckets 2 and 3 now require all channels, and a "look before you classify" rule
makes the check mandatory. Same fix in orx-evidence and root SKILL.md; the
local variant states explicitly that the log is its only channel, so a later
round doesn't "harmonize" the divergence away.

Also:
- "produced from this node's own commit" was a re-entry point for the causal
  argument (inherited code emitting a loss isn't "my change producing it").
  Now "this run computed", with the irrelevance of authorship stated.
- orx-compute told the agent to bump a node's flavor while cardinal rule 2 and
  orx-compute-k8s call hardware shape a fixed contract. Re-sizing is now a
  repair only until some node in the round has measured.
- The project-wide cap existed only in the on-demand skill, so an agent on the
  always-on prompt could repair node A twice, B twice, C twice indefinitely —
  the "N node problems instead of one setup problem" the clause exists to stop.
- The deleted reporting-defect guard carried a remedy, not just a
  classification; restored the remedy clause alone.
- Cut "Why this way round" and two other restatements: after the principle
  landed they said what the opening line and the over-freeze paragraph already
  say.

* Cut the review scar tissue: 681 -> 402 added lines

Ten review rounds each closed a loophole by adding a clause, and I let that
framing override the context budget. The result was a rule that argued with a
hypothetical adversarial reader instead of stating itself — an 86-line freeze
test for something that fits in three lines, with the definition restated
three times and six paragraphs of pre-empted rationalizations.

The rule is unchanged. What went is the arguing:

- Freeze test: 86 -> 25 lines. Kept the two buckets, the operational-figures
  exclusion, permanence, and the ambiguity tie-break. Cut "Why this way
  round", the re-reading guard, the over-freeze acknowledgement, the
  hypothesis-repair clause, the per-value/per-line gloss, and two restatements
  of the definition — every one of them written to pre-empt a reading rather
  than to state the rule.
- Classify section: 38 -> 17. The table's first row was a pointer to the
  freeze test; the cap prose restated itself twice.
- SYSTEM_PROMPT (every turn, 3 harnesses): +44 -> +24. The exclusion list and
  the error taxonomy belong in the skill the rule already links to.
- orx-git: +78 -> +62. The repair section duplicated the sync recipe 60 lines
  above it. The `merge --ff-only` fix and the worktree-lock case stay.
- orx-evidence: the new section was a 17-line restatement of the freeze test
  in the file that links to it. Now a 7-line pointer.
- orx-create, orx-compute (x2), k8s: same treatment — one statement of the
  rule at its home, cross-references elsewhere.
- Dropped session_env_tests: 45 lines of mutex, Drop guard, and save/restore
  to test `launching_chat_session().is_some()` on a one-line wrapper. The
  behaviour that actually changed is covered by
  find_serves_the_requested_variant_body, which stays.

fmt, clippy, and 277 tests green; no old-model phrasing survives.

* Collapse the duplicate sections: markdown now +217/-173 (net +44)

The last pass shortened each section but kept the structure, so the same rule
was still taught in three places. The real cut is structural.

`orx-experiment-tree` had two sections — "A node is an evidence contract" and
"Classify before you create" — that both answered repair-vs-child, plus a
Repair bullet in the loop restating it a third time with its own cap. Merged
into one 23-line section; the loop's bullet is now a pointer. Both variants
go net negative: +39/-51 and +37/-50.

Same test applied everywhere else — is this the rule's home, or a restatement?

- orx-compute (x2): 18-line block re-teaching the freeze rule and cap -> 7
  lines of the three compute-specific routes, deferring the rest.
- Root SKILL.md: cardinal rule 1 was 15 lines saying it twice -> 6; the
  four-move paragraph restated the freeze rule -> dropped.
- orx-git: the repair section duplicated the sync recipe above it -> 10 lines
  for what is genuinely git-specific (same branch, worktree lock).
- orx-create: prose trimmed to the baseline-specific facts.

Nothing lost: the freeze rule, the repair cap, `merge --ff-only`, and the
`set -e` seed guard are each stated once at their home and cross-referenced
elsewhere. fmt/clippy clean, 277 tests green.

* PR review: reframe on baseline/hypothesis, restore Shape the tree

Two review comments, both right.

**Restored "Shape the tree".** My slimming pass deleted the whole section —
the flat-fan/noodle/stacked-bushes diagram and the width-vs-depth rule — as
collateral from merging two adjacent sections. That is pre-existing content
unrelated to this PR and should never have been touched. Restored verbatim
from main in both variants.

**Dropped the metric framing.** The rule leaned on "did the log carry a
quality metric", which is a log-parsing heuristic standing in for the actual
criterion: every node exists to establish a baseline or test a hypothesis, and
a run that dies on an error does neither. That is *why* successive runs on one
node are right — nothing was established, so there is nothing to protect. The
section now says that directly, and the exclusion list of operational figures
goes away with it: unintended behaviour (OOM, timeout, a divergence from a
bug, a missing dep) is an implementation or hardware detail, not an answer —
unless the node's hypothesis is itself about memory or runtime.

Reconciled the same framing across all eleven sites that had said "metric":
SYSTEM_PROMPT, root SKILL.md, both tree variants, orx-git, and both compute
variants.

**k8s bullet** cut to five lines and OOMKilled moved to the repair side with
the other implementation details, per the comment.

Added markdown lines barely move (218 -> 228); the tree skills shrink
(+66/+63 -> +45/+43). Net rises to +134 only because restoring Shape the tree
gives back 79 deletions I had wrongly banked as savings.

* Stop editing skills that aren't about the experiment tree

Fair challenge on the PR: k8s and compute are about launching runs, not about
tree shape or node creation. Re-read those edits cold and most of them were
tree semantics squatting in the wrong file.

- **orx-compute-k8s: dropped entirely.** The bullet was pure tree vocabulary
  ("not a result", "same branch", "same expId") in a file whose job is the
  manifest contract. A k8s failure routes through orx-compute anyway, and a
  manifest fix is an ordinary branch edit — nothing k8s-specific to say.
- **orx-compute: 7 lines -> 3.** Kept one clause, because this skill's own
  description says "use when a job OOMs" and its pre-existing bullet already
  says "re-launch, or pick a different flavor or backend" — an agent following
  that needs to know the node is still repairable. Everything else deferred.
- **Reverted "Let a real failure escalate you" outright.** I had rewritten a
  sizing rule to carry my terminology; sizing advice doesn't need it.
- **orx-evidence: reverted both variants.** The section restated the tree rule
  in the skill that links to it, and still used the metric framing dropped in
  the last commit.

Also removed the "Measures: <metric>" scaffolding from the branch step, the
worked examples, and the exp desc bullet. That existed to feed the metric
test; with the rule now on baseline-vs-hypothesis it has no consumer, so the
create examples go back to their original form.

Markdown drops to +166/-75 across 8 files (from +228 across 11). The change
now touches only the tree skills, the playbook, orx-git, orx-create, and the
one compute clause.
2026-07-26 20:56:36 -07:00
Daniel Kim f84bdf3b75 Catch "Permission denied (publickey)" before it costs a box (v0.1.78) (#119)
* feat: catch "Permission denied (publickey)" before it costs a box

orx shells out to ssh with no -i, so it authenticates with whatever the
local agent and ~/.ssh offer, while boxes authorize the keys registered
on the account. A key registered from another computer leaves the box
online but unreachable, and the user only finds out after paying to
provision it.

Compare the registered public keys against the locally-available ones at
login, before launch, and on a failed launch, and add `orx ssh-key
add|list` — the command the dashboard has always pointed at but which
never existed.

- login offers to register this computer's key ([Y/n], TTY-gated)
- preflight blocks when nothing is registered, warns when we can't find
  a local match (ssh can still succeed via an IdentityFile elsewhere)
- a publickey denial becomes one actionable line in `orx runs`
- the settings badge distinguishes "on this computer" from "registered
  elsewhere", which the old boolean could not

* fix: don't call the openresearch backend "Configured" without a usable key

The compute row keyed "configured" off being signed in, so it showed a
green badge on a backend that would refuse every connection — the same
gap the launch failure exposed, one screen earlier.

Gate it on holding a key the account has registered, and say which of
the two things is missing. Also run the check when openresearch is made
the default, since that's the moment the user is committing to it.

Unknown (api unreachable, ~/.ssh unreadable) counts as ready: we
couldn't tell, so don't cry wolf.

* fix: say "Unknown" when the readiness check couldn't run

Folding a failed check into "configured" put a green badge on a backend
we never verified; folding it into "not configured" would show alarm
whenever the api hiccups. Neither is true, so stop making the boolean
answer a question it has no data for.

Adds an optional `unverified` flag — absent for backends whose state is
decidable locally, so no other row changes.

* fix: name the key file we found instead of guessing one

The compute row told everyone to run `orx ssh-key add
~/.ssh/id_ed25519.pub` — a path that doesn't exist on a machine whose
key is named anything else, so the fix led to a second error.

The readiness check already resolves the real file via preferred_local;
carry it through instead of discarding it. Split the keyless case out,
since `ssh-key add` alone can't help there, and surface the Unknown
reason so that state stops being a dead end.

* chore: v0.1.78

* fix: stop guessing ~/.ssh/id_ed25519.pub in the skill, notes and run failure

Same bug as the compute row, in three more places. The skill was the
worst of them: an agent reads it and runs the literal command, so on any
machine whose key is named differently it fails with nothing to go on.

- skill: tell the agent to read the path from `orx ssh-key list` rather
  than handing it one, and cover the no-key-at-all case
- settings notes: use the .pub the check already found (new sshKeyPath
  in the payload), with separate copy when there's nothing to register
- run failure: point at `orx ssh-key list`; the supervisor can't see
  ~/.ssh, so it has no path to name
v0.1.78
2026-07-24 16:58:21 -07:00
Daniel Kim fcba6a25d1 Show a loading state instead of the empty state while chat history loads (v0.1.77) (#118)
* Show a loading state instead of the empty state while chat history loads (v0.1.77)

Switching between agent conversations briefly flashed the empty state
(OpenResearch logo + 'Ask the agent to explore your codebase...') before
the conversation's messages finished fetching. The empty vs transcript
branch was gated purely on messages.length, which can't distinguish a
not-yet-loaded session from a genuinely empty one.

Add a distinct 'Loading conversation…' state, driven by key presence in
messagesBySession (absent = still fetching). A failed fetch now seeds an
empty transcript so it recovers to the usable empty state instead of a
stuck spinner, and drops the loadedSessions guard so a switch-back retries.

* Enlarge the chat loading spinner and text for legibility

The 13px muted text + 13px wheel read as too small and hidden. Bump the
wheel to 22px (thicker stroke), the label to 16px, and use --subtext for
better contrast.

* Guard failed-fetch recovery against clobbering streamed messages

Review nit: the .catch seeded [] unconditionally, which could overwrite a
message that streamed in via SSE during a failed history fetch (self-heals
on the next re-broadcast, but briefly drops it). Add an onlyIfAbsent flag to
the seed action so recovery no-ops when the transcript key already exists.
v0.1.77
2026-07-23 18:15:19 -07:00
Daniel Kim 7940344d44 Open agent-inlined files-dir report links from chat (v0.1.76) (#116)
Clicking a report.md link an agent inlined into the chat transcript failed
with "path must be repo-relative": the report lives in the files dir
(<data dir>/files/<slug>/…), but every chat file-open routed through
parseFilePath → FileViewer → the repo /file endpoint, which rejects absolute
paths. parseFilePath had no files-dir case and FileViewer carried no source
discriminator, so files-dir artifacts were unroutable.

- Backend surfaces a (non-canonical) absolute filesDir on every project
  payload via a shared project_json helper, routed through all six emission
  sites including the SSE project.updated diff — enriching only list_projects
  would let a bare project clobber filesDir on the next visit.
- parseFilePath gains a files-dir branch: exact-prefix match against filesDir
  (byte-for-byte with the path the agent inlines), plus a /files/<slug>/
  suffix-regex fallback for symlink-divergent data dirs. Tabs it produces
  carry source:"files".
- FileViewer branches its fetch by source: files-dir reports via /files/report,
  other files via /files/file, else the repo endpoint. Files-dir markdown
  renders through FilesTab's ReportMd (now exported) so report-relative image
  paths resolve; a missing file shows a friendly not-found; repo-only notes are
  suppressed.

Verified end-to-end: the failing report.md chip now opens the report from the
files endpoint with no error.
v0.1.76
2026-07-22 20:16:39 -07:00
Daniel Kim 43852e42c0 Route run-completion notifications to the launching chat session (v0.1.75) (#115)
* Route run-completion notifications to the launching chat session

Runs carried no owning-session id, so the run watcher guessed the
recipient from project_id alone — the most-recently-touched, non-busy
session in the project. With two agents in one project, the launcher was
skipped by the is_busy guard (it was in its exp-wait loop) and an idle
bystander got the '[orx] Run ... finished ... continue the loop' poke.

Persist the launching chat session on the run (new runs.chat_session_id,
migrated in), stamped from ORX_CHAT_SESSION_ID which every per-session
harness child now exports (claude, codex app-server, codex sandboxed
exec, opencode). The agent's 'orx exp run' subprocess inherits it, so
run creation records the owner. watch_runs now routes to that session
only (via notify_target); a busy owner is skipped as before, and an
orphan run (CLI-launched / pre-migration) pokes nobody.

Ownership is immutable: chat_session_id is excluded from the upsert
ON CONFLICT clause, so a later status upsert never rewrites it.

* Trim redundant routing-invariant comments (review nits)

* Bump version to v0.1.75
v0.1.75
2026-07-22 18:44:33 -07:00
Daniel Kim 7956d54af2 Per-session context-window usage meter (#114)
* Per-session context-window usage meter

Surface how much of the model's context window each chat session has used —
a compact percent pill beside the model picker, with a popover breaking it
down into a progress bar and token caption (amber >=80%, red >=95%).

Backend: harness adapters now parse the token usage the CLIs already emit and
discard today, normalize it into a ContextUsage (used = latest request's
input + cache + output, not cumulative), stream it live over a chat.usage SSE
event, and persist the final value to a new chat_sessions.context_usage_json
column so it survives restarts. session_json carries contextUsage thereafter.

  - Claude: assistant message.usage (live) + result modelUsage.contextWindow
    (window), falling back to the last usage.iterations entry.
  - Codex: app-server turn/completed usage and legacy exec token_count
    (input + output; cached_input is a subset). Window from model_context_window
    when reported; static fallback intentionally empty (no invented numbers).
  - OpenCode: assistant message.updated tokens (input+output+reasoning+cache);
    window left unknown (provider config only), so the pill shows the count.

Frontend: ChatSession.contextUsage + fmtTokens helper; a chat.usage listener
that updates the session in place; a ContextMeter pill/popover; and a shared
ProgressBar extracted from SettingsPage and reused in both.

ui/dist rebuilt. A release version bump is still needed to ship.

* Restyle context meter: ring trigger + desktop-style popover

* Context meter: darker value text, more gap before send
2026-07-22 16:50:30 -07:00
Daniel Kim cef4881032 Stop composer keystrokes from re-rendering the whole transcript (v0.1.74) (#113)
* Stop composer keystrokes from re-rendering the whole transcript

Typing in the composer was laggy on long conversations: the draft state
lives in ChatPanel, which also rendered the transcript inline, so every
keystroke re-ran the full component body, re-filtered every message, and
allocated + memo-compared all N Message elements (doubled by StrictMode
in dev). PR #111 fixed the streaming path but left the typing path.

- Extract the transcript map into a module-level memoized Transcript
  component; all its props are referentially stable across keystrokes,
  so typing now costs one shallow compare instead of O(messages) work.
- Memoize the waiting-session scan (ran per session row per render,
  iterating messages x parts) into a useMemo'd Set keyed on reducer
  state, which only changes on SSE dispatches.

* Review nits: mirror Message's onRespond type, state memo constraint

* Ship rebuilt ui/dist; bump to v0.1.74
v0.1.74
2026-07-22 13:59:37 -07:00
Daniel Kim d0388778ae One persistent Claude Code child per chat session (#112)
* Persistent Claude Code child per chat session

Replace the spawn-per-turn + --resume fork with one long-lived
`claude --print --input-format stream-json` child per chat session
(new src/local/claude.rs ClaudeHost/ClaudeClient, mirroring codex.rs),
collapsing ~3-6s of per-turn spawn/config/MCP-handshake overhead.

- Respawn policy: child keyed by SpawnConfig; permission-mode/effort/
  bridge change or a cleared model -> kill + respawn with --resume;
  model-only change -> set_model control request; identical -> reuse.
- run_turn rewritten around a pure, store-free apply_event(ctx, state,
  event) folded over the child's stream-json output; native-session-id
  and flush stay in the caller.
- Gate token minted only at spawn (re-minting would strand a live
  bridge child); process_group(0) so terminal SIGINT can't kill a
  resident child mid-turn.
- ChatHost gains a claude host; interrupt/delete/shutdown and up.rs's
  project-delete loop kill the resident child (it survives task-abort).
- Tests: child_action matrix, classify_line variants, apply_event
  fixture tests over hand-written stream-json.

* Review round 1 fixes: drop ClaudeHost back-ref, fix achieved bridge_active, prune dead code

- spawn_client: force config.bridge_active = false before the plan block so a
  failed bridge write records false (was inheriting wanted=true, which both
  suppressed plan cards with no bridge to hold them and defeated the
  respawn-to-retry recovery)
- SpawnSpec carries the ChatHost directly (transient, no cycle); deletes the
  Weak attach/OnceLock apparatus and the duplicated up_port
- drop dead ClaudeHost::client_for; claude_effort back to private
- truthful docs: write_mcp_config and gate_tokens are per-spawn/per-child now
- new fixture tests: result without is_error falls back to subtype; errored
  tool_result flips the tool part to error

* Comment nit from clean review round: gate token is per-child, not per-turn

* Stream partial text/thinking deltas into the transcript

Without --include-partial-messages the CLI only emits complete assistant
messages, so each text block landed at once instead of streaming. Deltas
build the part under the same {mid}-{index} id the final assistant event
upserts, so the authoritative text overwrites the accumulated one; a
subagent's nested stream (parent_tool_use_id) is ignored.

* Note the stream-protocol invariants the delta overwrite leans on
2026-07-22 12:15:21 -07:00
Daniel Kim 9fe3579c2d Stop re-rendering the whole transcript on every chat streaming tick (v0.1.73) (#111)
* Stop re-rendering the whole transcript on every streaming tick (v0.1.73)

Long chat sessions crawled during streaming because each 150ms flush
re-broadcast the full assistant message (with uncapped tool outputs) and
the UI re-rendered — and re-parsed markdown/KaTeX for — every message in
the session on every tick.

Server: cap tool part output/error at 16k chars in TurnCtx::flush (one
choke point covering all three harness adapters), with a visible
truncation marker; the UI never rendered more than 20k chars anyway.

UI: wrap Message, Md, ToolGroup and ToolRow in React.memo, and make the
callback props (respond, openPlan, per-session file opens) referentially
stable so the memoization holds. upsertMessage already preserves object
identity for untouched messages, so only the actively streaming message
re-renders now.

* Review nits: drop inert memos, document the 16k/20k cap coupling

ToolRow/ToolGroup get fresh part objects every streaming tick (the server
re-broadcasts the whole message), so memo on them never skips — Message's
memo is what short-circuits untouched messages. Keep memo only where it
works (Message, Md). Also note on ToolRow's 20k slice that the backend
caps at 16k so the slice must stay above it, and drop the render-site
comment that moved to openFileInSession.
v0.1.73
2026-07-22 11:57:41 -07:00
Daniel Kim 54d0be7521 Live session-worktree viewer (Worktree tab) (#110)
* Add live session-worktree viewer ("Worktree" tab)

A new right-pane "Worktree" tab shows, for the active chat session, what the
agent is changing in its private git worktree before any run/commit exists:

- Changes view: status-badged changed-file list (complete even when the diff
  truncates) plus the unified diff of the worktree vs merge-base(origin/<baseline>,
  HEAD), untracked files rendered as new-file diffs.
- Files view: the full live worktree tree (shared CodeTab tree components),
  clicks opening the existing FileViewer against the session's worktree.

Backend: new read-only GET /api/chat/sessions/{id}/worktree endpoint and a
sessionId param on the code-tree endpoint (threaded through resolve_checkout_root
instead of a hard-coded None; mutually exclusive with ref). New git.rs helpers
merge_base, working_tree_diff_against, and changed_files are all read-only.

UI polls every 5s only while the session is busy (chat.busy SSE), refreshes once
on busy->idle, manual refresh always; keeps last-good data on transient errors.
Entry point is a FolderGit2 button in the chat header when a session is active.

* Review fixes: clear worktree tab on project switch/session delete, seed busy poll on mount, changed-files dedup

* Worktree tab feedback: diff-only Changes view matching the experiment drawer, styled tooltip on the header button

* Tooltip: View session worktree
2026-07-22 11:28:34 -07:00
Daniel Kim 0329d50e3a Keep markdown table cells from breaking words mid-word (v0.1.72) (#109)
* Keep markdown table cells from breaking words mid-word

.md's word-break: break-word shrinks each cell's min-content width to a
single character, so auto table layout squeezed short columns (e.g. a
"Round" header) into vertical letter stacks. Restore normal word-breaking
in cells with overflow-wrap: break-word for genuinely overflowing tokens,
and give .md tables the same scroll-in-place treatment as .report-md
tables so whole-word column minimums can't clip in narrow panes.

* Rebuild ui/dist

* Bump version to 0.1.72
v0.1.72
2026-07-21 17:28:53 -07:00
Daniel Kim e1b6c9c608 Render LaTeX math in chat and report markdown via KaTeX (v0.1.71) (#108)
* Render LaTeX math in chat and report markdown via KaTeX

Agents emit \(...\) / \[...\] LaTeX which plain markdown mangled
(backslash escapes stripped, underscores italicized). Add remark-math +
rehype-katex to the shared Md renderer and the Files-tab report renderer,
with a fence-aware normalizer that rewrites backslash delimiters to the
dollar forms remark-math understands. Applies to all harnesses since
every transcript text part renders through Md.

* Bump version to 0.1.71
v0.1.71
2026-07-21 16:50:30 -07:00
Daniel Kim 3661374314 Composer command chips, optional paper args, drop /hf (v0.1.70) (#107)
* Composer command chips: picked slash-skills render as a removable unit

- Picking a skill (menu, typing /name<space>, or the empty-state suggest
  button) lifts the command out of the textarea into a chip; the textarea
  holds only the args, with the skill's arg hint as its placeholder.
- Backspace at position 0 (or clicking the chip) pops it back into
  editable /name args text; send() reassembles the plain wire form, so
  server-side expansion is unchanged.
- User bubbles render a leading known /command with the same chip.
- /reproduce-paper args are now optional: bare invocation tells the agent
  to infer the paper from the current repo and only ask if unidentifiable;
  arg hint is bracketed to signal optionality.
- Drop the /hf skill.
- Fix a latent IME bug: Enter during composition no longer sends.

* Review nits: rename activeSkill to pickedSkill (avoids collision with the menu's activeSkillIdx), fix stale pending-question comment

* Chip inline with the input line; paper-aware optional placeholder

- The chip and textarea now share a flex row (.composer-input), so the
  command and its arg hint read as one line, Claude-desktop style.
- When the project has an attached paper, the /reproduce-paper placeholder
  says the args are optional and names the paper it defaults to.

* Chip overlays the textarea with first-line indent so args wrap full-width

The flex-row layout squeezed wrapped args into a column beside the chip.
Instead the chip now sits absolutely on the textarea's first line, the
first line is text-indented past its measured width, and the chip rides
the textarea's scroll (clipped at the wrapper) so scrolled text is never
hidden behind it.

* Claude-style arg hints and chip semantics

- Bracket every skill's arg hint ([topic], [paper on compute], ...) to
  match the Claude-desktop placeholder convention.
- The chip is inert like inline text: pointer-events none so clicks land
  in the textarea beneath, and Backspace at the start deletes the command
  outright (args stay) instead of dissolving it into /name text.

* Hint reads [paper] on [compute] — bracket each supplied part, not the phrase

* Paper-attached hint keeps compute; extend to /paper-to-marimo

- Placeholder on paper-attached projects now reads
  '[paper — optional, defaults to <id>] on [compute]' for both
  paper-reproduction skills — only the paper part is optional; compute is
  still expected.
- Both templates and /paper-to-marimo's no_args now tell the agent to
  infer the paper from the repo when the args name none, so a
  compute-only invocation works too.

* v0.1.70
v0.1.70
2026-07-21 15:09:26 -07:00
Daniel Kim a61e7af59f README: bullet-point intro and demo video embed (#106)
* README: bullet-point intro and demo video embed

* README: link uploaded demo video

* README: simpler benefit-focused bullets; name supported harnesses

* README: final intro bullet copy

* README: add contact callout at top

* README: make tagline a header

* README: h3 tagline, no underline
2026-07-21 01:53:31 -07:00
Daniel Kim a7cc9947a7 Codex: render every item type in the transcript; orx-lit-first routing rule (#105)
* Render every Codex item type in the transcript; route orx-lit first

The Codex chat adapter's apply_item whitelisted five ThreadItem variants
(agentMessage, reasoning, commandExecution, fileChange, plan) and dropped
everything else through a `_ => {}` catch-all. Codex reports its web
searches, MCP tool calls, and dynamic tool calls over the wire, so those —
and any future protocol addition — vanished from the transcript.

Replace the catch-all with explicit arms for webSearch, mcpToolCall, and
dynamicToolCall (rendered as tool parts, parity with the Claude harness), an
explicit skip for userMessage/hookPrompt (input echoes that would duplicate
the user bubble), and a generic fallback that renders any other item type as
a tool part named after its raw type — nothing is silently swallowed. The UI
toolLine now shows the WebSearch query and the WebFetch url. Rebuild ui/dist.

Also rewrite the orx-lit skill description as a routing rule: use it FIRST,
before any web search, for anything academic or research-flavored (paper,
author, blog post, model release). GPT skipped orx-lit on research queries
because the old description read as experiment-loop-only. Mirrored across the
code const, the skill frontmatter + body, SYSTEM_PROMPT.md, and root SKILL.md.

* Render codex web-tool actions (openPage/findInPage), not just search queries

The webSearch item's query is legitimately empty for openPage/findInPage
actions; the action object carries the url/pattern. Merge action fields
into the tool part input and label each variant in the UI.

* Label opaque codex web-tool actions as Browsed the web

codex 0.144 reports page visits as action {type:"other"} with no url —
the openPage/findInPage variants exist in the protocol but aren't emitted
by the hosted web tool yet. Stop calling those rows searches.

* orx-lit description: preferred literature tool, not a web-search sequencing rule

The 'use FIRST, then web-search what the corpus lacks' phrasing implied a
web search always follows. Intent: orx-lit is the preferred method for
literature search on academic/research work; web search is a fallback and
often unnecessary.
2026-07-20 18:50:13 -07:00
Daniel Kim 7cfef22bf6 Tour copy and sizing polish (v0.1.69) (#104)
* Tour copy and sizing polish

Rewritten step descriptions (workspace pitch, prompt examples, worktree
framing) with no em dashes and a single colon across the deck. Card
sized between the original and the first enlargement: 390px wide,
fs-lg title, fs-base body, full-size footer buttons.

* Bump version to 0.1.69
v0.1.69
2026-07-20 15:49:13 -07:00
Daniel Kim 2dd67bc2ca UI design-consistency pass: token scales, sentence case, sans-first, rounded everywhere (v0.1.68) (#103)
* ui: add type/weight/radius token scales, drop dead CSS rules

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

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

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

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

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

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

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

* ui: promote composer reasoning pill to --text

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* chore: bump version to 0.1.64

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

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

* Bump version to 0.1.63

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

---------

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

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

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

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

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

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

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

* Mention files/ + memory/ in datadir module doc
2026-07-17 19:53:25 -07:00