/** * dsh (DeepSeek Harness) cordis plugin entry point — `src/dsh-plugin.ts` * * Exports a cordis plugin per the conventions verified in * `docs/dsh-plugin-contract.md` against the dsh 0.1.5-rc.1 source checkout * (`@deepseek-ai/cordis@4.0.2`, `@deepseek-ai/dsh-tools@0.1.5-rc.1`, ...): * * - `name` — `'rolebox'` * - `inject` — the dsh services rolebox's adapters consume: `tools` * (tool registration, §3.1), `sessions` (session lifecycle, * §4.1), `subagents` (agent catalog, §4.3). The live-agent * `agents` service (§4.2) is deliberately NOT in the `inject` * roster: `DshAgentRegistrar` manages the *catalog* of * spawnable definitions through `ctx.subagents` and explicitly * keeps the `ctx.agents` AgentRegistry side out of the catalog * seam (see `src/platform/adapters/dsh/agent-registrar.ts` * module docstring). It is instead probed OPTIONALLY * ({@link probeAgentRegistry}) as the graph-notify injection * seam — the dsh host's per-session message-injection surface * — so graph `` reminders reach the * orchestrator. Injecting it in the roster would gate plugin * activation on it; probing it lets minimal/headless profiles * boot with graph-notify degraded instead. * - `Config` — a StandardSchemaV1 config schema (contract §2.4) * - `apply(ctx, config)` — bootstrap + wire the dsh adapters * * ── Config mechanism (contract §2.4) ────────────────────────────────────── * The contract verified that cordis 4.0.1's `Config` field is typed * `StandardSchemaV1` and that schemas implementing the standard * `'~standard'` interface work directly as a plugin Config (defaults applied, * invalid values rejected). The dsh packages use the * `@deepseek-ai/schemastery` fork as their schema DSL; this repo does not * depend on that fork (or any `@deepseek-ai/*` package — the adapters are * deliberately SDK-free, structural). zod v4 — already a rolebox dependency — * implements the same `StandardSchemaV1` interface (`~standard`), which is the * exact mechanism the contract verified. So `Config` below is a zod schema: * identical mechanism, no new dependency. `live:` verified on zod@4.1.8: * `schema['~standard'].validate({})` → `{value:{...defaults}}` and invalid * input → `{issues:[...]}`. * * ── Tool registration ───────────────────────────────────────────────────── * `DshToolFactory.compileAll(buildCanonicalTools(...))` produces objects * structurally matching the verified `ToolDefinition` register input * (`DshToolDefinition` — name/description/parameters/output/execute, * contract §3.2); `ctx.tools.register(def)` consumes them (§3.1). The real * dsh registry stores the definition raw (no `defineTool()` compile step); * the structural contract is identical, so direct registration is safe. * * MUST NOT import `@opencode-ai/plugin` (or any platform SDK). * * @module */ import { z } from "zod"; import type { DshSpawnDelegate, DshSubagentProvider } from "../platform/adapters/dsh/agent-registrar.ts"; import type { DshSubagentDispatchRuntime } from "../platform/adapters/dsh/dispatch.ts"; import type { DshToolDefinition } from "../platform/adapters/dsh/tool-factory.ts"; import type { DshPromptInjector, DshSessionStoreLike } from "../platform/adapters/dsh/session.ts"; import type { ResolvedRole } from "../types.ts"; /** Plugin name — the cordis fiber/logger label (contract §2.2). */ export declare const name = "rolebox"; /** * dsh services this plugin waits for (contract §2.2 `inject`). * `tools` / `sessions` / `subagents` are the services rolebox's dsh adapters * consume; see the module docstring for why the live-agent `agents` service is * probed optionally (graph-notify) rather than injected. */ export declare const inject: string[]; /** * Plugin config schema — a zod v4 schema implementing the StandardSchemaV1 * interface cordis 4.0.1 requires for `Config` (see module docstring). * * All options are optional: * - `roleboxDir` — override the rolebox directory (default: * `{cwd}/rolebox` if present, else `{dsh home}/rolebox`) * - `skillsDir` — override the global skills directory (default: * `{dsh home}/skills`) * - `defaultRole` — role id (directory name) promoted to primary * - `enabledNamespaces` — allow-list of tool names / name-space prefixes; * `"*"` or absent registers every assembled tool * - `onSpawn` — programmatic spawn delegate (a host seam, NOT * representable in YAML); when supplied, registered * providers delegate to it. When omitted, they fall * back to the host provider named by * `spawnProviderName` (default `"spawn"`); only when * that provider is unregistered do they reject with * `DshSpawnNotWiredError` * - `spawnProviderName` — YAML-representable name of the `ctx.subagents` * provider rolebox delegates real spawning to when * `onSpawn` is absent (default `"spawn"`) * * There is deliberately no web-server config: the role-switch UI now mounts * on dsh's own host webserver via the optional `webServer` service seam (see * {@link probeWebServer}) and the `dsh.client` slot plugin — no bind host or * port belongs on this plugin. */ export declare const Config: z.ZodObject<{ roleboxDir: z.ZodOptional; skillsDir: z.ZodOptional; defaultRole: z.ZodOptional; enabledNamespaces: z.ZodOptional>; onSpawn: z.ZodOptional>; spawnProviderName: z.ZodOptional; }, z.core.$strip>; /** Inferred config type — the object passed to `apply(ctx, config)`. */ export type DshPluginConfig = z.infer; /** The dsh tool registry seam this plugin consumes (contract §3.1). */ export interface DshToolsRegistry { /** * Register a tool definition. Returns the disposer that removes it. * @param definition - A compiled tool definition (DshToolDefinition). */ register(definition: DshToolDefinition): () => void; } /** * Minimal structural surface of the cordis `Context` this plugin consumes. * Mirrors the documented cordis context (§2.5 — property reads resolve * services, `on` subscribes to events) plus the three injected dsh services. * The real dsh host supplies the full Context; tests inject a fake double. */ export interface DshPluginContext { /** dsh tools service (`ToolRuntime`, contract §3.1). */ tools: DshToolsRegistry; /** dsh session service (`SessionStore`, contract §4.1). */ sessions: DshSessionStoreLike; /** * dsh subagent service (`SubagentRuntime`, contract §4.3). Typed as the * dispatch superset (adds `start`) because this plugin both syncs agents * into the catalog (via {@link DshAgentRegistrar}) and dispatches graph * nodes / loop rounds through `ctx.subagents.start` (via * {@link DshDispatchAdapter}). */ subagents: DshSubagentDispatchRuntime; /** * The dsh system-prompt registry service (`@deepseek-ai/dsh-system-prompt`, * structural subset — see {@link DshSystemPromptRegistry}). Present only in * full profiles; headless profiles have no model-facing prompt assembly, so * the property is absent and the plugin degrades gracefully (see * {@link probeSystemPrompt}). Never injected via the `inject` roster — an * optional service must not gate plugin activation. */ systemPrompt?: unknown; /** * The dsh live-agent registry service (`@deepseek-ai/dsh-agent` `AgentRegistry`, * structural subset — see {@link DshAgentRegistryLike}). Present only in full * profiles where the agent-loop bundle rows are mounted; headless / minimal * profiles have no live agent registry, so the property is absent and graph * notify degrades gracefully (see {@link probeAgentRegistry}). Never * injected via the `inject` roster — an optional service must not gate plugin * activation. */ agents?: unknown; /** * The dsh llm service (`@deepseek-ai/dsh-llm` `LlmRuntime`, structural subset * — see {@link DshLlmRuntimeLike}). Always mounted in a full profile; probed * optionally by {@link probeLlmRoutes} so the agent registrar can degrade a * split model whose provider route has no registered adapter to a model-only * override instead of failing the spawn with `NO_ADAPTER`. Never injected via * the `inject` roster — an optional service must not gate plugin activation. */ llm?: unknown; /** * The dsh skill-registry service (`@deepseek-ai/dsh-skill` `ctx.skills`, * structural subset — see {@link DshSkillRegistryLike}). Present whenever the * profile mounts the `dsh-skill` registry row that resolves model- and * user-facing skills; headless / minimal profiles without it have the * property absent and rolebox's lazy skill provider is simply not registered * (see {@link probeSkillRegistry}). Never injected via the `inject` roster — * an optional service must not gate plugin activation. */ skills?: unknown; /** * Resolve a cordis service by name (optional-service seam). The dsh host * context resolves any registered service; this plugin probes for * `"webServer"` (present only when the web profile is active) and skips * gracefully when it is absent — headless profiles have no web server. */ get(name: string): unknown; /** Subscribe to a cordis/dsh event (contract §2.5). */ on(event: string, listener: (...args: unknown[]) => void): (() => void) | void; /** Emit a cordis/dsh event. */ emit(event: string, ...args: unknown[]): void; } /** Bootstrap + wiring statistics exposed on the returned disposer. */ export interface DshPluginStats { /** Roles discovered on disk. */ discovered: number; /** Roles successfully resolved. */ resolved: number; /** Roles that failed resolution. */ skipped: number; /** Tools registered into `ctx.tools` (after the namespace filter). */ registeredTools: number; /** Agents registered into `ctx.subagents` (roles + subagents). */ registeredAgents: number; /** The resolved roles (post `defaultRole` promotion). */ resolvedRoles: ResolvedRole[]; /** * Dispatch mode — always `"dsh"` on this platform. The graph engine and * loop mode dispatch subagent sessions through the dsh subagent seam * (`ctx.subagents.start` / the dsh session service) instead of the opencode * SDK client (see {@link DshDispatchAdapter}). */ dispatchMode: "dsh"; /** * Whether the loop coordinator was wired to the dsh dispatch adapter. * `false` would indicate a wiring failure (apply still degrades to * graph-only orchestration). */ loopWired: boolean; /** * Whether the `/rolebox` role-switch routes were registered on dsh's host * web server. `true` only when the optional `webServer` service was present * on the ctx (the web profile) AND registration succeeded; headless * profiles have no web server, so this stays `false` and the plugin keeps * running. The role-switch surface shares ONE `/rolebox` prefix * registration with the monitor surface (see `monitorRouteRegistered`) — * the real host webserver rejects duplicate prefix routes. */ webRouteRegistered: boolean; /** * Whether the `/rolebox` monitor routes (`/status`, `/metrics`) were * registered on dsh's host web server. Mirrors `webRouteRegistered` — the * role-switch and monitor surfaces are composed into a single `/rolebox` * prefix registration, so both flags are set from the same registration * outcome. `true` only when the optional `webServer` service was present * AND registration succeeded; headless profiles stay `false` and the * plugin keeps running. */ monitorRouteRegistered: boolean; /** * Whether graph-notify was wired on the dsh path. `true` only when the * optional live-agent registry (`ctx.agents`) was present at boot, so the * session adapter's `prompt()` can route graph `` reminders * into the target session's agent. When `false` (minimal/headless profiles * with no `ctx.agents`) the graph engine still assembles with a `graphNotify` * config, but `prompt()` degrades to its no-op and the F6 notifier marks the * degraded reminder instead of delivering one. */ graphNotifyWired: boolean; } /** * The fiber disposer returned by `apply()` (cordis convention, contract §8 * appendix: "return () => {...}; // fiber disposer"). Also carries the * `stats` from this boot so callers/tests can observe the bootstrap outcome. */ export interface DshPluginDisposer { /** Clean up tool registrations, hook listeners, and agent registrations. */ (): void; /** Bootstrap + wiring statistics for this apply() run. */ stats: DshPluginStats; /** * In-process role-reload seam: replace the four role-snapshot tools * (`asset_search` / `asset_inspect` / `asset_validate` / `reference_search`) * with a fresh generation built from `roles`. Disposes the previously * registered generation FIRST — the host registry frees each global tool * name synchronously, so re-registering the same names cannot collide — * then compiles and registers the new snapshot, honoring the * `enabledNamespaces` filter. Safe to call repeatedly; the fiber disposer * releases the last retained generation. * @param roles - the re-resolved roles the new snapshot binds to. * @returns the number of tools registered for this generation. */ registerRoleSnapshotTools(roles: ResolvedRole[]): number; } /** Minimal structural dsh `Agent` (the live agent backing a session). */ interface DshAgentLike { readonly id: string; /** Wake the driver with steering (idle starts a turn). rc.6 runtime-types.d.ts:123. */ steer?(message: unknown): unknown | Promise; /** Queue a follow-up turn and wake the driver. rc.6 runtime-types.d.ts:115. */ followup?(message: unknown): unknown | Promise; /** Queue context WITHOUT waking an idle driver. rc.6 runtime-types.d.ts:132. */ inject?(message: unknown): unknown | Promise; } /** Minimal structural dsh `AgentRegistry` (`ctx.agents`). */ interface DshAgentRegistryLike { /** Look up the live agent for a session id (§4.2 `get`). */ get(id: string): DshAgentLike | undefined; } /** * Build a {@link DshPromptInjector} over an optional dsh agent registry. * * The injector resolves the live `Agent` for a target session and delivers * the reminder as a full rc.6 `UserMessage` (message.d.ts:120-133). The * delivery member is chosen from `options.noReply` with the same semantics as * opencode/Pi (`triggerTurn = !noReply`), so dsh does not diverge: * * - `noReply: true` → deliver WITHOUT waking an idle driver: the non-waking * `inject` member only (rc.6 runtime-types.d.ts:124-132; never * `steer`/`followup`). Absent → `null`. * - `noReply: false` → deliver with a WAKING member — `steer` (an idle driver * starts a turn; a running driver consumes it at its next step boundary, * :116-123), then `followup` (:110-115). Absent → `null` (never silently * no-wake an explicit wake request). * - `noReply: undefined` → legacy best-effort: waking members preferred, the * non-waking `inject` fallback last. * * A missing registry, a session with no live agent, an agent exposing none of * the required members, or a throwing / rejecting delivery all degrade to * `null` (the reminder is dropped the same way a missing emperor session is) * rather than failing the graph engine. The `Agent` surface is duck-typed, so * the injector is best-effort and defensive. * * The message carries a per-injection unique `id` (a randomUUID string — a * branded `MessageId` is satisfied structurally at this duck-typed boundary): * the host inbox dedupes on `message.id`, so a shared or absent id would * silently drop the second of two consecutive reminders. */ export declare function buildAgentPromptInjector(registry: DshAgentRegistryLike | undefined): DshPromptInjector | undefined; /** * Cordis plugin `apply(ctx, config)` — boots rolebox on the dsh platform. * * Flow (mirroring `src/index.ts`): * 1. Resolve directories via the dsh platform paths (config overrides win). * 2. `initializeRoleboxRuntime()` with a `DshAgentRegistrar` bound to * `ctx.subagents` — discovers roles, resolves them, syncs agents into * the dsh subagent catalog. * 3. Apply `defaultRole` project-config promotion when configured. * 3a. Wire the dsh role switcher (per-session active-role state + the * `session/created` restore listener) and — OPTIONALLY — register the * composed `/rolebox` prefix route (role-switch surface + monitor * `/status`, `/metrics` surfaces) on dsh's host web server via the * `webServer` service seam (present only in the web profile). The two * surfaces share ONE prefix registration — the real host webserver * rejects duplicate `(kind, path)` pairs. The composed route also * carries the in-process `POST /rolebox/reload` surface * (`DshRoleboxReloader`), which re-discovers + re-resolves roles and * refreshes the role-snapshot tools and the skill catalog IN PLACE — * exposed only where this route is (the DSH web monitor panel). A * registration failure logs a warning and degrades — the plugin keeps * running without the web surface. * 3b. OPTIONALLY register the session-level system-prompt contributions * (`rolebox:role` section + `rolebox:context` context entry) when the * `systemPrompt` service is present on the ctx (full profiles only); * headless profiles warn-degrade and the plugin keeps running. * 4. Compile canonical tools via `DshToolFactory` from * `buildCanonicalTools(...)` (with the dsh session adapter as the * session client) and register them into `ctx.tools`, filtered by * `enabledNamespaces`. * 5. Mount hooks via `DshHookProvider` (rolebox hook kinds onto the dsh * `tools/*` / `session/event` extension points). * 6. Log discovered/resolved/skipped counts mirroring `src/index.ts`. * * @param ctx - The cordis context (structural; the injected dsh services). * @param config - Validated plugin config. cordis validates through * `Config['~standard']` and passes the defaults-applied * output; direct callers may pass a partial object — every * option is optional, so the partial path is safe too. * @returns A fiber disposer that also carries `stats`. */ export declare function apply(ctx: DshPluginContext, config?: DshPluginConfig): Promise; /** * Default export — the object plugin shape (`{ apply(ctx, config) }`, * contract §2.2 `Plugin.Object`), which is what the cordis loader consumes * from a package's default export. The named exports above (`name`, * `inject`, `Config`, `apply`) are also provided for direct import. */ declare const _default: { name: string; inject: string[]; Config: z.ZodObject<{ roleboxDir: z.ZodOptional; skillsDir: z.ZodOptional; defaultRole: z.ZodOptional; enabledNamespaces: z.ZodOptional>; onSpawn: z.ZodOptional>; spawnProviderName: z.ZodOptional; }, z.core.$strip>; apply: typeof apply; }; export default _default; export type { DshSubagentProvider }; //# sourceMappingURL=dsh.d.ts.map