AGENTS.md@packages/omo-senpi · diff
git:20261016.bf9fb40 to git:20261021.a2e3ab0
8 added, 2 removed. Audit A to A.
# omo-senpi
Native Senpi TypeScript extension adapter for oh-my-openagent.
+ Telemetry uses the SDK-free `@oh-my-opencode/telemetry-core/injected` entry. Its lazy transport imports `#omo-telemetry-posthog-runtime` only on the first capture; the source mapping targets `src/components/telemetry/posthog-runtime.ts`, while the plugin maps it to `extensions/omo-telemetry-posthog.js`. Keep build freshness, required installation artifacts, installer fixtures and bundle audits aligned with that sidecar.
+
This package is adapter-only. It may depend on harness-neutral core packages plus the Senpi-coupled `@oh-my-opencode/senpi-task` engine, but those packages must not import Senpi, Pi packages, or this adapter through their harness-neutral entrypoints. The Senpi runtime boundary stays here.
## Anatomy
| Path | Purpose |
|------|---------|
| `package.json` | Private workspace package `@oh-my-opencode/omo-senpi`; exports `.`, `./install`, `./extension` (all to `src/` TS) and maps `#omo-task-runtime` to `src/extension/omo-task.ts`, `#omo-computer-use-runtime` to `src/components/computer-use/runtime.ts`, `#omo-memory-doctor-runtime` to `src/components/memory/commands/doctor-runtime.ts`, `#omo-memory-memfs-runtime` to `src/components/memory/commands/memfs-runtime.ts`, and `#omo-memory-palace-runtime` to `src/components/memory/palace/runtime.ts` in dev; the plugin manifest remaps these to their generated bundles. |
| `src/` | Adapter source; own AGENTS.md, with deeper files for `extension/` and the five deep components. Small components are documented in the Components section here (`src/AGENTS.md` defers to this file). `senpi-test-runtime.ts` imports real host modules for tests. |
| `src/extension/` | Senpi ExtensionAPI composition layer. It validates the required API surface, registers global and per-component disable flags, and wires components defensively. Own AGENTS.md. |
- | `src/components/` | Live components, in `src/extension/component-list.ts` registration order: `config-startup`, `model-profile`, `bundled-skills`, `skill-commands`, `native-badge`, `onboarding`, `init-deep-advisor`, `side-panel`, `telemetry`, `ultrawork`, `skill-pointers`, `ulw-execute-continuation`, `ulw-loop`, `todo-fanout-reminder`, `question-gates`, `git-master`, `fallback-architect`, `ast-grep`, `builtin-mcps`, `lsp`, `x-search`, `computer-use`, `comment-checker`, `task`, `thread`, `memory`, `gateway`, and `config-watch`; documented alongside them are the `config-resolution` loader helper and the `agent-home` resolver (neither helper is a registered component). |
+ | `src/components/` | Live components, in `src/extension/component-list.ts` registration order: `config-startup`, `model-profile`, `bundled-skills`, `skill-commands`, `native-badge`, `onboarding`, `init-deep-advisor`, `onboarding-scan`, `side-panel`, `telemetry`, `ultrawork`, `skill-pointers`, `ulw-execute-continuation`, `ulw-loop`, `todo-fanout-reminder`, `question-gates`, `claim-receipt`, `git-master`, `fallback-architect`, `ast-grep`, `builtin-mcps`, `lsp`, `x-search`, `computer-use`, `comment-checker`, `task`, `thread`, `memory`, `gateway`, and `config-watch`; documented alongside them are the `config-resolution` loader helper and the `agent-home` resolver (neither helper is a registered component). |
| `src/install/` | Local Senpi installer and uninstaller helpers. They add or remove the absolute plugin path in the canonical agent directory resolved by `resolveAgentHome` (`~/.omo/agent` for an omo installation, an explicit `OMO_`/`SENPI_`/`PI_CODING_AGENT_DIR` when set, the flat `~/.omo` and `~/.senpi/agent` layouts only as detected fallbacks). The generated local launcher pins that same directory for the engine it spawns. |
| `plugin/` | The single Pi package `@code-yeongyu/omo-senpi`: generated `extensions/` bundles, generated skills, the tracked `runtime/dag` SDK, and plugin-local build scripts. Own AGENTS.md. |
| `scripts/qa/` | Live Senpi QA drivers, continuation probes, and mock providers; the real harness proof for adapter changes. Own AGENTS.md. `scripts/expect-exit.mjs <expected> -- <cmd...>` asserts child exit codes. |
| `skills/` | Native Senpi skills authored directly against the Senpi tool surface (not ported from Codex or the shared pool): `dag-library`, `give-me-tips`, `hyperplan`, `init-deep`, `mass-ulw`, `onboarding`, `ultrawork`, `ulw-loop`, `ulw-plan`, `ulw-research`, `visualize`. Own AGENTS.md. The credential-gated `x-search` skill is **not** in this pool: `plugin/scripts/stage-x-search-skill.mjs` copies `src/components/x-search/skill/SKILL.md` into `plugin/skills-conditional/x-search/` (kept out of `pi.skills`) and the `x-search` component contributes that path via `resources_discover` only when an xAI credential exists. |
| `test-support/` | `fake-extension-api.ts`: `FakeExtensionAPI` + fake event dispatch; records commands, flags, messages, tools, renderers, MCP registrations, and dispatched results for hostless tests. |
The v1 install surface is local-path only. Install the built Pi package from `packages/omo-senpi/plugin`; do not document npm, git, or marketplace distribution for this adapter until that exists in code.
## Components
- `config-startup`: runs the shared lock+journal migration engine (`runSenpiStartupMigration`, both legacy groups: `2026-07-opencode-config-unification` for `oh-my-*` files and `2026-07-codex-config-jsonc` for `~/.omo/config.jsonc`) plus the Native-only notice plan `2026-09-opencode-routing-notice` (`opencode-routing-notice.ts`: reports once the OpenCode edition's agent/category model choices Native does not use, through `omo-native/bin/lib/setup-opencode-models.js` `openCodeRoutingGap`, writing only its marker) before Senpi reads its unified configuration, then loads the profile-selected `[senpi]` view through `config-resolution` (`loadSenpiOmoConfig`: `loadOmoConfig` with `harness: "senpi"` plus `resolveModelReferences` catalog expansion). Migration results and config diagnostics surface once on the first `session_start` via the host notification UI (falling back to the logger).
- `native-badge`: publishes the native footer status badge into the TUI status line.
- - `onboarding`: first-run onboarding flow, claimed once per state dir on the first real interactive UI prompt (`claimOnboarding`), not on `session_start`. Explicit `--onboard` starts immediately in TUI and consumes its process latch at claim, without resetting it on release; isolated-worker host repetition is tracked in #9917.
+ - `onboarding`: first-run onboarding flow, claimed once per state dir on the first real interactive UI prompt (`claimOnboarding`), not on `session_start`. Explicit `--onboard` starts immediately in TUI and consumes its process latch at claim, without resetting it on release; isolated-worker host repetition is tracked in #9917. Its injected context and forced-tour bootstrap carry a one-line migration inventory from the shared setup scan (#10478): the scan loads lazily, once, only after a claim actually succeeds (never in `register()`, so sessions that never onboard pay none of its cost, fail-open) and appends the found sign-ins, custom providers, skills and MCP servers; the skill's migration lane starts from that inventory and hand-reads only harnesses the scan does not detect. It also serves the desktop's thread import over RPC (`session-history-rpc.ts`): `omo.onboarding.projects` answers `scanProjects()`, and `omo.onboarding.sessions` answers `{scanId}` and then pushes `omo.onboarding.sessions.event` outcomes from `recentSessions()`, ending with `done` or `failed`. Both are read-only filesystem scans that touch no model and no session. Since #10481 the scan runs lazily on the first onboarding turn with `sessions: false` (#10480: the first prompt never waits on session history, which arrives instead as the bounded background `onboardingHistorySummary` on the next turn) and the context also carries its rows (one per found item) and a value summary computed from it (`onboardingScanContext`); the session digests `onboarding_understand` reads come from a second scan with sessions that starts only when that tool runs; `onboarding_understand` runs the #10480 understanding runtime and returns candidates only, and `onboarding_memory_apply` writes just the user's selection through the memory component's `omo.memory.facts.apply` applier (`memory/facts-apply-rpc.ts`, published process-wide because onboarding registers before memory). The entry bundle carries only those tools' definitions and the applier shell: the tool bodies (`tui-memory-runtime.ts`, and `session-history-tool-runtime.ts` for `onboarding_session_history`), the TUI's own scan assembly (`tui-scan-runtime.ts`), the `omo.onboarding.understand` / `.skip` / `.stop` handler bodies (`createOnboardingUnderstandingHandlers` in `understand-component.ts`; the shell registers the method names synchronously and delegates), the understanding start (`understand-start.ts`) and the validating apply (`memory/facts-apply-runtime.ts`) ship in `extensions/omo-onboarding-scan.js` and load on first use; a sidecar that cannot load answers unavailable with the reason. The component also serves `omo.onboarding.state.get` / `.complete` / `.reset` (`state-rpc.ts`) over `state.ts` (the writes run in the shell; the marker reader `readOnboardingState` ships in the onboarding-scan sidecar and loads on the first state call), so the desktop and the TUI share one state: a held claim (`onboarding-completed`, written at claim with its owner's pid and start identity) reports `inProgress` while that owner runs (a dead or pid-reused owner's claim, or an ownerless older-build claim past 24 hours, is taken over by the next claimant; the reader is `claim-owner.ts` in the sidecar), and only a finished onboarding (the TUI's onboarding turn settling successfully, or `complete`) writes `onboarding-completion` and reports `completed`; a claim released by a failed turn never erases a recorded completion, a claim marker from an older engine still reads as completed, and `reset` clears both plus this process's pending run. The onboarding facts apply keeps the extractor's crash recovery: its envelope in `<facts>/applied/<batchId>/` (ledger + `request.json`) is removed on a terminal outcome, kept when the apply throws or its lock stays busy after planning, and finished by the facts maintenance reconcile (`memory/facts-applied-reconcile.ts`, shipped in the onboarding-scan sidecar and loaded only when `<facts>/applied` exists) or a retry with the same client `batchId` (a UUID v4; a committed batch is answered from its receipt).
+ - `onboarding-scan`: the `omo.onboarding.scan` / `omo.onboarding.apply` RPC (#10478). The scan streams `omo.onboarding.scan.event` frames (`source_started`, `item`, `source_done` / `source_failed`, `done`) from `omo-native/bin/lib/setup-stream.js`, the same stream `omo setup` consumes; apply runs that module's writers for the named stages, once per scan. After those sources it streams `projects` and `sessions` from the engine session-history module (`session-history/`, #10479), counts `{found, read, cap: 5000}`, holding each session digest in memory with the retained scan (nothing reaches disk before consent; the TUI's own scan keeps its digests in memory the same way). Retained scans (at most eight, keyed by a random scanId) and the last captured credential store are held for the whole engine process, not per session, because a client such as the desktop scans on one control session and applies on the next. Custom providers are off unless requested; no model is read and no socket is opened. Apply re-reads every requested stage's targets and compares content hashes against what the scan read: anything that changed answers `{kind:"stale", reason, paths}` and nothing is written (a mid-apply change refuses that stage instead of writing the scan-time copy), and a retained scan older than ten minutes answers stale with the expiry reason. Credential additions go through the engine's own store (`modelRegistry.authStorage`, captured at `session_start`) under its lock, so a token refresh that lands mid-apply is merged, never reverted; with no store the apply refuses rather than writing an unlocked auth.json. The same component serves `omo.onboarding.understand` (+ `.skip`, `.stop`; #10480, contract in `understand-contract.ts`): with `consent: true` and the caller's model it runs the memory facts extractor's own launch path (`launchFactsInProcess` + `record_fact`) over the scan's digests and AGENTS.md per project (`understand-runtime.ts`, `understand-step.ts`), streams step/fact/budget events, and returns fact candidates without writing memory; the model registry is the one the last `session_start` carried. The shell (`scan-component.ts`, contract in `scan-contract.ts`) validates an apply request and takes its scan out of the store synchronously; the stale, credential-store and write steps (`applyRetainedScan`) run in the sidecar. It lazily imports `scan-runtime.ts`, shipped as `extensions/omo-onboarding-scan.js`.
- `init-deep-advisor`: tracks init-deep state (coverage, drift, git-exclude managed blocks) and ships its own QA scripts (`qa-snapshot.sh`, `qa-managed-block.sh`, `qa-rpc-driver.mjs`). Own AGENTS.md.
- `side-panel`: opt-in right-hand column in the TUI (`side_panel` in `omo.json`, default off; `--omo-side-panel` forces it on for one run - senpi coerces a boolean extension flag to `true` and rejects `--no-` forms, so only `omo.json` can switch it off). The entry bundle carries only the registration shell (`index.ts`: the flag, `/side-panel-diff` registered only when the panel can be on for the run, one forwarder per event); the implementation (`runtime.ts`) ships as `extensions/omo-side-panel.js` and is imported on the first session that has the panel on, so a run without it loads none of the panel. The host coupling is isolated in `host-surface.ts`: the live renderer arrives through a zero-height anchor widget, the current layout root is wrapped in a pi-tui `hstack` so the transcript reflows beside the panel, and the original root is restored on `session_shutdown`, which senpi fires on new, resume, fork and quit (`session_before_switch` is not used: the host lets it be cancelled, and the session would carry on without its panel). Handlers on tool, message and turn events start the panel's reads and return at once, because the host awaits them and every tool call queues behind them; a read that lands after its session ended is discarded. A TUI mode switch rebuilds the renderer and moves widget instances across without calling their factories again, so a refresh that finds the root no longer ours re-registers the anchor and attaches to the new renderer. The reverse holds for the widget fallback: once the renderer it fell back on carries the layout seam and a root (back in fullscreen, or a lazily built root), the block steps aside for the column. A renderer without the layout seam falls back to a widget block above the editor; a headless run renders nothing. `visible(viewport)` on the panel entry drops the column below `min_columns`. Rows come from pure builders in `sections/` (session, context, usage, agents, tools, location) that emit `{text, color}` and never touch the host, so the theme is applied once in `body.ts`; the data seams are `panelFactsFrom(eventCtx)` for model, `getContextUsage()` and `getUsageTotals()`, and the task engine's own record store (`createTaskRecordStore` + `parent_session_id` scoping) for delegated children, which keeps the task component untouched. While a child runs, a 1s injected-timer repaint keeps its elapsed time ticking; with nothing running the timer is not armed. The `files` section reads `git status --porcelain=v1 -z` plus both `--numstat` sides through the host's `exec` (feature-detected; absent means the section stays empty), on a 2s floor rather than a watcher, and the branch comes from `.git/HEAD` without a subprocess. The `usage` section, off by default (`sections.usage: false`), is the only part of the side panel that reaches the network: `usage/poller.ts` polls the subscription endpoints for the account `usage/accounts.ts` says is serving - one decision producing the token, the on-screen name and the cache key together, because deriving them separately let a label sit over another account's quota - and it is never constructed when `sections.usage` is off. The credential is looked up under an ORDERED list of provider ids per endpoint (`CLAUDE_PROVIDERS`, `CODEX_PROVIDERS`): senpi renamed its subscription providers (`claude-sdk-oauth` -> `anthropic-subscription`, `openai-codex` -> `chatgpt-subscription`), and reading only the retired name is silent - no credential means no poll, so the bars keep the last numbers and simply age. Health is merged across every listed name because auth.json and the credential pool were renamed on different schedules. Freshness, the shared backoff and the anti-stampede claim are decisions over one machine-wide cache file (`usage/cache.ts`, `$XDG_CACHE_HOME/omo-senpi/side-panel-usage.json`), validated on read because sibling sessions and older versions write it too; a failed poll keeps the previous bars and adds an error line rather than blanking the section. Rows carry an optional `action` (`links.ts`) and are painted as OSC 8 hyperlinks to a private `omo-panel:` scheme, which is the host's only click path into an extension: the alt-screen renderer enables SGR mouse tracking unless a caller opts out (senpi does not), and on a press and release inside one cell with no drag it resolves a hyperlink from its own rendered buffer and calls `openUrl`. `host-surface.ts` claims that callback while the panel is mounted and hands every foreign URL back; ownership cannot be re-checked on dispose the way the layout root's is, because the renderer reaches an extension through a proxy that returns a fresh wrapper per function read. `side_panel.clickable` turns both the painting and the hook off. A clicked child shows its own work: `data/child-output.ts` lays out the text the task engine's own `defaultTranscriptReader` plus `renderTranscript` produce (`mode: "full"`), reached through the `#omo-task-runtime` alias so the entry bundle keeps none of the task graph. The shared viewer also owns the wheel while it is open (`popups/wheel.ts` + `ui.onTerminalInput`, whose listeners the host runs before its own handling and whose `consume` it honours), because the host routes a wheel event to the layout under the pointer and an overlay is composited outside that layout. `/side-panel-diff` reaches the same viewer by name, whose height is owned by `popups/viewport.ts` because the host clamps an overlay by slicing rows off the end; no keyboard chord is registered by default. The `goal` section reads the host's goal store (`goalStoreFile` on the extension context) through `data/goal.ts`, which re-parses only when `mtime`/`size` move and returns nothing for a half-written or invalid record, because that file is rewritten under a live session; no goal event exists in the extension event union, so the block rides the ordinary refresh rather than a subscription, and a clicked objective opens in the same shared viewer. A child the engine suspends keeps its record status (`senpi-task/src/lifecycle/host-session-record.ts` only adds `suspension_reason`), so `data/task-records.ts` folds that reason into a `suspended` panel status for live children only - a reason left on a terminal record describes a past life - and the heading counts parked children apart from running and done while the card names the reason. `suspension_reason` is only half the signal: the ORDINARY suspension sets no reason and shows up solely as `residency_state` leaving `resident`, so `childStatus` treats any non-resident residency on a live record as parked and falls back to the residency state as the reason. `runner_kind` (`host-session` = a session of the shared daemon, `child-process` = the rpc-process runner) and `execution_mode` give the card a `runs` line, because the two lanes fail differently and a card that hides which one is running tells you nothing about why it died. The `memory` section (`data/memory.ts` + `sections/memory.ts`) fills the `sections.memory` switch that has existed since the panel landed: the identity from `resolveMemoryIdentity(memory.agent, cwd, env)` resolved once on mount (it probes the memory root, so a section that is off never resolves one), then durable state only - `readReflectionParkFile` for the reflection circuit breaker, a `readdir` of `facts-queue` minus its own `consumed.json`/`failures.json` bookkeeping for the fact backlog, and the per-session recall pair (`recall/ledger`, `recall/pending`) read but never consumed, since `PendingNudges.take()` deletes what it reads. The kibitzer is read from the one durable trace it leaves: `recall/sidecars/<base64url(session)>/wakes.ndjson`, one closed record per settled wake (`kibitzer/observe.ts`). The panel reads its tail (`KIBITZER_WAKES_TAIL_BYTES`, partial head dropped) and parses fail-closed per line the way the host parses its own nudge file, counting wakes, nudged paths and provider tokens, and flagging a last wake that ended `diagnostic` - three of those in a row are what raise the host's own `omo-kibitzer:gate` notice. A truncated read reports its counts as a floor (`40+ wakes`) rather than claiming a total. Liveness is still absent on purpose: `KibitzerSidecarState` lives in the sidecar process and nothing persists it, so a row claiming "awake" would be guessing. `reflectionParkNextProbeAt` is recomputed from memory-core's exported interval rather than imported, because its module (`worker/park-alert.ts`) drags senpi's entry-renderer surface and the reflection completion runtime behind it. Reads ride the async refresh on a `MEMORY_REFRESH_FLOOR_MS` floor, and a clicked row opens the park detail in the shared viewer.
- `ultrawork`: injects the Senpi ultrawork directive on matching input as a hidden custom message (`pi.sendMessage({customType: 'omo-ultrawork:directive', content: DIRECTIVE, display: false})` followed by `{action: 'continue'}`), backed by `src/components/ultrawork/generated-directive.ts`. On the idle path the user's typed text is never modified; senpi converts the custom message into `role: 'user'` conversation context, so the directive reaches the model but is not rendered in the TUI. A prompt QUEUED mid-stream (the input event carries `streamingBehavior`) instead gets the directive appended inside that one message: senpi drains steering and follow-up queues one message at a time by default and answers each drained message, so a separate hidden message would burn its own turn before the user's ask arrived. Appending rather than prepending is what keeps `/skill:` expansion working on that path. Matching is word-bounded and keyword-only: the `/\b(?:ultrawork|ulw)\b/i` trigger fires on `ulw` as a word, including skill names (`ulw-plan`, `ulw-loop`, `ulw-research`, since `-` and space are boundaries) — overlapping keywords all load their surfaces, with the `skill-pointers` component loading the named skill in the same turn — but not on fused identifiers (`ulwfoo`, `ulw_helper`). Before the test runs, `skill-pointers/strip-quoted-regions.ts` blanks (offset-preserving) inline code spans, fenced code blocks, and already-injected `<omo-*-pointer>`, `<ultrawork-mode>`, and `<omo-ultrawork-reminder>` blocks, so a quoted or relayed mention never arms; the telemetry keyword classifier shares this filter. The remaining guards are structural dedup only: the `omo-senpi-ultrawork-disabled` flag, skipping `source === 'extension'` inputs, and skipping inputs that already carry a matched `<ultrawork-mode>`...`</ultrawork-mode>` tag pair (a lone open-tag mention still arms). For `/skill:` commands on the idle path there is no prepend/append distinction because text is not rewritten: `/skill:ultrawork` passes through untouched (expansion already inlines the directive), a trigger that appears only in the skill NAME does not arm, and senpi's native skill expansion can no longer be disturbed by the hook. The directive is authored senpi-native at `skills/ultrawork/SKILL.md` and ships verbatim; `plugin/scripts/embed-directive.mjs` embeds its body into `src/components/ultrawork/generated-directive.ts` and fails the build when non-senpi harness tokens (multi_agent, update_plan, codex, ...) appear in the source.
- `skill-commands`: makes the bare `/<bundled-skill> args` form that omo's docs, skills, and plan handoffs name (`/ulw-execute`, `/ulw-plan`, `/ulw-loop`, `/init-deep`, ...) behave exactly like `/skill:<name> args` (#9042). Its `input` handler runs before every other omo input handler and rewrites the text to the `/skill:` form, keeping the submission's `interactive`/`rpc` source, so the ulw-plan gate, `ultrawork` (no arming on the skill name), `skill-pointers` dedup, and continuation resets behave as for a typed `/skill:` command; a registered command could only re-submit through `sendUserMessage` with the untrusted `extension` source. The alias steps aside when a prompt template or another command owns the name (`pi.getCommands()`), and a bundled skill that is disabled or not loaded answers with a warning notice instead of reaching the model. On `session_start` it wraps the TUI autocomplete provider so each bare name takes its `skill:<name>` row's place at top level (one row per skill; `/skill:` lists stay senpi's own). `skill-commands/documented-commands.test.ts` fails when a shipped SKILL.md or Native guide names a backticked `/command` that neither a bundled skill, an omo `registerCommand`, nor an engine command provides.
- `skill-pointers`: ONE uniform keyword table, tested against the input with quoted and relayed regions blanked (`strip-quoted-regions.ts`, shared with `ultrawork`), that injects a hidden skill pointer per matched skill — `mass-ulw` on `/\b(?:mass[\s-]*ulw|ulw[\s-]*mass|mulw|meth)\b/i` (pointer instructs `workflow` orchestration), `ulw-plan` on `/\bulw[\s-]*plan\b/i`, `ulw-loop` on `/\bulw[\s-]*loop\b/i`, `ulw-research` on `/\b(?:ulw|<mass alias>)[\s-]*research\b/i` (all any case; spaced/hyphenated/fused spellings). The mass alias group is shared by both patterns via the `MASS_ALIAS` constant, so the aliases carrying no literal "ulw" (`mulw`, `meth`) and the reversed `ulw mass` also stand in for the `ulw` half of a research invocation: "mulw research" loads mass-ulw AND ulw-research, exactly like "mass ulw research". CustomTypes: `omo-mass-ulw:skill-pointer`, `omo-ulw-plan:skill-pointer`, `omo-ulw-loop:skill-pointer`, `omo-ulw-research:skill-pointer`, each `display: false` pointing at that skill's packaged `SKILL.md`. `ulw-research` additionally carries a COMPANION: `ultimate-browsing` (`omo-ultimate-browsing:skill-pointer`), the skill its browsing lanes run on. A companion has no keyword of its own — a bare "ultimate-browsing" mention injects nothing — and rides on every invocation of its parent, including the paths that dedup the parent's own pointer (`/skill:ulw-research`, an expanded `<skill name="ulw-research">` block); it is skipped only when the input already carries `/skill:ultimate-browsing` or an expanded `<skill name="ultimate-browsing">` block, and is injected once even when several keywords name the same parent. Its pointer tells the orchestrator to read the skill in the same turn as ulw-research and to arm every browsing lane with `load_skills: ["ultimate-browsing"]`, so the lane skill no longer depends on one bullet of the 50 KB research body surviving attention (local session mining found 0/23 research runs loading it before the companion existed). A keyword proves a mention, not a request, so the pointer text is conditional ("This message mentions X. If the user of this session is asking to run X, read ... If X is only being discussed, quoted, or relayed from another session, ignore this pointer.") rather than asserting intent; the `ulw-loop` pointer additionally teaches the JS eval import ``const { agentToolkit } = await import(`${env("OMO_AGENT_TOOLKIT_SDK_ROOT")}/sdk.js`)`` and `agentToolkit.status()`, with the SDK binding the session from the host env instead of accepting a session id or spawning a CLI. No cross-keyword negative lookaheads: overlapping mentions all fire, so "mass ulw-loop" injects the mass-ulw AND ulw-loop pointers while ultrawork arms on the same text. Stateless (pointers are a few hundred bytes; every visible mention re-injects). Structural dedup only, applied per skill: `source === 'extension'` inputs, a raw `/skill:<name>` command for that same skill (other mentioned skills still fire), and an already-expanded `<skill name="...">` block; queued prompts (input carrying `streamingBehavior`) get every pointer appended inside that one message so the group stays atomic through senpi's queue drain. Gated by `omo-senpi-skill-pointers-disabled`.
- `ulw-execute-continuation`: records the `agent_end` outcome (the Senpi analog of Codex's Stop hook) and, on `agent_settled`, reads `.omo/boulder.json` and injects a continuation directive when the current session owns an active or paused Prometheus work plan. Every read first runs `reconcileStaleWorks` over that file with the sessions directory from `components/agent-home/resolve-agent-home.ts` (`resolveAgentSessionsDirectory`), so a work abandoned by a dead session is demoted to `paused` + `stale_since` instead of staying `active` forever (#8413); resuming it restores `active`. It uses `senpi:<session_id>` state produced by the `ulw-execute` skill, suppresses repeats by a `work_id:updated_at:completed/total` signature, and caps consecutive continuations at 8 (reset on user input). It registers before `ulw-loop` so active boulder work takes precedence over ulw-loop continuation.
- `ulw-loop`: detects active ulw-loop state (read in-process through the SDK) and injects continuation guidance when the cwd has an incomplete run. Its `agent_end` handler is footer upkeep plus outcome recording only; the continuation itself is delivered on `agent_settled`. It explicitly defers to `ulw-execute-continuation` when boulder state is continuable for the same session.
- Continuation preconditions for BOTH producers (`components/ulw-execute-continuation/agent-end-eligibility.ts`): the decision happens on `agent_settled`, the event the host defines as "no automatic retry, compaction, or queued continuation will run", so a retry-owned turn, a turn held for required auto-compaction, and a user abort that lands after `agent_end` (the host mutates that same event object while its boundary is open) all block continuation without consuming a continuation slot or a dedupe signature. The recorded outcome must additionally be continuable in the host's own terms: a last assistant message that stopped with `stop`/`length` or as a malformed (tool-call-less) `toolUse`, no refusal or sensitive stop under the single `fallback-architect/detection.ts` refusal predicate, and no aborted error `toolResult` after that assistant. Every skip is logged with its reason. Live proof: `scripts/qa/policy-continuation-e2e.mjs`.
- `todo-fanout-reminder`: when ultrawork mode is armed for the session and the todo tool records its first task-adding result (`op` `init` or `append`), appends a once-per-session `<system-reminder>` to that tool result telling the agent to size the work and todos, compute the fan-out decision explicitly (parallelism gain vs spawn and coordination overhead), always surface the delegation decision to the user with its per-part category routing and reasoning (working directly is acceptable when fan-out does not pay, but the decision is stated either way), and keep the todo list obsessively fresh. Ulw detection reads the ultrawork component's shared `SessionArming` ledger (`sharedSessionArming().isArmed(sessionId)`), so compaction semantics stay consistent: the once-per-session gate resets on an accepted `session_compact`, clears on `session_shutdown`, and falls back to an anonymous slot on hosts without session ids. Gated by `omo-senpi-todo-fanout-reminder-disabled`.
- `question-gates`: one `tool_call` handler on `ask_user_question` / `request_user_input` that sets `required: true` in place when the call gates an action (#9774), so a model that omits the flag still gets senpi's refusal ("No answer: do not take the action it gates") instead of "continue on your best judgment". Primary contract: a question header that exactly equals one the skills pin (`PINNED_GATE_HEADERS`): `Approval` (the ulw-plan approval gate), `Authorize` (the ulw-plan final authorization block, mass-ulw's scope, spend and irreversible decisions), `승인` / `권한` in Korean. Every pinned header fits senpi's 12-character header limit, which senpi enforces before any extension sees the call. Fallback: an option label starting with `approve` / `authorize`. Question text is never read; the flag is never set to false; each forced call writes one `debug` line (tool, header, matched rule) and no question text. Disable with `--omo-senpi-question-gates-disabled`.
+ - `claim-receipt` (#10777): at `agent_before_settle` it classifies the final answer's own prose for pass/done/already-exists claims (Korean and English; code spans, fences, quotes, blockquotes, relayed reports, handoff `Ask:`/`Now:`/`Next:` lines, negated and conditional sentences are ignored) and matches each claim against the live branch (`classify.ts`, `evidence.ts`, `match.ts`, all pure). A check counts only when a command actually invokes it: `invocations.ts` splits each command line into shell segments and decides by the executable and its subcommand (through `cd`, env assignments, `timeout`, `bash -c`, `bunx`/`npx` and the heavy-slot wrappers), so `echo bun test`, `grep vitest` or a commit message never count. A claim is supported only by a check of a matching kind that passed before the answer and after the last edit-tool change (`edit`/`write`/`apply_patch`); it is flagged when no check of any kind ran, when the latest decisive matching check from a structured source (bash/host `tool.bash` results, terminal exit codes, `lsp_diagnostics`) failed (a later unreadable run does not hide it unless the claim quotes concrete numbers), or when an edit landed after the last pass. `exists` claims are proven by lookups (`read`, `grep`, symbol lookups, `rg`/`grep`/`cat` commands) or any passing check and are never failed by an unrelated check; `done` and `works` claims are not judged in sessions with no code edit. A monitor that failed to start or timed out is not a check. Eval-cell evidence can only support a claim: cell failures and cell writes are never attributed, because cells mix probes, log greps, scratch files and composed reports. It returns boundary entries only, never `continue` and never `sendMessage`: a `custom` `omo-claim-receipt:verdict` record per claim-bearing answer and, at most once per user-sourced `input` (re-armed by an accepted compaction), a `display: true` `omo-claim-receipt:nudge` custom message that rides the next provider request. A `task` spawn of a `*gate-reviewer` gets a `<claim-receipt-flags>` block appended to its prompt listing the unsupported claims since the last user input. `omo-senpi-claim-receipt-nudge-disabled` keeps verdicts and gate flags but hides the nudge. `scripts/qa/claim-receipt-replay.mjs` runs the same pure functions over saved session JSONL files; `scripts/qa/claim-receipt-e2e.mjs` is the live RPC proof. Design credit: the idea is inspired by dinobot by roach (PolyForm Noncommercial); no dinobot code, tests or prompt text were used.
- `git-master`: appends an opt-in commit-footer directive to git-master skill content based on the `git_master` section of `omo.json` (`commit_footer`, default `false`; `include_co_authored_by` is a deprecated no-op and no `Co-authored-by` trailer is ever emitted, so a default install appends nothing). Two channels: a `tool_result` hook rides on successful `read` results whose path ends in `git-master/SKILL.md`, and the task component's `createTaskSkillLoader` patches the resolved `git-master` block for `load_skills` children. Settings load lazily per matching event through `loadSenpiOmoConfig`, so an `omo.json` toggle applies to the next skill read without a restart; when `commit_footer` is off nothing is appended.
- `fallback-architect`: when senpi's retry-fallback controller moves the session off `claude-fable-5` because the model refused or the provider rejected the request under Anthropic's Usage Policy, it injects one hidden `omo-fallback-architect:directive` message telling the weaker active model to decompose the problem and consult `task(category: "architect")` with self-contained per-part queries. Detection uses only the extension surface: `message_end` supplies the refusal signal and `model_select` with `source: "fallback"` supplies the switch, with the refusal predicate in `detection.ts` mirroring senpi `isClassifierRefusal` (`packages/ai/src/utils/stop-details.ts`) including its stop-reason-first ordering. It fires only when `loadOmoConfig` reports an enabled `architect` category, and a compact reminder then rides on each later user prompt until fable 5 is active again. The reminder is ALWAYS a hidden custom message (`display: false`) and the typed text is never rewritten: on the mid-stream path senpi steers a custom message into the running turn (`sendCustomMessage` -> `agent.steer`), so hiding it costs no extra assistant turn for queued prompts either — the old transform-append leaked the reminder into the user's own bubble in the TUI. Arming also emits one user-VISIBLE `omo-fallback-architect:notice` custom message (`display: true`, structured `details: { from, to }`) framing the switch as an upgrade (the fallback model drives execution while Fable-5-grade reasoning stays reachable through the architect lane); `notice.ts` owns the copy plus a registered TUI message renderer, and the stable customType + details ride senpi's session/event stream so GUI surfaces (omo-desktop-app) can render the same event later without senpi core changes. Gated by `omo-senpi-fallback-architect-disabled`.
- `comment-checker`: runs the shared comment-checker flow after write-like tool results when a resolver finds the binary.
- `telemetry`: sends the anonymous once-per-UTC-day `omo_senpi_daily_active` event, with product-specific opt-outs. Own AGENTS.md.
- `ast-grep`: registers the ast-grep MCP (stdio); node-executable, cwd, and entry resolution are injectable for tests.
- `builtin-mcps`: registers the two remote MCP servers the OpenCode edition injects at runtime — `context7` (`https://mcp.context7.com/mcp`) and `grep_app` (`https://mcp.grep.app`) — as senpi `type: "http"` declarations, `enabled: true` and `lifecycle: "lazy"`. `context7` is anonymous until `CONTEXT7_API_KEY` holds a non-placeholder value (same normalization as `packages/omo-opencode/src/mcp/context7.ts`), and then switches to `auth: "bearer"` with `bearerTokenEnv: "CONTEXT7_API_KEY"` — never a literal token in `headers`. No websearch server (senpi ships `websearch`/`webfetch` builtins) and no MCP-shaped LSP (the `lsp` component registers direct tools). A trusted `mcp.json` entry of the same name wins over the extension declaration, so `{ "mcpServers": { "context7": { "enabled": false } } }` is the per-server off switch; `omo-senpi-builtin-mcps-disabled` drops both. Own AGENTS.md.
- `lsp`: registers direct LSP tools and optional post-edit diagnostics through the packaged shared LSP daemon runtime. The Senpi adapter owns only descriptors, schemas, renderers, path extraction, and project-config migration warnings. Own AGENTS.md.
- `x-search`: credential-gated `x_search` tool plus a conditional skill. Registers at extension load when `<agentDir>/auth.json` has an `xai` `oauth`/`api_key` entry (or `XAI_API_KEY` when that file is absent) so `tool_search` sees the tool in the same session; contributes `plugin/skills-conditional/x-search/SKILL.md` via `resources_discover` only then. No `omo.json` keys. Own AGENTS.md.
- `computer-use`: desktop computer use (#8893). On a supported host (macOS, Linux, Windows) whose `computer.enabled` resolves true, it registers these at extension load, like `x-search`, so tool_search indexes them at session_start:
- The search-exposed `computer` tool (from `@oh-my-opencode/senpi-desktop-tool`), carrying its eval-kernel `kernelPrelude` and a read/exec `permissionParser`.
- `computer_actions`, only when `computer.cua_adapter` is set.
- `/computer` (on, off, status, stop, resume; stop and resume are user-only).
- The computer-use skill, through `resources_discover`, unless `disabled_skills` names it or a skill of that name is already loaded (a user, project, settings or package skill). Then it yields without a collision diagnostic and `/computer status` names the winner (`bundled-skills/contributed-skill.ts`, shared with `x-search`, #9160).
The registration shell (settings, `/computer`, tool definitions from `@oh-my-opencode/senpi-desktop-tool/registration`, permission parsers, telemetry hooks) is all `omo.js` carries; the desktop service, engine child, handle, and `execute` live in `runtime.ts`, shipped as `extensions/omo-computer-use.js` and imported (`#omo-computer-use-runtime`) on the first `/computer`, `tool_activated` naming a computer tool, or tool call (#9113). The prelude and doc texts are NOT in any bundle: `senpi-desktop-prelude` generates them into `assets.generated.json`, the build stages it as `extensions/assets.generated.json`, and the definition's `kernelPrelude`/skill read it once per process through memoized getters (`senpi-desktop-prelude/src/assets.ts`), pinned byte-identical to dev by sha256 fixtures. Nothing starts until the tool is activated: a `tool_activated` event naming `computer` opens the engine session and arms the stop chord. Settings come from the omo `computer` block, snake_case and native-only, mapped onto desktop-tool's resolver. A host without `getActiveTools`, `setActiveTools` or `executeTool`, or an invalid block, registers nothing and warns. The permission tiers and the eval global need a senpi that reads `ToolDefinition.permissionParser` and `kernelPrelude` (code-yeongyu/senpi#2178). An older host ignores both fields, so the tool works without tiers or the global. Live proof: `scripts/qa/computer-use-e2e.mjs`.
- `memory`: Letta-Code-style persistent agent memory as a thin adapter over the harness-neutral `@oh-my-opencode/memory-core` engine (zero Senpi imports; local-capable-matrix parity with letta-code@a75f4d93e). Identity is config-resolved (`memory.agent`, default `"auto"` = per-project derived id; repos under `~/.omo/memory/agents/<safe-id>/`, overridable via `OMO_MEMORY_HOME`) and bound at `session_start` with a hidden `senpi-memory.session-binding` entry; resume identity conflicts fail closed, enablement latches at session start (config-watch reloads only notify restart-required), and memory is toggled by `memory.enabled` (default ON) or the `omo-senpi-memory-disabled` flag. Per run it composes (never clobbers) a sentinel-delimited compiled memory block through `before_agent_start`, committed-HEAD-only and cached by (template, HEAD). Registers two sequential tools (`memory`, `memory_apply_patch` with letta-exact semantics incl. first-occurrence `str_replace` and clean-check->commit; excluded from senpi-task children via `uiOnlyToolNames`), ten slash commands (`/memory /memfs /remember /init /doctor /recompile /memory-repository /sleeptime /reflect /search`), a self-contained palace HTML viewer (0600/0700, machine-gated), a `tool_call` soft cross-identity guard plus a registered filesystem policy when the host exposes `registerFilesystemPolicy`, memfs skill scope via `resources_discover`, and a journal/debug status line with an advisory at `compile_warn_tokens` (30000). Background reflection ("dreaming") is event-triggered (step-count off by default, compaction on by default, or `/reflect --recent N|--conversation ids`): on successful `agent_settled` a per-identity reservation (one active + single pending, cross-process locked) spawns a DETACHED `senpi -p` child resolved through the omo `quick` category in a git worktree (env `MEMORY_DIR`/`TRANSCRIPT_PATH`, hard wall-clock deadline, completion validation, `--no-ff` merge or explicit integration), with completions recorded durably (`runtime/reflection/completions`) and delivered as non-model-facing entries + notifications; cursor advances only on `merged|no_changes`. Declared divergences (documented in `src/components/memory/AGENTS.md`): no Letta Cloud rows, no mods-in-memory, no arena/channels, text-only local search semantics, no mid-conversation `<memory_update>` special case, no `/reflect --auto` selector or external-transcript staging, no recall subagent/bootstrap injection, reflection sandbox defaults to `auto` (letta fail-closed `required` optional). Registered after `task`, before `config-watch`.
- `task`: loads the unified `omo.jsonc` view at register (via `config-resolution`), composes the task engine over `@oh-my-opencode/senpi-task`, and registers the 4 task tools (`task`, `task_send`, `task_cancel`, `task_output`) plus the 6 lead-only team tools (`team_create`, `team_delete`, `task_create`, `task_get`, `task_list`, `task_update`).
The component also publishes optional `wake_source_state` snapshots under source `senpi-task` for the current session's non-terminal background children and owned team members. Store mutations emit only when the count changes, session start re-emits after child reconciliation and liveness observation, and session shutdown emits zero; older Senpi hosts without `pi.events` remain supported.
The engine overlays four builtin curated read-only subagents (`explore`, `librarian`, `plan-consultant`, `plan-reviewer`) under the omo.json `agents` record, so any session can delegate via `task(subagent_type: "<name>")` with zero configuration; omo.json `agents.<name>` replaces individual builtin fields field-level while unset fields keep the builtin, and `disable: true` hides one from the task tool description and spawn resolution even when a request supplies an explicit model. Curated agents are pinned to in-process execution (their `execution_mode` override is ignored) and are rejected as team members because process-mode member spawns drop the persona prompt and tool policy. The component also wires the plan-gated agent tier: `components/task/skill-invocation-tracker.ts` records three per-session channels - invocations from `read` tool results on `*/skills/<name>/SKILL.md`, raw `/skill:<name>` inputs, and expanded `<skill name="...">` blocks (senpi expands the slash command into that block BEFORE the input event, so the block's NAME ATTRIBUTE is the real channel; matching the block body instead would let any skill that merely mentions ulw-plan arm the gate); USER requests from user input either naming ulw-plan or asking for a plan before coding in their own words (the clause the ulw-plan SKILL.md contract promises), after stripping injected `<ultrawork-mode>`/`<system-reminder>` blocks and after dropping input whose `source` is `"extension"` because extension-injected text is agent-manufacturable; and plan artifacts from successful read/write/edit on `.omo/plans/*.md` paths at any root (worktrees included) or apply_patch bodies touching one (state is dropped on `session_shutdown`; `load_skills` on a spawn arms the child and is deliberately not a parent-session record) - and `createTaskTool` receives it as `resolveSkillInvocations`, so `plan-consultant`/`plan-reviewer` spawn only when the user explicitly requested `ulw-plan`, a plan artifact exists, and `ulw-execute` was never invoked (the classification and verdict live in senpi-task `agents/invocation-guard.ts`). Their nine-name tool surface replaces Senpi's general `bash` with a structured read-only GitHub/HTTPS broker and excludes direct edit/write plus mutating LSP tools. Team sends are durable file-only writes. The adapter owns one 1-second lead poller per team led by the current session; process members load the scoped member extension with only `task_send` and receive lead mail steered into the resident member's running turn. Session shutdown is reason-aware: the `session_shutdown` payload reason (`quit`/`reload`/`new`/`resume`/`fork`) is threaded into a scoped suspension of that session's children instead of teardown, and a missing session id fails closed with a warning so nothing is suspended. `task.resume_children` (default `true`) gates this; `false` restores the pre-feature dispose-at-shutdown behavior. It wires the ordered session-start recovery chain (flush or drop buffered completions, `reconcileOnSessionStart(sessionId)` reviving the resumed session's suspended children - an undefined session id still runs the legacy crash-orphan sweep - owned-member liveness re-observe, member/lead reservation reclaim, unnotified-completion redelivery, awaited TTL cleanup, owned-lead poll, status sync), transition suspension, a completion-message renderer, the `/tasks` and `/task-kill` slash commands, and the status-UI footer, which labels suspended children `suspended`. Gated by the `--no-omo-task` flag and skipped when required ExtensionAPI capabilities are missing.
- `thread`: registered right after `task`, and owns the nine cross-session tools (`thread_create`, `thread_list`, `thread_read`, `thread_send`, `thread_interrupt`, `thread_handoff`, `thread_rename`, `thread_set_model`, `thread_set_reasoning`), the eight chat-thread relay tools (`thread_bind`, `thread_unbind`, `thread_rebind`, `thread_bindings`, `thread_report`, `thread_outbox`, `thread_outbox_ack`, `thread_answer`) over the session gateway store, plus their addressing, transcript, receipt, prompt-routing, and tool-search metadata surfaces. Registration is unconditional; the tools talk to Senpi's existing multi-session socket and, when that socket is absent, each call returns `host_unavailable` as data. Own AGENTS.md: `src/components/thread/AGENTS.md`.
- `config-watch`: registers the resolved user and project `.omo` configuration chain with Senpi's optional `config-watch` event protocol. Its dry-run validation rejects new config diagnostics before the host reloads the extension; it safely skips with a warning on older Senpi APIs without the optional events capability. The user config directory is `~/.omo`; when it does not yet exist, its only parent is `$HOME`. Whenever the senpi agent dir sits under `$HOME` — including the default `~/.senpi/agent` — the bare-`$HOME` creation target is dropped by the protected-path filter below, so `userConfigCreationDiscovery` reports `reload_required` and later user-scope creation is discovered on the next session start. With `SENPI_CODING_AGENT_DIR` pointed outside `$HOME` the target survives and creation stays watched. Either way the flag is derived from the surviving targets rather than from directory existence, so it never claims a watch the host never received. Targets that cover the senpi agent dir's protected paths (`auth.json`, `sessions/`, `logs/` under `SENPI_CODING_AGENT_DIR`, default `~/.senpi/agent`) are filtered out of the resolution because the host rejects them deterministically — practically this drops the bare-`$HOME` ancestor target, so a NEW `.omo` created directly in the `$HOME` root is discovered only on the next session start. Rejections are never re-registered synchronously (the host rejects on the REGISTER stack, so a sync re-emit recurses until stack overflow): the refresh is deferred via `setTimeout(0)` and capped at 3 retries per registration-payload fingerprint, resetting when the payload changes.
`packages/omo-opencode` is a separate build that still uses its prior task/team names; cross-edition parity is a deliberate follow-up outside this adapter. Rules are intentionally not a Senpi component (Senpi has builtin rules, so this adapter must not add a `rules` component just to mirror Codex or OpenCode).
The adapter depends on `@oh-my-opencode/senpi-task` (task engine + tool factories), `@oh-my-opencode/omo-config-core` (`loadOmoConfig` + `resolveModelReferences` + the migration engine), `@oh-my-opencode/omo-opencode/config-migration` (dependency-clean legacy discovery + transform consumed by `config-startup`), `@oh-my-opencode/delegate-core`, `@oh-my-opencode/team-core`, `@oh-my-opencode/boulder-state` (Boulder work-plan state for `ulw-execute-continuation`), `@oh-my-opencode/comment-checker-core`, `@oh-my-opencode/telemetry-core`, `@oh-my-opencode/prompts-core`, `@oh-my-opencode/lsp-core`, `@code-yeongyu/lsp-daemon`, and `@oh-my-opencode/utils`, with `@code-yeongyu/senpi` as an optional peer (`package.json`).
## Build And Packaging
Build outputs under `plugin/extensions/` and `plugin/skills/` are generated and gitignored (only the senpi-local `init-deep`/`onboarding` overrides in `plugin/skills/` are tracked). Do not hand-edit them. The artifact map and pipeline live in `plugin/AGENTS.md` and `plugin/scripts/AGENTS.md`.
- `node packages/omo-senpi/plugin/scripts/build-extension.mjs [--check]` builds/verifies all six extension artifacts (main `omo.js` from `src/extension/bundled-index.ts`, task, member, memory MCP, supervisor, advisor).
- `node packages/omo-senpi/plugin/scripts/sync-skills.mjs` syncs Senpi-ready skills into `plugin/skills/` from three pools: component-owned native source shipped verbatim (`ulw-loop`), native `skills/` sources shipped verbatim (`dag-library`, `give-me-tips`, `hyperplan`, `init-deep`, `mass-ulw`, `onboarding`, `ultrawork`, `ulw-plan`, `ulw-research`, `visualize`; `init-deep` and `ulw-plan` are senpi-local overrides whose `nativeSkillNames` entries shadow the shared-pool copies so the shared files stay untouched for other editions, and `ulw-plan` is seeded from the fully senpi-adapted bundle output), and the repo `shared-skills` pool (ulw-execute gets a `codex:`->`senpi:` overlay; shared skills get a Senpi tool-compatibility banner).
- `bun packages/omo-senpi/plugin/scripts/stage-x-search-skill.mjs` copies the component-owned `x-search` SKILL.md into `plugin/skills-conditional/` (not `plugin/skills/`) so `pi.skills` never loads it eagerly; `--check` verifies the staged copy matches source.
- Peer-external build rule: the extension build must externalize the Senpi peer/import family so shared core packages stay harness-neutral and Senpi resolves those peers from the installed Senpi runtime. Keep `SENPI_LOADER_ALIASES` in `plugin/scripts/build-extension.mjs` aligned with `src/bundle-purity.test.ts`, including `@code-yeongyu/senpi`, `@earendil-works/pi-*`, `@mariozechner/pi-*`, the TypeBox aliases required by Senpi's loader, and Node builtins.
## QA
For adapter code changes, run the narrowest relevant unit tests plus the Senpi package gate; the full driver map, shared hubs, and anti-patterns live in `scripts/qa/AGENTS.md`:
```sh
tsgo --noEmit -p packages/omo-senpi/tsconfig.json
bun run test:senpi
SENPI_BIN="$(command -v senpi)" node packages/omo-senpi/scripts/qa/task-e2e.mjs
SENPI_BIN="$(command -v senpi)" node packages/omo-senpi/scripts/qa/team-e2e.mjs
```
`drive.mjs` and the task/team live drivers create isolated Senpi agent directories and ignore caller `SENPI_CODING_AGENT_DIR`; if the Senpi binary is unavailable they report `SKIP` or `FAIL` in final JSON instead of touching the real `~/.senpi/agent`. `task-13.test.ts` exercises the task engine wiring, `task-e2e.mjs` covers single and batch task lifecycles, `team-e2e.mjs` covers injection-driven delivery, shutdown-via-`task_send`, stale-reservation reclaim, member-liveness events, and kill/restart exactly-once recovery, and `task-rpc-e2e.mjs --self-test` pins the RPC driver scripts. The `@oh-my-opencode/senpi-task` unit + chaos suites (`bun test packages/senpi-task`) cover the state machine, runners, and completion invariants; the engine's standalone manual drivers live under `packages/senpi-task/scripts/` (see [`packages/senpi-task/AGENTS.md`](../senpi-task/AGENTS.md)).
## Evidence Rules
Live Senpi QA evidence goes under `.omo/evidence/omo-senpi-adapter/` (gitignored: the files stay local, the PR body carries the summary, nothing under it is committed), one subdirectory per change or task, resolved ONLY with the `senpi-qa` skill's `resolve-evidence-dir.mjs` rather than typed by hand (the invocation is recorded in the root AGENTS.md Senpi QA section; the script returns an absolute path, creates nothing, and rejects separators, `.`/`..`, traversal, absolute paths, a non-git root, and stray roots such as `local-ignore/qa-evidence/`). Record: the command or manual action run; the behavior it was meant to prove; the observed result including final JSON from the QA driver when present; isolation proof, especially the sandbox `SENPI_CODING_AGENT_DIR` and whether the real Senpi agent dir stayed untouched; and omitted or redacted material, especially raw logs that could contain secrets. Do not claim live Senpi QA from unit tests alone. `bun run test:senpi` is the package gate; the scripts in `scripts/qa/` are the real harness proof, and the `senpi-qa` skill (`.agents/skills/senpi-qa/`) routes a change to the right driver and owns the evidence-path contract.
## Engine hosts per session
Every session's `process` task children run on that session's own engine host (`p-<key>.sock` under `OMO_RPC_SHARD_ROOT` or `<agentDir>/rpc/shards`, routed by `src/components/task/shard-routing.ts`); a child's own children stay on the host it lives on, and `OMO_RPC_SOCKET*` never route task children. `rpc.sock` remains the operator endpoint (`omo daemon run`) and the endpoint the thread tools create sessions on; a terminal session never joins a host, and `omo daemon adopt` moves a host session into a terminal. The plugin ships `plugin/daemon-launch-spec.json` - the ONLY argv source for every host (`omo daemon`, child ensures and the desktop server all read it). Rebuild the plugin to change it; never hand-edit. `task.default_execution_mode: auto` routes children into the session's host when it is reachable and capable, in-process otherwise. No cap limits hosts, children or memory; the measured cost of isolation, the migration and the rollback runbook are in `docs/reference/omo-daemon.md`.
The `/memfs` registrar remains synchronous; its handlers are loaded on first invocation from `src/components/memory/commands/memfs-runtime.ts` via `#omo-memory-memfs-runtime`, mapped to `extensions/omo-memory-memfs.js` in the published plugin. Build freshness, required artifacts and bundle audits include this sidecar.
The `/palace` and `/people` registrars remain synchronous (same names, descriptions and argument hints); the palace HTML generator, its collectors and the `/people` runtime load on the first invocation of either command from `src/components/memory/palace/runtime.ts` via `#omo-memory-palace-runtime`, mapped to `extensions/omo-memory-palace.js` in the published plugin. One import is shared per process; a failed load is not cached and is reported as an actionable error (reinstall omo, then start a new session) instead of an unhandled rejection. Build freshness, required artifacts and bundle audits include this sidecar.
+
+ The onboarding session-history scan (transcript discovery, parsing, digests) loads on the first `omo.onboarding.projects`/`omo.onboarding.sessions` call or `onboarding_session_history` tool run from `src/components/onboarding/session-history/runtime.ts` via `#omo-session-history-runtime`, mapped to `extensions/omo-session-history.js` in the published plugin. The RPC methods and the tool still register synchronously; one import is shared per process, and a failed load is not cached and is reported as an actionable error (reinstall omo, then start a new session). Build freshness, required artifacts and bundle audits include this sidecar.
The live thread RPC client is loaded on the first asynchronous thread/gateway operation via `#omo-thread-client-runtime` (`src/components/thread/live-surface.ts` in source, `extensions/omo-thread-client.js` in the plugin). The component still registers tools, the store and lifecycle handlers synchronously; the registrar captures the operator socket before loading. One runtime promise/client is shared per facade across concurrent operations. A failed load is not cached: it rejects as `host_unavailable:<socket>` (classified by the thread tools) and the next operation loads again. Build freshness, required installation artifacts and bundle purity include this sidecar.