Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ Single-context repo. Read `CONTEXT.md` for domain language and testing/architect

## Routing
- Keep `src/daemon.ts` as a thin router.
- Keep command names and daemon routing groups centralized in `src/command-catalog.ts`; do not re-create command string sets in handlers or request policy modules.
- Keep command names centralized in `src/command-catalog.ts`; do not re-create command identity sets in handlers or request policy modules.
- Keep daemon routing and request-policy traits centralized in `src/daemon/daemon-command-registry.ts`; request modules should consume its predicates instead of recreating command string sets. See `docs/adr/0003-daemon-command-registry.md`.
- Keep command input/output contracts in the command modules:
- command surface and shared schemas: `src/commands/command-surface.ts`, `src/commands/command-contract.ts`, `src/commands/command-input.ts`
- typed client command execution: `src/commands/client-command-contracts.ts`
Expand All @@ -76,7 +77,7 @@ Single-context repo. Read `CONTEXT.md` for domain language and testing/architect
- CLI/client/runtime output projection: `src/commands/cli-output.ts`, `src/commands/client-output.ts`, `src/commands/runtime-output.ts`
- Do not reintroduce CLI-shaped command adapters or schemas as a second source of truth. CLI, Node.js, and MCP should project from command contracts.
- Keep `src/daemon/request-router.ts` as request orchestration: auth, diagnostics scope, request admission, locking, handler chain, and fallback dispatch.
- New daemon handler-family commands must update the relevant `DAEMON_COMMAND_GROUPS.*Handler` entry and the handler module's exported `*_COMMAND_HANDLERS` coverage table; `src/daemon/__tests__/request-handler-catalog.test.ts` guards drift and overlap.
- New daemon handler-family commands must update `src/daemon/daemon-command-registry.ts` with the route and request-policy traits. `src/daemon/__tests__/daemon-command-registry.test.ts` guards route and policy traits; handler catalog tests keep executable handler sanity checks.
- Put request policies in focused request modules:
- tenant/lease/selector/lock admission: `src/daemon/request-admission.ts`
- artifact/error finalization: `src/daemon/request-finalization.ts`
Expand All @@ -89,7 +90,7 @@ Single-context repo. Read `CONTEXT.md` for domain language and testing/architect
- snapshot/wait/alert/settings: `src/daemon/handlers/snapshot.ts`
- find: `src/daemon/handlers/find.ts`
- record/trace: `src/daemon/handlers/record-trace.ts`
- Generic passthrough (press/scroll/type) is daemon fallback only after handlers return null.
- Commands routed as generic in `src/daemon/daemon-command-registry.ts` fall through to daemon fallback dispatch after specialized handlers return null.

## Toolchain Snapshot
- Package manager: `pnpm` only. Do not add or restore `package-lock.json`.
Expand Down Expand Up @@ -274,9 +275,9 @@ Command-only flags (like `find --first`) that do not flow to the platform layer
- Shared action helpers: `src/daemon/action-utils.ts`
- Snapshot shaping + labels: `src/daemon/snapshot-processing.ts`
- Handler context helpers: `src/daemon/context.ts`, `src/daemon/device-ready.ts`
- Request routing/policy: `src/daemon/request-router.ts`, `src/daemon/request-admission.ts`, `src/daemon/request-generic-dispatch.ts`
- Request routing/policy: `src/daemon/daemon-command-registry.ts`, `src/daemon/request-router.ts`, `src/daemon/request-admission.ts`, `src/daemon/request-generic-dispatch.ts`
- Dispatcher + capability map: `src/core/dispatch.ts`, `src/core/dispatch-context.ts`, `src/core/dispatch-interactions.ts`, `src/core/capabilities.ts`
- Command catalog + command surface: `src/command-catalog.ts`, `src/commands/command-surface.ts`, `src/commands/command-contract.ts`, `src/commands/client-command-contracts.ts`
- Command identity + command surface: `src/command-catalog.ts`, `src/commands/command-surface.ts`, `src/commands/command-contract.ts`, `src/commands/client-command-contracts.ts`
- CLI grammar: `src/commands/cli-grammar.ts`, `src/commands/cli-grammar/*`
- Daemon request projection: `src/commands/command-projection.ts`
- Platform backends: `src/platforms/ios/*`, `ios-runner/*`, `src/platforms/android/*`
Expand Down
3 changes: 2 additions & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
- Modality: broad supported device family, such as mobile, tv, or desktop.
- Session: daemon-owned state for a selected target and opened app or surface.
- Command surface: catalog of public command identity, interface exposure, adapter policy, and shared command metadata across CLI, Node.js, MCP, and batch entrypoints.
- Runner command traits: the iOS XCTest runner's per-command-type classification across three independent axes — interaction (gates the foreground-guard and stabilization preflight), read-only (gates the session-invalidating retry; the alert command is read-only only for its `get` action), and runner-lifecycle (skips the app-activation preflight). One source of truth keyed by command type, distinct from the daemon-side Command surface.
- Daemon command registry: daemon-side source of truth for command route ownership and request-policy traits, including admission exemptions, session locking, selector validation, replay-scoped actions, recording invalidation, Android dialog guards, and request provider device resolution.
- Runner command traits: the iOS XCTest runner's per-command-type classification across three independent axes — interaction (gates the foreground-guard and stabilization preflight), read-only (gates the session-invalidating retry; the alert command is read-only only for its `get` action), and runner-lifecycle (skips the app-activation preflight). One source of truth keyed by command type, distinct from the public command surface and daemon command registry.

## Testing Principles

Expand Down
55 changes: 55 additions & 0 deletions docs/adr/0003-daemon-command-registry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# ADR 0003: Daemon Command Registry

## Status

Accepted

## Context

Daemon request handling depends on command traits that are not part of the public command surface:
which handler route owns a command, whether tenant lease admission applies, whether session
execution should lock, whether selector validation applies, whether replay can run an action in the
current session scope, whether invalid recordings block the request, whether Android blocking-dialog
recovery applies, and how request-scoped providers resolve a device.

Those traits used to be spread across `src/command-catalog.ts`, request-policy modules, and
handler-local coverage tables. That made `src/command-catalog.ts` carry daemon-only behavior next
to public command identity, and it required duplicate command sets to stay aligned by convention.

## Decision

Keep public command identity in `src/command-catalog.ts` and public input/output contracts in
`src/commands/**`.

Add `src/daemon/daemon-command-registry.ts` as the daemon-side source of truth for command route
ownership and daemon request-policy traits. Request modules consume predicate functions from the
registry instead of recreating command string sets. Handler modules own execution logic only; they do
not export duplicate coverage tables to prove route membership.

The daemon registry is internal-only. It must not define CLI grammar, Node.js client options, MCP
schemas, user-facing help, or platform capability support. Those remain owned by the command
contract, projection, help, and capability modules.

## Alternatives Considered

- Keep daemon groups in `src/command-catalog.ts`: this keeps one command-name file, but it mixes
public command identity with daemon runtime policy and makes the catalog grow for internal-only
routing decisions.
- Keep handler-local coverage tables: this makes each handler self-describing, but creates a second
route membership source that can drift from the router and request-policy modules.
- Put route checks directly in request modules: this is locally simple, but scatters command
classification across admission, locking, provider scoping, replay, recording, and generic
dispatch.

## Consequences

Adding or moving a daemon-handled command requires updating the daemon command registry with its
route and request-policy traits. The registry tests pin the trait decisions, while provider-backed
integration scenarios verify important request-policy behavior through the real daemon request path.

The registry file is intentionally a dense internal contract. Its interface should stay small:
callers ask daemon-policy questions through named predicates rather than reading or mutating command
sets.

`AGENTS.md` should contain only the operating rule and relevant file pointers for agents. This ADR
owns the rationale so future changes do not need to infer it from agent instructions.
7 changes: 0 additions & 7 deletions src/__tests__/cli-grammar.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { test } from 'vitest';
import assert from 'node:assert/strict';
import { DAEMON_COMMAND_GROUPS, PUBLIC_COMMANDS } from '../command-catalog.ts';
import { readInputFromCli } from '../commands/cli-grammar.ts';
import type { CliFlags } from '../utils/cli-flags.ts';

Expand All @@ -10,12 +9,6 @@ const BASE_FLAGS: CliFlags = {
version: false,
};

test('command catalog owns daemon routing groups', () => {
assert.equal(DAEMON_COMMAND_GROUPS.snapshot.has(PUBLIC_COMMANDS.wait), true);
assert.equal(DAEMON_COMMAND_GROUPS.observability.has(PUBLIC_COMMANDS.logs), true);
assert.equal(DAEMON_COMMAND_GROUPS.replay.has(PUBLIC_COMMANDS.test), true);
});

test('wait grammar preserves CLI bare text forms', () => {
const options = readInputFromCli('wait', ['Continue', '1500'], BASE_FLAGS);
assert.equal(options.text, 'Continue');
Expand Down
122 changes: 0 additions & 122 deletions src/command-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,128 +152,6 @@ const CAPABILITY_EXEMPT_CLI_COMMANDS = commandSet(
PUBLIC_COMMANDS.trace,
);

export const DAEMON_COMMAND_GROUPS = {
inventory: commandSet(
INTERNAL_COMMANDS.sessionList,
PUBLIC_COMMANDS.devices,
PUBLIC_COMMANDS.apps,
),
state: commandSet(PUBLIC_COMMANDS.boot, PUBLIC_COMMANDS.appState),
observability: commandSet(PUBLIC_COMMANDS.perf, PUBLIC_COMMANDS.logs, PUBLIC_COMMANDS.network),
replay: commandSet(PUBLIC_COMMANDS.replay, PUBLIC_COMMANDS.test),
snapshot: commandSet(
PUBLIC_COMMANDS.snapshot,
PUBLIC_COMMANDS.diff,
PUBLIC_COMMANDS.wait,
PUBLIC_COMMANDS.alert,
PUBLIC_COMMANDS.settings,
),
replayScopedAction: commandSet(
PUBLIC_COMMANDS.alert,
PUBLIC_COMMANDS.back,
PUBLIC_COMMANDS.click,
PUBLIC_COMMANDS.clipboard,
PUBLIC_COMMANDS.diff,
PUBLIC_COMMANDS.fill,
PUBLIC_COMMANDS.find,
PUBLIC_COMMANDS.gesture,
PUBLIC_COMMANDS.get,
PUBLIC_COMMANDS.home,
PUBLIC_COMMANDS.is,
PUBLIC_COMMANDS.keyboard,
PUBLIC_COMMANDS.longPress,
'pinch',
PUBLIC_COMMANDS.press,
PUBLIC_COMMANDS.record,
PUBLIC_COMMANDS.reactNative,
PUBLIC_COMMANDS.rotate,
PUBLIC_COMMANDS.screenshot,
PUBLIC_COMMANDS.scroll,
PUBLIC_COMMANDS.settings,
PUBLIC_COMMANDS.snapshot,
PUBLIC_COMMANDS.swipe,
PUBLIC_COMMANDS.type,
PUBLIC_COMMANDS.wait,
),
androidBlockingDialogGuardedAction: commandSet(
PUBLIC_COMMANDS.back,
PUBLIC_COMMANDS.click,
PUBLIC_COMMANDS.fill,
PUBLIC_COMMANDS.focus,
PUBLIC_COMMANDS.gesture,
PUBLIC_COMMANDS.home,
PUBLIC_COMMANDS.keyboard,
PUBLIC_COMMANDS.longPress,
'fling',
'pan',
'pinch',
PUBLIC_COMMANDS.press,
PUBLIC_COMMANDS.rotate,
'rotate-gesture',
PUBLIC_COMMANDS.scroll,
PUBLIC_COMMANDS.swipe,
'transform-gesture',
PUBLIC_COMMANDS.type,
),
selectorValidationExempt: commandSet(
INTERNAL_COMMANDS.sessionList,
PUBLIC_COMMANDS.devices,
INTERNAL_COMMANDS.releaseMaterializedPaths,
),
leaseAdmissionExempt: commandSet(
INTERNAL_COMMANDS.sessionList,
PUBLIC_COMMANDS.devices,
INTERNAL_COMMANDS.releaseMaterializedPaths,
INTERNAL_COMMANDS.leaseAllocate,
INTERNAL_COMMANDS.leaseHeartbeat,
INTERNAL_COMMANDS.leaseRelease,
),
// Specialized daemon handler families. Commands absent from these sets fall through to
// request-generic-dispatch after request admission and provider scoping.
leaseHandler: commandSet(
INTERNAL_COMMANDS.leaseAllocate,
INTERNAL_COMMANDS.leaseHeartbeat,
INTERNAL_COMMANDS.leaseRelease,
),
sessionHandler: commandSet(
INTERNAL_COMMANDS.installSource,
INTERNAL_COMMANDS.releaseMaterializedPaths,
INTERNAL_COMMANDS.sessionList,
PUBLIC_COMMANDS.appState,
PUBLIC_COMMANDS.apps,
PUBLIC_COMMANDS.batch,
PUBLIC_COMMANDS.boot,
PUBLIC_COMMANDS.clipboard,
PUBLIC_COMMANDS.close,
PUBLIC_COMMANDS.devices,
PUBLIC_COMMANDS.install,
PUBLIC_COMMANDS.keyboard,
PUBLIC_COMMANDS.logs,
PUBLIC_COMMANDS.network,
PUBLIC_COMMANDS.open,
PUBLIC_COMMANDS.perf,
PUBLIC_COMMANDS.prepare,
PUBLIC_COMMANDS.push,
PUBLIC_COMMANDS.reinstall,
PUBLIC_COMMANDS.replay,
PUBLIC_COMMANDS.test,
PUBLIC_COMMANDS.triggerAppEvent,
INTERNAL_COMMANDS.runtime,
),
reactNativeHandler: commandSet(PUBLIC_COMMANDS.reactNative),
recordTraceHandler: commandSet(PUBLIC_COMMANDS.record, PUBLIC_COMMANDS.trace),
findHandler: commandSet(PUBLIC_COMMANDS.find),
interactionHandler: commandSet(
PUBLIC_COMMANDS.click,
PUBLIC_COMMANDS.fill,
PUBLIC_COMMANDS.get,
PUBLIC_COMMANDS.is,
PUBLIC_COMMANDS.longPress,
PUBLIC_COMMANDS.press,
PUBLIC_COMMANDS.type,
),
} as const;

function commandSet(...commands: readonly string[]): ReadonlySet<string> {
return new Set(commands);
}
Expand Down
Loading
Loading