From a403cc57170514db95722ce7926aec5515f92453 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Mon, 28 Sep 2026 07:26:03 -0400 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20Retain=20a=20safe=20Agent=20per?= =?UTF-8?q?mission=20audit=20and=20project=20Agent=20conversations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A completed Prompt now retains a closed account of the permission requests answered while its turn ran, and the REPL can read Agent turns and conversations out of a hostile Journal and filter them from its canonical location. Nothing live runs yet: no Agent provider reaches `xmd repl`, no observer, no pending request, no form and no new command option. The audit is assembled field by named field from the request as it arrives and gains the decision when it is made, so `rawInput`, the Session, provider callbacks and errors cannot cross the durable boundary, and mutating what the caller kept afterwards changes nothing. Members are in arrival order rather than completion order, and a request that raised has no member. The list is closed: an option kind or outcome this build cannot read refuses the whole prompt record. Observing decides nothing — the installed policy still answers. The projection reads `agent_prompt` through core's own record parser, orders the chronology by the sequence each record states rather than by append order, groups conversations by the provider's session key alone, and leaves a turn that reached no provider in the chronology and in no conversation. Every value is copied out of the event graph and frozen, and one turn is one object however it is reached. `ReplRoute` gains `session`, canonically last in the query, and the Sessions-only `+permission` drawer, which carries no key. The resolver's `asking` boolean becomes one closed live-availability value, and a historical prefix ignores all of it. Slice A of #854. --- packages/cli/src/repl/application.ts | 55 +- packages/cli/src/repl/model.ts | 294 +++++++++- packages/cli/src/repl/route.ts | 258 ++++++++- packages/cli/tests/fixtures/repl/reference.ts | 178 +++++- packages/cli/tests/repl-boundaries.test.ts | 2 + packages/cli/tests/repl-journey.test.ts | 5 + packages/cli/tests/repl-model.test.ts | 337 ++++++++++++ packages/cli/tests/repl-route.test.ts | 253 ++++++++- .../core/src/agent/function-components.ts | 14 +- packages/core/src/agent/journal.ts | 317 ++++++++++- .../tests/agent-function-components.test.ts | 516 +++++++++++++++++- specs/acp-client-spec.md | 35 +- 12 files changed, 2179 insertions(+), 85 deletions(-) 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..014043646 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,11 @@ function build( }); } + // 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 +760,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 +882,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 +907,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..2555b930e 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,339 @@ 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, 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 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..e12c638a4 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,6 +614,7 @@ export function parsePromptRecord(value: unknown): PromptRecord | undefined { stopReason, text, error, + permissions, raised, checkpoint, } = value; @@ -333,6 +641,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..758b9c044 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); } @@ -1182,3 +1318,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 From fb2c5288ede2d12332b595ea5299d4cddb2b443b Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Mon, 28 Sep 2026 18:13:45 -0400 Subject: [PATCH 2/3] =?UTF-8?q?=E2=9A=A1=20Measure=20test=20weights=20at?= =?UTF-8?q?=20a403cc57?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The artifact of run 36417683621, unchanged. This slice adds one test file and changes what several others run, and a weight is only true of the runner that measured it. --- test-weights.json | 2164 +++++++++++++++++++++++---------------------- 1 file changed, 1085 insertions(+), 1079 deletions(-) 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 } } } From e017c67d4b2fc1f61e9e52f0589f6a9c301c930f Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Tue, 29 Sep 2026 09:04:41 -0400 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=90=9B=20Refuse=20ambiguous=20Prompt?= =?UTF-8?q?=20chronology?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cli/src/repl/model.ts | 10 ++++++++++ packages/cli/tests/repl-model.test.ts | 12 ++++++++++++ packages/core/src/agent/journal.ts | 7 ++++++- .../core/tests/agent-function-components.test.ts | 15 +++++++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/repl/model.ts b/packages/cli/src/repl/model.ts index 014043646..873a2b802 100644 --- a/packages/cli/src/repl/model.ts +++ b/packages/cli/src/repl/model.ts @@ -746,6 +746,16 @@ 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. diff --git a/packages/cli/tests/repl-model.test.ts b/packages/cli/tests/repl-model.test.ts index 2555b930e..b26e2c734 100644 --- a/packages/cli/tests/repl-model.test.ts +++ b/packages/cli/tests/repl-model.test.ts @@ -702,6 +702,8 @@ describe("REPL model: the Agent histories it refuses", () => { 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 }), ]) { @@ -715,6 +717,16 @@ describe("REPL model: the Agent histories it refuses", () => { 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; diff --git a/packages/core/src/agent/journal.ts b/packages/core/src/agent/journal.ts index e12c638a4..76d910d28 100644 --- a/packages/core/src/agent/journal.ts +++ b/packages/core/src/agent/journal.ts @@ -618,7 +618,12 @@ export function parsePromptRecord(value: unknown): PromptRecord | undefined { 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") { diff --git a/packages/core/tests/agent-function-components.test.ts b/packages/core/tests/agent-function-components.test.ts index 758b9c044..5c45dbdd8 100644 --- a/packages/core/tests/agent-function-components.test.ts +++ b/packages/core/tests/agent-function-components.test.ts @@ -994,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,