Skip to content

open-agent-connect-dsh

Verified

open-agent-connect-dsh · v0.9.0 · MIT · Web UI

DSH profile-bundle plugin for Open Agent Connect: five Settings sections, one MetaBot = one agent preset, CLI-backed UI.

Install

dsh plugin add open-agent-connect-dsh

Confirm the layer applied with dsh --profile default --dump-config — see the install guide.

Source

Creators

Readme

Open Agent Connect for DeepSeek Harness

DSH profile-bundle plugin for Open Agent Connect. Installed like better-sidebar:

dsh plugin --profile web add open-agent-connect-dsh

End-user install, Node >=20 <25, first Bot, and first chat: docs/hosts/dsh.md.

Host kernel requirement: this plugin is built and verified against the DSH 0.1.7-rc.2 client surface (npm next dist-tag) and runs on the 0.1.5, 0.1.6, and 0.1.7 kernel lines — most peer ranges are ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1, with the wider ^0.1.2-alpha.2 || ^0.1.3-alpha.1 spans kept on the packages the preset/persona surface still shares. The same build serves the DSH desktop app (Electron, 0.1.7-rc.2+): it runs profile desktop (~/.dsh/profiles/desktop) composed from the same web-app bundle, so webServer/webRuntime are present, the platform: "web" client bundle loads, and every slot injection behaves as in the browser. Install it from the app's plugin manager, or from a shell with dsh plugin --profile desktop add open-agent-connect-dsh. The 0.1.7 adaptations worth knowing about:

  • Directory presets (~/.dsh/.agent-presets, agentPresets.copy/read/remove) are gone: presets are declarative in-memory PresetDefinitions registered through agentPresets.register (feature-detected — the same plugin build keeps the directory backend on ≤0.1.6). Each oac-<slug> definition's plugins list is re-read from the host's CURRENT shipped @deepseek-ai/dsh-web-app/presets/standard.patch.yml on every apply, with the persona row rewritten to the Bot persona prefix; a content-identical definition is left mounted, a drifted one is unregistered then re-registered. Registrations are process-local, so a kernel restart simply re-declares them; the legacy shared oac preset and the agent-presets.default settings heal are pre-0.1.7 artifacts and do not run on this backend.
  • Icons moved from pixel-size names (IconSendOutline14) to stroke weights (IconSendOutlineRegular) with a 14px default size. All icon imports go through src/client/icons.ts, which resolves the modern name first, falls back to the legacy one, and restores the legacy default pixel size.
  • agent.cancel({ kind: 'timeout' }) now throws; timeout cancellations use { kind: 'hook', reason: 'timeout' } (legal on 0.1.6 too).
  • Session format v4 retired the { kind: 'plugin', plugin } message-source wrapper: durable messages require a producer-owned kind. Plugin-injected user messages go through src/message-source.ts, which emits plugin:oac-dsh on 0.1.7 (the same identity the v3→v4 migrator rewrites the old wrapper to) and keeps the wrapper on ≤0.1.6, whose v2→v3 migration admission rejects kinds outside its fixed vocabulary.
  • The same wrapper retirement renames every first-party context injection (time-context, compact-checkpoint, schedule, user-approval, …), so the post-turn memory extraction no longer blocklists plugin kinds — it mirrors a user/message only when source.kind === 'user', the single kind genuine human input carries on every supported host.
  • Turn output is consumed from the live session/event stream, never read back from the log: DSH deprecated the synchronous Session reads (snapshotEvents/eventAt/ownEvents) because the log is slated to move behind storage I/O. Each runner (host executor, delegation, group-task worker, schedule tick) taps the firehose for its session before the turn starts (src/session-event-tap.ts), and the memory extractor accumulates the in-flight turn's texts event by event. The deprecated read remains only as a cold fallback for contexts without the cordis event surface.
  • The preset roster row drops the trust tier; built-in preset display copy is resolved by id through the DSH-owned settings.agentPreset dictionary.
  • 0.1.7-rc.1 adds a plugin compatibility gate: at install time and at every profile (re)composition the kernel checks each plugin's @deepseek-ai/dsh* peer ranges against the running runtime version (prereleases included) and disables an unsatisfied row with an stderr diagnostic instead of letting it crash the profile. This plugin's peer ranges already cover 0.1.7-rc.2, so install and boot stay silent — keep every range anchored on a prerelease lower bound (^0.1.7-alpha.1, never a bare ^0.1.7, which excludes rc builds). If a future kernel outgrows the ranges, dsh plugin --profile web allow-version open-agent-connect-dsh@<version> --dsh-version <exact-runtime> --accept-risk grants an exact plugin-version × runtime-version exemption, recorded in the profile's compatibility.json.
  • 0.1.7-rc.2 renamed the host hero chip's gate hook (showPresetPicker → developerTools, Developer tools in General settings, default on) and dropped modeSelectionEnabled from the preset roster; the OAC chip registers its own injected face and reads only the roster rows, so neither reaches it. Right-Sidebar tab actions gained bindCommands and tab titles may expose a refreshShortcut — both host-provided; the Bot Browser tab keeps its own header refresh.

The 0.1.6 adaptations worth knowing about:

  • sessions.list no longer carries a current pointer: the current conversation is derived from SessionSummary.retainedBy.mainView, with a legacy-current fallback for 0.1.5 hosts.
  • Every reconcile re-syncs an oac-* preset's composition against the host's CURRENT standard preset, leaving content-identical rows untouched: 0.1.6 renamed workflow-worker-thread to workflow-ptc and stale copied rows hard-fail preset activation (agent-preset/invalid), so pre-0.1.6 installs are healed automatically on first reconcile. The 0.1.3-alpha.2 persona prefix/suffix split is handled the same way as before — the Bot persona becomes the persona row's prefix (the copied suffix is kept), and legacy text-only rows from pre-split presets are healed in place — kernels ≥0.1.3-alpha.2 require prefix and ignore text, so older plugin releases silently lose the Bot persona there.
  • The right-Sidebar guide entry carries a stable id (required by the 0.1.6 tab-type contract, ignored on 0.1.5), and the composer send glyph resolves through the icon compatibility layer (IconSendOutline14 on ≤0.1.6, IconSendOutlineRegular on 0.1.7).
  • On a 0.1.6 client reload the kernel restores right-Sidebar tabs as empty shells (navigation revision 0); the Bot Browser detects that restore and re-opens its home URL so the in-iframe Agent Browser rebuilds its real page state from the daemon.

Session format v3, the ctx.agent removal, and the Inbox API change all miss this plugin: sessions are read only through the in-process session surface (the live session/event stream since the 0.1.7 migration, snapshotEvents() before it), and the assistant/turn event matching skips unknown event kinds, so additive 0.1.6 event types are tolerated. The native-panels UI — the sidebar.panellist/main panel API and the right-Sidebar tab-type API (layout, sidebarRight, sidebarRightTabs services) — exists only on the 0.1.5+ kernel lines, so the web client loads there only. It will not load on 0.1.0-rc-era kernels; hosts still there should stay on plugin 0.3.x until their kernel is upgraded.

After a DSH restart, Settings left nav gains these sibling sections: Bots, Memory, User, Apps, and Traffic (流量 — the account-quota billing panel: mode toggle, balance, free grant, redeem codes, usage, and ledger, backed by metabot traffic *; the Services section is hidden until the service plugin matures; A2A Chat is a center-column overlay panel, and the Bot Browser is a right-Sidebar page tab). The left-rail A2A Chat panellist row is currently hidden (SHOW_A2A_PANELLIST_ROW = false); 线上对话 / 群任务 tabs open the overlay. New conversations pick a Bot from the shadowed agent-preset chip (oac-<slug> rows show the Bot name/avatar; stock DSH presets stay visible), and while a Bot is selected the blank-session hero shows that Bot's 100px avatar and name centered directly above the whale-logo/slogan headline (a DOM mount above the headline — DSH has no slot there; stock presets keep the stock hero). Each private-chat/group-task row carries unread dots: new incoming activity marks, opening the conversation clears, and the feed is push-only (see chat/events/all below).

A2A Chat panel shape. A2A Chat is a shell.overlay entry (id oac-a2a), not a kernel global main panel: it covers the center conversation column ONLY, so the official right Sidebar (Bot Browser included) stays mounted and interactive beside it — a global main panel would unmount that Sidebar. The overlay mirrors the frame's column template, tracking resizes, sidebar collapse, and right-Sidebar open/close exactly. The left-rail panellist row stays in the source (click still capture-intercepted into an overlay toggle, never layout.selectPanel) but is not registered while SHOW_A2A_PANELLIST_ROW is false; navigating to another session closes the overlay — and so does every path back to the conversation column, including 新会话 reusing the already-current blank session (the apply wiring watches sessions.list AND wraps layout.selectPanel(null), since that reuse changes neither) — and the conversation underneath never unmounts (drafts and scroll survive). While the overlay is open the panellist row carries the kernel's selected look (.oac-a2a-row-active, synced by the glyph, aria-current included) and clears it the moment the overlay closes. Every Agent Internet URI clicked inside A2A — private chat or Group Tasks — opens in the right-Sidebar Bot Browser tab, the same reveal every other surface uses.

The panel is a pure reading pane. Since the conversation-list tabs own every list, the overlay carries no header, no 私聊/群任务 mode tabs, and no list columns: what renders is the private thread full-width (Steer composer included; the empty state shows a hint plus the new-conversation composer that starts a brand-new chat) or the selected group task's detail (GroupTaskView in detail-only mode — info, members, deliverables, transcript, and the task drawer; its empty state points at the left list). Navigation arrives as one-shot targets on the panel store: an online row opens its thread, a task row opens that task, a collab row opens the guest transcript, and the 群任务 tab's + button opens the create-task modal (a grouptask target with an empty task key). Nothing auto-selects — with the lists on the left, the panel shows exactly what was clicked.

Conversation-list tabs (本地对话 / 线上对话 / 群任务). IDBots parity: the left conversation list carries a three-cell tab strip — 本地对话 (local DSH sessions, the stock browsing region), 线上对话 (A2A private conversations), and 群任务 (group tasks) — with unread dots on the two OAC tabs and the choice persisted in localStorage. DSH has no slot above the browsing region (sidebar.workspaces is one single-kind cell), so the strip mounts through the DOM like the hero Bot identity, but on the slot renderer's own [data-slot="sidebar.workspaces"] wrapper — a stable, non-hashed anchor. The local tab renders nothing but the strip: the official region is never re-hosted or unmounted, only hidden by one namespaced <html> class (oac-conv-tabs-active) while 线上对话/群任务 show their OAC lists in its place (both reuse the A2A panel's row vocabulary, live SSE refresh, and the unread feed; the online list keeps its own remembered from-Bot). These lists are THE list surfaces for A2A chat (the center overlay is a pure reading pane): rows carry the IDBots hover menu (Copy Session ID / Rename / Pin / Archive — group-task archive asks first), and clicking one opens the A2A Chat overlay pre-positioned on that conversation, task, or OpenTeam collaboration through a one-shot target on the panel store (applied and consumed on arrival; close drops stale targets). The 群任务 tab also owns the engine-health note, the pending staffing slate (the owner's confirm/revise/skip surface), the OpenTeam guest collaborations list, and the + button that opens the panel's create-task modal. Session navigation — including 新会话 — returns to 本地对话, and every failure path fails safe: a missing anchor, a crashed surface (ErrorBoundary releases the mount), or a collapse to the 56px rail each drop the hiding class and leave the stock region exactly as DSH shipped it.

Chip order and unavailable Bots. The chip lists the available Twin Bot first, then every other row in roster order, with local Bots sorted among themselves by profile creation time (oldest first — the newest Bot lands at the bottom of the Bot block). A Bot is unavailable when its Settings → Bots availability toggle is off OR no DSH LLM pair is configured (the same rule the daemon's group-task seat search applies). Unavailable Bots are hidden from the chip, excluded from the Twin's local_workers_list roster and refused by local_worker_delegate, skipped by the dream/hygiene and scheduled-task host schedulers, and kept out of group-task staffing end to end: search_candidates never returns them, the Group Tasks create modal's worker picker hides them, propose refuses a plan that seats one (the chair re-picks before the owner sees the slate), and every create — direct or from a proposal — skips an unavailable slug and reports the dropped seats as skippedWorkers (the chat tool prints them, the panel shows an info note); the daemon's own headless scheduled-task and study ticks also skip toggle-off Bots. One legacy cleanup rides the preset reconcile: the shared bare oac preset ("Open Agent Connect (MetaBot)") from pre-per-Bot installs is removed, and an agent-presets.default: oac setting pointing at it heals to standard.

LLM resolution: who generates what

Two LLM chains coexist, and every generation site uses exactly one of them:

  • DSH host chain — the Bot's DSH LLM pair (dshLlmProvider/dshLlmModel plus optional fallback pair, set in Settings → Bots or via metabot bot create/update --dsh-llm-*), executed by the DSH host's ctx.llm. Used for DSH oac-* conversations, dreams, chain-history summaries, scheduled tasks while DSH runs, and twin worker sub-sessions.
  • OAC daemon chain — local LLM CLI runtimes (claude-code, codex, …), auto-discovered at Bot create and bound per Bot. Used by group-task chair turns, memory deep consolidation, and headless scheduled tasks.

Unified passive-LLM priority. Every daemon-side passive turn — A2A private-chat replies (plus guided turns and buyer-rating replies), group-task chair turns, nightly study/Q&A-surf drains, memory deep consolidation, and headless scheduled-task runs — resolves its LLM in the same order:

  1. The Bot's DSH pair, when set and a host executor is connected (DSH running). The plugin holds one long-lived SSE lease on the daemon's /api/llm/host-executor/events stream, executes each pushed generate request, and POSTs the result back. Plain turns run via ctx.llm (primary pair, fallback pair retried once); skill-scoped turns run in agent mode — a real, ephemeral DSH session that reads and executes the allowed skill documents (~/.dsh/skills + .dsh/skills + ~/.agents/skills, which is also exactly what the Bot editor's chat-skills picker lists).
  2. Else the Bot's local CLI bindings.
  3. Else any healthy local runtime in the shared store.
  4. Else fixed template replies.

No scenario prefers the local CLI over the DSH pair. metabot llm host-executor reports the connected-executor count; gate the executor with the cordis.yml config toggle llmExecutor.enabled (default on).

The Bots → edit dialog gains a Knowledge tab (a DSH port of IDBots' KnowledgeBasePanel): one card per document knowledge base — name with a Default badge, description, raw-corpus path, doc/chunk counts with the last learned time, an auto-learn switch, and per-card actions: Learn now (incremental), Import files (browser file input over the raw kb/import route, 19 formats), Edit, Delete (never shown for the default KB), and an Advanced disclosure with the destructive Full rebuild. New KBs need both a name and a description; there is no manual document typing — the corpus is fed by agent tools (knowledge_base_add_document, skill_tool) and file import. The nightly study-jobs status panel (topics assigned via metaweb_study_enqueue) sits below, and the model gets the same registry every turn through the <knowledge_bases> volatile prompt block. The knowledge_base_* tools resolve the acting Bot per call (session oac-* agent first, then the machine-default Twin — the same target a no---from CLI call picks), so they also work from plain DSH conversations, and write results name the profile they landed on. Retrieval quality: queries tokenize to CJK bigrams + latin words with function words dropped, and scoring is absolute (coverage × share of the query's achievable best), so an unrelated query returns an honest empty instead of a high-scored noise hit; knowledge_base_add_document refreshes the index on save, making the document searchable immediately — knowledge_base_learn remains for corpus imports/edits and full rebuilds.

Group Tasks (群任务) and OpenTeam

Group Tasks live in the left 群任务 tab (list) and the A2A overlay (detail, opened by clicking a task row): one on-chain MetaWeb group chat per task, chaired by your Twin Bot. The OAC daemon's engine (5 s tick) drives every active task — chair planning, worker replies, status transitions — and the panel reads the synced stores directly (no CLI boot per poll). The thread head's Task panel toggle opens a floating drawer (the IDBots group-task right rail, ported into the panel): members with work/state badges, collapsible status history, transitions, integrity events (click to jump to the source message), and the deliverables list — kind pill, acceptance status, on-chain confirmation/verification pill, copyable/clickable URI, author — all read from the host-recorded task ledgers and refreshed by the 15 s detail poll.

Single commander (IDBots 2026-09-06 parity). The chair is the ONLY coordinator, and the host never speaks in the group: every group message is authored by a participant — the chair, a worker, or the human owner (the owner can always post via the panel composer). Host observations (a missing [WORKING] ACK, a chair-set [DEADLINE: Nm] that rang, a remote join, a dropped chair tag, chain-backend trouble) are recorded as host notes and delivered to the chair in one dedicated turn ([SYSTEM host environment notes …]); the chair decides what the group needs to hear, in its own voice — or stays silent. Worker sub-sessions speak mid-turn through a session-scoped group_chat tool (bound to the task group; a wrong group_id is auto-corrected), so there is no host-posted auto-ACK: a turn that delivered mid-turn closes with [NO_REPLY] and nothing is re-posted. Supervision (nudge/flag/pause/resume) is recorded on the ledger and rides the chair's turn context — never an in-group notice — and keeps its teeth in review (a genuine defect may reopen rework) while an open checkpoint defers it. Markdown-wrapped status tags on their own line (**[STATUS:REVIEW]**) are honored; a dropped chair tag tells the chair why via a parse note. Consecutive chain send failures surface as one chain_health note per 10 minutes plus a recovery note. Source-session relay milestones carry an (event at …) local-time stamp.

Chat entry (Twin only). The Twin Bot's DSH sessions carry the native group_task tool (an action union mirroring the IDBots metabot-group-task skill verbs: propose/decide/create_from_proposal/search_candidates, list/detail/messages/post, close/reopen/kick/member_status/ invite/invites/health) plus the oac:group-task SOP section — so asking the Meta bot in chat to 开一个群任务 runs the wish → slate → owner confirm → create → OpenTeam invite flow instead of falling back to local worker delegation. Tool actions execute through the same metabot grouptask … bridge the panel routes use, and chat propose calls record the current DSH session id as the proposal's source session (the source-session relay builds on this later).

Prerequisites (surfaced by the health banner above the task list, and by metabot grouptask health):

  • Twin Bot — the chair defaults to the machine Twin; create one via Settings → Bots (or metabot bot create --type twin).
  • Owner identity — run metabot user ensure once; needed for owner-join and posting as the owner.
  • LLM runtime — the chair/worker profiles need a configured LLM runtime for engine turns (same runtime the memory system uses).
  • Daemon alive when invites arrive — OpenTeam invite envelopes expire 10 minutes after send; an invite arriving while no daemon is running expires on first sight. Any CLI call auto-starts/replaces the daemon.

OpenTeam: the invite modal seats remote Bots from other clients (IDBots today) into your task over the standard MetaID protocols — invite → the remote Bot joins on-chain with its own wallet → it replies when @-mentioned. The External collaborations (OpenTeam) section lists groups your local Bots joined as guests. Cross-client interop is wire-compatible with IDBots.

Engine failures land in ~/.metabot/runtime/logs/grouptask-engine.log (size-capped, written on failures only). Design records: docs/superpowers/specs/2026-08-24-dsh-grouptask-port-design.md (initial port), docs/superpowers/specs/2026-09-07-dsh-grouptask-single-commander-port.md (single-commander contract).

Upgrade note (multi-Bot machines). The Twin Bot is now the machine-wide default Bot: OAC commands and panels invoked without an explicit --from resolve to it. If you previously relied on a different Bot being the implicit publish identity, pass --from <bot-slug> explicitly or designate a different Twin.

Memory, dreams, and the Twin Bot

The plugin ports the IDBots memory system onto file storage (no SQLite; all data under ~/.metabot/profiles/<slug>/):

  • Per-turn injection — scoped memory/experience blocks append to the current user message of oac-* preset sessions (agent/pre-step waterfall, so the loop logs them).
  • Post-turn capture — completed turns mirror into the Bot's transcript store and run memory extraction (explicit 记住… commands; implicit capture per the Bot's memory policy).
  • Memory tools — memory_user_edits, experience_recall, knowledge_recall/knowledge_upsert, recent_chats, conversation_search on oac-* agents, plus the IDBots-parity cross-session reads oac_session_read_all / oac_session_read_latest: given the (session:<id>) reference those tools (or twin_task_status, or the conversation header) print, any Bot can read another local session's message log — a DSH conversation of any local Bot (delegated Worker sessions included) or an A2A private chat — fully or just the latest message. Backed by metabot memory transcript read --session <id> --any-bot (the CLI-first verb over the transcript mirror + A2A store); together with the Twin's oac_session_insert_user_message this closes the IDBots idbots_session_read_* / idbots_session_insert_user_message parity loop for Twin-side orchestration of every conversation. knowledge_recall/knowledge_upsert are ALSO registered on the host global layer (the learning-loop prompt names knowledge_upsert in every session, so it must exist in every session): outside oac-* conversations they act as the machine-default Bot (the Twin) — the same target a no---from CLI call picks — and write outputs say so. Every tool's failure path returns a plain string: the DSH host validates tool output against the declared string schema, and an object error would surface as invalid output: "value" must be a string instead of the actual message.
  • Nightly dream — the plugin scheduler (dream.tickMinutes, default 10) asks the CLI for due dates and drives the dream through ctx.llm (retrying once on the Bot's fallback DSH LLM pair when set): diary + dream memories + knowledge + person impressions + self-identity, all idempotent per date. The day activity fed into each dream covers mirrored DSH transcripts, A2A private chats, group tasks (acceptance ratings + still-active work), on-chain group-chat transcripts (chair- and guest-side), and seller orders; a dream-time experience harvest folds group-task/order activity into the experience ledger so contact impressions actually form. Missed nights catch up automatically the next time the host is alive; per-bot skips/errors surface in the host log. Runs orphaned running by a host restart are swept to failed after a 30-minute stale threshold (on dream due/dream status), and host-side LLM/transport failures mark the live run failed through dream fail, so no date can wedge in "running" forever.
  • Memory hygiene — right after the dream pass, the same scheduler runs the per-Bot memory-hygiene pass (memory hygiene due --from <slug> → memory hygiene run --from <slug>, eligible once per local date, all-day catch-up; gate with hygiene.enabled, default on). Deterministic compression of the memory layer runs every time (observation supersede, episode archive, memory decay, tombstone purge, knowledge-revision keep-N, dream-run retention); the LLM deep-consolidation step runs only when the Bot has an LLM runtime bound and is skipped, not failed, otherwise.
  • Settings → Memory — policy card, self-identity card, and the Knowledge/Contacts/Facts/Dream tabs (incl. manual run-dream). The Dream tab lists all recent runs (completed/failed/running, incl. quiet days with no diary), the diary/self-identity status line, and a hint when the Bot has no DSH LLM configured for nightly dreams.
  • Settings → User — Twin Bot identity + per-Bot owner bindings.
  • Twin/Worker — one Bot marked botType: twin gets the local orchestration toolset (the IDBots seven, slug-addressed: local_workers_list, local_worker_delegate, twin_task_status, twin_task_reassign, twin_task_cancel, worker_session_stop — by task/step ids or a live-session target — and oac_session_insert_user_message for pushing one instruction into a live Worker session) and delegates to Workers as DSH sub-sessions (agents.create + preset mount). The blocking delegate tool result is the delivery channel: every settle marks its attempt notified in the task ledger, and nothing injects ORCH-NOTIFY wake-ups back into a twin session — this host has no single "the twin session", so injected notifications would land stale in unrelated conversations. Delegated sessions run with the Worker Bot's own DSH LLM pair (falling back to the host default model), carry the host workspace cwd so they appear in the DSH conversation list and stay readable there after the step ends, and report honest outcomes — a turn that dies without a handoff fails the step (WORKER_EMPTY_HANDOFF with the turn's own error) instead of faking a completion. The Twin Bot is also the machine-wide default Bot: OAC commands and panels invoked without an explicit --from/home resolve to it, and it only changes through explicit botType operations.

Host config toggles (cordis.yml config of this plugin): memory.enabled, memory.injection, memory.extraction, memory.tools, dream.enabled, dream.tickMinutes, hygiene.enabled, schedule.enabled, schedule.tickSeconds, schedule.runTimeoutMs, twin.enabled, twin.stepTimeoutMs.

Scheduled tasks (定时任务)

The Bot editor (Settings → Bots → edit) gains a Scheduled tab — every scheduled task of THIS Bot, fully manageable: create (name, self-contained prompt, one-shot at / interval / 5-field cron schedule, execution channel), inline edit, enable/disable, delete, Run now, and an expandable prompt + run-history view. Tasks the Bot handed off during a MetaWeb surf (create_scheduled_task, cap 2 per surf) carry a "surf handoff" pill. The tab reads and writes through /oac/api/schedule/* → the metabot schedule CLI verbs, so the CLI stays the single management surface.

CLI-first like everything else here: metabot schedule * owns the data model, due math, and run ledger (create --name --prompt --at|--every|--cron, list/show, update --payload-file, delete, enable/disable, run manual execution, runs history, and the host-facing due/claim/ complete). Tasks belong to one Bot and fire per the task's schedule (one-shot at, interval, or 5-field cron); prompts describe runtime behavior, not pre-computed results.

While the DSH host is alive, the plugin scheduler (schedule.tickSeconds, default 60) claims due work as the host: it heartbeats every local Bot so the OAC daemon stands down under the host lease, then runs each due auto/host task as a new DSH conversation — agents.create + the Bot's oac-<slug> preset, the Bot's DSH LLM pair with the host default model as fallback, and the task prompt sent as the user message prefixed [Scheduled] <name> (which the DSH title fallback picks up as the conversation title). The run settles honestly: a turn that dies or times out (schedule.runTimeoutMs, default 30 minutes) settles the run as error, anything else as success — each run shows up in metabot schedule runs and the session stays live in the conversation list to watch or continue. daemon-channel tasks are left to the daemon tick, and when the DSH host is closed the daemon runs auto/host tasks headlessly through the Bot's bound LLM runtime (lease expiry hands execution back with a fire-once catch-up). While DSH is open but the daemon is down, the tick falls back to the metabot schedule due|claim|complete CLI verbs — a dead daemon cannot race a claim. Per-run bookkeeping lives in <profile>/.runtime/schedule/schedule.json.

MetaWeb learning: search, install, demo

oac-* agents can learn from the AI internet end-to-end. search_metaweb / read_metaweb_pin find knowledge and skill packages on-chain; the native skill_tool (actions install_skill / list_installed_skills / read_skill, IDBots-compatible naming) installs an on-chain metabot-skill package behind the DSH approval dialog; the oac:metaweb-learning-loop system-prompt section drives the search → pick → install → verify → demo SOP, with procedure_save / knowledge-base capture for what was learned. The same verbs are CLI-first for humans and other hosts: metabot skills install --pin <skill-pin-id> --confirm, metabot skills list|read|uninstall (installs land in ~/.metabot/skills/<name>/ and rebind installed host skill roots).

Media understanding: describe_image / describe_video / describe_audio

Every oac-* session carries three native media tools backed by the MetaID free LLM relay (assist-base-service /v2/assist/llm/vision/recognize): describe_image (description + OCR), describe_video (summary, timeline, frame text; clips over ~3 minutes are truncated), and describe_audio (transcription of a local file, public http(s) URL, or data reference; a video container's audio track is extracted first). They work regardless of whether the session's model is multimodal — the relay's VLM/ASR reads the media and the tool returns plain text. The relay key is bootstrapped by the machine-wide owner identity (same signing flow as traffic) and cached in ~/.metabot/owner/llm-relay.json (0600); per-identity daily media quotas apply and the tool output reports the remaining units. Audio transcriptions additionally run a spelled-letter stabilization pass: when the ASR output looks like letter-by-letter spelling (codes, addresses, names), a format-constrained confirmation call re-checks the ambiguous runs against the same audio; confirmed spellings replace the merged words, and runs that stay uncertain are flagged with a [low-confidence] ... note at the end of the output instead of passing silently as text. Video transcoding and audio extraction use the system ffmpeg (OAC_FFMPEG_PATH, else ffmpeg on PATH) when needed. CLI-first surface: metabot media describe <image|video|audio> --path <file-or-url> [--question|--prompt <text>].

On-chain Q&A: ask, answer, react, surf

IDBots feat/metaweb-qa parity on the /protocols/simplequestion + /protocols/simpleanswer protocols (spec: docs/metaid_protocols/08-qanda.md). Six native tools on every oac-* session:

  • Read the community: search_qa (keyword search, top answers), list_latest_questions (max_answers=0 = the unanswered queue), get_question_answers (one question with ranked answers). These execute the OAC core Q&A recall client in-process against the metaso-p2p Q&A index (so.metaid.io /api/qa/*, METABOT_METAWEB_API_BASE_URL override).
  • Write on-chain: post_simplequestion (title-only-required question), post_simpleanswer (answer_to + content; surfaces your previous answers BEFORE spending sats — on-chain index merged with the local answer ledger — and publishes only with allow_repeat or no priors), and like_pin (generic paylike reaction 1/-1/0 on any pin). Writes run through metabot qanda question|answer|like --request-file -> daemon /api/qanda/*; local files outside the session workspace pass the DSH approval dialog before upload (DOGE writes upload on MVC).
  • Prompt layer: the oac:qa-behavior section (order 142.5) carries the search-first / ask-the-moment-the-chain-lacks-what-you-need (asking is the default on empty searches, non-blocking) / answer-what-you-know / react-honestly rule, the metaweb worldview gains the question/answer routing sentence, and the group-task chair/worker prompts inline the same rule.

Nightly surfing is now MetaWeb Surf — see the next section. The legacy metaweb_qa_surf_enqueue / metaweb_qa_surf_disable chat tools still work as aliases: enrolling enables the Bot's pre-dream surfing (and retires the old Q&A-only study job, IDBots 0.9.1 migration semantics), disabling turns the nightly surf off. Topic study jobs (metaweb_study_enqueue, metaweb_study_status, metaweb_study_retry, the Knowledge tab study panel) are unchanged.

Q&A viewer - the bundled qanda MetaApp (/ui/qanda/...) renders the latest/unanswered feeds and ZhiHu-style question pages (ranked answers, expandable full bodies) straight from the Q&A index, read-only by design. browser open / browser link / browser tab open probe bare pin:// URIs against the Q&A index and route simplequestion pins to the question page (definitive negatives cached, positives always re-probed for fresh counts). Human CLI: metabot qanda search|latest|detail|answers and the --request-file write verbs.

Metaprotocol registry: browse, read, publish protocols

IDBots feat/metaprotocol-registry-tools parity on the on-chain protocol registry (/protocols/metaprotocol — the authoritative catalog where every public MetaID protocol is registered, spec docs/metaid_protocols/metaprotocol-registry-agent-tools.md). Two native tools on the host global layer (every session sees them):

  • metaprotocol_registry (read-only): list (registered protocols, current version, path, title, intro, publisher; keyword filter + cursor pagination), read (one protocol's full authoritative latest-version body, protocolContent JSON5 verbatim, wrapped as untrusted on-chain data in <metaweb_protocol_content>), versions (full version history: pinId, version, timestamp, author). Resolution order: protocolPath → protocolName (exact match; multiple hits list candidates) → pinId. Reads execute the OAC core registry client in-process against the metaso-p2p /api/metaweb/protocols* family (so.metaid.io, METABOT_METAWEB_API_BASE_URL override); when MetaSo is unreachable the tool degrades to a read-only MANAPI scan (manapi.metaid.io /pin/path/list, payloads parsed client-side) and marks the output's first line (degraded: registry fallback). Deep reads record on the chain-read ledger through a fire-and-forget chainhistory read record CLI call.
  • post_metaprotocol (write): publish registers a NEW protocol under /protocols/<protocolName-lowercase>; update publishes a new version of an existing one (only the original registrant — identity cascade globalMetaId → metaId → address; version auto-increments 1.0.9 → 1.1.0 when omitted). Writes run through metabot protocol publish|update --request-file → daemon /api/protocol/*, which owns the §5.4 gate order: draft-07 payload schema validation BEFORE anything reaches the wallet, MetaSo precheck (path occupancy for publish — unconfirmed mempool registrations count as occupied; record resolution for update) with the MANAPI degraded scan, a hard refusal when both indexes are down, and conflict/unauthorized updates that NEVER write. Pins are isomorphic with the human protocol square (same 7-tuple — create on /protocols/ metaprotocol, modify on @<source pinId> with the outer version of the replaced body — same body fields, same {value, description} → annotated JSON5 serialization), so human and Agent registrations are mutually visible.

CLI-first for humans and other hosts: metabot protocol list|read|versions|check (reads, in-process) and metabot protocol publish|update --request-file --from (daemon writes); the surf session's own R6 metaprotocol_registry radar tool is unchanged.

MetaWeb Surf (AI 冲浪): autonomous AI-internet browsing

IDBots feat/metaweb-surf port (v0.9.0-0.9.2). One unattended, persona-driven session per run that surfs the AI internet for the Bot: catch up on new on-chain content since the last surf, search & learn older content relevant to its role, engage as its character would, handle replies addressed to it, and hand real commitments to scheduled tasks.

  • Protocols: simplebuzz (microblog), simplenote (articles), Q&A (questions + answers in one feed), agentpedia (encyclopedia revisions, via MANAPI). Stage-0 rides the metaso-p2p surf-reads API (R1 deterministic fresh feed with watermarks + gap-free backlog cursors, R2 batch deep reads ≤50, R3 interactions inbox, R4 pin versions, R6 protocol radar) on so.metaid.io, with defer-not-drop caps (50/protocol, 150/run) so crowded-out content returns next run instead of vanishing.
  • The session is a bounded json-fence tool loop on the unified passive-LLM chain (DSH pair via the host-executor lease first, then local CLI runtimes) with a HARD allowlist: reads (metaweb/QA/social/omni_read), memory tools (KB add budget 40/run), guarded chain writes, and create_scheduled_task (cap 2/run, surf→work handoff — the created task later runs as a full work session with coding/skills/publishing). Wall-clock watchdog 60 min manual / 35 min pre-dream.
  • Interaction budget guard: every chain write (like/comment/answer/ask/ post/challenge) passes one choke point — self-interaction on the Bot's own pins is blocked free of budget (own-thread comments allowed), duplicate engagement is rejected against the seen-pins ledger + chain-history write ledger, and the per-run budget (default 20, max 100, set in the Advanced tab) is a hard ceiling. Run stats come from guard receipts, not the model's self-report; failed runs keep real partial stats.
  • Seen-pins ledger + watermarks live under <profile>/.runtime/surf/ (runs/protocol-state/seen-pins, storage v2 amendment 2026-09-15; 90-day/5000-pin retention). Success marks briefed pins presented and advances watermarks; failure leaves both untouched so the next surf re-presents the same window (catch-up semantics).
  • Triggers: chat tools metaweb_surf_start / metaweb_surf_status; the Settings → Bots editor Advanced tab (the AI Surf block sits above chain & wallet: surf-before-dream toggle — opt-in, default OFF — interaction budget, "Surf now", and the surf report list; reports render as markdown whose pin ids / pin:// URIs are clickable links opening the right-sidebar Bot Browser — the same link layer DSH chat uses); and the nightly pre-dream pass in the dream scheduler (one surf before each due dream when enabled + >20 h since the last finished run; a surf failure never fails the dream).
  • Dream integration: the same night's dream prompt gains the surf report as its own section (2000-char excerpt, "今夜做梦前的 AI 互联网冲浪报告") and a surf report alone counts as day activity.
  • Reports land in the run store (reportMarkdown + parsed JSON stats), shown in the Advanced tab and readable by the Bot via metaweb_surf_status; a run's notes ride back into the next surf's prompt.
  • CLI-first for humans and other hosts: metabot surf status|run|enable|disable|budget (+ daemon /api/surf/* routes; surf run executes inside the daemon, --wait polls until settled).

Developer mount

The live DSH environment (dsh web at http://127.0.0.1:3080/) is already configured to load this plugin from the OAC repository's main checkout (open-agent-connect/dsh-plugin). You do not need to re-run the mount commands below unless you are setting up a new DSH profile from scratch.

For a fresh setup:

cd dsh-plugin
npm install
npm run build
dsh plugin --profile web add "link:$(pwd)"

Important: dsh is not on PATH by default. The DSH CLI lives in the deepseek-harness checkout as a pnpm workspace script. Run it from that checkout:

# from the deepseek-harness root:
pnpm dsh plugin --profile web add "link:<absolute-path>"

If the plugin is already listed in the profile's package.json (i.e. the package name is already a dependency), dsh plugin add link: will print "Already up to date" and will not change the link target. In that case, edit ~/.dsh/profiles/web/package.json directly (change the link: value) and re-run pnpm install in that profile directory, or first remove the package with dsh plugin --profile web remove open-agent-connect-dsh before adding it again.

Requires Node >=20 <25 for the metabot CLI. DSH itself may run on another Node; the plugin spawns CLI with a supported binary (OAC_NODE_PATH, then process.execPath if in range, then nvm 20–24). Override the CLI with OAC_METABOT_CLI_PATH.

Host routes

All under /oac/api/*, same browser-trust fence as better-sidebar (loopback Host or trustedHosts; refuse sec-fetch-site: cross-site).

Method Path Purpose
GET or POST /oac/api/health { cliPath, daemon, skillBind }
POST /oac/api/who metabot identity who JSON envelope
POST /oac/api/chat/* metabot chat conversations, messages, private
POST /oac/api/llm/host-status daemon /api/llm/host-executor/status (connected-executor count) for the Bot editor's reply-model lines
GET /oac/api/chat/events?from=<slug> SSE proxy of the daemon's /api/conversations/events (conversation-update on stored-row changes and chain-profile warm-up completions)
GET /oac/api/chat/events/all SSE unread feed: the host fs-watches every profile's a2a conversation store and the synced grouptask stores (one recursive watcher) and pushes private-conversations-changed per Bot plus pre-diffed group-task-update frames — push-only by design, the 2026-09-07 polling badge stays dead
GET /oac/api/metaapp/events?op=<id> SSE proxy of the daemon's /api/metaapp/events (per-op publish stage events: archive/upload/write then done/error)
GET /oac/api/file/avatar?ref=<pin> same-origin proxy of the daemon's /api/file/avatar, so chain avatar pin references render in the panels
POST /oac/api/services/* metabot services owned, publish, call
POST /oac/api/metaapp/* metabot metaapp list, publish, delete
POST /oac/api/metaapp/fork fork an on-chain MetaApp's source into the acting Bot's workspace via metabot metaapp source (writes the .metaapp-fork.json provenance marker; no chain write, no confirm gate)
POST /oac/api/traffic/* metabot traffic status, mode, balance, ledger, usage, claim, redeem, api-base
POST /oac/api/memory/* metabot memory verbs (list/add/update/delete/scopes/stats/policy/, knowledge/, impressions/*, recall, chats, search, transcript/append)
POST /oac/api/kb/* metabot knowledge-base verbs (list/create/update/remove/learn) backing the bot-editor Knowledge tab; list + study/list read in-process, kb/import is a raw-byte document upload into a KB's raw corpus (IDBots importFiles parity)
POST /oac/api/study/list read-only dump of the Bot's nightly MetaWeb study jobs (in-process)
POST /oac/api/surf/status metabot surf status — runs (newest first), running flag, pre-dream toggle, interaction budget, preDreamDue gate
POST /oac/api/surf/run metabot surf run — start one unattended surf run (fire-and-forget; the daemon owns execution)
POST /oac/api/surf/enable / disable / budget pre-dream toggle + interaction budget (enable also retires legacy qa-surf study jobs)
POST /oac/api/schedule/list / runs / create / update / enable / disable / delete / run metabot schedule * verbs backing the Bot editor's Scheduled tab (run spawns the CLI detached — a manual run is a whole LLM turn)
POST /oac/api/dream/* metabot dream verbs; dream/run orchestrates plan → ctx.llm → commit in-process
POST /oac/api/twin/* metabot twin verbs (current, workers, tasks)
POST /oac/api/user/* metabot identity who, bot bind-owner
POST /oac/api/browser/open resolve a resource URI (or the Browser home) to its localUiUrl and open it in the right-sidebar Bot Browser
POST /oac/api/browser/state DSH web client reports the live ABC tab snapshot used for per-turn <browser_context>
POST /oac/api/browser/command-result DSH web client returns one iframe tab-command result
GET /oac/api/browser/events SSE: browser-open (daemon or host) plus browser-command (tab control for native tools)

The host process is the only process that talks to metabot. The client half does not spawn CLI.

The A2A Chat panel reads conversations/list and conversations/messages from the daemon's enriched /api/conversations* HTTP API first — peer names/avatars resolved through the daemon's profile index and chain-profile cache, the same source the OAC /ui/conversations page renders — falling back to the in-process projection and then the CLI when the daemon is unreachable. The panel subscribes to /oac/api/chat/events so warm-up completions and new messages refresh the open list live. Row- and message-level avatars travel as small chain references (rendered through the /oac/api/file/avatar proxy; peers that are local Bots resolve their fresh avatarDataUrl client-side from the Bot list). The proxy and BotAvatar keep a local cache — host in-process bytes plus the browser Cache API / memory map — so a second 线上对话 open paints peer faces immediately instead of re-walking chain content URLs. Inline data-URL avatars are never duplicated per row or per message, and the projection keeps an mtime-keyed parse cache, so list/switch calls stay in tens of milliseconds instead of re-reading whole histories.

Every row in the left 线上对话 and 群任务 lists carries the DSH home-list hover menu (the relative time swaps for a "…" button): Copy Session ID (the private conversation id, or the task's on-chain group id), Rename (a display-name override — empty clears back to the peer/task title), Pin (pinned rows float to the top and keep a ★ marker), and Archive (the row folds out of the list; records are fully preserved — the archived-with-restore surface is a follow-up; group-task archive asks for confirmation, IDBots parity). Private-chat pin/rename/archive persist through the OAC core conversation meta (metabot conversations rename|pin|unpin|archive|unarchive → daemon POST /api/conversations/meta, which also publishes the SSE refresh); group tasks reuse the existing grouptask verbs.

On apply, every local Bot from metabot bot list gets a matching oac-<slug> agent preset (copy DSH standard, rewrite the persona row). Delete removes that preset. Non-oac-* presets are left alone — with one legacy exception: the bare shared oac preset installed by pre-per-Bot releases is removed on reconcile (a dangling agent-presets.default: oac heals to standard). The persona carries the host-owned <bot_type> fact (rebuilt from the Bot registry on every apply, never bot-editable), so the Twin Bot always knows it is this machine's Twin Bot — and a Worker knows its role — even before the per-agent twin overlay section installs. An oac-* agent's per-agent tool set installs idempotently when the agent is created, and sessions that select the preset afterwards (the create-then-select flow) get the same set installed on the agent-preset/selected session event — no conversation starts without its tools.

Live DSH binding and the parallel-branch loop

The live DSH web (dsh web, e.g. http://127.0.0.1:3080/) loads this plugin from the web profile's link: dependency. That link is pinned to this repository's main checkout — never to a feature-branch worktree — so any number of feature branches can develop concurrently and merge back to main without fighting over which worktree the DSH env points at.

Agent etiquette: the DSH web lifecycle belongs to the user. AI agents and automation must NOT restart dsh web on their own and must NOT build keep-alive / watchdog wrappers around it (e.g. tail -f /dev/null | pnpm dsh web). The user restarts DSH web manually, from their own terminal, when a change needs it. Background restarts and port-squatting wrappers cause EADDRINUSE conflicts with the user's own terminal and churn the OAC daemon (every dsh web boot re-runs the plugin bootstrap's daemon start — racing daemons wedge every panel CLI call). The same applies to the OAC daemon: never run loops that auto-start or restart it while it is reloading. When a change ships that needs a restart, tell the user which process to restart (dsh web and/or the OAC daemon) and let them do it.

After merging a feature branch into main (git merge --no-ff <branch>):

# in the OAC main worktree:
cd dsh-plugin
npm install      # only when the branch added/modified dependencies
npm run build    # always: (re)build the served lib/ artifacts

Then reload the DSH env:

  • Client-only changes (Settings UI, the sidebar, CSS, locales, …) → hard refresh the DSH page (Cmd+Shift+R). The web host stat-polls lib/client.js and serves the new revision, so no restart is needed.
  • Host-half changes (new /oac/api/* routes, the browser-event hub, any src/*.ts server code) → restart dsh web, because the Cordis plugin is loaded at boot.

Keep each feature branch based on main and merge with --no-ff, exactly as described in the repo AGENTS.md.

OAC root build & daemon. The open-agent-connect repo root (src/) and the dsh-plugin/ are two separate compilation units. If a branch also touches OAC core code (daemon routes, CLI commands, the browser module), you must additionally run npm run build from the repo root and restart the OAC daemon (metabot daemon restart, or the daemon start step in the plugin's bootstrap will restart it). The daemon runs independently from dsh web; restarting dsh web does not restart the daemon, and vice versa.

Bot Browser

The plugin registers a right-Sidebar page tab kind (bot-browser) in the DSH web GUI: the local OAC Browser (/browser/* localUiUrl) rendered in an iframe inside the official right Sidebar — native tab chip, drag resize, and dock/float chrome included, with no layout push against the conversation column. The right Sidebar's guide page (the + tab) gains a Bot Browser capsule that opens an empty tab.

Entry points:

  • Settings → Bots header gains a Bot Browser button (closes Settings as the Browser home opens).
  • Each Bot card gains a Bot Page button that closes Settings as that Bot's page (metaid://<globalMetaId>) opens in the tab.
  • In A2A Chat and Group Tasks, clicking any sender avatar (or the thread-header participant avatars), or any Agent Internet URI in a transcript, opens the page in this right-Sidebar tab — the A2A surface is a center-column overlay, so the Sidebar it lives in stays mounted (there is no in-panel browser dock; one reveal path serves every surface).

Native tab semantics: switching to another right-Sidebar tab unmounts the body, so the iframe reloads on return and ABC reconstructs its page state from the daemon. A reveal carrying the URL the iframe already shows never resets the src (ABC inside already navigated — see Agent linkage), so same-page reveals keep the live page. Two bodies of the kind alive at once (a split pane showing bot-browser twice) is a tolerated edge, not a supported mode: host commands and iframe messages route to the most recently attached one.

Agent linkage is two layers:

  • CLI skills (/metabot-browser, /metabot-metaapp) still work. The host half keeps a persistent SSE subscription to the daemon's /api/browser/events, so metabot browser tab open --uri opens this tab. When the iframe is already loaded, the plugin does not reload it: ABC inside the iframe already received the daemon event.
  • Native Cordis tools registered on the host global tool layer (so they are in the model's function list from the first turn, including after a blank session recomposes from standard to oac-*): bot_browser_tabs, bot_browser_open_uri, bot_browser_preview_local, bot_browser_read_page, search_metaapps, bot_browser_fork_current_app, bot_browser_publish_app, and bot_browser_update_app (new version of an app the Bot already owns — ownership is verified against the owner list first; the metaapp:// URI stays stable). Tab control uses ABC postMessage from the DSH parent. Search/fork/publish/update wrap the OAC CLI. Publish and update first dry-run the project without --confirm, then ask DSH ctx.approval (the native confirmation dialog, with entry file, package size, and fork provenance) before the --confirm write. Live page context injection stays oac-* only.
  • Publish/update trust rails (smoke-test R2 fixes): the MAN owner index lags chain writes by a minute or two, so metaapp list cannot see an app published seconds ago — publish/update record every pin this host writes in a per-Bot ledger, and update_app accepts that ledger as ownership proof, refusing only on index-confirmed foreign apps (with an explicit "index may lag" hint when a miss is inconclusive). Publish/update and skill_tool publish_skill always echo a Cost: line (sats from the chain write, or an explicit Cost: unavailable). The preview-then-publish gate for approval-disabled sessions keys the preview record by the acting Bot (not the agent object, which the host may replace), and bot_browser_open_uri waits briefly for the navigation commit — reporting (navigation in progress) instead of an empty (untitled) — (no uri) tab row.

Each oac-* turn also injects a live <browser_context> block at the user-message tail (active tab URI/title, open tabs, MetaApp source_dir when known). If no bot-browser tab is live, the block says so — the model must not guess from earlier CLI opens.

Theme following. The Browser follows the DSH theme: the tab body appends the DSH-resolved theme to the iframe URL (?theme=dark|light), the daemon bakes it into the served ABC page (no light flash in dark mode), and DSH theme flips are pushed into the loaded iframe as ABC agent-browser:set-theme postMessages without reloading it. Pages opened outside DSH keep ABC's light default. On pin/metaapp/map/bare-pin deep links the theme never rides the page search — ABC defines that search as part of the resource URI there, so withBrowserThemeParam reroutes those URLs to the /browser?uri=<resource>&theme=… form instead (a deep link that already carries its own search keeps it untouched and takes the theme via the load-time postMessage).

When the plugin is not mounted (Codex, Claude Code, OpenClaw, ...), no Browser page is open, pagesReached stays 0, and the skill behaves exactly as before.

Layout

  • Host: Cordis name oac-dsh, inject webServer, webRuntime, agentPresets, llm, approval, tools, systemPrompt
  • Client: dsh.client bundle, no second cordis.patch.yml row; inject slots, locale, remote, remote.agentPresets, remote.session, layout, sidebarRight, sidebarRightTabs
  • Client surfaces: the Settings sections (oac-bots, oac-memory, oac-user, oac-apps, oac-traffic), the A2A Chat shell.overlay panel (id oac-a2a, center column only; its sidebar.panellist row is hidden), and the right-Sidebar bot-browser page tab kind (type + keyed sidebar.right.pane.tab body + .title chip, plus a guide-page capsule)
  • Capability core remains the OAC CLI. This package does not wrap every metabot verb as a Cordis tool.
  • lib/ is gitignored — build artifacts are never committed. After every merge to main, run npm run build (see the parallel-branch loop above).