import { type Context } from '@deepseek-ai/cordis'; import type { AssignmentFields, GateResult, ValidationResult } from '@mstar-harness/engine'; import type { PreToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools'; import { HarnessResolver } from './_shared.ts'; import type { Config } from './_shared.ts'; import type { SessionHint } from './workflow-selection.ts'; import type { ExecutionWorkflowSourceRead } from './workflow-selection.ts'; import type { DshHostAdapter } from './adapter.ts'; /** Logger label for the dispatch gate (dsh logger naming: `/`). */ export declare const DISPATCH_LOGGER = "mstar/dispatch-gate"; /** * Default delegation tool names the dispatch gate matches (tool-subagent * default id + its fork sibling — roadmap §9 W-B1: fork dispatches carry the * same Assignment-shaped `{ description, prompt }` args and must be gated * like `subagent`). Exported SHARED with the agent-flow settle pairing * (`registerSettleListener` matches the same tool set) so the default cannot drift between * the gate and the settle seam. */ export declare const DEFAULT_DISPATCH_TOOLS: readonly ['subagent', 'subagent_fork']; /** * The fixed workflow/ralph tool names the workflow gate matches (plan * — architect-verified): the workflow tool * registers under Config-default name `'workflow'` and is RENAMEABLE per * instance (`toolName`, `tool-workflow/src/index.ts:41`); `ralph` is a * fixed name (`tool-ralph/src/index.ts:413`). A renamed instance is out * of reach until its name is configured — the same documented caveat as * `DEFAULT_DISPATCH_TOOLS`. The workflow tools are gated by THEIR OWN * branch, never by addition to `DEFAULT_DISPATCH_TOOLS` (plan Global * Constraint — that list stays `['subagent', 'subagent_fork']`). */ export declare const DEFAULT_WORKFLOW_TOOLS: readonly ['workflow', 'ralph']; /** * The fixed dsh Agent Teams tool names the dispatch gate matches, mapped to * the argument field that carries the Assignment-shaped text (the installed * `@deepseek-ai/dsh-experimental-tool-agent-team` 0.2.1-rc.2 surface: * `spawn_teammate.prompt`, `team_task_create.description`, * `team_task_update.description`, `send_message.message`). * * The team tools are gated by THEIR OWN branch, never by addition to * `DEFAULT_DISPATCH_TOOLS` (that list stays `['subagent', 'subagent_fork']`). * `team_task_update.description` is OPTIONAL upstream — a status-only * `claim`/`release`/`complete` update carries no carrier at all — so a * missing (or non-string) carrier is no gate decision, exactly like a * missing `prompt` on `subagent`. The tools are fixed names, so no Config * key is added for them; a RENAMED instance is out of reach until its name * is added here — the same documented caveat `DEFAULT_DISPATCH_TOOLS` * carries for renamed subagent instances. */ export declare const TEAM_DISPATCH_TOOLS: { readonly spawn_teammate: 'prompt'; readonly team_task_create: 'description'; readonly team_task_update: 'description'; readonly send_message: 'message'; }; /** * Advisory emitted on warn-mode dispatch-gate passes (the * `mstar/status-gate` decision reused for the dispatch gate — dsh's * `agent/status` lifecycle event stays untouched). Consumers (later tasks, * catalogs) observe this event for model-visible/session-log surfacing. */ export interface DispatchGateAdvisory { /** The matched delegation tool name. */ tool: string; /** The Assignment's declared `Execute as` ('' when missing). */ role: string; /** The gate verdict (warn-mode: `hardBlocked` is false). */ result: GateResult; /** Whether hard enforcement is on (advisory events are warn-mode by construction). */ hard: boolean; /** True when the gate errored internally and degraded to allow (structured degraded advisory). */ degraded?: boolean; } /** * True when the text looks like an Assignment (opencode parity: `## Assignment` * heading or at least one core field line). Non-Assignment delegation prompts * stay silent — no false-positive warnings. Callers MUST pass the engine * `assignmentHeaderRegion` slice: a `## Assignment` heading or * field line quoted in the task body must not shape a non-assignment prompt. * Exported for the agent-flow ledger's shape guard — the shared * `DshHostAdapter.dispatchGate` core applies the SAME guard on both dispatch * surfaces, so the exec-less host-hook path records nothing for * non-Assignment text either). */ export declare function isAssignmentShaped(assignmentText: string): boolean; /** * Resolve the hard-enforcement flag for one dispatch: explicit Config override * wins, else the Assignment's OWN `Enforcement: hard` header flag (opencode * parity — header region only, a body-quoted example never hardens), else the * repo `.mstarc` `[config] enforcement`, else the iteration compass * frontmatter, else warn-only. */ export declare function resolveDispatchHard(harnessDir: string | null, config: Config, assignmentText: string): boolean; /** A header value that means "no value" (placeholder conventions). Type guard so callers narrow to `string`. Shared with the agent-flow ledger (one grammar, no copy-paste drift). */ export declare function isNaValue(value: string | undefined): value is undefined; /** * Resolve the target plan id from the Assignment HEADER region: `Plan Path` * basename (`.md` stripped), else `SDD dir` basename, else a `plan_id` field. * Exported for the agent-flow ledger's dispatch derivation (the ledger * records the same plan identity the gate resolved — one grammar). * @param headerRegion - `assignmentHeaderRegion(assignmentText)` ( * only the header is read — a plan path quoted in the task body never * resolves a plan id). */ export declare function planIdOf(headerRegion: string): string | undefined; /** * The dispatching session's stable id, when the seam exposes it (dsh * Agent.id). Exported for the agent-flow ledger's dispatch derivation (the * ledger records the same agent identity the lease gate compares — one * grammar). */ export declare function sessionIdOf(exec: ToolExecution): string | undefined; /** * Lease gate — ADDITIVE beyond the opencode parity field set: * opencode's `validateDispatchAssignment` does NOT run lease checks at * dispatch, so every violation emitted here is dsh-only and clearly scoped: * the check fires ONLY for WRITABLE dispatches whose Assignment declares * `Execution mode: sdd` (engine `executionModeToN` semantics — sdd maps to * N=3; the function's violation path is intentionally unused so * `dispatch.execution-mode.*` codes stay out of the parity field set) OR * whose plan row is `InProgress`. * * Contract (status-and-residuals.md § Pre-dispatch re-verify; v3 * relocation —): before any * writable implement dispatch, reread the ACTIVE workflow snapshot * (`workflows//snapshot.json` — the v1 root `plans[]` home is gone; the * root v2 `status.json` supplies the active `workflows[]`) and confirm the * session dispatches into the row's own recorded scope — `worktree_path` and * `working_branch` must match the Assignment; mismatch or an unrecorded scope * → STOP. * * Degrade-allow cases (no false positives): no harness dir, unresolvable plan * id, and non-SDD assignments whose plan row is absent or not InProgress. * Unverifiable states (unreadable/missing root status.json, a * selection failure — v1/migration-required root, no active workflow — an * unreadable/missing snapshot, plan row not registered) are violations ONLY * for sdd dispatches (the lease state cannot be confirmed — the status gate * already guards the next write); unreadable docs never harden a soft * workflow. A missing status.json is NOT a silent fail-open for sdd: the * row's recorded scope, and a missing root/snapshot cannot confirm it — * `lease.dispatch.unverifiable` fires (advisory in warn, deny under hard). * @param hint - the carrying session's selection hint: it decides WHICH * active lifecycle's snapshot the lease is re-verified against, so a * session that is not bound to the lifecycle holding the plan row stops * with `lease.dispatch.plan-not-found` instead of verifying a lease it * does not own. */ export declare function leaseGateViolations(harnessDir: string | null, exec: ToolExecution, writable: boolean | undefined, prompt: string, hint?: SessionHint, activeSource?: ExecutionWorkflowSourceRead): Promise; /** * The anti-recursion CALLER for one dispatch — DERIVED from the calling * session, never invented: * * 1. a CHILD session (or a session the gate cannot read) → EMPTY, whatever * `dispatchBinding` says, so the engine's * `dispatch.anti-recursion.empty-binding` fails closed. A leaf must not * dispatch, and no configuration may lift that; * 2. a ROOT session → an explicit `dispatchBinding` (settings / profile) wins * verbatim, and an explicitly blank value stays blank and fails closed; * 3. no explicit value on a ROOT session → the coordinator identity * (`ROOT_SESSION_DISPATCH_BINDING`) — a root session in this harness IS the * coordinating agent, so this is an identity read, not a guess. * * Consequence: a deployment whose coordinator is NOT the root session cannot be * served by this field — the binding is a ROOT-session identity, never * inherited by a child. */ export declare function dispatchCallerOf(config: Config, agent: unknown): string; /** * The dispatch-gate validation core — the engine's SINGLE dispatch-gate * composition (`dispatch.composeDispatchGate`, opencode/omp/CLI parity — the * SAME composition, so violation codes are identical by construction): shape * guard + field gate (read-only roles skip the branch gate) + * anti-recursion precheck (Config binding) + default-branch gate + * header-region enforcement. The dsh-side additions layer ON TOP: the * worktree L1/L2 checks (additive beyond opencode parity; the * lease gate is exec-bound and joins via {@link DshHostAdapter.dispatchGate}). * Extracted from `gateDispatch` so the `tools/pre-execute` listener and the * host adapter's `beforeDispatch` share ONE code path. * * Header-region scoping: the engine `assignmentHeaderRegion` * slice is computed ONCE and feeds the composition AND the worktree parsers * (fields, branch forms, direct-on exception, worktree tracks) — body-quoted * field examples after a `# Task` / `# Target` / `---` marker never leak * into the header fields the gate validates (the same discipline * enforcement / plan-id / lease already honor). Well-formed assignments * (fields in the header) slice to the full text, so their verdicts are * unchanged. The composition never throws: unexpected failures degrade to * the silent non-shaped result. * * @returns the violations plus the writable flag (false for read-only * roles — the listener feeds it to the lease gate). * @param callerAgent - the CALLING agent (`exec.agent` on the listener path, * `undefined` on the exec-less host hook) — the session the caller identity * is derived from ({@link dispatchCallerOf}). * @param hint - the carrying session's selection hint (the adapter derives * it once per dispatch and shares it with the lease gate, the ledger * record and every selection read below). */ export declare function dispatchGateCore(config: Config, harnessDir: string | null, prompt: string, callerAgent: unknown, hint?: SessionHint, activeSource?: ExecutionWorkflowSourceRead): Promise<{ violations: ValidationResult[]; writable: boolean | undefined; }>; /** * The workflow-gate input composed from one `workflow`/`ralph` tool call * : the tool name + the structural reads of * `meta` (workflow) / `objective` (ralph) + the in-flight call. */ export interface WorkflowGateInput { /** The matched tool name (`'workflow'` | `'ralph'` — the gate name-guards on {@link DEFAULT_WORKFLOW_TOOLS} first). */ tool: 'workflow' | 'ralph'; /** `meta.name` for workflow calls — the P-a allowlist input; absent for ralph (no meta). */ metaName?: string; /** `objective` for ralph calls (objective-based ledger logging, P-b); absent for workflow. */ objective?: string; /** The in-flight tool call — Task 3 P-b lease attribution reads the calling agent/session off it. */ exec: ToolExecution; /** * P-b lease attribution: the calling workspace's first * `InProgress` plan lacking a recorded row scope (computed by * {@link writableFanOutUncovered} from the status.json read through the * contained resolver path — `preExecuteListener` already resolved the * harness dir from the calling agent's session workspace). Undefined → no * uncovered plan (allow axis). Applies to workflow AND ralph (P-b needs no * `meta.name`). */ uncoveredPlanId?: string; } /** * Compose the {@link WorkflowGateInput} from one workflow/ralph tool call's * arguments — structural reads, NEVER throws (plan * Task 1; args shapes architect-verified: * workflow `{ script, meta: { name, description, whenToUse?, phases? }, * args? }` (`tool-workflow/src/index.ts:152-161`); ralph * `{ objective, maxRounds?, maxHandoffChars? }` * (`tool-ralph/src/index.ts:76,416-419`)). Returns undefined for malformed * args — workflow without a non-empty string `meta.name`, ralph without a * string `objective` — the fail-open + one-warn path. * * `meta.name` is NORMALIZED through {@link normalizeWorkflowName} (control * chars stripped) BEFORE the empty check — the P-c cache-key congruence * fold-in: the run-start observation (workflow-ledger.ts) * keys the ask cache with the SAME normalized name, so a control-char name * (`au\u0000dit`) asks once and observes under one key instead of re-asking * forever. The length is NEVER capped here (the gate's identity axis is * uncapped — the ledger ROW display field is capped separately). A name * that is ONLY control characters normalizes to '' → malformed (fail-open), * matching the ledger's read boundary. */ export declare function workflowGateInputOf(exec: ToolExecution): WorkflowGateInput | undefined; /** * `tools/pre-execute` listener. The waterfall refusal channel is the returned * decision: a deny is returned WITHOUT calling `next()` (short-circuits the * chain — downstream listeners and the registry default never run); every * other path calls `next()` to delegate (the registry's default is * `{ kind: 'allow' }`). Engine failures degrade to allow in BOTH modes (hard * gates are opt-in — an engine failure must not harden a workflow that was * soft; opencode parity) but the degrade is NEVER silent: the * catch path emits the plugin-owned `mstar/dispatch-gate` advisory with * `degraded: true` + an error log, so a hard deployment can detect a dead * control instead of only finding it in logs. `next()` itself is invoked * outside the guard so a downstream rejection propagates untouched. */ export declare function preExecuteListener(ctx: Context, resolver: HarnessResolver, config: Config, adapter: DshHostAdapter, exec: ToolExecution, next: () => Promise): Promise; /** * Rebuild the canonical Assignment HEADER text from parsed fields (the * engine's OWN header grammar — `parseAssignmentFields` reads exactly these * labels — so the engine parsers round-trip losslessly). Every typed * `AssignmentFields` slot is emitted under its canonical header label: the * implement/ops `Task budget` (capacity contract spec § A1) after * `Task category`, and the review-seat `Budget` / `Return shape` round * bounds after the branch forms — dropping any of them would false-fire the * corresponding `assignment.field.*-missing` gate on a host that passed the * typed form. The host hook's engine-typed input is `AssignmentFields`; the * shared gate core validates assignment TEXT, so the fields form is * normalized to text before gating. */ export declare function assignmentTextFromFields(fields: AssignmentFields): string;