From 68ba73b9a18c557e18e133d9f7e05cc71f711bc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 29 Jun 2026 20:49:09 +0200 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20leveled=20response=20views=20+=20--?= =?UTF-8?q?level=20knob,=20with=20a=20snapshot=20digest=20=E2=80=94=20Phas?= =?UTF-8?q?e=204?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the agent-cost leveled-response system: a responseLevel knob (digest | default | full) plumbed end to end behind a global --level flag (mirroring --cost), and a per-command ResponseView registry applied in the router on the success path. - contracts: RESPONSE_LEVELS/ResponseLevel + meta.responseLevel + boundary schema whitelist. Plumbing mirrors --cost: cli-flags FlagDefinition + GLOBAL_FLAG_KEYS, AgentDeviceClientConfig + overrides, buildClientConfig, buildMeta. ResponseLevel exported from the public root. - src/daemon/response-views.ts: the ResponseView registry. Seeds the snapshot digest — the full node tree (the dominant token sink) collapses to { nodeCount, refs: first 12 hittable/non-occluded refs with labels } plus the cheap top-level signals (truncated/visibility/snapshotQuality). full returns today's shape (nothing richer is computed yet). - router graft (applyResponseLevelView + applyAgentCostGrafts): composes with the existing cost block. With responseLevel default (or unset) AND no registered view AND no --cost, the original response is returned UNCHANGED — byte-identical to today (Maestro .ad recompare safe). cost.nodeCount reads the original node tree so it stays accurate even after a digest. Tests: snapshot view unit test (digest filters hittable/occluded, drops the tree, keeps cheap signals; default/full passthrough); router graft test via an injected view (default identity byte-identical, digest applies, full passthrough, digest+cost composition, unregistered-command passthrough, boundary parse). Verified: tsc, oxfmt + oxlint --deny-warnings, fallow audit clean, rslib build, Layering Guard empty, 1106 daemon/contracts/client tests pass (incl. the existing cost/typed-error grafts after the restructure). --- src/cli.ts | 1 + src/client-normalizers.ts | 1 + src/client-types.ts | 3 + src/contracts.ts | 8 + .../request-router-response-level.test.ts | 153 ++++++++++++++++++ src/daemon/__tests__/response-views.test.ts | 49 ++++++ src/daemon/request-router.ts | 65 +++++--- src/daemon/response-views.ts | 41 +++++ src/index.ts | 1 + src/utils/cli-flags.ts | 29 +++- 10 files changed, 325 insertions(+), 26 deletions(-) create mode 100644 src/daemon/__tests__/request-router-response-level.test.ts create mode 100644 src/daemon/__tests__/response-views.test.ts create mode 100644 src/daemon/response-views.ts diff --git a/src/cli.ts b/src/cli.ts index dedfd3a18..7d3df3e41 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -253,6 +253,7 @@ export async function runCli(argv: string[], deps: CliDeps = DEFAULT_CLI_DEPS): cwd: process.cwd(), debug: debugOutputEnabled, cost: currentFlags.cost, + responseLevel: currentFlags.responseLevel, }); let parsedBatchSteps: BatchStep[] | undefined; if (command === 'batch') { diff --git a/src/client-normalizers.ts b/src/client-normalizers.ts index 867373545..caf8d77ac 100644 --- a/src/client-normalizers.ts +++ b/src/client-normalizers.ts @@ -352,6 +352,7 @@ export function buildMeta(options: InternalRequestOptions): DaemonRequest['meta' sessionExplicit: options.session !== undefined, debug: options.debug, includeCost: options.cost, + responseLevel: options.responseLevel, lockPolicy: options.lockPolicy, lockPlatform: options.lockPlatform, ...leaseScopeToRequestMeta(leaseScope), diff --git a/src/client-types.ts b/src/client-types.ts index 4ff970547..853e8c79c 100644 --- a/src/client-types.ts +++ b/src/client-types.ts @@ -8,6 +8,7 @@ import type { DaemonResponse, LeaseBackend, NetworkIncludeMode, + ResponseLevel, SessionIsolationMode, SessionRuntimeHints, } from './contracts.ts'; @@ -79,6 +80,7 @@ export type AgentDeviceClientConfig = RemoteConnectionProfileFields & { cwd?: string; debug?: boolean; cost?: boolean; + responseLevel?: ResponseLevel; iosXctestrunFile?: string; iosXctestDerivedDataPath?: string; iosXctestEnvDir?: string; @@ -106,6 +108,7 @@ export type AgentDeviceRequestOverrides = Pick< | 'cwd' | 'debug' | 'cost' + | 'responseLevel' | 'iosXctestrunFile' | 'iosXctestDerivedDataPath' | 'iosXctestEnvDir' diff --git a/src/contracts.ts b/src/contracts.ts index 1f88e2455..4209de839 100644 --- a/src/contracts.ts +++ b/src/contracts.ts @@ -58,10 +58,17 @@ export type SessionIsolationMode = (typeof SESSION_ISOLATION_MODES)[number]; export const NETWORK_INCLUDE_MODES = ['summary', 'headers', 'body', 'all'] as const; export type NetworkIncludeMode = (typeof NETWORK_INCLUDE_MODES)[number]; +// Agent-cost leveled response views (Phase 4). `default` == today's exact wire +// shape (Maestro `.ad` recompare safe); `digest` is a token-cheap view; `full` +// is the richest view (== default until a command surfaces extra detail). +export const RESPONSE_LEVELS = ['digest', 'default', 'full'] as const; +export type ResponseLevel = (typeof RESPONSE_LEVELS)[number]; + export type DaemonRequestMeta = { requestId?: string; debug?: boolean; includeCost?: boolean; + responseLevel?: ResponseLevel; cwd?: string; sessionExplicit?: boolean; tenantId?: string; @@ -447,6 +454,7 @@ export const daemonCommandRequestSchema = schema((input, path) => requestId: optionalString(meta, 'requestId', `${path}.meta`), debug: optionalBoolean(meta, 'debug', `${path}.meta`), includeCost: optionalBoolean(meta, 'includeCost', `${path}.meta`), + responseLevel: optionalEnum(meta, 'responseLevel', RESPONSE_LEVELS, `${path}.meta`), cwd: optionalString(meta, 'cwd', `${path}.meta`), sessionExplicit: optionalBoolean(meta, 'sessionExplicit', `${path}.meta`), tenantId: optionalString(meta, 'tenantId', `${path}.meta`), diff --git a/src/daemon/__tests__/request-router-response-level.test.ts b/src/daemon/__tests__/request-router-response-level.test.ts new file mode 100644 index 000000000..827855000 --- /dev/null +++ b/src/daemon/__tests__/request-router-response-level.test.ts @@ -0,0 +1,153 @@ +import { test, expect, vi, beforeEach } from 'vitest'; +import os from 'node:os'; +import path from 'node:path'; + +vi.mock('../../core/dispatch.ts', async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, dispatchCommand: vi.fn(async () => ({})) }; +}); + +vi.mock('../../platforms/ios/runner-client.ts', async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, stopIosRunnerSession: vi.fn(async () => {}) }; +}); + +vi.mock('../device-ready.ts', () => ({ ensureDeviceReady: vi.fn(async () => {}) })); + +// Register a test view on a command that flows through the (mocked) generic +// dispatch path, so the router graft mechanics can be exercised end to end +// without the real snapshot handler (the actual snapshot view is unit-tested in +// response-views.test.ts). +vi.mock('../response-views.ts', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + RESPONSE_VIEWS: { + ...actual.RESPONSE_VIEWS, + home: (data: Record, level: string) => + level === 'digest' ? { homeDigest: true, hadItems: Array.isArray(data.items) } : data, + }, + }; +}); + +import { dispatchCommand } from '../../core/dispatch.ts'; +import { createRequestHandler } from '../request-router.ts'; +import type { DaemonRequest, SessionState } from '../types.ts'; +import { LeaseRegistry } from '../lease-registry.ts'; +import { makeSessionStore } from '../../__tests__/test-utils/store-factory.ts'; +import { daemonCommandRequestSchema } from '../../contracts.ts'; + +const mockDispatch = vi.mocked(dispatchCommand); + +const REPRESENTATIVE_PAYLOAD = { message: 'home-ok', items: [1, 2, 3] } as const; + +function makeIosSession(name: string): SessionState { + return { + name, + createdAt: 1_700_000_000_000, + actions: [], + device: { + platform: 'ios', + target: 'mobile', + id: 'SIM-001', + name: 'iPhone 16', + kind: 'simulator', + booted: true, + simulatorSetPath: '/tmp/tenant-a/set', + }, + }; +} + +function makeHandler() { + const sessionStore = makeSessionStore('agent-device-router-level-'); + sessionStore.set('level-session', makeIosSession('level-session')); + return { + sessionStore, + handler: createRequestHandler({ + logPath: path.join(os.tmpdir(), 'daemon.log'), + token: 'test-token', + sessionStore, + leaseRegistry: new LeaseRegistry(), + trackDownloadableArtifact: () => 'artifact-id', + }), + }; +} + +function request(command: string, overrides: Partial = {}): DaemonRequest { + return { + token: 'test-token', + session: 'level-session', + command, + positionals: [], + flags: {}, + ...overrides, + }; +} + +beforeEach(() => { + mockDispatch.mockReset(); + mockDispatch.mockImplementation(async () => ({ ...REPRESENTATIVE_PAYLOAD })); +}); + +test('(a) default identity: responseLevel absent === default === no meta, byte-identical', async () => { + const { handler } = makeHandler(); + const noMeta = await handler(request('home')); + const emptyMeta = await handler(request('home', { meta: {} })); + const explicitDefault = await handler(request('home', { meta: { responseLevel: 'default' } })); + + expect(JSON.stringify(noMeta)).toBe(JSON.stringify(emptyMeta)); + expect(JSON.stringify(noMeta)).toBe(JSON.stringify(explicitDefault)); + if (noMeta.ok) expect(noMeta.data).toEqual(REPRESENTATIVE_PAYLOAD); +}); + +test('(b) digest applies the registered view, dropping the full payload', async () => { + const { handler } = makeHandler(); + const resp = await handler(request('home', { meta: { responseLevel: 'digest' } })); + expect(resp.ok).toBe(true); + if (!resp.ok) return; + expect(resp.data).toEqual({ homeDigest: true, hadItems: true }); + expect('message' in (resp.data ?? {})).toBe(false); +}); + +test('(c) full returns today’s shape (view passthrough) — byte-identical to default', async () => { + const { handler } = makeHandler(); + const full = await handler(request('home', { meta: { responseLevel: 'full' } })); + const def = await handler(request('home', { meta: { responseLevel: 'default' } })); + expect(JSON.stringify(full)).toBe(JSON.stringify(def)); +}); + +test('(d) digest composes with --cost: viewed data plus an additive cost block', async () => { + const { handler } = makeHandler(); + const resp = await handler( + request('home', { meta: { responseLevel: 'digest', includeCost: true } }), + ); + expect(resp.ok).toBe(true); + if (!resp.ok) return; + expect(resp.data).toMatchObject({ homeDigest: true, hadItems: true }); + expect(typeof resp.data?.cost?.wallClockMs).toBe('number'); + expect(resp.data?.cost?.runnerRoundTrips).toBe(0); +}); + +test('(e) digest on a command with no registered view is byte-identical to default', async () => { + const { handler } = makeHandler(); + const digest = await handler(request('back', { meta: { responseLevel: 'digest' } })); + const def = await handler(request('back', { meta: {} })); + expect(JSON.stringify(digest)).toBe(JSON.stringify(def)); + if (digest.ok) expect(digest.data).toEqual(REPRESENTATIVE_PAYLOAD); +}); + +test('(f) boundary survival: meta.responseLevel survives daemonCommandRequestSchema parsing', () => { + const parsed = daemonCommandRequestSchema.parse({ + command: 'snapshot', + positionals: [], + meta: { responseLevel: 'digest' }, + }); + expect(parsed.meta?.responseLevel).toBe('digest'); + + const parsedOff = daemonCommandRequestSchema.parse({ + command: 'snapshot', + positionals: [], + meta: {}, + }); + expect(parsedOff.meta?.responseLevel).toBeUndefined(); +}); diff --git a/src/daemon/__tests__/response-views.test.ts b/src/daemon/__tests__/response-views.test.ts new file mode 100644 index 000000000..e8d502a9c --- /dev/null +++ b/src/daemon/__tests__/response-views.test.ts @@ -0,0 +1,49 @@ +import { test, expect } from 'vitest'; +import { RESPONSE_VIEWS } from '../response-views.ts'; +import type { DaemonResponseData } from '../types.ts'; + +const snapshotView = RESPONSE_VIEWS.snapshot; + +const SNAPSHOT_DATA: DaemonResponseData = { + nodes: [ + { ref: 'e1', hittable: true, label: 'Login' }, + { ref: 'e2', hittable: false, label: 'Heading' }, // not hittable → excluded + { ref: 'e3', hittable: true, interactionBlocked: 'covered', label: 'Hidden' }, // occluded → excluded + { ref: 'e4', hittable: true, value: 'from-value' }, // label falls back to value + ], + truncated: false, + visibility: { partial: false, visibleNodeCount: 4, totalNodeCount: 4, reasons: [] }, + snapshotQuality: { state: 'healthy', backend: 'tree' }, + appName: 'Demo', // a non-cheap field that the digest intentionally drops +}; + +test('snapshot view is registered', () => { + expect(typeof snapshotView).toBe('function'); +}); + +test('digest collapses the node tree to count + actionable refs + cheap signals', () => { + const digest = snapshotView!(SNAPSHOT_DATA, 'digest'); + expect(digest).toEqual({ + nodeCount: 4, + refs: [ + { ref: 'e1', label: 'Login' }, + { ref: 'e4', label: 'from-value' }, + ], + truncated: false, + visibility: { partial: false, visibleNodeCount: 4, totalNodeCount: 4, reasons: [] }, + snapshotQuality: { state: 'healthy', backend: 'tree' }, + }); + // The full node tree (the token sink) and non-cheap fields are dropped. + expect('nodes' in digest).toBe(false); + expect('appName' in digest).toBe(false); +}); + +test('default and full return today’s shape unchanged (same reference)', () => { + expect(snapshotView!(SNAPSHOT_DATA, 'default')).toBe(SNAPSHOT_DATA); + expect(snapshotView!(SNAPSHOT_DATA, 'full')).toBe(SNAPSHOT_DATA); +}); + +test('digest tolerates missing/empty node trees', () => { + const digest = snapshotView!({ truncated: true }, 'digest'); + expect(digest).toMatchObject({ nodeCount: 0, refs: [], truncated: true }); +}); diff --git a/src/daemon/request-router.ts b/src/daemon/request-router.ts index bf0ed8cb3..a3b07d282 100644 --- a/src/daemon/request-router.ts +++ b/src/daemon/request-router.ts @@ -6,7 +6,8 @@ import { AppError, normalizeError, retriableForErrorCode } from '../kernel/error import { supportedPlatformsForCommand } from '../core/capabilities.ts'; import { timingSafeStringEqual } from '../utils/timing-safe-equal.ts'; import type { DaemonError, ResponseCost } from '../contracts.ts'; -import type { DaemonInvokeFn, DaemonRequest, DaemonResponse } from './types.ts'; +import type { DaemonInvokeFn, DaemonRequest, DaemonResponse, DaemonResponseData } from './types.ts'; +import { RESPONSE_VIEWS } from './response-views.ts'; import { SessionStore } from './session-store.ts'; import { noActiveSessionError } from './handlers/response.ts'; import { @@ -116,23 +117,9 @@ export function createRequestHandler(deps: RequestRouterDeps): DaemonInvokeFn { if (!response.ok) { return { ok: false, error: enrichDaemonError(req.command, response.error) }; } - // Phase 4 (agent-cost) graft: cost is purely additive and opt-in. With - // the flag off the serialized DaemonResponse is byte-identical to today - // (Maestro `.ad` recompare diffs it). Mirrors the conditional - // `registerDownloadableArtifacts` spread in request-finalization. Runs - // inside the diagnostics scope so it can read this request's accumulated - // runner-round-trip tally. - if (!req.meta?.includeCost) return response; - const cost: ResponseCost = { - wallClockMs: Date.now() - start, - runnerRoundTrips: countDiagnosticEventsByPhase(RUNNER_ROUND_TRIP_PHASES), - }; - // Generic, command-agnostic: only the node-tree commands (snapshot) put a - // `nodes` array on response.data, so this reads as a number there and is - // omitted everywhere else. - const nodes = response.data?.nodes; - if (Array.isArray(nodes)) cost.nodeCount = nodes.length; - return { ok: true, data: { ...(response.data ?? {}), cost } }; + // Phase 4 (agent-cost) grafts on the success path. Runs inside the + // diagnostics scope so cost can read this request's runner-round-trip tally. + return applyAgentCostGrafts(req, response, start); }, ); } @@ -339,3 +326,45 @@ function enrichDaemonError(command: string, error: DaemonError): DaemonError { ...(supportedOn !== undefined ? { supportedOn } : {}), }; } + +// Phase 4 (agent-cost) success-path grafts: a leveled response view and an +// opt-in cost block, both purely additive. With responseLevel `default` (or +// unset) AND no registered view AND no --cost, the original `response` object is +// returned unchanged — byte-identical to today (Maestro `.ad` recompare safe). +function applyAgentCostGrafts( + req: DaemonRequest, + response: Extract, + startedAt: number, +): DaemonResponse { + const viewed = applyResponseLevelView(req, response); + if (!req.meta?.includeCost) return viewed; + const cost = buildResponseCost(response.data, startedAt); + return { ok: true, data: { ...(viewed.data ?? {}), cost } }; +} + +// Returns the response untouched when responseLevel is `default` (or unset) or no +// view is registered for the command — preserving today's byte-exact wire shape. +function applyResponseLevelView( + req: DaemonRequest, + response: Extract, +): Extract { + const level = req.meta?.responseLevel ?? 'default'; + if (level === 'default') return response; + const view = RESPONSE_VIEWS[req.command]; + return view ? { ok: true, data: view(response.data ?? {}, level) } : response; +} + +function buildResponseCost( + originalData: DaemonResponseData | undefined, + startedAt: number, +): ResponseCost { + const cost: ResponseCost = { + wallClockMs: Date.now() - startedAt, + runnerRoundTrips: countDiagnosticEventsByPhase(RUNNER_ROUND_TRIP_PHASES), + }; + // nodeCount reads the ORIGINAL node tree (the digest view may have already + // collapsed `data.nodes`), so the count stays accurate. + const nodes = originalData?.nodes; + if (Array.isArray(nodes)) cost.nodeCount = nodes.length; + return cost; +} diff --git a/src/daemon/response-views.ts b/src/daemon/response-views.ts new file mode 100644 index 000000000..5e28fcfa3 --- /dev/null +++ b/src/daemon/response-views.ts @@ -0,0 +1,41 @@ +import type { ResponseLevel } from '../contracts.ts'; +import type { SnapshotNode } from '../kernel/snapshot.ts'; +import type { DaemonResponseData } from './types.ts'; + +/** + * Phase 4 leveled response views. A view maps a command's `default` result data + * to a leveled form. The router only calls a view when `responseLevel` is + * `digest` or `full` AND a view is registered — so `default` (and every + * unregistered command) is byte-identical to today (Maestro `.ad` recompare + * safe). Views are pure functions of the default `data`. + */ +export type ResponseView = (data: DaemonResponseData, level: ResponseLevel) => DaemonResponseData; + +const DIGEST_REF_LIMIT = 12; + +/** + * Token-cheap snapshot digest: the node count plus the first N actionable refs + * (hittable and not occluded) with a label, and the cheap top-level signals + * (`truncated`, `visibility`, `snapshotQuality`). The full node tree — the + * dominant token sink — is dropped. `full` returns today's shape unchanged + * (nothing richer is computed yet). + */ +function snapshotView(data: DaemonResponseData, level: ResponseLevel): DaemonResponseData { + if (level !== 'digest') return data; + const nodes = Array.isArray(data.nodes) ? (data.nodes as SnapshotNode[]) : []; + const refs = nodes + .filter((node) => node.hittable === true && node.interactionBlocked !== 'covered') + .slice(0, DIGEST_REF_LIMIT) + .map((node) => ({ ref: node.ref, label: node.label ?? node.value ?? node.identifier })); + return { + nodeCount: nodes.length, + refs, + truncated: data.truncated, + ...(data.visibility !== undefined ? { visibility: data.visibility } : {}), + ...(data.snapshotQuality !== undefined ? { snapshotQuality: data.snapshotQuality } : {}), + }; +} + +export const RESPONSE_VIEWS: Record = { + snapshot: snapshotView, +}; diff --git a/src/index.ts b/src/index.ts index bd3ad2dc5..f3109534b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,7 @@ export type { } from './cli-test-reporters/types.ts'; export type { CommandResult } from './core/command-descriptor/command-result.ts'; +export type { ResponseLevel } from './contracts.ts'; export type { BootCommandResult, ShutdownCommandResult } from './contracts/device.ts'; export type { ViewportCommandResult } from './contracts/viewport.ts'; diff --git a/src/utils/cli-flags.ts b/src/utils/cli-flags.ts index 7a9209e9d..6245ef41e 100644 --- a/src/utils/cli-flags.ts +++ b/src/utils/cli-flags.ts @@ -4,14 +4,16 @@ import type { BackMode } from '../core/back-mode.ts'; import type { ClickButton } from '../core/click-button.ts'; import type { SwipePattern } from '../core/scroll-gesture.ts'; import { PLATFORM_SELECTORS, type DeviceTarget, type PlatformSelector } from '../kernel/device.ts'; -import type { - DaemonInstallSource, - DaemonServerMode, - DaemonTransportPreference, - LeaseBackend, - NetworkIncludeMode, - SessionRuntimeHints, - SessionIsolationMode, +import { + type DaemonInstallSource, + type DaemonServerMode, + type DaemonTransportPreference, + type LeaseBackend, + type NetworkIncludeMode, + RESPONSE_LEVELS, + type ResponseLevel, + type SessionRuntimeHints, + type SessionIsolationMode, } from '../contracts.ts'; import type { RemoteConfigMetroOptions } from '../remote-config-schema.ts'; import { @@ -66,6 +68,7 @@ export type CliFlags = RemoteConfigMetroOptions & launchUrl?: string; verbose?: boolean; cost?: boolean; + responseLevel?: ResponseLevel; snapshotInteractiveOnly?: boolean; snapshotDiff?: boolean; snapshotDepth?: number; @@ -784,6 +787,15 @@ const FLAG_DEFINITIONS: readonly FlagDefinition[] = [ usageLabel: '--cost', usageDescription: 'Include per-command wall-clock latency (cost.wallClockMs) in the response', }, + { + key: 'responseLevel', + names: ['--level'], + type: 'enum', + enumValues: RESPONSE_LEVELS, + usageLabel: '--level digest|default|full', + usageDescription: + 'Response detail level: digest (token-cheap), default (today), or full. Default keeps the wire shape unchanged.', + }, { key: 'json', names: ['--json'], @@ -1156,6 +1168,7 @@ export const GLOBAL_FLAG_KEYS = new Set([ 'version', 'verbose', 'cost', + 'responseLevel', ]); const flagDefinitionByName = new Map(); From 744574ebeccec6715335b9478a0e0dc50b70432a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Tue, 30 Jun 2026 07:31:25 +0200 Subject: [PATCH 2/3] fix: repoint MCP output-schemas import to kernel/device (rebase fixup) The kernel move (#940) deleted src/utils/device.ts; #941's command-output-schemas.ts (merged after #940's codemod ran) still imported the old path. Same one-line fix as #943; de-dups once that lands. --- src/mcp/command-output-schemas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mcp/command-output-schemas.ts b/src/mcp/command-output-schemas.ts index cc81ad949..3507a895c 100644 --- a/src/mcp/command-output-schemas.ts +++ b/src/mcp/command-output-schemas.ts @@ -4,7 +4,7 @@ import { booleanSchema, looseObjectSchema, stringSchema } from '../commands/comm import { BACK_MODES } from '../core/back-mode.ts'; import { DEVICE_ROTATIONS } from '../core/device-rotation.ts'; import { SESSION_SURFACES } from '../core/session-surface.ts'; -import { DEVICE_TARGETS, PLATFORMS } from '../utils/device.ts'; +import { DEVICE_TARGETS, PLATFORMS } from '../kernel/device.ts'; /** * Hand-authored registry of per-command MCP `outputSchema`s, keyed by the daemon From d7d2c7746908e66195ce1a8d4b07ba6ef18764fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Tue, 30 Jun 2026 07:45:13 +0200 Subject: [PATCH 3/3] fix: re-classify responseLevel flag in integration-progress model The --level/responseLevel flag is a diagnostics/output flag (not device- observable), classified in the exclusion bucket alongside --cost. (Lost in an earlier rebase; re-applying.) --- scripts/integration-progress-model.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/integration-progress-model.ts b/scripts/integration-progress-model.ts index b347e47bf..4a51a38ff 100644 --- a/scripts/integration-progress-model.ts +++ b/scripts/integration-progress-model.ts @@ -222,6 +222,7 @@ function summarizeProviderScenarioFlagExclusions() { 'version', 'verbose', 'cost', + 'responseLevel', ], }, {