import type { Context } from '@deepseek-ai/cordis'; import { type Config } from './_shared.ts'; /** Logger label for the role-persona channel (dsh logger naming: `/`). */ export declare const ROLE_PERSONA_LOGGER = "mstar/role-persona"; /** * The interception seam: the cordis service-read waterfall event the channel * listener registers on. The registration and the apply-time seam probe both * reference THIS constant — a future cordis rename of `internal/get` becomes * a one-line, probe-family-caught edit here instead of a silent delivery * stop (the probe family pins the literal; see `probeRolePersonaSeam`). */ export declare const PERSONA_SEAM_EVENT = "internal/get"; /** * Wrapper brand — the non-enumerable symbol own property every persona * wrapper carries (`wrapSubagentsService` stamps it at creation). The apply- * time seam probe reads it to assert the wrapper was actually installed on * the controlled read. Non-enumerable + symbol keeps the wrapper's * key/spread/JSON surface identical to the wrapped service's (behavior- * neutral by construction). */ export declare const ROLE_PERSONA_WRAPPER_BRAND: symbol; /** One consumed prompt content block (`@deepseek-ai/dsh-llm` `ContentBlock` text members). */ interface PromptBlockView { readonly type: string; readonly text?: string; } /** * Structural view of the one-shot start request the channel merges into * (`@deepseek-ai/dsh-subagent` `SubagentStartRequest` — consumed fields * only: `prompt` is the role-extraction source, `agentPreset` is the * selection target). The wrapper forwards the request object SPREAD, so * every non-consumed field (`cwd`, `label`, `parent`, `signal`, * `agentOptions`, …) reaches the service unchanged at runtime; the view * types only what this module reads. */ export interface SubagentStartRequestView { /** Content delivered as the child's user message (the Assignment carrier). */ readonly prompt: readonly PromptBlockView[]; /** Optional preset already selected by the tool or the model — caller intent wins. */ readonly agentPreset?: string; /** * Initial child directory as requested (the role gate resolves the child's * EFFECTIVE start directory from it: absolute as-is, relative against * {@link parent}, omitted = the parent's own directory). */ readonly cwd?: string; /** * The spawning agent (`SubagentStartRequest.parent`). Its session cwd is the * default child directory, exactly what the runtime's own `resolveDirectory` * inherits when `cwd` is omitted. */ readonly parent?: unknown; } /** * Structural view of the activation spec the channel merges into * (`@deepseek-ai/dsh-subagent` `SubagentActivationSpec` — consumed fields * only). The wrapper forwards the spec object SPREAD, so every * non-consumed field (`label`, `childId`, `signal`, `delivery`) reaches the * service unchanged at runtime; the view types only what this module reads. */ export interface SubagentActivationSpecView { /** Registered backend the child is started on (the capability-gate key). */ readonly provider: string; /** The delegation request (the merge target). */ readonly request: SubagentStartRequestView; } /** * Structural view of the capability set the channel gates on * (`@deepseek-ai/dsh-subagent` `SubagentCapabilities` — only `agentPreset` * is consumed: an independent selection requires it, and the runtime rejects * one for a provider without it). */ interface SubagentCapabilitiesView { readonly agentPreset?: boolean; } /** * Structural view of one registered provider (`SubagentProvider` consumed * surface). A provider that inherits its parent's conversation cannot accept * an independent composition (upstream `tool-subagent` rejects the pair * `!capabilities.agentPreset || inheritsParentContext`), so the selection is * skipped for it. */ interface SubagentProviderView { readonly capabilities: SubagentCapabilitiesView; readonly inheritsParentContext?: boolean; } /** * Structural view of the `subagents` runtime the wrapper delegates to * (`@deepseek-ai/dsh-subagent` `SubagentRuntime` consumed surface: * capability reads + the one start call). `startActivation` returns the * service's own promise — opaque here, forwarded untouched. */ export interface SubagentsServiceView { /** Look up a provider by name (the capability-gate read). */ getProvider(name: string): SubagentProviderView | undefined; /** Establish one child on the named provider (the delegated start). */ startActivation(spec: SubagentActivationSpecView): unknown; } /** Role-persona log levels the module sink understands. */ export type RolePersonaLogLevel = 'debug' | 'warn'; /** Module-level log sink — bound by `apply` to `ctx.logger(ROLE_PERSONA_LOGGER)` (agent-flow ledger precedent). */ export type RolePersonaLogSink = (level: RolePersonaLogLevel, message: string) => void; /** * Bind the role-persona log sink (the entry `apply` binds it to * `ctx.logger(ROLE_PERSONA_LOGGER)`). Returns the PRIOR sink so a caller * can restore it (test pattern: agent-flow `setAgentFlowLogger`). */ export declare function setRolePersonaLogger(sink: RolePersonaLogSink): RolePersonaLogSink; /** * Register the role-preset channel on the plugin's context: an * `internal/get` waterfall listener (the cordis service-read interception * hook) that wraps `ctx.subagents` reads. The listener is owned by the * applying fiber — an HMR fiber swap unwinds it (reads return the raw * service again) and a re-apply restores it. * * Composition-order independence (`prepend`): the SAME seam may be wrapped by * another layer, and each wrapper only fills a slot that is still free — so * whichever wrapper runs FIRST owns it. A waterfall runs listeners * outermost-first, and `prepend` registers this one at the front, so the * harness channel is outermost on ANY row order: the default profile mounts * the mstar row first, while a re-ordered profile (or a re-applied plugin row) * mounts it last. That matters because an operator's `rolePresets` are the * harness's authoritative role identity — the channel must not lose the slot * to whichever layer happened to apply first (the previous competing layer, * `dsh-llm-fallbacks`, filled the removed native `persona` slot and can no * longer reach this one). A role the harness maps to no preset still falls * through to the other layer (the outermost wrapper delegates the caller's own * request object on every skip path), and reads of every other service are * returned untouched. * * Never throws: the wrap step is contained — on any internal error the read * returns the UNWRAPPED service value (delivery degrades, the runtime is * untouched). * * @param ctx - the plugin's registrant context (the app composition root). * @param config - validated plugin configuration (`rolePresets` is the * only payload source; `roleMap` is never consulted for the selection). */ export declare function registerRolePersonaChannel(ctx: Context, config: Config): void; /** Outcome of one apply-time seam probe ({@link probeRolePersonaSeam}). */ export interface PersonaSeamProbeResult { /** `true` = the channel is healthy OR the probe failed open (never block apply). */ ok: boolean; /** * Why the probe classified the channel the way it did. `ok: false` ALWAYS * carries a reason; `service-absent` may appear with `ok: true` (the * sanctioned no-warn unresolved-service classification). */ reason?: 'seam-absent' | 'wrap-skipped' | 'service-absent'; } /** One probe observation — the inputs of the pure decision core. */ export interface SeamProbeInputs { /** Whether the temporary canary listener fired during the controlled read. */ dispatched: boolean; /** The value the controlled read threw (`undefined` = the read resolved). */ readError: unknown; /** The controlled read's resolved value (`undefined` when the read threw). */ value: unknown; } /** * Pure decision core of the seam probe — the ENTIRE outcome table, unit- * pinnable without cordis internals: * * - canary silent → `{ ok: false, reason: 'seam-absent' }` — cordis no * longer dispatches the seam; our listener can never run. Dominates the * other inputs (a silent canary means the delivery channel is gone). * - canary fired + read threw → `{ ok: true, reason: 'service-absent' }` — * the seam works but the reading ctx does not resolve `subagents` (on the * real composition the apply ctx is inject-guarded; cordis resolves the * service in dispatch scopes, where reads are intercepted per read). Never * a warn. * - canary fired + branded value → `{ ok: true }` — healthy. * - canary fired + any other resolved value (unbranded object, primitive, * undefined) → `{ ok: false, reason: 'wrap-skipped' }` — the listener ran * but the shape was unrecognized (the `wrapSubagentsService` pass-through). */ export declare function evaluateSeamProbe({ dispatched, readError, value }: SeamProbeInputs): PersonaSeamProbeResult; /** * Apply-time seam probe — runs EXACTLY ONCE per apply, immediately after * {@link registerRolePersonaChannel} (entry wiring), and classifies the * channel's install state per {@link evaluateSeamProbe} (observation only — * it never changes delivery semantics): * * 1. register a TEMPORARY {@link PERSONA_SEAM_EVENT} canary listener * (`(ctx, name, error, next) => { dispatched = true; return next() }`), * keeping the disposer `ctx.on` returns; * 2. perform ONE controlled proxied read `ctx.subagents` inside try/catch — * the exact read path the per-call `ctx.subagents.startActivation(...)` * dispatch takes. NEVER `ctx.get('subagents')`: the accessor reads the * service store directly and bypasses the waterfall, which would * false-warn `seam-absent` on every healthy boot; * 3. dispose the canary SYNCHRONOUSLY (`finally` — also on the throwing * read path); * 4. classify via {@link evaluateSeamProbe} and log: `ok === false` → ONE * warn ({@link SEAM_WARN} + reason); `service-absent` → one debug; a * healthy probe stays silent. * * Contained failure: the body is fully try/catch-wrapped — any probe- * internal error (e.g. a rejected listener registration) fails OPEN with * `{ ok: true }` plus ONE debug naming the error. Never throws out of * `apply`; never affects a subagent start. * * @param ctx - the plugin's registrant context (a runtime-bearing context — * proxied property reads on it dispatch the seam waterfall). */ export declare function probeRolePersonaSeam(ctx: Context): PersonaSeamProbeResult; /** * Dispatch-role gate (`Config.roleGate`) — normalize an explicit role * declaration into the canonical Assignment header, refuse only when there is * none. Role identity is prompt-only on dsh, so a delegation that says * "Role: `fullstack-dev`" in prose binds nothing and both role consumers * (the fallbacks persona chain and `rolePresets`) silently see a role-less * child. The gate runs BEFORE the preset merge in the same wrapper, so the * rewritten request is what the inner merge and the runtime receive. * * Never a guess: only an explicit field line (`Execute as:` / `Act as:` / * `Role:`) or a backticked/bold token that is an ALREADY-DECLARED role id is * a repair candidate. Free prose ("You are an architect doing…") is never * mined, and two distinct candidates at the same or different tiers are * ambiguous — an ambiguous prompt is not repaired (it falls to the mode). */ /** Role-gate mode (`Config.roleGate`). */ export type RoleGateMode = 'off' | 'warn' | 'hard'; /** Which tier identified a repair candidate (`field` outranks `token`). */ export type RoleGateEvidence = 'field' | 'token'; /** Inputs of the pure role-gate decision ({@link evaluateRoleGate}). */ export interface RoleGateInput { /** The start request under gate (prompt + `agentPreset` + `cwd`). */ readonly request: SubagentStartRequestView; /** Configured mode. */ readonly mode: RoleGateMode; /** * The child's EFFECTIVE start directory (named in the refusal message; the * harness-project test is pre-resolved by the caller from this same value). */ readonly cwd?: string; /** `cwd` resolves a `{HARNESS_DIR}` — false exempts the start from the gate. */ readonly harnessProject: boolean; /** The runtime declares `provider`; an unknown provider fails loud its own way (`NO_PROVIDER`). */ readonly providerKnown: boolean; /** Declared role ids: `Config.rolePresets` keys ∪ packaged mirror shell stems. */ readonly declaredRoleIds: ReadonlySet; } /** * The gate's verdict: * - `pass` — silent pass-through (caller intent, canonical header, non-harness * project, unknown provider, `off`, or the wrapper's contained fail-open); * - `repair` — the request carries a rewritten prompt (canonical header) that * the binding seam's own reader PROVES it will bind; * - `warn` — no reliable declaration, an ambiguous one, a present-but-unusable * one (fenced, empty, or outside the role-id shape), or a declaration the * repair cannot make the seam see — advisory mode; * - `refuse` — the same causes, `hard` mode (throws before delegating). */ export type RoleGateDecision = { readonly kind: 'pass'; readonly reason: 'agent-preset' | 'canonical' | 'non-harness' | 'provider-unknown' | 'off' | 'degraded'; } | { readonly kind: 'repair'; readonly request: SubagentStartRequestView; readonly roleId: string; readonly evidence: RoleGateEvidence; } | { readonly kind: 'warn'; readonly message: string; } | { readonly kind: 'refuse'; readonly message: string; }; /** * Evaluate the dispatch-role gate for one start — PURE (the harness-project * test is pre-resolved by the caller), first match wins: * * 1. `agentPreset` already set → pass (caller intent wins, silent). * 2. header already carries a USABLE, UNFENCED `Execute as` value → pass * (idempotent, silent; read from the RAW header, the binding seam's own * scope). * 3. not a harness project → pass (a non-harness project never pays the gate). * 4. unknown provider → pass (the service fails loud its own way). * 5. a canonical `Execute as` value that exists ONLY inside a fenced block → * the seam binds that quoted line, so `hard` refuses (and `warn` advises) * with that cause; `off` passes. * 6. exactly one distinct repair candidate (field tier or declared token, both * header-scoped) → repair the prompt, then PROVE the repair: the rewritten * text is parsed with the binding seam's own reader * (`parseAssignmentFields` over `assignmentHeaderRegion`) and must resolve * to that role. A rewrite that leaves another `Execute as` line winning the * read (a wrapped value, or one past the header boundary) would log * "repaired" while the child still starts unbound, so it is NOT returned as * `repair`; ambiguous, none, unusable, or an unprovable repair → the * configured mode (`off` silent, `warn` one advisory, `hard` refusal). * * @param input - the request, mode, pre-resolved harness/provider facts and * the declared role-id set. * @returns the verdict described by {@link RoleGateDecision}. */ export declare function evaluateRoleGate(input: RoleGateInput): RoleGateDecision; /** * Rewrite one prompt with the canonical role declaration: every existing * (unusable) `Execute as` header line has its value replaced IN PLACE and its * label normalized to the canonical spelling — the parsers match the label * case-sensitively and bind the LAST field line, so replacing only the first * occurrence would leave a later broken line winning the read. An Assignment * section that OPENS the prompt gets `Execute as: ` as its first field * line, and any other prompt gets the canonical header block prepended. Every * character outside the repaired field lines is preserved. Fenced examples are * not field lines: the scan runs on the fence-masked text (same offsets as the * input), so a quoted `Execute as:` line is neither repaired nor shadowed — * and only a field line the RAW header reaches is repaired in place: one past * the raw header boundary (a fenced `## Task` truncates the header there) * binds nothing, so the canonical line is inserted after the `## Assignment` * heading instead, i.e. before the point the binding seam stops at. * * @param text - the prompt text to rewrite. * @param roleId - the repaired role id. * @returns the rewritten prompt text. */ export declare function rewritePromptWithExecuteAs(text: string, roleId: string): string; export {};