import { type FrictionCaptureArtifact } from '../io/frictionCapture.js'; import { type AgentReflection } from '../agentReflections.js'; import { type CapturedFrictionItem, type FrictionCategory, type FrictionCategoryAttestation, type FrictionDisposition, type FrictionDispositionRecord, type FrictionOpenObservation, type TriagedFrictionArtifact } from './frictionRecord.js'; export type { CapturedFrictionItem, FrictionCategory, FrictionCategoryAttestation, FrictionDisposition, FrictionDispositionRecord, FrictionOpenObservation, TriagedFrictionArtifact, } from './frictionRecord.js'; export { FRICTION_CATEGORIES, appendFrictionUnderLock, frictionLockPath, isFrictionCategory, } from './frictionRecord.js'; /** * O1 end-of-run friction TRIAGE — single-sourced for BOTH orchestrators so the * triage step shape, the disposition vocabulary, the blocking semantics, and the * close-out deciders cannot drift between the two halves of the pipeline. This is * the analog (and now the single source) of the former per-orchestrator * `decideAuditFrictionCloseout` / `decideRemediateFrictionCloseout` deciders, * collapsed here as one parameterized decider. * * What the triage gates: * - The MECHANICAL friction events the O3/O2 seams accreted through the sink * (`captureFrictionEvent` → the per-run `friction/.json` record). * - The OPT-IN agent-feedback reflections workers appended to * `agent-feedback.jsonl` during the run. * * The satisfaction predicate set is the UNION of those two sources. The triage is * a MANDATORY, BLOCKING end-of-run step: it stays unsatisfied until every captured * event AND every surfaced reflection carries a recorded disposition. This is * DISTINCT from the optional, untouched mid-run worker reflection channel — a * worker MAY append a reflection line, but the blocking triage is the host's * end-of-run obligation to dispose of what was captured. * * False-green is dropped: an empty up-front zero-friction record NO LONGER * satisfies the close-out. Satisfaction requires either: * - EMPTY SET: zero events AND zero reflections → trivially disposed (the * close-out fires once, persists a disposed record, and never re-loops); or * - DISPOSED: every event id and every reflection key has a `keep|discard| * annotate` disposition recorded against it. * * Friction appends ride O2's `withFileLock` (keyed off the friction record path) * so a friction append never races the locked critical section. */ /** The set of valid dispositions, for validation at the contract boundary. */ export declare const FRICTION_DISPOSITIONS: readonly FrictionDisposition[]; /** Whether a value is a valid disposition (mechanical contract check). */ export declare function isFrictionDisposition(value: unknown): value is FrictionDisposition; /** A captured item the host must dispose of, normalized across both sources. */ export interface TriageSubject { /** Stable key the disposition is recorded against. */ id: string; /** Which source surfaced it — a mechanical event or an agent reflection. */ source: 'event' | 'reflection'; /** Human-readable summary for the triage prompt. */ note: string; } /** * Named friction dimensions the host is prompted to reflect on — optional finer * "what happened" hints an observation MAY carry. The REQUIRED coverage axis is * `FRICTION_CATEGORIES` below, not these. */ export declare const FRICTION_NAMED_DIMENSIONS: readonly ["gate_reloops", "integration_guard_failures", "rescopes", "surprises", "manual_interventions", "other"]; export type FrictionDimension = (typeof FRICTION_NAMED_DIMENSIONS)[number] | string; /** One-line human labels for each category, shown in the triage prompt. */ export declare const FRICTION_CATEGORY_LABELS: Record; /** * Cost signals measured across an aggregate of same-category (and, where * applicable, same-artifact) mechanical events. These are what let the walk say * "this was expensive re-work" quantitatively, not just "something happened". */ export interface FrictionCostSignals { /** * Round-trips: the number of aggregated mechanical events. Each workflow * step-boundary fact is one avoidable round-trip (a re-emit, a repair round, a * re-derive, …), so the event count IS the round-trip count. */ round_trips: number; /** * Verbatim re-authors: aggregated events whose subject artifact was touched by * MORE THAN ONE event — i.e. the same artifact was re-worked repeatedly. This * is the "we re-authored the same thing again" signal. */ verbatim_re_authors: number; /** * Summed token cost across the aggregated events, when the events carried a * `tokens` measure (0 when none did — token accounting stays best-effort and * never fabricated). */ tokens: number; } /** * The threshold an aggregate must reach to SURFACE as a pre-populated, * auto-covering observation. Below it, the aggregate is still reported (so the * host sees it) but does NOT auto-cover its category — the host still walks it. * "Below MUST NOT fire" is the contract: a single, cheap round-trip is noise, not * a surfaced friction observation. */ export declare const FRICTION_COST_SURFACE_THRESHOLD = 2; /** Whether an aggregate's cost is at/above the surface threshold (fires). */ export declare function costSignalsSurface(signals: FrictionCostSignals): boolean; /** * ONE derived observation aggregated from N same-category mechanical events. This * is the pre-populated close-out entry: N same-artifact workflow facts collapse * into a SINGLE `inefficient_feeding` (or other-category) observation with the * measured cost signals and the covered event ids. */ export interface DerivedFrictionObservation { /** The real close-out category this aggregate covers. */ category: FrictionCategory; /** The aggregation subject (the shared artifact key, or "(mixed)" when several). */ artifact: string; /** The ids of the mechanical events folded into this observation. */ event_ids: string[]; /** The measured cost across the folded events. */ cost: FrictionCostSignals; /** * True when the aggregate's cost is at/above the surface threshold: only these * pre-populate/auto-cover the category. A below-threshold aggregate is still * listed for the host but never fires (never auto-covers). */ surfaced: boolean; /** A ready-to-paste observation note summarizing the aggregate + cost. */ note: string; } /** * Measure the cost signals across an aggregate of mechanical events. Pure and * deterministic — round_trips is the event count (each workflow fact is one * avoidable round-trip), verbatim_re_authors fires only when the SAME subject was * touched by MORE THAN ONE event, and tokens sums the best-effort per-event * measure (never fabricated: a missing/non-finite `tokens` contributes 0). */ export declare function measureFrictionCost(items: readonly CapturedFrictionItem[]): FrictionCostSignals; /** * Aggregate the run's mechanical events into ONE derived observation per * (real category, artifact) group — the pre-population source for the host's * category walk. Pure and deterministic: * * - ONLY events carrying a REAL `frictionCategory` feed the walk; an untagged * legacy event covers no category (it is a bare pending subject, not a walk * contribution) so pre-population never invents coverage. * - N same-artifact same-category events collapse to ONE observation carrying * the measured cost (`measureFrictionCost`) and the covered event ids. * - `surfaced` is the below/above threshold gate: a single cheap round-trip * (round_trips < `FRICTION_COST_SURFACE_THRESHOLD`) is reported but does NOT * fire (never auto-covers its category) — "below MUST NOT fire". * - Output order is stable (canonical category order, then artifact) so a * re-derive never churns the record. */ export declare function deriveFrictionObservations(frictions: readonly CapturedFrictionItem[]): DerivedFrictionObservation[]; /** * The SURFACED derived observations mapped into the persisted * `open_observations[]` shape — the pre-populated entries the close-out gate (and * the Stop-hook backstop) read for category coverage. Only aggregates at/above * the surface threshold are emitted; below-threshold aggregates never fire. */ export declare function prepopulatedObservations(derived: readonly DerivedFrictionObservation[]): FrictionOpenObservation[]; /** * The mandatory blocking triage decision. `pending` means the close-out is NOT * satisfied. `action`: * - "dispose" → run is BLOCKED (pending subjects and/or no open observations). * - "disposed" → all subjects disposed AND ≥1 open observation written. */ export interface FrictionTriageDecision { action: 'dispose' | 'disposed'; /** Subjects still awaiting a disposition (empty when all disposed). */ pending: TriageSubject[]; /** The run_id-keyed friction record path (always set). */ recordPath: string; /** * True while any required friction category is still uncovered (no observation * AND no attestation). Named `needs_open_observations` for continuity; it now * means "the host still owes a per-category disposition", i.e. * `missing_categories.length > 0`. */ needs_open_observations: boolean; /** Categories still lacking BOTH an observation and an attestation. */ missing_categories: FrictionCategory[]; /** Open observations already recorded (empty on first call). */ existing_observations: FrictionOpenObservation[]; /** Per-category "nothing to report" attestations already recorded. */ existing_attestations: FrictionCategoryAttestation[]; /** Free-form notes already recorded (undefined when none). */ free_form_notes?: string; /** * Every step-envelope run this record relates to (semantics: `FrictionRunLinks`). * Surfaced so the close-out states which runs it is closing out instead of leaving * the relation to be inferred from the record's key. Empty when none was recorded. */ step_run_ids: readonly string[]; /** Every dispatch (host-handoff) run this record relates to; empty when none. */ dispatch_run_ids: readonly string[]; /** * The OWN KEYS of other friction records under the same artifacts dir that reference * one of the SAME runs — found by reference, not by key, so a record filed under a * substrate-minted key is visible to a close-out walking a different key. Ordered by * key; empty when this record names no run or nothing else references one. */ related_record_keys: string[]; } /** A stable key for a surfaced reflection (task_id + ordinal within the run). */ export declare function reflectionKey(reflection: AgentReflection, ordinal: number): string; /** * Build the union triage-subject set for a run: every captured mechanical event * UNION every surfaced agent-feedback reflection. Deterministic — pure read off * the on-disk record + the reflections file, never host discretion. */ export declare function collectTriageSubjects(artifactsDir: string, runId: string): Promise; /** * The MANDATORY BLOCKING end-of-run triage close-out, single-sourced for both * orchestrators (caller passes its `tool`). Two satisfaction requirements: * * 1. Every captured mechanical event AND every surfaced agent-feedback * reflection carries a `keep|discard|annotate` disposition. * 2. The record carries ≥1 open observation (`open_observations[]`). Even * "no friction encountered" as `other` satisfies this — the host must * actively reflect, not auto-approve. * * The former trivial "empty set → disposed" path is intentionally dropped: a * run with zero mechanical events still requires the host to confirm that zero * friction occurred. The record file is materialized on first call so the host * always has an existing file to append to. * * Deterministic — keyed only off the on-disk record + reflections at * `(artifactsDir, runId)`. Never coupled to any repo's backlog doc. */ export declare function decideFrictionTriage(artifactsDir: string, runId: string, tool: FrictionCaptureArtifact['tool']): Promise; /** * Render the friction-triage block for a `present_report` step prompt. * Single-sourced in shared so both orchestrators use the exact same prompt * shape — the host's obligation never drifts between audit and remediate. * * The block is MANDATORY and BLOCKING every run: the host must (1) dispose of * every mechanical event/reflection and (2) write ≥1 open observation before * the run may present complete. */ export declare function buildFrictionTriageBlock(triage: FrictionTriageDecision): string; /** * Record one host disposition against a captured subject, under the shared lock. * Re-disposing the same `target_id` overwrites the prior verdict (idempotent on * the key). The `annotate` disposition carries the host annotation. */ export declare function recordFrictionDisposition(artifactsDir: string, runId: string, disposition: FrictionDispositionRecord, tool?: FrictionCaptureArtifact['tool']): Promise; //# sourceMappingURL=triage.d.ts.map