feat(bench): renderer-pinned samples, topic-coverage gate, error-recovery quizzes; trim skillgym to agentic checks (#1411)
* feat(bench): renderer-pinned samples, topic-coverage gate, error quizzes; trim skillgym to agentic checks
The help conformance bench's quoted CLI output is now sourced from
scripts/help-conformance-sample-outputs.mjs, and every sample is rebuilt
through the real production renderers (settle output formatters,
printHumanError, formatSnapshotText, refMutationAdmissionResponse) by
scripts/__tests__/help-conformance-sample-outputs.test.ts — a rendering or
message change fails deterministically instead of leaving the bench grading
against output the CLI no longer prints. This retires the fabricated
recoverable-failure envelope (production never throws a textual settle
timeout; that case is replaced by a real DEVICE_IN_USE recovery quiz).
Bench cases move to scripts/help-conformance-cases.mjs and are enumerated
against the help-topic registry: helpTopicIds() is exported from cli-help,
and scripts/__tests__/help-conformance-topic-coverage.test.ts fails when a
help topic has neither a bench case nor an explicit waiver. New case
families: error-envelope recovery quizzes (device-in-use, stale pinned ref,
ambiguous find match, app-not-installed) pinned to real error text, topic
coverage for tv/web/react-native/debugging/workflow, and a metamorphic twin
of the settled-diff quiz.
The skillgym smoke suite shrinks from 119 cases to the 5 that measure what
only an agentic runner can show: skill routing plus output interpretation
with a proven local CLI help probe (local-cli-help-policy). Its embedded
samples now import the same pinned constants, replacing hand-transcribed
output that had already drifted from the renderer. Knowledge checks belong
to the bench; live fixture behavior belongs to the iOS simulator e2e suite.
* review: drive error samples through the real producers; enforce local-help on the routing smoke
The DEVICE_IN_USE, AMBIGUOUS_MATCH, and APP_NOT_INSTALLED parity tests no
longer hand-author the producer message before rendering: each drives the
actual producer — buildDeviceInUseBySessionError (extracted in
session-open.ts and called by the handler), buildAmbiguousMatchError (now
exported from find.ts), and buildAppNotInstalledError (extracted in
app-resolution.ts and thrown by the resolver). Because each factory is
exported from its producer file and called by the production path, dropping
the production call would make it test-only and fail
check:production-exports — the wiring is gate-enforced, not conventional.
open-and-snapshot now sets requireLocalCliHelp and
allowOnlyLocalCliHelpCommands, so the 'skill plus local help' claim is
observed rather than assumed; without them the case can pass on model prior
alone.
2026-07-27 14:24:36 +02:00
import {
AMBIGUOUS _MATCH _SAMPLE ,
APP _NOT _INSTALLED _SAMPLE ,
2026-08-03 16:47:57 +02:00
BROWSERSTACK _CONNECT _SAMPLE ,
feat(bench): renderer-pinned samples, topic-coverage gate, error-recovery quizzes; trim skillgym to agentic checks (#1411)
* feat(bench): renderer-pinned samples, topic-coverage gate, error quizzes; trim skillgym to agentic checks
The help conformance bench's quoted CLI output is now sourced from
scripts/help-conformance-sample-outputs.mjs, and every sample is rebuilt
through the real production renderers (settle output formatters,
printHumanError, formatSnapshotText, refMutationAdmissionResponse) by
scripts/__tests__/help-conformance-sample-outputs.test.ts — a rendering or
message change fails deterministically instead of leaving the bench grading
against output the CLI no longer prints. This retires the fabricated
recoverable-failure envelope (production never throws a textual settle
timeout; that case is replaced by a real DEVICE_IN_USE recovery quiz).
Bench cases move to scripts/help-conformance-cases.mjs and are enumerated
against the help-topic registry: helpTopicIds() is exported from cli-help,
and scripts/__tests__/help-conformance-topic-coverage.test.ts fails when a
help topic has neither a bench case nor an explicit waiver. New case
families: error-envelope recovery quizzes (device-in-use, stale pinned ref,
ambiguous find match, app-not-installed) pinned to real error text, topic
coverage for tv/web/react-native/debugging/workflow, and a metamorphic twin
of the settled-diff quiz.
The skillgym smoke suite shrinks from 119 cases to the 5 that measure what
only an agentic runner can show: skill routing plus output interpretation
with a proven local CLI help probe (local-cli-help-policy). Its embedded
samples now import the same pinned constants, replacing hand-transcribed
output that had already drifted from the renderer. Knowledge checks belong
to the bench; live fixture behavior belongs to the iOS simulator e2e suite.
* review: drive error samples through the real producers; enforce local-help on the routing smoke
The DEVICE_IN_USE, AMBIGUOUS_MATCH, and APP_NOT_INSTALLED parity tests no
longer hand-author the producer message before rendering: each drives the
actual producer — buildDeviceInUseBySessionError (extracted in
session-open.ts and called by the handler), buildAmbiguousMatchError (now
exported from find.ts), and buildAppNotInstalledError (extracted in
app-resolution.ts and thrown by the resolver). Because each factory is
exported from its producer file and called by the production path, dropping
the production call would make it test-only and fail
check:production-exports — the wiring is gate-enforced, not conventional.
open-and-snapshot now sets requireLocalCliHelp and
allowOnlyLocalCliHelpCommands, so the 'skill plus local help' claim is
observed rather than assumed; without them the case can pass on model prior
alone.
2026-07-27 14:24:36 +02:00
DEVICE _IN _USE _SAMPLE ,
2026-08-08 10:35:12 +02:00
FOREGROUND _SNAPSHOT _FAILURE _SAMPLE ,
MERGED _CARD _ACTIONS _SAMPLE ,
feat(bench): renderer-pinned samples, topic-coverage gate, error-recovery quizzes; trim skillgym to agentic checks (#1411)
* feat(bench): renderer-pinned samples, topic-coverage gate, error quizzes; trim skillgym to agentic checks
The help conformance bench's quoted CLI output is now sourced from
scripts/help-conformance-sample-outputs.mjs, and every sample is rebuilt
through the real production renderers (settle output formatters,
printHumanError, formatSnapshotText, refMutationAdmissionResponse) by
scripts/__tests__/help-conformance-sample-outputs.test.ts — a rendering or
message change fails deterministically instead of leaving the bench grading
against output the CLI no longer prints. This retires the fabricated
recoverable-failure envelope (production never throws a textual settle
timeout; that case is replaced by a real DEVICE_IN_USE recovery quiz).
Bench cases move to scripts/help-conformance-cases.mjs and are enumerated
against the help-topic registry: helpTopicIds() is exported from cli-help,
and scripts/__tests__/help-conformance-topic-coverage.test.ts fails when a
help topic has neither a bench case nor an explicit waiver. New case
families: error-envelope recovery quizzes (device-in-use, stale pinned ref,
ambiguous find match, app-not-installed) pinned to real error text, topic
coverage for tv/web/react-native/debugging/workflow, and a metamorphic twin
of the settled-diff quiz.
The skillgym smoke suite shrinks from 119 cases to the 5 that measure what
only an agentic runner can show: skill routing plus output interpretation
with a proven local CLI help probe (local-cli-help-policy). Its embedded
samples now import the same pinned constants, replacing hand-transcribed
output that had already drifted from the renderer. Knowledge checks belong
to the bench; live fixture behavior belongs to the iOS simulator e2e suite.
* review: drive error samples through the real producers; enforce local-help on the routing smoke
The DEVICE_IN_USE, AMBIGUOUS_MATCH, and APP_NOT_INSTALLED parity tests no
longer hand-author the producer message before rendering: each drives the
actual producer — buildDeviceInUseBySessionError (extracted in
session-open.ts and called by the handler), buildAmbiguousMatchError (now
exported from find.ts), and buildAppNotInstalledError (extracted in
app-resolution.ts and thrown by the resolver). Because each factory is
exported from its producer file and called by the production path, dropping
the production call would make it test-only and fail
check:production-exports — the wiring is gate-enforced, not conventional.
open-and-snapshot now sets requireLocalCliHelp and
allowOnlyLocalCliHelpCommands, so the 'skill plus local help' claim is
observed rather than assumed; without them the case can pass on model prior
alone.
2026-07-27 14:24:36 +02:00
NOT _SETTLED _SAMPLE ,
feat: support --settle on scroll and back (#1638) (#1650)
* feat: support --settle on scroll and back (#1638)
Scroll-then-observe and back-then-observe are legitimate agent pairs, but
the post-action observation registry never grew past the touch commands, so
`--settle` on either was rejected with INVALID_ARGS — burning a tool call
each in AppControlBench's bsky-16.
Both commands now carry the `settle` descriptor trait, and every surface
derives from it rather than a hand list: CLI allowed flags, MCP/SDK input
fields, the flag-sourced timeout envelope, and MCP ref-pinning. The CLI
flag/metadata helpers moved out of the interaction family into
post-action-observation-grammar.ts (back is a system command), and
SETTLE_REF_ISSUING_TOOLS became a derivation — a hand list would have
silently stopped pinning the new commands' refs.
settleAfterInteraction and the new settleObservationCommand are two entry
points over one engine: same loop, storage, hints, and diff bounds, with the
target-less path supplying its own baseline and no proximity point. The
daemon reaches that command through the runtime surface, never by importing
`commands/` (R2) — the same seam the touch handlers use for press/fill —
and generic-settle.ts is loaded through a lazy `await import` returning a
closure, so the interaction runtime subgraph stays out of this dispatcher's
static graph (a static edge folded ~18 files into the daemon-server type
cycle; R10 caught it).
Both of generic-settle's orderings are load-bearing and tested: the baseline
is frozen before dispatch (and before the Android dialog preflight), and the
observation runs after markDeferredInteractionOutcome so settle's first
capture folds in the #1542 stabilization rather than racing it. The ADR 0014
"a settled diff publishes refs" rule moved to settle-ref-issuance.ts, shared
by both routes.
One divergence is deliberate: scroll/back resolve no element, so the diff
baseline is the session's STORED pre-action tree — "settled tree vs the
last tree you observed" — not press's freshly resolved pre-action capture.
Both commands also switch to preserve-daemon on timeout, which changes the
non-settle path too: with --settle their dominant hang mode is now a wedged
accessibility bridge, and a timed-out capture must not reset the daemon and
lose every session (#1105). The reviewed-set gate records it.
Live-validated on an iOS 26.2 simulator (Settings): scroll --settle settled
in 1786ms with a +6/-6 diff carrying fresh refs; back --settle in 771ms with
+15/-6. Alternating cost runs, one call vs the pair it replaces:
scroll 2.9-3.0s vs 5.3-5.6s, back 3.1-3.2s vs 4.7-5.1s. Those include the
#1627 deep-capture extension.
* fix: render settled-diff refs paste-ready in CLI output
A settled diff activates a PARTIAL ref frame (ADR 0014), which admits only
the pinned `@eN~s<gen>` form of the refs it issued. The unchanged-interactive
tail already rendered that way, but the diff's own added lines rendered the
bare `@eN` embedded in the snapshot line — so a CLI caller who copied the
ref the diff just handed them got `plain_ref_requires_complete_frame` and had
to append the generation by hand.
Added lines now render pinned when the response carries `refsGeneration`,
exactly like the tail. Removed lines render verbatim: they name elements that
just left the screen, and `SettleDiffLine` never gives them a ref.
This is not new to scroll/back — press/click/fill/longpress had the same gap
since #1101. MCP was never affected: its ref-pin store rewrites plain refs on
the way in, which is why the model never sees a suffix.
Live: `scroll down --settle` now emits `+ @e14~s218078 [cell] "Game Center"`,
and `press @e14~s218078` copied straight out of that line taps successfully.
* test: record the pinned-diff-ref bytes in the output-economy baseline
Rendering added diff-line refs pinned costs 8 bytes in the two settle CLI
text samples (two `~s<gen>` suffixes). The output-economy baseline is the
tripwire for exactly this, so the increase takes an explicit reviewed waiver
rather than a silent baseline bump — the same one the settled TAIL's pins
already carry, for the same ADR 0014 reason.
Only `bytes` moves: lines, refs, hints, and shape are unchanged, which is the
evidence that this is a suffix on existing refs and not a new payload.
Caught by CI, not locally: `pnpm test:unit` runs unit-core and
subprocess-stub only, while the Coverage lane runs every vitest project.
* test: prove the generic settle degrades when its runtime cannot be built
`createGenericSettleRuntime` catches and returns undefined so an observation
that cannot even start does not fail an action that already succeeded. That
was a claim in a docstring with nothing behind it — the one changed line the
coverage gate reported uncovered (95/96).
The test puts the session in the state the catch exists for: the router
handed us a session that is no longer in the store, so building the settle
runtime throws SESSION_NOT_FOUND. The response keeps its scroll result and
simply carries no settle payload. Removing the try/catch fails it.
* build: teach fallow that vi.mock reaches pinOwnProcessStartTime dynamically
Not from this PR: #1642 added `pinOwnProcessStartTime` on main, and its three
consumers reach it the only way a Vitest module mock can —
`vi.mock(path, async (importOriginal) => (await import('...')).pinOwnProcessStartTime(...))`.
Dependency analysis cannot follow that dynamic import to a consumer, so the
export reads as dead the moment any PR pulls that file into its audit scope.
This PR is the one that did.
The entry records the consumers by path and the reason, matching the
daemon route-handler entry directly above it, which exists for the same
dynamic-`import()` limitation.
* refactor: adopt the best of the parallel #1653 implementation
Two sessions independently built #1638 (PR #1650 and PR #1653) and converged
on the same architecture — trait in the registry, one engine with two entry
points, runtime-command seam, lazy import, preserve-daemon, stored-baseline
honesty. #1650 continues; this folds in what #1653 did better:
- The agent-facing help core loop (cli-help.ts) now names scroll and back as
settle-capable. Without this, the benchmarked closed-grammar help line kept
instructing agents that --settle is only for press/click/fill/longpress —
actively steering the AppControlBench models away from what #1638 shipped.
- issueSettleRefs moves into session-snapshot.ts, beside the partial-frame
primitive it wraps, deleting the single-function settle-ref-issuance module.
- Their seam tests: back reader→writer settle plumbing, back CLI settle
rendering, and a trait-less generic command (home) ignoring a stray settle
flag rather than observing or rejecting.
What #1650 had that #1653 lacked, for the record: the SETTLE_REF_ISSUING_TOOLS
registry derivation (without it, MCP never pins a scroll/back settle diff's
refs and the partial frame rejects every follow-up), BackCommandResult.settle
in contracts, back's MCP output schema, paste-ready pinned diff refs, and the
docs/changelog/baseline surfaces.
* bench: help-conformance case for settled scroll-to-find planning
The #1638 extension of the closed --settle grammar to scroll/back is the
feature's entire payoff — collapsing scroll-then-observe into one call — and
the closed command list is an enumerated N whose enumerator is this bench.
The regex over the help text proves the sentence exists; this case checks
whether a model plans differently because of it.
One focused case, deliberately not coached: a pinned visible-first snapshot
(rendered by formatSnapshotText, pinned by the sample-producers gate) whose
wanted row is summarized off-screen with no ref anywhere in the output. The
tempting pre-#1638 plan is `scroll` plus a separate `snapshot -i`; acceptance
is the single settled call. Scoring was verified against eight plan shapes in
both directions before recording.
Model-backed record (claude-haiku-4-5, 3 trials, current help): 0/3 — but the
decomposition is the finding. Settle eligibility GENERALIZED (3/3 trials put
--settle on scroll unprompted; the mutation-suffix framing concern did not
materialize) and the two-call habit is residual (1/3). All three trials failed
on `scroll @e3 down --settle` — the pre-existing #1366 scroll-takes-no-target
confusion, which the live CLI recovers with a dedicated hint but a single-shot
bench cannot. The recorded gap is therefore a first-30 doc gap (nothing
teaches that scroll takes no target), not a settle-eligibility gap; tuning the
case until it passes would just delete the evidence.
2026-08-06 20:04:16 +02:00
OFFSCREEN _TARGET _SNAPSHOT _SAMPLE ,
feat(bench): renderer-pinned samples, topic-coverage gate, error-recovery quizzes; trim skillgym to agentic checks (#1411)
* feat(bench): renderer-pinned samples, topic-coverage gate, error quizzes; trim skillgym to agentic checks
The help conformance bench's quoted CLI output is now sourced from
scripts/help-conformance-sample-outputs.mjs, and every sample is rebuilt
through the real production renderers (settle output formatters,
printHumanError, formatSnapshotText, refMutationAdmissionResponse) by
scripts/__tests__/help-conformance-sample-outputs.test.ts — a rendering or
message change fails deterministically instead of leaving the bench grading
against output the CLI no longer prints. This retires the fabricated
recoverable-failure envelope (production never throws a textual settle
timeout; that case is replaced by a real DEVICE_IN_USE recovery quiz).
Bench cases move to scripts/help-conformance-cases.mjs and are enumerated
against the help-topic registry: helpTopicIds() is exported from cli-help,
and scripts/__tests__/help-conformance-topic-coverage.test.ts fails when a
help topic has neither a bench case nor an explicit waiver. New case
families: error-envelope recovery quizzes (device-in-use, stale pinned ref,
ambiguous find match, app-not-installed) pinned to real error text, topic
coverage for tv/web/react-native/debugging/workflow, and a metamorphic twin
of the settled-diff quiz.
The skillgym smoke suite shrinks from 119 cases to the 5 that measure what
only an agentic runner can show: skill routing plus output interpretation
with a proven local CLI help probe (local-cli-help-policy). Its embedded
samples now import the same pinned constants, replacing hand-transcribed
output that had already drifted from the renderer. Knowledge checks belong
to the bench; live fixture behavior belongs to the iOS simulator e2e suite.
* review: drive error samples through the real producers; enforce local-help on the routing smoke
The DEVICE_IN_USE, AMBIGUOUS_MATCH, and APP_NOT_INSTALLED parity tests no
longer hand-author the producer message before rendering: each drives the
actual producer — buildDeviceInUseBySessionError (extracted in
session-open.ts and called by the handler), buildAmbiguousMatchError (now
exported from find.ts), and buildAppNotInstalledError (extracted in
app-resolution.ts and thrown by the resolver). Because each factory is
exported from its producer file and called by the production path, dropping
the production call would make it test-only and fail
check:production-exports — the wiring is gate-enforced, not conventional.
open-and-snapshot now sets requireLocalCliHelp and
allowOnlyLocalCliHelpCommands, so the 'skill plus local help' claim is
observed rather than assumed; without them the case can pass on model prior
alone.
2026-07-27 14:24:36 +02:00
SETTLE _DIFF _SAMPLE ,
SETTLE _DIFF _SAMPLE _NOTES ,
SETTLE _TAIL _SAMPLE ,
STALE _REF _SAMPLE ,
sampleText ,
} from './help-conformance-sample-outputs.mjs' ;
// Raw-coordinate fallback the quiz cases forbid: a click/fill/press targeting
// bare numbers instead of a ref or selector.
const RAW _COORDINATE _TARGET =
/(?:^|\n)(?:agent-device\s+)?(?:click|fill|press)\s+-?\d+(?:\.\d+)?\s+-?\d+(?:\.\d+)?/i ;
2026-08-10 20:43:01 +02:00
// Ref pasted without its leading @: byte-for-byte copying is the contract,
// and a dropped @ silently stops targeting the observed element.
const BARE _REF _TARGET =
/(?:^|\n)(?:agent-device\s+)?(?:press|tap|click|fill|longpress)\s+['"]?e\d+\b/i ;
feat(bench): renderer-pinned samples, topic-coverage gate, error-recovery quizzes; trim skillgym to agentic checks (#1411)
* feat(bench): renderer-pinned samples, topic-coverage gate, error quizzes; trim skillgym to agentic checks
The help conformance bench's quoted CLI output is now sourced from
scripts/help-conformance-sample-outputs.mjs, and every sample is rebuilt
through the real production renderers (settle output formatters,
printHumanError, formatSnapshotText, refMutationAdmissionResponse) by
scripts/__tests__/help-conformance-sample-outputs.test.ts — a rendering or
message change fails deterministically instead of leaving the bench grading
against output the CLI no longer prints. This retires the fabricated
recoverable-failure envelope (production never throws a textual settle
timeout; that case is replaced by a real DEVICE_IN_USE recovery quiz).
Bench cases move to scripts/help-conformance-cases.mjs and are enumerated
against the help-topic registry: helpTopicIds() is exported from cli-help,
and scripts/__tests__/help-conformance-topic-coverage.test.ts fails when a
help topic has neither a bench case nor an explicit waiver. New case
families: error-envelope recovery quizzes (device-in-use, stale pinned ref,
ambiguous find match, app-not-installed) pinned to real error text, topic
coverage for tv/web/react-native/debugging/workflow, and a metamorphic twin
of the settled-diff quiz.
The skillgym smoke suite shrinks from 119 cases to the 5 that measure what
only an agentic runner can show: skill routing plus output interpretation
with a proven local CLI help probe (local-cli-help-policy). Its embedded
samples now import the same pinned constants, replacing hand-transcribed
output that had already drifted from the renderer. Knowledge checks belong
to the bench; live fixture behavior belongs to the iOS simulator e2e suite.
* review: drive error samples through the real producers; enforce local-help on the routing smoke
The DEVICE_IN_USE, AMBIGUOUS_MATCH, and APP_NOT_INSTALLED parity tests no
longer hand-author the producer message before rendering: each drives the
actual producer — buildDeviceInUseBySessionError (extracted in
session-open.ts and called by the handler), buildAmbiguousMatchError (now
exported from find.ts), and buildAppNotInstalledError (extracted in
app-resolution.ts and thrown by the resolver). Because each factory is
exported from its producer file and called by the production path, dropping
the production call would make it test-only and fail
check:production-exports — the wiring is gate-enforced, not conventional.
open-and-snapshot now sets requireLocalCliHelp and
allowOnlyLocalCliHelpCommands, so the 'skill plus local help' claim is
observed rather than assumed; without them the case can pass on model prior
alone.
2026-07-27 14:24:36 +02:00
function quiz ( sample , question ) {
return ` Read this previous agent-device output, then plan the next command:
${ sampleText ( sample ) }
${ question } ` ;
}
feat(mcp): compact server instructions in both eras + MCP-only help tool (#1839)
* feat(mcp): compact server instructions in both eras + MCP-only help tool (#1833)
MCP-only clients got no workflow guidance: server/discover carried two
sentences, legacy initialize carried nothing, and the CLI guides
(agent-device --help, help <topic>) were unreachable over MCP.
- MCP_SERVER_INSTRUCTIONS: one MCP-phrased workflow card (<2 KB, the
Claude Code truncation limit) returned by server/discover and legacy
initialize alike.
- help tool, router-owned (not a command descriptor): no topic -> the CLI
decision card; topic -> agent-device help <topic|command> text, prefixed
with the one-line CLI->tool-property mapping; unknown topic -> isError
listing the topics. listCommandTools() stays descriptor-only for the AI
SDK; the router composes descriptors + help.
- Move src/cli/parser/cli-help{,-overview}.ts to src/cli-schema/ so
src/mcp (rank 3) can import the renderers without a layering back-edge
into src/cli (rank 6).
* fix(mcp): name terminal-only commands in help guides; colocate cli-help tests with their sources
- The MCP guide preamble claimed every `agent-device <command>` line is a
tool of that name; `help web` tells the reader to run `web setup` /
`web doctor` and no `web` tool exists. The preamble now lists the exact
CLI-only set (listCliCommandNames minus listMcpExposedCommandNames) —
derived, not scanned out of prose where `device`/`web` are ordinary
words. Regression: help web names `web` as terminal-only, and the listed
set equals the registry difference.
- cli-help-*.test.ts move from src/cli/parser/__tests__ to src/cli-schema/
to mirror the moved sources.
* perf(mcp): tighten the guide card, tool description, and preamble
Instructions card 1572 -> 1378 bytes (paid every session), tool
description and preamble trimmed, HELP_TOOL built once as a const.
Bundle delta vs main 3189 -> 2715 bytes; the remainder is the guide text
itself, which the bundle carried in no MCP-phrased form before.
2026-08-18 17:48:36 +02:00
// Case docs reference help topic ids from src/cli-schema/cli-help.ts plus the
feat(bench): renderer-pinned samples, topic-coverage gate, error-recovery quizzes; trim skillgym to agentic checks (#1411)
* feat(bench): renderer-pinned samples, topic-coverage gate, error quizzes; trim skillgym to agentic checks
The help conformance bench's quoted CLI output is now sourced from
scripts/help-conformance-sample-outputs.mjs, and every sample is rebuilt
through the real production renderers (settle output formatters,
printHumanError, formatSnapshotText, refMutationAdmissionResponse) by
scripts/__tests__/help-conformance-sample-outputs.test.ts — a rendering or
message change fails deterministically instead of leaving the bench grading
against output the CLI no longer prints. This retires the fabricated
recoverable-failure envelope (production never throws a textual settle
timeout; that case is replaced by a real DEVICE_IN_USE recovery quiz).
Bench cases move to scripts/help-conformance-cases.mjs and are enumerated
against the help-topic registry: helpTopicIds() is exported from cli-help,
and scripts/__tests__/help-conformance-topic-coverage.test.ts fails when a
help topic has neither a bench case nor an explicit waiver. New case
families: error-envelope recovery quizzes (device-in-use, stale pinned ref,
ambiguous find match, app-not-installed) pinned to real error text, topic
coverage for tv/web/react-native/debugging/workflow, and a metamorphic twin
of the settled-diff quiz.
The skillgym smoke suite shrinks from 119 cases to the 5 that measure what
only an agentic runner can show: skill routing plus output interpretation
with a proven local CLI help probe (local-cli-help-policy). Its embedded
samples now import the same pinned constants, replacing hand-transcribed
output that had already drifted from the renderer. Knowledge checks belong
to the bench; live fixture behavior belongs to the iOS simulator e2e suite.
* review: drive error samples through the real producers; enforce local-help on the routing smoke
The DEVICE_IN_USE, AMBIGUOUS_MATCH, and APP_NOT_INSTALLED parity tests no
longer hand-author the producer message before rendering: each drives the
actual producer — buildDeviceInUseBySessionError (extracted in
session-open.ts and called by the handler), buildAmbiguousMatchError (now
exported from find.ts), and buildAppNotInstalledError (extracted in
app-resolution.ts and thrown by the resolver). Because each factory is
exported from its producer file and called by the production path, dropping
the production call would make it test-only and fail
check:production-exports — the wiring is gate-enforced, not conventional.
open-and-snapshot now sets requireLocalCliHelp and
allowOnlyLocalCliHelpCommands, so the 'skill plus local help' claim is
observed rather than assumed; without them the case can pass on model prior
alone.
2026-07-27 14:24:36 +02:00
// synthetic '--help:first30' first-screen slice. Topic coverage is enforced by
// scripts/__tests__/help-conformance-topic-coverage.test.ts: a new help topic
// needs a case here or an explicit waiver there.
2026-07-28 15:46:27 +02:00
//
// A case whose quoted output is a rendered error declares
// `recovery: { code, sample }` — the error code the case teaches recovery from
// and the pinned sample it quotes. That marking is what
// scripts/__tests__/help-conformance-error-recovery-coverage.test.ts counts as
// coverage (a bare mention of an error code in prose is not a quiz), and it is
// how the gate proves the quoted text is verbatim sample output.
feat(bench): renderer-pinned samples, topic-coverage gate, error-recovery quizzes; trim skillgym to agentic checks (#1411)
* feat(bench): renderer-pinned samples, topic-coverage gate, error quizzes; trim skillgym to agentic checks
The help conformance bench's quoted CLI output is now sourced from
scripts/help-conformance-sample-outputs.mjs, and every sample is rebuilt
through the real production renderers (settle output formatters,
printHumanError, formatSnapshotText, refMutationAdmissionResponse) by
scripts/__tests__/help-conformance-sample-outputs.test.ts — a rendering or
message change fails deterministically instead of leaving the bench grading
against output the CLI no longer prints. This retires the fabricated
recoverable-failure envelope (production never throws a textual settle
timeout; that case is replaced by a real DEVICE_IN_USE recovery quiz).
Bench cases move to scripts/help-conformance-cases.mjs and are enumerated
against the help-topic registry: helpTopicIds() is exported from cli-help,
and scripts/__tests__/help-conformance-topic-coverage.test.ts fails when a
help topic has neither a bench case nor an explicit waiver. New case
families: error-envelope recovery quizzes (device-in-use, stale pinned ref,
ambiguous find match, app-not-installed) pinned to real error text, topic
coverage for tv/web/react-native/debugging/workflow, and a metamorphic twin
of the settled-diff quiz.
The skillgym smoke suite shrinks from 119 cases to the 5 that measure what
only an agentic runner can show: skill routing plus output interpretation
with a proven local CLI help probe (local-cli-help-policy). Its embedded
samples now import the same pinned constants, replacing hand-transcribed
output that had already drifted from the renderer. Knowledge checks belong
to the bench; live fixture behavior belongs to the iOS simulator e2e suite.
* review: drive error samples through the real producers; enforce local-help on the routing smoke
The DEVICE_IN_USE, AMBIGUOUS_MATCH, and APP_NOT_INSTALLED parity tests no
longer hand-author the producer message before rendering: each drives the
actual producer — buildDeviceInUseBySessionError (extracted in
session-open.ts and called by the handler), buildAmbiguousMatchError (now
exported from find.ts), and buildAppNotInstalledError (extracted in
app-resolution.ts and thrown by the resolver). Because each factory is
exported from its producer file and called by the production path, dropping
the production call would make it test-only and fail
check:production-exports — the wiring is gate-enforced, not conventional.
open-and-snapshot now sets requireLocalCliHelp and
allowOnlyLocalCliHelpCommands, so the 'skill plus local help' claim is
observed rather than assumed; without them the case can pass on model prior
alone.
2026-07-27 14:24:36 +02:00
export const CASES = [
{
id : 'raw-first-screen-bluesky' ,
docs : [ '--help:first30' ] ,
task : 'Plan commands to open an already installed Bluesky app, search "callstack", open the @callstack.com account, press Follow or Following, and close.' ,
expectations : [
'validPlanCommands' ,
'fullPrefix' ,
'usesSnapshotI' ,
'usesSettleOnMutations' ,
'noWaitStable' ,
] ,
} ,
2026-08-05 08:02:44 +02:00
{
id : 'focused-type-stops-at-success' ,
docs : [ '--help:first30' , 'workflow' ] ,
task : 'Plan commands to open the installed app com.example.messages, press the visible New message control, focus the Message field, type the exact text "hello", press Send, wait for the explicit "Sent" confirmation, and close. The task ends at Sent; do not open the transient "View message" follow-up.' ,
expectations : [
'validPlanCommands' ,
'fullPrefix' ,
'usesSnapshotI' ,
'usesSettleOnMutations' ,
'opensAndCloses' ,
] ,
matchers : [
{ id : 'typesExactMessage' , pattern : /\bagent-device\s+type\s+(?:"hello"|'hello')/i } ,
{ id : 'waitsForSent' , pattern : /\bagent-device\s+wait\s+text\s+(?:"Sent"|'Sent')/i } ,
] ,
forbidden : [
{ id : 'typeDoesNotUseSettle' , pattern : /\bagent-device\s+type\b[^\n]*--settle\b/i } ,
{ id : 'stopsBeforeTransientFollowUp' , pattern : /View message/i } ,
] ,
} ,
feat(bench): renderer-pinned samples, topic-coverage gate, error-recovery quizzes; trim skillgym to agentic checks (#1411)
* feat(bench): renderer-pinned samples, topic-coverage gate, error quizzes; trim skillgym to agentic checks
The help conformance bench's quoted CLI output is now sourced from
scripts/help-conformance-sample-outputs.mjs, and every sample is rebuilt
through the real production renderers (settle output formatters,
printHumanError, formatSnapshotText, refMutationAdmissionResponse) by
scripts/__tests__/help-conformance-sample-outputs.test.ts — a rendering or
message change fails deterministically instead of leaving the bench grading
against output the CLI no longer prints. This retires the fabricated
recoverable-failure envelope (production never throws a textual settle
timeout; that case is replaced by a real DEVICE_IN_USE recovery quiz).
Bench cases move to scripts/help-conformance-cases.mjs and are enumerated
against the help-topic registry: helpTopicIds() is exported from cli-help,
and scripts/__tests__/help-conformance-topic-coverage.test.ts fails when a
help topic has neither a bench case nor an explicit waiver. New case
families: error-envelope recovery quizzes (device-in-use, stale pinned ref,
ambiguous find match, app-not-installed) pinned to real error text, topic
coverage for tv/web/react-native/debugging/workflow, and a metamorphic twin
of the settled-diff quiz.
The skillgym smoke suite shrinks from 119 cases to the 5 that measure what
only an agentic runner can show: skill routing plus output interpretation
with a proven local CLI help probe (local-cli-help-policy). Its embedded
samples now import the same pinned constants, replacing hand-transcribed
output that had already drifted from the renderer. Knowledge checks belong
to the bench; live fixture behavior belongs to the iOS simulator e2e suite.
* review: drive error samples through the real producers; enforce local-help on the routing smoke
The DEVICE_IN_USE, AMBIGUOUS_MATCH, and APP_NOT_INSTALLED parity tests no
longer hand-author the producer message before rendering: each drives the
actual producer — buildDeviceInUseBySessionError (extracted in
session-open.ts and called by the handler), buildAmbiguousMatchError (now
exported from find.ts), and buildAppNotInstalledError (extracted in
app-resolution.ts and thrown by the resolver). Because each factory is
exported from its producer file and called by the production path, dropping
the production call would make it test-only and fail
check:production-exports — the wiring is gate-enforced, not conventional.
open-and-snapshot now sets requireLocalCliHelp and
allowOnlyLocalCliHelpCommands, so the 'skill plus local help' claim is
observed rather than assumed; without them the case can pass on model prior
alone.
2026-07-27 14:24:36 +02:00
{
id : 'metamorphic-community-search' ,
docs : [ '--help:first30' ] ,
task : 'Plan commands to open the already installed app com.example.community, open the visible Discover destination, fill the People search field with "react native", open the @react.dev account, press Connect or Connected, and close.' ,
expectations : [
'validPlanCommands' ,
'fullPrefix' ,
'usesSnapshotI' ,
'usesSettleOnMutations' ,
'noWaitStable' ,
'opensAndCloses' ,
] ,
matchers : [
{
id : 'opensKnownCommunityApp' ,
pattern : /\bagent-device\s+open\s+com\.example\.community\b/i ,
} ,
{
id : 'fillsExpectedSearch' ,
pattern : /\bagent-device\s+fill\b[^\n]*(?:"react native"|'react native')[^\n]*--settle\b/i ,
} ,
{
id : 'usesLiteralHandleSelector' ,
pattern :
/\bagent-device\s+(?:press|click|tap)\b[^\n]*(?:label|text)=@react\.dev\b[^\n]*--settle\b/i ,
} ,
] ,
forbidden : [
{
id : 'noBlueskyLeakage' ,
pattern : /(?:bluesky|callstack|@e64|@callstack\.com)/i ,
} ,
{ id : 'noRawCoordinateTarget' , pattern : RAW _COORDINATE _TARGET } ,
] ,
} ,
{
id : 'manual-qa-bluesky-script' ,
docs : [ '--help:first30' , 'manual-qa' ] ,
task : 'You are following a manual QA script: on Bluesky, open Search, search "callstack", open @callstack.com, press Follow or Following, verify the button state changed, then close. Plan commands only.' ,
expectations : [
'validPlanCommands' ,
'fullPrefix' ,
'usesSnapshotI' ,
'usesSettleOnMutations' ,
'verifiesNamedExpectation' ,
'noWaitStable' ,
] ,
} ,
{
id : 'dogfood-mode' ,
docs : [ '--help:first30' , 'dogfood' ] ,
task : 'Plan a short dogfood pass for the logged-in iOS shop app com.example.shop. Exercise the visible Home, Search, and Cart destinations and capture reproducible evidence for any issue found.' ,
allowedExternalCommands : [ 'mkdir' ] ,
expectations : [
'validPlanCommands' ,
'fullPrefix' ,
'usesSnapshotI' ,
'usesSettleOnMutations' ,
'usesDogfoodEvidence' ,
'opensAndCloses' ,
] ,
matchers : [
{ id : 'opensKnownDogfoodApp' , pattern : /\bagent-device\s+open\s+com\.example\.shop\b/i } ,
{
id : 'capturesStrongIssueEvidence' ,
pattern : /\b(?:screenshot\b[^\n]*--overlay-refs|record\s+start\b|logs\s+mark\b)/i ,
} ,
] ,
} ,
{
id : 'engineering-validate-mode' ,
docs : [ '--help:first30' , 'validate' ] ,
task : 'Plan commands to validate a TypeScript-only CLI/runtime change to settled press output against the already installed iOS Settings app. Use the known General control, prove current built output is running, and clean up. Swift runner code did not change.' ,
allowedExternalCommands : [ 'pnpm' ] ,
expectations : [
'validPlanCommands' ,
'fullPrefix' ,
'usesSnapshotI' ,
'usesSettleOnMutations' ,
'usesValidationPrep' ,
'opensAndCloses' ,
] ,
matchers : [
{
id : 'opensSettings' ,
pattern : /\bagent-device\s+open\s+(?:settings|com\.apple\.Preferences)\b/i ,
} ,
] ,
forbidden : [
{
id : 'avoidsUnrelatedPlatformBuild' ,
pattern : /\bpnpm\s+(?:run\s+)?build:(?:android|xcuitest)\b/i ,
} ,
] ,
} ,
{
id : 'tv-focus-first-remote' ,
docs : [ '--help:first30' , 'tv' ] ,
task : 'On an Android TV emulator, open the installed app com.example.tvhub, move focus to the "Continue watching" tile two positions to the right of the initially focused tile, activate it, verify the player screen appeared, and close. Plan commands only.' ,
expectations : [ 'validPlanCommands' , 'fullPrefix' , 'usesSnapshotI' , 'opensAndCloses' ] ,
matchers : [
{ id : 'movesFocusWithRemote' , pattern : /\btv-remote\s+press\s+right\b/i } ,
{ id : 'activatesWithSelect' , pattern : /\btv-remote\s+press\s+select\b/i } ,
{ id : 'verifiesOutcome' , pattern : /\b(?:is\s+focused|wait\b|find\b)/i } ,
] ,
forbidden : [
// Focus-first surface: activation goes through tv-remote select, not a
// coordinate/element tap (help tv "Do not assume press/click @ref works").
{
id : 'noDirectTapActivation' ,
pattern : /(?:^|\n)agent-device\s+(?:press|click|tap)\s/i ,
} ,
{ id : 'noRawAdbKeyevent' , pattern : /\badb\s+shell\s+input\b/i } ,
] ,
} ,
fix(daemon): keep close-time script-save failures from leaking the session/device claim (#1392)
* fix(daemon): keep close-time script-save failures from leaking the session/device claim
A close-time script write (implicit from `open --save-script`, or this
close's own `--save-script`) that refuses to publish (e.g. a no-clobber
target-exists AppError) threw uncaught out of `handleCloseCommand`,
skipping lease release, device-claim release, and `sessionStore.delete`
entirely — while the `close` action had already been recorded with no
rollback.
Live-repro'd over the real CLI against an Android emulator: this single
gap explained both symptoms split out of #1384 into #1391 — a lingering
`DEVICE_IN_USE` claim after a failed `close`, and a published `.ad`
rewritten with duplicated trailing `close` lines when the same close was
retried (each attempt re-recorded a `close` action on top of the one
never rolled back from the prior failure).
Catch the write failure, roll back the just-recorded `close` action
(mirroring the existing repair-armed commit-failure pattern), and let
teardown (lease release, device-claim clear, session delete) complete
regardless — exactly as an ordinary platform-close failure already
doesn't block them. The failure is still surfaced to the caller, but
after teardown, with a corrected hint: retrying the same close is no
longer meaningful since the session is now gone.
Fixes #1391
* refactor(daemon): shrink handleCloseCommand/runSessionCloseTeardown under fallow's complexity gate
CI's fallow code-quality check flagged handleCloseCommand (126 lines,
19 cyclomatic / 16 cognitive) and runSessionCloseTeardown (73 lines) as
exceeding the large-function/high-complexity thresholds after the
prior commit's fix.
Extract runCloseTeardownAndRelease (teardown + lease release + claim
clear + delete + ordered error surfacing) and buildCloseSuccessResponse
(final response shaping) out of handleCloseCommand, and
finalizeOrdinaryCloseScript out of runSessionCloseTeardown. No behavior
change — same control flow, split into named, independently-readable
steps; fallow now reports 0 complexity findings for this diff.
* fix(daemon): preserve the write error's structured details in the close-time save failure
Review feedback on #1392 (thymikee): toOrdinaryCloseSaveScriptFailure
rebuilt the AppError from only the original message, dropping its
machine-readable details.reason ("script_target_exists"), details.path,
and cause. A caller dispatching on those fields (or reading the CLI's
--json error.details) lost them even though the underlying write
failure carried them.
Preserve the original error's details/cause, overriding only the
close-specific hint and retriable:false. Extends the #1391 regression
test to assert the routed close response still carries reason/path.
* refactor(daemon): drop the vestigial close-time rollback, add router-level #1391 coverage
Review feedback on #1392 (thymikee), P2 items:
- The close-time save-script failure's session.actions rollback
(finalizeOrdinaryCloseScript) was left over from an earlier design
where a failed save could keep the session alive for retry. It
never does now — runCloseTeardownAndRelease always tears the
session down regardless of the outcome — so there is no surviving
session for a later write to duplicate the close action on. Drop
the rollback; the durable events.ndjson entry (which the rollback
never touched anyway) and the in-memory action now agree, both
accurately recording that the close happened.
- Add a request-router-level regression (request-router-typed-error.test.ts,
alongside the existing repair-close BLOCKER 2 test it mirrors) proving
the normalized JSON error shape a real client sees: top-level
retriable:false, details.reason/path preserved, and the session torn
down — not just that handleCloseCommand throws the right AppError
when called directly.
* test(daemon): assert the durable close event survives a failed close-time save
Review feedback on #1392 (thymikee), final P2 item: the previous commit
removed the actions rollback because there's no surviving session to
duplicate the close action on, but nothing actually asserted the
durable events.ndjson action.recorded:close event stays put. Flush and
read it back so a future rollback or event-order change can't silently
recreate the in-memory/durable mismatch the removed rollback used to
paper over asymmetrically.
* test(daemon): assert the retained session's in-memory close action, not just the durable event
Review feedback on #1392 (thymikee): the durable-event assertion alone
doesn't catch a reintroduced session.actions.length = actionsBeforeClose
rollback, because that event is queued (and durable) before the write
even attempts — a regression there would leave the assertion passing
while silently reintroducing the in-memory/durable mismatch.
Retain the session object past handleCloseCommand (store.delete only
drops the map entry, not the object a local variable still points at)
and assert its actions array contains exactly one close entry, matching
the durable event count. Verified by temporarily reintroducing the old
rollback locally: this assertion fails (0 !== 1) where the prior
durable-only check did not, then reverted.
* refactor(daemon): model repair close retry as receipt
* refactor(daemon): merge blockingError to state, not explain, the save-script exclusion
Following up on the comment-trimming pass already on this branch: the
device-claim condition (!platformCloseError && !cleanupAggregate) and
the two-line throw sequence right below it both needed a paragraph
explaining why saveScriptError is excluded from one but not the other.
Merge platformCloseError and cleanupAggregate into a single named
blockingError — its name now states the exclusion the comment used to
argue for, and the throw sequence collapses from two ifs to one.
Trimmed the remaining long docblocks in this file the same way: state
what's non-obvious in 1-3 lines instead of re-deriving it in prose.
* refactor(daemon): clarify close script finalization
2026-07-27 16:04:38 +02:00
{
id : 'ios-system-ui-widget-flow' ,
docs : [ '--help:first30' , 'ios-system-ui' ] ,
task : 'On an iOS simulator, add the Calendar widget from SpringBoard, capture visual evidence of the placed widget, return to the installed app com.example.calendar, and close. Plan commands only.' ,
expectations : [ 'validPlanCommands' , 'fullPrefix' , 'usesSnapshotI' , 'opensAndCloses' ] ,
matchers : [
{
id : 'bindsSessionToSpringBoard' ,
pattern : /\bagent-device\s+open\s+com\.apple\.springboard\b[^\n]*--platform\s+ios\b/i ,
} ,
{
id : 'entersEditModeWithCoordinateLongpress' ,
pattern : /\bagent-device\s+longpress\s+-?\d+(?:\.\d+)?\s+-?\d+(?:\.\d+)?\b/i ,
} ,
{
id : 'refreshesSnapshotAfterEnteringEditMode' ,
pattern : /\blongpress\b[\s\S]*\n[^\n]*\bsnapshot\s+-i\b/i ,
} ,
{
id : 'usesScreenshotForSparseGalleryResult' ,
pattern :
/\bagent-device\s+screenshot\b[\s\S]*\n[^\n]*\bagent-device\s+press\s+-?\d+(?:\.\d+)?\s+-?\d+(?:\.\d+)?\b/i ,
} ,
{
id : 'returnsToAppUnderTest' ,
pattern : /\bagent-device\s+open\s+com\.example\.calendar\b[^\n]*--platform\s+ios\b/i ,
} ,
] ,
forbidden : [
{
id : 'noRawSimulatorControl' ,
pattern : /(?:^|\n)(?:xcrun\s+simctl|idb|maestro)\b/i ,
} ,
] ,
} ,
feat(bench): renderer-pinned samples, topic-coverage gate, error-recovery quizzes; trim skillgym to agentic checks (#1411)
* feat(bench): renderer-pinned samples, topic-coverage gate, error quizzes; trim skillgym to agentic checks
The help conformance bench's quoted CLI output is now sourced from
scripts/help-conformance-sample-outputs.mjs, and every sample is rebuilt
through the real production renderers (settle output formatters,
printHumanError, formatSnapshotText, refMutationAdmissionResponse) by
scripts/__tests__/help-conformance-sample-outputs.test.ts — a rendering or
message change fails deterministically instead of leaving the bench grading
against output the CLI no longer prints. This retires the fabricated
recoverable-failure envelope (production never throws a textual settle
timeout; that case is replaced by a real DEVICE_IN_USE recovery quiz).
Bench cases move to scripts/help-conformance-cases.mjs and are enumerated
against the help-topic registry: helpTopicIds() is exported from cli-help,
and scripts/__tests__/help-conformance-topic-coverage.test.ts fails when a
help topic has neither a bench case nor an explicit waiver. New case
families: error-envelope recovery quizzes (device-in-use, stale pinned ref,
ambiguous find match, app-not-installed) pinned to real error text, topic
coverage for tv/web/react-native/debugging/workflow, and a metamorphic twin
of the settled-diff quiz.
The skillgym smoke suite shrinks from 119 cases to the 5 that measure what
only an agentic runner can show: skill routing plus output interpretation
with a proven local CLI help probe (local-cli-help-policy). Its embedded
samples now import the same pinned constants, replacing hand-transcribed
output that had already drifted from the renderer. Knowledge checks belong
to the bench; live fixture behavior belongs to the iOS simulator e2e suite.
* review: drive error samples through the real producers; enforce local-help on the routing smoke
The DEVICE_IN_USE, AMBIGUOUS_MATCH, and APP_NOT_INSTALLED parity tests no
longer hand-author the producer message before rendering: each drives the
actual producer — buildDeviceInUseBySessionError (extracted in
session-open.ts and called by the handler), buildAmbiguousMatchError (now
exported from find.ts), and buildAppNotInstalledError (extracted in
app-resolution.ts and thrown by the resolver). Because each factory is
exported from its producer file and called by the production path, dropping
the production call would make it test-only and fail
check:production-exports — the wiring is gate-enforced, not conventional.
open-and-snapshot now sets requireLocalCliHelp and
allowOnlyLocalCliHelpCommands, so the 'skill plus local help' claim is
observed rather than assumed; without them the case can pass on model prior
alone.
2026-07-27 14:24:36 +02:00
{
id : 'web-managed-backend-loop' ,
docs : [ '--help:first30' , 'web' ] ,
task : 'On a fresh machine that has never run web automation, plan commands to set up and verify the managed web backend, open https://shop.example/login, fill the Email field with "qa@example.com", press the "Sign in" button, verify the "Welcome back" text appears, capture a screenshot to ./artifacts/web-login.png, and close. Plan commands only.' ,
expectations : [ 'validPlanCommands' , 'fullPrefix' , 'usesSnapshotI' , 'opensAndCloses' ] ,
matchers : [
{
id : 'setsUpBackendBeforeOpen' ,
pattern : /\bagent-device\s+web\s+setup\b[\s\S]*\n[^\n]*\bopen\s+https:\/\//i ,
} ,
{ id : 'verifiesBackendWithDoctor' , pattern : /\bagent-device\s+web\s+doctor\b/i } ,
{ id : 'usesWebPlatform' , pattern : /--platform\s+web\b/i } ,
{ id : 'verifiesWelcomeText' , pattern : /\b(?:wait|is|find)\b[^\n]*welcome/i } ,
] ,
forbidden : [
// help web: native mobile/desktop setup commands are out of scope for
// --platform web sessions.
{
id : 'noNativeSetupCommands' ,
pattern : /(?:^|\n)agent-device\s+(?:boot|apps|install|alert|keyboard|perf|logs)\b/i ,
} ,
{ id : 'noStandaloneAgentBrowser' , pattern : /(?:^|\n)agent-browser\b/i } ,
] ,
} ,
{
id : 'react-native-overlay-before-tap' ,
docs : [ '--help:first30' , 'react-native' ] ,
task : 'An Expo dev-client app on the iOS simulator shows a React Native warning overlay in the latest snapshot. Plan the commands that safely get past it and then press the control with id "submit-order". Plan commands only.' ,
expectations : [ 'validPlanCommands' , 'fullPrefix' , 'usesSettleOnMutations' ] ,
matchers : [
{
id : 'usesDismissOverlayCommand' ,
pattern : /(?:^|\n)agent-device\s+react-native\s+dismiss-overlay\b/i ,
} ,
{
id : 'refreshesRefsAfterDismiss' ,
pattern : /dismiss-overlay\b[\s\S]*\n[^\n]*\bsnapshot\s+-i\b/i ,
} ,
{ id : 'pressesSubmitTarget' , pattern : /(?:^|\n)agent-device\s+press\s+[^\n]*submit-order/i } ,
] ,
forbidden : [
// help react-native: never press warning/error overlay text manually;
// the dismiss-overlay command owns LogBox/RedBox targeting.
{
id : 'noManualOverlayPress' ,
pattern : /(?:^|\n)agent-device\s+(?:press|click)\s+[^\n]*(?:warning|error|logbox|redbox)/i ,
} ,
{ id : 'noPlainReloadCommand' , pattern : /(?:^|\n)agent-device\s+reload\b/i } ,
] ,
} ,
{
id : 'debugging-small-log-window' ,
docs : [ '--help:first30' , 'debugging' ] ,
task : 'The "Load diagnostics" control (id "load-diagnostics") in the already-open iOS app intermittently fails. Plan commands to capture a small fresh log window plus request/response metadata around one reproduction. Plan commands only.' ,
expectations : [ 'validPlanCommands' , 'fullPrefix' ] ,
matchers : [
{
id : 'clearsAndRestartsLogs' ,
pattern : /(?:^|\n)agent-device\s+logs\s+clear\s+--restart\b/i ,
} ,
{ id : 'marksBeforeRepro' , pattern : /\blogs\s+mark\b/i } ,
{
id : 'reproducesTargetPress' ,
pattern : /(?:^|\n)agent-device\s+press\s+[^\n]*load-diagnostics/i ,
} ,
{ id : 'readsLogPath' , pattern : /\blogs\s+path\b/i } ,
{ id : 'dumpsNetworkMetadata' , pattern : /\bnetwork\s+dump\b/i } ,
] ,
forbidden : [
{ id : 'noSessionReopen' , pattern : /(?:^|\n)agent-device\s+open\b/i } ,
{ id : 'noSplitLogRestart' , pattern : /\blogs\s+stop\b/i } ,
] ,
} ,
{
id : 'workflow-install-artifact-before-open' ,
docs : [ '--help:first30' , 'workflow' ] ,
task : 'A local Android build artifact ./dist/app-release.apk contains the app com.example.orders, which is not yet on the emulator. Plan commands to get it running with fresh state and confirm its first screen shows "Orders". Plan commands only.' ,
expectations : [ 'validPlanCommands' , 'fullPrefix' , 'usesSnapshotI' ] ,
matchers : [
{
id : 'installsIdThenArtifact' ,
pattern : /(?:^|\n)agent-device\s+install\s+com\.example\.orders\s+\S*app-release\.apk/i ,
} ,
{
id : 'opensFreshAfterInstall' ,
pattern : /\binstall\b[\s\S]*\n[^\n]*\bopen\s+com\.example\.orders\b[^\n]*--relaunch\b/i ,
} ,
{ id : 'verifiesFirstScreen' , pattern : /\b(?:wait|find|is|get)\b[^\n]*orders/i } ,
] ,
forbidden : [
// help workflow: install for a first install; reinstall only when
// explicitly requested; never open an artifact path.
{ id : 'noReinstall' , pattern : /(?:^|\n)agent-device\s+reinstall\b/i } ,
{ id : 'noOpenArtifactPath' , pattern : /(?:^|\n)agent-device\s+open\s+[^\n]*\.apk\b/i } ,
] ,
} ,
feat(cli): compact workflow help card + version header (#1663)
* feat(cli): compact workflow help card + version header
Shrinks the per-task agent protocol tax of the help/skill surface.
`agent-device help workflow` drops from 41025 to 8466 bytes (-79%) by
moving depth into new `help scripting` (save-script, secret-safe
fills, batch JSON, replay divergence/repair) and `help gestures`
(multi-touch shapes/quirks) topics, and folding a few paragraphs into
topics that already owned the subject (help debugging,
help physical-device, help validate). Content is moved, not deleted.
Every `help <topic>` first line is now `agent-device <version> —
<topic>`, so the skill router reads the CLI version off the mandatory
first help read instead of a separate `agent-device --version` call.
SKILL.md is updated to do that and stays a thin router otherwise.
The compact card also gains two terse behavioral rules: chain
confident consecutive steps with `&&` (falling back to one command at
a time when uncertain), and confirm the requested end state is
actually visible on screen before declaring a task done.
help-conformance-bench (22 cases x 2 runners) improves after the
change: 29/44 -> 32/44 passing checks.
* fix(cli): review follow-ups on the compact workflow card (#1663)
Three fixes from PR review:
- Extend the help-conformance plan validator to split a command line
on unquoted && and validate each chained segment independently, so
a plan that follows the workflow card's "chain confident consecutive
steps with &&" guidance is accepted instead of rejected as one
shell-projection violation. && inside a quoted selector value (e.g.
label="A && B") is not a chain boundary and does not split. Adds
unit tests for the splitter and a chains-confident-consecutive-
settle-steps conformance case. batch stays out of this: it is
deliberately stop-only.
- Replace the literal @ref placeholder the compact card used in its
own "snapshot -s @ref" example with a concrete ref
(snapshot -s @e12 (the current concrete ref)), matching the same
card's rule against placeholder targets. Reverts the test to demand
the concrete shape.
- Give help scripting and help gestures real conformance cases
instead of waivers: a secret-safe recorded-fill + publish case, and
an Android transform-then-verify case whose exact verification text
only appears in the gestures topic. Removes both waivers.
help-conformance-bench (25 cases x 2 runners, repeat=1) after these
fixes: two full runs landed at 32/50 and 33/50. That is on par with
the pre-change baseline (29/44) once the topic-untouched cases'
run-to-run swings are accounted for (confirmed noise: one case with
zero exposure to any change here flipped 10/10 -> 1/10 on a runner API
error, and another swung across all three post-fix runs). The new
scripting case now passes 8/8 for both runners; the new chaining case
correctly reports the model's choice not to chain as a soft signal,
not a validator failure.
* fix(cli): update session.test.ts help pointer for moved script-authoring content
* fix(cli): reject empty && chain operands in the plan validator (#1663)
splitOnUnquotedAnd() previously trimmed and filtered out empty
segments, so a plan with a leading (`&& press ...`), trailing
(`press ... &&`), or doubled (`a && && b`) operator passed
validPlanCommands even though a real shell rejects all three as a
syntax error. The validator would bless a plan that fails at
execution.
Empty segments are now surfaced as an `empty-chain-operand` issue
instead of being silently dropped. The quoted-&& non-split behavior
(label="A && B") is unchanged, and a normal single command with no
chain still parses identically to before.
Adds regression tests for all three empty-operand shapes plus the
quoted-&& case.
2026-08-07 13:26:48 +02:00
{
// Review of the compact workflow card's && guidance found the plan
// validator failed a plan that followed it (unquoted && classified as
// shell-projection). Both target elements are already named/unambiguous
// here, which is exactly the "confident consecutive steps" case the card
// describes, so usesConfidentChaining is a real (not just possible)
// expectation, and validPlanCommands proves the fixed validator accepts
// the chained shape end to end.
id : 'chains-confident-consecutive-settle-steps' ,
docs : [ '--help:first30' , 'workflow' ] ,
task : 'The Search tab is visible, labeled "Search", and known to reveal a search field also labeled "Search" with no other candidate on screen. Plan commands to press the Search tab and fill that field with "react native", settling after each step, then close. Plan commands only.' ,
expectations : [
'validPlanCommands' ,
'fullPrefix' ,
'usesSettleOnMutations' ,
'usesConfidentChaining' ,
] ,
matchers : [
{
id : 'pressesSearchTab' ,
pattern : /\bagent-device\s+press\b[^\n]*label="?search"?[^\n]*--settle\b/i ,
} ,
{
id : 'fillsSearchField' ,
pattern :
/\bagent-device\s+fill\b[^\n]*label="?search"?[^\n]*(?:"react native"|'react native')[^\n]*--settle\b/i ,
} ,
] ,
} ,
{
// help scripting owns --record-as secret-safe fills and save-script
// authoring now that this content left the mandatory workflow card;
// this proves an agent can actually plan the loop from the topic alone.
id : 'scripting-secret-safe-recorded-login' ,
docs : [ '--help:first30' , 'scripting' ] ,
task : 'Author a reusable login script for the installed app com.example.app that never records the literal password. The AD_VAR_PASSWORD environment variable is already set in your shell, so do not plan a shell export line. Arm recording on open with --save-script=login.ad, fill the password field (id="password") from AD_VAR_PASSWORD using --record-as, verify the login succeeded, then publish the script without closing the session. Plan agent-device commands only.' ,
expectations : [ 'validPlanCommands' , 'fullPrefix' ] ,
matchers : [
{
id : 'armsSaveScriptOnOpen' ,
pattern : /\bagent-device\s+open\s+com\.example\.app\b[^\n]*--save-script[=\s]*login\.ad/i ,
} ,
{
id : 'recordsSecretSafeFill' ,
pattern :
/\bagent-device\s+fill\s+(?:'|")?id="?password"?(?:'|")?\s+"?\$AD_VAR_PASSWORD"?[^\n]*--record-as\s+PASSWORD\b/i ,
} ,
{
id : 'verifiesLoginSucceeded' ,
pattern : /\b(?:wait|is|get|find)\b/i ,
} ,
{ id : 'publishesWithoutClosing' , pattern : /\bagent-device\s+session\s+save-script\b/i } ,
] ,
forbidden : [
{ id : 'noBareClose' , pattern : /(?:^|\n)agent-device\s+close\b/i } ,
{ id : 'noNoRecordOnSecretFill' , pattern : /--no-record/i } ,
] ,
} ,
{
// help gestures owns multi-touch shapes now that this content left the
// mandatory workflow card. The exact verification text ("pan changed
// yes") only appears in the gestures topic's own example, so a correct
// plan proves the model actually read it rather than guessing a shape.
id : 'gestures-android-transform-then-verify' ,
docs : [ '--help:first30' , 'gestures' ] ,
task : 'On the already-open Android app, plan a combined pan/scale/rotate transform gesture centered at (200, 420) with dx=80, dy=-40, scale=2, rotate=35 degrees over 700ms, then verify the app-reported pan change using the exact confirmation text shown in the gesture reference. Plan commands only.' ,
expectations : [ 'validPlanCommands' , 'fullPrefix' ] ,
matchers : [
{
id : 'runsAndroidTransform' ,
pattern :
/\bagent-device\s+gesture\s+transform\s+200\s+420\s+80\s+-40\s+2\s+35\s+700\b[^\n]*--platform\s+android\b/i ,
} ,
{
id : 'verifiesSemanticPanChange' ,
pattern : /\bagent-device\s+wait\s+text\s+"pan changed yes"[^\n]*--platform\s+android\b/i ,
} ,
] ,
forbidden : [ { id : 'noRawCoordinateTarget' , pattern : RAW _COORDINATE _TARGET } ] ,
} ,
feat(bench): renderer-pinned samples, topic-coverage gate, error-recovery quizzes; trim skillgym to agentic checks (#1411)
* feat(bench): renderer-pinned samples, topic-coverage gate, error quizzes; trim skillgym to agentic checks
The help conformance bench's quoted CLI output is now sourced from
scripts/help-conformance-sample-outputs.mjs, and every sample is rebuilt
through the real production renderers (settle output formatters,
printHumanError, formatSnapshotText, refMutationAdmissionResponse) by
scripts/__tests__/help-conformance-sample-outputs.test.ts — a rendering or
message change fails deterministically instead of leaving the bench grading
against output the CLI no longer prints. This retires the fabricated
recoverable-failure envelope (production never throws a textual settle
timeout; that case is replaced by a real DEVICE_IN_USE recovery quiz).
Bench cases move to scripts/help-conformance-cases.mjs and are enumerated
against the help-topic registry: helpTopicIds() is exported from cli-help,
and scripts/__tests__/help-conformance-topic-coverage.test.ts fails when a
help topic has neither a bench case nor an explicit waiver. New case
families: error-envelope recovery quizzes (device-in-use, stale pinned ref,
ambiguous find match, app-not-installed) pinned to real error text, topic
coverage for tv/web/react-native/debugging/workflow, and a metamorphic twin
of the settled-diff quiz.
The skillgym smoke suite shrinks from 119 cases to the 5 that measure what
only an agentic runner can show: skill routing plus output interpretation
with a proven local CLI help probe (local-cli-help-policy). Its embedded
samples now import the same pinned constants, replacing hand-transcribed
output that had already drifted from the renderer. Knowledge checks belong
to the bench; live fixture behavior belongs to the iOS simulator e2e suite.
* review: drive error samples through the real producers; enforce local-help on the routing smoke
The DEVICE_IN_USE, AMBIGUOUS_MATCH, and APP_NOT_INSTALLED parity tests no
longer hand-author the producer message before rendering: each drives the
actual producer — buildDeviceInUseBySessionError (extracted in
session-open.ts and called by the handler), buildAmbiguousMatchError (now
exported from find.ts), and buildAppNotInstalledError (extracted in
app-resolution.ts and thrown by the resolver). Because each factory is
exported from its producer file and called by the production path, dropping
the production call would make it test-only and fail
check:production-exports — the wiring is gate-enforced, not conventional.
open-and-snapshot now sets requireLocalCliHelp and
allowOnlyLocalCliHelpCommands, so the 'skill plus local help' claim is
observed rather than assumed; without them the case can pass on model prior
alone.
2026-07-27 14:24:36 +02:00
// Next-command quiz cases: captured output (pinned to the real renderer by
// scripts/__tests__/help-conformance-sample-outputs.test.ts) plus a task,
// scored by regex instead of the named expectation scorers above.
{
id : 'settle-diff-is-observation' ,
docs : [ '--help:first30' ] ,
task : ` You already ran this command and observed its settled output:
${ sampleText ( SETTLE _TAIL _SAMPLE ) }
Use the output already shown to determine whether the feed-search UI is present, then close the session. What command should run next? ` ,
expectations : [ 'validPlanCommands' , 'fullPrefix' ] ,
matchers : [ { id : 'plansClose' , pattern : /(?:^|\n)(?:agent-device\s+)?close\b/i } ] ,
forbidden : [
{ id : 'noSnapshot' , pattern : /\bsnapshot\b/i } ,
{ id : 'noWait' , pattern : /\bwait\b/i } ,
{ id : 'noFind' , pattern : /\bfind\b/i } ,
{ id : 'noGet' , pattern : /\bget\b/i } ,
{ id : 'noIs' , pattern : /\bis\b/i } ,
{ id : 'noPressOrClick' , pattern : /\b(?:press|click)\b/i } ,
] ,
} ,
{
id : 'sample-output-settled-diff-next-target' ,
docs : [ '--help:first30' ] ,
task : quiz (
SETTLE _DIFF _SAMPLE ,
'The task is to open the matching account result. What command should run next?' ,
) ,
expectations : [ 'validPlanCommands' , 'fullPrefix' ] ,
matchers : [
{ id : 'pressOrClickOrTap' , pattern : /\b(?:press|click|tap)\b/i } ,
{ id : 'usesE64RefOrLabel' , pattern : /@e64\b|label=(?:["']?@callstack\.com["']?)/i } ,
{ id : 'usesSettleFlag' , pattern : /--settle\b/i } ,
] ,
forbidden : [
{ id : 'noSnapshot' , pattern : /\bsnapshot\b/i } ,
{ id : 'noWaitStable' , pattern : /wait\s+stable/i } ,
{ id : 'noFill' , pattern : /\bfill\b/i } ,
2026-08-10 20:43:01 +02:00
{ id : 'noBareRefTarget' , pattern : BARE _REF _TARGET } ,
feat(bench): renderer-pinned samples, topic-coverage gate, error-recovery quizzes; trim skillgym to agentic checks (#1411)
* feat(bench): renderer-pinned samples, topic-coverage gate, error quizzes; trim skillgym to agentic checks
The help conformance bench's quoted CLI output is now sourced from
scripts/help-conformance-sample-outputs.mjs, and every sample is rebuilt
through the real production renderers (settle output formatters,
printHumanError, formatSnapshotText, refMutationAdmissionResponse) by
scripts/__tests__/help-conformance-sample-outputs.test.ts — a rendering or
message change fails deterministically instead of leaving the bench grading
against output the CLI no longer prints. This retires the fabricated
recoverable-failure envelope (production never throws a textual settle
timeout; that case is replaced by a real DEVICE_IN_USE recovery quiz).
Bench cases move to scripts/help-conformance-cases.mjs and are enumerated
against the help-topic registry: helpTopicIds() is exported from cli-help,
and scripts/__tests__/help-conformance-topic-coverage.test.ts fails when a
help topic has neither a bench case nor an explicit waiver. New case
families: error-envelope recovery quizzes (device-in-use, stale pinned ref,
ambiguous find match, app-not-installed) pinned to real error text, topic
coverage for tv/web/react-native/debugging/workflow, and a metamorphic twin
of the settled-diff quiz.
The skillgym smoke suite shrinks from 119 cases to the 5 that measure what
only an agentic runner can show: skill routing plus output interpretation
with a proven local CLI help probe (local-cli-help-policy). Its embedded
samples now import the same pinned constants, replacing hand-transcribed
output that had already drifted from the renderer. Knowledge checks belong
to the bench; live fixture behavior belongs to the iOS simulator e2e suite.
* review: drive error samples through the real producers; enforce local-help on the routing smoke
The DEVICE_IN_USE, AMBIGUOUS_MATCH, and APP_NOT_INSTALLED parity tests no
longer hand-author the producer message before rendering: each drives the
actual producer — buildDeviceInUseBySessionError (extracted in
session-open.ts and called by the handler), buildAmbiguousMatchError (now
exported from find.ts), and buildAppNotInstalledError (extracted in
app-resolution.ts and thrown by the resolver). Because each factory is
exported from its producer file and called by the production path, dropping
the production call would make it test-only and fail
check:production-exports — the wiring is gate-enforced, not conventional.
open-and-snapshot now sets requireLocalCliHelp and
allowOnlyLocalCliHelpCommands, so the 'skill plus local help' claim is
observed rather than assumed; without them the case can pass on model prior
alone.
2026-07-27 14:24:36 +02:00
{ id : 'noRawCoordinateTarget' , pattern : RAW _COORDINATE _TARGET } ,
] ,
} ,
{
id : 'metamorphic-settled-diff-next-target-notes' ,
docs : [ '--help:first30' ] ,
task : quiz (
SETTLE _DIFF _SAMPLE _NOTES ,
'The task is to open the matching list result. What command should run next?' ,
) ,
expectations : [ 'validPlanCommands' , 'fullPrefix' ] ,
matchers : [
{ id : 'pressOrClickOrTap' , pattern : /\b(?:press|click|tap)\b/i } ,
{ id : 'usesE21RefOrLabel' , pattern : /@e21\b|label=(?:["']?groceries list["']?)/i } ,
{ id : 'usesSettleFlag' , pattern : /--settle\b/i } ,
] ,
forbidden : [
{ id : 'noSnapshot' , pattern : /\bsnapshot\b/i } ,
{ id : 'noWaitStable' , pattern : /wait\s+stable/i } ,
{ id : 'noFill' , pattern : /\bfill\b/i } ,
{ id : 'noCallstackLeakage' , pattern : /(?:callstack|@e64)/i } ,
2026-08-10 20:43:01 +02:00
{ id : 'noBareRefTarget' , pattern : BARE _REF _TARGET } ,
{ id : 'noRawCoordinateTarget' , pattern : RAW _COORDINATE _TARGET } ,
] ,
} ,
{
// ADR 0014: settled tails pin unchanged interactive refs because the
// partial frame admits only refs copied with the response generation.
id : 'sample-output-settle-tail-pinned-ref-copied-exactly' ,
docs : [ '--help:first30' ] ,
task : quiz (
SETTLE _TAIL _SAMPLE ,
'The task is to open the Profile tab. What command should run next?' ,
) ,
expectations : [ 'validPlanCommands' , 'fullPrefix' ] ,
matchers : [
{
id : 'pressesPinnedTailRefOrExactLabel' ,
pattern :
/(?:^|\n)agent-device\s+(?:press|click)\s+(?:@e40~s5\b|'?label="?Profile"?'?)[^\n]*--settle\b/i ,
} ,
] ,
forbidden : [
{ id : 'noUnpinnedRef' , pattern : /@e40(?!~s5\b)/i } ,
{ id : 'noBareRefTarget' , pattern : BARE _REF _TARGET } ,
{ id : 'noSnapshot' , pattern : /\bsnapshot\b/i } ,
feat(bench): renderer-pinned samples, topic-coverage gate, error-recovery quizzes; trim skillgym to agentic checks (#1411)
* feat(bench): renderer-pinned samples, topic-coverage gate, error quizzes; trim skillgym to agentic checks
The help conformance bench's quoted CLI output is now sourced from
scripts/help-conformance-sample-outputs.mjs, and every sample is rebuilt
through the real production renderers (settle output formatters,
printHumanError, formatSnapshotText, refMutationAdmissionResponse) by
scripts/__tests__/help-conformance-sample-outputs.test.ts — a rendering or
message change fails deterministically instead of leaving the bench grading
against output the CLI no longer prints. This retires the fabricated
recoverable-failure envelope (production never throws a textual settle
timeout; that case is replaced by a real DEVICE_IN_USE recovery quiz).
Bench cases move to scripts/help-conformance-cases.mjs and are enumerated
against the help-topic registry: helpTopicIds() is exported from cli-help,
and scripts/__tests__/help-conformance-topic-coverage.test.ts fails when a
help topic has neither a bench case nor an explicit waiver. New case
families: error-envelope recovery quizzes (device-in-use, stale pinned ref,
ambiguous find match, app-not-installed) pinned to real error text, topic
coverage for tv/web/react-native/debugging/workflow, and a metamorphic twin
of the settled-diff quiz.
The skillgym smoke suite shrinks from 119 cases to the 5 that measure what
only an agentic runner can show: skill routing plus output interpretation
with a proven local CLI help probe (local-cli-help-policy). Its embedded
samples now import the same pinned constants, replacing hand-transcribed
output that had already drifted from the renderer. Knowledge checks belong
to the bench; live fixture behavior belongs to the iOS simulator e2e suite.
* review: drive error samples through the real producers; enforce local-help on the routing smoke
The DEVICE_IN_USE, AMBIGUOUS_MATCH, and APP_NOT_INSTALLED parity tests no
longer hand-author the producer message before rendering: each drives the
actual producer — buildDeviceInUseBySessionError (extracted in
session-open.ts and called by the handler), buildAmbiguousMatchError (now
exported from find.ts), and buildAppNotInstalledError (extracted in
app-resolution.ts and thrown by the resolver). Because each factory is
exported from its producer file and called by the production path, dropping
the production call would make it test-only and fail
check:production-exports — the wiring is gate-enforced, not conventional.
open-and-snapshot now sets requireLocalCliHelp and
allowOnlyLocalCliHelpCommands, so the 'skill plus local help' claim is
observed rather than assumed; without them the case can pass on model prior
alone.
2026-07-27 14:24:36 +02:00
{ id : 'noRawCoordinateTarget' , pattern : RAW _COORDINATE _TARGET } ,
] ,
} ,
feat: support --settle on scroll and back (#1638) (#1650)
* feat: support --settle on scroll and back (#1638)
Scroll-then-observe and back-then-observe are legitimate agent pairs, but
the post-action observation registry never grew past the touch commands, so
`--settle` on either was rejected with INVALID_ARGS — burning a tool call
each in AppControlBench's bsky-16.
Both commands now carry the `settle` descriptor trait, and every surface
derives from it rather than a hand list: CLI allowed flags, MCP/SDK input
fields, the flag-sourced timeout envelope, and MCP ref-pinning. The CLI
flag/metadata helpers moved out of the interaction family into
post-action-observation-grammar.ts (back is a system command), and
SETTLE_REF_ISSUING_TOOLS became a derivation — a hand list would have
silently stopped pinning the new commands' refs.
settleAfterInteraction and the new settleObservationCommand are two entry
points over one engine: same loop, storage, hints, and diff bounds, with the
target-less path supplying its own baseline and no proximity point. The
daemon reaches that command through the runtime surface, never by importing
`commands/` (R2) — the same seam the touch handlers use for press/fill —
and generic-settle.ts is loaded through a lazy `await import` returning a
closure, so the interaction runtime subgraph stays out of this dispatcher's
static graph (a static edge folded ~18 files into the daemon-server type
cycle; R10 caught it).
Both of generic-settle's orderings are load-bearing and tested: the baseline
is frozen before dispatch (and before the Android dialog preflight), and the
observation runs after markDeferredInteractionOutcome so settle's first
capture folds in the #1542 stabilization rather than racing it. The ADR 0014
"a settled diff publishes refs" rule moved to settle-ref-issuance.ts, shared
by both routes.
One divergence is deliberate: scroll/back resolve no element, so the diff
baseline is the session's STORED pre-action tree — "settled tree vs the
last tree you observed" — not press's freshly resolved pre-action capture.
Both commands also switch to preserve-daemon on timeout, which changes the
non-settle path too: with --settle their dominant hang mode is now a wedged
accessibility bridge, and a timed-out capture must not reset the daemon and
lose every session (#1105). The reviewed-set gate records it.
Live-validated on an iOS 26.2 simulator (Settings): scroll --settle settled
in 1786ms with a +6/-6 diff carrying fresh refs; back --settle in 771ms with
+15/-6. Alternating cost runs, one call vs the pair it replaces:
scroll 2.9-3.0s vs 5.3-5.6s, back 3.1-3.2s vs 4.7-5.1s. Those include the
#1627 deep-capture extension.
* fix: render settled-diff refs paste-ready in CLI output
A settled diff activates a PARTIAL ref frame (ADR 0014), which admits only
the pinned `@eN~s<gen>` form of the refs it issued. The unchanged-interactive
tail already rendered that way, but the diff's own added lines rendered the
bare `@eN` embedded in the snapshot line — so a CLI caller who copied the
ref the diff just handed them got `plain_ref_requires_complete_frame` and had
to append the generation by hand.
Added lines now render pinned when the response carries `refsGeneration`,
exactly like the tail. Removed lines render verbatim: they name elements that
just left the screen, and `SettleDiffLine` never gives them a ref.
This is not new to scroll/back — press/click/fill/longpress had the same gap
since #1101. MCP was never affected: its ref-pin store rewrites plain refs on
the way in, which is why the model never sees a suffix.
Live: `scroll down --settle` now emits `+ @e14~s218078 [cell] "Game Center"`,
and `press @e14~s218078` copied straight out of that line taps successfully.
* test: record the pinned-diff-ref bytes in the output-economy baseline
Rendering added diff-line refs pinned costs 8 bytes in the two settle CLI
text samples (two `~s<gen>` suffixes). The output-economy baseline is the
tripwire for exactly this, so the increase takes an explicit reviewed waiver
rather than a silent baseline bump — the same one the settled TAIL's pins
already carry, for the same ADR 0014 reason.
Only `bytes` moves: lines, refs, hints, and shape are unchanged, which is the
evidence that this is a suffix on existing refs and not a new payload.
Caught by CI, not locally: `pnpm test:unit` runs unit-core and
subprocess-stub only, while the Coverage lane runs every vitest project.
* test: prove the generic settle degrades when its runtime cannot be built
`createGenericSettleRuntime` catches and returns undefined so an observation
that cannot even start does not fail an action that already succeeded. That
was a claim in a docstring with nothing behind it — the one changed line the
coverage gate reported uncovered (95/96).
The test puts the session in the state the catch exists for: the router
handed us a session that is no longer in the store, so building the settle
runtime throws SESSION_NOT_FOUND. The response keeps its scroll result and
simply carries no settle payload. Removing the try/catch fails it.
* build: teach fallow that vi.mock reaches pinOwnProcessStartTime dynamically
Not from this PR: #1642 added `pinOwnProcessStartTime` on main, and its three
consumers reach it the only way a Vitest module mock can —
`vi.mock(path, async (importOriginal) => (await import('...')).pinOwnProcessStartTime(...))`.
Dependency analysis cannot follow that dynamic import to a consumer, so the
export reads as dead the moment any PR pulls that file into its audit scope.
This PR is the one that did.
The entry records the consumers by path and the reason, matching the
daemon route-handler entry directly above it, which exists for the same
dynamic-`import()` limitation.
* refactor: adopt the best of the parallel #1653 implementation
Two sessions independently built #1638 (PR #1650 and PR #1653) and converged
on the same architecture — trait in the registry, one engine with two entry
points, runtime-command seam, lazy import, preserve-daemon, stored-baseline
honesty. #1650 continues; this folds in what #1653 did better:
- The agent-facing help core loop (cli-help.ts) now names scroll and back as
settle-capable. Without this, the benchmarked closed-grammar help line kept
instructing agents that --settle is only for press/click/fill/longpress —
actively steering the AppControlBench models away from what #1638 shipped.
- issueSettleRefs moves into session-snapshot.ts, beside the partial-frame
primitive it wraps, deleting the single-function settle-ref-issuance module.
- Their seam tests: back reader→writer settle plumbing, back CLI settle
rendering, and a trait-less generic command (home) ignoring a stray settle
flag rather than observing or rejecting.
What #1650 had that #1653 lacked, for the record: the SETTLE_REF_ISSUING_TOOLS
registry derivation (without it, MCP never pins a scroll/back settle diff's
refs and the partial frame rejects every follow-up), BackCommandResult.settle
in contracts, back's MCP output schema, paste-ready pinned diff refs, and the
docs/changelog/baseline surfaces.
* bench: help-conformance case for settled scroll-to-find planning
The #1638 extension of the closed --settle grammar to scroll/back is the
feature's entire payoff — collapsing scroll-then-observe into one call — and
the closed command list is an enumerated N whose enumerator is this bench.
The regex over the help text proves the sentence exists; this case checks
whether a model plans differently because of it.
One focused case, deliberately not coached: a pinned visible-first snapshot
(rendered by formatSnapshotText, pinned by the sample-producers gate) whose
wanted row is summarized off-screen with no ref anywhere in the output. The
tempting pre-#1638 plan is `scroll` plus a separate `snapshot -i`; acceptance
is the single settled call. Scoring was verified against eight plan shapes in
both directions before recording.
Model-backed record (claude-haiku-4-5, 3 trials, current help): 0/3 — but the
decomposition is the finding. Settle eligibility GENERALIZED (3/3 trials put
--settle on scroll unprompted; the mutation-suffix framing concern did not
materialize) and the two-call habit is residual (1/3). All three trials failed
on `scroll @e3 down --settle` — the pre-existing #1366 scroll-takes-no-target
confusion, which the live CLI recovers with a dedicated hint but a single-shot
bench cannot. The recorded gap is therefore a first-30 doc gap (nothing
teaches that scroll takes no target), not a settle-eligibility gap; tuning the
case until it passes would just delete the evidence.
2026-08-06 20:04:16 +02:00
{
// #1638/#1650: the closed --settle grammar grew scroll and back, and this
// extension IS the feature's payoff — collapsing scroll-then-observe into
// one call. The old guidance framed settle as a mutation suffix, and
// scroll reads as navigation, so eligibility generalizing is exactly what
// this case checks. The task deliberately does not mention settle: the
// wanted row is off-screen with no ref anywhere in the output, the
// tempting pre-#1638 plan is `scroll` + a separate `snapshot -i`, and
// acceptance is the single settled call.
id : 'sample-output-offscreen-target-scrolls-settled' ,
docs : [ '--help:first30' ] ,
task : quiz (
OFFSCREEN _TARGET _SNAPSHOT _SAMPLE ,
'The task is to open the Notifications row of this list. What command should run next?' ,
) ,
expectations : [ 'validPlanCommands' , 'fullPrefix' ] ,
matchers : [
{
id : 'scrollsDownSettled' ,
pattern : /(?:^|\n)(?:agent-device\s+)?scroll\s+down\b[^\n]*--settle\b/i ,
} ,
] ,
forbidden : [
// The two-call habit this case exists to catch: a scroll line without
// --settle means a separate observation call is coming.
{
id : 'noUnsettledScroll' ,
pattern : /(?:^|\n)(?:agent-device\s+)?scroll\b(?:(?!--settle)[^\n])*(?=\n|$)/i ,
} ,
{ id : 'noSnapshot' , pattern : /\bsnapshot\b/i } ,
{ id : 'noWaitStable' , pattern : /wait\s+stable/i } ,
// Notifications never appears in the output, so any bare @eN press is a
// guessed ref, not a resolved target.
{
id : 'noGuessedRef' ,
pattern : /(?:^|\n)(?:agent-device\s+)?(?:press|click|fill|longpress)\s+@e\d/i ,
} ,
{ id : 'noRawCoordinateTarget' , pattern : RAW _COORDINATE _TARGET } ,
] ,
} ,
feat(bench): renderer-pinned samples, topic-coverage gate, error-recovery quizzes; trim skillgym to agentic checks (#1411)
* feat(bench): renderer-pinned samples, topic-coverage gate, error quizzes; trim skillgym to agentic checks
The help conformance bench's quoted CLI output is now sourced from
scripts/help-conformance-sample-outputs.mjs, and every sample is rebuilt
through the real production renderers (settle output formatters,
printHumanError, formatSnapshotText, refMutationAdmissionResponse) by
scripts/__tests__/help-conformance-sample-outputs.test.ts — a rendering or
message change fails deterministically instead of leaving the bench grading
against output the CLI no longer prints. This retires the fabricated
recoverable-failure envelope (production never throws a textual settle
timeout; that case is replaced by a real DEVICE_IN_USE recovery quiz).
Bench cases move to scripts/help-conformance-cases.mjs and are enumerated
against the help-topic registry: helpTopicIds() is exported from cli-help,
and scripts/__tests__/help-conformance-topic-coverage.test.ts fails when a
help topic has neither a bench case nor an explicit waiver. New case
families: error-envelope recovery quizzes (device-in-use, stale pinned ref,
ambiguous find match, app-not-installed) pinned to real error text, topic
coverage for tv/web/react-native/debugging/workflow, and a metamorphic twin
of the settled-diff quiz.
The skillgym smoke suite shrinks from 119 cases to the 5 that measure what
only an agentic runner can show: skill routing plus output interpretation
with a proven local CLI help probe (local-cli-help-policy). Its embedded
samples now import the same pinned constants, replacing hand-transcribed
output that had already drifted from the renderer. Knowledge checks belong
to the bench; live fixture behavior belongs to the iOS simulator e2e suite.
* review: drive error samples through the real producers; enforce local-help on the routing smoke
The DEVICE_IN_USE, AMBIGUOUS_MATCH, and APP_NOT_INSTALLED parity tests no
longer hand-author the producer message before rendering: each drives the
actual producer — buildDeviceInUseBySessionError (extracted in
session-open.ts and called by the handler), buildAmbiguousMatchError (now
exported from find.ts), and buildAppNotInstalledError (extracted in
app-resolution.ts and thrown by the resolver). Because each factory is
exported from its producer file and called by the production path, dropping
the production call would make it test-only and fail
check:production-exports — the wiring is gate-enforced, not conventional.
open-and-snapshot now sets requireLocalCliHelp and
allowOnlyLocalCliHelpCommands, so the 'skill plus local help' claim is
observed rather than assumed; without them the case can pass on model prior
alone.
2026-07-27 14:24:36 +02:00
{
id : 'sample-output-not-settled-needs-observe' ,
docs : [ '--help:first30' ] ,
task : quiz (
NOT _SETTLED _SAMPLE ,
'The next target is not known yet. What command should run next?' ,
) ,
expectations : [ 'validPlanCommands' , 'fullPrefix' ] ,
matchers : [
{
id : 'observesBeforeActing' ,
pattern : /(?:^|\n)(?:agent-device\s+)?(?:wait\b|snapshot\b[^\n]*-i\b)/i ,
} ,
] ,
forbidden : [
{
id : 'noBareRefMutation' ,
pattern : /(?:^|\n)(?:agent-device\s+)?(?:press|click|fill|longpress)\s+@e\d+/i ,
} ,
{ id : 'noRawCoordinateTarget' , pattern : RAW _COORDINATE _TARGET } ,
] ,
} ,
{
id : 'sample-output-device-in-use-reuses-session' ,
docs : [ '--help:first30' ] ,
2026-07-28 15:46:27 +02:00
recovery : { code : 'DEVICE_IN_USE' , sample : DEVICE _IN _USE _SAMPLE } ,
feat(bench): renderer-pinned samples, topic-coverage gate, error-recovery quizzes; trim skillgym to agentic checks (#1411)
* feat(bench): renderer-pinned samples, topic-coverage gate, error quizzes; trim skillgym to agentic checks
The help conformance bench's quoted CLI output is now sourced from
scripts/help-conformance-sample-outputs.mjs, and every sample is rebuilt
through the real production renderers (settle output formatters,
printHumanError, formatSnapshotText, refMutationAdmissionResponse) by
scripts/__tests__/help-conformance-sample-outputs.test.ts — a rendering or
message change fails deterministically instead of leaving the bench grading
against output the CLI no longer prints. This retires the fabricated
recoverable-failure envelope (production never throws a textual settle
timeout; that case is replaced by a real DEVICE_IN_USE recovery quiz).
Bench cases move to scripts/help-conformance-cases.mjs and are enumerated
against the help-topic registry: helpTopicIds() is exported from cli-help,
and scripts/__tests__/help-conformance-topic-coverage.test.ts fails when a
help topic has neither a bench case nor an explicit waiver. New case
families: error-envelope recovery quizzes (device-in-use, stale pinned ref,
ambiguous find match, app-not-installed) pinned to real error text, topic
coverage for tv/web/react-native/debugging/workflow, and a metamorphic twin
of the settled-diff quiz.
The skillgym smoke suite shrinks from 119 cases to the 5 that measure what
only an agentic runner can show: skill routing plus output interpretation
with a proven local CLI help probe (local-cli-help-policy). Its embedded
samples now import the same pinned constants, replacing hand-transcribed
output that had already drifted from the renderer. Knowledge checks belong
to the bench; live fixture behavior belongs to the iOS simulator e2e suite.
* review: drive error samples through the real producers; enforce local-help on the routing smoke
The DEVICE_IN_USE, AMBIGUOUS_MATCH, and APP_NOT_INSTALLED parity tests no
longer hand-author the producer message before rendering: each drives the
actual producer — buildDeviceInUseBySessionError (extracted in
session-open.ts and called by the handler), buildAmbiguousMatchError (now
exported from find.ts), and buildAppNotInstalledError (extracted in
app-resolution.ts and thrown by the resolver). Because each factory is
exported from its producer file and called by the production path, dropping
the production call would make it test-only and fail
check:production-exports — the wiring is gate-enforced, not conventional.
open-and-snapshot now sets requireLocalCliHelp and
allowOnlyLocalCliHelpCommands, so the 'skill plus local help' claim is
observed rather than assumed; without them the case can pass on model prior
alone.
2026-07-27 14:24:36 +02:00
task : quiz (
DEVICE _IN _USE _SAMPLE ,
'You are continuing the checkout flow that the "checkout" session was already running on this device. What command should run next?' ,
) ,
expectations : [ 'validPlanCommands' , 'fullPrefix' ] ,
matchers : [
{
id : 'retriesWithOwningSession' ,
pattern : /(?:^|\n)agent-device\s+press\b[^\n]*--session\s+checkout\b/i ,
} ,
{ id : 'keepsSettle' , pattern : /--settle\b/i } ,
] ,
forbidden : [
{ id : 'noClose' , pattern : /(?:^|\n)agent-device\s+close\b/i } ,
{ id : 'noReopen' , pattern : /(?:^|\n)agent-device\s+open\b/i } ,
{ id : 'noRawCoordinateTarget' , pattern : RAW _COORDINATE _TARGET } ,
] ,
} ,
{
id : 'sample-output-stale-ref-resnapshots' ,
docs : [ '--help:first30' ] ,
2026-07-28 15:46:27 +02:00
recovery : { code : 'COMMAND_FAILED' , sample : STALE _REF _SAMPLE } ,
feat(bench): renderer-pinned samples, topic-coverage gate, error-recovery quizzes; trim skillgym to agentic checks (#1411)
* feat(bench): renderer-pinned samples, topic-coverage gate, error quizzes; trim skillgym to agentic checks
The help conformance bench's quoted CLI output is now sourced from
scripts/help-conformance-sample-outputs.mjs, and every sample is rebuilt
through the real production renderers (settle output formatters,
printHumanError, formatSnapshotText, refMutationAdmissionResponse) by
scripts/__tests__/help-conformance-sample-outputs.test.ts — a rendering or
message change fails deterministically instead of leaving the bench grading
against output the CLI no longer prints. This retires the fabricated
recoverable-failure envelope (production never throws a textual settle
timeout; that case is replaced by a real DEVICE_IN_USE recovery quiz).
Bench cases move to scripts/help-conformance-cases.mjs and are enumerated
against the help-topic registry: helpTopicIds() is exported from cli-help,
and scripts/__tests__/help-conformance-topic-coverage.test.ts fails when a
help topic has neither a bench case nor an explicit waiver. New case
families: error-envelope recovery quizzes (device-in-use, stale pinned ref,
ambiguous find match, app-not-installed) pinned to real error text, topic
coverage for tv/web/react-native/debugging/workflow, and a metamorphic twin
of the settled-diff quiz.
The skillgym smoke suite shrinks from 119 cases to the 5 that measure what
only an agentic runner can show: skill routing plus output interpretation
with a proven local CLI help probe (local-cli-help-policy). Its embedded
samples now import the same pinned constants, replacing hand-transcribed
output that had already drifted from the renderer. Knowledge checks belong
to the bench; live fixture behavior belongs to the iOS simulator e2e suite.
* review: drive error samples through the real producers; enforce local-help on the routing smoke
The DEVICE_IN_USE, AMBIGUOUS_MATCH, and APP_NOT_INSTALLED parity tests no
longer hand-author the producer message before rendering: each drives the
actual producer — buildDeviceInUseBySessionError (extracted in
session-open.ts and called by the handler), buildAmbiguousMatchError (now
exported from find.ts), and buildAppNotInstalledError (extracted in
app-resolution.ts and thrown by the resolver). Because each factory is
exported from its producer file and called by the production path, dropping
the production call would make it test-only and fail
check:production-exports — the wiring is gate-enforced, not conventional.
open-and-snapshot now sets requireLocalCliHelp and
allowOnlyLocalCliHelpCommands, so the 'skill plus local help' claim is
observed rather than assumed; without them the case can pass on model prior
alone.
2026-07-27 14:24:36 +02:00
task : quiz (
STALE _REF _SAMPLE ,
'The Continue control this ref pointed at may have moved. What command should run next?' ,
) ,
expectations : [ 'validPlanCommands' , 'fullPrefix' ] ,
matchers : [
{ id : 'refreshesInteractiveRefs' , pattern : /(?:^|\n)agent-device\s+snapshot\s+-i\b/i } ,
] ,
forbidden : [
{
id : 'noBareRefRetry' ,
pattern : /(?:^|\n)agent-device\s+(?:press|click|fill|longpress)\s+@e\d/i ,
} ,
{ id : 'noReopen' , pattern : /(?:^|\n)agent-device\s+open\b/i } ,
{ id : 'noRawCoordinateTarget' , pattern : RAW _COORDINATE _TARGET } ,
] ,
} ,
{
id : 'sample-output-ambiguous-match-reobserves' ,
docs : [ '--help:first30' ] ,
2026-07-28 15:46:27 +02:00
recovery : { code : 'AMBIGUOUS_MATCH' , sample : AMBIGUOUS _MATCH _SAMPLE } ,
feat(bench): renderer-pinned samples, topic-coverage gate, error-recovery quizzes; trim skillgym to agentic checks (#1411)
* feat(bench): renderer-pinned samples, topic-coverage gate, error quizzes; trim skillgym to agentic checks
The help conformance bench's quoted CLI output is now sourced from
scripts/help-conformance-sample-outputs.mjs, and every sample is rebuilt
through the real production renderers (settle output formatters,
printHumanError, formatSnapshotText, refMutationAdmissionResponse) by
scripts/__tests__/help-conformance-sample-outputs.test.ts — a rendering or
message change fails deterministically instead of leaving the bench grading
against output the CLI no longer prints. This retires the fabricated
recoverable-failure envelope (production never throws a textual settle
timeout; that case is replaced by a real DEVICE_IN_USE recovery quiz).
Bench cases move to scripts/help-conformance-cases.mjs and are enumerated
against the help-topic registry: helpTopicIds() is exported from cli-help,
and scripts/__tests__/help-conformance-topic-coverage.test.ts fails when a
help topic has neither a bench case nor an explicit waiver. New case
families: error-envelope recovery quizzes (device-in-use, stale pinned ref,
ambiguous find match, app-not-installed) pinned to real error text, topic
coverage for tv/web/react-native/debugging/workflow, and a metamorphic twin
of the settled-diff quiz.
The skillgym smoke suite shrinks from 119 cases to the 5 that measure what
only an agentic runner can show: skill routing plus output interpretation
with a proven local CLI help probe (local-cli-help-policy). Its embedded
samples now import the same pinned constants, replacing hand-transcribed
output that had already drifted from the renderer. Knowledge checks belong
to the bench; live fixture behavior belongs to the iOS simulator e2e suite.
* review: drive error samples through the real producers; enforce local-help on the routing smoke
The DEVICE_IN_USE, AMBIGUOUS_MATCH, and APP_NOT_INSTALLED parity tests no
longer hand-author the producer message before rendering: each drives the
actual producer — buildDeviceInUseBySessionError (extracted in
session-open.ts and called by the handler), buildAmbiguousMatchError (now
exported from find.ts), and buildAppNotInstalledError (extracted in
app-resolution.ts and thrown by the resolver). Because each factory is
exported from its producer file and called by the production path, dropping
the production call would make it test-only and fail
check:production-exports — the wiring is gate-enforced, not conventional.
open-and-snapshot now sets requireLocalCliHelp and
allowOnlyLocalCliHelpCommands, so the 'skill plus local help' claim is
observed rather than assumed; without them the case can pass on model prior
alone.
2026-07-27 14:24:36 +02:00
task : quiz (
AMBIGUOUS _MATCH _SAMPLE ,
fix: surface AMBIGUOUS_MATCH candidates and name find's supported actions (#1602)
* fix: surface AMBIGUOUS_MATCH candidates and name find's supported actions (#1597)
AMBIGUOUS_MATCH errors now list the matching candidates (ref, role,
label/identifier) rendered the same way as snapshot -i lines, capped at
5 with a "+N more" marker. buildAmbiguousMatchError (the single
producer, src/daemon/handlers/find.ts) reuses formatSnapshotLine to
build the list; formatAmbiguousMatchCandidateLines (src/utils/output.ts)
renders it unconditionally on both text surfaces an agent actually
reads (CLI printHumanError and MCP formatToolErrorText) — previously
the candidates lived only in details, which neither surface printed.
find's "Unsupported find action: X" (e.g. from `find <text> press`)
now attaches a hint naming every action find actually supports and the
two-step recovery shape: run find "<text>" to resolve the ref, then
dispatch the gesture as its own command (press @eNN). The hint is a
single exported constant (UNSUPPORTED_FIND_ACTION_HINT) shared by both
throw sites — packages/selectors' raw-token parser and the CLI's typed
reader (src/commands/interaction/selectors.ts) — so they can't drift.
Matching semantics are unchanged; ambiguous rejection stays by-design.
The help-conformance corpus's AMBIGUOUS_MATCH quiz is updated: its
premise ("candidate refs were not shown") no longer holds, but with 3
identically-labeled candidates the lesson (don't guess a specific ref)
still holds.
* fix: guard the AMBIGUOUS_MATCH candidate renderer against device-domain shapes
Review on #1602 (P2): formatAmbiguousMatchCandidateLines ran for every
normalized error and stringified details.candidates unconditionally,
but device-domain AMBIGUOUS_MATCH/APP_NOT_INSTALLED errors
(findBootedAppleSimulatorWithApp, src/core/dispatch-resolve.ts) reuse
that key for { id, name } device objects with no `matches` field —
CLI and MCP would have printed "Candidates: [object Object]" for
those. The renderer now requires numeric details.matches AND every
candidate to be a string before rendering anything, restricting it to
buildAmbiguousMatchError's element-match shape; unrecognized shapes
render nothing, same as before this feature existed. Added regression
tests against the exact device-error shape on both text surfaces.
Also unexports AMBIGUOUS_MATCH_CANDIDATE_LIMIT (fallow flagged it as
an unused production export) — it has no consumer outside find.ts.
2026-08-04 21:10:05 +02:00
'The intent is to follow the @callstack.com account row. The candidate refs shown (@e2, @e5, @e9) all carry the identical "Follow" label, so this output alone cannot tell them apart. What command should run next?' ,
feat(bench): renderer-pinned samples, topic-coverage gate, error-recovery quizzes; trim skillgym to agentic checks (#1411)
* feat(bench): renderer-pinned samples, topic-coverage gate, error quizzes; trim skillgym to agentic checks
The help conformance bench's quoted CLI output is now sourced from
scripts/help-conformance-sample-outputs.mjs, and every sample is rebuilt
through the real production renderers (settle output formatters,
printHumanError, formatSnapshotText, refMutationAdmissionResponse) by
scripts/__tests__/help-conformance-sample-outputs.test.ts — a rendering or
message change fails deterministically instead of leaving the bench grading
against output the CLI no longer prints. This retires the fabricated
recoverable-failure envelope (production never throws a textual settle
timeout; that case is replaced by a real DEVICE_IN_USE recovery quiz).
Bench cases move to scripts/help-conformance-cases.mjs and are enumerated
against the help-topic registry: helpTopicIds() is exported from cli-help,
and scripts/__tests__/help-conformance-topic-coverage.test.ts fails when a
help topic has neither a bench case nor an explicit waiver. New case
families: error-envelope recovery quizzes (device-in-use, stale pinned ref,
ambiguous find match, app-not-installed) pinned to real error text, topic
coverage for tv/web/react-native/debugging/workflow, and a metamorphic twin
of the settled-diff quiz.
The skillgym smoke suite shrinks from 119 cases to the 5 that measure what
only an agentic runner can show: skill routing plus output interpretation
with a proven local CLI help probe (local-cli-help-policy). Its embedded
samples now import the same pinned constants, replacing hand-transcribed
output that had already drifted from the renderer. Knowledge checks belong
to the bench; live fixture behavior belongs to the iOS simulator e2e suite.
* review: drive error samples through the real producers; enforce local-help on the routing smoke
The DEVICE_IN_USE, AMBIGUOUS_MATCH, and APP_NOT_INSTALLED parity tests no
longer hand-author the producer message before rendering: each drives the
actual producer — buildDeviceInUseBySessionError (extracted in
session-open.ts and called by the handler), buildAmbiguousMatchError (now
exported from find.ts), and buildAppNotInstalledError (extracted in
app-resolution.ts and thrown by the resolver). Because each factory is
exported from its producer file and called by the production path, dropping
the production call would make it test-only and fail
check:production-exports — the wiring is gate-enforced, not conventional.
open-and-snapshot now sets requireLocalCliHelp and
allowOnlyLocalCliHelpCommands, so the 'skill plus local help' claim is
observed rather than assumed; without them the case can pass on model prior
alone.
2026-07-27 14:24:36 +02:00
) ,
expectations : [ 'validPlanCommands' , 'fullPrefix' ] ,
matchers : [
{
id : 'reobservesOrNarrows' ,
pattern :
/(?:^|\n)agent-device\s+(?:snapshot\s+-i\b|(?:find|press|click)\s+[^\n]*(?:role=|id=|label="?@callstack\.com))/i ,
} ,
] ,
forbidden : [
fix: surface AMBIGUOUS_MATCH candidates and name find's supported actions (#1602)
* fix: surface AMBIGUOUS_MATCH candidates and name find's supported actions (#1597)
AMBIGUOUS_MATCH errors now list the matching candidates (ref, role,
label/identifier) rendered the same way as snapshot -i lines, capped at
5 with a "+N more" marker. buildAmbiguousMatchError (the single
producer, src/daemon/handlers/find.ts) reuses formatSnapshotLine to
build the list; formatAmbiguousMatchCandidateLines (src/utils/output.ts)
renders it unconditionally on both text surfaces an agent actually
reads (CLI printHumanError and MCP formatToolErrorText) — previously
the candidates lived only in details, which neither surface printed.
find's "Unsupported find action: X" (e.g. from `find <text> press`)
now attaches a hint naming every action find actually supports and the
two-step recovery shape: run find "<text>" to resolve the ref, then
dispatch the gesture as its own command (press @eNN). The hint is a
single exported constant (UNSUPPORTED_FIND_ACTION_HINT) shared by both
throw sites — packages/selectors' raw-token parser and the CLI's typed
reader (src/commands/interaction/selectors.ts) — so they can't drift.
Matching semantics are unchanged; ambiguous rejection stays by-design.
The help-conformance corpus's AMBIGUOUS_MATCH quiz is updated: its
premise ("candidate refs were not shown") no longer holds, but with 3
identically-labeled candidates the lesson (don't guess a specific ref)
still holds.
* fix: guard the AMBIGUOUS_MATCH candidate renderer against device-domain shapes
Review on #1602 (P2): formatAmbiguousMatchCandidateLines ran for every
normalized error and stringified details.candidates unconditionally,
but device-domain AMBIGUOUS_MATCH/APP_NOT_INSTALLED errors
(findBootedAppleSimulatorWithApp, src/core/dispatch-resolve.ts) reuse
that key for { id, name } device objects with no `matches` field —
CLI and MCP would have printed "Candidates: [object Object]" for
those. The renderer now requires numeric details.matches AND every
candidate to be a string before rendering anything, restricting it to
buildAmbiguousMatchError's element-match shape; unrecognized shapes
render nothing, same as before this feature existed. Added regression
tests against the exact device-error shape on both text surfaces.
Also unexports AMBIGUOUS_MATCH_CANDIDATE_LIMIT (fallow flagged it as
an unused production export) — it has no consumer outside find.ts.
2026-08-04 21:10:05 +02:00
// #1597: candidates now print (ref, role, label), but all 3 here share
// the exact same "Follow" label — picking any single @eN from this
// output alone would still be an unverified guess, not a resolved match.
feat(bench): renderer-pinned samples, topic-coverage gate, error-recovery quizzes; trim skillgym to agentic checks (#1411)
* feat(bench): renderer-pinned samples, topic-coverage gate, error quizzes; trim skillgym to agentic checks
The help conformance bench's quoted CLI output is now sourced from
scripts/help-conformance-sample-outputs.mjs, and every sample is rebuilt
through the real production renderers (settle output formatters,
printHumanError, formatSnapshotText, refMutationAdmissionResponse) by
scripts/__tests__/help-conformance-sample-outputs.test.ts — a rendering or
message change fails deterministically instead of leaving the bench grading
against output the CLI no longer prints. This retires the fabricated
recoverable-failure envelope (production never throws a textual settle
timeout; that case is replaced by a real DEVICE_IN_USE recovery quiz).
Bench cases move to scripts/help-conformance-cases.mjs and are enumerated
against the help-topic registry: helpTopicIds() is exported from cli-help,
and scripts/__tests__/help-conformance-topic-coverage.test.ts fails when a
help topic has neither a bench case nor an explicit waiver. New case
families: error-envelope recovery quizzes (device-in-use, stale pinned ref,
ambiguous find match, app-not-installed) pinned to real error text, topic
coverage for tv/web/react-native/debugging/workflow, and a metamorphic twin
of the settled-diff quiz.
The skillgym smoke suite shrinks from 119 cases to the 5 that measure what
only an agentic runner can show: skill routing plus output interpretation
with a proven local CLI help probe (local-cli-help-policy). Its embedded
samples now import the same pinned constants, replacing hand-transcribed
output that had already drifted from the renderer. Knowledge checks belong
to the bench; live fixture behavior belongs to the iOS simulator e2e suite.
* review: drive error samples through the real producers; enforce local-help on the routing smoke
The DEVICE_IN_USE, AMBIGUOUS_MATCH, and APP_NOT_INSTALLED parity tests no
longer hand-author the producer message before rendering: each drives the
actual producer — buildDeviceInUseBySessionError (extracted in
session-open.ts and called by the handler), buildAmbiguousMatchError (now
exported from find.ts), and buildAppNotInstalledError (extracted in
app-resolution.ts and thrown by the resolver). Because each factory is
exported from its producer file and called by the production path, dropping
the production call would make it test-only and fail
check:production-exports — the wiring is gate-enforced, not conventional.
open-and-snapshot now sets requireLocalCliHelp and
allowOnlyLocalCliHelpCommands, so the 'skill plus local help' claim is
observed rather than assumed; without them the case can pass on model prior
alone.
2026-07-27 14:24:36 +02:00
{ id : 'noGuessedRef' , pattern : /(?:^|\n)agent-device\s+(?:press|click)\s+@e\d/i } ,
{
id : 'noVerbatimRetry' ,
fix: surface AMBIGUOUS_MATCH candidates and name find's supported actions (#1602)
* fix: surface AMBIGUOUS_MATCH candidates and name find's supported actions (#1597)
AMBIGUOUS_MATCH errors now list the matching candidates (ref, role,
label/identifier) rendered the same way as snapshot -i lines, capped at
5 with a "+N more" marker. buildAmbiguousMatchError (the single
producer, src/daemon/handlers/find.ts) reuses formatSnapshotLine to
build the list; formatAmbiguousMatchCandidateLines (src/utils/output.ts)
renders it unconditionally on both text surfaces an agent actually
reads (CLI printHumanError and MCP formatToolErrorText) — previously
the candidates lived only in details, which neither surface printed.
find's "Unsupported find action: X" (e.g. from `find <text> press`)
now attaches a hint naming every action find actually supports and the
two-step recovery shape: run find "<text>" to resolve the ref, then
dispatch the gesture as its own command (press @eNN). The hint is a
single exported constant (UNSUPPORTED_FIND_ACTION_HINT) shared by both
throw sites — packages/selectors' raw-token parser and the CLI's typed
reader (src/commands/interaction/selectors.ts) — so they can't drift.
Matching semantics are unchanged; ambiguous rejection stays by-design.
The help-conformance corpus's AMBIGUOUS_MATCH quiz is updated: its
premise ("candidate refs were not shown") no longer holds, but with 3
identically-labeled candidates the lesson (don't guess a specific ref)
still holds.
* fix: guard the AMBIGUOUS_MATCH candidate renderer against device-domain shapes
Review on #1602 (P2): formatAmbiguousMatchCandidateLines ran for every
normalized error and stringified details.candidates unconditionally,
but device-domain AMBIGUOUS_MATCH/APP_NOT_INSTALLED errors
(findBootedAppleSimulatorWithApp, src/core/dispatch-resolve.ts) reuse
that key for { id, name } device objects with no `matches` field —
CLI and MCP would have printed "Candidates: [object Object]" for
those. The renderer now requires numeric details.matches AND every
candidate to be a string before rendering anything, restricting it to
buildAmbiguousMatchError's element-match shape; unrecognized shapes
render nothing, same as before this feature existed. Added regression
tests against the exact device-error shape on both text surfaces.
Also unexports AMBIGUOUS_MATCH_CANDIDATE_LIMIT (fallow flagged it as
an unused production export) — it has no consumer outside find.ts.
2026-08-04 21:10:05 +02:00
pattern : /(?:^|\n)agent-device\s+find\s+text\s+"?follow"?\s*(?:\n|$)/i ,
feat(bench): renderer-pinned samples, topic-coverage gate, error-recovery quizzes; trim skillgym to agentic checks (#1411)
* feat(bench): renderer-pinned samples, topic-coverage gate, error quizzes; trim skillgym to agentic checks
The help conformance bench's quoted CLI output is now sourced from
scripts/help-conformance-sample-outputs.mjs, and every sample is rebuilt
through the real production renderers (settle output formatters,
printHumanError, formatSnapshotText, refMutationAdmissionResponse) by
scripts/__tests__/help-conformance-sample-outputs.test.ts — a rendering or
message change fails deterministically instead of leaving the bench grading
against output the CLI no longer prints. This retires the fabricated
recoverable-failure envelope (production never throws a textual settle
timeout; that case is replaced by a real DEVICE_IN_USE recovery quiz).
Bench cases move to scripts/help-conformance-cases.mjs and are enumerated
against the help-topic registry: helpTopicIds() is exported from cli-help,
and scripts/__tests__/help-conformance-topic-coverage.test.ts fails when a
help topic has neither a bench case nor an explicit waiver. New case
families: error-envelope recovery quizzes (device-in-use, stale pinned ref,
ambiguous find match, app-not-installed) pinned to real error text, topic
coverage for tv/web/react-native/debugging/workflow, and a metamorphic twin
of the settled-diff quiz.
The skillgym smoke suite shrinks from 119 cases to the 5 that measure what
only an agentic runner can show: skill routing plus output interpretation
with a proven local CLI help probe (local-cli-help-policy). Its embedded
samples now import the same pinned constants, replacing hand-transcribed
output that had already drifted from the renderer. Knowledge checks belong
to the bench; live fixture behavior belongs to the iOS simulator e2e suite.
* review: drive error samples through the real producers; enforce local-help on the routing smoke
The DEVICE_IN_USE, AMBIGUOUS_MATCH, and APP_NOT_INSTALLED parity tests no
longer hand-author the producer message before rendering: each drives the
actual producer — buildDeviceInUseBySessionError (extracted in
session-open.ts and called by the handler), buildAmbiguousMatchError (now
exported from find.ts), and buildAppNotInstalledError (extracted in
app-resolution.ts and thrown by the resolver). Because each factory is
exported from its producer file and called by the production path, dropping
the production call would make it test-only and fail
check:production-exports — the wiring is gate-enforced, not conventional.
open-and-snapshot now sets requireLocalCliHelp and
allowOnlyLocalCliHelpCommands, so the 'skill plus local help' claim is
observed rather than assumed; without them the case can pass on model prior
alone.
2026-07-27 14:24:36 +02:00
} ,
{ id : 'noRawCoordinateTarget' , pattern : RAW _COORDINATE _TARGET } ,
] ,
} ,
{
id : 'sample-output-app-not-installed-discovers-first' ,
docs : [ '--help:first30' ] ,
2026-07-28 15:46:27 +02:00
recovery : { code : 'APP_NOT_INSTALLED' , sample : APP _NOT _INSTALLED _SAMPLE } ,
feat(bench): renderer-pinned samples, topic-coverage gate, error-recovery quizzes; trim skillgym to agentic checks (#1411)
* feat(bench): renderer-pinned samples, topic-coverage gate, error quizzes; trim skillgym to agentic checks
The help conformance bench's quoted CLI output is now sourced from
scripts/help-conformance-sample-outputs.mjs, and every sample is rebuilt
through the real production renderers (settle output formatters,
printHumanError, formatSnapshotText, refMutationAdmissionResponse) by
scripts/__tests__/help-conformance-sample-outputs.test.ts — a rendering or
message change fails deterministically instead of leaving the bench grading
against output the CLI no longer prints. This retires the fabricated
recoverable-failure envelope (production never throws a textual settle
timeout; that case is replaced by a real DEVICE_IN_USE recovery quiz).
Bench cases move to scripts/help-conformance-cases.mjs and are enumerated
against the help-topic registry: helpTopicIds() is exported from cli-help,
and scripts/__tests__/help-conformance-topic-coverage.test.ts fails when a
help topic has neither a bench case nor an explicit waiver. New case
families: error-envelope recovery quizzes (device-in-use, stale pinned ref,
ambiguous find match, app-not-installed) pinned to real error text, topic
coverage for tv/web/react-native/debugging/workflow, and a metamorphic twin
of the settled-diff quiz.
The skillgym smoke suite shrinks from 119 cases to the 5 that measure what
only an agentic runner can show: skill routing plus output interpretation
with a proven local CLI help probe (local-cli-help-policy). Its embedded
samples now import the same pinned constants, replacing hand-transcribed
output that had already drifted from the renderer. Knowledge checks belong
to the bench; live fixture behavior belongs to the iOS simulator e2e suite.
* review: drive error samples through the real producers; enforce local-help on the routing smoke
The DEVICE_IN_USE, AMBIGUOUS_MATCH, and APP_NOT_INSTALLED parity tests no
longer hand-author the producer message before rendering: each drives the
actual producer — buildDeviceInUseBySessionError (extracted in
session-open.ts and called by the handler), buildAmbiguousMatchError (now
exported from find.ts), and buildAppNotInstalledError (extracted in
app-resolution.ts and thrown by the resolver). Because each factory is
exported from its producer file and called by the production path, dropping
the production call would make it test-only and fail
check:production-exports — the wiring is gate-enforced, not conventional.
open-and-snapshot now sets requireLocalCliHelp and
allowOnlyLocalCliHelpCommands, so the 'skill plus local help' claim is
observed rather than assumed; without them the case can pass on model prior
alone.
2026-07-27 14:24:36 +02:00
task : quiz (
APP _NOT _INSTALLED _SAMPLE ,
'The goal is still to open the shop app on this simulator; no build artifact was provided. What command should run next?' ,
) ,
expectations : [ 'validPlanCommands' , 'fullPrefix' ] ,
matchers : [ { id : 'discoversInstalledApps' , pattern : /(?:^|\n)agent-device\s+apps\b/i } ] ,
forbidden : [
{ id : 'noBlindReopen' , pattern : /(?:^|\n)agent-device\s+open\s+"?shoply\b/i } ,
// No artifact exists to install; inventing one is the failure mode help
// workflow forbids ("Do not open artifact paths or invent package ids").
{
id : 'noInventedInstall' ,
pattern : /(?:^|\n)agent-device\s+(?:install|install-from-source)\b/i ,
} ,
{ id : 'noRawCoordinateTarget' , pattern : RAW _COORDINATE _TARGET } ,
] ,
} ,
2026-08-03 16:47:57 +02:00
{
id : 'sample-output-browserstack-connect-opens-known-package' ,
docs : [ '--help:first30' , 'remote' ] ,
task : quiz (
BROWSERSTACK _CONNECT _SAMPLE ,
'The uploaded app has package id com.example.demo. What command should run next?' ,
) ,
expectations : [ 'validPlanCommands' , 'fullPrefix' ] ,
matchers : [
{
id : 'opensKnownPackage' ,
pattern : /(?:^|\n)agent-device\s+open\s+com\.example\.demo\b[^\n]*--relaunch\b/i ,
} ,
2026-08-08 07:54:43 +02:00
{
id : 'keepsConnectedSession' ,
pattern : /(?:^|\n)agent-device\s+open\b[^\n]*--session\s+adc-browserstack\b/i ,
} ,
2026-08-03 16:47:57 +02:00
] ,
forbidden : [
{
id : 'noPreallocationCatalogProbe' ,
pattern : /(?:^|\n)agent-device\s+(?:devices|apps)\b/i ,
} ,
{
id : 'noArtifactAsAppId' ,
pattern : /(?:^|\n)agent-device\s+open\s+(?:sample\.apk|bs:\/\/app-id)\b/i ,
} ,
{ id : 'noRedundantInstall' , pattern : /(?:^|\n)agent-device\s+install\b/i } ,
] ,
} ,
2026-08-08 10:35:12 +02:00
{
id : 'foreground-attach-single-sim' ,
docs : [ '--help:first30' , 'workflow' ] ,
2026-08-09 10:10:11 +02:00
task : 'You are starting fresh with no active session. The configured iOS target already has com.example.demo in the foreground. Plan the command that keeps this deterministic app/device selection and gets its initial interactive snapshot in a single call, then press the visible Continue control and close the session.' ,
expectations : [ 'validPlanCommands' , 'fullPrefix' , 'usesSettleOnMutations' , 'opensAndCloses' ] ,
2026-08-08 10:35:12 +02:00
matchers : [
{
id : 'startsWithForegroundOpen' ,
2026-08-09 10:10:11 +02:00
// Flag order is not semantically meaningful; require the known app
// and --foreground on the first command, in either order.
pattern :
/^agent-device\s+open\b(?=[^\n]*\bcom\.example\.demo\b)(?=[^\n]*--foreground\b)[^\n]*$/i ,
2026-08-08 10:35:12 +02:00
} ,
{
id : 'pressesContinueAfterAttach' ,
pattern : /agent-device\s+press\s+[^\n]*continue[^\n]*--settle\b/i ,
} ,
] ,
2026-08-09 10:10:11 +02:00
forbidden : [ { id : 'noRawCoordinateTarget' , pattern : RAW _COORDINATE _TARGET } ] ,
2026-08-08 10:35:12 +02:00
} ,
{
id : 'merged-card-actions-not-directly-invokable' ,
docs : [ '--help:first30' , 'workflow' ] ,
task : quiz (
MERGED _CARD _ACTIONS _SAMPLE ,
'The goal is to reply to this post. The actions list names "Reply" as a hidden affordance on @e72, but that name is not a pressable selector. What command should run next?' ,
) ,
expectations : [ 'validPlanCommands' , 'fullPrefix' ] ,
matchers : [
{
id : 'opensCardToReachReply' ,
pattern : /(?:^|\n)agent-device\s+(?:press|click)\s+@e72\b[^\n]*--settle\b/i ,
} ,
] ,
forbidden : [
{
id : 'noPressingActionNameAsSelector' ,
pattern : /(?:^|\n)agent-device\s+(?:press|click|find)\b[^\n]*(?:label|text)="?reply"?/i ,
} ,
2026-08-10 20:43:01 +02:00
{ id : 'noSnapshotDetour' , pattern : /\bsnapshot\b/i } ,
{ id : 'noBareRefTarget' , pattern : BARE _REF _TARGET } ,
2026-08-08 10:35:12 +02:00
{ id : 'noRawCoordinateTarget' , pattern : RAW _COORDINATE _TARGET } ,
] ,
} ,
{
id : 'foreground-attach-snapshot-recovery' ,
docs : [ '--help:first30' , 'workflow' ] ,
task : quiz (
FOREGROUND _SNAPSHOT _FAILURE _SAMPLE ,
'The foreground attach succeeded and the session is still open, but its initial snapshot failed. What command should run next to get interactive refs?' ,
) ,
expectations : [ 'validPlanCommands' , 'fullPrefix' , 'usesSnapshotI' ] ,
matchers : [
{
id : 'retriesSnapshotInOpenSession' ,
pattern : /(?:^|\n)agent-device\s+snapshot\s+-i\b/i ,
} ,
] ,
forbidden : [
{ id : 'noSecondOpen' , pattern : /(?:^|\n)agent-device\s+open\b/i } ,
{ id : 'noPrematureClose' , pattern : /(?:^|\n)agent-device\s+close\b/i } ,
] ,
} ,
feat(bench): renderer-pinned samples, topic-coverage gate, error-recovery quizzes; trim skillgym to agentic checks (#1411)
* feat(bench): renderer-pinned samples, topic-coverage gate, error quizzes; trim skillgym to agentic checks
The help conformance bench's quoted CLI output is now sourced from
scripts/help-conformance-sample-outputs.mjs, and every sample is rebuilt
through the real production renderers (settle output formatters,
printHumanError, formatSnapshotText, refMutationAdmissionResponse) by
scripts/__tests__/help-conformance-sample-outputs.test.ts — a rendering or
message change fails deterministically instead of leaving the bench grading
against output the CLI no longer prints. This retires the fabricated
recoverable-failure envelope (production never throws a textual settle
timeout; that case is replaced by a real DEVICE_IN_USE recovery quiz).
Bench cases move to scripts/help-conformance-cases.mjs and are enumerated
against the help-topic registry: helpTopicIds() is exported from cli-help,
and scripts/__tests__/help-conformance-topic-coverage.test.ts fails when a
help topic has neither a bench case nor an explicit waiver. New case
families: error-envelope recovery quizzes (device-in-use, stale pinned ref,
ambiguous find match, app-not-installed) pinned to real error text, topic
coverage for tv/web/react-native/debugging/workflow, and a metamorphic twin
of the settled-diff quiz.
The skillgym smoke suite shrinks from 119 cases to the 5 that measure what
only an agentic runner can show: skill routing plus output interpretation
with a proven local CLI help probe (local-cli-help-policy). Its embedded
samples now import the same pinned constants, replacing hand-transcribed
output that had already drifted from the renderer. Knowledge checks belong
to the bench; live fixture behavior belongs to the iOS simulator e2e suite.
* review: drive error samples through the real producers; enforce local-help on the routing smoke
The DEVICE_IN_USE, AMBIGUOUS_MATCH, and APP_NOT_INSTALLED parity tests no
longer hand-author the producer message before rendering: each drives the
actual producer — buildDeviceInUseBySessionError (extracted in
session-open.ts and called by the handler), buildAmbiguousMatchError (now
exported from find.ts), and buildAppNotInstalledError (extracted in
app-resolution.ts and thrown by the resolver). Because each factory is
exported from its producer file and called by the production path, dropping
the production call would make it test-only and fail
check:production-exports — the wiring is gate-enforced, not conventional.
open-and-snapshot now sets requireLocalCliHelp and
allowOnlyLocalCliHelpCommands, so the 'skill plus local help' claim is
observed rather than assumed; without them the case can pass on model prior
alone.
2026-07-27 14:24:36 +02:00
] ;