* feat: simplify the skills experience
The Customize tab was five sections deep in choices that no longer earned
their place: a Global/This project scope picker on every card, a separate
"import from your agent" list, and two split skill lists.
- Skills installed in a coding agent are now mirrored automatically —
read live from its skills dir on every listing and every session write,
so a skill edited in Claude Code is the one the next session runs. The
import step and its two endpoints are gone. A session hosted by the
agent a skill came from is not handed a copy it already loads.
- Claude Code's installed plugins are mirrored too, discovered through
installed_plugins.json so only real installs count.
- Every skill and LaTeX template is global. Project scope is removed
from the store, the API, and the UI; anything already saved under it
is migrated into the single store on first access.
- Skill frontmatter is parsed the way skills are actually written:
folded and literal blocks, and values wrapped over indented lines. A
skill whose frontmatter we cannot read is a skill the user never sees.
- ~/.agents/skills is read whenever it exists, rather than only when
~/.codex does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: harden the skills mirror after review
Review of the mirroring change turned up defects worth fixing before it
ships:
- A plugin's skills are registered namespaced (`runpod:flash`), so the
bare `/name` only resolves from a copy. They are no longer treated as
natively loaded by the agent that installed them.
- Uploaded skills resolve through their folder name again, not the
frontmatter `name` a hand-edit can change out from under them.
- The hosting agent's own skills are dropped only after they have won
their `/name`, so a same-named skill from another agent can't take
their place in the worktree while the dashboard shows the first.
- Session skill dirs are replaced and pruned only when the manifest says
we wrote them, so a `.claude/skills` the project itself commits is
left alone; manifest names are re-validated before any removal.
- A folder whose source is unchanged is not re-copied, folders over the
upload budget are skipped, and neither the copy nor the size walk
follows symlinks.
- The retired per-project store is emptied, never deleted: anything that
can't move stays put as the user's only copy.
- A `#` comment no longer folds into the value above it, and a long
description truncates instead of dropping the skill.
- The Customize tab distinguishes a failed skills fetch from an empty
one, and ignores drops while an upload is in flight.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: keep one answer for what a skill name resolves to
Second review round:
- `source_dirs` resolves uploads through the same listing the dashboard
reads, so a folder whose SKILL.md won't parse can't win a name in the
worktree while the tab shows the mirrored skill it shadowed.
- A folder tally now carries a digest over every (path, size) pair, so a
rename or a move inside a skill brings the session copy forward; each
source and destination is walked once per turn instead of three times.
- A destination whose content already matches its source is adopted as
ours, so a lost manifest heals instead of freezing that skill forever.
- A mirrored folder over the upload budget is left out of the listing
too, rather than offering a `/name` that never reaches the worktree.
- LaTeX templates get the same skip-if-unchanged treatment.
- Archive junk under the retired per-project store no longer keeps it
alive on every call.
The freshness test now watches the inode: `fs::copy` carries the mtime
across on macOS, so the timestamp it asserted on could never have failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: never let an adopted skill dir become prunable
Round three: adopting a destination whose content already matches its
source healed a lost manifest, but it also recorded a directory orx had
never written — so once the source went away, the prune deleted a skill
the project itself commits.
Adoption is now limited to the case it was for: no manifest at all. While
a manifest exists it stays the whole truth about what we own.
Also from that round: the upload budget moved into `source_dirs`, so the
menu, the hover preview and the session write agree on which `/name`
exists; a stray `.DS_Store` beside the retired per-project store no
longer keeps it alive; and the uncapped walks stopped pretending they can
fail.
The fingerprint test never reached the SKILL.md byte comparison it was
supposed to cover, and the migration test never asserted the retired tree
was gone — both fixed, and both verified by reverting the fix under them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor: one budget answer for uploads too
The listing kept sizing uploads with an uncapped walk while the resolver
had started budgeting them, so a hand-edited store could list a skill no
session would be given.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
An .html file opened in the right pane now renders as the page it is —
a generated plot or report — instead of showing its source, matching how
markdown already behaves. The existing header toggle switches to source
(and, for a checkout file, the editor).
The document renders in an iframe sandboxed without allow-same-origin, so
its scripts run in an opaque origin and cannot reach the loopback API, the
dashboard's storage, or the parent page. That origin is also refused any
loopback subresource, so project-local assets a document references are
fetched by the parent and inlined as data: URIs rather than rewritten.
Inlining is gated on the response Content-Type matching the element it
would fill, which requires the server to type a response by the path it
resolved to. It did not: content type came from the requested name while
the bytes came from the canonicalized target, so an in-repo symlink
(logo.png -> .env) served secrets as image/png. The three raw handlers now
pass the resolved path, disk_response's parameter is renamed type_path to
say so, and chat_attachment is made uniform.
A srcdoc document inherits the embedder's base URL, so a report's #section
link navigated the frame into the dashboard SPA. A base of about:srcdoc is
injected unless the document names an origin of its own, and external
anchors are given target="_blank".
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix: give a new project folder inside another repository its own repository
Creating a blank or paper project under a folder that merely sits inside an
enclosing checkout — a dotfiles repository at $HOME, for example — failed with
"is already inside a Git repository; a blank project needs a folder of its own".
A folder the project owns, because it was empty or freshly created, now gets a
nested repository of its own, so the project root stays the folder the user
picked. Adopting an existing folder is unchanged: it still resolves outwards to
the enclosing repository, which is the project the user means.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8iVn4CSa9Q38FgogXzRch
* fix: handle nested project repository edge cases
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Daniel Kim <sox8502@gmail.com>
* feat(ui): tell the dashboard when the backend is gone
The dashboard's one EventSource was also its only evidence the local
server still exists, and a dropped stream was treated as a transient
reconnect. Quitting the macOS app therefore left a tab that looked live
but was frozen, on an ephemeral port nothing will ever answer again.
Track the stream's state and surface it as a banner. The stream is now
re-openable too: a CLOSED EventSource is the browser giving up for good,
which no retry of its own will recover from.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ui): address review on the offline banner
Copy is descriptive rather than prescriptive: the macOS app binds a fresh
ephemeral port per launch, so telling the user to reopen it promised this
tab a recovery it will never get.
The live region is now permanently mounted with swapping text, since one
inserted together with its content is missed by VoiceOver and NVDA, and
an error arriving after teardown can no longer arm a timer nothing is
left to clear.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(ui): name the open project in the browser tab title
The tab now reads "OpenResearch - {Project Name}" while a project is open,
so multiple dashboard tabs are distinguishable.
The title falls back to plain "OpenResearch" on every screen that renders
instead of a project — projects home, the startup-error page, and the
loading spinner — since a partial startup failure or an SSE project event
can leave `projects` populated behind those. The project name is wrapped in
`autoDir` so an RTL name cannot reorder the brand prefix, matching how
project names are interpolated elsewhere.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ui): lead the tab title with the project name
Browser tabs truncate from the right, so the invariant "OpenResearch"
prefix survived while the project name — the part that distinguishes one
tab from another — was clipped first. The title now reads
"{Project Name} - OpenResearch".
The autoDir isolate around the name becomes load-bearing in a second way
here: as the leading run it would otherwise supply the title's first strong
character, so an RTL project name would flip the whole title's base
direction. FSI is skipped by the base-direction scan, keeping it LTR.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Remove subtitles that restate their own heading, and rewrite the
model-picker harness note as two sentences instead of an em-dash splice.
Drops the "General" subtitle and four message keys whose UI was already
removed in #262 and #258, leaving the catalogs with dead entries. Restores
the Environment subtitle wording that #262 replaced.
The Persian model-picker string deliberately ends without a period: it
renders in a bare div, and index.html pins dir="ltr" for every locale, so
a trailing neutral would paint at the wrong end of the RTL line.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
`prepare_env` fronts this build's directory on a harness child's PATH, but
the child's tool shell then sources the user's own startup files, and a
profile prepending its bin dir — or macOS `path_helper` rebuilding PATH
wholesale — pushes it back down. Agents shelling out to plain `orx` reached
whatever copy was installed instead: one session ran `orx agent spawn`
against a build predating the subcommand, while the skill documenting it
came from the binary serving that session.
Export the running orx's bin dir and re-front it from the per-session shell
hooks, which are the last thing orx gets to run after the user's files. The
guard rides every zsh startup file and the bash BASH_ENV hook; a harness
that snapshots the user's shell captures the guarded order, since the
capture itself passes through these files.
`orx_bin_dir` now degrades to the un-canonicalized `current_exe` path rather
than giving up, which also stops app-mode children from inheriting launchd's
bare PATH after a failed canonicalize, and drops a relative or colon-bearing
directory that could not be spelled in a PATH entry.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Add an orx-figures skill for publication-quality figures
Figures were left to matplotlib defaults: screen-sized, titled where a
caption belongs, and rasterized where the document wants vector. This adds
a bundled skill that routes by the question a figure answers and ships the
style layer rather than describing it.
`orx-figures` follows the `orx-compute` shape — one SKILL.md carrying the
non-negotiables, then exactly one of six references (curves, scaling,
comparison, pareto, matrix, diagram). Two assets install beside them:
`orx_figstyle.py`, vendored into the project so a figure stays reproducible
after the session ends, and a TikZ scaffold sharing its palette.
`save()` audits every figure and prints the result: printed width against
the known column widths, Type 42 font embedding, stray axes titles, missing
axis labels, sub-5pt text, overlapping text, and text running off the
canvas. Prose the agent can skip is how the first unusable figures shipped;
these checks run whether or not the guidance was read.
The playbook and the `orx skill` overview both direct figure work here — a
session with the skill installed still plotted raw matplotlib until they
did.
Content is calibrated against 39 popular recent alphaXiv papers (283
captions) and 10 arXiv e-print sources: 87% of real figure PDFs are built
wider than any single-column page, so the width check is the load-bearing
one; multi-panel is 27% of figures; and captions run a median of 28 words.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Fix review findings in the figures skill
Correctness:
- `label_ends` computed the reserved x-room in the pre-`set_xlim` scale, so
the labels overhung the spine by need^2/(width+need) — about a quarter of a
long label. Solve for the limit at which the original span occupies
(axes width - label width) pixels instead.
- The audit's categorical-axis exemption keyed off whether tick text parsed as
a number, but log formatters emit mathtext, so every log axis was classified
categorical and escaped the missing-axis-label check. Decide by scale.
- `save()` audited before writing, and the audit needs an Agg-family renderer
that is not guaranteed in the very case its first check reports. Write the
files first; the audit can no longer be the reason a figure is lost.
- `curves.md` interpolated every seed onto the first seed's grid, and
`np.interp` clamps, so a run that stopped early gained a fabricated flat
tail. Clip to the range every seed reached.
- `comparison.md` hardcoded `ylim(0, 90)`: any score above it drew as a bar
clipped at the axes edge, which is the dishonesty that reference is about.
- `scaling.md` zipped families against three markers, silently dropping a
fourth family from the main panel while still plotting its residuals.
- Guards for degenerate input: zero-variance `diff_ci`, all-NaN heatmaps,
empty confusion rows, resamples that fail to fit, empty label lists.
Consistency:
- `diagram.md` still taught the retired natural-size include, and its inline
path dropped `\familydefault`, which would render a serif diagram beside
sans plots. Both contradicted SKILL.md.
- `orx-paper` and `orx-reports` examples named a rasterized plot.
- The audit emits a font-fallback finding the docs never listed, and it is the
one finding an agent should hand over rather than fix.
Tests now assert the rendered playbook rather than the raw template, and pin
the audit's problem strings rather than text that also appears in docstrings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Fix round-two review findings
The round-one TikZ fix was worse than the bug: it told the agent to move
`\renewcommand{\familydefault}{\sfdefault}` into the paper's preamble, which
is document-wide and would have set the body text, headings, and captions of
a submission to sans. The face now rides in the `orx`, `edgelbl`, and
`stagelbl` styles, so it travels with the picture and touches nothing else.
Also from review:
- `diagram.md` still carried the broken relative `cp assets/...`, which does
not resolve from a session's cwd.
- The new "write a report figure to the artifacts directory" note contradicted
"keep the generating script beside its output"; both destinations now keep
the pair together.
- A `WIDE` figure only gets a 6.75in `\linewidth` inside `figure*`; in a plain
`figure` it silently halves, which non-negotiable #1 now says.
- Skipping all text on an axis-off axes also exempted the user's own content,
which on such an axes is the figure. Exclude only the undrawn axis artists.
- `save()` leaked the figure when the audit raised, and did not create the
stem's parent directory.
- A single seed drew a zero-height band that reads as certainty; `mean_ci` and
`diff_ci` now warn, matching what the references demand of the caption.
- Guards for disjoint seed ranges and all-zero scores.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Close the round-three review findings
None were blockers, but three were real defects on secondary paths:
- The inline `\input` route omitted `\usepackage{tikz}` and the
`\usetikzlibrary` line, so a paper following it literally would not compile:
`trapezium`, `Stealth`, `fit=`, and `on background layer` each need one.
- Vendoring was hardcoded to `figs/`, so a report script written under the
artifacts directory could not import the style module beside it.
- Removing `\familydefault` left no documented way back to a serif diagram
while `use_style(family="serif")` still exists for plots.
The colorbar guard now accepts `ax.get_label() == "<colorbar>"` as well as the
private `_colorbar`; verified against matplotlib 3.11 that both hold today, so
a rename can no longer make `clean` unreachable for every heatmap.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
A user's `codex` npm install had lost its vendored platform binary, so the
node wrapper on PATH ran and exited 1 on every invocation. orx reported the
harness ready anyway — `installed` came from PATH presence alone, and codex
reads its credentials off disk — so the composer let them send, and the CLI's
own crash landed in the transcript. The failed turn was then classified as
possibly-delivered, so the card offered a "may have been accepted" Continue
that re-ran the same doomed spawn.
A `--version` probe that fails conclusively now marks the install broken:
never ready, with a reinstall note in place of a sign-in one. Conclusively
means the CLI printed no version and either exited non-zero or could not be
executed for a stable reason — a timeout or fork pressure stays inconclusive,
because falsely locking a working harness out of chat is worse than the bug.
Claude needed two more guards: its auth overlay was overwriting the reinstall
note with `claude auth status`, and a claude that broke mid-session had its
cached ready entry restored and re-cached, permanently.
For the turn itself, a codex exec that exits having emitted no event never
reached the model, so it reports as undelivered and the card offers Retry.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat: add Tinker hybrid compute backend
* fix: use official Tinker logo
* fix: handle zombie process groups during cancellation
* fix: use portable process table arguments
* fix: delimit process group signal targets
* chore: bump version to 0.1.115
* refactor: use standard cancellation for Tinker
File and run chips looked like inline code, so nothing said a click opens
them in the right pane. Each chip now ends with a panel glyph and carries
`.tool-target`'s underline on its label, and the tooltip names the
destination. Non-interactive chips drop both cues.
The rules live unlayered in tailwind.css beside `.md .file-chip` so the
wrapper's `.run-chip svg` utilities cannot tint the trailing glyph.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
A `.tex` in the checkout can be linked to an Overleaf project and synced
with it in both directions over Overleaf's git bridge: edits made here are
pushed on save, edits made in Overleaf are pulled while the tab is open.
Accounts without the bridge — it is a paid Overleaf feature, and Overleaf
publishes no way to ask whether one has it — can upload the paper as a new
project instead, which any account can do.
Each sync records what both sides agreed on, path to content hash. That
agreement is what gives a later difference a direction: one side moved, or
both did. A file both sides changed is left untouched on both sides and
reported, because resolving it either way would discard somebody's writing;
the panel asks which copy to keep. The agreement is scoped to the checkout
it was taken from, since a session worktree and the hub clone hold different
copies of the same path.
A sync clones the project fresh, works out the whole exchange without
writing anything, pushes, and only then touches the checkout — so a rejected
push cannot leave the disk ahead of what was recorded, and a co-author's
edits can never be clobbered by a stale local copy.
The token is stored outside `~/.openresearch/env`: that file is fanned out
to every compute backend and into the agent's environment, and nothing off
this machine pushes to Overleaf. It reaches git only through a credential
helper scoped to the linked project's host.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(codex): offer every provider model when a model catalog is declared
A custom Codex provider currently advertises only the single `model` set in
config.toml, so the orx model picker has nothing to switch to even when the
provider exposes more models (e.g. DeepSeek via model_catalog_json).
When a custom provider declares an explicit model catalog, query the
app-server's model/list (the same live catalog the first-party path uses)
and offer every model it lists, with the configured model still the default
entry. Without a declared catalog the CLI falls back to its bundled
first-party list, which says nothing about a custom endpoint, so the
single-configured-model behavior is preserved there.
Closes#244
* fix(codex): preserve configured provider model selection
---------
Co-authored-by: jie.yuan <yuanjielovejesus@gmail.com>
Co-authored-by: Daniel Kim <sox8502@gmail.com>
The composer hoisted a picked command into React state and pinned its chip to
the textarea's first line, so a command chosen mid-message jumped to the front
and the message had to be reassembled on send. The `/name` token now stays in
the draft exactly where it was typed and a mirror of the textarea paints the
chip behind the real text — the draft is the wire form, with nothing to
reassemble. The mirror copies the textarea's metrics at runtime and its pill
bleeds via box-shadow spread, so it adds no layout width and every glyph after
it stays put.
Backspace just behind a chip deletes the whole command, a command typed with no
args shows its arg hint as ghost text (and to screen readers, which the
placeholder used to carry), and the transcript chips the tokens where they were
sent. Only a leading command expands server-side, so only that one is
lowercased for the backend's exact match.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Clicking a `file:line` code chip highlighted the target line with
`--primary`, a deep red that read as an error marker. Use the existing
`--accent-blue-subtle` band with an accent-blue rail instead.
The dist rebuild also prunes three stale bundles and repairs
index.html's dangling stylesheet href, which release builds embed.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Drop the brand mark from the DMG background so the window is just the app
icon, an arrow, and the /Applications alias. With the mark gone the window
shrinks 640x400 -> 640x320 and the icon row recenters (86px above the icons,
86px below the labels).
The arrow now sits level with the icon centers (ICON_Y) and is centered on
the icons' artwork gap rather than their 128px cells, since the two icons
inset their art by different amounts.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The chat composer only offered skills while `/name` was the first token;
inline slashes were narrowed to `/plan`. A `/name` under the caret now
opens the menu wherever it sits, and picking it chips the command with
the surrounding text as its args.
Picking mid-sentence takes a deliberate Tab or click: Enter accepts only
where the command opens the draft or one of its lines, so an ordinary
sentence mentioning a `/token` still sends as typed. Auto-convert on a
trailing space stays limited to a draft that is nothing but the command.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The editor layers a transparent-text textarea over the highlighted code, so
the opaque global ::selection background painted blank rectangles over the
tokens underneath — selecting a line hid the code you were selecting. Give
the edit surface its own translucent selection tint instead.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
A paper with no linked GitHub repository could not become a project at
all: the API rejected it and the form hid every control below the paper
picker. Such a paper now creates a project seeded with its PDF, committed
as the repository's initial commit, and the form explains why there is no
repository to clone.
The destination has to be an empty folder of its own, outside any Git
repository, because the paper is committed into the repository the
project initializes — seeding an existing repository would leave the
paper untracked, and a subdirectory would put it outside the project
root. A failed PDF download fails the request rather than quietly
producing an empty project, since the PDF is the only content such a
project has.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Give shell_env the one PATH search every lookup uses
`find_on_path` sat in `harness::detect` as `pub(super)`, so anything outside
the harness registry that needed a shell-PATH lookup wrote the loop again —
`find_opencode` and `resolves_on_path` both had a copy, the former commented
"Mirrors `find_on_path`". Move it to `shell_env`, which already owns
`search_path()` and whose module doc is about which PATH orx searches, and
point all four call sites at it.
The shared lookup now also drops *relative* PATH entries, not just empty ones.
A relative entry names no fixed directory: it resolved against orx's own cwd,
which for a Finder-launched bundle is never where a user's tool lives, and the
child that later ran it would resolve the same name somewhere else again.
`other_orx_on_path` in install_cli.rs keeps its own loop — it needs every
candidate and canonicalizes them, so it is a different search.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Find the LaTeX toolchain on the user's PATH, not launchd's
A `.tex` file opened in the macOS app reported "No LaTeX toolchain found on
PATH" on a machine with tectonic installed. The bundle is started by launchd
with `PATH=/usr/bin:/bin:/usr/sbin:/sbin`, where no TeX lives, and `latex.rs`
was the one lookup still resolving against the process environment — the
harnesses had gone through `shell_env`'s probed shell PATH for exactly this
reason. Verified against a bundle launched with launchd's environment:
`/api/latex/engine` returns `tectonic` where the installed build returns null.
Finding the tool is not enough on its own: latexmk finds the engine, biber and
bibtex on PATH itself, so the child is handed the same PATH we found it on.
The tool is spawned by its absolute path but symlinks are left unresolved —
TeX picks its format file from the name it was invoked as, so canonicalizing
`pdflatex` to `pdftex` would silently run plain TeX.
`on_path` no longer spawns `<binary> --version` to decide. It costs a probe
that a present-but-unrunnable binary would have caught, and buys back five
subprocesses on every `.tex` tab open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Point a user with no LaTeX at Tectonic rather than MacTeX
The no-engine hint led with MacTeX and offered `brew install --cask mactex` as
the command to copy — a multi-gigabyte install for someone who just wants to
see their paper. Tectonic is one self-contained binary that fetches each
document's packages, so it is the install a user can actually finish, and it
is what the copyable command now installs.
The distribution stays named second, because Tectonic runs only XeTeX. The
hint says so: it is the last screen that can, since the hint disappears the
moment an engine exists and the amber substitution note only fires for a
document that names its engine explicitly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
A `.tex` file in the live checkout now compiles on open and shows the real
PDF, with a toggle to a live source editor that recompiles on save.
Compilation targets Overleaf's behaviour: `latexmk` drives whichever engine
the document asks for via `% !TeX program`, running biber or bibtex and
repeating passes until references settle. Without latexmk the engine is driven
directly and that orchestration is done here; with neither, `tectonic` stands
in and says so, since it is XeTeX and cannot honour a LuaLaTeX document.
Users can upload their own templates — a conference class, a house style — as
a `.tex` or a `.zip` carrying its `.cls`/`.sty`/`.bst`. Templates are global or
scoped to one project, and are written into each session worktree so the agent
can start a paper from one. Uploading and managing them lives in the renamed
Customize tab.
The `orx-paper` skill and the `/write-paper` command teach the agent to write
papers as `.tex` in the working tree, follow an uploaded template when one
exists, and ground every reported number in a run rather than in memory.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>