import { type Context } from '@deepseek-ai/cordis'; import type { GateResult, ValidationResult } from '@mstar-harness/engine'; import type { FsTarget, FsVersion, FsWriteIntent } from '@deepseek-ai/dsh-fs'; import { HarnessResolver } from './_shared.ts'; import type { Config } from './_shared.ts'; import type { DshHostAdapter } from './adapter.ts'; /** * Advisory emitted on status-gate decisions (the plan's "emit `agent/status` * (advisory)" step). Named `mstar/status-gate` instead: the dsh `agent/status` * event is a lifecycle-only channel (`{ agent, status }`, idle ⇄ running, with * an invariant rejecting no-op transitions) — emitting gate warnings on it * would violate the seam contract. Consumers (later tasks, catalogs) observe * this event for model-visible/session-log surfacing. * * The status gate throws only the typed store-authority veto * ({@link StatusVetoError}) — the one refusal this content-blind seam CAN * make, because no write to the document can repair an unreadable issue * authority. Every other decision surfaces through this advisory: the intent * waterfall carries no incoming content, so the only hard-mode decision about * an ALREADY-invalid document is to allow the write as a repair escape. * Unexpected internal errors degrade to an allow with `degraded: true`. */ export interface StatusGateAdvisory { /** Which intent slot passed the gate. */ operation: 'write' | 'edit'; /** `displayPath` of the guarded file. */ target: string; /** The gate verdict (warn-mode: `hardBlocked` false; hard repair escape: `hardBlocked` true). */ result: GateResult; /** * Resolved enforcement flag: true whenever the gate ran under hard * enforcement — for hard-mode repair escapes AND for degraded-hard * advisories (f9: the catch emits `hard` reflecting ACTUAL enforcement, * so a hard deployment can distinguish a dead control from a warn-only * pass). False only for warn-mode advisories. */ hard: boolean; /** True when hard mode allowed a write/edit to an ALREADY-invalid document (repair escape). */ repair?: boolean; /** True when the gate errored internally and degraded to allow (error-containment envelope). */ degraded?: boolean; } /** * Canonical `{HARNESS_DIR}` coordination-document kinds the status gate * covers (opencode-parity vocabulary — the SAME three kinds the P2-fixed * opencode `harnessDocKindOfTarget` classifies): the v2 root `status.json`, * `workflows//snapshot.json`, and `projects//residuals.json`. Each * kind maps to its matching engine validator in * {@link validateStatusValue}. */ export type HarnessDocKind = 'status' | 'snapshot' | 'register'; /** * Classify one fs target as a canonical `{HARNESS_DIR}` coordination * document: basename ∈ {status.json, snapshot.json, residuals.json} AND the * harness-relative path matches the canonical home (root `status.json`, * `{WORKFLOW_DIR}//snapshot.json`, `{PROJECT_DIR}//residuals.json` * — one path component each, mirroring the P2-fixed opencode * `harnessDocKindOfTarget`). * * Phase-5 F1: the snapshot/register rel is computed against the RESOLVED * layout dirs (`resolveWorkflowDir` / `resolveProjectDir` — a `.mstarc` * `[config] workflow_dir` / `project_dir` declaration wins, defaults * `workflows` / `projects` under the harness dir), so a custom layout * classifies at the same location the runtime writes; resolver failure * (never expected with the explicit harness dir) falls back to the default * names. * * dsh divergence from the opencode probe (documented): opencode resolves * the harness root by marker probe (W-REV-1) with a declared-root fallback; * the dsh gates ALREADY resolve `{HARNESS_DIR}` per calling session * workspace through the shared `HarnessResolver` BEFORE the intent slot * runs, so classification here is against that resolved root — no second * probe, no W-REV-3 rebuild. Returns the kind when gated, `null` otherwise * (a non-coordination target, or a path outside the resolved harness dir — * the gate is inert for them). */ export declare function harnessDocKindOfTarget(harnessDir: string, targetPath: string): HarnessDocKind | null; /** * Typed hard-mode veto for the status gate's store-authority refusal class * (the dsh fs-policy veto channel: "veto = throw"; the fs tool turns the throw * into an isError tool result carrying `{ name, code }`). Two decision * classes throw it: * * - the UNCONDITIONAL store-authority refusals (G4b, plan QC fix wave FW-1): * a hand write of `{HARNESS_DIR}/store.db` (`store.direct-write-refused`) or * of a retired/unreadable-authority register (`project.register.retired` / * `store.authority-unavailable`) — an authority invariant, vetoed in BOTH * enforcement modes; * - the hard-mode cleanup-authority class (see * {@link CLEANUP_AUTHORITY_UNAVAILABLE_CODE}) — the repair escape every * other violation class still gets is unavailable there. * * The content-blind listener has no incoming content to lint, but it does not * need any for THESE decisions: they are about the authority itself, which no * content can change. */ export declare class StatusVetoError extends Error { /** Stable code for tool-result serialization (the `{ name, code }` convention). */ readonly code: 'status.veto'; /** The violations that caused the veto (the store-authority refusal included). */ readonly violations: readonly ValidationResult[]; constructor(target: string, violations: readonly ValidationResult[], header: string); } /** * Validate a PARSED harness coordination document through the * kind-matched engine validator (v2 root / workflow snapshot / project * register — the P2-fixed "one validator per kind" shape) plus the * snapshot-only `findingsCleanupGate` extension per plan row that * CONFIGURES a mode (the v1 per-plan-row cleanup gate relocated: plan rows * live on the snapshot, the plan's linked OPEN issues live in the issue * store). Shared by {@link validateStatusDoc} (the on-disk single-read path) * and the host adapter's `beforeStatusWrite` (the incoming document) — the * fs-intent gate, the adapter hook and the repair escape all surface the * SAME violation codes. * * ASYNC because the cleanup extension reads the issue authority * (`{HARNESS_DIR}/store.db`), the engine's async domain boundary. The * register kind stays SYNCHRONOUS shape validation: a project register is * migration history, and validating a document about to be written to that * retired path is not a lookup. * @param kind - the target's {@link HarnessDocKind} (matching engine validator). * @param harnessDir - the resolved `{HARNESS_DIR}`; required for the * snapshot kind's cleanup extension (it is the store context), otherwise * unused. */ export declare function validateStatusValue(doc: unknown, kind: HarnessDocKind, harnessDir?: string | null): Promise; /** * Run the status gate over the CURRENT on-disk document at a canonical * harness coordination target. The fs intent waterfall carries only * `(target, actor)` — never the incoming content — so the vetoable check * is the pre-write state (the opencode hook's fallback for the same * reason). The kind-matched validator runs via {@link validateStatusValue} * (the snapshot kind's `findingsCleanupGate` extension included) — schema * violations short-circuit it (the doc must parse for the cleanup gate to * be meaningful). * * Single-read contract: the file is parsed exactly once and the * parsed doc is passed to {@link validateStatusValue} — the previous * path-first read then `readJson` re-read was a TOCTOU window (a concurrent * writer between the two reads threw a raw error from inside the gate). * Malformed JSON is contained here with the engine's `status.invalid-json` * shape; never throws. Missing files are guarded by the callers * (`gateStatusIntent`, {@link DshHostAdapter.statusGate}) — first create has * no document to validate and passes before this function runs. * @param path - the canonical coordination-document path (root status.json * / workflow snapshot / project register — the caller classifies it). * @param kind - the target's {@link HarnessDocKind}. * @param harnessDir - the resolved `{HARNESS_DIR}` (the store context of the * snapshot kind's cleanup extension). */ export declare function validateStatusDoc(path: string, kind: HarnessDocKind, harnessDir?: string | null): Promise; /** * `fs/write-intent` listener. Registered with `prepend` so this decider runs * BEFORE dsh-fs-policy regardless of mount order: the slot is first-wins by * registration order (dsh-fs-policy README), so without prepend a policy * plugin mounted earlier would make this gate unreachable. Every NON-VETO gate * decision (warn advisory, repair escape, degraded allow) calls `next()` — * delegating the observed-state intent decision to the remaining chain * (fs-policy when mounted; the bare `undefined` default otherwise) rather * than terminating the slot with `undefined` (which would silently disable * fs-policy's CAS for status.json in composed deployments). The store-authority * veto ({@link StatusVetoError}) is the single decision that does NOT delegate: * it rejects the waterfall so the write cannot land (dsh fs-policy "veto = * throw"). */ export declare function writeIntentListener(ctx: Context, resolver: HarnessResolver, config: Config, adapter: DshHostAdapter, target: FsTarget, actor: object | undefined, next: () => FsWriteIntent | undefined | Promise): Promise; /** `fs/edit-intent` listener — same gate and delegation contract as {@link writeIntentListener}. */ export declare function editIntentListener(ctx: Context, resolver: HarnessResolver, config: Config, adapter: DshHostAdapter, target: FsTarget, actor: object | undefined, next: () => { version: FsVersion; } | undefined | Promise<{ version: FsVersion; } | undefined>): Promise<{ version: FsVersion; } | undefined>;