nandi/oripublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/nandi/ori.git
git clone ssh://git@rickub.com/nandi/ori.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

forked from bots-garden/ori

🛟 Updated. b6cd929Unverified · on main · k33g · 11h ago
summary.md · 79 lines · 17.7 KBmarkdown
Blame HistoryOpen raw

Ori — project summary

What it is

Ori is a web client for ACP (Agent Client Protocol, https://agentclientprotocol.com) code agents: a Go backend serves an embedded React SPA and connects over ACP (JSON-RPC on stdio) to an agent subprocess — Claude Code via the @agentclientprotocol/claude-agent-acp adapter by default (since 2026-09-18). The browser gets a Zed-style agent panel: streamed markdown answers, collapsible thoughts, tool call cards (with statuses, locations and diffs), the agent's plan, and permission prompts answered from the UI. Implements ticket .tickets/issues/0004-specifications.yaml.

Architecture

browser ⇆ WebSocket /ws ⇆ Go backend ⇆ stdio (ACP) ⇆ agent subprocess
  • Go module: rickub.com/bots-garden/ori (NOT github — the repo is not hosted on GitHub; the user corrected this explicitly).
  • cmd/ori — server binary; flags --addr (default :8888), --cwd, --agent-cmd (whitespace-split).
  • cmd/ori-mock-agent — deterministic demo ACP agent on stdio (thought → plan → read tool call → permission → edit tool call with diff → answer); used by make run-mock and the e2e test.
  • internal/config — flag parsing/validation.
  • internal/httpserver — embedded SPA (via ui/embed.go, go:embed all:dist), /healthz, SPA fallback, extra-route injection point for /ws.
  • internal/agent — spawns the agent, drives the ACP lifecycle through github.com/coder/acp-go-sdk v0.13.5 (community SDK listed by the official ACP docs; the zed-industries repo itself has NO Go library); generic Handler interface (session updates + blocking permission requests); real fs read/write; terminals refused (capability not announced).
  • internal/files — workspace file API (GET /api/files, GET /api/file, PUT /api/file, plus GET /api/files/search?q=&limit= — recursive, case-insensitive, skips .git/node_modules, default 50 / max 500 results, walk capped at 50 000 entries — and GET /api/raw?path= streaming bytes with pinned image Content-Types, Range support, no size cap): list/read/write, relative paths against --cwd, absolute allowed; access deliberately unrestricted (user's choice: ori targets already-isolated sandboxes); binary → 415, >5MiB → 413.
  • internal/terminal — one shell per WebSocket on GET /ws/terminal in a PTY (creack/pty v1.1.24); binary frames out, JSON input/resize frames in; shell from $SHELL→bash→sh.
  • internal/skillsGET /api/skills: discovers <cwd>/.claude/skills/*/SKILL.md and ~/.claude/skills/*/SKILL.md (frontmatter name/description, directory-name fallback, project shadows user, sorted).
  • internal/bridge — hub between agent and browsers: JSON protocol (see docs/*/reference/websocket-protocol.md), broadcast with per-client buffers (slow client = dropped), append-only history (cap 4096) replayed on every connection, user_message echo so the server is the single source of truth, permission routing by requestId (pending requests replayed to late joiners; agent-side ctx cancellation handled). The prompt message carries optional attachments: [{path,name}], turned server-side into ACP resource_link blocks (file://<abs>) next to the text block — Prompter.Prompt(ctx, []acp.ContentBlock) replaced PromptText; relative paths are joined to the root given by Bridge.SetWorkspaceRoot (wired in cmd/ori/main.go); user_message echoes the attachments. Uses github.com/coder/websocket v1.8.15; origins restricted to localhost.
  • ui/ — Vite + React 19 + TS strict; zustand store around a pure reducer (ui/src/reducer.ts, handler-table style); WS client with backoff reconnect; thread resets on hello so replays are idempotent; unknown ACP update kinds ignored by design. Workspace panel (separate zustand store ui/src/workspace.ts): file tree (click=preview, double-click=edit; column resizable via ResizeHandle — ARIA separator, pointer drag / arrows / Home-End / double-click reset — width fileTreeWidth in the store, bounded 120–800 px, default 224, applied through the --filetree-width CSS custom property, persisted in localStorage ori.fileTreeWidth). Colour theme (ui/src/theme.ts, own zustand store): light (default) | dark, stamped as data-theme on <html> at module load and on change, remembered in localStorage ori.theme, toggled by the header ☾/☀ button; app.css keys the dark palette on :root[data-theme="dark"] (the former prefers-color-scheme media query is gone — decision: explicit choice, light default), MonacoViewer reads the store (vs-dark/light), the terminal stays dark. Preview tab (Monaco read-only for code — bundled workers, no CDN, lazy chunk ~1MB gzip; rendered Markdown; rendered AsciiDoc via lazily-imported @asciidoctor/core, Rendered/Source toggle), Editor tab (Monaco, dirty ●, Save/Ctrl+S via PUT /api/file), Terminal tab (xterm.js on /ws/terminal, starts on first activation); inactive panes are hidden not unmounted so shell/edit state survives tab switches; execute tool-call output renders in a monospace <pre>, never markdown. Composer completions (ui/src/mentions.ts pure helpers, ui/src/useCompletion.ts, CompletionPopup): @ opens a workspace file search (→ attachments), / opens a merged list of local skills (/api/skills) and ACP available_commands_update commands (stored as commands in the reducer, cleared on hello; deduped by name, skill wins) and inserts /<name> as plain text — the text is the invocation mechanism, there is no ACP "run command" method. Preview kinds image (ImageView, <img> over checkerboard via /api/raw, natural size shown) and drawio (DrawioView: iframe on embed.diagrams.net fed by postMessage, Rendered/Source toggle, 8 s timeout → Source fallback; .drawio.svg/.png are images). The mock agent emits available_commands_update (review, compact) on session start and echoes [attached: name].
  • ori-desktop/ — separate Go module rickub.com/bots-garden/ori-desktop (Wails v2.16.0, Go ≥ 1.25, "plain" template, //go:embed all:frontend/src, no npm; frontend/src/main.js is loaded as an ES module — type="module" — since the 2026-09-17 lint cleanup): connection screen (URL remembered in <UserConfigDir>/ori-desktop/settings.json, key serverUrl; auto-connect at startup), /healthz probe via bound Go methods (GetConfig, SaveConfig, CheckHealth, Connect, OpenInBrowser, SettingsPath), then the ori SPA loaded in an <iframe> (Wails v2 cannot navigate the main webview to an external URL; runtime.BrowserOpenURL is the fallback). Ticket 0005. make desktop / make desktop-test; root go test ./... unaffected (nested module). On Linux, wails build needs -tags webkit2_41 (Ubuntu 26.04 ships only webkit2gtk-4.1; the plain command dies on Package 'webkit2gtk-4.0' not found). make desktop does NOT pass that tag, so it fails in this sandbox — run cd ori-desktop && wails build -tags webkit2_41 instead. Build outputs coexist in build/bin/: ori-desktop (Linux ELF), ori-desktop.app (macOS, built on the Mac), ori-desktop.exe (Windows cross-build); Wails cannot cross-compile to macOS from Linux. Build instructions for the three platforms: ori-desktop/how-to-build.md (2026-09-18, EN only — no .fr.md counterpart yet, and no README links to it).

Key decisions in force

  • Claude Code is reached through @agentclientprotocol/claude-agent-acp (default npx -y @agentclientprotocol/claude-agent-acp, since 2026-09-18; internal/config.DefaultAgentCommand, pinned by TestDefaultAgentCommandIsClaudeAgentACP). It bundles its own Claude Code CLI via @anthropic-ai/claude-agent-sdk 0.3.274 → CLI 2.1.274 delivered as per-platform optional deps (…-sdk-linux-arm64, -linux-x64, …; no cli.js any more). Replaced Zed's @zed-industries/claude-code-acp 0.16.2, whose embedded CLI 2.1.44 the API rejects for current models ("version 2.1.251 or newer is required"). The claude CLI itself has no native ACP mode; verified.
  • draw.io diagrams render online via embed.diagrams.net (no bundlable offline renderer exists: viewer.min.js is not on npm, mxgraph is archived and lacks draw.io's shapes, drawio2svg is GPL); future offline path = vendoring viewer-static.min.js (~3 MB, Apache-2.0). Documented in the architecture explanations.
  • ACP payloads cross the WebSocket verbatim (raw SessionUpdate / RequestPermissionRequest), keeping the front forward-compatible.
  • One agent session shared by all browsers in v1; multi-session is a planned evolution seam (bridge.Prompter interface, per-connection subscriptions).
  • The server owns the thread (history + user_message echo); the SPA rebuilds from replay on every (re)connection.
  • Quality config decisions (all user-approved): .claude/** excluded from qlty (vendored kit sources, the measurement instrument itself); radarlint-python plugin removed (its ~300MB JDK cannot fit the dedicated 488MB ~/.qlty volume — diagnosis in .qlty/qlty.toml comments); golangci-lint + biome added manually (init missed both languages); biome.json at root parses .vscode/** as JSONC.

Build / test / run

  • make deps — npm install in ui/.
  • make build — SPA build then Go binaries (bin/ori, bin/ori-mock-agent). Order matters: the Go build embeds ui/dist.
  • make testgo test ./... + vitest run (the ui test script sets NODE_OPTIONS=--no-experimental-webstorage: Node 25's built-in localStorage lacks clear() and shadows jsdom's — 15 store tests fail without it; verified 2026-09-18 on Node 25.9). Also useful: go test -race ./..., go test -short ./... (skips e2e).
  • make run — with Claude Code adapter; make run-mock — with the demo agent (no Claude/network needed).
  • make template — builds the k33g/ori sandbox template image (template/Dockerfile, FROM docker/sandbox-templates:claude-code); launch with sbx run -d claude <project> --template k33g/ori:0.0.2 --kit docker.io/k33g/ori-kit:latest --name ori -p 8888:8888/tcp (detached — see the auto-stop trap below) (see kits/ori/README.md and docs how-to run-in-a-sandbox).
  • scripts/launch-ori.applescript (2026-09-18) — macOS launcher: sbx ls -q → restart by name or create (k33g/ori:0.0.2, ./kits/ori, -p 5555:8888/tcp), poll http://localhost:5555/healthz up to 90 s, then open ori-desktop/build/bin/ori-desktop.app. Run with osascript, or osacompile into scripts/Launch Ori.app; repo root derived from the script's own location. Compile-checked and handlers unit-tested; full end-to-end run not yet done.
  • Kit publishing (sbx v0.43.0, verified 2026-09-18): sbx kit validate kits/ori, sbx kit pack kits/ori -o ori-kit.zip, sbx kit push kits/ori docker.io/k33g/ori-kit:<tag> — directory argument, not spec.yaml; there is no sbx kit package. Auth: sbx login session, then sbx secret set --registry, then the Docker credential store.
  • Kit packaging trap (verified 2026-09-18): pack/push decode spec.yaml raw (no ${{ kit.args }} expansion) — an arg placeholder in an integer field (ports[].container) breaks them even though validate/run accept it. ports[0].container is therefore a literal 8888; the port arg only drives the startup command and agentInstructions.
  • Published artefacts (2026-09-18): template k33g/ori:0.0.2 on Docker Hub (multi-arch amd64+arm64, pushed by the user with ./template/build.sh, ships only claude-agent-acp); kit docker.io/k33g/ori-kit:latest re-pushed with --agent-cmd claude-agent-acp + template 0.0.2. They must move together: the first hello-world sandbox paired image 0.0.2 with the morning's kit (claude-code-acp) and ori died with "executable file not found".
  • Port publishing traps: -p H:8888 without protocol binds tcp4 only — browsers resolving localhost to ::1 fail; always -p H:8888/tcp (dual-stack). Chrome refuses ports 6665–6669 (ERR_UNSAFE_PORT) whatever the server does. sbx ports NAME lists; sbx ports ls looks for a sandbox called ls.
  • sbx auto-stop trap (verified in the sbx source + daemon log, 2026-09-17): sandboxd stops a sandbox 30 s (hard-coded WithAutoStopDelay(30*time.Second), sandboxd/pkg/server/backend_dockernext.go) after its last CLI "sentinel" session closes; sbx create/sbx exec/sbx run all hold one for their own duration, browser traffic on a published port counts for nothing. Only a sandbox created with the spec field detached: true (CLI: sbx run -d …, absent from sbx create) is exempt; the field is create-time only (no PATCH), so an existing sandbox must be sbx rm'd and recreated with sbx run -d. No settings/env knob for the delay.
  • Quality: python3 .claude/skills/quality/scripts/quality_report.py --workspace . (the script lives in the repo, not ~/.claude). Gate PASS, run #18, 2026-09-18: 0 errors, 0 warnings, 0 smells, complex 532 (covers the adapter switch, the MIME pin, the copy-paste feature). Mac trap: .qlty/{logs,out,plugin_cachedir,results} are symlinks qlty creates into its cache; ones created inside the sandbox point at /home/agent/… and dangle on the Mac, making every plugin invocation "FATAL" with no detail — delete the dangling links, qlty recreates them. .qlty/qlty.toml pins golangci-lint 2.13.2 (built with Go 1.27): qlty's default 1.61.0 and its latest-known 2.6.2 refuse this repo's go 1.26.5.
  • docs/source-code-analysis.adoc — AsciiDoc snapshot analysis of the codebase (2026-09-17), outside the bilingual Diátaxis set.

Environment facts (sandbox)

  • ~/.qlty is a dedicated 488MB volume, too small for qlty's tool cache; the cache was moved to ~/.qlty-cache on the main disk with a symlink ~/.qlty/cache -> /home/agent/.qlty-cache. If qlty reports "No space left on device", check this symlink survived. (It did NOT survive a sandbox recreation on 2026-09-17: the volume came back empty and the first run failed exactly that way; recreating the symlink fixed it.)
  • ~/.npm is the same kind of 488MB dedicated volume and filled up when installing Monaco; the npm cache now lives at ~/.npm-big via NPM_CONFIG_CACHE exported in /etc/sandbox-persistent.sh.
  • Running ori inside this Claude-Code-driven sandbox requires env -u CLAUDECODE ./bin/ori (Claude refuses nested sessions). With the current template/kit (claude-agent-acp, bundled CLI 2.1.274) no CLAUDE_CODE_EXECUTABLE is needed. It remains the lever if something spawns the old Zed adapter (points it at another claude binary) — but the template's own CLI is 2.1.246 (base image 2026-08-26), below the 2.1.251 floor the API enforces for claude-fable-5-1[1m] (host ~/.claude/settings.json), so claude update inside the sandbox would come first. Handoffs 2026-09-18-claude-version-too-old.md, 2026-09-18-claude-agent-acp-adapter.md.
  • npm blocks install scripts by default here; esbuild's postinstall was approved via npm approve-scripts (recorded in ui/package.json allowScripts).
  • The Wails GUI toolchain is installable here (verified 2026-09-18): go install github.com/wailsapp/wails/v2/cmd/wails@v2.16.0 then sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev (~15 GB free on /, plenty). wails doctor still reports libwebkit: Not Found because it only looks for webkit2gtk-4.0 — ignore it; pkg-config --modversion webkit2gtk-4.1 → 2.52.6 is what matters. These are apt packages on the container disk, so a sandbox recreation loses them.

Known limitations

  • --agent-cmd is split on whitespace; arguments containing spaces need a wrapper script.

  • One prompt turn at a time; a second prompt while a turn runs is rejected with an error event.

  • Terminal ACP methods are not supported (declared absent; answered with JSON-RPC method-not-found).

  • user_message / thought / message chunks only render text content blocks; images/audio/resources are not rendered yet; user_message.attachments are not rendered as chips (only the @path text mention).

  • Skills list is fetched once per page load; the file search walks whatever sits under --cwd (an untracked sandboxes/ clone at the repo root gets walked).

  • Licence: MIT, LICENSE at the repository root (added 2026-09-18, holder k33g — the name used in the tickets and git identity). No per-module licence file; ori-desktop/ is covered by the root one. Nothing references it yet from the READMEs or ui/package.json.

  • Git: remote origin = https://git.rickub.com/bots-garden/ori.git (Gitea-style self-hosted, not GitHub). main fast-forwarded to feat/acp-webapp at commit 76d62ac on 2026-09-17; not pushed yet. hello.md (a story generated during an ori test) and .claude/settings.local.json are deliberately left untracked.

Not yet established

  • ori-desktop: the window was launched by the user on their Mac (build OK, Check OK); after the CSS specificity fix (#connect-screen outranked .screen[hidden], so the connect screen never hid and the viewer rendered below the fold) it has not yet been confirmed that the iframe actually shows ori inside WKWebView (wails:// origin loading http://localhost; NSAllowsLocalNetworking set in build/darwin/Info.plist).

  • Real Claude Code sessions: exercised by the user on 2026-09-18 (sandbox hello-world, template 0.0.2 + published kit, real prompt answered). Still missing: an automated end-to-end test that drives a real prompt.

Tickets (.tickets/)

  • Managed by the IssueSpec VSCodium extension (k33g.issuespec 0.1.28, author = the user; source clone at ~/CodeBerg/issuespec, format reference docs/en/reference/ticket-files.md). Hand edits are allowed but must keep the schema: a task is id (int ≥ 1, unique in the issue), title, state: open|closed, priority: none|low|medium|high|urgent (always written), author {name,email}, createdAt, optional updatedAt, and closedAt only while closed. The text/done checklist shape is invalid (fixed in ticket 0005 on 2026-09-18; validation recipe in handoff 2026-09-18-ticket-0005-format.md).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# Ori — project summary

## What it is

Ori is a web client for ACP (Agent Client Protocol, https://agentclientprotocol.com) code agents: a Go backend serves an embedded React SPA and connects over ACP (JSON-RPC on stdio) to an agent subprocess — Claude Code via the `@agentclientprotocol/claude-agent-acp` adapter by default (since 2026-09-18). The browser gets a Zed-style agent panel: streamed markdown answers, collapsible thoughts, tool call cards (with statuses, locations and diffs), the agent's plan, and permission prompts answered from the UI. Implements ticket `.tickets/issues/0004-specifications.yaml`.

## Architecture

```
browser ⇆ WebSocket /ws ⇆ Go backend ⇆ stdio (ACP) ⇆ agent subprocess
```

- Go module: `rickub.com/bots-garden/ori` (NOT github — the repo is not hosted on GitHub; the user corrected this explicitly).
- `cmd/ori` — server binary; flags `--addr` (default `:8888`), `--cwd`, `--agent-cmd` (whitespace-split).
- `cmd/ori-mock-agent` — deterministic demo ACP agent on stdio (thought → plan → read tool call → permission → edit tool call with diff → answer); used by `make run-mock` and the e2e test.
- `internal/config` — flag parsing/validation.
- `internal/httpserver` — embedded SPA (via `ui/embed.go`, `go:embed all:dist`), `/healthz`, SPA fallback, extra-route injection point for `/ws`.
- `internal/agent` — spawns the agent, drives the ACP lifecycle through `github.com/coder/acp-go-sdk` v0.13.5 (community SDK listed by the official ACP docs; the zed-industries repo itself has NO Go library); generic `Handler` interface (session updates + blocking permission requests); real fs read/write; terminals refused (capability not announced).
- `internal/files` — workspace file API (`GET /api/files`, `GET /api/file`, `PUT /api/file`, plus `GET /api/files/search?q=&limit=` — recursive, case-insensitive, skips `.git`/`node_modules`, default 50 / max 500 results, walk capped at 50 000 entries — and `GET /api/raw?path=` streaming bytes with pinned image Content-Types, Range support, no size cap): list/read/write, relative paths against `--cwd`, absolute allowed; access deliberately unrestricted (user's choice: ori targets already-isolated sandboxes); binary → 415, >5MiB → 413.
- `internal/terminal` — one shell per WebSocket on `GET /ws/terminal` in a PTY (`creack/pty` v1.1.24); binary frames out, JSON `input`/`resize` frames in; shell from `$SHELL`→bash→sh.
- `internal/skills``GET /api/skills`: discovers `<cwd>/.claude/skills/*/SKILL.md` and `~/.claude/skills/*/SKILL.md` (frontmatter `name`/`description`, directory-name fallback, project shadows user, sorted).
- `internal/bridge` — hub between agent and browsers: JSON protocol (see `docs/*/reference/websocket-protocol.md`), broadcast with per-client buffers (slow client = dropped), append-only history (cap 4096) replayed on every connection, `user_message` echo so the server is the single source of truth, permission routing by requestId (pending requests replayed to late joiners; agent-side ctx cancellation handled). The `prompt` message carries optional `attachments: [{path,name}]`, turned server-side into ACP `resource_link` blocks (`file://<abs>`) next to the text block — `Prompter.Prompt(ctx, []acp.ContentBlock)` replaced `PromptText`; relative paths are joined to the root given by `Bridge.SetWorkspaceRoot` (wired in `cmd/ori/main.go`); `user_message` echoes the attachments. Uses `github.com/coder/websocket` v1.8.15; origins restricted to localhost.
- `ui/` — Vite + React 19 + TS strict; zustand store around a pure reducer (`ui/src/reducer.ts`, handler-table style); WS client with backoff reconnect; thread resets on `hello` so replays are idempotent; unknown ACP update kinds ignored by design. Workspace panel (separate zustand store `ui/src/workspace.ts`): file tree (click=preview, double-click=edit; column resizable via `ResizeHandle` — ARIA separator, pointer drag / arrows / Home-End / double-click reset — width `fileTreeWidth` in the store, bounded 120–800 px, default 224, applied through the `--filetree-width` CSS custom property, persisted in localStorage `ori.fileTreeWidth`). Colour theme (`ui/src/theme.ts`, own zustand store): `light` (default) | `dark`, stamped as `data-theme` on `<html>` at module load and on change, remembered in localStorage `ori.theme`, toggled by the header ☾/☀ button; `app.css` keys the dark palette on `:root[data-theme="dark"]` (the former `prefers-color-scheme` media query is gone — decision: explicit choice, light default), `MonacoViewer` reads the store (`vs-dark`/`light`), the terminal stays dark. Preview tab (Monaco read-only for code — bundled workers, no CDN, lazy chunk ~1MB gzip; rendered Markdown; rendered AsciiDoc via lazily-imported `@asciidoctor/core`, Rendered/Source toggle), Editor tab (Monaco, dirty ●, Save/Ctrl+S via PUT /api/file), Terminal tab (xterm.js on /ws/terminal, starts on first activation); inactive panes are hidden not unmounted so shell/edit state survives tab switches; `execute` tool-call output renders in a monospace `<pre>`, never markdown. Composer completions (`ui/src/mentions.ts` pure helpers, `ui/src/useCompletion.ts`, `CompletionPopup`): `@` opens a workspace file search (→ attachments), `/` opens a merged list of local skills (`/api/skills`) and ACP `available_commands_update` commands (stored as `commands` in the reducer, cleared on `hello`; deduped by name, skill wins) and inserts `/<name> ` as plain text — the text is the invocation mechanism, there is no ACP "run command" method. Preview kinds `image` (`ImageView`, `<img>` over checkerboard via `/api/raw`, natural size shown) and `drawio` (`DrawioView`: iframe on `embed.diagrams.net` fed by postMessage, Rendered/Source toggle, 8 s timeout → Source fallback; `.drawio.svg/.png` are images). The mock agent emits `available_commands_update` (`review`, `compact`) on session start and echoes `[attached: name]`.
- `ori-desktop/` — separate Go module `rickub.com/bots-garden/ori-desktop` (Wails v2.16.0, Go ≥ 1.25, "plain" template, `//go:embed all:frontend/src`, no npm; `frontend/src/main.js` is loaded as an ES module — `type="module"` — since the 2026-09-17 lint cleanup): connection screen (URL remembered in `<UserConfigDir>/ori-desktop/settings.json`, key `serverUrl`; auto-connect at startup), `/healthz` probe via bound Go methods (GetConfig, SaveConfig, CheckHealth, Connect, OpenInBrowser, SettingsPath), then the ori SPA loaded in an `<iframe>` (Wails v2 cannot navigate the main webview to an external URL; `runtime.BrowserOpenURL` is the fallback). Ticket 0005. `make desktop` / `make desktop-test`; root `go test ./...` unaffected (nested module). **On Linux, `wails build` needs `-tags webkit2_41`** (Ubuntu 26.04 ships only `webkit2gtk-4.1`; the plain command dies on `Package 'webkit2gtk-4.0' not found`). `make desktop` does NOT pass that tag, so it fails in this sandbox — run `cd ori-desktop && wails build -tags webkit2_41` instead. Build outputs coexist in `build/bin/`: `ori-desktop` (Linux ELF), `ori-desktop.app` (macOS, built on the Mac), `ori-desktop.exe` (Windows cross-build); Wails cannot cross-compile to macOS from Linux. Build instructions for the three platforms: `ori-desktop/how-to-build.md` (2026-09-18, EN only — no `.fr.md` counterpart yet, and no README links to it).

## Key decisions in force

- Claude Code is reached through `@agentclientprotocol/claude-agent-acp` (default `npx -y @agentclientprotocol/claude-agent-acp`, since 2026-09-18; `internal/config.DefaultAgentCommand`, pinned by `TestDefaultAgentCommandIsClaudeAgentACP`). It bundles its own Claude Code CLI via `@anthropic-ai/claude-agent-sdk` 0.3.274 → CLI 2.1.274 delivered as per-platform optional deps (`…-sdk-linux-arm64`, `-linux-x64`, …; no `cli.js` any more). Replaced Zed's `@zed-industries/claude-code-acp` 0.16.2, whose embedded CLI 2.1.44 the API rejects for current models ("version 2.1.251 or newer is required"). The `claude` CLI itself has no native ACP mode; verified.
- draw.io diagrams render online via `embed.diagrams.net` (no bundlable offline renderer exists: `viewer.min.js` is not on npm, `mxgraph` is archived and lacks draw.io's shapes, `drawio2svg` is GPL); future offline path = vendoring `viewer-static.min.js` (~3 MB, Apache-2.0). Documented in the architecture explanations.
- ACP payloads cross the WebSocket verbatim (raw `SessionUpdate` / `RequestPermissionRequest`), keeping the front forward-compatible.
- One agent session shared by all browsers in v1; multi-session is a planned evolution seam (`bridge.Prompter` interface, per-connection subscriptions).
- The server owns the thread (history + user_message echo); the SPA rebuilds from replay on every (re)connection.
- Quality config decisions (all user-approved): `.claude/**` excluded from qlty (vendored kit sources, the measurement instrument itself); `radarlint-python` plugin removed (its ~300MB JDK cannot fit the dedicated 488MB `~/.qlty` volume — diagnosis in `.qlty/qlty.toml` comments); `golangci-lint` + `biome` added manually (init missed both languages); `biome.json` at root parses `.vscode/**` as JSONC.

## Build / test / run

- `make deps` — npm install in `ui/`.
- `make build` — SPA build then Go binaries (`bin/ori`, `bin/ori-mock-agent`). Order matters: the Go build embeds `ui/dist`.
- `make test``go test ./...` + `vitest run` (the ui `test` script sets `NODE_OPTIONS=--no-experimental-webstorage`: Node 25's built-in `localStorage` lacks `clear()` and shadows jsdom's — 15 store tests fail without it; verified 2026-09-18 on Node 25.9). Also useful: `go test -race ./...`, `go test -short ./...` (skips e2e).
- `make run` — with Claude Code adapter; `make run-mock` — with the demo agent (no Claude/network needed).
- `make template` — builds the `k33g/ori` sandbox template image (`template/Dockerfile`, FROM `docker/sandbox-templates:claude-code`); launch with `sbx run -d claude <project> --template k33g/ori:0.0.2 --kit docker.io/k33g/ori-kit:latest --name ori -p 8888:8888/tcp` (detached — see the auto-stop trap below) (see `kits/ori/README.md` and docs how-to `run-in-a-sandbox`).
- `scripts/launch-ori.applescript` (2026-09-18) — macOS launcher: `sbx ls -q` → restart by name or create (`k33g/ori:0.0.2`, `./kits/ori`, `-p 5555:8888/tcp`), poll `http://localhost:5555/healthz` up to 90 s, then `open ori-desktop/build/bin/ori-desktop.app`. Run with `osascript`, or `osacompile` into `scripts/Launch Ori.app`; repo root derived from the script's own location. Compile-checked and handlers unit-tested; full end-to-end run not yet done.
- Kit publishing (sbx v0.43.0, verified 2026-09-18): `sbx kit validate kits/ori`, `sbx kit pack kits/ori -o ori-kit.zip`, `sbx kit push kits/ori docker.io/k33g/ori-kit:<tag>` — directory argument, not `spec.yaml`; there is no `sbx kit package`. Auth: `sbx login` session, then `sbx secret set --registry`, then the Docker credential store.
- **Kit packaging trap (verified 2026-09-18)**: `pack`/`push` decode `spec.yaml` raw (no `${{ kit.args }}` expansion) — an arg placeholder in an integer field (`ports[].container`) breaks them even though `validate`/`run` accept it. `ports[0].container` is therefore a literal 8888; the `port` arg only drives the startup command and agentInstructions.
- Published artefacts (2026-09-18): template `k33g/ori:0.0.2` on Docker Hub (multi-arch amd64+arm64, pushed by the user with `./template/build.sh`, ships only `claude-agent-acp`); kit `docker.io/k33g/ori-kit:latest` re-pushed with `--agent-cmd claude-agent-acp` + template 0.0.2. **They must move together**: the first `hello-world` sandbox paired image 0.0.2 with the morning's kit (`claude-code-acp`) and ori died with "executable file not found".
- **Port publishing traps**: `-p H:8888` without protocol binds `tcp4` only — browsers resolving `localhost` to `::1` fail; always `-p H:8888/tcp` (dual-stack). Chrome refuses ports 6665–6669 (`ERR_UNSAFE_PORT`) whatever the server does. `sbx ports NAME` lists; `sbx ports ls` looks for a sandbox called `ls`.
- **sbx auto-stop trap (verified in the sbx source + daemon log, 2026-09-17)**: sandboxd stops a sandbox 30 s (hard-coded `WithAutoStopDelay(30*time.Second)`, `sandboxd/pkg/server/backend_dockernext.go`) after its last CLI "sentinel" session closes; `sbx create`/`sbx exec`/`sbx run` all hold one for their own duration, browser traffic on a published port counts for nothing. Only a sandbox created with the spec field `detached: true` (CLI: `sbx run -d …`, absent from `sbx create`) is exempt; the field is create-time only (no PATCH), so an existing sandbox must be `sbx rm`'d and recreated with `sbx run -d`. No settings/env knob for the delay.
- Quality: `python3 .claude/skills/quality/scripts/quality_report.py --workspace .` (the script lives in the repo, not `~/.claude`). Gate PASS, run #18, 2026-09-18: 0 errors, 0 warnings, 0 smells, complex 532 (covers the adapter switch, the MIME pin, the copy-paste feature). **Mac trap**: `.qlty/{logs,out,plugin_cachedir,results}` are symlinks qlty creates into its cache; ones created inside the sandbox point at `/home/agent/…` and dangle on the Mac, making every plugin invocation "FATAL" with no detail — delete the dangling links, qlty recreates them. `.qlty/qlty.toml` pins golangci-lint 2.13.2 (built with Go 1.27): qlty's default 1.61.0 and its latest-known 2.6.2 refuse this repo's `go 1.26.5`.
- `docs/source-code-analysis.adoc` — AsciiDoc snapshot analysis of the codebase (2026-09-17), outside the bilingual Diátaxis set.

## Environment facts (sandbox)

- `~/.qlty` is a dedicated 488MB volume, too small for qlty's tool cache; the cache was moved to `~/.qlty-cache` on the main disk with a symlink `~/.qlty/cache -> /home/agent/.qlty-cache`. If qlty reports "No space left on device", check this symlink survived. (It did NOT survive a sandbox recreation on 2026-09-17: the volume came back empty and the first run failed exactly that way; recreating the symlink fixed it.)
- `~/.npm` is the same kind of 488MB dedicated volume and filled up when installing Monaco; the npm cache now lives at `~/.npm-big` via `NPM_CONFIG_CACHE` exported in `/etc/sandbox-persistent.sh`.
- Running ori inside this Claude-Code-driven sandbox requires `env -u CLAUDECODE ./bin/ori` (Claude refuses nested sessions). With the current template/kit (`claude-agent-acp`, bundled CLI 2.1.274) no `CLAUDE_CODE_EXECUTABLE` is needed. It remains the lever if something spawns the old Zed adapter (points it at another `claude` binary) — but the template's own CLI is 2.1.246 (base image 2026-08-26), below the 2.1.251 floor the API enforces for `claude-fable-5-1[1m]` (host `~/.claude/settings.json`), so `claude update` inside the sandbox would come first. Handoffs `2026-09-18-claude-version-too-old.md`, `2026-09-18-claude-agent-acp-adapter.md`.
- npm blocks install scripts by default here; esbuild's postinstall was approved via `npm approve-scripts` (recorded in `ui/package.json` `allowScripts`).
- The Wails GUI toolchain is installable here (verified 2026-09-18): `go install github.com/wailsapp/wails/v2/cmd/wails@v2.16.0` then `sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev` (~15 GB free on `/`, plenty). `wails doctor` still reports `libwebkit: Not Found` because it only looks for `webkit2gtk-4.0` — ignore it; `pkg-config --modversion webkit2gtk-4.1` → 2.52.6 is what matters. These are apt packages on the container disk, so a sandbox recreation loses them.

## Known limitations

- `--agent-cmd` is split on whitespace; arguments containing spaces need a wrapper script.
- One prompt turn at a time; a second `prompt` while a turn runs is rejected with an error event.
- Terminal ACP methods are not supported (declared absent; answered with JSON-RPC method-not-found).
- `user_message` / thought / message chunks only render text content blocks; images/audio/resources are not rendered yet; `user_message.attachments` are not rendered as chips (only the `@path` text mention).
- Skills list is fetched once per page load; the file search walks whatever sits under `--cwd` (an untracked `sandboxes/` clone at the repo root gets walked).

- Licence: MIT, `LICENSE` at the repository root (added 2026-09-18, holder `k33g` — the name used in the tickets and git identity). No per-module licence file; `ori-desktop/` is covered by the root one. Nothing references it yet from the READMEs or `ui/package.json`.

- Git: remote `origin` = `https://git.rickub.com/bots-garden/ori.git` (Gitea-style self-hosted, not GitHub). `main` fast-forwarded to `feat/acp-webapp` at commit `76d62ac` on 2026-09-17; **not pushed** yet. `hello.md` (a story generated during an ori test) and `.claude/settings.local.json` are deliberately left untracked.

## Not yet established

- ori-desktop: the window was launched by the user on their Mac (build OK, Check OK); after the CSS specificity fix (`#connect-screen` outranked `.screen[hidden]`, so the connect screen never hid and the viewer rendered below the fold) it has not yet been confirmed that the iframe actually shows ori inside WKWebView (`wails://` origin loading `http://localhost`; `NSAllowsLocalNetworking` set in `build/darwin/Info.plist`).

- Real Claude Code sessions: exercised by the user on 2026-09-18 (sandbox `hello-world`, template 0.0.2 + published kit, real prompt answered). Still missing: an automated end-to-end test that drives a real prompt.

## Tickets (`.tickets/`)

- Managed by the **IssueSpec** VSCodium extension (`k33g.issuespec` 0.1.28, author = the user; source clone at `~/CodeBerg/issuespec`, format reference `docs/en/reference/ticket-files.md`). Hand edits are allowed but must keep the schema: a task is `id` (int ≥ 1, unique in the issue), `title`, `state: open|closed`, `priority: none|low|medium|high|urgent` (always written), `author {name,email}`, `createdAt`, optional `updatedAt`, and `closedAt` only while closed. The `text/done` checklist shape is **invalid** (fixed in ticket 0005 on 2026-09-18; validation recipe in handoff `2026-09-18-ticket-0005-format.md`).