Quests for Ryu — an auto-detecting todo list surfaced from your chats and activity, tracked as a lightweight quest board.
The public home of
ryu-quests. Source, builds, and releases live here — binaries for every platform are attached to each release.This tree is generated from the Ryu monorepo, so commits pushed here directly are replaced on the next sync. Pull requests are welcome — open them here and they are ported into the monorepo, then flow back out. Ryu as a whole: https://github.com/amajorai/ryu
- Binary:
ryu-questsfrom the Ryu releases. - Crate:
cargo install ryu-quests.
Apache-2.0 — see LICENSE.
An auto-detecting todo list and a keep pile for AI-assisted work. Two item families share one board, and the split between them is load-bearing:
- Tasks (
kind: "task") — a task with a natural-language completion condition. On a schedule the engine gathers what the user has recently been doing from Shadow's always-on context (screen text / activity / semantic history) and asks a judge model whether the task looks done, then either suggests completion (a chip the user confirms) or auto-completes it, per the configured detection mode. - Captures (
kind: "note" | "link" | "prompt" | "snippet") — the answers, links, code fragments, and follow-up prompts collected while moving between chats and editors. A capture keeps itsbodyverbatim plus thesourceit came from (app / window title / URL), so a kept quote never becomes an orphan.
A capture has no completion condition and stays status: "open" forever. If the detection
sweep did not gate on the kind it would judge every kept note on every tick, forever —
a model call per item per interval, producing a meaningless verdict each time. Three places
enforce this and must move together:
QuestKind::is_judged— the single predicate.judge_quest_with_contextreturnsOk(None)for a non-task before it reaches a model.sync_job/capture/update_questskipsync_backing_jobentirely for a capture — not "call it withopen: false", which would still write a permanently-disabled job row per kept item.
captures_are_never_judged_and_carry_no_backing_job covers all three.
backend/(ryu-quests) — an extracted Core capability crate:QuestEngine, the SQLiteQuestStore, event types, and the/api/quests/*HTTP surface. Now served OUT-OF-PROCESS by theryu-questsbin ([[bin]],kind:local,public_mount,RYU_QUESTS_BIN/RYU_QUESTS_PORT, default:7991); Core links zero quest code (no path-dep, noquestscargo feature). Its three reverse-couplings — the scheduler judge run, theJobTarget::Questjob lifecycle, and the activity feed — reach the sidecar over loopback viaapps/core/src/quests_client.rs. Everything the engine needs from the host is inverted through theQuestsHosttrait, so the crate has zero dependency onapps/core.ui/(@ryu/quests-app) — the companion surface: a React app built to one self-contained HTML viavite-plugin-singlefile, consuming@ryu/ui. Shipped as a full-page Companion (Path B,ui_format: "html").
- id
@ryu/quests· companionQuests(icontarget). - grant
quests:crud— the bridge capability the UI drives Core's/api/quests/*through. - permission levels
quests.view⊂quests.edit⊂quests.capture. Capture is its own level because keeping text selected in another app is a wider reach than editing the board. - contributes a declarative
quest-boardlist-detailview (GET/api/quests, aCompleteitem action) for hosts that render manifest views directly, plus thequest.created/quest.captured/quest.suggested/quest.completedhook events.
/api/quests (list/create, ?kind= filter) · /api/quests/capture · /api/quests/events
(SSE) · /api/quests/scratchpad (GET/PUT) · per-quest judge, complete, dismiss,
reopen, use, pin, and suggestion/{accept,dismiss}.
Every one of those paths is also listed in sidecars[0].http.routes — that array is the
ext-proxy allowlist, and a route missing from it is unreachable from outside the process no
matter what the router says.
The companion frame has connect-src 'none', so it reaches Core only through
window.ryu.quests.*. Adding an endpoint therefore means adding a verb, and the verb
table has one source of truth — crates/core/kernel-contracts/src/host_api.rs
(HOST_API_METHODS), from which schemas/host-api.json is regenerated with
RYU_REGEN_SCHEMAS=1 and both the TS host and Core's Rust bridge derive their maps. The
capture work added quests.capture, quests.use, quests.pin, quests.scratchpad, and
quests.setScratchpad, which meant touching, in order:
host_api.rs(+ regen the JSON),packages/app-host/src/rpc-tables.test.ts— the frozen lockstep fixture,packages/app-host/src/rpc.ts—HostServices+ dispatch + arg narrowers,packages/app-host/src/third-party-plugin.ts— the injectedwindow.ryusurface,packages/core-client/src/quests.tsandapps/desktop/src/lib/api/quests.ts— clients,- the two host closure sites (
PluginHostPanel.tsx,plugin-app-mount.tsx).
Select text in any app, tap Shift twice, and it lands on the board with the app,
window title, and URL it came from. The trigger lives in the desktop app
(apps/desktop/src-tauri/src/quick_capture/), NOT here, and reaches this sidecar
through Core's public mount — POST /api/quests/capture — so Core carries no
gesture code and this satellite carries no OS code.
tauri-plugin-global-shortcut cannot express it: a registered accelerator needs a
non-modifier key code, and a double tap needs keydown/keyup timing a shortcut
callback never sees. It has to be a CGEventTap, and the desktop app is the process
macOS attributes the TCC grants to.
Settings → Keyboard Shortcuts → Quick capture reports each one separately, because "it doesn't work" with no reason is this feature's worst failure mode:
- Input Monitoring — gates
CGEventTapCreate. Missing ⇒ the gesture never fires at all. (NOT Accessibility, which is what gatesNSEvent's global monitor; getting these two confused yields a null tap and silence.) - Accessibility — gates the
AXSelectedText/ focused-window reads. Missing ⇒ captures still work through the ⌘C fallback, and still record which app they came from (that comes fromNSWorkspace, which is ungated); what is lost is the window title and the page URL. - This app being enabled —
@ryu/questsis default-OFF, so the POST 404s until the user turns it on from the Store. The status row says so in those words.
Typing Hello World presses Shift twice in a few hundred ms. gesture.rs is a pure,
fully-tested state machine that fires only on a clean double tap: a non-modifier key
between the taps cancels, another modifier cancels, a hold (rather than a tap)
cancels, and the two taps must be the same physical key (keycode 56 vs 60 — the
modifier flags cannot tell left from right).
AXSelectedText first: instant and non-destructive. It is empty in many of the apps
this targets (browser and Electron UIs often do not publish it), so the fallback
synthesizes ⌘C, watches NSPasteboard.changeCount, and restores the previous
clipboard — tapping Shift twice must never eat what the user had copied.
The quests.capture bridge verb has its own grant, split out of quests:crud,
because keeping text the user selected in another app is a different reach than
editing the board. @ryu/approvals holds quests:crud for the inbox's task
check-off and deliberately does not hold this one. Existing installs gain the new
grant through plugins::seed::backfill_declared_grants (additive; skips disabled
records, since disabling revokes consent).
Note the desktop gesture does not go through that bridge — it is a Rust process
calling Core over HTTP with the node token — so the grant governs third-party apps
calling quests.capture, not the gesture.