diff --git a/packages/cli/src/repl/application.ts b/packages/cli/src/repl/application.ts index 4651555db..26ae51930 100644 --- a/packages/cli/src/repl/application.ts +++ b/packages/cli/src/repl/application.ts @@ -33,8 +33,14 @@ import type { ReplDescription } from "./description.ts"; import { drawerWidth, HISTORY_ROWS, NARROW, surfaceWidth } from "./layout.ts"; import type { ReplSurface as ReplPlacedSurface, ReplSurfaceCell } from "./layout.ts"; import type { ReplTerminalSize } from "./terminal.ts"; -import { decodeLocation, encodeLocation, resolveLocation } from "./route.ts"; -import type { ReplDrawerRef, ReplRoute, ReplSelection, ReplSurface } from "./route.ts"; +import { decodeLocation, encodeLocation, NO_LIVE, resolveLocation } from "./route.ts"; +import type { + ReplDrawerRef, + ReplLiveAvailability, + ReplRoute, + ReplSelection, + ReplSurface, +} from "./route.ts"; import type { ReplModel, ReplRow, ReplScope } from "./model.ts"; import type { ReplQuestion } from "./elicitation.ts"; import type { ExpansionState } from "./expansion.ts"; @@ -163,6 +169,7 @@ export function initialRoute(execution: string): ReplRoute { at: undefined, inspect: false, draft: undefined, + session: undefined, }); } @@ -209,7 +216,7 @@ export function viewFor( // This process is the only thing that can say a question is waiting, so it // says so here rather than leaving resolution to infer it from a history that // does not record it. - const resolved = resolveLocation(model, state.route, live.question !== undefined); + const resolved = resolveLocation(model, state.route, availabilityOf(live)); if (!resolved.ok) { return resolved; } @@ -255,6 +262,7 @@ export function refusedView( entry: undefined, ancestry: Object.freeze([]), scope: undefined, + session: undefined, drawers: Object.freeze([]), }), live: Object.freeze({ @@ -270,6 +278,21 @@ export function refusedView( }); } +/** + * What this process can say about state no history holds. + * + * A live question is the only one of them this slice has: no Agent runtime runs + * in the REPL yet, so there is no pending permission request and no conversation + * that has started without settling anything. + */ +function availabilityOf(live: ReplLive): ReplLiveAvailability { + return { + elicit: live.question !== undefined, + permission: false, + sessions: NO_LIVE.sessions, + }; +} + const EMPTY_MODEL: ReplModel = Object.freeze({ selection: undefined, head: true, @@ -278,6 +301,8 @@ const EMPTY_MODEL: ReplModel = Object.freeze({ terminal: undefined, checkpoints: Object.freeze([]), transcript: Object.freeze([]), + turns: Object.freeze([]), + sessions: Object.freeze([]), }); /** @@ -330,12 +355,7 @@ export function reduceRepl( }; } case "select-surface": { - return navigate( - state, - model, - { ...state.route, surface: action.surface, drawers: [] }, - live.question !== undefined, - ); + return navigate(state, model, { ...state.route, surface: action.surface, drawers: [] }, live); } case "select-scope": { return navigate( @@ -349,7 +369,7 @@ export function reduceRepl( // different scope cannot keep it. drawers: Object.freeze([]), }, - live.question !== undefined, + live, ); } case "open-drawer": { @@ -368,7 +388,7 @@ export function reduceRepl( state, model, { ...state.route, drawers: Object.freeze([...state.route.drawers, action.drawer]) }, - live.question !== undefined, + live, ); } case "close-drawer": { @@ -377,12 +397,7 @@ export function reduceRepl( } const closing = state.route.drawers[state.route.drawers.length - 1]; const remaining = Object.freeze(state.route.drawers.slice(0, -1)); - const closed = navigate( - state, - model, - { ...state.route, drawers: remaining }, - live.question !== undefined, - ); + const closed = navigate(state, model, { ...state.route, drawers: remaining }, live); // Dismissing the question's drawer discards what was typed into it. It is // not an answer, and keeping it would offer it again as though it were. return closing.kind === "live-elicit" @@ -484,10 +499,10 @@ function navigate( state: ReplState, model: ReplModel, candidate: ReplRoute, - asking: boolean, + live: ReplLive, ): ReplTransition { const route = Object.freeze({ ...candidate }); - const resolved = resolveLocation(model, route, asking); + const resolved = resolveLocation(model, route, availabilityOf(live)); if (!resolved.ok) { return refuse(state, resolved.error.message); } @@ -939,6 +954,8 @@ function describeRow(entry: ReplRow): string { return `generated ${entry.decision}${entry.source === undefined ? "" : `: ${entry.source}`}`; case "elicit": return `answered ${entry.location} ${summarize(entry.answer)}`; + case "agent": + return `agent ${entry.turn.agent} ${entry.turn.status}`; case "effect": return `${entry.type} ${entry.status}`; case "terminal": diff --git a/packages/cli/src/repl/model.ts b/packages/cli/src/repl/model.ts index e64422012..873a2b802 100644 --- a/packages/cli/src/repl/model.ts +++ b/packages/cli/src/repl/model.ts @@ -26,11 +26,23 @@ * that coroutine had already settled — so the same event has the same marker in * every process that reads the file, with nothing extra written down to make * that true. + * + * ## Agent conversations + * + * A retained `agent_prompt` is one Agent turn, read through Core's own record + * parser rather than through a second spelling of that shape here. Turns are + * ordered by the sequence the record states, because a Journal's append order + * is the order turns *finished* and a chronology built from it would reorder a + * conversation whenever one turn took longer than the next. Conversations are + * grouped by the `sessionKey` the provider named and by nothing else — not the + * agent, the authored name, the native id, or the text — and a turn that never + * reached a provider has no conversation to join rather than a guessed one. */ import { Err, Ok } from "effection"; import type { Result } from "effection"; -import { readElicitationSchema } from "@executablemd/core/host"; +import { AGENT_PROMPT, parsePromptRecord, readElicitationSchema } from "@executablemd/core/host"; +import type { PromptRecord } from "@executablemd/core/host"; import type { Close, DurableEvent, Json, Yield } from "@executablemd/durable-streams"; /** The authored root scope: this slice admits one entry and this is its key. */ @@ -103,10 +115,85 @@ export interface ReplScope { readonly scopes: readonly ReplScope[]; } +/** One provider choice a retained permission request offered. */ +export interface ReplAgentOption { + readonly optionId: string; + readonly name: string; + readonly kind: "allow_once" | "allow_always" | "reject_once" | "reject_always"; +} + +/** + * One permission request a retained turn answered. + * + * A fact about a decision already made. It has no action and no authority: the + * request it describes was answered while that turn ran, by a process that may + * no longer exist. + */ +export interface ReplAgentPermission { + readonly toolCallId: string; + readonly title: string | undefined; + readonly kind: string | undefined; + readonly options: readonly ReplAgentOption[]; + readonly outcome: "selected" | "cancelled"; + /** The choice that answered it, or none where it was cancelled. */ + readonly selected: string | undefined; +} + +/** What one conversation was running under, where it said. */ +export interface ReplAgentConfiguration { + readonly model: string | undefined; + readonly effort: string | undefined; +} + +/** + * One retained Agent turn, at the position its record was appended. + * + * Everything a reader can be shown about a Prompt that has settled, and nothing + * about the process that ran it. A provider checkpoint a host retained beside + * the record is not here: it names a turn something could be *continued* from, + * which is durable identity rather than something to read. + */ +export interface ReplAgentTurn { + readonly marker: string; + /** The order this turn ran in, as its record states it. */ + readonly sequence: number; + /** The durable name the Prompt was journaled under. */ + readonly name: string; + /** The text the Prompt was asked. */ + readonly input: string; + /** The key of the one scope whose source this Prompt was written in. */ + readonly scope: string; + readonly position: ReplPosition; + readonly agent: string; + /** The provider's name for the conversation, empty where none was reached. */ + readonly sessionKey: string; + readonly agentSessionId: string | undefined; + readonly status: "completed" | "failed" | "cancelled"; + readonly stopReason: string | undefined; + /** Whatever the turn produced, including partial text on a failure. */ + readonly text: string; + /** + * What went wrong, as one line a reader can act on. + * + * The message alone, for the reason the entry's own outcome keeps only the + * message: a serialized failure names host paths and engine frames, and a + * transcript is a reader's view of their own entry. + */ + readonly failure: string | undefined; + readonly configuration: ReplAgentConfiguration | undefined; + readonly permissions: readonly ReplAgentPermission[]; +} + +/** One retained conversation: a non-empty session key and the turns it held. */ +export interface ReplAgentSession { + readonly sessionKey: string; + readonly turns: readonly ReplAgentTurn[]; +} + /** One offered history position, in append order. */ export interface ReplCheckpoint { readonly marker: string; - readonly kind: "entry" | "scope" | "binding" | "generated" | "elicit" | "terminal"; + readonly kind: "entry" | "scope" | "binding" | "generated" | "elicit" | "agent" | "terminal"; readonly label: string; } @@ -166,6 +253,13 @@ export type ReplRow = readonly location: string; readonly answer: Json; } + | { + readonly kind: "agent"; + readonly marker: string; + readonly scope: string; + /** The exact turn the chronology and its conversation also hold. */ + readonly turn: ReplAgentTurn; + } | { readonly kind: "effect"; readonly marker: string; @@ -190,6 +284,16 @@ export interface ReplModel { readonly terminal: ReplTerminal | undefined; readonly checkpoints: readonly ReplCheckpoint[]; readonly transcript: readonly ReplRow[]; + /** Every retained turn, in Prompt sequence order. */ + readonly turns: readonly ReplAgentTurn[]; + /** + * The conversations those turns belong to. + * + * One per non-empty `sessionKey`, in the order each group's earliest turn + * appears in the chronology. A turn whose key is empty reached no provider, + * so it belongs to no conversation and appears only in `turns`. + */ + readonly sessions: readonly ReplAgentSession[]; } /** @@ -291,6 +395,8 @@ function build( ): Result { const transcript: ReplRow[] = []; const checkpoints: ReplCheckpoint[] = []; + /** Every retained turn, in append order, before the chronology is ordered. */ + const turns: ReplAgentTurn[] = []; let entry: ScopeDraft | undefined; let terminal: ReplTerminal | undefined; /** Every scope by the source path it was admitted from, for owner lookup. */ @@ -416,7 +522,7 @@ function build( if (!owner.ok) { return owner; } - const ordinalKey = `${owner.value.key}/${description.name}`; + const ordinalKey = `${owner.value.scope.key}/${description.name}`; const ordinal = (occurrences.get(ordinalKey) ?? 0) + 1; occurrences.set(ordinalKey, ordinal); const scope: ScopeDraft = { @@ -432,7 +538,7 @@ function build( generated: [], scopes: [], }; - owner.value.scopes.push(scope); + owner.value.scope.scopes.push(scope); register(byPath, scope); transcript.push({ kind: "scope", @@ -463,13 +569,13 @@ function build( return owner; } for (const [name, value] of published.bindings) { - bind(owner.value, name, value); + bind(owner.value.scope, name, value); } if (published.bindings.length > 0) { transcript.push({ kind: "binding", marker, - scope: owner.value.key, + scope: owner.value.scope.key, names: Object.freeze(published.bindings.map(([name]) => name)), }); checkpoints.push({ @@ -479,7 +585,12 @@ function build( }); } if (published.output !== undefined) { - transcript.push({ kind: "output", marker, scope: owner.value.key, text: published.output }); + transcript.push({ + kind: "output", + marker, + scope: owner.value.scope.key, + text: published.output, + }); } continue; } @@ -506,16 +617,16 @@ function build( if (!owner.ok) { return owner; } - owner.value.generated.push({ marker, ...admission }); + owner.value.scope.generated.push({ marker, ...admission }); if (admission.decision === "admitted" && admission.source !== undefined) { - const ordinalKey = `${owner.value.key}/generated`; + const ordinalKey = `${owner.value.scope.key}/generated`; const ordinal = (occurrences.get(ordinalKey) ?? 0) + 1; occurrences.set(ordinalKey, ordinal); - owner.value.scopes.push({ + owner.value.scope.scopes.push({ key: `generated-${ordinal}`, kind: "generated", name: "generated", - path: owner.value.path, + path: owner.value.scope.path, source: admission.source, position, marker, @@ -528,7 +639,7 @@ function build( transcript.push({ kind: "generated", marker, - scope: owner.value.key, + scope: owner.value.scope.key, source: admission.source, decision: admission.decision, }); @@ -563,12 +674,70 @@ function build( : description.name; const asked = detach(schema); const given = detach(answer); - owner.value.elicitations.push({ marker, location, schema: asked, answer: given, position }); - transcript.push({ kind: "elicit", marker, scope: owner.value.key, location, answer: given }); + owner.value.scope.elicitations.push({ + marker, + location, + schema: asked, + answer: given, + position, + }); + transcript.push({ + kind: "elicit", + marker, + scope: owner.value.scope.key, + location, + answer: given, + }); checkpoints.push({ marker, kind: "elicit", label: `Answered ${location}` }); continue; } + if (description.type === AGENT_PROMPT) { + if (event.result.status !== "ok") { + transcript.push({ + kind: "effect", + marker, + type: AGENT_PROMPT, + status: event.result.status, + }); + continue; + } + // Core's own parser, because the record is core's. Restating its shape + // here would be a second reader that could disagree with the one the + // runtime writes through, and the audits are the half of it that must not + // be read loosely. + const record = parsePromptRecord(event.result.value); + if (record === undefined) { + return Err( + new ReplProjectionError( + "a recorded Agent prompt cannot be read by this version of the REPL.", + ), + ); + } + const input = description["input"]; + if (typeof input !== "string") { + return Err( + new ReplProjectionError("a recorded Agent prompt does not retain the text it asked."), + ); + } + const owner = ownerOf(byPath, position, "an Agent prompt"); + if (!owner.ok) { + return owner; + } + const turn = agentTurn( + marker, + owner.value.scope.key, + owner.value.position, + description.name, + input, + record, + ); + turns.push(turn); + transcript.push({ kind: "agent", marker, scope: owner.value.scope.key, turn }); + checkpoints.push({ marker, kind: "agent", label: `Agent prompt ${record.status}` }); + continue; + } + transcript.push({ kind: "effect", marker, @@ -577,6 +746,21 @@ function build( }); } + const sequences = new Set(); + for (const turn of turns) { + if (sequences.has(turn.sequence)) { + return Err( + new ReplProjectionError(`two recorded Agent prompts claim sequence ${turn.sequence}.`), + ); + } + sequences.add(turn.sequence); + } + + // By the sequence each record states, not by where its event landed: a + // Journal appends a turn when it finished, and two conversations running + // beside each other finish in whatever order their providers answered. + const chronology = [...turns].sort((left, right) => left.sequence - right.sequence); + return Ok( freeze({ selection, @@ -586,10 +770,95 @@ function build( terminal: terminal === undefined ? undefined : freeze({ ...terminal }), checkpoints: Object.freeze(checkpoints.map((checkpoint) => freeze({ ...checkpoint }))), transcript: Object.freeze(transcript.map((row) => freeze({ ...row }))), + turns: Object.freeze(chronology), + sessions: conversationsOf(chronology), }), ); } +/** + * One retained turn, copied out of the parsed record and frozen all the way + * down. + * + * Copied again rather than kept: the parser's record is a reading of the event, + * and what a component is handed has to be this model's own object — frozen, so + * one reader cannot change what every other reader sees, and unshared, so + * nothing the model holds is reachable from the events. + */ +function agentTurn( + marker: string, + scope: string, + position: ReplPosition, + name: string, + input: string, + record: PromptRecord, +): ReplAgentTurn { + return freeze({ + marker, + sequence: record.sequence, + name, + input, + scope, + position: freeze({ ...position }), + agent: record.agent, + sessionKey: record.sessionKey, + agentSessionId: record.agentSessionId, + status: record.status, + stopReason: record.stopReason, + text: record.text, + failure: record.error?.message, + configuration: + record.configuration === undefined + ? undefined + : freeze({ model: record.configuration.model, effort: record.configuration.effort }), + permissions: Object.freeze((record.permissions ?? []).map(agentPermission)), + }); +} + +function agentPermission(permission: NonNullable[number]) { + return freeze({ + toolCallId: permission.toolCallId, + title: permission.title, + kind: permission.kind, + options: Object.freeze( + permission.options.map((option) => + freeze({ optionId: option.optionId, name: option.name, kind: option.kind }), + ), + ), + outcome: permission.outcome.outcome, + selected: permission.outcome.outcome === "selected" ? permission.outcome.optionId : undefined, + }); +} + +/** + * The conversations one chronology holds. + * + * Grouped by the session key the provider named, and by nothing else: an agent + * name, an authored Prompt name and a native session id all describe something + * other than which conversation this was, and grouping by one of them would + * merge two conversations or split one. A turn that reached no provider has an + * empty key and joins nothing — inventing a conversation for it would put a + * failure in a history it was never part of. + */ +function conversationsOf(chronology: readonly ReplAgentTurn[]): readonly ReplAgentSession[] { + const grouped = new Map(); + for (const turn of chronology) { + if (turn.sessionKey.length === 0) { + continue; + } + const held = grouped.get(turn.sessionKey); + if (held === undefined) { + grouped.set(turn.sessionKey, [turn]); + continue; + } + held.push(turn); + } + // Insertion order, so a conversation appears where its earliest turn does. + return Object.freeze( + [...grouped].map(([sessionKey, held]) => freeze({ sessionKey, turns: Object.freeze(held) })), + ); +} + /** Publish one name into a scope, replacing an earlier value for that name. */ function bind(scope: ScopeDraft, name: string, value: Json): void { const at = scope.bindings.findIndex((binding) => binding.name === name); @@ -623,7 +892,7 @@ function ownerOf( index: Map, position: ReplPosition | undefined, subject: string, -): Result { +): Result<{ scope: ScopeDraft; position: ReplPosition }> { if (position === undefined || position.path === undefined) { return Err( new ReplProjectionError( @@ -648,7 +917,10 @@ function ownerOf( ), ); } - return Ok(held[0]); + // The position travels back with the scope: an effect that has an owner has a + // readable position by construction, and saying so here is what lets a caller + // retain it without asking again whether it was there. + return Ok({ scope: held[0], position }); } /** What a position that will not read is, as distinct from one that is absent. */ diff --git a/packages/cli/src/repl/route.ts b/packages/cli/src/repl/route.ts index 82cddf2c3..c36fb6e63 100644 --- a/packages/cli/src/repl/route.ts +++ b/packages/cli/src/repl/route.ts @@ -11,7 +11,7 @@ * * ```text * xmd://repl//[/][/]*[/+]* - * [?at=][&inspect][&draft=] + * [?at=][&inspect][&draft=][&session=] * ``` * * The codec is pure and has no model. `decodeLocation` decides only what the @@ -20,13 +20,26 @@ * Journal actually holds, by looking the exact objects up in a frozen model. The * split is what keeps a typo in a location from being answered with a guess * about the history. + * + * `session` selects one Agent conversation to filter the Sessions list by. It + * is the provider's own session key, which is the one name for a conversation + * that a second process reading the same Journal would arrive at too. The live + * permission drawer is spelled `+permission` and carries nothing: the request it + * opens over belongs to this process, and a URL naming one would be a URL + * naming something no other reader could ever have. */ import { Err, Ok } from "effection"; import type { Result } from "effection"; import { ENTRY_SCOPE } from "./model.ts"; -import type { ReplBinding, ReplElicitation, ReplModel, ReplScope } from "./model.ts"; +import type { + ReplAgentSession, + ReplBinding, + ReplElicitation, + ReplModel, + ReplScope, +} from "./model.ts"; /** The one scheme and authority a REPL location has. */ const PREFIX = "xmd://repl/"; @@ -47,6 +60,7 @@ export type ReplDrawerRef = | { readonly kind: "binding"; readonly name: string } | { readonly kind: "recorded-elicit"; readonly marker: string } | { readonly kind: "live-elicit" } + | { readonly kind: "live-permission" } | { readonly kind: "history" }; /** One decoded location. Every member is what the grammar said, parsed. */ @@ -62,13 +76,46 @@ export interface ReplRoute { readonly inspect: boolean; /** Text typed but not yet admitted. Exists only before an entry does. */ readonly draft: string | undefined; + /** + * The one conversation the Sessions list is filtered to, by provider session + * key. Absent means every conversation. + */ + readonly session: string | undefined; +} + +/** + * What this process can say about state no Journal holds. + * + * A waiting question, a waiting permission request and a conversation that has + * started but settled nothing are all live facts: a history records answers and + * completed turns, so no reading of any prefix can establish one. The process + * holding them says so, and a caller that holds none passes {@link NO_LIVE} — + * which is also the default, because a caller that cannot say is a caller with + * nothing to say, and resolving a drawer it does not have would accept a URL + * naming something nothing will mount. + */ +export interface ReplLiveAvailability { + /** Whether a live question is waiting to be answered. */ + readonly elicit: boolean; + /** Whether a live permission request is selected and waiting. */ + readonly permission: boolean; + /** The non-empty session keys live turns have started under. */ + readonly sessions: readonly string[]; } +/** No live capability at all: what a cold reader of a Journal has. */ +export const NO_LIVE: ReplLiveAvailability = Object.freeze({ + elicit: false, + permission: false, + sessions: Object.freeze([]), +}); + /** One drawer resolved against a model. */ export type ReplDrawer = | { readonly kind: "binding"; readonly name: string; readonly binding: ReplBinding } | { readonly kind: "recorded-elicit"; readonly elicitation: ReplElicitation } | { readonly kind: "live-elicit" } + | { readonly kind: "live-permission" } | { readonly kind: "history" }; /** What one location selects in one model. */ @@ -80,6 +127,14 @@ export interface ReplSelection { readonly ancestry: readonly ReplScope[]; /** The innermost selected scope, which is the entry when only it was named. */ readonly scope: ReplScope | undefined; + /** + * The retained conversation the filter names, when the history holds it. + * + * None where no filter was asked for, and none where the key names a live + * conversation that has settled nothing yet — there is no retained group to + * point at until one of its turns is appended. + */ + readonly session: ReplAgentSession | undefined; readonly drawers: readonly ReplDrawer[]; } @@ -146,8 +201,9 @@ export function decodeLocation(location: string): Result { } scopes.push(segment); } - if (surface === "sessions" && (scopes.length > 0 || drawers.length > 0)) { - return refuse("the Sessions surface holds no entry, scope or drawer"); + const misplaced = misplacedDrawer(surface, scopes, drawers); + if (misplaced !== undefined) { + return refuse(misplaced); } const query = readQuery(queryText); @@ -172,9 +228,36 @@ export function decodeLocation(location: string): Result { at: query.at, inspect: query.inspect, draft: query.draft, + session: query.session, }); } +/** + * Why this surface cannot hold what this path put on it, or nothing. + * + * The two surfaces hold different things, and each drawer belongs to one of + * them. Sessions has no entry to select inside and no binding or answer to + * inspect; the permission drawer opens over the turn a conversation is having, + * which is a thing only Sessions shows. + */ +function misplacedDrawer( + surface: ReplSurface, + scopes: readonly string[], + drawers: readonly ReplDrawerRef[], +): string | undefined { + if (surface !== "sessions") { + return drawers.some((drawer) => drawer.kind === "live-permission") + ? "a live permission request is answered on the Sessions surface" + : undefined; + } + if (scopes.length > 0) { + return "the Sessions surface holds no entry or scope"; + } + return drawers.every((drawer) => drawer.kind === "live-permission") + ? undefined + : "the Sessions surface holds only the live permission drawer"; +} + /** * The one canonical spelling of a route. * @@ -193,8 +276,12 @@ export function encodeLocation(route: ReplRoute): string { if (route.draft !== undefined && (route.at !== undefined || route.scopes.length > 0)) { throw new TypeError("a draft cannot accompany an admitted structure or a history position"); } - if (route.surface === "sessions" && (route.scopes.length > 0 || route.drawers.length > 0)) { - throw new TypeError("the Sessions surface holds no entry, scope or drawer"); + if (route.session !== undefined && route.session.length === 0) { + throw new TypeError("a session filter names one conversation"); + } + const misplaced = misplacedDrawer(route.surface, route.scopes, route.drawers); + if (misplaced !== undefined) { + throw new TypeError(misplaced); } const path = [route.execution, route.surface, ...route.scopes].map(encodeSegment); @@ -211,6 +298,12 @@ export function encodeLocation(route: ReplRoute): string { if (route.draft !== undefined) { query.push(`draft=${encodeValue(route.draft)}`); } + // Last, after every member that was canonical before it: a location written + // by an older build and one written by this one have to agree character for + // character where they say the same thing. + if (route.session !== undefined) { + query.push(`session=${encodeValue(route.session)}`); + } const search = query.length === 0 ? "" : `?${query.join("&")}`; return `${PREFIX}${path.join("/")}${search}`; } @@ -224,18 +317,17 @@ export function encodeLocation(route: ReplRoute): string { * earlier members were left out, because a stack with a hole in it does not * describe a surface anybody saw. * - * `asking` is the one thing the model cannot answer. A question that is waiting - * has by definition not been recorded — a Journal holds answered questions, not - * pending ones — so no reading of any history can establish that a live - * question's drawer is mountable. The process that is asking says so. It - * defaults to false, because a caller that cannot say is a caller with no - * question: resolving one it does not have would accept a URL naming a drawer - * nothing will mount. + * `live` is everything the model cannot answer. A waiting question, a waiting + * permission request and a conversation that has started but settled nothing + * are facts no history holds — a Journal records answers and completed turns — + * so the process holding them says so here. A historical route ignores all of + * it: a view frozen at an earlier position is a view of what was recorded then, + * and letting the present satisfy it would show the present as the past. */ export function resolveLocation( model: ReplModel, route: ReplRoute, - asking = false, + live: ReplLiveAvailability = NO_LIVE, ): Result { if (route.at !== model.selection) { return Err( @@ -245,14 +337,23 @@ export function resolveLocation( ), ); } + const filtered = selectConversation(model, route, live); + if (!filtered.ok) { + return filtered; + } if (route.surface === "sessions") { + const opened = openDrawers(model, route, undefined, live); + if (!opened.ok) { + return opened; + } return Ok({ route, surface: "sessions", entry: undefined, ancestry: Object.freeze([]), scope: undefined, - drawers: Object.freeze([]), + session: filtered.value, + drawers: opened.value, }); } if (route.draft !== undefined && model.entry !== undefined) { @@ -290,13 +391,9 @@ export function resolveLocation( ancestry.push(child); } - const drawers: ReplDrawer[] = []; - for (const reference of route.drawers) { - const drawer = openDrawer(model, route, scope, reference, asking); - if (!drawer.ok) { - return drawer; - } - drawers.push(drawer.value); + const opened = openDrawers(model, route, scope, live); + if (!opened.ok) { + return opened; } return Ok({ @@ -305,20 +402,103 @@ export function resolveLocation( entry: model.entry, ancestry: Object.freeze(ancestry), scope, - drawers: Object.freeze(drawers), + session: filtered.value, + drawers: opened.value, }); } +/** + * The conversation a filter names, or why this prefix cannot show one. + * + * At the live head a key may be one this process has started and nothing has + * settled yet, which no history holds and only the process can say. At a + * historical position it may only be one that prefix retained: a conversation + * that started after the position a reader is looking at had not happened yet, + * and answering with it would show the present as the past. + * + * An unknown key refuses. Filtering to a conversation nothing holds would be an + * empty list that looks exactly like a conversation with no turns. + */ +function selectConversation( + model: ReplModel, + route: ReplRoute, + live: ReplLiveAvailability, +): Result { + if (route.session === undefined) { + return Ok(undefined); + } + const retained = model.sessions.find((session) => session.sessionKey === route.session); + if (retained !== undefined) { + return Ok(retained); + } + if (route.at === undefined && live.sessions.includes(route.session)) { + return Ok(undefined); + } + return Err( + new ReplRouteError( + route.at === undefined + ? `this execution holds no conversation ${route.session}.` + : `no conversation ${route.session} had started at this history position.`, + ), + ); +} + +function openDrawers( + model: ReplModel, + route: ReplRoute, + scope: ReplScope | undefined, + live: ReplLiveAvailability, +): Result { + const drawers: ReplDrawer[] = []; + for (const reference of route.drawers) { + const drawer = openDrawer(model, route, scope, reference, live); + if (!drawer.ok) { + return drawer; + } + drawers.push(drawer.value); + } + return Ok(Object.freeze(drawers)); +} + function openDrawer( model: ReplModel, route: ReplRoute, scope: ReplScope | undefined, reference: ReplDrawerRef, - asking: boolean, + live: ReplLiveAvailability, ): Result { if (reference.kind === "history") { return Ok({ kind: "history" }); } + if (reference.kind === "live-permission") { + if (route.surface !== "sessions") { + return Err( + new ReplRouteError("a live permission request is answered on the Sessions surface."), + ); + } + if (route.at !== undefined) { + return Err( + new ReplRouteError( + "a permission request belongs to a turn this process is running, and this view is " + + "frozen at an earlier position. Return to the live head to answer it.", + ), + ); + } + if (!live.permission) { + // The same reason a live question's drawer cannot be reopened: a request + // that is still waiting is not in any history, so every retained fact is + // consistent with none ever arriving. Only the process holding one — and + // holding the exact one this drawer opens over — may say so. + return Err( + new ReplRouteError( + "nothing is asking for permission. A permission drawer belongs to the process holding " + + "the request, and no history records one that is still waiting — so this location " + + "cannot be reopened into answering it.", + ), + ); + } + return Ok({ kind: "live-permission" }); + } if (reference.kind === "binding") { if (scope === undefined) { return Err( @@ -342,7 +522,7 @@ function openDrawer( ), ); } - if (!asking) { + if (!live.elicit) { // Nothing retained can establish this drawer. A waiting question is the // one thing a Journal never holds — it records answers — so `settled`, // the recorded elicitations and every other fact in the model are all @@ -381,6 +561,11 @@ function readDrawer(text: string): ReplDrawerRef | undefined { if (kind === "history") { return argument === undefined ? { kind: "history" } : undefined; } + if (kind === "permission") { + // No argument, ever. Which request is being answered is process-local, and + // a key for it in a URL would publish a live identity nothing else can use. + return argument === undefined ? { kind: "live-permission" } : undefined; + } if (kind === "binding") { return argument === undefined || argument.length === 0 ? undefined @@ -402,18 +587,27 @@ function spellDrawer(drawer: ReplDrawerRef): string { if (drawer.kind === "binding") { return `binding:${drawer.name}`; } + if (drawer.kind === "live-permission") { + return "permission"; + } return drawer.kind === "recorded-elicit" ? `elicit:${drawer.marker}` : "elicit"; } /** What the query said, or none when it said something it does not define. */ -function readQuery( - text: string, -): { at: string | undefined; inspect: boolean; draft: string | undefined } | undefined { +function readQuery(text: string): + | { + at: string | undefined; + inspect: boolean; + draft: string | undefined; + session: string | undefined; + } + | undefined { let at: string | undefined; let inspect = false; let draft: string | undefined; + let session: string | undefined; if (text.length === 0) { - return { at, inspect, draft }; + return { at, inspect, draft, session }; } for (const member of text.split("&")) { if (member.length === 0) { @@ -437,9 +631,13 @@ function readQuery( draft = value; continue; } + if (name === "session" && value !== undefined && value.length > 0 && session === undefined) { + session = value; + continue; + } return undefined; } - return { at, inspect, draft }; + return { at, inspect, draft, session }; } /** diff --git a/packages/cli/tests/fixtures/repl/reference.ts b/packages/cli/tests/fixtures/repl/reference.ts index f5c338edc..5388283e9 100644 --- a/packages/cli/tests/fixtures/repl/reference.ts +++ b/packages/cli/tests/fixtures/repl/reference.ts @@ -17,11 +17,18 @@ import { readTextFile } from "@effectionx/fs"; import { fileURLToPath } from "node:url"; import { InMemoryStream } from "@executablemd/durable-streams"; import type { DurableEvent, Json } from "@executablemd/durable-streams"; -import { collect, Elicitation } from "@executablemd/core"; -import type { ElicitationRequest } from "@executablemd/core"; +import { Agent, collect, Elicitation, installAgentComponents } from "@executablemd/core"; +import type { + AgentPromptEvent, + AgentProviderFactory, + ElicitationRequest, + PermissionOption, + PromptOptions, + Session, +} from "@executablemd/core"; import { executeInstalled } from "@executablemd/core/host"; import { inlineSource } from "@executablemd/core"; -import type { Operation } from "effection"; +import type { Operation, Stream } from "effection"; import { scoped } from "effection"; import { ordinaryEvaluationProfile } from "../../../src/evaluation-profile.ts"; @@ -102,3 +109,168 @@ export function* referenceEvents(): Operation { } return run.events; } + +/** + * The Agent reference entry, executed for real against a scripted provider. + * + * One document holding one of each thing the Agent projection has to read: two + * different agents talking in one conversation, one agent talking in two, a + * turn that was granted permission while it ran, and a turn refused before it + * reached a provider at all — which is the only way a retained turn has no + * conversation to belong to. + * + * The provider is scripted rather than recorded, for the reason the entry above + * is executed rather than replayed: what is under test is the projector against + * the records the *current* runtime writes. + */ +export const AGENT_REFERENCE_SOURCE = [ + "# Agent entry", + "", + "The planner drafts, the builder checks, and the planner runs the build.", + "", + '', + '', + "", + "", + '', + '', + "", + "", + '', + '', + "", + "", + "The last one never reaches a provider.", + "", + '', + "", +].join("\n"); + +/** The choices the scripted provider offers for the build turn. */ +const BUILD_OPTIONS: readonly PermissionOption[] = [ + { optionId: "allow", name: "Allow once", kind: "allow_once" }, + { optionId: "deny", name: "Deny", kind: "reject_once" }, +]; + +/** + * A provider that answers by the text it was sent. + * + * `refuse:` refuses before the turn starts, which is what leaves a record with + * no session key. The build turn asks for permission, so one retained turn + * carries an audit and the others carry none. + */ +function scriptedAgent(): AgentProviderFactory { + const issued = new Map(); + return function* () { + yield* Agent.around( + { + // deno-lint-ignore require-yield + *agent([name]) { + return name ?? "planner"; + }, + // deno-lint-ignore require-yield + *session(routed) { + const [name] = routed; + const key = `stub:${typeof name === "string" ? name : "default"}`; + const held = issued.get(key); + if (held !== undefined) { + return held; + } + const session: Session = { sessionKey: key, cwd: "." }; + issued.set(key, session); + return session; + }, + // deno-lint-ignore require-yield + *prompt([text, options]) { + return scriptedTurn(text, options); + }, + }, + { at: "min" }, + ); + }; +} + +function scriptedTurn( + text: string, + options: PromptOptions | undefined, +): Stream { + return { + *[Symbol.iterator]() { + if (text.startsWith("refuse:")) { + throw new Error("this provider has nothing to run that on"); + } + const named = typeof options?.session === "string" ? options.session : "default"; + const session: Session = { sessionKey: `stub:${named}`, cwd: "." }; + const events: AgentPromptEvent[] = [ + { type: "started", agent: options?.agent ?? "planner", session }, + { type: "text_delta", text: `[${text}]` }, + { type: "terminal", status: "completed" }, + ]; + let index = 0; + let asked = false; + return { + *next() { + if (index === 1 && !asked) { + asked = true; + if (text.startsWith("run the build")) { + yield* Agent.operations.requestPermission({ + session, + toolCall: { + toolCallId: "call-build", + title: "Run npm build", + kind: "execute", + rawInput: { command: "npm run build" }, + }, + options: BUILD_OPTIONS, + }); + } + } + if (index < events.length) { + return { done: false, value: events[index++] }; + } + return { done: true, value: `[${text}]` }; + }, + }; + }, + }; +} + +/** Run the Agent reference entry once, over `stream`. */ +export function runAgentReference( + stream: InMemoryStream = new InMemoryStream(), +): Operation { + return scoped(function* () { + yield* installAgentComponents({ + rootProvider: { + factory: scriptedAgent(), + options: { defaultAgent: "planner", permissionMode: "deny-all" }, + }, + }); + try { + const execution = yield* executeInstalled( + { ...inlineSource(AGENT_REFERENCE_SOURCE), stream }, + [], + ); + const output = yield* collect(execution); + return { + output: String(output), + events: yield* stream.readAll(), + asked: [], + failure: undefined, + }; + } catch (error) { + return { + output: "", + events: yield* stream.readAll(), + asked: [], + failure: error instanceof Error ? error : new Error(String(error)), + }; + } + }); +} + +/** The complete Agent reference Journal of one run. */ +export function* agentReferenceEvents(): Operation { + const run = yield* runAgentReference(); + return run.events; +} diff --git a/packages/cli/tests/repl-boundaries.test.ts b/packages/cli/tests/repl-boundaries.test.ts index 1747163c3..5face6ee2 100644 --- a/packages/cli/tests/repl-boundaries.test.ts +++ b/packages/cli/tests/repl-boundaries.test.ts @@ -290,6 +290,7 @@ describe("REPL documentation: what it says is what the code does", () => { at: undefined, inspect: false, draft: undefined, + session: undefined, }); expect(encoded).toBe("xmd://repl/kf39sla2/repl"); const decoded = decodeLocation(encoded); @@ -312,6 +313,7 @@ describe("REPL documentation: what it says is what the code does", () => { at: undefined, inspect: false, draft: undefined, + session: undefined, }), ).not.toBe("xmd://repl/kf39sla2/entries"); }); diff --git a/packages/cli/tests/repl-journey.test.ts b/packages/cli/tests/repl-journey.test.ts index 9ba2944d4..7bcbdfd9e 100644 --- a/packages/cli/tests/repl-journey.test.ts +++ b/packages/cli/tests/repl-journey.test.ts @@ -55,6 +55,8 @@ const EMPTY_MODEL_FOR_TEST = Object.freeze({ terminal: undefined, checkpoints: Object.freeze([]), transcript: Object.freeze([]), + turns: Object.freeze([]), + sessions: Object.freeze([]), }); const TEXT = new TextDecoder(); @@ -953,6 +955,7 @@ describe("REPL journey: one entry, from raw bytes", () => { at: marker, inspect: true, draft: undefined, + session: undefined, }); // A fresh host: a new terminal, a new repository handle, and nothing carried @@ -1962,6 +1965,7 @@ describe("REPL journey: what it settles before it acts", () => { at: undefined, inspect: false, draft: undefined, + session: undefined, }); const second = recordingTerminal(); @@ -2063,6 +2067,7 @@ describe("REPL journey: what it settles before it acts", () => { at: undefined, inspect: false, draft: undefined, + session: undefined, }); expect(stale).toContain("+elicit"); diff --git a/packages/cli/tests/repl-model.test.ts b/packages/cli/tests/repl-model.test.ts index c49ef5854..b26e2c734 100644 --- a/packages/cli/tests/repl-model.test.ts +++ b/packages/cli/tests/repl-model.test.ts @@ -19,6 +19,7 @@ import type { DurableEvent, Json } from "@executablemd/durable-streams"; import { ENTRY_SCOPE, projectRepl } from "../src/repl/model.ts"; import type { ReplModel, ReplScope } from "../src/repl/model.ts"; import { + agentReferenceEvents, answering, REFERENCE_ANSWER, referenceEvents, @@ -436,3 +437,351 @@ describe("REPL model: the histories it refuses", () => { expect(model.transcript.some((row) => row.kind === "elicit")).toBe(false); }); }); + +/** + * The Agent conversations one journal projects (#854 M1, M2, M3). + * + * The journal comes from a real run of the Agent reference entry, so what these + * assert is the projector against the Prompt records the current runtime + * writes. The negative controls doctor that real journal one member at a time: + * a sequence that disagrees with append order, a record that will not read, an + * audit that will not read, and an owning position removed or pointed + * elsewhere. + */ + +/** The index of the nth `agent_prompt` yield, in append order. */ +function promptAt(events: readonly DurableEvent[], ordinal: number): number { + const found = events.flatMap((event, index) => + event.type === "yield" && event.description.type === "agent_prompt" ? [index] : [], + ); + if (found[ordinal] === undefined) { + throw new Error(`the Agent reference journal records ${found.length} prompts`); + } + return found[ordinal]; +} + +/** The durable record one `agent_prompt` yield holds. */ +function promptValue(events: readonly DurableEvent[], at: number): { [key: string]: Json } { + const event = events[at]; + if (event.type !== "yield" || event.result.status !== "ok") { + throw new Error("this event records no successful prompt"); + } + return asObject(event.result.value); +} + +/** The same journal with one prompt record's members changed. */ +function withPrompt( + events: readonly DurableEvent[], + ordinal: number, + change: (record: { [key: string]: Json }) => { [key: string]: Json }, +): DurableEvent[] { + const at = promptAt(events, ordinal); + return withValue(events, at, change({ ...promptValue(events, at) })); +} + +describe("REPL model: the Agent conversations it projects", () => { + beforeAll(() => useTempFileCompiler()); + + it("M1: projects each retained Prompt as one turn owned by its exact scope", function* () { + const events = yield* agentReferenceEvents(); + const model = projected(events); + + expect(model.turns.map((turn) => turn.input)).toEqual([ + "draft the plan", + "check the plan", + "run the build", + "refuse: nothing to do", + ]); + expect(model.turns.map((turn) => turn.sequence)).toEqual([0, 1, 2, 3]); + expect(model.turns.map((turn) => turn.agent)).toEqual([ + "planner", + "builder", + "planner", + "planner", + ]); + // Every turn belongs to the one scope whose source it was written in, at + // the exact position the record retained. + expect(model.turns.map((turn) => turn.scope)).toEqual([ + ENTRY_SCOPE, + ENTRY_SCOPE, + ENTRY_SCOPE, + ENTRY_SCOPE, + ]); + expect(model.turns.map((turn) => turn.position.line)).toEqual([6, 10, 14, 19]); + expect(model.turns.map((turn) => turn.position.path)).toEqual([ + "", + "", + "", + "", + ]); + + const [drafted, , built, refused] = model.turns; + expect(drafted.status).toBe("completed"); + expect(drafted.text).toBe("[draft the plan]"); + expect(drafted.name).toBe("prompt::6:1#0"); + expect(drafted.failure).toBe(undefined); + expect(built.permissions).toEqual([ + { + toolCallId: "call-build", + title: "Run npm build", + kind: "execute", + options: [ + { optionId: "allow", name: "Allow once", kind: "allow_once" }, + { optionId: "deny", name: "Deny", kind: "reject_once" }, + ], + outcome: "selected", + selected: "deny", + }, + ]); + // The agent's own argument text was never durable, so it is not here to be + // shown either. + expect(JSON.stringify(built)).not.toContain("npm run build"); + expect(refused.status).toBe("failed"); + expect(refused.failure).toBe("this provider has nothing to run that on"); + expect(refused.text).toBe(""); + }); + + it("M1: groups conversations by session key and by nothing else", function* () { + const events = yield* agentReferenceEvents(); + const model = projected(events); + + // Two different agents talking in one conversation are one conversation, + // and one agent talking in two is two. + expect(model.sessions.map((session) => session.sessionKey)).toEqual([ + "stub:review", + "stub:build", + ]); + expect(model.sessions[0].turns.map((turn) => turn.agent)).toEqual(["planner", "builder"]); + expect(model.sessions[1].turns.map((turn) => turn.agent)).toEqual(["planner"]); + + // The turn that reached no provider has no key, so it joins nothing — and + // it is still in the chronology, which is where every turn is. + const unassigned = model.turns.filter((turn) => turn.sessionKey.length === 0); + expect(unassigned.map((turn) => turn.input)).toEqual(["refuse: nothing to do"]); + expect(model.sessions.flatMap((session) => session.turns)).not.toContain(unassigned[0]); + }); + + it("M1: orders the chronology by sequence where append order disagrees", function* () { + const events = yield* agentReferenceEvents(); + // The two turns swap the order they ran in, keeping the order they were + // appended in. A projector reading append order cannot tell the difference. + const swapped = withPrompt( + withPrompt(events, 0, (record) => ({ ...record, sequence: 2 })), + 2, + (record) => ({ ...record, sequence: 0 }), + ); + const model = projected(swapped); + + expect(model.turns.map((turn) => turn.input)).toEqual([ + "run the build", + "check the plan", + "draft the plan", + "refuse: nothing to do", + ]); + // A conversation appears where its earliest turn now appears, and its own + // turns keep sequence order too. + expect(model.sessions.map((session) => session.sessionKey)).toEqual([ + "stub:build", + "stub:review", + ]); + expect(model.sessions[1].turns.map((turn) => turn.input)).toEqual([ + "check the plan", + "draft the plan", + ]); + // Checkpoints are positions in the file, so they stay in append order. + expect(model.checkpoints.filter((checkpoint) => checkpoint.kind === "agent")).toHaveLength(4); + }); + + it("M1: settlement contributes one semantic transcript row and checkpoint", function* () { + const events = yield* agentReferenceEvents(); + const model = projected(events); + + expect( + model.checkpoints.filter((checkpoint) => checkpoint.kind === "agent").map((one) => one.label), + ).toEqual([ + "Agent prompt completed", + "Agent prompt completed", + "Agent prompt completed", + "Agent prompt failed", + ]); + const rows = model.transcript.filter((row) => row.kind === "agent"); + expect(rows).toHaveLength(4); + expect(rows.map((row) => (row.kind === "agent" ? row.scope : ""))).toEqual([ + ENTRY_SCOPE, + ENTRY_SCOPE, + ENTRY_SCOPE, + ENTRY_SCOPE, + ]); + // A label, not a record dump: nothing a reader has to be shown is spelled + // out twice, and the prompt's own text is not in it. + for (const checkpoint of model.checkpoints) { + expect(checkpoint.label).not.toContain("draft the plan"); + } + }); + + it("M3: one retained turn is one object, however it is reached", function* () { + const events = yield* agentReferenceEvents(); + const model = projected(events); + const built = model.turns[2]; + + expect(model.sessions[1].turns[0]).toBe(built); + const row = model.transcript.find((entry) => entry.kind === "agent" && entry.turn === built); + expect(row).toBeDefined(); + // Not a clone that happens to be equal: two readings of one turn would let + // one of them go stale while the other did not. + expect(model.sessions[1].turns[0]).toBe(model.turns[2]); + }); + + it("M3: Agent values are frozen copies, and the events come out unchanged", function* () { + const events = yield* agentReferenceEvents(); + const written = events.map((event) => serializeDurableEvent(event)); + const graph = reachable(events); + expect(graph.filter((member) => Object.isFrozen(member))).toEqual([]); + + const model = projected(events); + const built = model.turns[2]; + + // Everything an Agent reading reaches is frozen, all the way down. + const held = [...reachable(model.turns), ...reachable(model.sessions)]; + expect(held.length).toBeGreaterThan(10); + expect(held.filter((member) => !Object.isFrozen(member))).toEqual([]); + expect(() => { + Object.assign(built, { agent: "someone else" }); + }).toThrow(); + + // And none of it is an object the events still hold. + const borrowed = new Set(graph); + expect(held.filter((member) => borrowed.has(member))).toEqual([]); + + // Reading a history is not a way of changing it. + expect(graph.filter((member) => Object.isFrozen(member))).toEqual([]); + expect(events.map((event) => serializeDurableEvent(event))).toEqual(written); + + // The copy is what the model retains, so mutating the record's own audit + // afterwards changes nothing a reader is looking at. + const record = promptValue(events, promptAt(events, 2)); + const audits = record.permissions; + if (!Array.isArray(audits)) { + throw new Error("the build turn's record retains its audit"); + } + asObject(audits[0]).toolCallId = "something else"; + expect(built.permissions[0].toolCallId).toBe("call-build"); + }); +}); + +describe("REPL model: the Agent histories it refuses", () => { + beforeAll(() => useTempFileCompiler()); + + it("M2: a prefix before a Prompt append holds no turn, and after it holds all of it", function* () { + const events = yield* agentReferenceEvents(); + const spelling = markers(events); + const built = promptAt(events, 2); + + const before = projected(events, spelling[built - 1]); + const after = projected(events, spelling[built]); + + expect(before.turns.map((turn) => turn.input)).toEqual(["draft the plan", "check the plan"]); + expect(before.sessions.map((session) => session.sessionKey)).toEqual(["stub:review"]); + + expect(after.turns.map((turn) => turn.input)).toEqual([ + "draft the plan", + "check the plan", + "run the build", + ]); + expect(after.sessions.map((session) => session.sessionKey)).toEqual([ + "stub:review", + "stub:build", + ]); + // The complete terminal turn, audit included, the moment its record lands. + expect(after.turns[2].status).toBe("completed"); + expect(after.turns[2].permissions.map((audit) => audit.toolCallId)).toEqual(["call-build"]); + }); + + it("M2: refuses a Prompt record it cannot read, and exposes no partial model", function* () { + const events = yield* agentReferenceEvents(); + + for (const change of [ + (record: { [key: string]: Json }) => ({ ...record, sequence: "second" }), + (record: { [key: string]: Json }) => ({ ...record, sequence: -1 }), + (record: { [key: string]: Json }) => ({ ...record, sequence: 0.5 }), + (record: { [key: string]: Json }) => ({ ...record, status: "nearly" }), + (record: { [key: string]: Json }) => ({ ...record, sessionKey: 7 }), + ]) { + expect(refusal(withPrompt(events, 0, change))).toContain("Agent prompt cannot be read"); + } + + const projection = projectRepl( + withPrompt(events, 0, (record) => ({ ...record, sequence: "second" })), + ); + expect(projection.ok).toBe(false); + expect("value" in projection).toBe(false); + }); + + it("M2: refuses duplicate Prompt sequences instead of guessing from append order", function* () { + const events = yield* agentReferenceEvents(); + const duplicate = withPrompt(events, 1, (record) => ({ ...record, sequence: 0 })); + + expect(refusal(duplicate)).toContain("two recorded Agent prompts claim sequence 0"); + const projection = projectRepl(duplicate); + expect(projection.ok).toBe(false); + expect("value" in projection).toBe(false); + }); + + it("M2: refuses an audit it cannot read rather than reading it as none", function* () { + const events = yield* agentReferenceEvents(); + const audited = promptValue(events, promptAt(events, 2)).permissions; + if (!Array.isArray(audited)) { + throw new Error("the build turn's record retains its audit"); + } + const [audit] = audited.map((member) => asObject(member)); + + for (const permissions of [ + "granted", + [{ ...audit, outcome: { outcome: "granted" } }], + [{ ...audit, options: [{ optionId: "allow", name: "Allow once", kind: "allow_maybe" }] }], + // The one thing an audit may never carry, arriving as a member nothing + // here defines: read past, it would be published to every reader. + [{ ...audit, rawInput: { command: "rm -rf /" } }], + ]) { + expect(refusal(withPrompt(events, 2, (record) => ({ ...record, permissions })))).toContain( + "Agent prompt cannot be read", + ); + } + }); + + it("M2: refuses a Prompt whose owner cannot be decided, and never guesses one", function* () { + const events = yield* agentReferenceEvents(); + const at = promptAt(events, 0); + + const removed = withDescription(events, at, (description) => { + delete description["executablemd.source-position"]; + return description; + }); + expect(refusal(removed)).toContain("recorded without the source position"); + + const elsewhere = withDescription(events, at, (description) => ({ + ...description, + "executablemd.source-position": { path: "somewhere-else.md", offset: 0, line: 1, column: 1 }, + })); + expect(refusal(elsewhere)).toContain("never admitted"); + + const corrupted = withDescription(events, at, (description) => ({ + ...description, + "executablemd.source-position": { path: "", offset: -1, line: 0, column: 0 }, + })); + expect(refusal(corrupted)).toContain("cannot read"); + + const unnamed = withDescription(events, at, (description) => { + delete description["input"]; + return { ...description, name: description.name }; + }); + expect(refusal(unnamed)).toContain("does not retain the text it asked"); + }); + + it("M2: a marker this journal does not hold refuses rather than showing the head", function* () { + const events = yield* agentReferenceEvents(); + + expect(refusal(events, "yield:root:99")).toContain("not in this journal"); + expect(projected(events).turns).toHaveLength(4); + }); +}); diff --git a/packages/cli/tests/repl-route.test.ts b/packages/cli/tests/repl-route.test.ts index c6c4f755f..bee615ba6 100644 --- a/packages/cli/tests/repl-route.test.ts +++ b/packages/cli/tests/repl-route.test.ts @@ -16,9 +16,9 @@ import type { DurableEvent } from "@executablemd/durable-streams"; import { projectRepl } from "../src/repl/model.ts"; import type { ReplModel } from "../src/repl/model.ts"; -import { decodeLocation, encodeLocation, resolveLocation } from "../src/repl/route.ts"; -import type { ReplRoute, ReplSelection } from "../src/repl/route.ts"; -import { referenceEvents } from "./fixtures/repl/reference.ts"; +import { decodeLocation, encodeLocation, NO_LIVE, resolveLocation } from "../src/repl/route.ts"; +import type { ReplLiveAvailability, ReplRoute, ReplSelection } from "../src/repl/route.ts"; +import { agentReferenceEvents, referenceEvents } from "./fixtures/repl/reference.ts"; function decoded(location: string): ReplRoute { const result = decodeLocation(location); @@ -44,16 +44,29 @@ function projected(events: readonly DurableEvent[], selection?: string): ReplMod return result.value; } -function resolved(model: ReplModel, location: string, asking = false): ReplSelection { - const result = resolveLocation(model, decoded(location), asking); +/** What a process holding some live state would say, from the parts named. */ +function holding(availability: Partial): ReplLiveAvailability { + return { ...NO_LIVE, ...availability }; +} + +function resolved( + model: ReplModel, + location: string, + availability: ReplLiveAvailability = NO_LIVE, +): ReplSelection { + const result = resolveLocation(model, decoded(location), availability); if (!result.ok) { throw result.error; } return result.value; } -function unresolved(model: ReplModel, location: string, asking = false): string { - const result = resolveLocation(model, decoded(location), asking); +function unresolved( + model: ReplModel, + location: string, + availability: ReplLiveAvailability = NO_LIVE, +): string { + const result = resolveLocation(model, decoded(location), availability); if (result.ok) { throw new Error(`${location} resolved, and this model cannot answer it`); } @@ -268,7 +281,7 @@ describe("REPL route: resolving against one model", () => { expect(unresolved(unclosed, live)).toContain("nothing is being asked"); // Only the process actually holding the question may open it, and it says so. - expect(resolved(head, live, true).drawers[0].kind).toBe("live-elicit"); + expect(resolved(head, live, holding({ elicit: true })).drawers[0].kind).toBe("live-elicit"); }); it("R2: refuses a missing entry, scope, binding or incomplete path", function* () { @@ -340,3 +353,227 @@ describe("REPL route: resolving against one model", () => { ); }); }); + +/** + * The conversation filter and the live permission drawer (#854 R1, R2). + * + * Two halves again. The grammar has to spell one more query member without + * moving any of the ones that were canonical before it, and has to keep the new + * drawer on the one surface that has it. Resolution has to answer a filter from + * what the *selected prefix* retained, plus — only at the live head — what the + * process says it has started, and has to refuse anything else rather than + * showing an empty list. + */ +describe("REPL route: the conversation filter", () => { + it("R1: encodes session last, after every member that was canonical before it", function* () { + const locations = [ + `xmd://repl/${EXECUTION}/sessions?session=xmd:v1:a`, + `xmd://repl/${EXECUTION}/sessions/+permission?session=xmd:v1:a`, + `xmd://repl/${EXECUTION}/repl/entry-1?session=xmd:v1:a`, + `xmd://repl/${EXECUTION}/repl/entry-1/+history?at=yield:root:1&inspect&session=one`, + `xmd://repl/${EXECUTION}/repl?draft=one%20more%20line&session=one`, + `xmd://repl/${EXECUTION}/repl/entry-1/+binding:plan?at=yield:root:1&session=one`, + ]; + + for (const location of locations) { + expect(encodeLocation(decoded(location))).toBe(location); + expect(decoded(encodeLocation(decoded(location)))).toEqual(decoded(location)); + } + }); + + it("R1: leaves every location that names no conversation byte-identical", function* () { + const locations = [ + `xmd://repl/${EXECUTION}/sessions`, + `xmd://repl/${EXECUTION}/repl`, + `xmd://repl/${EXECUTION}/repl/entry-1`, + `xmd://repl/${EXECUTION}/repl/entry-1/+history/+binding:plan`, + `xmd://repl/${EXECUTION}/repl/entry-1?at=yield:root:1&inspect`, + `xmd://repl/${EXECUTION}/repl?draft=one%20more%20line`, + ]; + + for (const location of locations) { + expect(encodeLocation(decoded(location))).toBe(location); + expect(decoded(location).session).toBe(undefined); + } + }); + + it("R1: reads query members by name, so order still does not matter", function* () { + const canonical = decoded( + `xmd://repl/${EXECUTION}/repl/entry-1?at=yield:root:6&inspect&session=xmd:v1:a`, + ); + + expect(canonical).toEqual( + decoded(`xmd://repl/${EXECUTION}/repl/entry-1?session=xmd:v1:a&inspect&at=yield%3Aroot%3A6`), + ); + expect(canonical.session).toBe("xmd:v1:a"); + }); + + it("R1: refuses an empty, repeated or unspellable conversation", function* () { + expect(refused(`xmd://repl/${EXECUTION}/sessions?session=`)).toContain("query"); + expect(refused(`xmd://repl/${EXECUTION}/sessions?session=a&session=b`)).toContain("query"); + expect(refused(`xmd://repl/${EXECUTION}/sessions?session`)).toContain("query"); + expect(refused(`xmd://repl/${EXECUTION}/sessions?session=%zz`)).toContain("query"); + // Not a path segment. A conversation is a filter over what Sessions lists, + // not a place inside it, and a segment would make one URL mean two things. + expect(refused(`xmd://repl/${EXECUTION}/sessions/xmd:v1:a`)).toContain("Sessions surface"); + + const route = decoded(`xmd://repl/${EXECUTION}/sessions?session=one`); + expect(() => encodeLocation({ ...route, session: "" })).toThrow(); + }); + + it("R1: the permission drawer belongs to Sessions, carries nothing, and is one word", function* () { + const location = `xmd://repl/${EXECUTION}/sessions/+permission`; + expect(encodeLocation(decoded(location))).toBe(location); + expect(decoded(location).drawers).toEqual([{ kind: "live-permission" }]); + + // It names no request. The one being answered belongs to this process, and + // a key for it in a URL would publish an identity nothing else can use. + expect(refused(`xmd://repl/${EXECUTION}/sessions/+permission:turn-3`)).toContain( + "names a drawer", + ); + expect(refused(`xmd://repl/${EXECUTION}/repl/entry-1/+permission`)).toContain( + "Sessions surface", + ); + // And Sessions still holds nothing else. + expect(refused(`xmd://repl/${EXECUTION}/sessions/+history`)).toContain("Sessions surface"); + expect(refused(`xmd://repl/${EXECUTION}/sessions/+elicit`)).toContain("Sessions surface"); + + const sessions = decoded(`xmd://repl/${EXECUTION}/sessions`); + expect(() => + encodeLocation({ ...sessions, surface: "repl", drawers: [{ kind: "live-permission" }] }), + ).toThrow(); + }); +}); + +describe("REPL route: resolving a conversation", () => { + beforeAll(() => useTempFileCompiler()); + + it("R2: resolves a retained conversation to the exact group the model holds", function* () { + const events = yield* agentReferenceEvents(); + const model = projected(events); + + const filtered = resolved(model, `xmd://repl/${EXECUTION}/sessions?session=stub%3Areview`); + expect(filtered.session).toBe(model.sessions[0]); + // The filter is orthogonal to everything else a location can say. + const beside = resolved(model, `xmd://repl/${EXECUTION}/repl/entry-1?session=stub%3Abuild`); + expect(beside.session).toBe(model.sessions[1]); + expect(beside.scope).toBe(model.entry); + // And absent means every conversation rather than none. + expect(resolved(model, `xmd://repl/${EXECUTION}/sessions`).session).toBe(undefined); + }); + + it("R2: resolves a live key only at the head, and only when the process says so", function* () { + const events = yield* agentReferenceEvents(); + const model = projected(events); + const started = `xmd://repl/${EXECUTION}/sessions?session=stub%3Aplanning`; + + // Nothing has settled in it yet, so no history holds it and there is no + // retained group to point at — but the process running it can say it exists. + expect(unresolved(model, started)).toContain("no conversation stub:planning"); + const live = resolved(model, started, holding({ sessions: ["stub:planning"] })); + expect(live.session).toBe(undefined); + expect(live.route.session).toBe("stub:planning"); + + // A key nobody claims is still refused rather than becoming an empty list. + expect(unresolved(model, started, holding({ sessions: ["stub:other"] }))).toContain( + "no conversation stub:planning", + ); + }); + + it("R2: a historical prefix answers from its own retained groups and ignores the head", function* () { + const events = yield* agentReferenceEvents(); + const head = projected(events); + const early = head.turns[0].marker; + const historical = projected(events, early); + + const retained = resolved( + historical, + `xmd://repl/${EXECUTION}/sessions?at=${early}&session=stub%3Areview`, + ); + expect(retained.session).toBe(historical.sessions[0]); + + // The build conversation had not started at this position. It is in the + // head and in this process, and neither may answer for the past. + const later = `xmd://repl/${EXECUTION}/sessions?at=${early}&session=stub%3Abuild`; + expect(head.sessions.map((session) => session.sessionKey)).toContain("stub:build"); + expect(unresolved(historical, later)).toContain("had started at this history position"); + expect( + unresolved(historical, later, holding({ sessions: ["stub:build", "stub:planning"] })), + ).toContain("had started at this history position"); + }); + + it("R2: a conversation no turn ever joined cannot be selected", function* () { + const events = yield* agentReferenceEvents(); + const model = projected(events); + + // The refused turn is in the chronology and in no conversation, so there is + // nothing for a filter to name — least of all the empty key itself. + expect(model.turns.some((turn) => turn.sessionKey.length === 0)).toBe(true); + expect(unresolved(model, `xmd://repl/${EXECUTION}/sessions?session=planner`)).toContain( + "no conversation planner", + ); + expect( + unresolved( + model, + `xmd://repl/${EXECUTION}/sessions?session=prompt%3A%3Ceval%3E%3A19%3A1%230`, + ), + ).toContain("no conversation"); + }); + + it("R2: a refused filter leaves the standing route and selection untouched", function* () { + const events = yield* agentReferenceEvents(); + const model = projected(events); + const standing = resolved(model, `xmd://repl/${EXECUTION}/sessions?session=stub%3Areview`); + const before = standing.session; + + expect(unresolved(model, `xmd://repl/${EXECUTION}/sessions?session=stub%3Anowhere`)).toContain( + "no conversation stub:nowhere", + ); + + expect(standing.session).toBe(before); + expect(standing.route.session).toBe("stub:review"); + expect(Object.isFrozen(model)).toBe(true); + }); + + it("R2: a live permission drawer is live-only, and independent of a live question", function* () { + const events = yield* agentReferenceEvents(); + const model = projected(events); + const drawer = `xmd://repl/${EXECUTION}/sessions/+permission`; + + // No reading of any history can establish it: a request still waiting is + // the one thing a Journal never holds. + expect(unresolved(model, drawer)).toContain("nothing is asking for permission"); + // A live question is a different fact and does not stand in for it. + expect(unresolved(model, drawer, holding({ elicit: true }))).toContain( + "nothing is asking for permission", + ); + expect(resolved(model, drawer, holding({ permission: true })).drawers).toEqual([ + { kind: "live-permission" }, + ]); + + // And a frozen view cannot answer what this process is being asked now. + const early = model.turns[0].marker; + const historical = projected(events, early); + expect( + unresolved( + historical, + `xmd://repl/${EXECUTION}/sessions/+permission?at=${early}`, + holding({ permission: true }), + ), + ).toContain("frozen at an earlier position"); + }); + + it("R2: the two live facts stay independent of each other", function* () { + const events = yield* referenceEvents(); + const model = projected(events); + const question = `xmd://repl/${EXECUTION}/repl/entry-1/+elicit`; + + // Holding a permission request says nothing about a waiting question. + expect(unresolved(model, question, holding({ permission: true }))).toContain( + "nothing is being asked", + ); + expect(resolved(model, question, holding({ elicit: true })).drawers[0].kind).toBe( + "live-elicit", + ); + }); +}); diff --git a/packages/core/src/agent/function-components.ts b/packages/core/src/agent/function-components.ts index 3edf4e4f4..f7b6b3884 100644 --- a/packages/core/src/agent/function-components.ts +++ b/packages/core/src/agent/function-components.ts @@ -54,7 +54,7 @@ import { installApproveAll, installAskPermission } from "./permission.ts"; import { AgentInternal, formatLocation } from "./internal.ts"; import { serializePromptFailure } from "./errors.ts"; import type { SerializedPromptFailure } from "./errors.ts"; -import { persistPrompt, promptFailureFromRecord } from "./journal.ts"; +import { persistPrompt, promptFailureFromRecord, promptPermissionAudit } from "./journal.ts"; import type { PromptRecord } from "./journal.ts"; import { checkpointOf } from "./checkpoint.ts"; import type { AgentPromptCheckpoint } from "./checkpoint.ts"; @@ -392,6 +392,9 @@ function* runPrompt( carried: { association?: AgentPromptAssociation }, ): Operation { let consumed: ConsumedTurn = { text: "" }; + // Held out here because the audit outlives the turn's own scope: a turn that + // failed still answered whatever it was asked before it did. + const permissions = promptPermissionAudit(); // What this turn was authored to run under, and — once the provider has been // reached — the exact value it was reached with. The record below is written // from the second, so the journal describes the turn that actually ran. @@ -402,6 +405,9 @@ function* runPrompt( // document teardown. consumed = yield* scoped(function* (): Operation { const result: ConsumedTurn = { text: "" }; + // Installed before the turn is asked for, so a provider that requests + // permission the moment it is subscribed is already being watched. + yield* permissions.observe(); const stream = yield* Agent.operations.prompt(text, options); const subscription = yield* stream; let next = yield* subscription.next(); @@ -484,6 +490,12 @@ function* runPrompt( if (failure !== undefined) { record.error = failure; } + // Written once the turn is over, with the rest of the result: a decision is + // part of the account of this turn, not an event of its own. + const answered = permissions.completed(); + if (answered.length > 0) { + record.permissions = answered; + } if (throwOnError && status !== "completed") { record.raised = true; } diff --git a/packages/core/src/agent/journal.ts b/packages/core/src/agent/journal.ts index 4f95f20c6..76d910d28 100644 --- a/packages/core/src/agent/journal.ts +++ b/packages/core/src/agent/journal.ts @@ -8,6 +8,22 @@ * conversation ran under. `sequence` records prompt execution order explicitly, * so restoration never depends on asynchronous completion order. * + * ## What a turn retains about the permissions it was granted + * + * A turn that answered permission requests also retains an audit of them, so a + * reader of the history can see what the agent was allowed to do without + * re-running anything. An audit is a closed account of one request — its tool + * call, the choices the provider offered, and which one answered it — assembled + * field by field by {@link promptPermissionAudit}. + * + * Nothing else crosses. The request's `rawInput` is the agent's own argument + * text, its `Session` is a live object, and a provider's callbacks, waiters and + * errors are not data at all: a durable record that spread the request would + * publish all of them and would keep whatever the caller mutated afterwards. + * So the audit is copied at the moment the request arrives, and the decision is + * added only once it has been made — by whichever policy makes it, which this + * observation neither replaces nor delays. + * * On a full replay (journal already holds the root Close), durableRun * returns the stored root result without re-expanding, so the failed * records are restored from the stream instead of re-recording. @@ -40,7 +56,13 @@ import type { Workflow, } from "@executablemd/durable-streams"; import type { Operation } from "effection"; -import type { SessionConfiguration } from "./agent-api.ts"; +import { Agent } from "./agent-api.ts"; +import type { + PermissionOption, + PermissionOutcome, + PermissionRequest, + SessionConfiguration, +} from "./agent-api.ts"; import { readConfiguration, serializeConfiguration } from "./configuration-record.ts"; import { readCheckpoint } from "./checkpoint.ts"; import type { AgentPromptCheckpoint } from "./checkpoint.ts"; @@ -54,6 +76,21 @@ import type { SourcePosition } from "../types.ts"; /** The durable effect type every journaled Agent Prompt is recorded under. */ export const AGENT_PROMPT = "agent_prompt"; +/** + * One permission request a prompt's turn answered, as the journal retains it. + * + * The safe half of a `PermissionRequest` — which tool call asked, what the + * provider offered, and the decision that came back. Nothing here is a live + * object, and nothing here is the agent's own argument text. + */ +export interface PromptPermission { + readonly toolCallId: string; + readonly title?: string; + readonly kind?: string; + readonly options: readonly PermissionOption[]; + readonly outcome: PermissionOutcome; +} + export interface PromptRecord { sequence: number; agent: string; @@ -73,6 +110,17 @@ export interface PromptRecord { * `` carries no member at all. */ configuration?: SessionConfiguration; + /** + * The permission requests this turn answered, in the order they arrived. + * + * Arrival order rather than completion order: two overlapping requests are + * answered whenever their policies finish, and a list ordered by that would + * say the agent asked in an order it did not. A request that raised instead + * of returning has no entry — nothing decided it, so there is nothing to + * record. Absent on a turn that answered none, and on every record written + * before this member existed. + */ + permissions?: readonly PromptPermission[]; /** * True only for failed prompts thrown through `throwOnError`. Replay * uses the stored marker: a partial replay re-throws, and a full @@ -95,6 +143,141 @@ export interface PromptRecord { checkpoint?: AgentPromptCheckpoint; } +/** The safe half of one request, before its decision has been made. */ +type PermissionSubject = Omit; + +interface PermissionDraft { + readonly subject: PermissionSubject; + outcome?: PermissionOutcome; +} + +/** What one turn observed of the permission requests made while it ran. */ +export interface PromptPermissionAudit { + /** Observe permission requests for as long as the installing scope lives. */ + observe(): Operation; + /** The requests that were decided, in the order they arrived. */ + completed(): readonly PromptPermission[]; +} + +/** + * Observe the permission requests one prompt turn answers. + * + * Ordinary middleware around the existing `Agent.requestPermission()`, so it + * sits outside every installed policy and inside whatever a document composed: + * it sees the request on its way to being decided and the decision on its way + * back, and it delegates both unchanged. It decides nothing, substitutes + * nothing, and swallows nothing — a policy that raises raises through here, and + * the request it was answering is simply never completed. + * + * The subject is copied when the call begins rather than when it ends, because + * a caller is free to reuse or rewrite the request object it passed once the + * answer is in hand. A request whose safe fields do not read as this closed + * shape is observed as nothing at all: retaining a half-read audit would make + * the record unparseable, which would fail a turn over how it was watched. + */ +export function promptPermissionAudit(): PromptPermissionAudit { + const drafts: PermissionDraft[] = []; + return { + observe() { + return Agent.around({ + *requestPermission([request], next) { + const subject = permissionSubject(request); + if (subject === undefined) { + return yield* next(request); + } + const draft: PermissionDraft = { subject }; + drafts.push(draft); + const outcome = yield* next(request); + draft.outcome = permissionDecision(outcome); + return outcome; + }, + }); + }, + completed() { + return drafts.flatMap((draft) => { + if (draft.outcome === undefined) { + return []; + } + const permission: PromptPermission & { title?: string; kind?: string } = { + toolCallId: draft.subject.toolCallId, + options: draft.subject.options, + outcome: draft.outcome, + }; + if (draft.subject.title !== undefined) { + permission.title = draft.subject.title; + } + if (draft.subject.kind !== undefined) { + permission.kind = draft.subject.kind; + } + return [Object.freeze(permission)]; + }); + }, + }; +} + +function isPermissionOptionKind(value: unknown): value is PermissionOption["kind"] { + return ( + value === "allow_once" || + value === "allow_always" || + value === "reject_once" || + value === "reject_always" + ); +} + +/** + * The safe fields of one live request, named one at a time. + * + * Named rather than spread: a spread would carry `rawInput` and every member a + * provider added to the object, and the next member somebody adds to + * `PermissionRequest` would join the journal without anyone deciding it should. + */ +function permissionSubject(request: PermissionRequest): PermissionSubject | undefined { + const { toolCallId, title, kind } = request.toolCall; + if (typeof toolCallId !== "string") { + return undefined; + } + if (title !== undefined && typeof title !== "string") { + return undefined; + } + if (kind !== undefined && typeof kind !== "string") { + return undefined; + } + const options: PermissionOption[] = []; + for (const option of request.options) { + if (typeof option.optionId !== "string" || typeof option.name !== "string") { + return undefined; + } + if (!isPermissionOptionKind(option.kind)) { + return undefined; + } + options.push( + Object.freeze({ optionId: option.optionId, name: option.name, kind: option.kind }), + ); + } + const subject: PermissionSubject & { title?: string; kind?: string } = { + toolCallId, + options: Object.freeze(options), + }; + if (title !== undefined) { + subject.title = title; + } + if (kind !== undefined) { + subject.kind = kind; + } + return Object.freeze(subject); +} + +/** One decision, as this record retains it, or nothing that is one. */ +function permissionDecision(outcome: PermissionOutcome): PermissionOutcome | undefined { + if (outcome.outcome === "cancelled") { + return Object.freeze({ outcome: "cancelled" }); + } + if (outcome.outcome === "selected" && typeof outcome.optionId === "string") { + return Object.freeze({ outcome: "selected", optionId: outcome.optionId }); + } + return undefined; +} + export function* persistPrompt( identity: { name: string; input: string; position?: Readonly }, live: () => Operation, @@ -268,6 +451,9 @@ function serializePromptRecord(record: PromptRecord): Json { if (record.error !== undefined) { payload.error = record.error; } + if (record.permissions !== undefined) { + payload.permissions = record.permissions.map(serializePermission); + } Object.assign(payload, serializeConfiguration(record.configuration)); if (record.raised === true) { payload.raised = true; @@ -282,6 +468,127 @@ function serializePromptRecord(record: PromptRecord): Json { return payload; } +/** One audit as the journal writes it, member by named member. */ +function serializePermission(permission: PromptPermission): Json { + const payload: Record = { + toolCallId: permission.toolCallId, + options: permission.options.map((option) => ({ + optionId: option.optionId, + name: option.name, + kind: option.kind, + })), + outcome: + permission.outcome.outcome === "selected" + ? { outcome: "selected", optionId: permission.outcome.optionId } + : { outcome: "cancelled" }, + }; + if (permission.title !== undefined) { + payload.title = permission.title; + } + if (permission.kind !== undefined) { + payload.kind = permission.kind; + } + return payload; +} + +/** What a retained audit that does not read back is, as distinct from none. */ +const UNREADABLE = Symbol("unreadable permission audit"); + +/** + * The audits one durable record carries, read as the closed shape they claim. + * + * Closed in both directions: every member is checked, and a member nothing here + * defines refuses the whole record rather than being read past. An audit is + * evidence about what an agent was permitted to do, and a reader that ignored + * the parts it did not recognize would be reporting a decision it had not read. + */ +function readPermissions(value: unknown): readonly PromptPermission[] | typeof UNREADABLE { + if (!Array.isArray(value)) { + return UNREADABLE; + } + const permissions: PromptPermission[] = []; + for (const member of value) { + if (!isRecord(member)) { + return UNREADABLE; + } + const { toolCallId, title, kind, options, outcome, ...rest } = member; + if (Object.keys(rest).length > 0) { + return UNREADABLE; + } + if (typeof toolCallId !== "string") { + return UNREADABLE; + } + if (title !== undefined && typeof title !== "string") { + return UNREADABLE; + } + if (kind !== undefined && typeof kind !== "string") { + return UNREADABLE; + } + const offered = readPermissionOptions(options); + if (offered === UNREADABLE) { + return UNREADABLE; + } + const decided = readPermissionOutcome(outcome); + if (decided === undefined) { + return UNREADABLE; + } + const permission: PromptPermission & { title?: string; kind?: string } = { + toolCallId, + options: offered, + outcome: decided, + }; + if (title !== undefined) { + permission.title = title; + } + if (kind !== undefined) { + permission.kind = kind; + } + permissions.push(permission); + } + return permissions; +} + +function readPermissionOptions(value: unknown): readonly PermissionOption[] | typeof UNREADABLE { + if (!Array.isArray(value)) { + return UNREADABLE; + } + const options: PermissionOption[] = []; + for (const member of value) { + if (!isRecord(member)) { + return UNREADABLE; + } + const { optionId, name, kind, ...rest } = member; + if (Object.keys(rest).length > 0) { + return UNREADABLE; + } + if (typeof optionId !== "string" || typeof name !== "string") { + return UNREADABLE; + } + if (!isPermissionOptionKind(kind)) { + return UNREADABLE; + } + options.push({ optionId, name, kind }); + } + return options; +} + +function readPermissionOutcome(value: unknown): PermissionOutcome | undefined { + if (!isRecord(value)) { + return undefined; + } + const { outcome, optionId, ...rest } = value; + if (Object.keys(rest).length > 0) { + return undefined; + } + if (outcome === "cancelled" && optionId === undefined) { + return { outcome: "cancelled" }; + } + if (outcome === "selected" && typeof optionId === "string") { + return { outcome: "selected", optionId }; + } + return undefined; +} + function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } @@ -307,10 +614,16 @@ export function parsePromptRecord(value: unknown): PromptRecord | undefined { stopReason, text, error, + permissions, raised, checkpoint, } = value; - if (typeof sequence !== "number" || typeof agent !== "string") { + if ( + typeof sequence !== "number" || + !Number.isInteger(sequence) || + sequence < 0 || + typeof agent !== "string" + ) { return undefined; } if (typeof sessionKey !== "string" || typeof text !== "string") { @@ -333,6 +646,13 @@ export function parsePromptRecord(value: unknown): PromptRecord | undefined { } record.error = parsed; } + if (permissions !== undefined) { + const audited = readPermissions(permissions); + if (audited === UNREADABLE) { + return undefined; + } + record.permissions = audited; + } // Refused rather than read past: a record whose configuration does not read // back is not describing work this build can say anything about. const configuration = readConfiguration(value); diff --git a/packages/core/tests/agent-function-components.test.ts b/packages/core/tests/agent-function-components.test.ts index 71629b2b1..5c45dbdd8 100644 --- a/packages/core/tests/agent-function-components.test.ts +++ b/packages/core/tests/agent-function-components.test.ts @@ -10,9 +10,9 @@ import { beforeAll, describe, it } from "@executablemd/test-support/bdd"; import { expect } from "@executablemd/test-support/expect"; -import { InMemoryStream } from "@executablemd/durable-streams"; +import { InMemoryStream, serializeDurableEvent } from "@executablemd/durable-streams"; import type { DurableEvent } from "@executablemd/durable-streams"; -import { createContext, ensure, scoped } from "effection"; +import { createContext, ensure, scoped, spawn, withResolvers } from "effection"; import type { Operation, Result, Stream } from "effection"; import { ensureDir, rm, writeTextFile } from "@effectionx/fs"; import { randomUUID } from "node:crypto"; @@ -26,6 +26,9 @@ import { isSessionRequest } from "../src/agent/session-request.ts"; import type { ConfigureAgentSession } from "../src/agent/session-placement.ts"; import type { AgentPromptEvent, + PermissionOption, + PermissionOutcome, + PermissionRequest, PromptOptions, Session, SessionConfiguration, @@ -37,7 +40,7 @@ import { installAgentComponents } from "../src/agent/components.ts"; import { registerComponents } from "../src/components/registration.ts"; import { AgentInternal } from "../src/agent/internal.ts"; import { parsePromptRecord } from "../src/agent/journal.ts"; -import { installPromptFailurePolicy } from "../src/agent/permission.ts"; +import { installApproveAll, installPromptFailurePolicy } from "../src/agent/permission.ts"; import { inspectComponent } from "../src/inspect.ts"; import type { AgentProviderFactory } from "../src/agent/provider-api.ts"; import type { Json } from "../src/types.ts"; @@ -72,10 +75,122 @@ interface Trace { sessionArgumentCounts?: number[]; } +/** + * One permission request a scripted turn makes while it is running. + * + * `options` is the exact array the provider hands over, so a test that mutates + * it afterwards is mutating what the caller kept — which is the only way to + * show the audit copied rather than borrowed it. + */ +interface ScriptedPermission { + toolCallId: string; + title?: string; + kind?: string; + rawInput?: unknown; + options: PermissionOption[]; +} + +/** What a turn asks for, and what it saw come back. */ +interface PermissionPlan { + /** The requests this turn makes, in the order they must arrive. */ + readonly requests: readonly ScriptedPermission[]; + /** + * Hold the first request open until the last one has answered. + * + * The two gates make the interleaving exact rather than likely: the provider + * does not send the second request until the policy has seen the first, and + * the policy does not answer the first until it has answered the second. + */ + readonly reversed?: { arrived: PermissionGate; released: PermissionGate }; + /** Overwrite everything the caller kept, once a request has settled. */ + readonly mutateAfterSettlement?: boolean; + /** What the provider was answered, in the order the answers arrived. */ + answered: { toolCallId: string; outcome: PermissionOutcome }[]; + /** The requests that raised instead of answering. */ + raised: string[]; +} + +interface PermissionGate { + readonly operation: Operation; + open(): void; +} + +function permissionGate(): PermissionGate { + const { operation, resolve } = withResolvers(); + return { operation, open: () => resolve() }; +} + +/** The text every mutated field is overwritten with after settlement. */ +const MUTATED = "mutated-after-settlement"; + +function permissionRequest(scripted: ScriptedPermission, session: Session): PermissionRequest { + const toolCall: PermissionRequest["toolCall"] = { toolCallId: scripted.toolCallId }; + if (scripted.title !== undefined) { + toolCall.title = scripted.title; + } + if (scripted.kind !== undefined) { + toolCall.kind = scripted.kind; + } + if (scripted.rawInput !== undefined) { + toolCall.rawInput = scripted.rawInput; + } + return { session, toolCall, options: scripted.options }; +} + +function* askPermission( + plan: PermissionPlan, + scripted: ScriptedPermission, + session: Session, +): Operation { + const asked = scripted.toolCallId; + const request = permissionRequest(scripted, session); + try { + const outcome = yield* Agent.operations.requestPermission(request); + plan.answered.push({ toolCallId: asked, outcome }); + } catch (error) { + plan.raised.push(error instanceof Error ? error.message : String(error)); + } + if (plan.mutateAfterSettlement !== true) { + return; + } + // Everything a caller could still be holding, rewritten the instant the + // answer is in hand and long before the record is written. + request.toolCall.toolCallId = MUTATED; + request.toolCall.title = MUTATED; + request.toolCall.kind = MUTATED; + request.toolCall.rawInput = MUTATED; + for (const option of scripted.options) { + option.optionId = MUTATED; + option.name = MUTATED; + option.kind = "allow_always"; + } + scripted.options.length = 0; +} + +/** Make this turn's permission requests, in the order the plan names. */ +function* askPermissions(plan: PermissionPlan, session: Session): Operation { + if (plan.reversed === undefined) { + for (const scripted of plan.requests) { + yield* askPermission(plan, scripted, session); + } + return; + } + const [first, ...rest] = plan.requests; + const held = yield* spawn(() => askPermission(plan, first, session)); + // Sent only once the policy has the first request in hand, so what arrived + // first is a fact rather than a scheduling accident. + yield* plan.reversed.arrived.operation; + for (const scripted of rest) { + yield* askPermission(plan, scripted, session); + } + yield* held; +} + function stubFactory( trace: Trace, fail?: boolean, refuseBeforeStart?: boolean, + permissions?: PermissionPlan, ): AgentProviderFactory { const issued = new Map(); // One operation per conversation, as a provider has: the owner that settles @@ -147,7 +262,7 @@ function stubFactory( }, }; } - return stubStream(content, options, fail, trace); + return stubStream(content, options, fail, trace, permissions); }, }, { at: "min" }, @@ -160,6 +275,7 @@ function stubStream( options: PromptOptions | undefined, fail?: boolean, trace?: Trace, + permissions?: PermissionPlan, ): Stream { return { *[Symbol.iterator]() { @@ -184,9 +300,17 @@ function stubStream( { type: "terminal", status: fail ? "failed" : "completed" }, ]; let index = 0; + let asked = false; return { - // deno-lint-ignore require-yield *next() { + // After the turn started and before it produced anything: where a + // real agent asks whether it may use the tool it is about to use. + if (index === 1 && !asked) { + asked = true; + if (permissions !== undefined) { + yield* askPermissions(permissions, session); + } + } if (index < events.length) { return { done: false, value: events[index++]! }; } @@ -211,6 +335,15 @@ interface RunOptions { promptTimeout?: number; /** Refuse the turn before it starts, as a failed configuration does. */ refuseBeforeStart?: boolean; + /** The permission requests the provider makes while its turn runs. */ + permissions?: PermissionPlan; + /** + * The policy that answers them, installed where a permission policy lives. + * + * At `min`, like every installed policy, so it sits inside the observation + * the prompt turn itself installs — which is the arrangement under test. + */ + permissionPolicy?: () => Operation; /** * An ordinary public handler, installed around the whole execution. * @@ -244,10 +377,13 @@ function* runDoc( } yield* installAgentComponents({ rootProvider: { - factory: stubFactory(trace, options.fail, options.refuseBeforeStart), + factory: stubFactory(trace, options.fail, options.refuseBeforeStart, options.permissions), options: { defaultAgent: "stub-agent", permissionMode: "deny-all" }, }, }); + if (options.permissionPolicy) { + yield* options.permissionPolicy(); + } if (options.policy) { yield* installPromptFailurePolicy(options.policy); } @@ -858,6 +994,21 @@ describe("Tier AF — Session configuration", () => { expect(parsed?.configuration).toBe(undefined); }); + it("AF32a: a prompt sequence is a non-negative integer", function* () { + const complete = { + sequence: 0, + agent: "codex", + sessionKey: "xmd:v1:a", + status: "completed", + text: "hello", + }; + + expect(parsePromptRecord(complete)?.sequence).toBe(0); + for (const sequence of [-1, 0.5]) { + expect(parsePromptRecord({ ...complete, sequence })).toBe(undefined); + } + }); + it("AF33: a configuration member that names no choice refuses the record", function* () { const complete = { sequence: 0, @@ -1182,3 +1333,371 @@ describe("Tier AF — the final routed Session decides", () => { expect(prompt?.value.configuration).toBe(undefined); }); }); + +/** + * Tier AF — the permission audit one turn retains (issue #854 A1, A2). + * + * A turn that was granted permission retains an account of it, so a reader of + * the history can see what the agent was allowed to do. What is under test is + * both halves of that: the account is complete and exact, and it is only the + * account — a live request object, its `rawInput`, its session and anything the + * caller went on to mutate all stay out of the journal. + * + * Every journal here comes from a real turn through the real Prompt, because + * the audit's whole claim is about where in a turn the copy is taken. + */ +function permissionPlan( + requests: readonly ScriptedPermission[], + extra: Partial = {}, +): PermissionPlan { + return { requests, answered: [], raised: [], ...extra }; +} + +/** The options a tool call usually offers. Fresh per plan, because they mutate. */ +function offeredOptions(): PermissionOption[] { + return [ + { optionId: "allow", name: "Allow once", kind: "allow_once" }, + { optionId: "always", name: "Always allow", kind: "allow_always" }, + { optionId: "deny", name: "Deny", kind: "reject_once" }, + ]; +} + +/** The audits one prompt record carries, as the durable Json holds them. */ +function recordedPermissions(record: Record | undefined): Json { + return record?.permissions ?? null; +} + +/** Whether a canary string occurs anywhere in a value. */ +function mentions(value: unknown, canary: string): boolean { + return JSON.stringify(value)?.includes(canary) === true; +} + +describe("Tier AF — the permission audit one turn retains", () => { + beforeAll(() => useTempFileCompiler()); + + it("A1: a turn that was asked nothing retains no audit at all", function* () { + const { result, events } = yield* runDoc('\n'); + + expect(result.ok).toBe(true); + const [prompt] = promptRecords(events); + // Absent rather than empty: a turn nobody asked about is the history every + // record written before this member existed describes. + expect(prompt?.value.permissions).toBe(undefined); + expect(Object.keys(prompt?.value ?? {})).not.toContain("permissions"); + }); + + it("A1: one answered request round-trips as exactly its safe fields", function* () { + const plan = permissionPlan([ + { + toolCallId: "call-1", + title: "Write README.md", + kind: "edit", + options: offeredOptions(), + }, + ]); + const { result, events } = yield* runDoc('\n', { permissions: plan }); + + expect(result.ok).toBe(true); + // The base policy denies, and denial selects the reject option the + // provider offered — the decision the provider itself was answered with. + expect(plan.answered).toEqual([ + { toolCallId: "call-1", outcome: { outcome: "selected", optionId: "deny" } }, + ]); + const [prompt] = promptRecords(events); + expect(recordedPermissions(prompt?.value)).toEqual([ + { + toolCallId: "call-1", + title: "Write README.md", + kind: "edit", + options: [ + { optionId: "allow", name: "Allow once", kind: "allow_once" }, + { optionId: "always", name: "Always allow", kind: "allow_always" }, + { optionId: "deny", name: "Deny", kind: "reject_once" }, + ], + outcome: { outcome: "selected", optionId: "deny" }, + }, + ]); + // And the same record read back out of the journal, because the shape the + // parser accepts is the shape the writer wrote. + expect(parsePromptRecord(prompt?.value)?.permissions).toEqual([ + { + toolCallId: "call-1", + title: "Write README.md", + kind: "edit", + options: [ + { optionId: "allow", name: "Allow once", kind: "allow_once" }, + { optionId: "always", name: "Always allow", kind: "allow_always" }, + { optionId: "deny", name: "Deny", kind: "reject_once" }, + ], + outcome: { outcome: "selected", optionId: "deny" }, + }, + ]); + }); + + it("A1: several requests retain one audit each, in the order they arrived", function* () { + const plan = permissionPlan([ + { toolCallId: "call-1", kind: "read", options: offeredOptions() }, + { toolCallId: "call-2", options: offeredOptions() }, + // No way to say no, so the decision is cancellation rather than a choice. + { + toolCallId: "call-3", + title: "Run the build", + options: [{ optionId: "allow", name: "Allow once", kind: "allow_once" }], + }, + ]); + const { result, events } = yield* runDoc('\n', { permissions: plan }); + + expect(result.ok).toBe(true); + const [prompt] = promptRecords(events); + const audits = parsePromptRecord(prompt?.value)?.permissions ?? []; + expect(audits.map((audit) => audit.toolCallId)).toEqual(["call-1", "call-2", "call-3"]); + expect(audits.map((audit) => audit.outcome)).toEqual([ + { outcome: "selected", optionId: "deny" }, + { outcome: "selected", optionId: "deny" }, + { outcome: "cancelled" }, + ]); + // The optional members are exactly as asked, absent included. + expect(audits[0].title).toBe(undefined); + expect(audits[0].kind).toBe("read"); + expect(audits[1].title).toBe(undefined); + expect(audits[1].kind).toBe(undefined); + expect(audits[2].title).toBe("Run the build"); + // A cancelled audit names no choice, and the choices it was offered survive + // in the order the provider offered them. + expect(audits[2].options).toEqual([ + { optionId: "allow", name: "Allow once", kind: "allow_once" }, + ]); + }); + + it("A1: occurrence order survives a reversed return order", function* () { + const arrived = permissionGate(); + const released = permissionGate(); + const plan = permissionPlan( + [ + { toolCallId: "first", options: offeredOptions() }, + { toolCallId: "second", options: offeredOptions() }, + ], + { reversed: { arrived, released } }, + ); + const { result, events } = yield* runDoc('\n', { + permissions: plan, + permissionPolicy: () => + Agent.around( + { + *requestPermission([request]): Operation { + if (request.toolCall.toolCallId === "first") { + arrived.open(); + yield* released.operation; + return { outcome: "selected", optionId: "allow" }; + } + released.open(); + return { outcome: "selected", optionId: "always" }; + }, + }, + { at: "min" }, + ), + }); + + expect(result.ok).toBe(true); + // The interleaving really happened: the second request was answered first. + expect(plan.answered.map((answer) => answer.toolCallId)).toEqual(["second", "first"]); + const [prompt] = promptRecords(events); + const audits = parsePromptRecord(prompt?.value)?.permissions ?? []; + // And the record says what the agent asked, in the order it asked. + expect(audits.map((audit) => audit.toolCallId)).toEqual(["first", "second"]); + expect(audits.map((audit) => audit.outcome)).toEqual([ + { outcome: "selected", optionId: "allow" }, + { outcome: "selected", optionId: "always" }, + ]); + }); + + it("A1: a request that raised instead of answering retains no audit", function* () { + const plan = permissionPlan([ + { toolCallId: "call-1", options: offeredOptions() }, + { toolCallId: "call-2", options: offeredOptions() }, + ]); + const { result, events } = yield* runDoc('\n', { + permissions: plan, + permissionPolicy: () => + Agent.around( + { + // deno-lint-ignore require-yield + *requestPermission([request]): Operation { + if (request.toolCall.toolCallId === "call-1") { + throw new Error("this policy could not decide"); + } + return { outcome: "selected", optionId: "allow" }; + }, + }, + { at: "min" }, + ), + }); + + expect(result.ok).toBe(true); + expect(plan.raised).toEqual(["this policy could not decide"]); + const [prompt] = promptRecords(events); + const audits = parsePromptRecord(prompt?.value)?.permissions ?? []; + // Nothing decided the first request, so there is no decision to describe. + expect(audits.map((audit) => audit.toolCallId)).toEqual(["call-2"]); + }); + + it("A1: a turn that failed still retains what it was granted before it did", function* () { + const plan = permissionPlan([{ toolCallId: "call-1", options: offeredOptions() }]); + const { events } = yield* runDoc('\n', { permissions: plan, fail: true }); + + const [prompt] = promptRecords(events); + expect(prompt?.value.status).toBe("failed"); + expect(parsePromptRecord(prompt?.value)?.permissions?.map((audit) => audit.toolCallId)).toEqual( + ["call-1"], + ); + }); + + it("A1: a record written before this member existed still parses", function* () { + const legacy = { + sequence: 0, + agent: "codex", + sessionKey: "xmd:v1:a", + status: "completed", + text: "hello", + }; + + const parsed = parsePromptRecord(legacy); + expect(parsed?.text).toBe("hello"); + expect(parsed?.permissions).toBe(undefined); + }); + + it("A1: an audit this build cannot read refuses the whole prompt record", function* () { + const complete = { + sequence: 0, + agent: "codex", + sessionKey: "xmd:v1:a", + status: "completed", + text: "hello", + permissions: [ + { + toolCallId: "call-1", + options: [{ optionId: "allow", name: "Allow once", kind: "allow_once" }], + outcome: { outcome: "selected", optionId: "allow" }, + }, + ], + }; + expect(parsePromptRecord(complete)?.permissions).toHaveLength(1); + + const audit = complete.permissions[0]; + const refused: Json[] = [ + "not a list", + [{ ...audit, toolCallId: 7 }], + [{ ...audit, title: 7 }], + [{ ...audit, kind: [] }], + // A kind nothing offers is not a choice this build can report. + [{ ...audit, options: [{ optionId: "allow", name: "Allow once", kind: "allow_maybe" }] }], + [{ ...audit, options: [{ optionId: "allow", name: "Allow once" }] }], + [{ ...audit, options: "allow" }], + // Closed: a member nothing here defines is a record describing something + // this build cannot state, not a record with an extra field. + [{ ...audit, options: [{ optionId: "a", name: "A", kind: "allow_once", rawInput: "x" }] }], + [{ ...audit, rawInput: { path: "/etc/passwd" } }], + [{ ...audit, outcome: "selected" }], + [{ ...audit, outcome: { outcome: "selected" } }], + [{ ...audit, outcome: { outcome: "cancelled", optionId: "allow" } }], + [{ ...audit, outcome: { outcome: "refused" } }], + [{ ...audit, outcome: { outcome: "cancelled", why: "no" } }], + ]; + for (const permissions of refused) { + const described = JSON.stringify(permissions); + expect([described, parsePromptRecord({ ...complete, permissions })]).toEqual([ + described, + undefined, + ]); + } + }); + + it("A2: nothing the request carried beyond its audit crosses the boundary", function* () { + const canary = "canary-7f3c1a-raw-input"; + const plan = permissionPlan([ + { + toolCallId: "call-1", + title: "Write README.md", + kind: "edit", + rawInput: { path: "/tmp/README.md", content: canary }, + options: offeredOptions(), + }, + ]); + const { result, events } = yield* runDoc( + ['', '', "", ""].join("\n"), + { permissions: plan }, + ); + + expect(result.ok).toBe(true); + // It really was asked with the canary, so its absence below is an absence + // rather than a request that never carried one. + expect(plan.answered).toHaveLength(1); + const [prompt] = promptRecords(events); + // The journal's own bytes, not just the value read back out of them. + const written = events.map((event) => serializeDurableEvent(event)).join("\n"); + expect(written.includes("agent_prompt")).toBe(true); + expect(written.includes(canary)).toBe(false); + expect(mentions(prompt?.value, canary)).toBe(false); + expect(mentions(parsePromptRecord(prompt?.value), canary)).toBe(false); + + // Nor the live objects the request travelled with. The audit names the + // tool call and the choices, and nothing that could reach a provider. + const audits = parsePromptRecord(prompt?.value)?.permissions ?? []; + expect(Object.keys(audits[0]).sort()).toEqual([ + "kind", + "options", + "outcome", + "title", + "toolCallId", + ]); + expect(mentions(audits[0], "cwd")).toBe(false); + expect(mentions(audits[0], "stub:review")).toBe(false); + }); + + it("A2: mutating everything the caller kept cannot change the record", function* () { + const plan = permissionPlan( + [{ toolCallId: "call-1", title: "Write README.md", kind: "edit", options: offeredOptions() }], + { mutateAfterSettlement: true }, + ); + const { result, events } = yield* runDoc('\n', { permissions: plan }); + + expect(result.ok).toBe(true); + // The caller really did rewrite what it was holding, before the turn ended + // and long before this record was written. + expect(plan.requests[0].options).toEqual([]); + const [prompt] = promptRecords(events); + expect(mentions(prompt?.value, MUTATED)).toBe(false); + expect(recordedPermissions(prompt?.value)).toEqual([ + { + toolCallId: "call-1", + title: "Write README.md", + kind: "edit", + options: [ + { optionId: "allow", name: "Allow once", kind: "allow_once" }, + { optionId: "always", name: "Always allow", kind: "allow_always" }, + { optionId: "deny", name: "Deny", kind: "reject_once" }, + ], + outcome: { outcome: "selected", optionId: "deny" }, + }, + ]); + }); + + it("A2: the observation decides nothing — the installed policy still does", function* () { + const plan = permissionPlan([{ toolCallId: "call-1", options: offeredOptions() }]); + const { result, events } = yield* runDoc('\n', { + permissions: plan, + permissionPolicy: () => installApproveAll(), + }); + + expect(result.ok).toBe(true); + // Approve-all still chose, and the provider was answered with its choice. + expect(plan.answered).toEqual([ + { toolCallId: "call-1", outcome: { outcome: "selected", optionId: "allow" } }, + ]); + const [prompt] = promptRecords(events); + expect(parsePromptRecord(prompt?.value)?.permissions?.[0].outcome).toEqual({ + outcome: "selected", + optionId: "allow", + }); + }); +}); diff --git a/specs/acp-client-spec.md b/specs/acp-client-spec.md index f11f7a9ad..fe1b966f4 100644 --- a/specs/acp-client-spec.md +++ b/specs/acp-client-spec.md @@ -498,7 +498,8 @@ boundary runs after the component has returned. Each prompt is one durable operation (`agent_prompt`). The record carries the prompt's identity and input, the agent and session identity, terminal status, stop reason, text (including partial text on failure), any structured failure, -and one optional canonical `configuration`. The configuration is retained only +one optional canonical `configuration`, and one optional list of `permissions`. +The configuration is retained only after the provider emits `started`: a turn that started is one the provider put under exactly those settings, while a configuration refusal started no turn and retains none. A prompt that starts and later fails or is cancelled retains @@ -506,6 +507,38 @@ the configuration it ran under. An unconfigured prompt omits it. A `sequence` records execution order explicitly, and per-location ordinals keep durable identities stable across `` loops. +### The permission audit a turn retains + +`permissions` is the account of what the agent was allowed to do while this turn +ran. Each member describes one request that was decided: + +| Member | Meaning | +| --- | --- | +| `toolCallId` | the tool call the agent asked about | +| `title`, `kind` | the provider's own words for it, where it supplied them | +| `options` | the choices offered, each `optionId`, `name` and permission-option `kind`, in the order the provider offered them | +| `outcome` | `{ outcome: "selected", optionId }` or `{ outcome: "cancelled" }` | + +Members appear in the order the requests **arrived**, not the order they were +answered: two overlapping requests settle whenever their policies finish, and an +order taken from that would report an order the agent did not ask in. + +Nothing else crosses the durable boundary. The request's `rawInput`, its +`Session`, the provider's callbacks and waiters, and any error are absent by +construction — the record is assembled field by named field from the request as +it arrives, and the decision is added when it is made. A request that raised +instead of returning was decided by nothing and has no member. + +The list is **closed**: a member this specification does not define, an option +`kind` outside the four, or an outcome that is neither of the two refuses the +whole prompt record rather than being read past. A turn that answered no request +omits the member entirely, which is also how every record written before this +member existed reads. Parsing an older record is unchanged, and nothing rewrites +one. + +Observing a request changes no decision. The installed `PermissionMode` policy +still answers it, and these records describe decisions rather than making them. + On a **full replay** (the journal already holds the root `Close`), completed records are restored from the journal without contacting any provider — producing identical output and identical aggregated failures. A prompt failure diff --git a/test-weights.json b/test-weights.json index bb32b4d5b..cd8f17f35 100644 --- a/test-weights.json +++ b/test-weights.json @@ -1,8 +1,8 @@ { "version": 1, "source": { - "commit": "05c912f26682c7ddf1818403aa332f057675e1cb", - "runUrl": "https://github.com/taras/executable.md/actions/runs/36319908505", + "commit": "a403cc57170514db95722ce7926aec5515f92453", + "runUrl": "https://github.com/taras/executable.md/actions/runs/36417683621", "attempt": 1, "runner": "ubuntu-latest", "deno": "deno 2.9.5 (stable, release, x86_64-unknown-linux-gnu)", @@ -11,1088 +11,1094 @@ }, "runtimes": { "deno": { - "packages/acp/tests/acceptance.test.ts": 4424, - "packages/acp/tests/acpx-checkpoint-meta.test.ts": 7578, - "packages/acp/tests/acpx-materialization.test.ts": 7886, - "packages/acp/tests/acpx-transient-env.test.ts": 1594, - "packages/acp/tests/adapter-materialization.test.ts": 71437, - "packages/acp/tests/adapter-protocol.test.ts": 54597, - "packages/acp/tests/codex-surfaces.test.ts": 1607, - "packages/acp/tests/loaded-copy.test.ts": 3996, - "packages/acp/tests/native-launch.test.ts": 4703, - "packages/acp/tests/permission-bridge.test.ts": 3504, - "packages/acp/tests/provider.test.ts": 4604, - "packages/acp/tests/serial-queue.test.ts": 1383, - "packages/acp/tests/session-key.test.ts": 3417, - "packages/acp/tests/session-route.test.ts": 3642, - "packages/acp/tests/terminal-screen.test.ts": 1467, - "packages/cli/tests/agent-adapters.test.ts": 4822, - "packages/cli/tests/agent-cli.test.ts": 34852, - "packages/cli/tests/agent-config.test.ts": 3147, - "packages/cli/tests/agent-options-cli.test.ts": 14309, - "packages/cli/tests/agent-options.test.ts": 3162, - "packages/cli/tests/agent-session-coordinator.test.ts": 3791, - "packages/cli/tests/cli-help.test.ts": 12834, - "packages/cli/tests/command.test.ts": 9005, - "packages/cli/tests/compiled-upgrade.test.ts": 9187, - "packages/cli/tests/document-suites/inline/inline-markdown.test.ts": 7688, - "packages/cli/tests/document-suites/plan/plan-markdown.test.ts": 106746, - "packages/cli/tests/document-suites/props/props-markdown.test.ts": 7176, - "packages/cli/tests/document-suites/syntax/syntax-markdown.test.ts": 10090, - "packages/cli/tests/document-suites/targets/targets-markdown.test.ts": 8942, - "packages/cli/tests/document-suites/verbose/verbose-markdown.test.ts": 12701, - "packages/cli/tests/evaluate-component.test.ts": 9823, - "packages/cli/tests/evaluate-workflow.test.ts": 4862, - "packages/cli/tests/fetch-cli.test.ts": 3226, - "packages/cli/tests/file-stream.test.ts": 1584, - "packages/cli/tests/github-zero-effect.test.ts": 6274, - "packages/cli/tests/inline-cli.test.ts": 26698, - "packages/cli/tests/launch-timeout.test.ts": 11426, - "packages/cli/tests/packaged-document.test.ts": 4260, - "packages/cli/tests/plan-args.test.ts": 1382, - "packages/cli/tests/plan-cli.test.ts": 88923, - "packages/cli/tests/plan-command-document.test.ts": 8750, - "packages/cli/tests/plan-component.test.ts": 10892, - "packages/cli/tests/plan-host-acts.test.ts": 4787, - "packages/cli/tests/plan.test.ts": 14517, - "packages/cli/tests/plugin-cli.test.ts": 32667, - "packages/cli/tests/plugin-host.test.ts": 5344, - "packages/cli/tests/plugin-modules.test.ts": 1381, - "packages/cli/tests/plugin-selection.test.ts": 1373, - "packages/cli/tests/process-retention.test.ts": 5082, - "packages/cli/tests/props-cli.test.ts": 24282, - "packages/cli/tests/props-schema.test.ts": 3610, - "packages/cli/tests/props-sources.test.ts": 2011, - "packages/cli/tests/pull-request-read-fork.test.ts": 6259, - "packages/cli/tests/repl-composition.test.ts": 1823, - "packages/cli/tests/repl-execution.test.ts": 4755, - "packages/cli/tests/repl-model.test.ts": 3988, - "packages/cli/tests/repl-route.test.ts": 3896, - "packages/cli/tests/repl-terminal.test.ts": 2683, - "packages/cli/tests/run-composition-deno.test.ts": 6681, - "packages/cli/tests/run-composition-nested.test.ts": 18901, - "packages/cli/tests/run-composition.test.ts": 6238, - "packages/cli/tests/run-deadline.test.ts": 7605, - "packages/cli/tests/run-profile.test.ts": 3765, - "packages/cli/tests/run-timeouts.test.ts": 11458, - "packages/cli/tests/secret-detection-cli.test.ts": 15679, - "packages/cli/tests/service-document.test.ts": 4634, - "packages/cli/tests/service-host.test.ts": 4545, - "packages/cli/tests/session-launch-cli.test.ts": 10090, - "packages/cli/tests/stdin-cli.test.ts": 30552, - "packages/cli/tests/stdout-delivery.test.ts": 1377, - "packages/cli/tests/syntax-cli.test.ts": 30181, - "packages/cli/tests/targets-cli.test.ts": 23314, - "packages/cli/tests/test-root-profile.test.ts": 6783, - "packages/cli/tests/test-target.test.ts": 30382, - "packages/cli/tests/testing-activation.test.ts": 3995, - "packages/cli/tests/testing-execution-host.test.ts": 47616, - "packages/cli/tests/upgrade-cli.test.ts": 16284, - "packages/cli/tests/upgrade-command-document.test.ts": 6450, - "packages/cli/tests/upgrade-output.test.ts": 3951, - "packages/cli/tests/value-root.test.ts": 14567, - "packages/cli/tests/verbose-component.test.ts": 4400, - "packages/cli/tests/workflow-agent-sessions.test.ts": 18435, - "packages/cli/tests/workflow-agent.test.ts": 14865, - "packages/cli/tests/workflow-cli.test.ts": 95078, - "packages/cli/tests/workflow-crash.test.ts": 78332, - "packages/cli/tests/workflow-declaration.test.ts": 3899, - "packages/cli/tests/workflow-export-publication.test.ts": 16177, - "packages/cli/tests/workflow-fetch.test.ts": 7205, - "packages/cli/tests/workflow-fork.test.ts": 71027, - "packages/cli/tests/workflow-host.test.ts": 13119, - "packages/cli/tests/workflow-inspection.test.ts": 96455, - "packages/cli/tests/workflow-installation.test.ts": 5249, - "packages/cli/tests/workflow-lifecycle-control.test.ts": 17955, - "packages/cli/tests/workflow-retention.test.ts": 20614, - "packages/cli/tests/workflow-suspension.test.ts": 6970, - "packages/code-review-agent/tests/doctor.test.ts": 1350, - "packages/code-review-agent/tests/parse-diagnostics.test.ts": 1390, - "packages/code-review-agent/tests/parse-diff.test.ts": 1358, - "packages/code-review-agent/tests/parse-doctor.test.ts": 3524, - "packages/code-review-agent/tests/parse-oxlint.test.ts": 1375, - "packages/code-review-agent/tests/policy.test.ts": 1347, - "packages/core/tests/agent-api.test.ts": 1408, - "packages/core/tests/agent-components.test.ts": 4063, - "packages/core/tests/agent-function-components.test.ts": 5307, - "packages/core/tests/agent-session-launch.test.ts": 5840, - "packages/core/tests/agent-session-placement.test.ts": 1433, - "packages/core/tests/agent-session-use-loaded-copy.test.ts": 1455, - "packages/core/tests/agent-session-use.test.ts": 1413, - "packages/core/tests/agent-stack.test.ts": 3680, - "packages/core/tests/answer-identity.test.ts": 3169, - "packages/core/tests/answers-component.test.ts": 4212, - "packages/core/tests/answers-expansion-recursion.test.ts": 3393, - "packages/core/tests/capture-errors.test.ts": 3569, - "packages/core/tests/capture-props.test.ts": 3371, - "packages/core/tests/cli-journal.test.ts": 9912, - "packages/core/tests/code-block-component.test.ts": 4810, - "packages/core/tests/compiler-boundary.test.ts": 3363, - "packages/core/tests/component-api.test.ts": 3113, - "packages/core/tests/component-registration.test.ts": 4113, - "packages/core/tests/component-returns.test.ts": 4502, - "packages/core/tests/component-schema-conformance.test.ts": 3500, - "packages/core/tests/config-api.test.ts": 3526, - "packages/core/tests/construct-error-observation.test.ts": 3615, - "packages/core/tests/contextual-cwd.test.ts": 4650, - "packages/core/tests/daemon-integration.test.ts": 4166, - "packages/core/tests/daemon.test.ts": 3179, - "packages/core/tests/declared-markdown-component.test.ts": 4814, - "packages/core/tests/document-output-api.test.ts": 1362, - "packages/core/tests/document-target-execution.test.ts": 6032, - "packages/core/tests/document-targets.test.ts": 3566, - "packages/core/tests/document-validation.test.ts": 4876, - "packages/core/tests/documentation-index.test.ts": 3296, - "packages/core/tests/each.test.ts": 3510, - "packages/core/tests/elicit-component.test.ts": 4087, - "packages/core/tests/ephemeral-service.test.ts": 3859, - "packages/core/tests/eval-bindings.test.ts": 3481, - "packages/core/tests/eval-context.test.ts": 1930, - "packages/core/tests/eval-durable.test.ts": 3556, - "packages/core/tests/eval-error-mode.test.ts": 3742, - "packages/core/tests/eval-interpolate.test.ts": 1360, - "packages/core/tests/eval-middleware.test.ts": 3130, - "packages/core/tests/eval-persist.test.ts": 3608, - "packages/core/tests/eval-return.test.ts": 3484, - "packages/core/tests/eval-scope.test.ts": 3447, - "packages/core/tests/eval-timeout.test.ts": 3504, - "packages/core/tests/eval-transform.test.ts": 3218, - "packages/core/tests/evaluate-component.test.ts": 5249, - "packages/core/tests/evaluate-loaded-copy.test.ts": 3566, - "packages/core/tests/evaluate-provider-lifetime.test.ts": 3581, - "packages/core/tests/evaluation-profile.test.ts": 3307, - "packages/core/tests/exec-timeout.test.ts": 3535, - "packages/core/tests/execute.test.ts": 4007, - "packages/core/tests/execution-protocol.test.ts": 4649, - "packages/core/tests/expand.test.ts": 4108, - "packages/core/tests/expansion-identity.test.ts": 4041, - "packages/core/tests/expansion-metadata.test.ts": 3413, - "packages/core/tests/expression-props.test.ts": 3980, - "packages/core/tests/fail-component.test.ts": 3720, - "packages/core/tests/failure-printing.test.ts": 3659, - "packages/core/tests/fatal-cause.test.ts": 3284, - "packages/core/tests/fetch-component.test.ts": 4673, - "packages/core/tests/file-component.test.ts": 5289, - "packages/core/tests/file-delete-component.test.ts": 3630, - "packages/core/tests/files-fatal.test.ts": 4348, - "packages/core/tests/foreground-exec.test.ts": 4296, - "packages/core/tests/frontmatter.test.ts": 3181, - "packages/core/tests/function-components.test.ts": 3910, - "packages/core/tests/generated-composition.test.ts": 3965, - "packages/core/tests/generated-xmd.test.ts": 5249, - "packages/core/tests/glob-api.test.ts": 1703, - "packages/core/tests/glob-component.test.ts": 4091, - "packages/core/tests/guarded-journal.test.ts": 3461, - "packages/core/tests/has-content.test.ts": 3307, - "packages/core/tests/heal.test.ts": 3335, - "packages/core/tests/if.test.ts": 3870, - "packages/core/tests/inline-root.test.ts": 3506, - "packages/core/tests/invocation-failures.test.ts": 3231, - "packages/core/tests/invocation-identity.test.ts": 3841, - "packages/core/tests/invocation-scope.test.ts": 3560, - "packages/core/tests/journal-source-position.test.ts": 3626, - "packages/core/tests/json-component.test.ts": 3650, - "packages/core/tests/json.test.ts": 3147, - "packages/core/tests/let.test.ts": 3507, - "packages/core/tests/loaded-copy-files.test.ts": 3398, - "packages/core/tests/loop.test.ts": 4242, - "packages/core/tests/named-slots.test.ts": 4184, - "packages/core/tests/output-error-mode.test.ts": 4420, - "packages/core/tests/output-normalize.test.ts": 1366, - "packages/core/tests/output-terminal.test.ts": 1461, - "packages/core/tests/parse-components.test.ts": 3611, - "packages/core/tests/plan-response.test.ts": 1407, - "packages/core/tests/plugin-api.test.ts": 3190, - "packages/core/tests/plugin.test.ts": 3272, - "packages/core/tests/props-binding.test.ts": 3585, - "packages/core/tests/protected-content.test.ts": 3135, - "packages/core/tests/registered-printed-errors.test.ts": 3300, - "packages/core/tests/replay-stream.test.ts": 1345, - "packages/core/tests/retain.test.ts": 3494, - "packages/core/tests/root-composition.test.ts": 3529, - "packages/core/tests/root-props.test.ts": 3588, - "packages/core/tests/root-provider.test.ts": 3522, - "packages/core/tests/sample-component.test.ts": 4207, - "packages/core/tests/scanner.test.ts": 3193, - "packages/core/tests/scope-local.test.ts": 1345, - "packages/core/tests/secret-detection.test.ts": 4135, - "packages/core/tests/secret-files.test.ts": 1807, - "packages/core/tests/secret-rules.test.ts": 1826, - "packages/core/tests/secret-scanner-baseline.test.ts": 1583, - "packages/core/tests/secret-scanner.test.ts": 1698, - "packages/core/tests/source-position.test.ts": 3353, - "packages/core/tests/state-ownership.test.ts": 3390, - "packages/core/tests/streaming-emission.test.ts": 3389, - "packages/core/tests/switch.test.ts": 3887, - "packages/core/tests/syntax-catalog.test.ts": 3913, - "packages/core/tests/syntax-component.test.ts": 4609, - "packages/core/tests/syntax-loaded-copy.test.ts": 3505, - "packages/core/tests/temp-dir.test.ts": 5795, - "packages/core/tests/temp-file-compiler.test.ts": 1861, - "packages/core/tests/text-interpolation.test.ts": 3310, - "packages/core/tests/try-content.test.ts": 3296, - "packages/core/tests/unused-in-diff.test.ts": 3528, - "packages/core/tests/validate.test.ts": 3352, - "packages/core/tests/validation-integration.test.ts": 3414, - "packages/core/tests/workflow-component-bundle.test.ts": 3634, - "packages/durable-streams/tests/context.test.ts": 1576, - "packages/durable-streams/tests/deterministic-id.test.ts": 1600, - "packages/durable-streams/tests/divergence-api.test.ts": 1606, - "packages/durable-streams/tests/divergence.test.ts": 1638, - "packages/durable-streams/tests/durable-each.test.ts": 1650, - "packages/durable-streams/tests/durable-position.test.ts": 1600, - "packages/durable-streams/tests/durable-run.test.ts": 1625, - "packages/durable-streams/tests/ephemeral.test.ts": 1620, - "packages/durable-streams/tests/fail-stop.test.ts": 1644, - "packages/durable-streams/tests/guard-stream.test.ts": 1678, - "packages/durable-streams/tests/live-coordinator.test.ts": 1627, - "packages/durable-streams/tests/parse.test.ts": 1611, - "packages/durable-streams/tests/replay-guard.test.ts": 1678, - "packages/durable-streams/tests/replay-index.test.ts": 1424, - "packages/durable-streams/tests/retained.test.ts": 1456, - "packages/durable-streams/tests/serialize-event.test.ts": 1595, - "packages/durable-streams/tests/smoke.test.ts": 1580, - "packages/durable-streams/tests/structured-concurrency.test.ts": 1706, - "packages/durable-streams/tests/terminal-boundary.test.ts": 1663, - "packages/durable-streams/tests/types.test.ts": 1304, - "packages/git/tests/ambient-authentication.test.ts": 22496, - "packages/git/tests/api-entrypoint.test.ts": 4301, - "packages/git/tests/credential-helper.test.ts": 5315, - "packages/git/tests/git-add-crash.test.ts": 6108, - "packages/git/tests/git-add-durability.test.ts": 23187, - "packages/git/tests/git-add.test.ts": 30526, - "packages/git/tests/git-commit-crash.test.ts": 6315, - "packages/git/tests/git-commit-durability.test.ts": 29041, - "packages/git/tests/git-commit.test.ts": 34181, - "packages/git/tests/git-host-effect.test.ts": 8105, - "packages/git/tests/git-push-crash.test.ts": 8524, - "packages/git/tests/git-push-durability.test.ts": 35860, - "packages/git/tests/git-push.test.ts": 41808, - "packages/git/tests/git-switch-crash.test.ts": 6124, - "packages/git/tests/git-switch-durability.test.ts": 18852, - "packages/git/tests/git-switch.test.ts": 19532, - "packages/git/tests/git.test.ts": 3879, - "packages/git/tests/github-activation.test.ts": 4893, - "packages/git/tests/github-issues.test.ts": 3848, - "packages/git/tests/github-pull-requests.test.ts": 3543, - "packages/git/tests/github-workflow-activation.test.ts": 5247, - "packages/git/tests/issue-markdown.test.ts": 10253, - "packages/git/tests/issue-records.test.ts": 3746, - "packages/git/tests/materialization.test.ts": 4410, - "packages/git/tests/module-partition.test.ts": 4585, - "packages/git/tests/plugin.test.ts": 4163, - "packages/git/tests/provider-neutrality.test.ts": 2991, - "packages/git/tests/public-entrypoint.test.ts": 7837, - "packages/git/tests/pull-request-crash.test.ts": 7172, - "packages/git/tests/pull-request-durability.test.ts": 34453, - "packages/git/tests/pull-request-read.test.ts": 7415, - "packages/git/tests/pull-request-records.test.ts": 3776, - "packages/git/tests/pull-request.test.ts": 61232, - "packages/git/tests/repository-components.test.ts": 11337, - "packages/git/tests/repository-control-plane.test.ts": 10024, - "packages/git/tests/repository-materialization.test.ts": 7531, - "packages/git/tests/repository-replay.test.ts": 14210, - "packages/git/tests/repository-storage.test.ts": 17123, - "packages/git/tests/run-composition-ambient.test.ts": 11137, - "packages/git/tests/run-composition-lazy.test.ts": 5053, - "packages/git/tests/run-composition-managed.test.ts": 12698, - "packages/git/tests/run-composition-remote.test.ts": 12758, - "packages/git/tests/selection-authentication.test.ts": 1635, - "packages/git/tests/worktree-replay.test.ts": 6088, - "packages/runtime/tests/agent-session-coordinator.test.ts": 1930, - "packages/runtime/tests/duration.test.ts": 1367, - "packages/runtime/tests/executable-observer.test.ts": 1655, - "packages/runtime/tests/fetch.test.ts": 2173, - "packages/runtime/tests/fs.test.ts": 1553, - "packages/runtime/tests/host-files.test.ts": 2332, - "packages/runtime/tests/native-launcher.test.ts": 3774, - "packages/runtime/tests/process.test.ts": 1538, - "packages/runtime/tests/service.test.ts": 1662, - "packages/test-agent/tests/acp-server.test.ts": 1495, - "packages/test-agent/tests/behavior-engine.test.ts": 3766, - "packages/test-agent/tests/bridge.test.ts": 3279, - "packages/test-agent/tests/command-lazy.test.ts": 7029, - "packages/test-agent/tests/components.test.ts": 52910, - "packages/test-agent/tests/controller.test.ts": 2587, - "packages/test-agent/tests/cross-package-resolution.test.ts": 5474, - "packages/test-agent/tests/native-launch.test.ts": 25742, - "packages/test-agent/tests/net.test.ts": 1467, - "packages/test-agent/tests/profile.test.ts": 3508, - "packages/test-agent/tests/protocol.test.ts": 2226, - "packages/test-agent/tests/provider.test.ts": 3817, - "packages/test-agent/tests/public-api.test.ts": 4500, - "packages/test-agent/tests/route-slot.test.ts": 1372, - "packages/test-agent/tests/smoke.test.ts": 18550, - "packages/test-agent/tests/template.test.ts": 3469, - "packages/test-agent/tests/worker-lifecycle.test.ts": 11058, - "packages/test-support/tests/journal.test.ts": 1615, - "packages/testing/tests/agent-composition.test.ts": 3843, - "packages/testing/tests/assert-throws.test.ts": 5007, - "packages/testing/tests/assert.test.ts": 1407, - "packages/testing/tests/assertions.test.ts": 5541, - "packages/testing/tests/boundary-contract.test.ts": 4575, - "packages/testing/tests/cli.test.ts": 5696, - "packages/testing/tests/execution-harness.test.ts": 8139, - "packages/testing/tests/replay.test.ts": 4940, - "packages/testing/tests/smoke.test.ts": 5117, - "packages/testing/tests/test-component.test.ts": 5579, - "packages/testing/tests/testing-activation.test.ts": 5204, - "packages/testing/tests/testing-boundary.test.ts": 4126, - "packages/testing/tests/testing-mode.test.ts": 5629, - "packages/testing/tests/use-testing.test.ts": 4696, - "packages/web/tests/assets.test.ts": 1438, - "packages/web/tests/client-logic.test.ts": 1487, - "packages/web/tests/compile.test.ts": 4113, - "packages/web/tests/component.test.ts": 4592, - "packages/web/tests/declaration.test.ts": 3930, - "packages/web/tests/document.test.ts": 4128, - "packages/web/tests/elicitation.test.ts": 3903, - "packages/web/tests/live-form.test.ts": 3831, - "packages/web/tests/markdown.test.ts": 1561, - "packages/web/tests/opener.test.ts": 3788, - "packages/web/tests/page.test.ts": 1420, - "packages/web/tests/responder.test.ts": 4048, - "packages/web/tests/server-lifecycle.test.ts": 3937, - "packages/web/tests/server.test.ts": 4322, - "packages/workflow/tests/generated-agent-component.test.ts": 6153, - "packages/workflow/tests/generated-observations.test.ts": 3674, - "packages/workflow/tests/public-entrypoint.test.ts": 5334, - "packages/workflow/tests/retained-run.test.ts": 4451, - "packages/workflow/tests/service-denial.test.ts": 1668, - "packages/workflow/tests/workflow-agent-checkpoints.test.ts": 5434, - "packages/workflow/tests/workflow-agent-sessions.test.ts": 4432, - "packages/workflow/tests/workflow-bundle.test.ts": 3963, - "packages/workflow/tests/workflow-checkpoint.test.ts": 5425, - "packages/workflow/tests/workflow-definition.test.ts": 3662, - "packages/workflow/tests/workflow-export.test.ts": 6869, - "packages/workflow/tests/workflow-fork-source.test.ts": 4445, - "packages/workflow/tests/workflow-fork.test.ts": 3552, - "packages/workflow/tests/workflow-lifecycle-authority.test.ts": 5209, - "packages/workflow/tests/workflow-lifecycle-control.test.ts": 5737, - "packages/workflow/tests/workflow-lifecycle-inspection.test.ts": 10270, - "packages/workflow/tests/workflow-run-journal.test.ts": 6295, - "packages/workflow/tests/workflow-run-storage.test.ts": 7706, - "packages/workflow/tests/workflow-run.test.ts": 4308, - "packages/workflow/tests/workflow-suspension-answer.test.ts": 5313, - "packages/workflow/tests/workflow-suspension.test.ts": 4992, - "packages/workflow/tests/workspace-crash-recovery.test.ts": 6630, - "packages/workflow/tests/workspace-effect-loaded-copy.test.ts": 4477, - "packages/workflow/tests/workspace-effect-transaction.test.ts": 5230, - "packages/workflow/tests/workspace-effect.test.ts": 5127, - "packages/workflow/tests/workspace-files.test.ts": 11932, - "packages/workflow/tests/workspace-root-restoration.test.ts": 6018, - "packages/workflow/tests/workspace-root.test.ts": 4403, - "packages/workflow/tests/workspace-transaction.test.ts": 4469, - "packages/workflow/tests/xmd-artifact.test.ts": 6309, - "scripts/tests/acpx-vendor.test.ts": 1854, - "scripts/tests/adapter-distribution.test.ts": 3955, - "scripts/tests/adapter-npm-package.test.ts": 28449, - "scripts/tests/adapter-vendor.test.ts": 3547, - "scripts/tests/bootstrap-npm-package.test.ts": 9311, - "scripts/tests/build-npm.test.ts": 34883, - "scripts/tests/build-web-client.test.ts": 25828, - "scripts/tests/bump-version.test.ts": 1462, - "scripts/tests/changed-paths.test.ts": 1348, - "scripts/tests/ci-workflow.test.ts": 4145, - "scripts/tests/cli-npm-bin.test.ts": 202292, - "scripts/tests/cloudflare-dofs-vendor.test.ts": 3760, - "scripts/tests/consumer-cycle.test.ts": 1461, - "scripts/tests/documentation-validation.test.ts": 5282, - "scripts/tests/filesystem-contract-workflow.test.ts": 1398, - "scripts/tests/frozen-entry.test.ts": 7108, - "scripts/tests/jsr-consumer-documentation.test.ts": 16048, - "scripts/tests/main-green.test.ts": 1711, - "scripts/tests/main-health.test.ts": 1558, - "scripts/tests/measure-test-weights.test.ts": 1729, - "scripts/tests/no-module-scoped-registry.test.ts": 2111, - "scripts/tests/no-redundant-test-scope.test.ts": 5639, - "scripts/tests/no-sync-filesystem.test.ts": 5497, - "scripts/tests/no-yield-in-finally.test.ts": 2072, - "scripts/tests/oxlint-policy.test.ts": 10227, - "scripts/tests/packaged-document.test.ts": 1419, - "scripts/tests/prefer-effection-operation.test.ts": 6711, - "scripts/tests/prefer-effection-result.test.ts": 4848, - "scripts/tests/prepared-state.test.ts": 2214, - "scripts/tests/publish-workflow-generator.test.ts": 5123, - "scripts/tests/publish-workflow-membership.test.ts": 1527, - "scripts/tests/publishable-membership-agreement.test.ts": 3436, - "scripts/tests/readme-targets.test.ts": 15416, - "scripts/tests/release-targets.test.ts": 1465, - "scripts/tests/review-infrastructure.test.ts": 5411, - "scripts/tests/runtime-exclusions.test.ts": 1827, - "scripts/tests/runtime-tests.test.ts": 1526, - "scripts/tests/scope-bound-event-registration.test.ts": 5395, - "scripts/tests/shard-execution.test.ts": 10601, - "scripts/tests/staged-write.test.ts": 1446, - "scripts/tests/test-file-discovery.test.ts": 1579, - "scripts/tests/test-shards.test.ts": 1406, - "scripts/tests/test-weights.test.ts": 1665, - "scripts/tests/tracked.test.ts": 1390, - "scripts/tests/verify-adapter.test.ts": 6517, - "scripts/tests/verify-clean.test.ts": 1574, - "scripts/tests/verify-coordinator.test.ts": 1748, - "scripts/tests/version-lockstep.test.ts": 1532, - "scripts/tests/web-client-module.test.ts": 1425, - "scripts/tests/workspace.test.ts": 1352 + "packages/acp/tests/acceptance.test.ts": 4890, + "packages/acp/tests/acpx-checkpoint-meta.test.ts": 7990, + "packages/acp/tests/acpx-materialization.test.ts": 8093, + "packages/acp/tests/acpx-transient-env.test.ts": 1812, + "packages/acp/tests/adapter-materialization.test.ts": 77274, + "packages/acp/tests/adapter-protocol.test.ts": 59906, + "packages/acp/tests/codex-surfaces.test.ts": 1784, + "packages/acp/tests/loaded-copy.test.ts": 4628, + "packages/acp/tests/native-launch.test.ts": 5345, + "packages/acp/tests/permission-bridge.test.ts": 4076, + "packages/acp/tests/provider.test.ts": 5260, + "packages/acp/tests/serial-queue.test.ts": 1533, + "packages/acp/tests/session-key.test.ts": 3980, + "packages/acp/tests/session-route.test.ts": 4132, + "packages/acp/tests/terminal-screen.test.ts": 1731, + "packages/cli/tests/agent-adapters.test.ts": 5722, + "packages/cli/tests/agent-cli.test.ts": 36964, + "packages/cli/tests/agent-config.test.ts": 3639, + "packages/cli/tests/agent-options-cli.test.ts": 15109, + "packages/cli/tests/agent-options.test.ts": 3659, + "packages/cli/tests/agent-session-coordinator.test.ts": 4348, + "packages/cli/tests/cli-help.test.ts": 20236, + "packages/cli/tests/command.test.ts": 10301, + "packages/cli/tests/compiled-upgrade.test.ts": 10637, + "packages/cli/tests/document-suites/inline/inline-markdown.test.ts": 9094, + "packages/cli/tests/document-suites/plan/plan-markdown.test.ts": 120013, + "packages/cli/tests/document-suites/props/props-markdown.test.ts": 8572, + "packages/cli/tests/document-suites/syntax/syntax-markdown.test.ts": 12296, + "packages/cli/tests/document-suites/targets/targets-markdown.test.ts": 10476, + "packages/cli/tests/document-suites/verbose/verbose-markdown.test.ts": 15699, + "packages/cli/tests/evaluate-component.test.ts": 11856, + "packages/cli/tests/evaluate-workflow.test.ts": 5562, + "packages/cli/tests/fetch-cli.test.ts": 3864, + "packages/cli/tests/file-stream.test.ts": 1811, + "packages/cli/tests/github-zero-effect.test.ts": 7260, + "packages/cli/tests/inline-cli.test.ts": 33591, + "packages/cli/tests/launch-timeout.test.ts": 11660, + "packages/cli/tests/packaged-document.test.ts": 4899, + "packages/cli/tests/plan-args.test.ts": 1597, + "packages/cli/tests/plan-cli.test.ts": 97616, + "packages/cli/tests/plan-command-document.test.ts": 9852, + "packages/cli/tests/plan-component.test.ts": 12024, + "packages/cli/tests/plan-host-acts.test.ts": 5483, + "packages/cli/tests/plan.test.ts": 16125, + "packages/cli/tests/plugin-cli.test.ts": 40561, + "packages/cli/tests/plugin-host.test.ts": 6162, + "packages/cli/tests/plugin-modules.test.ts": 1540, + "packages/cli/tests/plugin-selection.test.ts": 1528, + "packages/cli/tests/process-retention.test.ts": 5307, + "packages/cli/tests/props-cli.test.ts": 30410, + "packages/cli/tests/props-schema.test.ts": 4087, + "packages/cli/tests/props-sources.test.ts": 2293, + "packages/cli/tests/pull-request-read-fork.test.ts": 7148, + "packages/cli/tests/repl-boundaries.test.ts": 7654, + "packages/cli/tests/repl-composition.test.ts": 2084, + "packages/cli/tests/repl-execution.test.ts": 5446, + "packages/cli/tests/repl-journey.test.ts": 20562, + "packages/cli/tests/repl-model.test.ts": 5271, + "packages/cli/tests/repl-route.test.ts": 4830, + "packages/cli/tests/repl-terminal.test.ts": 2940, + "packages/cli/tests/run-composition-deno.test.ts": 7865, + "packages/cli/tests/run-composition-nested.test.ts": 20765, + "packages/cli/tests/run-composition.test.ts": 7086, + "packages/cli/tests/run-deadline.test.ts": 8852, + "packages/cli/tests/run-profile.test.ts": 4456, + "packages/cli/tests/run-timeouts.test.ts": 13293, + "packages/cli/tests/secret-detection-cli.test.ts": 19637, + "packages/cli/tests/service-document.test.ts": 5349, + "packages/cli/tests/service-host.test.ts": 5227, + "packages/cli/tests/session-launch-cli.test.ts": 10605, + "packages/cli/tests/stdin-cli.test.ts": 37206, + "packages/cli/tests/stdout-delivery.test.ts": 1542, + "packages/cli/tests/syntax-cli.test.ts": 36546, + "packages/cli/tests/targets-cli.test.ts": 28883, + "packages/cli/tests/test-root-profile.test.ts": 7230, + "packages/cli/tests/test-target.test.ts": 37366, + "packages/cli/tests/testing-activation.test.ts": 4596, + "packages/cli/tests/testing-execution-host.test.ts": 56559, + "packages/cli/tests/upgrade-cli.test.ts": 21067, + "packages/cli/tests/upgrade-command-document.test.ts": 7332, + "packages/cli/tests/upgrade-output.test.ts": 4599, + "packages/cli/tests/value-root.test.ts": 17482, + "packages/cli/tests/verbose-component.test.ts": 5046, + "packages/cli/tests/workflow-agent-sessions.test.ts": 19205, + "packages/cli/tests/workflow-agent.test.ts": 15879, + "packages/cli/tests/workflow-cli.test.ts": 108223, + "packages/cli/tests/workflow-crash.test.ts": 81045, + "packages/cli/tests/workflow-declaration.test.ts": 4494, + "packages/cli/tests/workflow-export-publication.test.ts": 17358, + "packages/cli/tests/workflow-fetch.test.ts": 8151, + "packages/cli/tests/workflow-fork.test.ts": 85702, + "packages/cli/tests/workflow-host.test.ts": 14581, + "packages/cli/tests/workflow-inspection.test.ts": 116193, + "packages/cli/tests/workflow-installation.test.ts": 6320, + "packages/cli/tests/workflow-lifecycle-control.test.ts": 20687, + "packages/cli/tests/workflow-retention.test.ts": 22913, + "packages/cli/tests/workflow-suspension.test.ts": 7981, + "packages/code-review-agent/tests/doctor.test.ts": 1540, + "packages/code-review-agent/tests/parse-diagnostics.test.ts": 1597, + "packages/code-review-agent/tests/parse-diff.test.ts": 1571, + "packages/code-review-agent/tests/parse-doctor.test.ts": 4103, + "packages/code-review-agent/tests/parse-oxlint.test.ts": 1566, + "packages/code-review-agent/tests/policy.test.ts": 1556, + "packages/core/tests/agent-api.test.ts": 1576, + "packages/core/tests/agent-components.test.ts": 4731, + "packages/core/tests/agent-function-components.test.ts": 6430, + "packages/core/tests/agent-session-launch.test.ts": 6743, + "packages/core/tests/agent-session-placement.test.ts": 1607, + "packages/core/tests/agent-session-use-loaded-copy.test.ts": 1718, + "packages/core/tests/agent-session-use.test.ts": 1588, + "packages/core/tests/agent-stack.test.ts": 4270, + "packages/core/tests/answer-identity.test.ts": 3719, + "packages/core/tests/answers-component.test.ts": 4881, + "packages/core/tests/answers-expansion-recursion.test.ts": 3883, + "packages/core/tests/capture-errors.test.ts": 4117, + "packages/core/tests/capture-props.test.ts": 3922, + "packages/core/tests/cli-journal.test.ts": 12311, + "packages/core/tests/code-block-component.test.ts": 5440, + "packages/core/tests/compiler-boundary.test.ts": 3925, + "packages/core/tests/component-api.test.ts": 3668, + "packages/core/tests/component-registration.test.ts": 4720, + "packages/core/tests/component-returns.test.ts": 5150, + "packages/core/tests/component-schema-conformance.test.ts": 3996, + "packages/core/tests/config-api.test.ts": 4074, + "packages/core/tests/construct-error-observation.test.ts": 4166, + "packages/core/tests/contextual-cwd.test.ts": 5197, + "packages/core/tests/daemon-integration.test.ts": 4655, + "packages/core/tests/daemon.test.ts": 3729, + "packages/core/tests/declared-markdown-component.test.ts": 5441, + "packages/core/tests/document-output-api.test.ts": 1545, + "packages/core/tests/document-target-execution.test.ts": 6773, + "packages/core/tests/document-targets.test.ts": 4046, + "packages/core/tests/document-validation.test.ts": 5420, + "packages/core/tests/documentation-index.test.ts": 3727, + "packages/core/tests/each.test.ts": 4043, + "packages/core/tests/elicit-component.test.ts": 4583, + "packages/core/tests/ephemeral-service.test.ts": 4478, + "packages/core/tests/eval-bindings.test.ts": 3943, + "packages/core/tests/eval-context.test.ts": 2191, + "packages/core/tests/eval-durable.test.ts": 3995, + "packages/core/tests/eval-error-mode.test.ts": 4245, + "packages/core/tests/eval-interpolate.test.ts": 1534, + "packages/core/tests/eval-middleware.test.ts": 3662, + "packages/core/tests/eval-persist.test.ts": 4113, + "packages/core/tests/eval-return.test.ts": 4024, + "packages/core/tests/eval-scope.test.ts": 3917, + "packages/core/tests/eval-timeout.test.ts": 4025, + "packages/core/tests/eval-transform.test.ts": 3582, + "packages/core/tests/evaluate-component.test.ts": 5882, + "packages/core/tests/evaluate-loaded-copy.test.ts": 4011, + "packages/core/tests/evaluate-provider-lifetime.test.ts": 4134, + "packages/core/tests/evaluation-profile.test.ts": 3805, + "packages/core/tests/exec-timeout.test.ts": 4106, + "packages/core/tests/execute.test.ts": 4636, + "packages/core/tests/execution-protocol.test.ts": 5358, + "packages/core/tests/expand.test.ts": 4719, + "packages/core/tests/expansion-identity.test.ts": 4585, + "packages/core/tests/expansion-metadata.test.ts": 3837, + "packages/core/tests/expression-props.test.ts": 4475, + "packages/core/tests/fail-component.test.ts": 4210, + "packages/core/tests/failure-printing.test.ts": 4239, + "packages/core/tests/fatal-cause.test.ts": 3750, + "packages/core/tests/fetch-component.test.ts": 5335, + "packages/core/tests/file-component.test.ts": 5869, + "packages/core/tests/file-delete-component.test.ts": 4128, + "packages/core/tests/files-fatal.test.ts": 4916, + "packages/core/tests/foreground-exec.test.ts": 5059, + "packages/core/tests/frontmatter.test.ts": 3629, + "packages/core/tests/function-components.test.ts": 4481, + "packages/core/tests/generated-composition.test.ts": 4481, + "packages/core/tests/generated-xmd.test.ts": 5791, + "packages/core/tests/glob-api.test.ts": 1898, + "packages/core/tests/glob-component.test.ts": 4625, + "packages/core/tests/guarded-journal.test.ts": 3945, + "packages/core/tests/has-content.test.ts": 3812, + "packages/core/tests/heal.test.ts": 3799, + "packages/core/tests/if.test.ts": 4449, + "packages/core/tests/inline-root.test.ts": 3986, + "packages/core/tests/invocation-failures.test.ts": 3705, + "packages/core/tests/invocation-identity.test.ts": 4403, + "packages/core/tests/invocation-scope.test.ts": 4078, + "packages/core/tests/journal-source-position.test.ts": 4195, + "packages/core/tests/json-component.test.ts": 4217, + "packages/core/tests/json.test.ts": 3606, + "packages/core/tests/let.test.ts": 4016, + "packages/core/tests/loaded-copy-files.test.ts": 3900, + "packages/core/tests/loop.test.ts": 4825, + "packages/core/tests/named-slots.test.ts": 4729, + "packages/core/tests/output-error-mode.test.ts": 5015, + "packages/core/tests/output-normalize.test.ts": 1545, + "packages/core/tests/output-terminal.test.ts": 1661, + "packages/core/tests/parse-components.test.ts": 4054, + "packages/core/tests/plan-response.test.ts": 1590, + "packages/core/tests/plugin-api.test.ts": 3570, + "packages/core/tests/plugin.test.ts": 3779, + "packages/core/tests/props-binding.test.ts": 4161, + "packages/core/tests/protected-content.test.ts": 3610, + "packages/core/tests/registered-printed-errors.test.ts": 3784, + "packages/core/tests/replay-stream.test.ts": 1575, + "packages/core/tests/retain.test.ts": 3987, + "packages/core/tests/root-composition.test.ts": 4067, + "packages/core/tests/root-props.test.ts": 4128, + "packages/core/tests/root-provider.test.ts": 4123, + "packages/core/tests/sample-component.test.ts": 4798, + "packages/core/tests/scanner.test.ts": 3617, + "packages/core/tests/scope-local.test.ts": 1526, + "packages/core/tests/secret-detection.test.ts": 4746, + "packages/core/tests/secret-files.test.ts": 2071, + "packages/core/tests/secret-rules.test.ts": 2055, + "packages/core/tests/secret-scanner-baseline.test.ts": 1753, + "packages/core/tests/secret-scanner.test.ts": 1903, + "packages/core/tests/source-position.test.ts": 3848, + "packages/core/tests/state-ownership.test.ts": 3814, + "packages/core/tests/streaming-emission.test.ts": 3810, + "packages/core/tests/switch.test.ts": 4503, + "packages/core/tests/syntax-catalog.test.ts": 4532, + "packages/core/tests/syntax-component.test.ts": 5259, + "packages/core/tests/syntax-loaded-copy.test.ts": 4023, + "packages/core/tests/temp-dir.test.ts": 6377, + "packages/core/tests/temp-file-compiler.test.ts": 2196, + "packages/core/tests/text-interpolation.test.ts": 3757, + "packages/core/tests/try-content.test.ts": 3793, + "packages/core/tests/unused-in-diff.test.ts": 4133, + "packages/core/tests/validate.test.ts": 3870, + "packages/core/tests/validation-integration.test.ts": 3900, + "packages/core/tests/workflow-component-bundle.test.ts": 4243, + "packages/durable-streams/tests/context.test.ts": 1809, + "packages/durable-streams/tests/deterministic-id.test.ts": 1863, + "packages/durable-streams/tests/divergence-api.test.ts": 1830, + "packages/durable-streams/tests/divergence.test.ts": 1908, + "packages/durable-streams/tests/durable-each.test.ts": 1894, + "packages/durable-streams/tests/durable-position.test.ts": 1812, + "packages/durable-streams/tests/durable-run.test.ts": 1886, + "packages/durable-streams/tests/ephemeral.test.ts": 1825, + "packages/durable-streams/tests/fail-stop.test.ts": 1874, + "packages/durable-streams/tests/guard-stream.test.ts": 1952, + "packages/durable-streams/tests/live-coordinator.test.ts": 1854, + "packages/durable-streams/tests/parse.test.ts": 1812, + "packages/durable-streams/tests/replay-guard.test.ts": 1923, + "packages/durable-streams/tests/replay-index.test.ts": 1619, + "packages/durable-streams/tests/retained.test.ts": 1664, + "packages/durable-streams/tests/serialize-event.test.ts": 1803, + "packages/durable-streams/tests/smoke.test.ts": 1780, + "packages/durable-streams/tests/structured-concurrency.test.ts": 1977, + "packages/durable-streams/tests/terminal-boundary.test.ts": 1892, + "packages/durable-streams/tests/types.test.ts": 1520, + "packages/git/tests/ambient-authentication.test.ts": 27034, + "packages/git/tests/api-entrypoint.test.ts": 5078, + "packages/git/tests/credential-helper.test.ts": 6204, + "packages/git/tests/git-add-crash.test.ts": 7227, + "packages/git/tests/git-add-durability.test.ts": 26695, + "packages/git/tests/git-add.test.ts": 35508, + "packages/git/tests/git-commit-crash.test.ts": 7481, + "packages/git/tests/git-commit-durability.test.ts": 34971, + "packages/git/tests/git-commit.test.ts": 41874, + "packages/git/tests/git-host-effect.test.ts": 9161, + "packages/git/tests/git-push-crash.test.ts": 10272, + "packages/git/tests/git-push-durability.test.ts": 44877, + "packages/git/tests/git-push.test.ts": 53878, + "packages/git/tests/git-switch-crash.test.ts": 7271, + "packages/git/tests/git-switch-durability.test.ts": 22118, + "packages/git/tests/git-switch.test.ts": 23370, + "packages/git/tests/git.test.ts": 4434, + "packages/git/tests/github-activation.test.ts": 5703, + "packages/git/tests/github-issues.test.ts": 4432, + "packages/git/tests/github-pull-requests.test.ts": 4086, + "packages/git/tests/github-workflow-activation.test.ts": 6012, + "packages/git/tests/issue-markdown.test.ts": 11443, + "packages/git/tests/issue-records.test.ts": 4436, + "packages/git/tests/materialization.test.ts": 5130, + "packages/git/tests/module-partition.test.ts": 5472, + "packages/git/tests/plugin.test.ts": 4812, + "packages/git/tests/provider-neutrality.test.ts": 3598, + "packages/git/tests/public-entrypoint.test.ts": 9766, + "packages/git/tests/pull-request-crash.test.ts": 8575, + "packages/git/tests/pull-request-durability.test.ts": 42850, + "packages/git/tests/pull-request-read.test.ts": 8277, + "packages/git/tests/pull-request-records.test.ts": 4350, + "packages/git/tests/pull-request.test.ts": 73466, + "packages/git/tests/repository-components.test.ts": 12917, + "packages/git/tests/repository-control-plane.test.ts": 11733, + "packages/git/tests/repository-materialization.test.ts": 8745, + "packages/git/tests/repository-replay.test.ts": 16106, + "packages/git/tests/repository-storage.test.ts": 20115, + "packages/git/tests/run-composition-ambient.test.ts": 13438, + "packages/git/tests/run-composition-lazy.test.ts": 5937, + "packages/git/tests/run-composition-managed.test.ts": 15140, + "packages/git/tests/run-composition-remote.test.ts": 15293, + "packages/git/tests/selection-authentication.test.ts": 1841, + "packages/git/tests/worktree-replay.test.ts": 6981, + "packages/runtime/tests/agent-session-coordinator.test.ts": 2234, + "packages/runtime/tests/duration.test.ts": 1545, + "packages/runtime/tests/executable-observer.test.ts": 1928, + "packages/runtime/tests/fetch.test.ts": 2383, + "packages/runtime/tests/fs.test.ts": 1733, + "packages/runtime/tests/host-files.test.ts": 2589, + "packages/runtime/tests/native-launcher.test.ts": 3955, + "packages/runtime/tests/process.test.ts": 1740, + "packages/runtime/tests/service.test.ts": 1849, + "packages/test-agent/tests/acp-server.test.ts": 1719, + "packages/test-agent/tests/behavior-engine.test.ts": 4298, + "packages/test-agent/tests/bridge.test.ts": 3716, + "packages/test-agent/tests/command-lazy.test.ts": 8032, + "packages/test-agent/tests/components.test.ts": 61791, + "packages/test-agent/tests/controller.test.ts": 2881, + "packages/test-agent/tests/cross-package-resolution.test.ts": 6157, + "packages/test-agent/tests/native-launch.test.ts": 30913, + "packages/test-agent/tests/net.test.ts": 1613, + "packages/test-agent/tests/profile.test.ts": 4043, + "packages/test-agent/tests/protocol.test.ts": 2443, + "packages/test-agent/tests/provider.test.ts": 4376, + "packages/test-agent/tests/public-api.test.ts": 5102, + "packages/test-agent/tests/route-slot.test.ts": 1527, + "packages/test-agent/tests/smoke.test.ts": 21250, + "packages/test-agent/tests/template.test.ts": 4007, + "packages/test-agent/tests/worker-lifecycle.test.ts": 13359, + "packages/test-support/tests/journal.test.ts": 1846, + "packages/testing/tests/agent-composition.test.ts": 4367, + "packages/testing/tests/assert-throws.test.ts": 5605, + "packages/testing/tests/assert.test.ts": 1565, + "packages/testing/tests/assertions.test.ts": 6194, + "packages/testing/tests/boundary-contract.test.ts": 5041, + "packages/testing/tests/cli.test.ts": 6822, + "packages/testing/tests/execution-harness.test.ts": 8902, + "packages/testing/tests/replay.test.ts": 5555, + "packages/testing/tests/smoke.test.ts": 5707, + "packages/testing/tests/test-component.test.ts": 6164, + "packages/testing/tests/testing-activation.test.ts": 5833, + "packages/testing/tests/testing-boundary.test.ts": 4698, + "packages/testing/tests/testing-mode.test.ts": 6306, + "packages/testing/tests/use-testing.test.ts": 5192, + "packages/web/tests/assets.test.ts": 1596, + "packages/web/tests/client-logic.test.ts": 1649, + "packages/web/tests/compile.test.ts": 4674, + "packages/web/tests/component.test.ts": 5194, + "packages/web/tests/declaration.test.ts": 4460, + "packages/web/tests/document.test.ts": 4731, + "packages/web/tests/elicitation.test.ts": 4488, + "packages/web/tests/live-form.test.ts": 4360, + "packages/web/tests/markdown.test.ts": 1790, + "packages/web/tests/opener.test.ts": 4349, + "packages/web/tests/page.test.ts": 1575, + "packages/web/tests/responder.test.ts": 4637, + "packages/web/tests/server-lifecycle.test.ts": 4495, + "packages/web/tests/server.test.ts": 4765, + "packages/workflow/tests/generated-agent-component.test.ts": 6806, + "packages/workflow/tests/generated-observations.test.ts": 4144, + "packages/workflow/tests/public-entrypoint.test.ts": 5919, + "packages/workflow/tests/retained-run.test.ts": 5048, + "packages/workflow/tests/service-denial.test.ts": 1865, + "packages/workflow/tests/workflow-agent-checkpoints.test.ts": 6099, + "packages/workflow/tests/workflow-agent-sessions.test.ts": 5001, + "packages/workflow/tests/workflow-bundle.test.ts": 4467, + "packages/workflow/tests/workflow-checkpoint.test.ts": 6174, + "packages/workflow/tests/workflow-definition.test.ts": 4212, + "packages/workflow/tests/workflow-export.test.ts": 8073, + "packages/workflow/tests/workflow-fork-source.test.ts": 5140, + "packages/workflow/tests/workflow-fork.test.ts": 4118, + "packages/workflow/tests/workflow-lifecycle-authority.test.ts": 6062, + "packages/workflow/tests/workflow-lifecycle-control.test.ts": 6723, + "packages/workflow/tests/workflow-lifecycle-inspection.test.ts": 11202, + "packages/workflow/tests/workflow-run-journal.test.ts": 7244, + "packages/workflow/tests/workflow-run-storage.test.ts": 8184, + "packages/workflow/tests/workflow-run.test.ts": 4985, + "packages/workflow/tests/workflow-suspension-answer.test.ts": 6002, + "packages/workflow/tests/workflow-suspension.test.ts": 5685, + "packages/workflow/tests/workspace-crash-recovery.test.ts": 7784, + "packages/workflow/tests/workspace-effect-loaded-copy.test.ts": 5102, + "packages/workflow/tests/workspace-effect-transaction.test.ts": 5918, + "packages/workflow/tests/workspace-effect.test.ts": 6014, + "packages/workflow/tests/workspace-files.test.ts": 12933, + "packages/workflow/tests/workspace-root-restoration.test.ts": 6760, + "packages/workflow/tests/workspace-root.test.ts": 5094, + "packages/workflow/tests/workspace-transaction.test.ts": 5097, + "packages/workflow/tests/xmd-artifact.test.ts": 6972, + "scripts/tests/acpx-vendor.test.ts": 2160, + "scripts/tests/adapter-distribution.test.ts": 4586, + "scripts/tests/adapter-npm-package.test.ts": 29243, + "scripts/tests/adapter-vendor.test.ts": 4198, + "scripts/tests/bootstrap-npm-package.test.ts": 11443, + "scripts/tests/build-npm.test.ts": 40709, + "scripts/tests/build-web-client.test.ts": 30613, + "scripts/tests/bump-version.test.ts": 1659, + "scripts/tests/changed-paths.test.ts": 1583, + "scripts/tests/ci-workflow.test.ts": 4871, + "scripts/tests/cli-npm-bin.test.ts": 252922, + "scripts/tests/cloudflare-dofs-vendor.test.ts": 4431, + "scripts/tests/consumer-cycle.test.ts": 1661, + "scripts/tests/documentation-validation.test.ts": 6267, + "scripts/tests/filesystem-contract-workflow.test.ts": 1572, + "scripts/tests/frozen-entry.test.ts": 7664, + "scripts/tests/jsr-consumer-documentation.test.ts": 18170, + "scripts/tests/main-green.test.ts": 1936, + "scripts/tests/main-health.test.ts": 1715, + "scripts/tests/measure-test-weights.test.ts": 1992, + "scripts/tests/no-module-scoped-registry.test.ts": 2370, + "scripts/tests/no-redundant-test-scope.test.ts": 6520, + "scripts/tests/no-sync-filesystem.test.ts": 6462, + "scripts/tests/no-yield-in-finally.test.ts": 2377, + "scripts/tests/oxlint-policy.test.ts": 12237, + "scripts/tests/packaged-document.test.ts": 1618, + "scripts/tests/prefer-effection-operation.test.ts": 7468, + "scripts/tests/prefer-effection-result.test.ts": 5680, + "scripts/tests/prepared-state.test.ts": 2853, + "scripts/tests/publish-workflow-generator.test.ts": 6220, + "scripts/tests/publish-workflow-membership.test.ts": 1734, + "scripts/tests/publishable-membership-agreement.test.ts": 4083, + "scripts/tests/readme-targets.test.ts": 18898, + "scripts/tests/release-targets.test.ts": 1695, + "scripts/tests/review-infrastructure.test.ts": 6279, + "scripts/tests/runtime-exclusions.test.ts": 2097, + "scripts/tests/runtime-tests.test.ts": 1686, + "scripts/tests/scope-bound-event-registration.test.ts": 6340, + "scripts/tests/shard-execution.test.ts": 11463, + "scripts/tests/staged-write.test.ts": 1604, + "scripts/tests/test-file-discovery.test.ts": 1840, + "scripts/tests/test-shards.test.ts": 1619, + "scripts/tests/test-weights.test.ts": 1864, + "scripts/tests/tracked.test.ts": 1585, + "scripts/tests/verify-adapter.test.ts": 7915, + "scripts/tests/verify-clean.test.ts": 1806, + "scripts/tests/verify-coordinator.test.ts": 1992, + "scripts/tests/version-lockstep.test.ts": 1782, + "scripts/tests/web-client-module.test.ts": 1570, + "scripts/tests/workspace.test.ts": 1581 }, "node": { - "packages/acp/tests/acceptance.test.ts": 1938, - "packages/acp/tests/acpx-checkpoint-meta.test.ts": 4778, - "packages/acp/tests/acpx-materialization.test.ts": 7002, - "packages/acp/tests/acpx-transient-env.test.ts": 729, - "packages/acp/tests/adapter-materialization.test.ts": 72138, - "packages/acp/tests/adapter-protocol.test.ts": 50348, - "packages/acp/tests/codex-surfaces.test.ts": 594, - "packages/acp/tests/native-launch.test.ts": 1960, - "packages/acp/tests/permission-bridge.test.ts": 1103, - "packages/acp/tests/provider.test.ts": 1931, - "packages/acp/tests/serial-queue.test.ts": 448, - "packages/acp/tests/session-key.test.ts": 477, - "packages/acp/tests/terminal-screen.test.ts": 550, - "packages/cli/tests/agent-adapters.test.ts": 2170, - "packages/cli/tests/agent-cli.test.ts": 35674, - "packages/cli/tests/agent-config.test.ts": 445, - "packages/cli/tests/agent-options-cli.test.ts": 13495, - "packages/cli/tests/agent-options.test.ts": 445, - "packages/cli/tests/agent-session-coordinator.test.ts": 1178, - "packages/cli/tests/cli-help.test.ts": 25826, - "packages/cli/tests/command.test.ts": 13058, - "packages/cli/tests/document-suites/inline/inline-markdown.test.ts": 3789, - "packages/cli/tests/document-suites/plan/plan-markdown.test.ts": 152031, - "packages/cli/tests/document-suites/props/props-markdown.test.ts": 3190, - "packages/cli/tests/document-suites/syntax/syntax-markdown.test.ts": 8329, - "packages/cli/tests/document-suites/targets/targets-markdown.test.ts": 5469, - "packages/cli/tests/document-suites/verbose/verbose-markdown.test.ts": 15256, - "packages/cli/tests/evaluate-component.test.ts": 13048, - "packages/cli/tests/fetch-cli.test.ts": 4161, - "packages/cli/tests/file-stream.test.ts": 489, - "packages/cli/tests/inline-cli.test.ts": 56000, - "packages/cli/tests/launch-timeout.test.ts": 20499, - "packages/cli/tests/packaged-document.test.ts": 1361, - "packages/cli/tests/plan-args.test.ts": 464, - "packages/cli/tests/plan-cli.test.ts": 98200, - "packages/cli/tests/plan-command-document.test.ts": 7235, - "packages/cli/tests/plan-component.test.ts": 9842, - "packages/cli/tests/plan-host-acts.test.ts": 2046, - "packages/cli/tests/plan.test.ts": 14362, - "packages/cli/tests/plugin-cli.test.ts": 68128, - "packages/cli/tests/plugin-host.test.ts": 2999, - "packages/cli/tests/plugin-modules.test.ts": 438, - "packages/cli/tests/plugin-selection.test.ts": 463, - "packages/cli/tests/process-retention.test.ts": 4143, - "packages/cli/tests/props-cli.test.ts": 52873, - "packages/cli/tests/props-schema.test.ts": 1178, - "packages/cli/tests/props-sources.test.ts": 629, - "packages/cli/tests/repl-composition.test.ts": 672, - "packages/cli/tests/repl-execution.test.ts": 2560, - "packages/cli/tests/repl-model.test.ts": 1692, - "packages/cli/tests/repl-route.test.ts": 1522, - "packages/cli/tests/repl-terminal.test.ts": 1196, - "packages/cli/tests/run-composition.test.ts": 3677, - "packages/cli/tests/run-deadline.test.ts": 3354, - "packages/cli/tests/run-profile.test.ts": 1115, - "packages/cli/tests/run-timeouts.test.ts": 17659, - "packages/cli/tests/secret-detection-cli.test.ts": 31779, - "packages/cli/tests/service-document.test.ts": 2391, - "packages/cli/tests/service-host.test.ts": 2125, - "packages/cli/tests/session-launch-cli.test.ts": 9257, - "packages/cli/tests/stdin-cli.test.ts": 53126, - "packages/cli/tests/stdout-delivery.test.ts": 458, - "packages/cli/tests/syntax-cli.test.ts": 52374, - "packages/cli/tests/targets-cli.test.ts": 42219, - "packages/cli/tests/test-root-profile.test.ts": 5964, - "packages/cli/tests/test-target.test.ts": 60898, - "packages/cli/tests/testing-activation.test.ts": 1273, - "packages/cli/tests/testing-execution-host.test.ts": 77038, - "packages/cli/tests/upgrade-cli.test.ts": 37529, - "packages/cli/tests/upgrade-command-document.test.ts": 4894, - "packages/cli/tests/upgrade-output.test.ts": 1615, - "packages/cli/tests/value-root.test.ts": 26199, - "packages/cli/tests/verbose-component.test.ts": 1659, - "packages/cli/tests/workflow-declaration.test.ts": 1279, - "packages/cli/tests/workflow-host.test.ts": 8807, - "packages/code-review-agent/tests/doctor.test.ts": 427, - "packages/code-review-agent/tests/parse-diagnostics.test.ts": 479, - "packages/code-review-agent/tests/parse-diff.test.ts": 455, - "packages/code-review-agent/tests/parse-doctor.test.ts": 1053, - "packages/code-review-agent/tests/parse-oxlint.test.ts": 448, - "packages/code-review-agent/tests/policy.test.ts": 448, - "packages/core/tests/agent-api.test.ts": 450, - "packages/core/tests/agent-components.test.ts": 1797, - "packages/core/tests/agent-function-components.test.ts": 3393, - "packages/core/tests/agent-session-launch.test.ts": 4061, - "packages/core/tests/agent-session-placement.test.ts": 462, - "packages/core/tests/agent-session-use.test.ts": 441, - "packages/core/tests/agent-stack.test.ts": 1342, - "packages/core/tests/answer-identity.test.ts": 471, - "packages/core/tests/answers-component.test.ts": 2116, - "packages/core/tests/answers-expansion-recursion.test.ts": 838, - "packages/core/tests/capture-errors.test.ts": 1092, - "packages/core/tests/capture-props.test.ts": 866, - "packages/core/tests/cli-journal.test.ts": 18551, - "packages/core/tests/code-block-component.test.ts": 2737, - "packages/core/tests/compiler-boundary.test.ts": 1017, - "packages/core/tests/component-api.test.ts": 490, - "packages/core/tests/component-registration.test.ts": 1860, - "packages/core/tests/component-returns.test.ts": 2465, - "packages/core/tests/component-schema-conformance.test.ts": 1012, - "packages/core/tests/config-api.test.ts": 1107, - "packages/core/tests/construct-error-observation.test.ts": 1179, - "packages/core/tests/contextual-cwd.test.ts": 2284, - "packages/core/tests/daemon-integration.test.ts": 1708, - "packages/core/tests/daemon.test.ts": 505, - "packages/core/tests/declared-markdown-component.test.ts": 2808, - "packages/core/tests/document-output-api.test.ts": 458, - "packages/core/tests/document-target-execution.test.ts": 4148, - "packages/core/tests/document-targets.test.ts": 1089, - "packages/core/tests/document-validation.test.ts": 2803, - "packages/core/tests/documentation-index.test.ts": 737, - "packages/core/tests/each.test.ts": 1142, - "packages/core/tests/elicit-component.test.ts": 1760, - "packages/core/tests/ephemeral-service.test.ts": 1587, - "packages/core/tests/eval-bindings.test.ts": 1132, - "packages/core/tests/eval-context.test.ts": 526, - "packages/core/tests/eval-durable.test.ts": 1158, - "packages/core/tests/eval-error-mode.test.ts": 1448, - "packages/core/tests/eval-interpolate.test.ts": 448, - "packages/core/tests/eval-middleware.test.ts": 539, - "packages/core/tests/eval-persist.test.ts": 1228, - "packages/core/tests/eval-return.test.ts": 1119, - "packages/core/tests/eval-scope.test.ts": 999, - "packages/core/tests/eval-timeout.test.ts": 1108, - "packages/core/tests/eval-transform.test.ts": 535, - "packages/core/tests/evaluate-component.test.ts": 3200, - "packages/core/tests/evaluate-loaded-copy.test.ts": 1184, - "packages/core/tests/evaluate-provider-lifetime.test.ts": 1256, - "packages/core/tests/evaluation-profile.test.ts": 774, - "packages/core/tests/exec-timeout.test.ts": 1151, - "packages/core/tests/execute.test.ts": 1740, - "packages/core/tests/execution-protocol.test.ts": 2560, - "packages/core/tests/expand.test.ts": 1768, - "packages/core/tests/expansion-identity.test.ts": 1780, - "packages/core/tests/expansion-metadata.test.ts": 904, - "packages/core/tests/expression-props.test.ts": 1695, - "packages/core/tests/fail-component.test.ts": 1453, - "packages/core/tests/failure-printing.test.ts": 1311, - "packages/core/tests/fatal-cause.test.ts": 782, - "packages/core/tests/fetch-component.test.ts": 2676, - "packages/core/tests/file-component.test.ts": 3120, - "packages/core/tests/file-delete-component.test.ts": 1306, - "packages/core/tests/files-fatal.test.ts": 2177, - "packages/core/tests/foreground-exec.test.ts": 2091, - "packages/core/tests/frontmatter.test.ts": 498, - "packages/core/tests/function-components.test.ts": 1795, - "packages/core/tests/generated-composition.test.ts": 1816, - "packages/core/tests/generated-xmd.test.ts": 3229, - "packages/core/tests/glob-api.test.ts": 523, - "packages/core/tests/glob-component.test.ts": 1907, - "packages/core/tests/guarded-journal.test.ts": 1182, - "packages/core/tests/has-content.test.ts": 807, - "packages/core/tests/heal.test.ts": 843, - "packages/core/tests/if.test.ts": 1619, - "packages/core/tests/inline-root.test.ts": 1202, - "packages/core/tests/invocation-failures.test.ts": 544, - "packages/core/tests/invocation-identity.test.ts": 1548, - "packages/core/tests/invocation-scope.test.ts": 1068, - "packages/core/tests/journal-source-position.test.ts": 1292, - "packages/core/tests/json-component.test.ts": 1360, - "packages/core/tests/json.test.ts": 462, - "packages/core/tests/let.test.ts": 1152, - "packages/core/tests/loop.test.ts": 2036, - "packages/core/tests/named-slots.test.ts": 2073, - "packages/core/tests/output-error-mode.test.ts": 2340, - "packages/core/tests/output-normalize.test.ts": 464, - "packages/core/tests/output-terminal.test.ts": 626, - "packages/core/tests/parse-components.test.ts": 1304, - "packages/core/tests/plan-response.test.ts": 538, - "packages/core/tests/plugin-api.test.ts": 486, - "packages/core/tests/plugin.test.ts": 884, - "packages/core/tests/props-binding.test.ts": 1295, - "packages/core/tests/protected-content.test.ts": 497, - "packages/core/tests/registered-printed-errors.test.ts": 767, - "packages/core/tests/replay-stream.test.ts": 451, - "packages/core/tests/retain.test.ts": 1037, - "packages/core/tests/root-composition.test.ts": 1279, - "packages/core/tests/root-props.test.ts": 1298, - "packages/core/tests/root-provider.test.ts": 1195, - "packages/core/tests/sample-component.test.ts": 2088, - "packages/core/tests/scanner.test.ts": 521, - "packages/core/tests/scope-local.test.ts": 456, - "packages/core/tests/secret-detection.test.ts": 1897, - "packages/core/tests/secret-files.test.ts": 687, - "packages/core/tests/secret-rules.test.ts": 794, - "packages/core/tests/secret-scanner-baseline.test.ts": 704, - "packages/core/tests/secret-scanner.test.ts": 641, - "packages/core/tests/source-position.test.ts": 1014, - "packages/core/tests/state-ownership.test.ts": 1068, - "packages/core/tests/streaming-emission.test.ts": 1022, - "packages/core/tests/switch.test.ts": 1684, - "packages/core/tests/syntax-catalog.test.ts": 1636, - "packages/core/tests/syntax-component.test.ts": 2426, - "packages/core/tests/temp-dir.test.ts": 3533, - "packages/core/tests/temp-file-compiler.test.ts": 508, - "packages/core/tests/text-interpolation.test.ts": 794, - "packages/core/tests/try-content.test.ts": 818, - "packages/core/tests/unused-in-diff.test.ts": 1205, - "packages/core/tests/validate.test.ts": 735, - "packages/core/tests/validation-integration.test.ts": 1052, - "packages/core/tests/workflow-component-bundle.test.ts": 1387, - "packages/durable-streams/tests/context.test.ts": 479, - "packages/durable-streams/tests/deterministic-id.test.ts": 510, - "packages/durable-streams/tests/divergence-api.test.ts": 495, - "packages/durable-streams/tests/divergence.test.ts": 521, - "packages/durable-streams/tests/durable-each.test.ts": 522, - "packages/durable-streams/tests/durable-position.test.ts": 504, - "packages/durable-streams/tests/durable-run.test.ts": 507, - "packages/durable-streams/tests/ephemeral.test.ts": 506, - "packages/durable-streams/tests/fail-stop.test.ts": 562, - "packages/durable-streams/tests/guard-stream.test.ts": 563, - "packages/durable-streams/tests/live-coordinator.test.ts": 500, - "packages/durable-streams/tests/parse.test.ts": 512, - "packages/durable-streams/tests/replay-guard.test.ts": 522, - "packages/durable-streams/tests/replay-index.test.ts": 483, - "packages/durable-streams/tests/retained.test.ts": 471, - "packages/durable-streams/tests/serialize-event.test.ts": 492, - "packages/durable-streams/tests/smoke.test.ts": 508, - "packages/durable-streams/tests/structured-concurrency.test.ts": 551, - "packages/durable-streams/tests/terminal-boundary.test.ts": 517, - "packages/durable-streams/tests/types.test.ts": 450, - "packages/git/tests/api-entrypoint.test.ts": 1119, - "packages/git/tests/git-host-effect.test.ts": 6430, - "packages/git/tests/git.test.ts": 1131, - "packages/git/tests/github-activation.test.ts": 1434, - "packages/git/tests/github-issues.test.ts": 1073, - "packages/git/tests/github-pull-requests.test.ts": 542, - "packages/git/tests/issue-markdown.test.ts": 9143, - "packages/git/tests/issue-records.test.ts": 1047, - "packages/git/tests/module-partition.test.ts": 2661, - "packages/git/tests/plugin.test.ts": 1514, - "packages/git/tests/provider-neutrality.test.ts": 2039, - "packages/git/tests/pull-request-records.test.ts": 1105, - "packages/git/tests/run-composition-lazy.test.ts": 1805, - "packages/git/tests/selection-authentication.test.ts": 461, - "packages/runtime/tests/duration.test.ts": 446, - "packages/runtime/tests/fetch.test.ts": 1049, - "packages/runtime/tests/fs.test.ts": 499, - "packages/runtime/tests/host-files.test.ts": 1219, - "packages/runtime/tests/native-launcher.test.ts": 2801, - "packages/runtime/tests/process.test.ts": 493, - "packages/runtime/tests/service.test.ts": 478, - "packages/test-agent/tests/acp-server.test.ts": 607, - "packages/test-agent/tests/behavior-engine.test.ts": 1403, - "packages/test-agent/tests/bridge.test.ts": 475, - "packages/test-agent/tests/command-lazy.test.ts": 5674, - "packages/test-agent/tests/components.test.ts": 91334, - "packages/test-agent/tests/controller.test.ts": 715, - "packages/test-agent/tests/cross-package-resolution.test.ts": 2578, - "packages/test-agent/tests/native-launch.test.ts": 45647, - "packages/test-agent/tests/net.test.ts": 538, - "packages/test-agent/tests/profile.test.ts": 1044, - "packages/test-agent/tests/protocol.test.ts": 518, - "packages/test-agent/tests/provider.test.ts": 1162, - "packages/test-agent/tests/public-api.test.ts": 1360, - "packages/test-agent/tests/route-slot.test.ts": 460, - "packages/test-agent/tests/smoke.test.ts": 24811, - "packages/test-agent/tests/template.test.ts": 1030, - "packages/test-agent/tests/worker-lifecycle.test.ts": 19686, - "packages/test-support/tests/journal.test.ts": 449, - "packages/testing/tests/agent-composition.test.ts": 1315, - "packages/testing/tests/assert-throws.test.ts": 2789, - "packages/testing/tests/assert.test.ts": 474, - "packages/testing/tests/assertions.test.ts": 3439, - "packages/testing/tests/boundary-contract.test.ts": 2288, - "packages/testing/tests/cli.test.ts": 9209, - "packages/testing/tests/execution-harness.test.ts": 6818, - "packages/testing/tests/replay.test.ts": 2793, - "packages/testing/tests/smoke.test.ts": 2855, - "packages/testing/tests/test-component.test.ts": 3610, - "packages/testing/tests/testing-activation.test.ts": 3089, - "packages/testing/tests/testing-boundary.test.ts": 1788, - "packages/testing/tests/testing-mode.test.ts": 3703, - "packages/testing/tests/use-testing.test.ts": 2450, - "packages/web/tests/assets.test.ts": 455, - "packages/web/tests/client-logic.test.ts": 480, - "packages/web/tests/compile.test.ts": 1703, - "packages/web/tests/component.test.ts": 2198, - "packages/web/tests/declaration.test.ts": 1459, - "packages/web/tests/document.test.ts": 1514, - "packages/web/tests/elicitation.test.ts": 1370, - "packages/web/tests/live-form.test.ts": 1291, - "packages/web/tests/markdown.test.ts": 597, - "packages/web/tests/opener.test.ts": 1279, - "packages/web/tests/page.test.ts": 453, - "packages/web/tests/responder.test.ts": 1571, - "packages/web/tests/server-lifecycle.test.ts": 1398, - "packages/web/tests/server.test.ts": 1813, - "packages/workflow/tests/generated-observations.test.ts": 1253, - "packages/workflow/tests/retained-run.test.ts": 1926, - "packages/workflow/tests/service-denial.test.ts": 461, - "packages/workflow/tests/workflow-bundle.test.ts": 1460, - "packages/workflow/tests/workflow-definition.test.ts": 1172, - "packages/workflow/tests/workflow-fork.test.ts": 1038, - "packages/workflow/tests/workflow-run.test.ts": 1839, - "packages/workflow/tests/workspace-effect.test.ts": 2652, - "packages/workflow/tests/xmd-artifact.test.ts": 3522, - "scripts/tests/adapter-vendor.test.ts": 657, - "scripts/tests/bootstrap-npm-package.test.ts": 7338, - "scripts/tests/bump-version.test.ts": 563, - "scripts/tests/changed-paths.test.ts": 477, - "scripts/tests/ci-workflow.test.ts": 3191, - "scripts/tests/consumer-cycle.test.ts": 615, - "scripts/tests/filesystem-contract-workflow.test.ts": 523, - "scripts/tests/main-green.test.ts": 722, - "scripts/tests/main-health.test.ts": 558, - "scripts/tests/no-module-scoped-registry.test.ts": 1169, - "scripts/tests/no-redundant-test-scope.test.ts": 4617, - "scripts/tests/no-sync-filesystem.test.ts": 4929, - "scripts/tests/no-yield-in-finally.test.ts": 1166, - "scripts/tests/oxlint-policy.test.ts": 9186, - "scripts/tests/packaged-document.test.ts": 542, - "scripts/tests/prefer-effection-operation.test.ts": 5702, - "scripts/tests/prefer-effection-result.test.ts": 4004, - "scripts/tests/publish-workflow-membership.test.ts": 597, - "scripts/tests/review-infrastructure.test.ts": 2845, - "scripts/tests/runtime-exclusions.test.ts": 904, - "scripts/tests/runtime-tests.test.ts": 508, - "scripts/tests/scope-bound-event-registration.test.ts": 4708, - "scripts/tests/staged-write.test.ts": 492, - "scripts/tests/test-file-discovery.test.ts": 732, - "scripts/tests/test-shards.test.ts": 497, - "scripts/tests/test-weights.test.ts": 660, - "scripts/tests/tracked.test.ts": 479, - "scripts/tests/verify-coordinator.test.ts": 670, - "scripts/tests/version-lockstep.test.ts": 613, - "scripts/tests/web-client-module.test.ts": 464, - "scripts/tests/workspace.test.ts": 453 + "packages/acp/tests/acceptance.test.ts": 2214, + "packages/acp/tests/acpx-checkpoint-meta.test.ts": 4877, + "packages/acp/tests/acpx-materialization.test.ts": 7085, + "packages/acp/tests/acpx-transient-env.test.ts": 831, + "packages/acp/tests/adapter-materialization.test.ts": 74897, + "packages/acp/tests/adapter-protocol.test.ts": 55440, + "packages/acp/tests/codex-surfaces.test.ts": 670, + "packages/acp/tests/native-launch.test.ts": 2283, + "packages/acp/tests/permission-bridge.test.ts": 1272, + "packages/acp/tests/provider.test.ts": 2223, + "packages/acp/tests/serial-queue.test.ts": 499, + "packages/acp/tests/session-key.test.ts": 551, + "packages/acp/tests/terminal-screen.test.ts": 624, + "packages/cli/tests/agent-adapters.test.ts": 2528, + "packages/cli/tests/agent-cli.test.ts": 43989, + "packages/cli/tests/agent-config.test.ts": 487, + "packages/cli/tests/agent-options-cli.test.ts": 16335, + "packages/cli/tests/agent-options.test.ts": 494, + "packages/cli/tests/agent-session-coordinator.test.ts": 1365, + "packages/cli/tests/cli-help.test.ts": 40794, + "packages/cli/tests/command.test.ts": 14816, + "packages/cli/tests/document-suites/inline/inline-markdown.test.ts": 4258, + "packages/cli/tests/document-suites/plan/plan-markdown.test.ts": 172135, + "packages/cli/tests/document-suites/props/props-markdown.test.ts": 3627, + "packages/cli/tests/document-suites/syntax/syntax-markdown.test.ts": 9948, + "packages/cli/tests/document-suites/targets/targets-markdown.test.ts": 6186, + "packages/cli/tests/document-suites/verbose/verbose-markdown.test.ts": 18155, + "packages/cli/tests/evaluate-component.test.ts": 15406, + "packages/cli/tests/fetch-cli.test.ts": 4942, + "packages/cli/tests/file-stream.test.ts": 536, + "packages/cli/tests/inline-cli.test.ts": 67652, + "packages/cli/tests/launch-timeout.test.ts": 20546, + "packages/cli/tests/packaged-document.test.ts": 1594, + "packages/cli/tests/plan-args.test.ts": 507, + "packages/cli/tests/plan-cli.test.ts": 118889, + "packages/cli/tests/plan-command-document.test.ts": 8078, + "packages/cli/tests/plan-component.test.ts": 11071, + "packages/cli/tests/plan-host-acts.test.ts": 2359, + "packages/cli/tests/plan.test.ts": 15760, + "packages/cli/tests/plugin-cli.test.ts": 82043, + "packages/cli/tests/plugin-host.test.ts": 3277, + "packages/cli/tests/plugin-modules.test.ts": 487, + "packages/cli/tests/plugin-selection.test.ts": 514, + "packages/cli/tests/process-retention.test.ts": 4876, + "packages/cli/tests/props-cli.test.ts": 63573, + "packages/cli/tests/props-schema.test.ts": 1342, + "packages/cli/tests/props-sources.test.ts": 690, + "packages/cli/tests/repl-boundaries.test.ts": 2486, + "packages/cli/tests/repl-composition.test.ts": 725, + "packages/cli/tests/repl-execution.test.ts": 2896, + "packages/cli/tests/repl-journey.test.ts": 11109, + "packages/cli/tests/repl-model.test.ts": 2703, + "packages/cli/tests/repl-route.test.ts": 2270, + "packages/cli/tests/repl-terminal.test.ts": 1276, + "packages/cli/tests/run-composition.test.ts": 4115, + "packages/cli/tests/run-deadline.test.ts": 3685, + "packages/cli/tests/run-profile.test.ts": 1236, + "packages/cli/tests/run-timeouts.test.ts": 20388, + "packages/cli/tests/secret-detection-cli.test.ts": 37989, + "packages/cli/tests/service-document.test.ts": 2573, + "packages/cli/tests/service-host.test.ts": 2340, + "packages/cli/tests/session-launch-cli.test.ts": 11070, + "packages/cli/tests/stdin-cli.test.ts": 63409, + "packages/cli/tests/stdout-delivery.test.ts": 497, + "packages/cli/tests/syntax-cli.test.ts": 62826, + "packages/cli/tests/targets-cli.test.ts": 51186, + "packages/cli/tests/test-root-profile.test.ts": 7084, + "packages/cli/tests/test-target.test.ts": 74365, + "packages/cli/tests/testing-activation.test.ts": 1516, + "packages/cli/tests/testing-execution-host.test.ts": 93380, + "packages/cli/tests/upgrade-cli.test.ts": 47918, + "packages/cli/tests/upgrade-command-document.test.ts": 5587, + "packages/cli/tests/upgrade-output.test.ts": 1896, + "packages/cli/tests/value-root.test.ts": 31919, + "packages/cli/tests/verbose-component.test.ts": 1944, + "packages/cli/tests/workflow-declaration.test.ts": 1458, + "packages/cli/tests/workflow-host.test.ts": 10902, + "packages/code-review-agent/tests/doctor.test.ts": 489, + "packages/code-review-agent/tests/parse-diagnostics.test.ts": 515, + "packages/code-review-agent/tests/parse-diff.test.ts": 507, + "packages/code-review-agent/tests/parse-doctor.test.ts": 1157, + "packages/code-review-agent/tests/parse-oxlint.test.ts": 477, + "packages/code-review-agent/tests/policy.test.ts": 494, + "packages/core/tests/agent-api.test.ts": 514, + "packages/core/tests/agent-components.test.ts": 2041, + "packages/core/tests/agent-function-components.test.ts": 4301, + "packages/core/tests/agent-session-launch.test.ts": 4590, + "packages/core/tests/agent-session-placement.test.ts": 517, + "packages/core/tests/agent-session-use.test.ts": 503, + "packages/core/tests/agent-stack.test.ts": 1569, + "packages/core/tests/answer-identity.test.ts": 513, + "packages/core/tests/answers-component.test.ts": 2342, + "packages/core/tests/answers-expansion-recursion.test.ts": 979, + "packages/core/tests/capture-errors.test.ts": 1261, + "packages/core/tests/capture-props.test.ts": 991, + "packages/core/tests/cli-journal.test.ts": 22526, + "packages/core/tests/code-block-component.test.ts": 3318, + "packages/core/tests/compiler-boundary.test.ts": 1179, + "packages/core/tests/component-api.test.ts": 559, + "packages/core/tests/component-registration.test.ts": 2126, + "packages/core/tests/component-returns.test.ts": 2950, + "packages/core/tests/component-schema-conformance.test.ts": 1126, + "packages/core/tests/config-api.test.ts": 1317, + "packages/core/tests/construct-error-observation.test.ts": 1432, + "packages/core/tests/contextual-cwd.test.ts": 2499, + "packages/core/tests/daemon-integration.test.ts": 1955, + "packages/core/tests/daemon.test.ts": 595, + "packages/core/tests/declared-markdown-component.test.ts": 3319, + "packages/core/tests/document-output-api.test.ts": 520, + "packages/core/tests/document-target-execution.test.ts": 4941, + "packages/core/tests/document-targets.test.ts": 1246, + "packages/core/tests/document-validation.test.ts": 3151, + "packages/core/tests/documentation-index.test.ts": 904, + "packages/core/tests/each.test.ts": 1348, + "packages/core/tests/elicit-component.test.ts": 2100, + "packages/core/tests/ephemeral-service.test.ts": 1907, + "packages/core/tests/eval-bindings.test.ts": 1306, + "packages/core/tests/eval-context.test.ts": 673, + "packages/core/tests/eval-durable.test.ts": 1382, + "packages/core/tests/eval-error-mode.test.ts": 1732, + "packages/core/tests/eval-interpolate.test.ts": 521, + "packages/core/tests/eval-middleware.test.ts": 604, + "packages/core/tests/eval-persist.test.ts": 1475, + "packages/core/tests/eval-return.test.ts": 1359, + "packages/core/tests/eval-scope.test.ts": 1173, + "packages/core/tests/eval-timeout.test.ts": 1347, + "packages/core/tests/eval-transform.test.ts": 590, + "packages/core/tests/evaluate-component.test.ts": 3762, + "packages/core/tests/evaluate-loaded-copy.test.ts": 1404, + "packages/core/tests/evaluate-provider-lifetime.test.ts": 1467, + "packages/core/tests/evaluation-profile.test.ts": 898, + "packages/core/tests/exec-timeout.test.ts": 1413, + "packages/core/tests/execute.test.ts": 2063, + "packages/core/tests/execution-protocol.test.ts": 2931, + "packages/core/tests/expand.test.ts": 2017, + "packages/core/tests/expansion-identity.test.ts": 2065, + "packages/core/tests/expansion-metadata.test.ts": 1026, + "packages/core/tests/expression-props.test.ts": 1884, + "packages/core/tests/fail-component.test.ts": 1678, + "packages/core/tests/failure-printing.test.ts": 1484, + "packages/core/tests/fatal-cause.test.ts": 893, + "packages/core/tests/fetch-component.test.ts": 2938, + "packages/core/tests/file-component.test.ts": 3461, + "packages/core/tests/file-delete-component.test.ts": 1504, + "packages/core/tests/files-fatal.test.ts": 2437, + "packages/core/tests/foreground-exec.test.ts": 2319, + "packages/core/tests/frontmatter.test.ts": 553, + "packages/core/tests/function-components.test.ts": 2057, + "packages/core/tests/generated-composition.test.ts": 2059, + "packages/core/tests/generated-xmd.test.ts": 3590, + "packages/core/tests/glob-api.test.ts": 595, + "packages/core/tests/glob-component.test.ts": 2107, + "packages/core/tests/guarded-journal.test.ts": 1330, + "packages/core/tests/has-content.test.ts": 949, + "packages/core/tests/heal.test.ts": 945, + "packages/core/tests/if.test.ts": 1850, + "packages/core/tests/inline-root.test.ts": 1347, + "packages/core/tests/invocation-failures.test.ts": 590, + "packages/core/tests/invocation-identity.test.ts": 1686, + "packages/core/tests/invocation-scope.test.ts": 1215, + "packages/core/tests/journal-source-position.test.ts": 1456, + "packages/core/tests/json-component.test.ts": 1549, + "packages/core/tests/json.test.ts": 500, + "packages/core/tests/let.test.ts": 1354, + "packages/core/tests/loop.test.ts": 2322, + "packages/core/tests/named-slots.test.ts": 2291, + "packages/core/tests/output-error-mode.test.ts": 2574, + "packages/core/tests/output-normalize.test.ts": 523, + "packages/core/tests/output-terminal.test.ts": 678, + "packages/core/tests/parse-components.test.ts": 1418, + "packages/core/tests/plan-response.test.ts": 617, + "packages/core/tests/plugin-api.test.ts": 524, + "packages/core/tests/plugin.test.ts": 994, + "packages/core/tests/props-binding.test.ts": 1442, + "packages/core/tests/protected-content.test.ts": 550, + "packages/core/tests/registered-printed-errors.test.ts": 849, + "packages/core/tests/replay-stream.test.ts": 492, + "packages/core/tests/retain.test.ts": 1158, + "packages/core/tests/root-composition.test.ts": 1382, + "packages/core/tests/root-props.test.ts": 1478, + "packages/core/tests/root-provider.test.ts": 1360, + "packages/core/tests/sample-component.test.ts": 2419, + "packages/core/tests/scanner.test.ts": 602, + "packages/core/tests/scope-local.test.ts": 513, + "packages/core/tests/secret-detection.test.ts": 2190, + "packages/core/tests/secret-files.test.ts": 770, + "packages/core/tests/secret-rules.test.ts": 841, + "packages/core/tests/secret-scanner-baseline.test.ts": 790, + "packages/core/tests/secret-scanner.test.ts": 718, + "packages/core/tests/source-position.test.ts": 1169, + "packages/core/tests/state-ownership.test.ts": 1257, + "packages/core/tests/streaming-emission.test.ts": 1186, + "packages/core/tests/switch.test.ts": 1908, + "packages/core/tests/syntax-catalog.test.ts": 1912, + "packages/core/tests/syntax-component.test.ts": 2776, + "packages/core/tests/temp-dir.test.ts": 3772, + "packages/core/tests/temp-file-compiler.test.ts": 575, + "packages/core/tests/text-interpolation.test.ts": 947, + "packages/core/tests/try-content.test.ts": 957, + "packages/core/tests/unused-in-diff.test.ts": 1369, + "packages/core/tests/validate.test.ts": 807, + "packages/core/tests/validation-integration.test.ts": 1211, + "packages/core/tests/workflow-component-bundle.test.ts": 1592, + "packages/durable-streams/tests/context.test.ts": 529, + "packages/durable-streams/tests/deterministic-id.test.ts": 557, + "packages/durable-streams/tests/divergence-api.test.ts": 553, + "packages/durable-streams/tests/divergence.test.ts": 586, + "packages/durable-streams/tests/durable-each.test.ts": 591, + "packages/durable-streams/tests/durable-position.test.ts": 554, + "packages/durable-streams/tests/durable-run.test.ts": 564, + "packages/durable-streams/tests/ephemeral.test.ts": 550, + "packages/durable-streams/tests/fail-stop.test.ts": 617, + "packages/durable-streams/tests/guard-stream.test.ts": 630, + "packages/durable-streams/tests/live-coordinator.test.ts": 553, + "packages/durable-streams/tests/parse.test.ts": 582, + "packages/durable-streams/tests/replay-guard.test.ts": 587, + "packages/durable-streams/tests/replay-index.test.ts": 546, + "packages/durable-streams/tests/retained.test.ts": 530, + "packages/durable-streams/tests/serialize-event.test.ts": 546, + "packages/durable-streams/tests/smoke.test.ts": 540, + "packages/durable-streams/tests/structured-concurrency.test.ts": 611, + "packages/durable-streams/tests/terminal-boundary.test.ts": 577, + "packages/durable-streams/tests/types.test.ts": 522, + "packages/git/tests/api-entrypoint.test.ts": 1280, + "packages/git/tests/git-host-effect.test.ts": 7174, + "packages/git/tests/git.test.ts": 1304, + "packages/git/tests/github-activation.test.ts": 1694, + "packages/git/tests/github-issues.test.ts": 1218, + "packages/git/tests/github-pull-requests.test.ts": 605, + "packages/git/tests/issue-markdown.test.ts": 10086, + "packages/git/tests/issue-records.test.ts": 1178, + "packages/git/tests/module-partition.test.ts": 3239, + "packages/git/tests/plugin.test.ts": 1737, + "packages/git/tests/provider-neutrality.test.ts": 2298, + "packages/git/tests/pull-request-records.test.ts": 1273, + "packages/git/tests/run-composition-lazy.test.ts": 1993, + "packages/git/tests/selection-authentication.test.ts": 497, + "packages/runtime/tests/duration.test.ts": 476, + "packages/runtime/tests/fetch.test.ts": 1115, + "packages/runtime/tests/fs.test.ts": 569, + "packages/runtime/tests/host-files.test.ts": 1317, + "packages/runtime/tests/native-launcher.test.ts": 2885, + "packages/runtime/tests/process.test.ts": 542, + "packages/runtime/tests/service.test.ts": 555, + "packages/test-agent/tests/acp-server.test.ts": 684, + "packages/test-agent/tests/behavior-engine.test.ts": 1616, + "packages/test-agent/tests/bridge.test.ts": 527, + "packages/test-agent/tests/command-lazy.test.ts": 6698, + "packages/test-agent/tests/components.test.ts": 108320, + "packages/test-agent/tests/controller.test.ts": 807, + "packages/test-agent/tests/cross-package-resolution.test.ts": 2949, + "packages/test-agent/tests/native-launch.test.ts": 55467, + "packages/test-agent/tests/net.test.ts": 597, + "packages/test-agent/tests/profile.test.ts": 1185, + "packages/test-agent/tests/protocol.test.ts": 607, + "packages/test-agent/tests/provider.test.ts": 1400, + "packages/test-agent/tests/public-api.test.ts": 1576, + "packages/test-agent/tests/route-slot.test.ts": 510, + "packages/test-agent/tests/smoke.test.ts": 29490, + "packages/test-agent/tests/template.test.ts": 1181, + "packages/test-agent/tests/worker-lifecycle.test.ts": 24412, + "packages/test-support/tests/journal.test.ts": 516, + "packages/testing/tests/agent-composition.test.ts": 1550, + "packages/testing/tests/assert-throws.test.ts": 3272, + "packages/testing/tests/assert.test.ts": 546, + "packages/testing/tests/assertions.test.ts": 3984, + "packages/testing/tests/boundary-contract.test.ts": 2633, + "packages/testing/tests/cli.test.ts": 11610, + "packages/testing/tests/execution-harness.test.ts": 7778, + "packages/testing/tests/replay.test.ts": 3111, + "packages/testing/tests/smoke.test.ts": 3510, + "packages/testing/tests/test-component.test.ts": 4058, + "packages/testing/tests/testing-activation.test.ts": 3530, + "packages/testing/tests/testing-boundary.test.ts": 2114, + "packages/testing/tests/testing-mode.test.ts": 4196, + "packages/testing/tests/use-testing.test.ts": 2982, + "packages/web/tests/assets.test.ts": 505, + "packages/web/tests/client-logic.test.ts": 564, + "packages/web/tests/compile.test.ts": 2013, + "packages/web/tests/component.test.ts": 2622, + "packages/web/tests/declaration.test.ts": 1740, + "packages/web/tests/document.test.ts": 1828, + "packages/web/tests/elicitation.test.ts": 1633, + "packages/web/tests/live-form.test.ts": 1516, + "packages/web/tests/markdown.test.ts": 719, + "packages/web/tests/opener.test.ts": 1551, + "packages/web/tests/page.test.ts": 497, + "packages/web/tests/responder.test.ts": 1899, + "packages/web/tests/server-lifecycle.test.ts": 1696, + "packages/web/tests/server.test.ts": 2090, + "packages/workflow/tests/generated-observations.test.ts": 1528, + "packages/workflow/tests/retained-run.test.ts": 2350, + "packages/workflow/tests/service-denial.test.ts": 536, + "packages/workflow/tests/workflow-bundle.test.ts": 1645, + "packages/workflow/tests/workflow-definition.test.ts": 1368, + "packages/workflow/tests/workflow-fork.test.ts": 1228, + "packages/workflow/tests/workflow-run.test.ts": 2138, + "packages/workflow/tests/workspace-effect.test.ts": 3176, + "packages/workflow/tests/xmd-artifact.test.ts": 3981, + "scripts/tests/adapter-vendor.test.ts": 748, + "scripts/tests/bootstrap-npm-package.test.ts": 8714, + "scripts/tests/bump-version.test.ts": 640, + "scripts/tests/changed-paths.test.ts": 555, + "scripts/tests/ci-workflow.test.ts": 3812, + "scripts/tests/consumer-cycle.test.ts": 695, + "scripts/tests/filesystem-contract-workflow.test.ts": 598, + "scripts/tests/main-green.test.ts": 797, + "scripts/tests/main-health.test.ts": 626, + "scripts/tests/no-module-scoped-registry.test.ts": 1399, + "scripts/tests/no-redundant-test-scope.test.ts": 5451, + "scripts/tests/no-sync-filesystem.test.ts": 5830, + "scripts/tests/no-yield-in-finally.test.ts": 1331, + "scripts/tests/oxlint-policy.test.ts": 11385, + "scripts/tests/packaged-document.test.ts": 636, + "scripts/tests/prefer-effection-operation.test.ts": 6792, + "scripts/tests/prefer-effection-result.test.ts": 4734, + "scripts/tests/publish-workflow-membership.test.ts": 681, + "scripts/tests/review-infrastructure.test.ts": 3482, + "scripts/tests/runtime-exclusions.test.ts": 1041, + "scripts/tests/runtime-tests.test.ts": 585, + "scripts/tests/scope-bound-event-registration.test.ts": 5632, + "scripts/tests/staged-write.test.ts": 551, + "scripts/tests/test-file-discovery.test.ts": 877, + "scripts/tests/test-shards.test.ts": 570, + "scripts/tests/test-weights.test.ts": 762, + "scripts/tests/tracked.test.ts": 541, + "scripts/tests/verify-coordinator.test.ts": 769, + "scripts/tests/version-lockstep.test.ts": 739, + "scripts/tests/web-client-module.test.ts": 541, + "scripts/tests/workspace.test.ts": 522 }, "bun": { - "packages/acp/tests/acceptance.test.ts": 744, - "packages/acp/tests/acpx-checkpoint-meta.test.ts": 4286, - "packages/acp/tests/acpx-materialization.test.ts": 6484, - "packages/acp/tests/acpx-transient-env.test.ts": 248, - "packages/acp/tests/adapter-materialization.test.ts": 66479, - "packages/acp/tests/adapter-protocol.test.ts": 49439, - "packages/acp/tests/codex-surfaces.test.ts": 118, - "packages/acp/tests/native-launch.test.ts": 951, - "packages/acp/tests/permission-bridge.test.ts": 339, - "packages/acp/tests/provider.test.ts": 1055, - "packages/acp/tests/serial-queue.test.ts": 63, - "packages/acp/tests/session-key.test.ts": 67, - "packages/acp/tests/terminal-screen.test.ts": 80, - "packages/cli/tests/agent-adapters.test.ts": 1099, - "packages/cli/tests/agent-cli.test.ts": 19452, - "packages/cli/tests/agent-config.test.ts": 26, - "packages/cli/tests/agent-options-cli.test.ts": 7289, - "packages/cli/tests/agent-options.test.ts": 29, - "packages/cli/tests/agent-session-coordinator.test.ts": 367, - "packages/cli/tests/cli-help.test.ts": 10618, - "packages/cli/tests/command.test.ts": 7507, - "packages/cli/tests/document-suites/inline/inline-markdown.test.ts": 2750, - "packages/cli/tests/document-suites/plan/plan-markdown.test.ts": 100519, - "packages/cli/tests/document-suites/props/props-markdown.test.ts": 2160, - "packages/cli/tests/document-suites/syntax/syntax-markdown.test.ts": 4408, - "packages/cli/tests/document-suites/targets/targets-markdown.test.ts": 4564, - "packages/cli/tests/document-suites/verbose/verbose-markdown.test.ts": 8036, - "packages/cli/tests/evaluate-component.test.ts": 7095, - "packages/cli/tests/fetch-cli.test.ts": 2106, - "packages/cli/tests/file-stream.test.ts": 53, - "packages/cli/tests/inline-cli.test.ts": 28810, - "packages/cli/tests/launch-timeout.test.ts": 10079, - "packages/cli/tests/packaged-document.test.ts": 428, - "packages/cli/tests/plan-args.test.ts": 30, - "packages/cli/tests/plan-cli.test.ts": 54947, - "packages/cli/tests/plan-command-document.test.ts": 5393, - "packages/cli/tests/plan-component.test.ts": 8517, - "packages/cli/tests/plan-host-acts.test.ts": 1118, - "packages/cli/tests/plan.test.ts": 12187, - "packages/cli/tests/plugin-cli.test.ts": 35808, - "packages/cli/tests/plugin-host.test.ts": 1844, - "packages/cli/tests/plugin-modules.test.ts": 33, - "packages/cli/tests/plugin-selection.test.ts": 31, - "packages/cli/tests/process-retention.test.ts": 2291, - "packages/cli/tests/props-cli.test.ts": 25899, - "packages/cli/tests/props-schema.test.ts": 383, - "packages/cli/tests/props-sources.test.ts": 117, - "packages/cli/tests/repl-composition.test.ts": 132, - "packages/cli/tests/repl-execution.test.ts": 1763, - "packages/cli/tests/repl-model.test.ts": 890, - "packages/cli/tests/repl-route.test.ts": 724, - "packages/cli/tests/repl-terminal.test.ts": 799, - "packages/cli/tests/run-composition.test.ts": 2884, - "packages/cli/tests/run-deadline.test.ts": 2305, - "packages/cli/tests/run-profile.test.ts": 285, - "packages/cli/tests/run-timeouts.test.ts": 10667, - "packages/cli/tests/secret-detection-cli.test.ts": 16731, - "packages/cli/tests/service-document.test.ts": 1429, - "packages/cli/tests/service-host.test.ts": 1328, - "packages/cli/tests/session-launch-cli.test.ts": 5280, - "packages/cli/tests/stdin-cli.test.ts": 28646, - "packages/cli/tests/stdout-delivery.test.ts": 46, - "packages/cli/tests/syntax-cli.test.ts": 30236, - "packages/cli/tests/targets-cli.test.ts": 19960, - "packages/cli/tests/test-root-profile.test.ts": 3486, - "packages/cli/tests/test-target.test.ts": 32810, - "packages/cli/tests/testing-activation.test.ts": 531, - "packages/cli/tests/testing-execution-host.test.ts": 45619, - "packages/cli/tests/upgrade-cli.test.ts": 16213, - "packages/cli/tests/upgrade-command-document.test.ts": 3396, - "packages/cli/tests/upgrade-output.test.ts": 781, - "packages/cli/tests/value-root.test.ts": 14408, - "packages/cli/tests/verbose-component.test.ts": 708, - "packages/cli/tests/workflow-declaration.test.ts": 470, - "packages/cli/tests/workflow-host.test.ts": 3873, - "packages/code-review-agent/tests/doctor.test.ts": 27, - "packages/code-review-agent/tests/parse-diagnostics.test.ts": 34, - "packages/code-review-agent/tests/parse-diff.test.ts": 32, - "packages/code-review-agent/tests/parse-doctor.test.ts": 272, - "packages/code-review-agent/tests/parse-oxlint.test.ts": 27, - "packages/code-review-agent/tests/policy.test.ts": 28, - "packages/core/tests/agent-api.test.ts": 30, - "packages/core/tests/agent-components.test.ts": 1143, - "packages/core/tests/agent-function-components.test.ts": 2785, - "packages/core/tests/agent-session-launch.test.ts": 3217, - "packages/core/tests/agent-session-placement.test.ts": 32, - "packages/core/tests/agent-session-use.test.ts": 27, - "packages/core/tests/agent-stack.test.ts": 655, - "packages/core/tests/answer-identity.test.ts": 36, - "packages/core/tests/answers-component.test.ts": 1309, - "packages/core/tests/answers-expansion-recursion.test.ts": 281, - "packages/core/tests/capture-errors.test.ts": 525, - "packages/core/tests/capture-props.test.ts": 290, - "packages/core/tests/cli-journal.test.ts": 9215, - "packages/core/tests/code-block-component.test.ts": 2245, - "packages/core/tests/compiler-boundary.test.ts": 316, - "packages/core/tests/component-api.test.ts": 49, - "packages/core/tests/component-registration.test.ts": 978, - "packages/core/tests/component-returns.test.ts": 1439, - "packages/core/tests/component-schema-conformance.test.ts": 525, - "packages/core/tests/config-api.test.ts": 315, - "packages/core/tests/construct-error-observation.test.ts": 633, - "packages/core/tests/contextual-cwd.test.ts": 1490, - "packages/core/tests/daemon-integration.test.ts": 998, - "packages/core/tests/daemon.test.ts": 63, - "packages/core/tests/declared-markdown-component.test.ts": 1873, - "packages/core/tests/document-output-api.test.ts": 33, - "packages/core/tests/document-target-execution.test.ts": 3168, - "packages/core/tests/document-targets.test.ts": 381, - "packages/core/tests/document-validation.test.ts": 1906, - "packages/core/tests/documentation-index.test.ts": 149, - "packages/core/tests/each.test.ts": 384, - "packages/core/tests/elicit-component.test.ts": 1014, - "packages/core/tests/ephemeral-service.test.ts": 761, - "packages/core/tests/eval-bindings.test.ts": 355, - "packages/core/tests/eval-context.test.ts": 73, - "packages/core/tests/eval-durable.test.ts": 388, - "packages/core/tests/eval-error-mode.test.ts": 620, - "packages/core/tests/eval-interpolate.test.ts": 33, - "packages/core/tests/eval-middleware.test.ts": 67, - "packages/core/tests/eval-persist.test.ts": 449, - "packages/core/tests/eval-return.test.ts": 397, - "packages/core/tests/eval-scope.test.ts": 306, - "packages/core/tests/eval-timeout.test.ts": 386, - "packages/core/tests/eval-transform.test.ts": 57, - "packages/core/tests/evaluate-component.test.ts": 2345, - "packages/core/tests/evaluate-loaded-copy.test.ts": 472, - "packages/core/tests/evaluate-provider-lifetime.test.ts": 542, - "packages/core/tests/evaluation-profile.test.ts": 169, - "packages/core/tests/exec-timeout.test.ts": 447, - "packages/core/tests/execute.test.ts": 970, - "packages/core/tests/execution-protocol.test.ts": 1559, - "packages/core/tests/expand.test.ts": 1116, - "packages/core/tests/expansion-identity.test.ts": 1121, - "packages/core/tests/expansion-metadata.test.ts": 304, - "packages/core/tests/expression-props.test.ts": 800, - "packages/core/tests/fail-component.test.ts": 687, - "packages/core/tests/failure-printing.test.ts": 627, - "packages/core/tests/fatal-cause.test.ts": 174, - "packages/core/tests/fetch-component.test.ts": 1566, - "packages/core/tests/file-component.test.ts": 2428, - "packages/core/tests/file-delete-component.test.ts": 581, - "packages/core/tests/files-fatal.test.ts": 1402, - "packages/core/tests/foreground-exec.test.ts": 1136, - "packages/core/tests/frontmatter.test.ts": 44, - "packages/core/tests/function-components.test.ts": 858, - "packages/core/tests/generated-composition.test.ts": 1049, - "packages/core/tests/generated-xmd.test.ts": 2201, - "packages/core/tests/glob-api.test.ts": 94, - "packages/core/tests/glob-component.test.ts": 1141, - "packages/core/tests/guarded-journal.test.ts": 411, - "packages/core/tests/has-content.test.ts": 241, - "packages/core/tests/heal.test.ts": 225, - "packages/core/tests/if.test.ts": 730, - "packages/core/tests/inline-root.test.ts": 453, - "packages/core/tests/invocation-failures.test.ts": 97, - "packages/core/tests/invocation-identity.test.ts": 722, - "packages/core/tests/invocation-scope.test.ts": 491, - "packages/core/tests/journal-source-position.test.ts": 537, - "packages/core/tests/json-component.test.ts": 2648, - "packages/core/tests/json.test.ts": 29, - "packages/core/tests/let.test.ts": 436, - "packages/core/tests/loop.test.ts": 1126, - "packages/core/tests/named-slots.test.ts": 1181, - "packages/core/tests/output-error-mode.test.ts": 1468, - "packages/core/tests/output-normalize.test.ts": 36, - "packages/core/tests/output-terminal.test.ts": 143, - "packages/core/tests/parse-components.test.ts": 523, - "packages/core/tests/plan-response.test.ts": 76, - "packages/core/tests/plugin-api.test.ts": 40, - "packages/core/tests/plugin.test.ts": 170, - "packages/core/tests/props-binding.test.ts": 505, - "packages/core/tests/protected-content.test.ts": 44, - "packages/core/tests/registered-printed-errors.test.ts": 191, - "packages/core/tests/replay-stream.test.ts": 31, - "packages/core/tests/retain.test.ts": 428, - "packages/core/tests/root-composition.test.ts": 509, - "packages/core/tests/root-props.test.ts": 527, - "packages/core/tests/root-provider.test.ts": 524, - "packages/core/tests/sample-component.test.ts": 1067, - "packages/core/tests/scanner.test.ts": 69, - "packages/core/tests/scope-local.test.ts": 31, - "packages/core/tests/secret-detection.test.ts": 1139, - "packages/core/tests/secret-files.test.ts": 109, - "packages/core/tests/secret-rules.test.ts": 149, - "packages/core/tests/secret-scanner-baseline.test.ts": 139, - "packages/core/tests/secret-scanner.test.ts": 99, - "packages/core/tests/source-position.test.ts": 307, - "packages/core/tests/state-ownership.test.ts": 361, - "packages/core/tests/streaming-emission.test.ts": 284, - "packages/core/tests/switch.test.ts": 765, - "packages/core/tests/syntax-catalog.test.ts": 830, - "packages/core/tests/syntax-component.test.ts": 1646, - "packages/core/tests/temp-dir.test.ts": 2764, - "packages/core/tests/temp-file-compiler.test.ts": 67, - "packages/core/tests/text-interpolation.test.ts": 216, - "packages/core/tests/try-content.test.ts": 250, - "packages/core/tests/unused-in-diff.test.ts": 458, - "packages/core/tests/validate.test.ts": 296, - "packages/core/tests/validation-integration.test.ts": 360, - "packages/core/tests/workflow-component-bundle.test.ts": 690, - "packages/durable-streams/tests/context.test.ts": 32, - "packages/durable-streams/tests/deterministic-id.test.ts": 64, - "packages/durable-streams/tests/divergence-api.test.ts": 40, - "packages/durable-streams/tests/divergence.test.ts": 51, - "packages/durable-streams/tests/durable-each.test.ts": 57, - "packages/durable-streams/tests/durable-position.test.ts": 46, - "packages/durable-streams/tests/durable-run.test.ts": 60, - "packages/durable-streams/tests/ephemeral.test.ts": 53, - "packages/durable-streams/tests/fail-stop.test.ts": 65, - "packages/durable-streams/tests/guard-stream.test.ts": 103, - "packages/durable-streams/tests/live-coordinator.test.ts": 54, - "packages/durable-streams/tests/parse.test.ts": 43, - "packages/durable-streams/tests/replay-guard.test.ts": 65, - "packages/durable-streams/tests/replay-index.test.ts": 39, - "packages/durable-streams/tests/retained.test.ts": 43, - "packages/durable-streams/tests/serialize-event.test.ts": 35, - "packages/durable-streams/tests/smoke.test.ts": 30, - "packages/durable-streams/tests/structured-concurrency.test.ts": 79, - "packages/durable-streams/tests/terminal-boundary.test.ts": 71, - "packages/durable-streams/tests/types.test.ts": 24, - "packages/git/tests/api-entrypoint.test.ts": 280, - "packages/git/tests/git-host-effect.test.ts": 6090, - "packages/git/tests/git.test.ts": 347, - "packages/git/tests/github-activation.test.ts": 646, - "packages/git/tests/github-issues.test.ts": 283, - "packages/git/tests/github-pull-requests.test.ts": 76, - "packages/git/tests/issue-markdown.test.ts": 8192, - "packages/git/tests/issue-records.test.ts": 267, - "packages/git/tests/module-partition.test.ts": 1580, - "packages/git/tests/plugin.test.ts": 746, - "packages/git/tests/provider-neutrality.test.ts": 984, - "packages/git/tests/pull-request-records.test.ts": 297, - "packages/git/tests/run-composition-lazy.test.ts": 732, - "packages/git/tests/selection-authentication.test.ts": 75, - "packages/runtime/tests/duration.test.ts": 54, - "packages/runtime/tests/fetch.test.ts": 617, - "packages/runtime/tests/fs.test.ts": 65, - "packages/runtime/tests/host-files.test.ts": 725, - "packages/runtime/tests/native-launcher.test.ts": 2395, - "packages/runtime/tests/process.test.ts": 81, - "packages/runtime/tests/service.test.ts": 57, - "packages/test-agent/tests/acp-server.test.ts": 95, - "packages/test-agent/tests/behavior-engine.test.ts": 627, - "packages/test-agent/tests/bridge.test.ts": 63, - "packages/test-agent/tests/command-lazy.test.ts": 3054, - "packages/test-agent/tests/components.test.ts": 49393, - "packages/test-agent/tests/controller.test.ts": 192, - "packages/test-agent/tests/cross-package-resolution.test.ts": 1739, - "packages/test-agent/tests/native-launch.test.ts": 22544, - "packages/test-agent/tests/net.test.ts": 89, - "packages/test-agent/tests/profile.test.ts": 288, - "packages/test-agent/tests/protocol.test.ts": 52, - "packages/test-agent/tests/provider.test.ts": 381, - "packages/test-agent/tests/public-api.test.ts": 446, - "packages/test-agent/tests/route-slot.test.ts": 54, - "packages/test-agent/tests/smoke.test.ts": 14887, - "packages/test-agent/tests/template.test.ts": 297, - "packages/test-agent/tests/worker-lifecycle.test.ts": 8501, - "packages/test-support/tests/journal.test.ts": 38, - "packages/testing/tests/agent-composition.test.ts": 574, - "packages/testing/tests/assert-throws.test.ts": 2186, - "packages/testing/tests/assert.test.ts": 51, - "packages/testing/tests/assertions.test.ts": 2973, - "packages/testing/tests/boundary-contract.test.ts": 1553, - "packages/testing/tests/cli.test.ts": 4523, - "packages/testing/tests/execution-harness.test.ts": 6276, - "packages/testing/tests/replay.test.ts": 2131, - "packages/testing/tests/smoke.test.ts": 1803, - "packages/testing/tests/test-component.test.ts": 2875, - "packages/testing/tests/testing-activation.test.ts": 2504, - "packages/testing/tests/testing-boundary.test.ts": 1056, - "packages/testing/tests/testing-mode.test.ts": 3089, - "packages/testing/tests/use-testing.test.ts": 1818, - "packages/web/tests/assets.test.ts": 27, - "packages/web/tests/client-logic.test.ts": 32, - "packages/web/tests/compile.test.ts": 866, - "packages/web/tests/component.test.ts": 1393, - "packages/web/tests/declaration.test.ts": 624, - "packages/web/tests/document.test.ts": 724, - "packages/web/tests/elicitation.test.ts": 502, - "packages/web/tests/live-form.test.ts": 457, + "packages/acp/tests/acceptance.test.ts": 848, + "packages/acp/tests/acpx-checkpoint-meta.test.ts": 4324, + "packages/acp/tests/acpx-materialization.test.ts": 6530, + "packages/acp/tests/acpx-transient-env.test.ts": 278, + "packages/acp/tests/adapter-materialization.test.ts": 72466, + "packages/acp/tests/adapter-protocol.test.ts": 54696, + "packages/acp/tests/codex-surfaces.test.ts": 128, + "packages/acp/tests/native-launch.test.ts": 1014, + "packages/acp/tests/permission-bridge.test.ts": 376, + "packages/acp/tests/provider.test.ts": 1144, + "packages/acp/tests/serial-queue.test.ts": 113, + "packages/acp/tests/session-key.test.ts": 66, + "packages/acp/tests/terminal-screen.test.ts": 92, + "packages/cli/tests/agent-adapters.test.ts": 1259, + "packages/cli/tests/agent-cli.test.ts": 21242, + "packages/cli/tests/agent-config.test.ts": 31, + "packages/cli/tests/agent-options-cli.test.ts": 8066, + "packages/cli/tests/agent-options.test.ts": 34, + "packages/cli/tests/agent-session-coordinator.test.ts": 395, + "packages/cli/tests/cli-help.test.ts": 16491, + "packages/cli/tests/command.test.ts": 8086, + "packages/cli/tests/document-suites/inline/inline-markdown.test.ts": 3158, + "packages/cli/tests/document-suites/plan/plan-markdown.test.ts": 108257, + "packages/cli/tests/document-suites/props/props-markdown.test.ts": 2354, + "packages/cli/tests/document-suites/syntax/syntax-markdown.test.ts": 5383, + "packages/cli/tests/document-suites/targets/targets-markdown.test.ts": 4959, + "packages/cli/tests/document-suites/verbose/verbose-markdown.test.ts": 9210, + "packages/cli/tests/evaluate-component.test.ts": 8317, + "packages/cli/tests/fetch-cli.test.ts": 2541, + "packages/cli/tests/file-stream.test.ts": 60, + "packages/cli/tests/inline-cli.test.ts": 34088, + "packages/cli/tests/launch-timeout.test.ts": 10106, + "packages/cli/tests/packaged-document.test.ts": 496, + "packages/cli/tests/plan-args.test.ts": 40, + "packages/cli/tests/plan-cli.test.ts": 60861, + "packages/cli/tests/plan-command-document.test.ts": 5509, + "packages/cli/tests/plan-component.test.ts": 9021, + "packages/cli/tests/plan-host-acts.test.ts": 1232, + "packages/cli/tests/plan.test.ts": 12888, + "packages/cli/tests/plugin-cli.test.ts": 40868, + "packages/cli/tests/plugin-host.test.ts": 2176, + "packages/cli/tests/plugin-modules.test.ts": 39, + "packages/cli/tests/plugin-selection.test.ts": 38, + "packages/cli/tests/process-retention.test.ts": 2572, + "packages/cli/tests/props-cli.test.ts": 29734, + "packages/cli/tests/props-schema.test.ts": 441, + "packages/cli/tests/props-sources.test.ts": 145, + "packages/cli/tests/repl-boundaries.test.ts": 1250, + "packages/cli/tests/repl-composition.test.ts": 148, + "packages/cli/tests/repl-execution.test.ts": 2066, + "packages/cli/tests/repl-journey.test.ts": 10452, + "packages/cli/tests/repl-model.test.ts": 1788, + "packages/cli/tests/repl-route.test.ts": 1362, + "packages/cli/tests/repl-terminal.test.ts": 870, + "packages/cli/tests/run-composition.test.ts": 3267, + "packages/cli/tests/run-deadline.test.ts": 2464, + "packages/cli/tests/run-profile.test.ts": 329, + "packages/cli/tests/run-timeouts.test.ts": 11631, + "packages/cli/tests/secret-detection-cli.test.ts": 18885, + "packages/cli/tests/service-document.test.ts": 1561, + "packages/cli/tests/service-host.test.ts": 1429, + "packages/cli/tests/session-launch-cli.test.ts": 5781, + "packages/cli/tests/stdin-cli.test.ts": 32151, + "packages/cli/tests/stdout-delivery.test.ts": 53, + "packages/cli/tests/syntax-cli.test.ts": 34163, + "packages/cli/tests/targets-cli.test.ts": 22514, + "packages/cli/tests/test-root-profile.test.ts": 3779, + "packages/cli/tests/test-target.test.ts": 38358, + "packages/cli/tests/testing-activation.test.ts": 623, + "packages/cli/tests/testing-execution-host.test.ts": 51169, + "packages/cli/tests/upgrade-cli.test.ts": 19117, + "packages/cli/tests/upgrade-command-document.test.ts": 3715, + "packages/cli/tests/upgrade-output.test.ts": 849, + "packages/cli/tests/value-root.test.ts": 16527, + "packages/cli/tests/verbose-component.test.ts": 871, + "packages/cli/tests/workflow-declaration.test.ts": 586, + "packages/cli/tests/workflow-host.test.ts": 4412, + "packages/code-review-agent/tests/doctor.test.ts": 30, + "packages/code-review-agent/tests/parse-diagnostics.test.ts": 44, + "packages/code-review-agent/tests/parse-diff.test.ts": 40, + "packages/code-review-agent/tests/parse-doctor.test.ts": 305, + "packages/code-review-agent/tests/parse-oxlint.test.ts": 31, + "packages/code-review-agent/tests/policy.test.ts": 32, + "packages/core/tests/agent-api.test.ts": 39, + "packages/core/tests/agent-components.test.ts": 1350, + "packages/core/tests/agent-function-components.test.ts": 3753, + "packages/core/tests/agent-session-launch.test.ts": 3650, + "packages/core/tests/agent-session-placement.test.ts": 38, + "packages/core/tests/agent-session-use.test.ts": 32, + "packages/core/tests/agent-stack.test.ts": 739, + "packages/core/tests/answer-identity.test.ts": 44, + "packages/core/tests/answers-component.test.ts": 1492, + "packages/core/tests/answers-expansion-recursion.test.ts": 342, + "packages/core/tests/capture-errors.test.ts": 622, + "packages/core/tests/capture-props.test.ts": 353, + "packages/core/tests/cli-journal.test.ts": 10935, + "packages/core/tests/code-block-component.test.ts": 2418, + "packages/core/tests/compiler-boundary.test.ts": 350, + "packages/core/tests/component-api.test.ts": 57, + "packages/core/tests/component-registration.test.ts": 1189, + "packages/core/tests/component-returns.test.ts": 1729, + "packages/core/tests/component-schema-conformance.test.ts": 588, + "packages/core/tests/config-api.test.ts": 353, + "packages/core/tests/construct-error-observation.test.ts": 755, + "packages/core/tests/contextual-cwd.test.ts": 1556, + "packages/core/tests/daemon-integration.test.ts": 1057, + "packages/core/tests/daemon.test.ts": 72, + "packages/core/tests/declared-markdown-component.test.ts": 2229, + "packages/core/tests/document-output-api.test.ts": 39, + "packages/core/tests/document-target-execution.test.ts": 3709, + "packages/core/tests/document-targets.test.ts": 460, + "packages/core/tests/document-validation.test.ts": 2283, + "packages/core/tests/documentation-index.test.ts": 168, + "packages/core/tests/each.test.ts": 444, + "packages/core/tests/elicit-component.test.ts": 1213, + "packages/core/tests/ephemeral-service.test.ts": 862, + "packages/core/tests/eval-bindings.test.ts": 438, + "packages/core/tests/eval-context.test.ts": 94, + "packages/core/tests/eval-durable.test.ts": 456, + "packages/core/tests/eval-error-mode.test.ts": 722, + "packages/core/tests/eval-interpolate.test.ts": 38, + "packages/core/tests/eval-middleware.test.ts": 87, + "packages/core/tests/eval-persist.test.ts": 525, + "packages/core/tests/eval-return.test.ts": 446, + "packages/core/tests/eval-scope.test.ts": 354, + "packages/core/tests/eval-timeout.test.ts": 461, + "packages/core/tests/eval-transform.test.ts": 67, + "packages/core/tests/evaluate-component.test.ts": 2725, + "packages/core/tests/evaluate-loaded-copy.test.ts": 543, + "packages/core/tests/evaluate-provider-lifetime.test.ts": 677, + "packages/core/tests/evaluation-profile.test.ts": 197, + "packages/core/tests/exec-timeout.test.ts": 522, + "packages/core/tests/execute.test.ts": 1101, + "packages/core/tests/execution-protocol.test.ts": 1668, + "packages/core/tests/expand.test.ts": 1284, + "packages/core/tests/expansion-identity.test.ts": 1262, + "packages/core/tests/expansion-metadata.test.ts": 354, + "packages/core/tests/expression-props.test.ts": 904, + "packages/core/tests/fail-component.test.ts": 778, + "packages/core/tests/failure-printing.test.ts": 712, + "packages/core/tests/fatal-cause.test.ts": 195, + "packages/core/tests/fetch-component.test.ts": 1808, + "packages/core/tests/file-component.test.ts": 2448, + "packages/core/tests/file-delete-component.test.ts": 655, + "packages/core/tests/files-fatal.test.ts": 1553, + "packages/core/tests/foreground-exec.test.ts": 1280, + "packages/core/tests/frontmatter.test.ts": 48, + "packages/core/tests/function-components.test.ts": 953, + "packages/core/tests/generated-composition.test.ts": 1238, + "packages/core/tests/generated-xmd.test.ts": 2487, + "packages/core/tests/glob-api.test.ts": 102, + "packages/core/tests/glob-component.test.ts": 1268, + "packages/core/tests/guarded-journal.test.ts": 456, + "packages/core/tests/has-content.test.ts": 283, + "packages/core/tests/heal.test.ts": 267, + "packages/core/tests/if.test.ts": 859, + "packages/core/tests/inline-root.test.ts": 525, + "packages/core/tests/invocation-failures.test.ts": 107, + "packages/core/tests/invocation-identity.test.ts": 833, + "packages/core/tests/invocation-scope.test.ts": 551, + "packages/core/tests/journal-source-position.test.ts": 564, + "packages/core/tests/json-component.test.ts": 2397, + "packages/core/tests/json.test.ts": 34, + "packages/core/tests/let.test.ts": 431, + "packages/core/tests/loop.test.ts": 1360, + "packages/core/tests/named-slots.test.ts": 1386, + "packages/core/tests/output-error-mode.test.ts": 1660, + "packages/core/tests/output-normalize.test.ts": 42, + "packages/core/tests/output-terminal.test.ts": 161, + "packages/core/tests/parse-components.test.ts": 607, + "packages/core/tests/plan-response.test.ts": 88, + "packages/core/tests/plugin-api.test.ts": 49, + "packages/core/tests/plugin.test.ts": 192, + "packages/core/tests/props-binding.test.ts": 552, + "packages/core/tests/protected-content.test.ts": 55, + "packages/core/tests/registered-printed-errors.test.ts": 212, + "packages/core/tests/replay-stream.test.ts": 39, + "packages/core/tests/retain.test.ts": 482, + "packages/core/tests/root-composition.test.ts": 587, + "packages/core/tests/root-props.test.ts": 594, + "packages/core/tests/root-provider.test.ts": 682, + "packages/core/tests/sample-component.test.ts": 1135, + "packages/core/tests/scanner.test.ts": 79, + "packages/core/tests/scope-local.test.ts": 37, + "packages/core/tests/secret-detection.test.ts": 1155, + "packages/core/tests/secret-files.test.ts": 135, + "packages/core/tests/secret-rules.test.ts": 155, + "packages/core/tests/secret-scanner-baseline.test.ts": 148, + "packages/core/tests/secret-scanner.test.ts": 101, + "packages/core/tests/source-position.test.ts": 348, + "packages/core/tests/state-ownership.test.ts": 407, + "packages/core/tests/streaming-emission.test.ts": 326, + "packages/core/tests/switch.test.ts": 853, + "packages/core/tests/syntax-catalog.test.ts": 903, + "packages/core/tests/syntax-component.test.ts": 1824, + "packages/core/tests/temp-dir.test.ts": 2847, + "packages/core/tests/temp-file-compiler.test.ts": 81, + "packages/core/tests/text-interpolation.test.ts": 245, + "packages/core/tests/try-content.test.ts": 283, + "packages/core/tests/unused-in-diff.test.ts": 501, + "packages/core/tests/validate.test.ts": 332, + "packages/core/tests/validation-integration.test.ts": 378, + "packages/core/tests/workflow-component-bundle.test.ts": 736, + "packages/durable-streams/tests/context.test.ts": 40, + "packages/durable-streams/tests/deterministic-id.test.ts": 72, + "packages/durable-streams/tests/divergence-api.test.ts": 47, + "packages/durable-streams/tests/divergence.test.ts": 57, + "packages/durable-streams/tests/durable-each.test.ts": 71, + "packages/durable-streams/tests/durable-position.test.ts": 59, + "packages/durable-streams/tests/durable-run.test.ts": 65, + "packages/durable-streams/tests/ephemeral.test.ts": 63, + "packages/durable-streams/tests/fail-stop.test.ts": 80, + "packages/durable-streams/tests/guard-stream.test.ts": 108, + "packages/durable-streams/tests/live-coordinator.test.ts": 59, + "packages/durable-streams/tests/parse.test.ts": 50, + "packages/durable-streams/tests/replay-guard.test.ts": 75, + "packages/durable-streams/tests/replay-index.test.ts": 46, + "packages/durable-streams/tests/retained.test.ts": 103, + "packages/durable-streams/tests/serialize-event.test.ts": 52, + "packages/durable-streams/tests/smoke.test.ts": 34, + "packages/durable-streams/tests/structured-concurrency.test.ts": 97, + "packages/durable-streams/tests/terminal-boundary.test.ts": 82, + "packages/durable-streams/tests/types.test.ts": 29, + "packages/git/tests/api-entrypoint.test.ts": 311, + "packages/git/tests/git-host-effect.test.ts": 6642, + "packages/git/tests/git.test.ts": 375, + "packages/git/tests/github-activation.test.ts": 745, + "packages/git/tests/github-issues.test.ts": 326, + "packages/git/tests/github-pull-requests.test.ts": 83, + "packages/git/tests/issue-markdown.test.ts": 8625, + "packages/git/tests/issue-records.test.ts": 314, + "packages/git/tests/module-partition.test.ts": 1952, + "packages/git/tests/plugin.test.ts": 855, + "packages/git/tests/provider-neutrality.test.ts": 1105, + "packages/git/tests/pull-request-records.test.ts": 335, + "packages/git/tests/run-composition-lazy.test.ts": 827, + "packages/git/tests/selection-authentication.test.ts": 39, + "packages/runtime/tests/duration.test.ts": 30, + "packages/runtime/tests/fetch.test.ts": 613, + "packages/runtime/tests/fs.test.ts": 78, + "packages/runtime/tests/host-files.test.ts": 737, + "packages/runtime/tests/native-launcher.test.ts": 2414, + "packages/runtime/tests/process.test.ts": 89, + "packages/runtime/tests/service.test.ts": 67, + "packages/test-agent/tests/acp-server.test.ts": 109, + "packages/test-agent/tests/behavior-engine.test.ts": 695, + "packages/test-agent/tests/bridge.test.ts": 68, + "packages/test-agent/tests/command-lazy.test.ts": 3360, + "packages/test-agent/tests/components.test.ts": 53390, + "packages/test-agent/tests/controller.test.ts": 210, + "packages/test-agent/tests/cross-package-resolution.test.ts": 1966, + "packages/test-agent/tests/native-launch.test.ts": 24823, + "packages/test-agent/tests/net.test.ts": 109, + "packages/test-agent/tests/profile.test.ts": 305, + "packages/test-agent/tests/protocol.test.ts": 60, + "packages/test-agent/tests/provider.test.ts": 442, + "packages/test-agent/tests/public-api.test.ts": 483, + "packages/test-agent/tests/route-slot.test.ts": 59, + "packages/test-agent/tests/smoke.test.ts": 15697, + "packages/test-agent/tests/template.test.ts": 303, + "packages/test-agent/tests/worker-lifecycle.test.ts": 9536, + "packages/test-support/tests/journal.test.ts": 43, + "packages/testing/tests/agent-composition.test.ts": 629, + "packages/testing/tests/assert-throws.test.ts": 2381, + "packages/testing/tests/assert.test.ts": 58, + "packages/testing/tests/assertions.test.ts": 3069, + "packages/testing/tests/boundary-contract.test.ts": 1670, + "packages/testing/tests/cli.test.ts": 5153, + "packages/testing/tests/execution-harness.test.ts": 6520, + "packages/testing/tests/replay.test.ts": 2317, + "packages/testing/tests/smoke.test.ts": 1858, + "packages/testing/tests/test-component.test.ts": 3031, + "packages/testing/tests/testing-activation.test.ts": 2654, + "packages/testing/tests/testing-boundary.test.ts": 1131, + "packages/testing/tests/testing-mode.test.ts": 3241, + "packages/testing/tests/use-testing.test.ts": 1882, + "packages/web/tests/assets.test.ts": 33, + "packages/web/tests/client-logic.test.ts": 38, + "packages/web/tests/compile.test.ts": 900, + "packages/web/tests/component.test.ts": 1465, + "packages/web/tests/declaration.test.ts": 687, + "packages/web/tests/document.test.ts": 794, + "packages/web/tests/elicitation.test.ts": 560, + "packages/web/tests/live-form.test.ts": 508, "packages/web/tests/markdown.test.ts": 135, - "packages/web/tests/opener.test.ts": 475, - "packages/web/tests/page.test.ts": 30, - "packages/web/tests/responder.test.ts": 785, - "packages/web/tests/server-lifecycle.test.ts": 607, - "packages/web/tests/server.test.ts": 990, - "packages/workflow/tests/generated-observations.test.ts": 519, - "packages/workflow/tests/retained-run.test.ts": 1181, - "packages/workflow/tests/service-denial.test.ts": 53, - "packages/workflow/tests/workflow-bundle.test.ts": 740, - "packages/workflow/tests/workflow-definition.test.ts": 321, - "packages/workflow/tests/workflow-fork.test.ts": 283, - "packages/workflow/tests/workflow-run.test.ts": 1000, - "packages/workflow/tests/workspace-effect.test.ts": 1310, - "scripts/tests/adapter-vendor.test.ts": 146, - "scripts/tests/bootstrap-npm-package.test.ts": 4873, - "scripts/tests/bump-version.test.ts": 73, - "scripts/tests/changed-paths.test.ts": 32, - "scripts/tests/ci-workflow.test.ts": 1616, - "scripts/tests/consumer-cycle.test.ts": 91, - "scripts/tests/filesystem-contract-workflow.test.ts": 67, - "scripts/tests/main-green.test.ts": 279, - "scripts/tests/main-health.test.ts": 101, - "scripts/tests/no-module-scoped-registry.test.ts": 753, - "scripts/tests/no-redundant-test-scope.test.ts": 4157, - "scripts/tests/no-sync-filesystem.test.ts": 3835, - "scripts/tests/no-yield-in-finally.test.ts": 733, - "scripts/tests/oxlint-policy.test.ts": 8746, - "scripts/tests/packaged-document.test.ts": 64, - "scripts/tests/prefer-effection-operation.test.ts": 5312, - "scripts/tests/prefer-effection-result.test.ts": 3540, - "scripts/tests/publish-workflow-membership.test.ts": 84, - "scripts/tests/review-infrastructure.test.ts": 1660, - "scripts/tests/runtime-exclusions.test.ts": 277, - "scripts/tests/runtime-tests.test.ts": 74, - "scripts/tests/scope-bound-event-registration.test.ts": 3871, - "scripts/tests/staged-write.test.ts": 68, - "scripts/tests/test-file-discovery.test.ts": 211, - "scripts/tests/test-shards.test.ts": 38, - "scripts/tests/test-weights.test.ts": 205, - "scripts/tests/tracked.test.ts": 34, - "scripts/tests/verify-coordinator.test.ts": 216, - "scripts/tests/version-lockstep.test.ts": 111, - "scripts/tests/web-client-module.test.ts": 53, - "scripts/tests/workspace.test.ts": 42 + "packages/web/tests/opener.test.ts": 505, + "packages/web/tests/page.test.ts": 34, + "packages/web/tests/responder.test.ts": 862, + "packages/web/tests/server-lifecycle.test.ts": 658, + "packages/web/tests/server.test.ts": 1077, + "packages/workflow/tests/generated-observations.test.ts": 540, + "packages/workflow/tests/retained-run.test.ts": 1212, + "packages/workflow/tests/service-denial.test.ts": 64, + "packages/workflow/tests/workflow-bundle.test.ts": 730, + "packages/workflow/tests/workflow-definition.test.ts": 359, + "packages/workflow/tests/workflow-fork.test.ts": 312, + "packages/workflow/tests/workflow-run.test.ts": 1068, + "packages/workflow/tests/workspace-effect.test.ts": 1523, + "scripts/tests/adapter-vendor.test.ts": 162, + "scripts/tests/bootstrap-npm-package.test.ts": 5570, + "scripts/tests/bump-version.test.ts": 88, + "scripts/tests/changed-paths.test.ts": 40, + "scripts/tests/ci-workflow.test.ts": 1939, + "scripts/tests/consumer-cycle.test.ts": 108, + "scripts/tests/filesystem-contract-workflow.test.ts": 76, + "scripts/tests/main-green.test.ts": 297, + "scripts/tests/main-health.test.ts": 104, + "scripts/tests/no-module-scoped-registry.test.ts": 891, + "scripts/tests/no-redundant-test-scope.test.ts": 4993, + "scripts/tests/no-sync-filesystem.test.ts": 4628, + "scripts/tests/no-yield-in-finally.test.ts": 883, + "scripts/tests/oxlint-policy.test.ts": 10564, + "scripts/tests/packaged-document.test.ts": 74, + "scripts/tests/prefer-effection-operation.test.ts": 6080, + "scripts/tests/prefer-effection-result.test.ts": 4219, + "scripts/tests/publish-workflow-membership.test.ts": 100, + "scripts/tests/review-infrastructure.test.ts": 1963, + "scripts/tests/runtime-exclusions.test.ts": 348, + "scripts/tests/runtime-tests.test.ts": 88, + "scripts/tests/scope-bound-event-registration.test.ts": 4633, + "scripts/tests/staged-write.test.ts": 71, + "scripts/tests/test-file-discovery.test.ts": 254, + "scripts/tests/test-shards.test.ts": 46, + "scripts/tests/test-weights.test.ts": 207, + "scripts/tests/tracked.test.ts": 38, + "scripts/tests/verify-coordinator.test.ts": 230, + "scripts/tests/version-lockstep.test.ts": 124, + "scripts/tests/web-client-module.test.ts": 63, + "scripts/tests/workspace.test.ts": 48 } } }