/** * Diagnostics spine — the one guaranteed lane for severity=warning * diagnostics. Producers call {@link emitWarning}; the surface that owns the * terminal end of the lane binds a {@link DiagnosticsSink} via * {@link configureDiagnosticsSink}; screen-scoped consumers tap * {@link subscribeWarnings} and replay via {@link consumeWarnings}; and * {@link drainUnconsumed} is the end-of-session backstop that keeps a * warning no surface ever consumed visible rather than silent. * * `consumed` means exactly "this event reached a user-visible surface": * a rendering sink emitted it ({@link DiagnosticsSink.rendersToUser}), a * scope-matched live subscriber received it, or a consuming read replayed * it onto a screen. Nothing else sets the flag, so * {@link drainUnconsumed} is a true complement — everything it returns * was never shown anywhere, and everything shown is never reprinted. * * Masking is owned structurally: emitWarning strips control sequences THEN * masks (F-R12 mechanised — no call-site ordering to get wrong), over the * message AND every string value inside plain-object/array metadata. The * buffer and every subscriber receive the same post-mask event object; the * subscription taps after the mask, never before. Sinks declaring * masksInternally receive the post-strip PRE-mask message and raw metadata * because their own render path masks the whole tree (the agent * renderEvent maskTree, the MCP emitter) — one mask per path, decided by * the sink's declaration. Producers whose payloads are masked at * construction by documented contract (e.g. StepOutput.warnings) * double-mask idempotently — a benign overlap, named here so nobody * deletes the construction masks: they also protect persistence surfaces * this spine never sees. * * Scope binding is explicit (per-package scope constants), never ambient. * AsyncLocalStorage was considered and rejected: ALS would give * per-operation isolation without scope strings, but context propagation * through Ink's React scheduler and AWS SDK callback chains is not * async-hooks-reliable — scope binding would be silently wrong exactly * where it matters. Explicit scope constants and subscriptions are * inspectable. * * The confirm-style warn-and-wait lane (ProgressCallbacks.onWarning(message, * proceedCallback)) is a consent flow, not a diagnostic — it must never be * flattened onto this fire-and-forget spine. */ export { stripControlSequences } from "./controlSequences.js"; export interface WarningEvent { /** * Warning text, stored and delivered post-strip+mask. Only a sink * declaring masksInternally sees the post-strip pre-mask spelling. */ message: string; /** * Per-package scope constant (diagnosticScopes.ts in each consumer) — * free-form scope literals are forbidden in production code. */ scope?: string; code?: string; /** * Agent TOON operation spelling; the bound sink defaults it to the * bound command path when absent. Producer-owned constant (a command * path), never remote text — it bypasses the strip+mask, as does * `code`. */ operation?: string; /** * Spread into the agent TOON value by the agent sink. String values in * plain objects/arrays are stripped+masked for the buffered/subscriber * copy; non-plain objects pass through unprocessed and must therefore * never carry sensitive text. */ metadata?: Record; } export interface DiagnosticsSink { emit(event: WarningEvent): void; /** * true — the sink's render path masks (agent renderEvent maskTree, MCP * emitter) and receives the post-strip PRE-mask message and raw * metadata so credentials are masked exactly once. false — the sink * renders verbatim and receives the post-mask event. */ masksInternally: boolean; /** * true — emit() puts the event on a user-visible surface (stderr, the * TOON stream, MCP frames), so emission marks it consumed and the * end-of-session drain never reprints it. false — emit() renders * nothing (the tui/mcp buffer-only defaults); the event stays * unconsumed until a scope-matched subscriber, a consuming read, or * the drain shows it. */ rendersToUser: boolean; } /** * Emit a warning onto the spine. Strips control sequences, masks, buffers * the post-mask event, delivers it to the bound sink synchronously and to * scope-matched subscribers on a microtask. The event is marked consumed * only when a user-visible surface actually shows it: a rendering sink at * emit, or a still-subscribed scope-matched listener at dispatch. */ export declare function emitWarning(message: string, opts?: Omit): void; /** * Bind the sink that owns warning rendering. Returns a restore function * that reinstates the previously bound sink — guarded, so a stale restore * (a surface torn down after a later rebind) never clobbers the newer * binding. */ export declare function configureDiagnosticsSink(sink: DiagnosticsSink): () => void; export interface SubscribeWarningsOptions { /** * Deliver only events carrying exactly this scope. The declaration * lives in the spine so delivery to a non-matching listener can never * mark an event consumed — a foreign-scope event stays unconsumed for * its own consumer or the drain. Omitted = every event, any scope. */ scope?: string; } /** * Subscribe to post-mask warning events (screen-scoped capture). Dispatch * is microtask-deferred and scope-filtered by the spine. Returns * unsubscribe. */ export declare function subscribeWarnings(listener: (event: WarningEvent) => void, options?: SubscribeWarningsOptions): () => void; /** * Inspect the buffered warnings for one scope WITHOUT consuming them — * tests and diagnostics only. A screen replaying onto a surface must use * {@link consumeWarnings}, or the drain will reprint what it showed. */ export declare function readWarnings(scope: string): WarningEvent[]; /** * Read the buffered warnings for one scope onto a user-visible surface * (replay-on-mount), marking them consumed. Events stay buffered so a * remount replays the same set — {@link clearWarnings} at operation start * deletes the shown ones. */ export declare function consumeWarnings(scope: string): WarningEvent[]; /** * Delete one scope's CONSUMED buffered warnings (call when an operation * starts, so back-nav or re-run never re-shows a previous run's * warnings). Never-shown events survive — they belong to the next replay * (where showing them consumes them) or the end-of-session drain, so a * clear can never destroy a warning nobody saw. Scoped only — the full * clear is resetDiagnostics, and that is for tests. */ export declare function clearWarnings(scope: string): void; /** * Return every buffered event no rendering sink, scope-matched * subscriber, or consuming read ever showed, in emission order, marking * them consumed — the end-of-session flush that keeps buffer-only modes * honest. In rendering-sink modes every emission is consumed at emit, so * a drain here is a no-op rather than a double-print. */ export declare function drainUnconsumed(): WarningEvent[]; /** Tests only: drop all buffered events and subscribers, restore the default sink. */ export declare function resetDiagnostics(): void;