import { Service, type Context } from '@deepseek-ai/cordis'; import type { AssignmentFields, GateResult, HostAdapter, IntegrationMergeLease, ValidationResult } from '@mstar-harness/engine'; import type { ToolExecution } from '@deepseek-ai/dsh-tools'; import type { HarnessResolver, Config, SessionHintRead } from './_shared.ts'; import { type HarnessDocKind } from './status.ts'; import type { AgentFlowPairing, WorkflowVerdictInput } from './agent-flow.ts'; import { WorkflowAskCache } from './workflow-policy.ts'; /** Options for {@link DshHostAdapter}. */ export interface DshHostAdapterOptions { /** * The per-workspace `{HARNESS_DIR}` resolver (explicit config wins; the * probe never starts from the process cwd). The exec-bound gate paths * resolve per the calling session's workspace. */ readonly resolver: HarnessResolver; /** The plugin Config the gates resolve enforcement + anti-recursion binding from. */ readonly config: Config; /** * The apply-scoped agent-flow pairing store (plan * Task 1 — created by the entry `apply`, * shared with the settle listener): passed to `recordDispatch` so an * exec-bound dispatch registers `callId → dispatchRef` for the later * post-execute settle pairing. Absent (host-adapter tests / direct * construction) → no pairing registration (record-only). */ readonly pairing?: AgentFlowPairing; /** * The P-c first-seen ask cache (plan workflow name → resolved decision, apply-scoped. Absent (the * entry relies on this default — `index.ts` constructs the adapter * without it) → the constructor builds ONE cache per adapter per apply, * shared by the gate and any answerer integration via the readonly * property. An explicit instance may be passed to share a specific cache * (host-adapter tests / direct construction). */ readonly workflowAskCache?: WorkflowAskCache; /** * Log sink for `HostAdapter.log`. Defaults to the dsh ctx logger scoped * `mstar/host-adapter` (dsh logger naming: `/`). */ readonly log?: (level: 'info' | 'warn' | 'error', msg: string) => void; } /** * The plugin's `HostAdapter` implementation (engine `host.ts` type-only * contract) — the HOST-FACING facade over the gate * internals: `host: 'dsh'`, `log` → dsh ctx logger, and the optional hooks * wired to the SAME code paths the in-plugin gates use, so host hooks and * gates share ONE validation path: * * - `beforeStatusWrite(path, doc)` — validates the incoming document when * the host provides it (the write's content — the opencode consumer * convention for this engine hook), else the current on-disk document at * `path` via the gate's single-read `validateStatusDoc` semantics (missing * file = first create = pass). The path is classified against the resolved * harness dir (v2 root / workflow snapshot / project register — the * P2-fixed `harnessDocKindOfTarget` shape) and validated with the MATCHING * engine validator; non-coordination paths pass. Both inputs flow through * `validateStatusValue` — the same pipeline the fs-intent gate runs, so * codes match by construction. Returns the FIRST violation: the engine * hook shape is one `ValidationResult`; the gate's full violation list * stays available on the fs-intent slot. * - `beforeDispatch(assignment)` — the dispatch gate validation path * (engine `composeDispatchGate` — fields + branch gate + anti-recursion — * plus worktree L1/L2 checks; read-only roles skip the branch gate). The lease gate * stays listener-side: it binds the ToolExecution context (session id) * this hook's contract does not carry. The parsed `AssignmentFields` form * is normalized to the engine's own header grammar (lossless — the * parsers read exactly these labels) and gated through the same text path. * Enforcement is applied like the listener (opencode parity): the * returned GateResult carries `hardBlocked` so a refusal-capable host can * refuse the dispatch. On an ACTIVE/unreadable execution authority the * registration selection's legacy file route is never read: the hook * refuses with the engine's own code first. * - `beforeMerge(lease)` — thin wrapper over the engine * `validateIntegrationMergeLease` (reserve/validate the integration merge * lease; the reservation WRITE into status.json is a P3 seam) plus the * no-steal comparison against the ACTIVE snapshot's recorded lease — a * legacy-file read the execution authority is asked about first. */ export declare class DshHostAdapter extends Service implements HostAdapter { /** Engine host identity (`HostId` union). */ readonly host: 'dsh'; private readonly resolver; private readonly config; private readonly pairing; /** * The P-c first-seen ask cache (apply-scoped with the adapter: the dispatch gate reads it * through `gateDispatch`, and tests/answerer integrations reach the same * instance via `ctx.dshHostAdapter.workflowAskCache`. Dies with the * fiber (no module-level reference — an HMR reload starts a fresh cache). */ readonly workflowAskCache: WorkflowAskCache; private readonly logSink; constructor(ctx: Context, options: DshHostAdapterOptions); /** * `HostAdapter.log` — the adapter's own reporting channel (the gates keep * their scoped loggers; this is the host-facing sink). * @param level - log level. * @param msg - message. */ log(level: 'info' | 'warn' | 'error', msg: string): void; /** * Shared status-gate core (plugin-internal): the fs-intent listeners and * the `beforeStatusWrite` on-disk fallback route through this method — * ONE validation code path, kind-dispatched (v2 root / workflow snapshot * / project register — the matching engine validator per * {@link validateStatusValue}). Missing file = first create = pass (the * intent waterfall carries no incoming content, so the vetoable signal is * the pre-write on-disk state). ASYNC: the snapshot kind's findings-cleanup * extension reads the issue store (the engine's async domain boundary). * @param path - the canonical coordination-document path (the caller * classifies it against the resolved harness dir). * @param kind - the target's {@link HarnessDocKind}. * @param harnessDir - the resolved `{HARNESS_DIR}` (the store context of * the snapshot kind's cleanup extension). */ statusGate(path: string, kind: HarnessDocKind, harnessDir: string | null): Promise; /** * Derive the carrying session's workflow-selection hint for one event * (D4): the structural identity read off the agent (`session.header.cwd` / * `session.header.id`) folded with this session's DURABLE binding preference * from the engine-status store. Both gate reads and ledger writes route through * this ONE derivation, so a session is attributed consistently within a * single decision. * * The store is the plugin's only durable picker state, and it is read at * THIS composition edge: `dispatch.ts` (and the writers in `agent-flow.ts`) * receive plain hints and never touch the store — no circular * `agent-flow → store → agent-flow` import. * * Degrades honestly, never with a fabricated hint: * - no session at all (exec-less hook / agent stub) → `ok` without a hint, * so the automatic rungs miss and only a unique active lifecycle * resolves; * - a session without both id AND cwd → `ok` with what IS known (the * lease/cwd rungs and the unique-active case still work — only the * durable pick needs the session key); * - an unreadable/refused/cwd-mismatched binding record → `unavailable` * (reported once through the adapter's log), so the callers keep the * structural evidence for GATE validation but write no ledger row. * @param agent - the event's agent (`exec.agent`), structural read. */ sessionHintFor(agent: unknown): SessionHintRead; /** * Shared dispatch-gate core (plugin-internal): the `tools/pre-execute` * listener and `beforeDispatch` route through this method — ONE * validation code path (field gate + anti-recursion + branch gate + * worktree L1/L2 checks; read-only roles skip the branch gate). The * listener passes `exec` so the lease gate (ToolExecution-bound: session * id, in-flight call) joins the same verdict; the host hook has no exec * context and therefore no calling session, so its anti-recursion caller is * always EMPTY — an explicit `dispatchBinding` is a ROOT-session identity and * is never inherited by that path, which fails closed with * `dispatch.anti-recursion.empty-binding` while still covering the * field/branch/worktree checks. * @param prompt - the Assignment text (engine header grammar). * @param exec - the in-flight delegation tool call (listener path only). * @param hard - the caller's ONE `resolveDispatchHard` resolution): passed in so the record block and * the caller's enforcement decision share a single compass resolution; * when omitted (external callers) the adapter resolves it itself. * @param hintRead - the caller's already-derived hint read (the * `tools/pre-execute` listener derives it once and shares it with the * workflow branch); omitted → derived here from `exec.agent`. */ dispatchGate(prompt: string, exec?: ToolExecution, hard?: boolean, hintRead?: SessionHintRead): Promise; /** * Record one workflow/ralph gate verdict row (plan * Task 4 — the durable ledger row for every * gated workflow/ralph call: verdict + metaName/objective + mode, via the * ledger plan's record path). `gateWorkflow` routes the record through * this adapter method so dispatch.ts stays free of a runtime agent-flow * import (the ledger imports dispatch helpers — the adapter is the * acyclic junction, the same role it plays for `dispatchGate`). The * underlying record is fully try/catch-contained (a failing ledger write * logs and never reaches the gate) — this method never throws. * @param input - the gate's decision identity (harness dir + tool + * workflow/objective + mode + verdict + code). */ recordWorkflowVerdict(input: WorkflowVerdictInput): void; /** * `HostAdapter.beforeStatusWrite` — see the class doc for the doc-first / * on-disk-fallback semantics, now KIND-DISPATCHED: the path is classified * against the resolved harness dir (`harnessDocKindOfTarget` — root * status.json / workflow snapshot / project register, the P2-fixed shape) * and validated with the matching engine validator * (`validateStatusValue`). FW-1/G4b: the store-authority paths are decided * BEFORE that classification and refuse in BOTH enforcement modes — a hand * write of the store database (`store.direct-write-refused`) or of a * retired/unreadable-authority register (`project.register.retired` / * `store.authority-unavailable`) is an authority invariant, not a document * judgment; the pre-activation legacy register route keeps the document * validator. Other non-coordination paths (unclassifiable) still pass. * Never throws; a failing gate maps to its FIRST violation * (severity/code/message/fix/aliases preserved — failing gates always * carry ≥1 violation), a passing gate to * `host.beforeStatusWrite.ok` (the engine test convention for this hook). * @param path - the coordination-document target path. * @param doc - the document about to be written (undefined → validate the * on-disk document at `path`). */ beforeStatusWrite(path: string, doc: unknown): Promise; /** * `HostAdapter.beforeDispatch` — the dispatch gate validation path (see * the class doc). Accepts the raw Assignment text (full fidelity: the * `Enforcement` header field participates in enforcement resolution) or the * parsed `AssignmentFields` (engine-typed hook input; normalized to the * engine's header grammar before gating). Returns the enforced GateResult * — `hardBlocked` mirrors the `tools/pre-execute` deny decision under the * same enforcement resolution. * * @param assignment - raw Assignment text or parsed header fields. */ beforeDispatch(assignment: AssignmentFields | string): Promise; /** * Validate the supplied merge lease against ACTIVE workflow state when a * workflow is selected. File snapshots are never read as an execution source. */ beforeMerge(lease: IntegrationMergeLease): Promise; }