Files
callstack__agent-device/package.json
T

328 lines
19 KiB
JSON
Raw Normal View History

2026-01-30 19:41:00 +01:00
{
"name": "agent-device",
Harden the MCP surface: registry rug-pull fix, operator-only credentials/endpoints, device-shell argv gate, declared timeouts (#2023) * chore(release): keep the version on main distinct from every published version Registry scanners diff the repository's tool surface per version string, so a released number left on main while main keeps changing is indistinguishable from a republished ("rug-pull") version — two scans of the same version see two different tool sets (AS-012). - release:publish now runs release:mark-dev after npm publish, moving package.json and the synchronized server.json to the next patch with a -dev prerelease marker. - release:prepare refuses to publish while the -dev marker is in place, so a forgotten version bump cannot ship a prerelease as latest. - Mark the current tree 0.20.11-dev: main had been sitting on the published 0.20.10 while the tool surface kept changing, which is the live finding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wb8EuoySL26UsdtCtRj5X8 * fix(mcp): remove credential inputs from the model-writable tool surface Every MCP tool advertised daemonAuthToken (and the Metro tools bearerToken) as a free-form string the model writes. The model both reads untrusted app UI text and picks tool arguments, so on-screen text steering it to set a token was a prompt-injection exfiltration path. Credentials are operator-owned: - the keys are omitted from every advertised tool schema (MCP and AI SDK, which share listCommandTools()), - an explicit value is refused with env-var guidance instead of being forwarded (the retired-field posture: refuse, never silently drop), - operator-sourced values are untouched — env/config defaults still merge, and the daemon and Metro clients keep their AGENT_DEVICE_DAEMON_AUTH_TOKEN / AGENT_DEVICE_METRO_BEARER_TOKEN fallbacks. CLI flags are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wb8EuoySL26UsdtCtRj5X8 * fix(mcp): move operator endpoints and paths off the model-writable surface; declare timeouts Follow-up to the credential removal: daemonBaseUrl and the Metro proxyBaseUrl are the endpoints the env-resolved tokens are SENT to, so a model-writable value redirects the operator's token to an arbitrary server — same exfiltration path, one step removed. stateDir, cwd, iosSimulatorDeviceSet, and the three iosXctest* paths select operator infrastructure, never per-call work. All of them leave the advertised MCP/AI-SDK tool schemas and are refused as explicit input with env/config guidance; operator env/config defaults keep flowing exactly as before (config-backed defaults still merge, and explicit input can no longer override them). Dropping these shared properties also cuts tools/list substantially. Every tool description now also declares its enforced client timeout envelope (90s default, 180s install, unbounded only for the streaming test runner), sourced from the descriptor registry's timeout policy so the declared number cannot drift from the enforced one (answers AS-011, which read the undeclared envelope as "no timeout"). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wb8EuoySL26UsdtCtRj5X8 * feat(ci): inventory every dynamic value reaching a device shell adb shell, adb exec-out, and hdc shell join their argv into one string the device's sh evaluates, so any unquoted dynamic element is a potential argv injection — the class of bug the audit found (and fixed) on input text and cmd clipboard set text. Nothing enumerated the surface, so a new call site could regress it silently. scripts/shell-argv is an AST-based gate (oxc-parser, same as di-seams and layering) keeping an exact inventory of every dynamic device-shell argv element, keyed by (file, expression) with counts: 121 values today. A new or grown entry fails CI until the author quotes it through shellQuoteIfNeeded or records it with --update in the same PR, making "a new value now reaches the device shell" a reviewable diff; a stale entry fails the other way so the inventory always matches the code. Wired as the shell-argv gate in the lint lane and registered in the check catalog. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wb8EuoySL26UsdtCtRj5X8 * fix(shell-argv): satisfy the fallow audit without suppressions The Compatibility & Provenance lane's fallow audit flagged the new gate: main was an unused export (only the self-run guard consumed it) and four functions sat over the complexity thresholds. Restructure instead of suppressing: the AST walk dispatches through a composite-child-field table, the argv detection is hoisted out of the visitor, drift reporting moves into helpers, and main is no longer exported. Behavior is unchanged — the model tests pass as written and the regenerated inventory is byte-identical (121 values). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wb8EuoySL26UsdtCtRj5X8 * fix(test): close the port-reuse race in the unreachable-takeover test Coverage (1) failed once in CI with the takeover notice missing while the response still came from the fresh daemon — the exact signature of the fresh fixture being handed the just-freed ephemeral port: the recorded daemon becomes reachable and reusable (same version and signature), so the takeover path is skipped. Bind the fresh fixture before acquiring and freeing the unreachable port; with no bind after the close, the port can never be reclaimed. Line-neutral so the size-ratchet pin holds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wb8EuoySL26UsdtCtRj5X8 * fix(mcp): enforce the advertised tool schema at a real admission boundary P1 (reported by the PR author): hiding operator keys from tools/list did not stop them reaching the command route. The router forwards raw tools/call arguments verbatim and resolveMcpConfigDefaults reads them as CLI flags, so an unadvertised `config`/`remoteConfig` key loaded an arbitrary file whose daemonBaseUrl/daemonAuthToken then flowed to runCommand — a model-writable redirect to an attacker endpoint with the operator's token. Reproduced: {config: <path>} on `snapshot` put both values into the command input. Replace the per-key operator refusal with a deny-by-default admission boundary in the shared executor (the one path both the MCP router and the AI SDK adapter use): every raw input key must appear in the tool's advertised schema, else it is rejected with guidance BEFORE config/env resolution. This closes the config loaders, the operator keys, and any unknown key at once, and makes the advertised additionalProperties:false contract actually enforced. Operator env/config defaults still resolve — they never arrive as tool input. Retired keys (maxSize) are admitted so the command's own reader still answers with migration guidance; they're exposed as metadata.retiredInputKeys for that. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wb8EuoySL26UsdtCtRj5X8 * revert(ci): remove the check:shell-argv inventory gate The PR author correctly flagged that this gate is an inventory, not a security invariant: --update lets any site self-approve a raw value, and the literal-first array heuristic is blind to indirect argv (a variable-built subcommand, or an argv assembled in a helper). Reproduced: adb(['shell', 'input','text',text]) is inventoried, but const s='shell'; adb([s,'input','text',text]) yields no finding. Shipping it security-framed gives false assurance. Remove it. The sound fix — a typed device-shell execution boundary where a raw string cannot reach adb/hdc shell without being quoted or explicitly marked — is a ~188-site cross-package migration on device execution paths, scoped to a dedicated follow-up PR. The two known-dangerous sites (input text, cmd clipboard set text) already quote through shellQuoteIfNeeded on main, so no regression. This keeps the PR focused on the MCP tool surface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wb8EuoySL26UsdtCtRj5X8 --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-08-25 15:19:07 +02:00
"version": "0.20.11-dev",
"description": "Mobile app automation and verification for AI coding agents. CLI, MCP server, and typed Node.js API for iOS, Android, HarmonyOS, TV, web, macOS, and Linux.",
fix: update MCP registry namespace (#1618) * fix: update MCP registry namespace * feat: inherit MCP descriptions from CLI help * refactor: project command guidance per surface * refactor: make command guidance a single canonical description The guidance type carried seven fields, but only three were ever set, and all twenty call sites used it the same way: to hold a second, hand-written MCP string next to a near-identical CLI one. That is the drift the abstraction was meant to remove, so the type no longer offers a per-surface description at all. A command now has one canonical description plus an optional tail per surface: guidance: { description: 'Shared body.', cliDetail: 'Flags, positional syntax, terminal examples.', mcpDetail: 'When-to-use and sequencing hints.', } Because a surface can only append, CLI help and MCP tool text cannot diverge — the guard against CLI syntax in MCP descriptions becomes structural rather than a review tripwire, since flag vocabulary only lives in cliDetail. All twenty commands that previously carried two descriptions now share one body. Also: - Drop `summary` from the description fallback chain. It is the short list-view line, so falling back to it replaced the full description with a fragment on both surfaces: artifacts, boot, and shutdown each lost their real description. - Stop writing the MCP variant back over `metadata.description`. That field feeds CLI help, `explain`, and docs; `explain` was printing MCP-only text. MCP now reads a separate `mcpDescription`. - Drop `mcp.parameters`. It restated inputSchema property descriptions inside the tool description — 1232 characters duplicated verbatim across six tools, and three of sixteen declared hints silently rendered nothing because the property had no description. Those properties are documented in the schema instead, which serves MCP, --help, and docs at once. - Drop `cli.flags`. Its one use appended "Relevant flags: --surface, --launch-console." to help text that already named both flags inline. Tests assert the structural property (both surfaces share a canonical prefix) and the summary-fallback regression, alongside the existing CLI-syntax guard. CLI help wording assertions follow the new copy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dp3J8UUgYxtw5vzJzvjkSf * test: gate undocumented MCP tool inputs Guidance no longer restates input fields in prose, so a tool's inputSchema is the only place its inputs are documented — for the model, for --help, and for the docs site. An undescribed property is a silent gap rather than a cosmetic one, which is exactly the failure mode the removed `mcp.parameters` selection had: it dropped hints for properties that carried no description and reported nothing. Describe the two trigger-app-event inputs that mechanism used to name, and add a ratcheting gate over every MCP tool input. A property key that is not already in the budget fails immediately; the total may never grow, and lowering it is required once properties gain descriptions, so the 132 remaining stay visible instead of settling in as permanent debt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dp3J8UUgYxtw5vzJzvjkSf * fix: project the canonical description to every surface Storing only the MCP variant left the shared body unpropagated: `metadata.description` and the executable definition kept their pre-guidance text, so `explain click` reported "Click or tap a semantic UI target..." while CLI help and the MCP tool both used the canonical "Activate a UI target...". 53 commands were affected — the CLI schema base, `explain`, and docs all read `metadata.description`. `projectCommandGuidance` now returns the canonical body plus the MCP-only tail, and `defineCommandFacet` writes the body to both metadata and the definition. Only the tail is stored apart, as `mcpDetail`, so the body has exactly one home instead of a second full copy that could drift; `composeMcpDescription` joins them for the tool surface. The surface gate pins the invariant: definition, metadata, and `explain` must report the identical body, and neither CLI help nor the MCP description may do anything but extend it. Both arms verified by breaking them. Also replace the undocumented-input ratchet's bare-key allowlist plus aggregate budget with exact `tool.property` identities. The old shape stayed green while a gap migrated: describing `foo.text` and adding an undescribed `bar.text` left both the allowed-name set and the total of 132 unchanged, and stale names kept authorizing later gaps. Verified with that exact scenario — `app` was already an allowed name via push/reinstall/settings and the total held at 132, yet a newly undescribed `open.app` now fails. Recording a fix requires deleting its baseline entry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dp3J8UUgYxtw5vzJzvjkSf * refactor: drop guidance.description in favour of the command's own `guidance.description` restated what `metadata.description` already is. Setting it shadowed the metadata literal rather than replacing it, so every command that used it shipped two bodies: the canonical one and a terse original that no surface could observe — 768 bytes of unreachable strings across 20 commands. Move each canonical body to the metadata literal where it belongs and delete the field. Guidance is now tails only, `cliDetail` and `mcpDetail`, which also removes the question of where a body is written: there is one place, and no chain to consult. Three guidance blocks held nothing else and are gone entirely. registry.js drops 1117 bytes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dp3J8UUgYxtw5vzJzvjkSf * refactor: give every command one text block with a mandatory summary The CLI carried four prose fields with a fallback chain between them, and one of them — `helpDescription` — was authored on 45 commands and generated on the rest. That ambiguity is why surfaces drifted: whichever field a reader looked at, some other field might be the one actually rendered. Prose now lives in a single `CommandText`, and `CommandSchema` keeps only grammar: summary what is this command, in a list of ninety? (mandatory) description what does it do, and when do I reach for it? (mandatory) cliDetail flags, argument shapes, terminal examples mcpDetail sequencing and cross-tool hints `--help`, the command list, the MCP tool description and `explain` are projections computed where they render, so nothing derived is stored and no field can be both input and output. The four-field model was validated against the whole surface before the migration: all 67 commands reproduce their MCP text exactly and derive their help body from `description`, so none needed a fifth field. Making `summary` mandatory fixes a regression this branch introduced. 23 commands had none, so the command list fell back to the full detail paragraph; lengthening those descriptions earlier turned `click`'s list entry from 61 characters into 267, `fill`'s into 214, `devices`' into 159. Every command now states its own line, and a gate holds them under 72 characters, non-empty, period-free, and distinct from the description. Two duplications go with it: the per-command help printed its synopsis twice, once as a header and again under `Usage:`, and `press` said "use longpress" in both its body and its tail. * refactor: tighten the command text plumbing Self-review follow-ups on the text model, all quality-only: `command-text.ts` moves from `cli-schema/` to `commands/`. It is a command concept that MCP reads as much as the CLI does; living under `cli-schema` made the MCP surface import a CLI module to render its own tool descriptions. `defineCommandFacet` no longer casts. It took a facet and returned it with the schema completed, but claimed to return the input type, which needed `as unknown as` — a double cast is the type system reporting that the signature was wrong. Splitting `CommandFacetInput` from `CommandFacet` states the completion in the return type, so both that cast and the registry's `as CommandSchema` go. `push`'s summary duplicated its description apart from a trailing period, which the gate missed by comparing exact strings; it now compares normalised text, and the summary says something the description does not. `install-from-source`'s summary loses a clause it did not need. --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-08-05 18:51:21 +02:00
"mcpName": "io.github.callstack/agent-device",
2026-01-30 19:41:00 +01:00
"license": "MIT",
"author": "Callstack",
2026-04-17 13:11:42 +02:00
"homepage": "https://agent-device.dev/",
"repository": {
"type": "git",
"url": "git+https://github.com/callstack/agent-device.git"
2026-04-17 13:11:42 +02:00
},
"bugs": {
"url": "https://github.com/callstack/agent-device/issues"
2026-04-17 13:11:42 +02:00
},
2026-01-30 19:41:00 +01:00
"type": "module",
"packageManager": "pnpm@11.17.0",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
"exports": {
".": {
build: verify the published tarball instead of grepping the bundle (#1578) * build: verify the published tarball instead of grepping the bundle Replaces the bundle-dependency grep with one gate that packs the tarball npm would publish and proves it sound from a clean consumer install: publint and attw on the tarball, a two-way dependency-closure audit, an import of every `exports` subpath, and the CLI smoke run — all from outside the workspace, where no pnpm link can mask an unresolvable specifier. Also stops the build from emitting a publishable bundle in the first place: a missing workspace link now fails `pnpm build` instead of warning and exiting 0, which is how 0.20.4 shipped an unresolvable `@agent-device/ad-script` import. publint found 12 real defects in the current package — every `exports` entry listed `types` after `import`, so TypeScript resolved declarations by accident rather than by condition. The dependency audit found `pngjs` declared as a runtime dependency while tsdown inlines it, an install every user paid for and no shipped code reached; it moves to devDependencies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(ci): run the package gate without pnpm on the Node floor pnpm 11.17 requires Node >= 22.13, so `pnpm check:package` could not start on the 22.12 floor the Packaged CLI job exists to cover. The gate needs only `node` and `npm`, so the job invokes the script directly. Splits the dependency-closure audit into a collector and a message builder to clear Fallow's complexity threshold, and classifies both packaging linters in ignoreDependencies: they are subprocess CLIs with no importable API here, which dependency analysis cannot follow to an import. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(publishing): read every literal resolution form in the closure audit The dependency-closure audit derived shipped imports from the ESM module record alone, so it could not see a package resolved through `require` or a `createRequire` result: neither produces a module-record entry. A lazy `createRequire('@agent-device/…')` would therefore clear the audit, the all-export probe and the exercised CLI paths, reintroducing the 0.20.4 published-install failure class for another command. Measuring the built bundle turned up a second, larger hole in the same reader. The shipped files are minified, and the minifier rewrites every string literal to a no-substitution template literal, so the dynamic-import extraction — which accepted quoted strings only — matched 0 of the 99 dynamic imports the bundle contains. The lazy `import()` path that broke 0.20.4 was reported as covered while checking nothing. Specifiers now come from the module record plus an AST walk over every literal runtime-resolution form: `import()`, `require()`, `require.resolve()`, an immediately-invoked `createRequire(...)`, and calls through a `createRequire` result under any import or minified alias. Both spellings of a string literal count everywhere, and `.cjs` joins the scanned extensions. Computed specifiers stay explicitly out of scope, and are pinned as such. Rejecting them is not available: minifiers reuse short identifiers across scopes, and the packed bundle really does contain an unrelated `a(h[t],f,g,l,e,m)` that no name-based match can distinguish from a require call. Those are covered by the gate's runtime half instead, which resolves them for real. Bare-identifier calls need the one-string-argument shape for the same reason. The audit moves to scripts/lib/shipped-imports.ts so fixture packages can exercise it. The gate needs a real `npm pack` behind minutes of Swift and Android builds, so every check that runs it can only watch a healthy package pass — which is how a reader that matched nothing looked covered. The new fixtures assert the failure direction per resolution form: 16 of the 22 fail against the previous reader, and the 6 that pass are the quoted-spelling and pinned-limitation cases. A wiring assertion keeps the audit and both runtime probes attached to the gate, since fixtures alone would stay green if the call were deleted. Verified against the real built bundle: the closure resolves to exactly the two declared dependencies, so the stricter reader adds no false positives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 12:28:05 +02:00
"types": "./dist/src/index.d.ts",
"import": "./dist/src/index.js"
},
"./io": {
build: verify the published tarball instead of grepping the bundle (#1578) * build: verify the published tarball instead of grepping the bundle Replaces the bundle-dependency grep with one gate that packs the tarball npm would publish and proves it sound from a clean consumer install: publint and attw on the tarball, a two-way dependency-closure audit, an import of every `exports` subpath, and the CLI smoke run — all from outside the workspace, where no pnpm link can mask an unresolvable specifier. Also stops the build from emitting a publishable bundle in the first place: a missing workspace link now fails `pnpm build` instead of warning and exiting 0, which is how 0.20.4 shipped an unresolvable `@agent-device/ad-script` import. publint found 12 real defects in the current package — every `exports` entry listed `types` after `import`, so TypeScript resolved declarations by accident rather than by condition. The dependency audit found `pngjs` declared as a runtime dependency while tsdown inlines it, an install every user paid for and no shipped code reached; it moves to devDependencies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(ci): run the package gate without pnpm on the Node floor pnpm 11.17 requires Node >= 22.13, so `pnpm check:package` could not start on the 22.12 floor the Packaged CLI job exists to cover. The gate needs only `node` and `npm`, so the job invokes the script directly. Splits the dependency-closure audit into a collector and a message builder to clear Fallow's complexity threshold, and classifies both packaging linters in ignoreDependencies: they are subprocess CLIs with no importable API here, which dependency analysis cannot follow to an import. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(publishing): read every literal resolution form in the closure audit The dependency-closure audit derived shipped imports from the ESM module record alone, so it could not see a package resolved through `require` or a `createRequire` result: neither produces a module-record entry. A lazy `createRequire('@agent-device/…')` would therefore clear the audit, the all-export probe and the exercised CLI paths, reintroducing the 0.20.4 published-install failure class for another command. Measuring the built bundle turned up a second, larger hole in the same reader. The shipped files are minified, and the minifier rewrites every string literal to a no-substitution template literal, so the dynamic-import extraction — which accepted quoted strings only — matched 0 of the 99 dynamic imports the bundle contains. The lazy `import()` path that broke 0.20.4 was reported as covered while checking nothing. Specifiers now come from the module record plus an AST walk over every literal runtime-resolution form: `import()`, `require()`, `require.resolve()`, an immediately-invoked `createRequire(...)`, and calls through a `createRequire` result under any import or minified alias. Both spellings of a string literal count everywhere, and `.cjs` joins the scanned extensions. Computed specifiers stay explicitly out of scope, and are pinned as such. Rejecting them is not available: minifiers reuse short identifiers across scopes, and the packed bundle really does contain an unrelated `a(h[t],f,g,l,e,m)` that no name-based match can distinguish from a require call. Those are covered by the gate's runtime half instead, which resolves them for real. Bare-identifier calls need the one-string-argument shape for the same reason. The audit moves to scripts/lib/shipped-imports.ts so fixture packages can exercise it. The gate needs a real `npm pack` behind minutes of Swift and Android builds, so every check that runs it can only watch a healthy package pass — which is how a reader that matched nothing looked covered. The new fixtures assert the failure direction per resolution form: 16 of the 22 fail against the previous reader, and the 6 that pass are the quoted-spelling and pinned-limitation cases. A wiring assertion keeps the audit and both runtime probes attached to the gate, since fixtures alone would stay green if the call were deleted. Verified against the real built bundle: the closure resolves to exactly the two declared dependencies, so the stricter reader adds no false positives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 12:28:05 +02:00
"types": "./dist/src/io.d.ts",
"import": "./dist/src/io.js"
},
"./artifacts": {
build: verify the published tarball instead of grepping the bundle (#1578) * build: verify the published tarball instead of grepping the bundle Replaces the bundle-dependency grep with one gate that packs the tarball npm would publish and proves it sound from a clean consumer install: publint and attw on the tarball, a two-way dependency-closure audit, an import of every `exports` subpath, and the CLI smoke run — all from outside the workspace, where no pnpm link can mask an unresolvable specifier. Also stops the build from emitting a publishable bundle in the first place: a missing workspace link now fails `pnpm build` instead of warning and exiting 0, which is how 0.20.4 shipped an unresolvable `@agent-device/ad-script` import. publint found 12 real defects in the current package — every `exports` entry listed `types` after `import`, so TypeScript resolved declarations by accident rather than by condition. The dependency audit found `pngjs` declared as a runtime dependency while tsdown inlines it, an install every user paid for and no shipped code reached; it moves to devDependencies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(ci): run the package gate without pnpm on the Node floor pnpm 11.17 requires Node >= 22.13, so `pnpm check:package` could not start on the 22.12 floor the Packaged CLI job exists to cover. The gate needs only `node` and `npm`, so the job invokes the script directly. Splits the dependency-closure audit into a collector and a message builder to clear Fallow's complexity threshold, and classifies both packaging linters in ignoreDependencies: they are subprocess CLIs with no importable API here, which dependency analysis cannot follow to an import. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(publishing): read every literal resolution form in the closure audit The dependency-closure audit derived shipped imports from the ESM module record alone, so it could not see a package resolved through `require` or a `createRequire` result: neither produces a module-record entry. A lazy `createRequire('@agent-device/…')` would therefore clear the audit, the all-export probe and the exercised CLI paths, reintroducing the 0.20.4 published-install failure class for another command. Measuring the built bundle turned up a second, larger hole in the same reader. The shipped files are minified, and the minifier rewrites every string literal to a no-substitution template literal, so the dynamic-import extraction — which accepted quoted strings only — matched 0 of the 99 dynamic imports the bundle contains. The lazy `import()` path that broke 0.20.4 was reported as covered while checking nothing. Specifiers now come from the module record plus an AST walk over every literal runtime-resolution form: `import()`, `require()`, `require.resolve()`, an immediately-invoked `createRequire(...)`, and calls through a `createRequire` result under any import or minified alias. Both spellings of a string literal count everywhere, and `.cjs` joins the scanned extensions. Computed specifiers stay explicitly out of scope, and are pinned as such. Rejecting them is not available: minifiers reuse short identifiers across scopes, and the packed bundle really does contain an unrelated `a(h[t],f,g,l,e,m)` that no name-based match can distinguish from a require call. Those are covered by the gate's runtime half instead, which resolves them for real. Bare-identifier calls need the one-string-argument shape for the same reason. The audit moves to scripts/lib/shipped-imports.ts so fixture packages can exercise it. The gate needs a real `npm pack` behind minutes of Swift and Android builds, so every check that runs it can only watch a healthy package pass — which is how a reader that matched nothing looked covered. The new fixtures assert the failure direction per resolution form: 16 of the 22 fail against the previous reader, and the 6 that pass are the quoted-spelling and pinned-limitation cases. A wiring assertion keeps the audit and both runtime probes attached to the gate, since fixtures alone would stay green if the call were deleted. Verified against the real built bundle: the closure resolves to exactly the two declared dependencies, so the stricter reader adds no false positives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 12:28:05 +02:00
"types": "./dist/src/artifacts.d.ts",
"import": "./dist/src/artifacts.js"
},
"./metro": {
build: verify the published tarball instead of grepping the bundle (#1578) * build: verify the published tarball instead of grepping the bundle Replaces the bundle-dependency grep with one gate that packs the tarball npm would publish and proves it sound from a clean consumer install: publint and attw on the tarball, a two-way dependency-closure audit, an import of every `exports` subpath, and the CLI smoke run — all from outside the workspace, where no pnpm link can mask an unresolvable specifier. Also stops the build from emitting a publishable bundle in the first place: a missing workspace link now fails `pnpm build` instead of warning and exiting 0, which is how 0.20.4 shipped an unresolvable `@agent-device/ad-script` import. publint found 12 real defects in the current package — every `exports` entry listed `types` after `import`, so TypeScript resolved declarations by accident rather than by condition. The dependency audit found `pngjs` declared as a runtime dependency while tsdown inlines it, an install every user paid for and no shipped code reached; it moves to devDependencies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(ci): run the package gate without pnpm on the Node floor pnpm 11.17 requires Node >= 22.13, so `pnpm check:package` could not start on the 22.12 floor the Packaged CLI job exists to cover. The gate needs only `node` and `npm`, so the job invokes the script directly. Splits the dependency-closure audit into a collector and a message builder to clear Fallow's complexity threshold, and classifies both packaging linters in ignoreDependencies: they are subprocess CLIs with no importable API here, which dependency analysis cannot follow to an import. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(publishing): read every literal resolution form in the closure audit The dependency-closure audit derived shipped imports from the ESM module record alone, so it could not see a package resolved through `require` or a `createRequire` result: neither produces a module-record entry. A lazy `createRequire('@agent-device/…')` would therefore clear the audit, the all-export probe and the exercised CLI paths, reintroducing the 0.20.4 published-install failure class for another command. Measuring the built bundle turned up a second, larger hole in the same reader. The shipped files are minified, and the minifier rewrites every string literal to a no-substitution template literal, so the dynamic-import extraction — which accepted quoted strings only — matched 0 of the 99 dynamic imports the bundle contains. The lazy `import()` path that broke 0.20.4 was reported as covered while checking nothing. Specifiers now come from the module record plus an AST walk over every literal runtime-resolution form: `import()`, `require()`, `require.resolve()`, an immediately-invoked `createRequire(...)`, and calls through a `createRequire` result under any import or minified alias. Both spellings of a string literal count everywhere, and `.cjs` joins the scanned extensions. Computed specifiers stay explicitly out of scope, and are pinned as such. Rejecting them is not available: minifiers reuse short identifiers across scopes, and the packed bundle really does contain an unrelated `a(h[t],f,g,l,e,m)` that no name-based match can distinguish from a require call. Those are covered by the gate's runtime half instead, which resolves them for real. Bare-identifier calls need the one-string-argument shape for the same reason. The audit moves to scripts/lib/shipped-imports.ts so fixture packages can exercise it. The gate needs a real `npm pack` behind minutes of Swift and Android builds, so every check that runs it can only watch a healthy package pass — which is how a reader that matched nothing looked covered. The new fixtures assert the failure direction per resolution form: 16 of the 22 fail against the previous reader, and the 6 that pass are the quoted-spelling and pinned-limitation cases. A wiring assertion keeps the audit and both runtime probes attached to the gate, since fixtures alone would stay green if the call were deleted. Verified against the real built bundle: the closure resolves to exactly the two declared dependencies, so the stricter reader adds no false positives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 12:28:05 +02:00
"types": "./dist/src/metro.d.ts",
"import": "./dist/src/metro.js"
},
"./batch": {
build: verify the published tarball instead of grepping the bundle (#1578) * build: verify the published tarball instead of grepping the bundle Replaces the bundle-dependency grep with one gate that packs the tarball npm would publish and proves it sound from a clean consumer install: publint and attw on the tarball, a two-way dependency-closure audit, an import of every `exports` subpath, and the CLI smoke run — all from outside the workspace, where no pnpm link can mask an unresolvable specifier. Also stops the build from emitting a publishable bundle in the first place: a missing workspace link now fails `pnpm build` instead of warning and exiting 0, which is how 0.20.4 shipped an unresolvable `@agent-device/ad-script` import. publint found 12 real defects in the current package — every `exports` entry listed `types` after `import`, so TypeScript resolved declarations by accident rather than by condition. The dependency audit found `pngjs` declared as a runtime dependency while tsdown inlines it, an install every user paid for and no shipped code reached; it moves to devDependencies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(ci): run the package gate without pnpm on the Node floor pnpm 11.17 requires Node >= 22.13, so `pnpm check:package` could not start on the 22.12 floor the Packaged CLI job exists to cover. The gate needs only `node` and `npm`, so the job invokes the script directly. Splits the dependency-closure audit into a collector and a message builder to clear Fallow's complexity threshold, and classifies both packaging linters in ignoreDependencies: they are subprocess CLIs with no importable API here, which dependency analysis cannot follow to an import. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(publishing): read every literal resolution form in the closure audit The dependency-closure audit derived shipped imports from the ESM module record alone, so it could not see a package resolved through `require` or a `createRequire` result: neither produces a module-record entry. A lazy `createRequire('@agent-device/…')` would therefore clear the audit, the all-export probe and the exercised CLI paths, reintroducing the 0.20.4 published-install failure class for another command. Measuring the built bundle turned up a second, larger hole in the same reader. The shipped files are minified, and the minifier rewrites every string literal to a no-substitution template literal, so the dynamic-import extraction — which accepted quoted strings only — matched 0 of the 99 dynamic imports the bundle contains. The lazy `import()` path that broke 0.20.4 was reported as covered while checking nothing. Specifiers now come from the module record plus an AST walk over every literal runtime-resolution form: `import()`, `require()`, `require.resolve()`, an immediately-invoked `createRequire(...)`, and calls through a `createRequire` result under any import or minified alias. Both spellings of a string literal count everywhere, and `.cjs` joins the scanned extensions. Computed specifiers stay explicitly out of scope, and are pinned as such. Rejecting them is not available: minifiers reuse short identifiers across scopes, and the packed bundle really does contain an unrelated `a(h[t],f,g,l,e,m)` that no name-based match can distinguish from a require call. Those are covered by the gate's runtime half instead, which resolves them for real. Bare-identifier calls need the one-string-argument shape for the same reason. The audit moves to scripts/lib/shipped-imports.ts so fixture packages can exercise it. The gate needs a real `npm pack` behind minutes of Swift and Android builds, so every check that runs it can only watch a healthy package pass — which is how a reader that matched nothing looked covered. The new fixtures assert the failure direction per resolution form: 16 of the 22 fail against the previous reader, and the 6 that pass are the quoted-spelling and pinned-limitation cases. A wiring assertion keeps the audit and both runtime probes attached to the gate, since fixtures alone would stay green if the call were deleted. Verified against the real built bundle: the closure resolves to exactly the two declared dependencies, so the stricter reader adds no false positives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 12:28:05 +02:00
"types": "./dist/src/batch.d.ts",
"import": "./dist/src/batch.js"
},
"./remote-config": {
build: verify the published tarball instead of grepping the bundle (#1578) * build: verify the published tarball instead of grepping the bundle Replaces the bundle-dependency grep with one gate that packs the tarball npm would publish and proves it sound from a clean consumer install: publint and attw on the tarball, a two-way dependency-closure audit, an import of every `exports` subpath, and the CLI smoke run — all from outside the workspace, where no pnpm link can mask an unresolvable specifier. Also stops the build from emitting a publishable bundle in the first place: a missing workspace link now fails `pnpm build` instead of warning and exiting 0, which is how 0.20.4 shipped an unresolvable `@agent-device/ad-script` import. publint found 12 real defects in the current package — every `exports` entry listed `types` after `import`, so TypeScript resolved declarations by accident rather than by condition. The dependency audit found `pngjs` declared as a runtime dependency while tsdown inlines it, an install every user paid for and no shipped code reached; it moves to devDependencies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(ci): run the package gate without pnpm on the Node floor pnpm 11.17 requires Node >= 22.13, so `pnpm check:package` could not start on the 22.12 floor the Packaged CLI job exists to cover. The gate needs only `node` and `npm`, so the job invokes the script directly. Splits the dependency-closure audit into a collector and a message builder to clear Fallow's complexity threshold, and classifies both packaging linters in ignoreDependencies: they are subprocess CLIs with no importable API here, which dependency analysis cannot follow to an import. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(publishing): read every literal resolution form in the closure audit The dependency-closure audit derived shipped imports from the ESM module record alone, so it could not see a package resolved through `require` or a `createRequire` result: neither produces a module-record entry. A lazy `createRequire('@agent-device/…')` would therefore clear the audit, the all-export probe and the exercised CLI paths, reintroducing the 0.20.4 published-install failure class for another command. Measuring the built bundle turned up a second, larger hole in the same reader. The shipped files are minified, and the minifier rewrites every string literal to a no-substitution template literal, so the dynamic-import extraction — which accepted quoted strings only — matched 0 of the 99 dynamic imports the bundle contains. The lazy `import()` path that broke 0.20.4 was reported as covered while checking nothing. Specifiers now come from the module record plus an AST walk over every literal runtime-resolution form: `import()`, `require()`, `require.resolve()`, an immediately-invoked `createRequire(...)`, and calls through a `createRequire` result under any import or minified alias. Both spellings of a string literal count everywhere, and `.cjs` joins the scanned extensions. Computed specifiers stay explicitly out of scope, and are pinned as such. Rejecting them is not available: minifiers reuse short identifiers across scopes, and the packed bundle really does contain an unrelated `a(h[t],f,g,l,e,m)` that no name-based match can distinguish from a require call. Those are covered by the gate's runtime half instead, which resolves them for real. Bare-identifier calls need the one-string-argument shape for the same reason. The audit moves to scripts/lib/shipped-imports.ts so fixture packages can exercise it. The gate needs a real `npm pack` behind minutes of Swift and Android builds, so every check that runs it can only watch a healthy package pass — which is how a reader that matched nothing looked covered. The new fixtures assert the failure direction per resolution form: 16 of the 22 fail against the previous reader, and the 6 that pass are the quoted-spelling and pinned-limitation cases. A wiring assertion keeps the audit and both runtime probes attached to the gate, since fixtures alone would stay green if the call were deleted. Verified against the real built bundle: the closure resolves to exactly the two declared dependencies, so the stricter reader adds no false positives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 12:28:05 +02:00
"types": "./dist/src/remote-config.d.ts",
"import": "./dist/src/remote-config.js"
},
"./install-source": {
build: verify the published tarball instead of grepping the bundle (#1578) * build: verify the published tarball instead of grepping the bundle Replaces the bundle-dependency grep with one gate that packs the tarball npm would publish and proves it sound from a clean consumer install: publint and attw on the tarball, a two-way dependency-closure audit, an import of every `exports` subpath, and the CLI smoke run — all from outside the workspace, where no pnpm link can mask an unresolvable specifier. Also stops the build from emitting a publishable bundle in the first place: a missing workspace link now fails `pnpm build` instead of warning and exiting 0, which is how 0.20.4 shipped an unresolvable `@agent-device/ad-script` import. publint found 12 real defects in the current package — every `exports` entry listed `types` after `import`, so TypeScript resolved declarations by accident rather than by condition. The dependency audit found `pngjs` declared as a runtime dependency while tsdown inlines it, an install every user paid for and no shipped code reached; it moves to devDependencies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(ci): run the package gate without pnpm on the Node floor pnpm 11.17 requires Node >= 22.13, so `pnpm check:package` could not start on the 22.12 floor the Packaged CLI job exists to cover. The gate needs only `node` and `npm`, so the job invokes the script directly. Splits the dependency-closure audit into a collector and a message builder to clear Fallow's complexity threshold, and classifies both packaging linters in ignoreDependencies: they are subprocess CLIs with no importable API here, which dependency analysis cannot follow to an import. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(publishing): read every literal resolution form in the closure audit The dependency-closure audit derived shipped imports from the ESM module record alone, so it could not see a package resolved through `require` or a `createRequire` result: neither produces a module-record entry. A lazy `createRequire('@agent-device/…')` would therefore clear the audit, the all-export probe and the exercised CLI paths, reintroducing the 0.20.4 published-install failure class for another command. Measuring the built bundle turned up a second, larger hole in the same reader. The shipped files are minified, and the minifier rewrites every string literal to a no-substitution template literal, so the dynamic-import extraction — which accepted quoted strings only — matched 0 of the 99 dynamic imports the bundle contains. The lazy `import()` path that broke 0.20.4 was reported as covered while checking nothing. Specifiers now come from the module record plus an AST walk over every literal runtime-resolution form: `import()`, `require()`, `require.resolve()`, an immediately-invoked `createRequire(...)`, and calls through a `createRequire` result under any import or minified alias. Both spellings of a string literal count everywhere, and `.cjs` joins the scanned extensions. Computed specifiers stay explicitly out of scope, and are pinned as such. Rejecting them is not available: minifiers reuse short identifiers across scopes, and the packed bundle really does contain an unrelated `a(h[t],f,g,l,e,m)` that no name-based match can distinguish from a require call. Those are covered by the gate's runtime half instead, which resolves them for real. Bare-identifier calls need the one-string-argument shape for the same reason. The audit moves to scripts/lib/shipped-imports.ts so fixture packages can exercise it. The gate needs a real `npm pack` behind minutes of Swift and Android builds, so every check that runs it can only watch a healthy package pass — which is how a reader that matched nothing looked covered. The new fixtures assert the failure direction per resolution form: 16 of the 22 fail against the previous reader, and the 6 that pass are the quoted-spelling and pinned-limitation cases. A wiring assertion keeps the audit and both runtime probes attached to the gate, since fixtures alone would stay green if the call were deleted. Verified against the real built bundle: the closure resolves to exactly the two declared dependencies, so the stricter reader adds no false positives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 12:28:05 +02:00
"types": "./dist/src/install-source.d.ts",
"import": "./dist/src/install-source.js"
},
"./android-adb": {
build: verify the published tarball instead of grepping the bundle (#1578) * build: verify the published tarball instead of grepping the bundle Replaces the bundle-dependency grep with one gate that packs the tarball npm would publish and proves it sound from a clean consumer install: publint and attw on the tarball, a two-way dependency-closure audit, an import of every `exports` subpath, and the CLI smoke run — all from outside the workspace, where no pnpm link can mask an unresolvable specifier. Also stops the build from emitting a publishable bundle in the first place: a missing workspace link now fails `pnpm build` instead of warning and exiting 0, which is how 0.20.4 shipped an unresolvable `@agent-device/ad-script` import. publint found 12 real defects in the current package — every `exports` entry listed `types` after `import`, so TypeScript resolved declarations by accident rather than by condition. The dependency audit found `pngjs` declared as a runtime dependency while tsdown inlines it, an install every user paid for and no shipped code reached; it moves to devDependencies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(ci): run the package gate without pnpm on the Node floor pnpm 11.17 requires Node >= 22.13, so `pnpm check:package` could not start on the 22.12 floor the Packaged CLI job exists to cover. The gate needs only `node` and `npm`, so the job invokes the script directly. Splits the dependency-closure audit into a collector and a message builder to clear Fallow's complexity threshold, and classifies both packaging linters in ignoreDependencies: they are subprocess CLIs with no importable API here, which dependency analysis cannot follow to an import. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(publishing): read every literal resolution form in the closure audit The dependency-closure audit derived shipped imports from the ESM module record alone, so it could not see a package resolved through `require` or a `createRequire` result: neither produces a module-record entry. A lazy `createRequire('@agent-device/…')` would therefore clear the audit, the all-export probe and the exercised CLI paths, reintroducing the 0.20.4 published-install failure class for another command. Measuring the built bundle turned up a second, larger hole in the same reader. The shipped files are minified, and the minifier rewrites every string literal to a no-substitution template literal, so the dynamic-import extraction — which accepted quoted strings only — matched 0 of the 99 dynamic imports the bundle contains. The lazy `import()` path that broke 0.20.4 was reported as covered while checking nothing. Specifiers now come from the module record plus an AST walk over every literal runtime-resolution form: `import()`, `require()`, `require.resolve()`, an immediately-invoked `createRequire(...)`, and calls through a `createRequire` result under any import or minified alias. Both spellings of a string literal count everywhere, and `.cjs` joins the scanned extensions. Computed specifiers stay explicitly out of scope, and are pinned as such. Rejecting them is not available: minifiers reuse short identifiers across scopes, and the packed bundle really does contain an unrelated `a(h[t],f,g,l,e,m)` that no name-based match can distinguish from a require call. Those are covered by the gate's runtime half instead, which resolves them for real. Bare-identifier calls need the one-string-argument shape for the same reason. The audit moves to scripts/lib/shipped-imports.ts so fixture packages can exercise it. The gate needs a real `npm pack` behind minutes of Swift and Android builds, so every check that runs it can only watch a healthy package pass — which is how a reader that matched nothing looked covered. The new fixtures assert the failure direction per resolution form: 16 of the 22 fail against the previous reader, and the 6 that pass are the quoted-spelling and pinned-limitation cases. A wiring assertion keeps the audit and both runtime probes attached to the gate, since fixtures alone would stay green if the call were deleted. Verified against the real built bundle: the closure resolves to exactly the two declared dependencies, so the stricter reader adds no false positives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 12:28:05 +02:00
"types": "./dist/src/android-adb.d.ts",
"import": "./dist/src/android-adb.js"
},
"./limrun": {
build: verify the published tarball instead of grepping the bundle (#1578) * build: verify the published tarball instead of grepping the bundle Replaces the bundle-dependency grep with one gate that packs the tarball npm would publish and proves it sound from a clean consumer install: publint and attw on the tarball, a two-way dependency-closure audit, an import of every `exports` subpath, and the CLI smoke run — all from outside the workspace, where no pnpm link can mask an unresolvable specifier. Also stops the build from emitting a publishable bundle in the first place: a missing workspace link now fails `pnpm build` instead of warning and exiting 0, which is how 0.20.4 shipped an unresolvable `@agent-device/ad-script` import. publint found 12 real defects in the current package — every `exports` entry listed `types` after `import`, so TypeScript resolved declarations by accident rather than by condition. The dependency audit found `pngjs` declared as a runtime dependency while tsdown inlines it, an install every user paid for and no shipped code reached; it moves to devDependencies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(ci): run the package gate without pnpm on the Node floor pnpm 11.17 requires Node >= 22.13, so `pnpm check:package` could not start on the 22.12 floor the Packaged CLI job exists to cover. The gate needs only `node` and `npm`, so the job invokes the script directly. Splits the dependency-closure audit into a collector and a message builder to clear Fallow's complexity threshold, and classifies both packaging linters in ignoreDependencies: they are subprocess CLIs with no importable API here, which dependency analysis cannot follow to an import. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(publishing): read every literal resolution form in the closure audit The dependency-closure audit derived shipped imports from the ESM module record alone, so it could not see a package resolved through `require` or a `createRequire` result: neither produces a module-record entry. A lazy `createRequire('@agent-device/…')` would therefore clear the audit, the all-export probe and the exercised CLI paths, reintroducing the 0.20.4 published-install failure class for another command. Measuring the built bundle turned up a second, larger hole in the same reader. The shipped files are minified, and the minifier rewrites every string literal to a no-substitution template literal, so the dynamic-import extraction — which accepted quoted strings only — matched 0 of the 99 dynamic imports the bundle contains. The lazy `import()` path that broke 0.20.4 was reported as covered while checking nothing. Specifiers now come from the module record plus an AST walk over every literal runtime-resolution form: `import()`, `require()`, `require.resolve()`, an immediately-invoked `createRequire(...)`, and calls through a `createRequire` result under any import or minified alias. Both spellings of a string literal count everywhere, and `.cjs` joins the scanned extensions. Computed specifiers stay explicitly out of scope, and are pinned as such. Rejecting them is not available: minifiers reuse short identifiers across scopes, and the packed bundle really does contain an unrelated `a(h[t],f,g,l,e,m)` that no name-based match can distinguish from a require call. Those are covered by the gate's runtime half instead, which resolves them for real. Bare-identifier calls need the one-string-argument shape for the same reason. The audit moves to scripts/lib/shipped-imports.ts so fixture packages can exercise it. The gate needs a real `npm pack` behind minutes of Swift and Android builds, so every check that runs it can only watch a healthy package pass — which is how a reader that matched nothing looked covered. The new fixtures assert the failure direction per resolution form: 16 of the 22 fail against the previous reader, and the 6 that pass are the quoted-spelling and pinned-limitation cases. A wiring assertion keeps the audit and both runtime probes attached to the gate, since fixtures alone would stay green if the call were deleted. Verified against the real built bundle: the closure resolves to exactly the two declared dependencies, so the stricter reader adds no false positives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 12:28:05 +02:00
"types": "./dist/src/limrun.d.ts",
"import": "./dist/src/limrun.js"
},
"./contracts": {
build: verify the published tarball instead of grepping the bundle (#1578) * build: verify the published tarball instead of grepping the bundle Replaces the bundle-dependency grep with one gate that packs the tarball npm would publish and proves it sound from a clean consumer install: publint and attw on the tarball, a two-way dependency-closure audit, an import of every `exports` subpath, and the CLI smoke run — all from outside the workspace, where no pnpm link can mask an unresolvable specifier. Also stops the build from emitting a publishable bundle in the first place: a missing workspace link now fails `pnpm build` instead of warning and exiting 0, which is how 0.20.4 shipped an unresolvable `@agent-device/ad-script` import. publint found 12 real defects in the current package — every `exports` entry listed `types` after `import`, so TypeScript resolved declarations by accident rather than by condition. The dependency audit found `pngjs` declared as a runtime dependency while tsdown inlines it, an install every user paid for and no shipped code reached; it moves to devDependencies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(ci): run the package gate without pnpm on the Node floor pnpm 11.17 requires Node >= 22.13, so `pnpm check:package` could not start on the 22.12 floor the Packaged CLI job exists to cover. The gate needs only `node` and `npm`, so the job invokes the script directly. Splits the dependency-closure audit into a collector and a message builder to clear Fallow's complexity threshold, and classifies both packaging linters in ignoreDependencies: they are subprocess CLIs with no importable API here, which dependency analysis cannot follow to an import. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(publishing): read every literal resolution form in the closure audit The dependency-closure audit derived shipped imports from the ESM module record alone, so it could not see a package resolved through `require` or a `createRequire` result: neither produces a module-record entry. A lazy `createRequire('@agent-device/…')` would therefore clear the audit, the all-export probe and the exercised CLI paths, reintroducing the 0.20.4 published-install failure class for another command. Measuring the built bundle turned up a second, larger hole in the same reader. The shipped files are minified, and the minifier rewrites every string literal to a no-substitution template literal, so the dynamic-import extraction — which accepted quoted strings only — matched 0 of the 99 dynamic imports the bundle contains. The lazy `import()` path that broke 0.20.4 was reported as covered while checking nothing. Specifiers now come from the module record plus an AST walk over every literal runtime-resolution form: `import()`, `require()`, `require.resolve()`, an immediately-invoked `createRequire(...)`, and calls through a `createRequire` result under any import or minified alias. Both spellings of a string literal count everywhere, and `.cjs` joins the scanned extensions. Computed specifiers stay explicitly out of scope, and are pinned as such. Rejecting them is not available: minifiers reuse short identifiers across scopes, and the packed bundle really does contain an unrelated `a(h[t],f,g,l,e,m)` that no name-based match can distinguish from a require call. Those are covered by the gate's runtime half instead, which resolves them for real. Bare-identifier calls need the one-string-argument shape for the same reason. The audit moves to scripts/lib/shipped-imports.ts so fixture packages can exercise it. The gate needs a real `npm pack` behind minutes of Swift and Android builds, so every check that runs it can only watch a healthy package pass — which is how a reader that matched nothing looked covered. The new fixtures assert the failure direction per resolution form: 16 of the 22 fail against the previous reader, and the 6 that pass are the quoted-spelling and pinned-limitation cases. A wiring assertion keeps the audit and both runtime probes attached to the gate, since fixtures alone would stay green if the call were deleted. Verified against the real built bundle: the closure resolves to exactly the two declared dependencies, so the stricter reader adds no false positives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 12:28:05 +02:00
"types": "./dist/src/contracts.d.ts",
"import": "./dist/src/contracts.js"
},
"./selectors": {
build: verify the published tarball instead of grepping the bundle (#1578) * build: verify the published tarball instead of grepping the bundle Replaces the bundle-dependency grep with one gate that packs the tarball npm would publish and proves it sound from a clean consumer install: publint and attw on the tarball, a two-way dependency-closure audit, an import of every `exports` subpath, and the CLI smoke run — all from outside the workspace, where no pnpm link can mask an unresolvable specifier. Also stops the build from emitting a publishable bundle in the first place: a missing workspace link now fails `pnpm build` instead of warning and exiting 0, which is how 0.20.4 shipped an unresolvable `@agent-device/ad-script` import. publint found 12 real defects in the current package — every `exports` entry listed `types` after `import`, so TypeScript resolved declarations by accident rather than by condition. The dependency audit found `pngjs` declared as a runtime dependency while tsdown inlines it, an install every user paid for and no shipped code reached; it moves to devDependencies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(ci): run the package gate without pnpm on the Node floor pnpm 11.17 requires Node >= 22.13, so `pnpm check:package` could not start on the 22.12 floor the Packaged CLI job exists to cover. The gate needs only `node` and `npm`, so the job invokes the script directly. Splits the dependency-closure audit into a collector and a message builder to clear Fallow's complexity threshold, and classifies both packaging linters in ignoreDependencies: they are subprocess CLIs with no importable API here, which dependency analysis cannot follow to an import. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(publishing): read every literal resolution form in the closure audit The dependency-closure audit derived shipped imports from the ESM module record alone, so it could not see a package resolved through `require` or a `createRequire` result: neither produces a module-record entry. A lazy `createRequire('@agent-device/…')` would therefore clear the audit, the all-export probe and the exercised CLI paths, reintroducing the 0.20.4 published-install failure class for another command. Measuring the built bundle turned up a second, larger hole in the same reader. The shipped files are minified, and the minifier rewrites every string literal to a no-substitution template literal, so the dynamic-import extraction — which accepted quoted strings only — matched 0 of the 99 dynamic imports the bundle contains. The lazy `import()` path that broke 0.20.4 was reported as covered while checking nothing. Specifiers now come from the module record plus an AST walk over every literal runtime-resolution form: `import()`, `require()`, `require.resolve()`, an immediately-invoked `createRequire(...)`, and calls through a `createRequire` result under any import or minified alias. Both spellings of a string literal count everywhere, and `.cjs` joins the scanned extensions. Computed specifiers stay explicitly out of scope, and are pinned as such. Rejecting them is not available: minifiers reuse short identifiers across scopes, and the packed bundle really does contain an unrelated `a(h[t],f,g,l,e,m)` that no name-based match can distinguish from a require call. Those are covered by the gate's runtime half instead, which resolves them for real. Bare-identifier calls need the one-string-argument shape for the same reason. The audit moves to scripts/lib/shipped-imports.ts so fixture packages can exercise it. The gate needs a real `npm pack` behind minutes of Swift and Android builds, so every check that runs it can only watch a healthy package pass — which is how a reader that matched nothing looked covered. The new fixtures assert the failure direction per resolution form: 16 of the 22 fail against the previous reader, and the 6 that pass are the quoted-spelling and pinned-limitation cases. A wiring assertion keeps the audit and both runtime probes attached to the gate, since fixtures alone would stay green if the call were deleted. Verified against the real built bundle: the closure resolves to exactly the two declared dependencies, so the stricter reader adds no false positives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 12:28:05 +02:00
"types": "./dist/src/selectors.d.ts",
"import": "./dist/src/selectors.js"
},
"./finders": {
build: verify the published tarball instead of grepping the bundle (#1578) * build: verify the published tarball instead of grepping the bundle Replaces the bundle-dependency grep with one gate that packs the tarball npm would publish and proves it sound from a clean consumer install: publint and attw on the tarball, a two-way dependency-closure audit, an import of every `exports` subpath, and the CLI smoke run — all from outside the workspace, where no pnpm link can mask an unresolvable specifier. Also stops the build from emitting a publishable bundle in the first place: a missing workspace link now fails `pnpm build` instead of warning and exiting 0, which is how 0.20.4 shipped an unresolvable `@agent-device/ad-script` import. publint found 12 real defects in the current package — every `exports` entry listed `types` after `import`, so TypeScript resolved declarations by accident rather than by condition. The dependency audit found `pngjs` declared as a runtime dependency while tsdown inlines it, an install every user paid for and no shipped code reached; it moves to devDependencies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(ci): run the package gate without pnpm on the Node floor pnpm 11.17 requires Node >= 22.13, so `pnpm check:package` could not start on the 22.12 floor the Packaged CLI job exists to cover. The gate needs only `node` and `npm`, so the job invokes the script directly. Splits the dependency-closure audit into a collector and a message builder to clear Fallow's complexity threshold, and classifies both packaging linters in ignoreDependencies: they are subprocess CLIs with no importable API here, which dependency analysis cannot follow to an import. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(publishing): read every literal resolution form in the closure audit The dependency-closure audit derived shipped imports from the ESM module record alone, so it could not see a package resolved through `require` or a `createRequire` result: neither produces a module-record entry. A lazy `createRequire('@agent-device/…')` would therefore clear the audit, the all-export probe and the exercised CLI paths, reintroducing the 0.20.4 published-install failure class for another command. Measuring the built bundle turned up a second, larger hole in the same reader. The shipped files are minified, and the minifier rewrites every string literal to a no-substitution template literal, so the dynamic-import extraction — which accepted quoted strings only — matched 0 of the 99 dynamic imports the bundle contains. The lazy `import()` path that broke 0.20.4 was reported as covered while checking nothing. Specifiers now come from the module record plus an AST walk over every literal runtime-resolution form: `import()`, `require()`, `require.resolve()`, an immediately-invoked `createRequire(...)`, and calls through a `createRequire` result under any import or minified alias. Both spellings of a string literal count everywhere, and `.cjs` joins the scanned extensions. Computed specifiers stay explicitly out of scope, and are pinned as such. Rejecting them is not available: minifiers reuse short identifiers across scopes, and the packed bundle really does contain an unrelated `a(h[t],f,g,l,e,m)` that no name-based match can distinguish from a require call. Those are covered by the gate's runtime half instead, which resolves them for real. Bare-identifier calls need the one-string-argument shape for the same reason. The audit moves to scripts/lib/shipped-imports.ts so fixture packages can exercise it. The gate needs a real `npm pack` behind minutes of Swift and Android builds, so every check that runs it can only watch a healthy package pass — which is how a reader that matched nothing looked covered. The new fixtures assert the failure direction per resolution form: 16 of the 22 fail against the previous reader, and the 6 that pass are the quoted-spelling and pinned-limitation cases. A wiring assertion keeps the audit and both runtime probes attached to the gate, since fixtures alone would stay green if the call were deleted. Verified against the real built bundle: the closure resolves to exactly the two declared dependencies, so the stricter reader adds no false positives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 12:28:05 +02:00
"types": "./dist/src/finders.d.ts",
"import": "./dist/src/finders.js"
feat(ai-sdk): add agent-device/ai-sdk tool set and document the MCP zero-code path (#1804) * feat(ai-sdk): add agent-device/ai-sdk tool set and document the MCP zero-code path Adds `createAgentDeviceTools()` under a new `agent-device/ai-sdk` subpath, built from the same command registry the MCP server uses so both stay in lockstep without a hand-maintained tool list. Introduces a `frameworkTier` descriptor facet ('core' | 'extended') so the factory can default to a curated perceive/act loop instead of handing a model dozens of tools. `ai` is wired as an optional peer dependency, imported lazily inside the factory rather than at module scope, so importing the subpath itself never requires `ai` to be installed - only calling it does. The package's own publishing gate (scripts/lib/shipped-imports.ts) is extended to recognize peerDependencies as a valid resolution source, since this is the first optional peer this package has shipped. Also restructures the AI SDK doc around three tiers (zero-code via @ai-sdk/mcp, the new typed tool set, hand-written tools) and fixes a stale `needsApproval` reference in favor of the current `toolApproval` API. * fix(layering): classify src/ai-sdk as a rank-4 zone The layering guard requires every src/<folder>/ to be explicitly ranked or unranked; the new src/ai-sdk/ subpath (added in the prior commit) was left unclassified, failing CI's Layering Guard job. It sits at the same tier as client/compat/daemon-server/metro/remote/sdk - a public integration surface consuming mcp (3) and core (2), imported by nothing else in the tree. * fix(ci): cover, exempt, and pack the new ai-sdk subpath Fixes the remaining CI failures on the ai-sdk subpath commit: - Coverage: src/ai-sdk/index.ts had no dedicated unit test (only manual/ integration verification), so changed-line coverage sat at 6.9% against the 70% gate. Adds src/ai-sdk/__tests__/index.test.ts (core vs 'all' tool filtering, session/platform pinning and schema hiding, error normalization, toolApproval passthrough) with createCommandToolExecutor and createAgentDeviceClient mocked the same way command-tools.test.ts does, plus a dedicated missing-peer-dependency.test.ts that mocks `ai` itself to throw, isolated to its own file so it doesn't affect the other tests' use of the real, installed `ai` package. Changed-line coverage is now 29/29 (100%). - Fallow Code Quality: src/ai-sdk/index.ts and examples/sdk/ai-sdk-tools.ts are entry points with no in-repo importer (reached only via package.json exports / run directly), and the new subpath's exports are unused internally by design - both need the same treatment src/sdk/*.ts and its examples already have in .fallowrc.json. - Integration Tests: test/integration/installed-package-metro.test.ts and src/__tests__/package-exports.test.ts each hand-list every published subpath and smoke-check it from a real packed install; added ./ai-sdk to both so the new subpath is actually exercised, not just silently passing. * fix(ai-sdk): hide MCP transport/config fields from the model too createAgentDeviceTools() only removed session and mcpOutputFormat from tool schemas. stateDir was still model-visible and reached the shared executor as client configuration, letting a tool call redirect into a different daemon state directory - defeating the "one pinned session" guarantee the factory exists to provide. includeCost and responseLevel are MCP tool-config knobs in the same category, irrelevant to this adapter. Widens the hidden-field set to session/stateDir/mcpOutputFormat/ includeCost/responseLevel, and now strips them from the runtime input inside execute() too (not just the schema), so the guarantee holds even if a caller bypasses schema validation. The schema-properties filter and the input filter now share one omitHidden() helper instead of two near- duplicate implementations. Addresses the P1 review comment on #1804.
2026-08-18 11:57:34 +02:00
},
"./ai-sdk": {
"types": "./dist/src/ai-sdk.d.ts",
"import": "./dist/src/ai-sdk.js"
}
},
2026-01-30 19:41:00 +01:00
"engines": {
"node": ">=22.12"
2026-01-30 19:41:00 +01:00
},
"bin": {
"agent-device": "bin/agent-device.mjs"
},
"scripts": {
build: migrate the library build from rslib to tsdown (Rolldown) (#1087) * build: migrate the library build from rslib to tsdown (Rolldown) Replace the Rspack-based rslib build with tsdown, the Rolldown-based library bundler from the Vite toolchain family, so bundling, testing (Vitest/Vite), linting (oxlint), and formatting (oxfmt) all run on the same OXC/Rolldown stack. Outcome vs the rslib baseline (size-report): - build time: ~53s -> ~2s - JS raw +16.2 kB (+1.1%), JS gzip +2.7 kB (+0.6%) - the residual gap is OXC vs SWC minifier tightness, not chunking - npm tarball -3.0 kB - CLI --version startup ~3 ms faster; --help within the +/-5 ms measurement noise of interleaved A/B runs Chunk-merging experiments (single shared group, entries-aware groups, small-module groups) all regressed either total size or --help startup (a merged shared chunk adds +140 ms), so the default Rolldown split graph is kept. Custom codeSplitting groups also currently trip a rolldown-plugin-dts bug that re-emits type-only imports as runtime imports. Declarations still bundle per entry via tsgo; dist layout, entry names, and the internal/ worker/daemon entry resolution contract are unchanged. @microsoft/api-extractor was only consumed by rslib dts bundling and is removed together with @rslib/core. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FqeW8sA2ZnvnftdvpCqFMS * ci: only cache the pnpm store when setup installs dependencies The layering-guard job uses setup-node-pnpm with install-deps: false, so it never creates a pnpm store. setup-node's post-job cache save then fails with a path validation error whenever the lockfile hash misses the cache - which any lockfile-changing PR does. Gate the cache on install-deps so no-install jobs skip pnpm store caching entirely. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FqeW8sA2ZnvnftdvpCqFMS --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-07-04 15:58:05 +02:00
"build": "tsdown",
"clean:daemon": "node --experimental-strip-types scripts/clean-daemon.ts",
"daemon:state-dir": "node --experimental-strip-types scripts/print-daemon-state-dir.ts",
chore: add agent-efficiency regression guards (#1174) * chore: ratchet architecture dependency graph Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test: ratchet agent-facing output economy Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * feat: derive command navigation explanations Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: keep efficiency checks fallow-clean Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(layering): enforce back-edge ceiling monotonicity and cover root src files Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * ci(layering): pin back-edge-ceiling ratchet to PR merge-base Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(output-economy): baseline-independent actionability floors, policy-derived error, like-for-like screenshot surfaces Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(explain): resolve true CLI aliases, canonical usage, and derived owners Surface true CLI aliases from parser normalization (long-press, metrics, tap, launch, relaunch) distinct from catalog keys, preserving implied-flag semantics (relaunch => open --relaunch). Extract the canonical single-line usage builder to src/utils/cli-usage.ts so schemas without usageOverride include positionals and flags. Replace guessed handler paths with a completeness-checked daemon-route owner map keyed by the closed DaemonCommandRoute union, fixing silently-dropped non-kebab routes (reactNative, recordTrace) and generic dispatch. Add table-driven coverage for aliases, synthesized usage, split-family/route-variant/dispatch owners, structured output, and explain:command CLI exit/stdout/stderr. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: enforce exact ratchets and compact command explain Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: colocate command ownership metadata Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: bind daemon owners to production routes Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: preserve generic dispatch bundling Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: enforce monotonic output budgets Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Michał Pierzchała <thymikee@gmail.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-10 11:54:09 +02:00
"explain:command": "node --experimental-strip-types scripts/explain-command.ts",
"clean:xcuitest": "node scripts/clean-xcuitest-derived.mjs",
"clean:xcuitest:ios": "node scripts/clean-xcuitest-derived.mjs ios",
"clean:xcuitest:macos": "node scripts/clean-xcuitest-derived.mjs macos",
"clean:xcuitest:tvos": "node scripts/clean-xcuitest-derived.mjs tvos",
"clean:xcuitest:visionos": "node scripts/clean-xcuitest-derived.mjs visionos",
"rebuild:cli": "pnpm build && pnpm clean:daemon",
"build:xcuitest": "pnpm build:xcuitest:ios && pnpm build:xcuitest:macos",
"build:xcuitest:ios": "AGENT_DEVICE_XCUITEST_PLATFORM=ios sh ./scripts/build-xcuitest-apple.sh",
"build:xcuitest:ios:clean": "pnpm clean:xcuitest:ios && pnpm build:xcuitest:ios",
"build:xcuitest:macos": "AGENT_DEVICE_XCUITEST_PLATFORM=macos sh ./scripts/build-xcuitest-apple.sh",
"build:xcuitest:macos:clean": "pnpm clean:xcuitest:macos && pnpm build:xcuitest:macos",
"build:xcuitest:tvos": "AGENT_DEVICE_XCUITEST_PLATFORM=tvos sh ./scripts/build-xcuitest-apple.sh",
"build:xcuitest:tvos:clean": "pnpm clean:xcuitest:tvos && pnpm build:xcuitest:tvos",
"build:xcuitest:visionos": "AGENT_DEVICE_XCUITEST_PLATFORM=visionos sh ./scripts/build-xcuitest-apple.sh",
"build:xcuitest:visionos:clean": "pnpm clean:xcuitest:visionos && pnpm build:xcuitest:visionos",
"build:android-snapshot-helper": "AGENT_DEVICE_ANDROID_HELPER=snapshot sh ./scripts/build-android-helper.sh $(node -p \"require('./package.json').version\") .tmp/android-snapshot-helper",
"package:android-snapshot-helper": "AGENT_DEVICE_ANDROID_HELPER=snapshot sh ./scripts/package-android-helper.sh $(node -p \"require('./package.json').version\") v$(node -p \"require('./package.json').version\") .tmp/android-snapshot-helper",
"package:android-snapshot-helper:npm": "rm -rf android/snapshot-helper/dist && AGENT_DEVICE_ANDROID_HELPER=snapshot sh ./scripts/package-android-helper.sh $(node -p \"require('./package.json').version\") v$(node -p \"require('./package.json').version\") android/snapshot-helper/dist",
"package:apple-runner:npm": "node scripts/package-apple-runner-source.mjs",
feat(android): one persistent automation helper owning snapshot + viewport + canonical injection (#1281) * feat(android): consolidate touch injection and gesture viewport into the persistent snapshot helper (#1275) One Android automation helper now owns snapshot capture, gesture viewport resolution, and canonical one-/two-pointer plan injection. A live persistent helper session executes gesture/viewport commands over its socket protocol; without a session the same APK runs one-shot via am instrument. The separate one-shot multitouch helper APK is deleted (atomic replacement, no fallback). Touch scheduling/injection is extracted into focused Java classes (TouchPlan, TouchPlanInjector, PointerEventSchedule, GestureViewportReader) instead of growing SnapshotInstrumentation. ADR 0013 amended. * fix(android): stop a structurally-failed helper session before the one-shot viewport retry A structured ok=false viewport response leaves the session process alive, and Android permits only one instrumentation owner of UiAutomation - running the one-shot fallback against a still-live helper contends with it and masks the original structured failure. Stop the session first; regression pins that the one-shot retry only executes once the session is gone. * refactor(android): extract helper touch dispatch into focused classes; split session tests; document helper API v2 (PR #1281 review) Addresses findings 2 and 3 from PR #1281 review (finding 1, viewport session-stop ordering, was already fixed in 5961b9247). - Extract SnapshotInstrumentation.java's one-shot/session touch dispatch into TouchCommandHandler.java (viewport/gesture population, UiAutomation- parameterized) and SessionResponseWriter.java (session response encoding), with shared PROTOCOL/HELPER_API_VERSION/OUTPUT_FORMAT constants moved to a tiny HelperProtocol.java. SnapshotInstrumentation.java shrinks from 908 to 803 lines; wire format (header keys/values, error shapes) is unchanged. - Split touch-helper.test.ts (~720 lines) into touch-helper.test.ts (normalize/parse/one-shot gesture+viewport+result envelope) and touch-helper-session.test.ts (persistent-session transport + fake-session harness), moving shared device/plan/install-probe fixtures used by both files into touch-helper.fixtures.ts. - Update android/snapshot-helper/README.md to document helper API v2: the one-shot viewport/gesture modes, the android-touch-plan-v1 payload shape, and the persistent session's socket command/response contract. * fix(android): invalidate helper session after APK replacement; recycle viewport windows; align ADR 0002 (PR #1281 re-review) - prepareAndroidTouchHelper now mirrors the snapshot path: when ensureAndroidSnapshotHelper replaces the APK (install.installed), the persistent session started against the previous binary is stopped before any touch command, so gestures run one-shot against the fresh install instead of a dead/stale session socket. Regression: 'an APK replacement stops the stale session and the gesture runs one-shot' drives a live fake session through an outdated-install probe (new outdatedVersionAdb fixture) and asserts the session socket receives no gesture, the one-shot instrumentation path executes, and the session is gone. - GestureViewportReader.read no longer leaks AccessibilityWindowInfo: a single pass copies the active/focused and first-application bounds into locals, every window is recycled in a finally, and the existing precedence (active/focused app bounds, root-in-active-window, fallback app bounds, IllegalStateException) is applied afterwards, unchanged. - ADR 0002's touch-synthesis paragraph is amended (2026-07, issue #1275) to the shared-helper model, consistent with ADR 0013: a live persistent helper session executes touch commands directly, one-shot otherwise; the old stop-before-gesture requirement is kept as historical context. * fix(android): resolve touch helper artifact from the ADB provider like snapshots do (PR #1281 re-review) - prepareAndroidTouchHelper now uses the same artifact precedence as snapshot capture: the scoped adbProvider's snapshotHelperArtifact when present, otherwise the bundled resolver (whose strict unavailable error is preserved). The provider artifact drives both the install decision and the instrumentationRunner used for one-shot commands, so an ADB-backed provider that supplies a helper artifact but no native touch override runs snapshots and gestures against the same single helper (issue #1275). Regression: 'a provider-supplied snapshotHelperArtifact overrides the bundled artifact for touch' pins the provider packageName on the install probe, the provider apkPath on the install call, the provider instrumentationRunner on the am instrument args, and that the bundled resolver is never invoked. - ADR 0002 now states explicitly that one-shot retry applies only to idempotent reads (viewport) after the failed session is stopped; non-idempotent gesture failures surface directly. - Helper README session transport corrected: a persistent process serving one short-lived socket connection per request (the server closes each accepted connection), not a single long-lived connection. * fix(android): guard touch session reuse on helper identity, stop mismatched sessions (PR #1281 re-review) Persistent helper sessions are keyed by device, so touch reuse must also prove the live session runs the helper binary the command selected. The session record now stores its helper identity (packageName, runner, helperVersion, helperVersionCode — the same values that feed the snapshot session identity), and runAndroidSnapshotHelperSessionTouchCommand takes the requesting helper identity: on mismatch (packageName/runner always; version/versionCode when both sides define them) it stops the session and returns undefined, so the touch command runs one-shot against the selected artifact — gestures never start sessions; the next snapshot restarts one with the right artifact. Matching identity reuses the session as before. Snapshot capture identity and behavior are unchanged. Regression: 'a provider artifact that mismatches the live session helper stops it and runs one-shot' — a live fake session from the bundled fixture artifact, then a gesture through an ADB provider supplying an already-current artifact with a distinct packageName/runner (no install): the old session socket receives zero gesture commands, the session is stopped, the one-shot am instrument args end with the provider runner, and helperTransport is 'instrumentation'. * fix(android): include artifact sha in helper session identity; evict stale install memo entries (PR #1281 re-review) Same-version binary replacement changes only the APK sha, so identity guards keyed on package/runner/version/versionCode could not detect a crossover between two artifacts that differ only in bytes: - The artifact sha256 now joins the helper identity end-to-end: AndroidSnapshotHelperCaptureOptions gains helperSha256 (snapshot.ts passes artifact.manifest.sha256 alongside version/versionCode), the session record stores it, createSessionIdentity includes it (making snapshot session reuse sha-aware, consistent with the install path's existing sha check), and the touch identity guard compares it via the same both-defined rule. - ensureAndroidSnapshotHelper's install memo now evicts every other cached decision for the same device+package when it records an install/current decision, so installing B invalidates A's stale 'current' memo and a later command selecting A re-inspects the device instead of skipping the sha check. Regressions: 'a same-version artifact with a different sha stops the live session and runs one-shot' (touch-helper-session.test.ts — B owns the live session, a gesture selecting same-version different-sha A sends zero commands to B's socket, stops it, and completes one-shot) and 'installing a same-version different-sha helper evicts the stale install memo' (snapshot-helper.test.ts — A:current cached, B installed, selecting A re-inspects and reinstalls instead of serving the stale memo). Both verified to fail without their fix.
2026-07-16 17:04:42 +02:00
"build:android": "pnpm package:android-snapshot-helper:npm && pnpm package:android-ime-helper:npm",
"build:android-ime-helper": "AGENT_DEVICE_ANDROID_HELPER=ime sh ./scripts/build-android-helper.sh $(node -p \"require('./package.json').version\") .tmp/android-ime-helper",
"package:android-ime-helper": "AGENT_DEVICE_ANDROID_HELPER=ime sh ./scripts/package-android-helper.sh $(node -p \"require('./package.json').version\") .tmp/android-ime-helper",
"package:android-ime-helper:npm": "rm -rf android/ime-helper/dist && AGENT_DEVICE_ANDROID_HELPER=ime sh ./scripts/package-android-helper.sh $(node -p \"require('./package.json').version\") android/ime-helper/dist",
"build:macos-helper": "node --experimental-strip-types scripts/swift-toolchain-tmpdir.ts swift build -c release --package-path apple/macos-helper",
"build:macos-helper:clean": "node --experimental-strip-types scripts/swift-toolchain-tmpdir.ts swift package --package-path apple/macos-helper clean && pnpm build:macos-helper",
test(ios): establish snapshot convergence baselines and permanent evidence (#2204) * test(ios): add snapshot convergence evidence harness * fix(ios): satisfy benchmark CI guards * fix(ios): constrain benchmark proxy routes * fix(ios-benchmark): enforce cell admission evidence * fix(ios-benchmark): protect benchmark state ownership * fix(ios-benchmark): use proxy port flag * fix(ios-benchmark): let proxy choose an ephemeral port * test(ios-benchmark): keep CLI process seam local * fix(ios-benchmark): parse proxy startup envelope * fix(ios-benchmark): bind proxy lease to simulator * fix(ios-benchmark): keep fresh proxy CLI sessions isolated * fix(ios-benchmark): preserve async timeout evidence * docs(ios-benchmark): retain exact-head evidence * test(ios): reveal offscreen alert fixture controls * test(ios): reset alert between relaunch samples * test(ios): admit native alert snapshots * docs(ios): publish snapshot convergence corpus * chore(ios): format benchmark evidence * fix(ios-benchmark): admit proxy fixture anchors * docs(ios): republish exact-head benchmark corpus * fix(size): make publish asset evidence hermetic * style(size): format package evidence test * test(size): update publish preparation contracts * fix: retire stale utils layering zone * test: pin shared publish asset owner * test: verify preserved size reporter closure * fix: move mutation ownership to snapshot module * test(ios): add snapshot convergence evidence harness * fix(ios): satisfy benchmark CI guards * fix(ios): constrain benchmark proxy routes * fix(ios-benchmark): enforce cell admission evidence * fix(ios-benchmark): protect benchmark state ownership * fix(ios-benchmark): use proxy port flag * fix(ios-benchmark): let proxy choose an ephemeral port * test(ios-benchmark): keep CLI process seam local * fix(ios-benchmark): parse proxy startup envelope * fix(ios-benchmark): bind proxy lease to simulator * fix(ios-benchmark): keep fresh proxy CLI sessions isolated * fix(ios-benchmark): preserve async timeout evidence * docs(ios-benchmark): retain exact-head evidence * test(ios): reveal offscreen alert fixture controls * test(ios): reset alert between relaunch samples * test(ios): admit native alert snapshots * docs(ios): publish snapshot convergence corpus * chore(ios): format benchmark evidence * fix(ios-benchmark): admit proxy fixture anchors * docs(ios): republish exact-head benchmark corpus * fix(size): make publish asset evidence hermetic * test(size): update publish preparation contracts * fix: keep git-state gates out of mutation sandboxes
2026-09-01 21:39:05 +02:00
"prepare:publish-assets": "node scripts/prepare-publish-assets.mjs",
"build:package": "pnpm build && pnpm build:xcuitest:ios && pnpm build:xcuitest:macos && pnpm build:xcuitest:tvos && pnpm build:xcuitest:visionos && pnpm build:macos-helper:clean && pnpm prepare:publish-assets",
"package:npm": "pnpm build:package && pnpm check:package",
Harden the MCP surface: registry rug-pull fix, operator-only credentials/endpoints, device-shell argv gate, declared timeouts (#2023) * chore(release): keep the version on main distinct from every published version Registry scanners diff the repository's tool surface per version string, so a released number left on main while main keeps changing is indistinguishable from a republished ("rug-pull") version — two scans of the same version see two different tool sets (AS-012). - release:publish now runs release:mark-dev after npm publish, moving package.json and the synchronized server.json to the next patch with a -dev prerelease marker. - release:prepare refuses to publish while the -dev marker is in place, so a forgotten version bump cannot ship a prerelease as latest. - Mark the current tree 0.20.11-dev: main had been sitting on the published 0.20.10 while the tool surface kept changing, which is the live finding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wb8EuoySL26UsdtCtRj5X8 * fix(mcp): remove credential inputs from the model-writable tool surface Every MCP tool advertised daemonAuthToken (and the Metro tools bearerToken) as a free-form string the model writes. The model both reads untrusted app UI text and picks tool arguments, so on-screen text steering it to set a token was a prompt-injection exfiltration path. Credentials are operator-owned: - the keys are omitted from every advertised tool schema (MCP and AI SDK, which share listCommandTools()), - an explicit value is refused with env-var guidance instead of being forwarded (the retired-field posture: refuse, never silently drop), - operator-sourced values are untouched — env/config defaults still merge, and the daemon and Metro clients keep their AGENT_DEVICE_DAEMON_AUTH_TOKEN / AGENT_DEVICE_METRO_BEARER_TOKEN fallbacks. CLI flags are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wb8EuoySL26UsdtCtRj5X8 * fix(mcp): move operator endpoints and paths off the model-writable surface; declare timeouts Follow-up to the credential removal: daemonBaseUrl and the Metro proxyBaseUrl are the endpoints the env-resolved tokens are SENT to, so a model-writable value redirects the operator's token to an arbitrary server — same exfiltration path, one step removed. stateDir, cwd, iosSimulatorDeviceSet, and the three iosXctest* paths select operator infrastructure, never per-call work. All of them leave the advertised MCP/AI-SDK tool schemas and are refused as explicit input with env/config guidance; operator env/config defaults keep flowing exactly as before (config-backed defaults still merge, and explicit input can no longer override them). Dropping these shared properties also cuts tools/list substantially. Every tool description now also declares its enforced client timeout envelope (90s default, 180s install, unbounded only for the streaming test runner), sourced from the descriptor registry's timeout policy so the declared number cannot drift from the enforced one (answers AS-011, which read the undeclared envelope as "no timeout"). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wb8EuoySL26UsdtCtRj5X8 * feat(ci): inventory every dynamic value reaching a device shell adb shell, adb exec-out, and hdc shell join their argv into one string the device's sh evaluates, so any unquoted dynamic element is a potential argv injection — the class of bug the audit found (and fixed) on input text and cmd clipboard set text. Nothing enumerated the surface, so a new call site could regress it silently. scripts/shell-argv is an AST-based gate (oxc-parser, same as di-seams and layering) keeping an exact inventory of every dynamic device-shell argv element, keyed by (file, expression) with counts: 121 values today. A new or grown entry fails CI until the author quotes it through shellQuoteIfNeeded or records it with --update in the same PR, making "a new value now reaches the device shell" a reviewable diff; a stale entry fails the other way so the inventory always matches the code. Wired as the shell-argv gate in the lint lane and registered in the check catalog. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wb8EuoySL26UsdtCtRj5X8 * fix(shell-argv): satisfy the fallow audit without suppressions The Compatibility & Provenance lane's fallow audit flagged the new gate: main was an unused export (only the self-run guard consumed it) and four functions sat over the complexity thresholds. Restructure instead of suppressing: the AST walk dispatches through a composite-child-field table, the argv detection is hoisted out of the visitor, drift reporting moves into helpers, and main is no longer exported. Behavior is unchanged — the model tests pass as written and the regenerated inventory is byte-identical (121 values). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wb8EuoySL26UsdtCtRj5X8 * fix(test): close the port-reuse race in the unreachable-takeover test Coverage (1) failed once in CI with the takeover notice missing while the response still came from the fresh daemon — the exact signature of the fresh fixture being handed the just-freed ephemeral port: the recorded daemon becomes reachable and reusable (same version and signature), so the takeover path is skipped. Bind the fresh fixture before acquiring and freeing the unreachable port; with no bind after the close, the port can never be reclaimed. Line-neutral so the size-ratchet pin holds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wb8EuoySL26UsdtCtRj5X8 * fix(mcp): enforce the advertised tool schema at a real admission boundary P1 (reported by the PR author): hiding operator keys from tools/list did not stop them reaching the command route. The router forwards raw tools/call arguments verbatim and resolveMcpConfigDefaults reads them as CLI flags, so an unadvertised `config`/`remoteConfig` key loaded an arbitrary file whose daemonBaseUrl/daemonAuthToken then flowed to runCommand — a model-writable redirect to an attacker endpoint with the operator's token. Reproduced: {config: <path>} on `snapshot` put both values into the command input. Replace the per-key operator refusal with a deny-by-default admission boundary in the shared executor (the one path both the MCP router and the AI SDK adapter use): every raw input key must appear in the tool's advertised schema, else it is rejected with guidance BEFORE config/env resolution. This closes the config loaders, the operator keys, and any unknown key at once, and makes the advertised additionalProperties:false contract actually enforced. Operator env/config defaults still resolve — they never arrive as tool input. Retired keys (maxSize) are admitted so the command's own reader still answers with migration guidance; they're exposed as metadata.retiredInputKeys for that. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wb8EuoySL26UsdtCtRj5X8 * revert(ci): remove the check:shell-argv inventory gate The PR author correctly flagged that this gate is an inventory, not a security invariant: --update lets any site self-approve a raw value, and the literal-first array heuristic is blind to indirect argv (a variable-built subcommand, or an argv assembled in a helper). Reproduced: adb(['shell', 'input','text',text]) is inventoried, but const s='shell'; adb([s,'input','text',text]) yields no finding. Shipping it security-framed gives false assurance. Remove it. The sound fix — a typed device-shell execution boundary where a raw string cannot reach adb/hdc shell without being quoted or explicitly marked — is a ~188-site cross-package migration on device execution paths, scoped to a dedicated follow-up PR. The two known-dangerous sites (input text, cmd clipboard set text) already quote through shellQuoteIfNeeded on main, so no regression. This keeps the PR focused on the MCP tool surface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wb8EuoySL26UsdtCtRj5X8 --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-08-25 15:19:07 +02:00
"release:prepare": "node scripts/release-mark-dev.mjs --check-release-version && rm -rf .tmp/release && pnpm check:mcp-metadata && pnpm build:package && pnpm check:package -- --pack-destination .tmp/release",
"release:publish": "pnpm release:prepare && npm publish --ignore-scripts .tmp/release/*.tgz && pnpm release:mark-dev",
"release:mark-dev": "node scripts/release-mark-dev.mjs",
2026-02-03 16:10:21 +01:00
"ad": "node bin/agent-device.mjs",
"bench:help-conformance": "node scripts/help-conformance-bench.mjs",
"maestro:conformance": "node --experimental-strip-types scripts/node-test-tmpdir.ts --experimental-strip-types --test scripts/maestro-conformance/format-generated-json.test.mjs packages/maestro/test/conformance/verify.test.ts packages/maestro/test/conformance/differential/engine-process.test.ts packages/maestro/test/conformance/differential/report-output.test.ts packages/maestro/test/conformance/differential/run.test.ts packages/maestro/test/conformance/differential/invariants.test.ts",
"maestro:conformance:regenerate": "node --experimental-strip-types scripts/maestro-conformance/regenerate.mjs",
"maestro:conformance:differential": "node --experimental-strip-types packages/maestro/test/conformance/differential/run.ts",
"test:ios-snapshot-differential": "node --experimental-strip-types scripts/swift-toolchain-tmpdir.ts swift test --package-path apple/snapshot-presentation && node --experimental-strip-types scripts/node-test-tmpdir.ts --experimental-strip-types --test scripts/ios-snapshot-differential.test.ts",
"size": "node scripts/size-report.mjs",
feat: e2e command perf benchmark harness + nightly CI (#630) * feat: add e2e command perf benchmark harness + nightly CI Adds scripts/perf, a cheap end-to-end perf benchmark that drives the built CLI through an ordered Settings tour of ~24 commands for N rounds, on a fully isolated daemon/state-dir and self-cleaning device, and emits JSON + Markdown reports. Per-command timing comes from wrapping each batchable command in its own single-step batch (daemon durationMs) plus wall-clock around the process. Wires a scheduled + workflow_dispatch CI job (perf-nightly.yml) that reuses the cached iOS XCUITest runner (setup-apple-replay) and the Android replay host, and runs the CLI from source via --experimental-strip-types (no dist build). * refactor(perf): drive the harness CLI via runCmdSync, not spawnSync Review (P2): repo rule is to spawn processes through src/utils/exec.ts, not node:child_process directly. Switch the perf harness's invokeCli to runCmdSync (allowFailure so non-zero exits are recorded as samples) and add a maxBuffer option to ExecOptions/runCmdSync (snapshot payloads exceed Node's ~1MB default). * perf(harness): warm the runner after open so the first measured command is clean The first interaction after open/relaunch pays the one-time iOS XCUITest runner startup (~10s+ cold) and a per-relaunch first-AX-query settle cost (~4s). That was landing on the first measured command each round (snapshot -i), inflating it ~10x vs the next snapshot. Run an untimed warmup snapshot -i after establishSession, after each round's reset-open, and after every freshRoot relaunch, so no measured command absorbs runner startup. Noted in the report header. * refactor(perf): address review + fix Fallow CI - exec.ts: extract spawnRejectionError + commandCloseFailure helpers, deduping the error/close handler clones (Fallow duplication ✗ that surfaced once the maxBuffer change pulled exec.ts into the audit scope). - .fallowrc: exclude scripts/perf/** (non-shipped benchmark tooling, like examples/ test-app) so its naturally-moderate functions don't trip the complexity gate. - config.ts: drop unused exports CLI_BIN/DEFAULT_OUT_DIR; add readIntValue so --n/--rounds/--warmup report the actual flag + reject non-integers clearly. - harness.ts: extract toSample(); type sampleError param as CliResult. - scenario.ts: ScenarioStep is now a discriminated union on execMode (removes step.step!/ step.args ?? []). - comment/legend rewords (platform defaults are local-convenience/CI-overridden; elements = node count). check:fallow now green; typecheck/lint/unit pass. * perf(harness): downgrade sample ok when a batch step reports ok:false Defensive belt-and-suspenders for the Codex review note: stop-only batch already surfaces a failed step as a top-level failure (caught by invokeCli), but if an on-error=continue mode ever keeps the batch ok while a step fails, don't silently count that step as a successful sample — derive ok from the step's own result.ok.
2026-05-31 14:37:59 +02:00
"perf": "node --experimental-strip-types scripts/perf/run.ts",
test(ios): establish snapshot convergence baselines and permanent evidence (#2204) * test(ios): add snapshot convergence evidence harness * fix(ios): satisfy benchmark CI guards * fix(ios): constrain benchmark proxy routes * fix(ios-benchmark): enforce cell admission evidence * fix(ios-benchmark): protect benchmark state ownership * fix(ios-benchmark): use proxy port flag * fix(ios-benchmark): let proxy choose an ephemeral port * test(ios-benchmark): keep CLI process seam local * fix(ios-benchmark): parse proxy startup envelope * fix(ios-benchmark): bind proxy lease to simulator * fix(ios-benchmark): keep fresh proxy CLI sessions isolated * fix(ios-benchmark): preserve async timeout evidence * docs(ios-benchmark): retain exact-head evidence * test(ios): reveal offscreen alert fixture controls * test(ios): reset alert between relaunch samples * test(ios): admit native alert snapshots * docs(ios): publish snapshot convergence corpus * chore(ios): format benchmark evidence * fix(ios-benchmark): admit proxy fixture anchors * docs(ios): republish exact-head benchmark corpus * fix(size): make publish asset evidence hermetic * style(size): format package evidence test * test(size): update publish preparation contracts * fix: retire stale utils layering zone * test: pin shared publish asset owner * test: verify preserved size reporter closure * fix: move mutation ownership to snapshot module * test(ios): add snapshot convergence evidence harness * fix(ios): satisfy benchmark CI guards * fix(ios): constrain benchmark proxy routes * fix(ios-benchmark): enforce cell admission evidence * fix(ios-benchmark): protect benchmark state ownership * fix(ios-benchmark): use proxy port flag * fix(ios-benchmark): let proxy choose an ephemeral port * test(ios-benchmark): keep CLI process seam local * fix(ios-benchmark): parse proxy startup envelope * fix(ios-benchmark): bind proxy lease to simulator * fix(ios-benchmark): keep fresh proxy CLI sessions isolated * fix(ios-benchmark): preserve async timeout evidence * docs(ios-benchmark): retain exact-head evidence * test(ios): reveal offscreen alert fixture controls * test(ios): reset alert between relaunch samples * test(ios): admit native alert snapshots * docs(ios): publish snapshot convergence corpus * chore(ios): format benchmark evidence * fix(ios-benchmark): admit proxy fixture anchors * docs(ios): republish exact-head benchmark corpus * fix(size): make publish asset evidence hermetic * test(size): update publish preparation contracts * fix: keep git-state gates out of mutation sandboxes
2026-09-01 21:39:05 +02:00
"bench:ios-snapshot": "node --experimental-strip-types scripts/ios-snapshot-benchmark/run.ts",
"bench:ios-snapshot:deep-button": "node --experimental-strip-types scripts/ios-snapshot-benchmark/deep-button.ts",
test: ratchet mutation score over enumerated decision kernels (#1441) * test: ratchet mutation score over enumerated decision kernels Adds a Stryker (vitest runner) mutation lane scoped to the decision kernels, a per-module baseline with tool/config provenance, and a ratchet that only lets scores rise. Non-gating until two consecutive stable weekly sweeps. Refs #1415 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * chore: declare the mutation test-scope seam for production-export analysis Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test: own kernel tests in the mutation registry and ship the #1430 lane envelope - restore bench:help-conformance, broken by a formatting-path edit - kernel test files select their module on PRs (registry `tests` + workflow paths), asserted to reach the kernel through the import graph - every mutation run writes the standard scheduled-lane artifact envelope - move src/utils/__tests__/errors.test.ts beside its source per the mirror rule Refs #1415, #1430 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test: derive kernel test ownership and land the scheduled-lane health monitor Ownership of a kernel's tests is now computed from the static import graph (scripts/mutation/ownership.ts) instead of a hand-listed set, so a test that reaches a kernel indirectly -- src/__tests__/daemon-error.test.ts through src/daemon.ts -- selects that kernel on a PR. The PR lane triggers on every src test and shards the derived modules, keeping wall clock at one module. The lane envelope (#1430) is now written on every exit path with the stage it reached, so a crash before any mutant runs is distinguishable from a lane that never ran. Adds the derived cadence monitor (scripts/lane-health, daily workflow): scheduled lanes are enumerated from .github/workflows/ and reported dark, failing, or never-run against their own cron cadence. * fix: merge only Stryker reports from a shard directory The shard artifacts now carry the lane envelope beside mutation.json, and the merge globbed every .json under the download path, so the ratchet job fed the envelope to the report parser and died after the mutants had already run. * fix(mutation): fail on incomplete shard sets and envelope pre-run failures Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(mutation): downgrade a passing envelope when a later lane step fails Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor(mutation): shard by registry, defer the PR lane, drop the bundled watcher Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * docs: describe registry sharding and the deferred PR mutation lane Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(mutation): make the pre-graduation tooling exception select real mutants Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(mutation): give the worktree fixture commits their own identity Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Michał Pierzchała <thymikee@gmail.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-28 10:07:14 +02:00
"mutation:run": "node --experimental-strip-types scripts/mutation/run.ts",
"mutation:check": "node --experimental-strip-types scripts/mutation/run.ts --no-run",
"mutation:affected": "node --experimental-strip-types scripts/mutation/run.ts --affected",
fix(test): deterministic temp-dir cleanup across node --test lanes (#1661) * fix(test): deterministic temp-dir cleanup across node --test lanes node --test has no global setup/teardown hook, so unlike Vitest (#1593) every node --test package.json script (maestro:conformance, mutation:test, check:affected:test, check:coverage-changed:test, check:layering, depgraph:test, check:tmpdir-leaks:test, check:contention-retry, test:fixture-cache, test:smoke(:web), test:integration:node, test:concurrency-torture) still created scratch directories against the real, unredirected os.tmpdir(), with cleanup only as reliable as each call site's own try/finally — which a crash, OOM, or timeout kill bypasses entirely. Add scripts/node-test-tmpdir.ts: it wraps the whole `node --test` invocation as a child process, redirecting TMPDIR to one disposable, pid-tagged directory (shared root/prefix with the Vitest lane) and removing it from the process 'exit' event, which fires on normal completion, a thrown error, or a forwarded SIGINT/SIGTERM alike. Every node --test script now runs through it. check-tmpdir-leaks.ts already scans by root/prefix, so it covers both mechanisms with no changes to its detection logic. Verified: a node --test process that mkdtemp's then gets SIGKILL'd leaves a directory behind unwrapped; wrapped and SIGTERM'd, TMPDIR is redirected and the directory is gone with no orphaned processes. All 13 wrapped lanes and the full Vitest suite (5,591 tests) pass with zero residual agent-device-test-run-* directories after the run. Fixes #1595 * test(tmpdir): ratchet every node --test script through the wrapper The 13 lanes wrapped in package.json were a one-time hand sweep with nothing enforcing the pattern going forward — a 14th node --test script added later without scripts/node-test-tmpdir.ts would silently reopen #1595 for that one lane. Add a structural check to scripts/node-test-tmpdir.test.ts (now part of check:tmpdir-leaks:test) that reads package.json and fails if any script invokes `node ... --test` without routing through the wrapper. Dumb string matching over the scripts map, no shell parsing, with an explicit (currently empty) NODE_TEST_WRAPPER_BYPASS_ALLOWLIST for any lane that must legitimately bypass it. Verified it both passes on the current package.json and fails when a synthetic unwrapped `node --test` script is added. * fix(test): preserve the Swift cache and close the raw node --test bypasses Review on #1661 found two gaps: 1. The wrapper only overrode TMPDIR, so it discarded and forced a recompile of the durable Swift compiler cache every run instead of mirroring vitest-tmpdir-global-setup.ts's carve-out for it. Read os.tmpdir() before the child's TMPDIR redirect takes effect and set AGENT_DEVICE_SWIFT_CACHE_DIR from that (only when unset), same as the Vitest lane — the two now share one durable cache instead of each discarding and recompiling their own. Added a probe assertion (scripts/node-test-tmpdir.test.ts) that fails without the fix and passes with it (verified both ways). 2. docs/agents/testing.md documented raw `node --test` commands for the iOS smoke files, and the android/ios/conformance-regenerate/nightly workflows invoked `node --test` directly outside package.json. Routed all of them through scripts/node-test-tmpdir.ts so the documented local commands and CI lanes get the same crash/timeout-safe cleanup the package.json scripts already have.
2026-08-07 13:26:20 +02:00
"mutation:test": "node --experimental-strip-types scripts/node-test-tmpdir.ts --experimental-strip-types --test scripts/mutation/*.test.ts",
"lint": "oxlint . --deny-warnings",
"format": "node ./node_modules/oxfmt/bin/oxfmt --write",
"format:check": "node ./node_modules/oxfmt/bin/oxfmt --check",
test: nightly parser fuzz lane — parser input fails as typed AppErrors, never hangs (#1414) (#1438) * test: nightly parser fuzz lane with typed-AppError invariant (#1414) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(fuzz): run envelope, artifact promotion, and harness self-check tests (#1414) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(fuzz): shared scheduled-lane envelope on every terminal path, watchdog after ready (#1414) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(fuzz): envelope for malformed options; add scheduled-lane health consumer (#1414, #1430) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(lanes): actions:read scope, terminal error envelope, first-due grace (#1414, #1430) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(lanes): anchor first-run grace to schedule registration, use exec helper in tests (#1414, #1430) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(lanes): portable POSIX pickaxe pattern for schedule registration (#1414, #1430) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor(fuzz): fast-check generators over the shared hazard list, drop the bundled lane-health work (#1414) - Strip scripts/scheduled-lane/* and scheduled-lane-health.yml: that watcher is #1430's own deliverable and collides with PR #1439's implementation of the same lane. What this lane owes (a per-run envelope) moves into scripts/fuzz/envelope.ts. - Rebase onto #1437 and rebuild the generator layer on fast-check: cases come from arbitraries sharing SELECTOR_VALUE_HAZARDS with the property suite, and counterexamples are shrunk, so a failure names a minimal input plus fast-check's seed/path instead of a 20k-char random string. - Route harness.test.ts into the serialized subprocess-stub project. - Drop the AGENT_DEVICE_FUZZ_STARTUP_DELAY_MS test seam: the ready handshake is now proven by a case budget far below real worker startup. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(fuzz): replay the regression corpus through the worker watchdog (#1414) A promoted hang case used to wedge the unit job until the CI timeout, because corpus replay called checkCase in-process. It now goes through the same worker-backed watchdog the nightly lane uses, so such a case fails against a 5s per-case budget; the file moves to the serialized subprocess-stub project with the rest of the worker-driven fuzz tests. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(fuzz): let the watchdog outlive vitest's default case timeout (#1414) A wedged parser was surfacing as a bare 'Test timed out in 5000ms' instead of the named hang: failure that says which input wedged, because the file's vitest timeout was shorter than the watchdog budget times the number of replayed cases. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(fuzz): complete drift provenance in the lane envelope (#1414) configHash now covers every input that decides what a seed generates (generate.ts and the shared property arbitraries, not just the arbitraries/targets/invariant), and tool records fast-check's installed version. A generation-loop edit or a fast-check upgrade previously changed the case set while the envelope looked unchanged. A test recomputes the hash with each input omitted so a future omission fails. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Michał Pierzchała <thymikee@gmail.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-28 11:29:25 +02:00
"fuzz:parsers": "node --experimental-strip-types scripts/fuzz/run.ts",
chore(fallow): align local runs with the CI diff gate and upgrade to 2.91 (#735) * chore(fallow): fit config to repo profile so baselines stay near-empty - Raise health thresholds in .fallowrc.json to the smallest values that pass on a clean tree (maxCyclomatic 58, maxCognitive 77, maxCrap 591) instead of grandfathering ~180 findings in fallow-baselines/health.json. - Raise duplicates.minTokens to 66, the smallest value covering the four tolerated clone groups (largest is 65 tokens). - Regenerate baselines: health.json shrinks from ~18.6 KB of grandfathered finding counts to refactoring-target metadata only; dead-code.json is empty. - Upgrade fallow 2.52.0 -> 2.91.0: 2.87.0 made ignorePatterns silence the "examples/test-app is not declared as a workspace" warning, which 2.52.0 emitted regardless of config. - Remove the unused ensureAdb export (and its now-unused imports) from src/platforms/android/adb.ts; it is not re-exported by any public entry and has no references anywhere in the repo. - Document local (pnpm fallow) vs CI (fallow audit) usage in CONTRIBUTING.md. https://claude.ai/code/session_01LXZXzxi55sZ11DSyqWyBA2 * chore(fallow): keep default thresholds, gate locally via diff-based audit Revise the previous commit after review: pinning global thresholds at the repo's historical maxima (cyclomatic 58, cognitive 77, CRAP 591, minTokens 66) weakened the gate for brand-new code and left zero headroom on the worst existing functions. Restore the original design — fallow default thresholds with legacy findings grandfathered per-file in fallow-baselines/health.json — and fix the local-DX problem at the script level instead: - .fallowrc.json: drop the health/duplicates overrides so fallow defaults (cyclomatic 20, cognitive 15, CRAP 30, minTokens 50) apply to new code. - fallow-baselines/health.json: regenerate at default thresholds under fallow 2.91 (201 grandfathered findings across 108 files); dead-code baseline stays empty. - package.json: `pnpm fallow` now runs `fallow audit --base origin/main`, the same diff-based gate CI uses, so it passes on a clean tree. The old full-tree summary moves to `pnpm fallow:all` (expected to report legacy findings). `check:fallow` is unchanged (CI passes an explicit --base). - CONTRIBUTING.md: correct the fallow docs accordingly. Verified: clean tree passes; a new unused export fails the audit; a new cyclomatic-25 function fails the audit; +1 branch growth in an already- grandfathered function (classifyBootFailure) is absorbed by the baseline. https://claude.ai/code/session_01LXZXzxi55sZ11DSyqWyBA2 --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-06-10 10:53:04 +02:00
"fallow:all": "fallow --summary",
"fallow:baseline": "fallow dead-code --save-baseline fallow-baselines/dead-code.json --summary && fallow health --report-only --save-baseline fallow-baselines/health.json --summary",
"check:fallow": "fallow audit",
feat: add derived fail-open check:affected selector (#1195) * feat: add derived fail-open check:affected selector Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor: simplify selector for complexity gate; add docs Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: fail open on ambiguous non-source fixtures; guard catalog against real package.json/vitest.config Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor: use src/utils/exec.ts process helpers in check:affected runner Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(check:affected): SkillGym ownership, honest catalog, working-tree discovery - Add SkillGym ownership for skills/ and test/skillgym/; stop short-circuiting their Markdown as docs-only (findings 2 & 4). - Drop the fabricated GitHub 'SkillGym' job: it is a local-only gate, now localRunnable with no CI job, guarded by a workflow-existence self-test (3). - Fold working-tree (staged/unstaged/untracked) state into local discovery and disable rename detection so both rename paths classify (1). - Add run.test.ts entrypoint regressions (real diff/status/rename discovery, --run order/skip/stop-on-failure). Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(check:affected): union staged + unstaged diffs so they cannot cancel A single `git diff HEAD` nets index against working tree, so a staged add and an unstaged delete of the same file cancel and hide it. Collect `--cached` (staged) and unstaged diffs separately and union them; add a cancellation regression test. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(check:affected): cover required suite gates * refactor(check:affected): delegate tests to vitest --------- Co-authored-by: Michał Pierzchała <thymikee@gmail.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-10 17:53:52 +02:00
"check:affected": "node --experimental-strip-types scripts/check-affected/run.ts",
dx(check): fail fast when node_modules lags the lockfile (#1967) * dx(doctor): flag a worktree whose node_modules lags the lockfile Add a doctor probe and a check:affected preflight that compare node_modules/.pnpm/lock.yaml (the exact lockfile snapshot pnpm installed from) against pnpm-lock.yaml via a content hash — no subprocess. On mismatch both surfaces report the same one-liner: "node_modules was installed from a different lockfile; run pnpm install", so a stale install names its own cause instead of surfacing as bogus format diffs on files a change never touched (the #1956 incident). Closes #1963 * fix(doctor): scope the node-modules probe to a local source checkout The probe ran unconditionally from findProjectRoot(), so it fired in two contexts it cannot diagnose: - Packaged installs. Published packages ship neither pnpm-lock.yaml (not in the package.json `files` allowlist) nor an installed snapshot, so every end user's `doctor` gained a spurious node-modules line and a degraded overall status. - `--remote`, where the daemon's own root describes the server deployment rather than the caller's worktree, so the answer could not address #1963 at all. Whether a root is a source checkout is now decided by the presence of pnpm-lock.yaml itself rather than a heuristic about install location, and 'no-source-checkout' is a distinct result rather than a warning, so the packaged case cannot be represented as a defect. The probe returns undefined there and the route appends no check, matching how doctor already models an out-of-scope question (the device family is likewise absent under --remote). The fresh-worktree catch is preserved: a lockfile with no installed snapshot is still a hard failure. The check:affected preflight is unchanged in behavior. Route-level assertions cover all three contexts (source, packaged, remote); each was verified to fail against the pre-fix wiring. * refactor(check): keep stale-install probe worktree-local
2026-08-22 17:01:04 +02:00
"check:affected:test": "node --experimental-strip-types scripts/node-test-tmpdir.ts --experimental-strip-types --test scripts/check-affected/model.test.ts scripts/check-affected/platform-packages.test.ts scripts/check-affected/device-lanes.test.ts scripts/check-affected/lockfile-install-sync.test.ts scripts/check-affected/run.test.ts",
refactor(ci): make gate ownership structural (#1429) (#1753) * test(ci): prove every registered gate is owned and reachable (#1429) A check that silently stops running looks exactly like a green build. Two suites had already stopped: `check:tmpdir-leaks` (with its model tests) and `test:fixture-cache` are real package scripts that no workflow ran, reachable only through the `check:unit` aggregate CI never invokes. `CHECK_CATALOG` becomes the registry of every check and `pnpm gate <id>` the only way CI runs one, so finding what a lane runs is a scan for `pnpm gate` rather than an attempt to interpret shell. `pnpm check:gate-manifest` then asserts against the real workflows that every registered check is run by some qualifying lane (per unit, not per script name), that every check the real selector activates for a path is run by a lane that path would start (#1420's class), and that every Vitest project and suite script belongs to a check. The wiring that keeps those honest is asserted too: a gate id must name a registered check, an `if:` must be ruled on in GATE_CONDITIONS so `if: false` unowns what it guards, an action declared to run a gate is proven to, and a job whose steps the loader cannot open fails closed. It deliberately does not try to prove CI runs project code only through `pnpm gate`. Whether a shell block executes project code is not decidable from its text, so shell this model does not recognise earns no ownership credit — the failure direction is a check reported unowned, never one waved through. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * test(ci): update the two suites that assert on rewired workflow text `scripts/mutation/workflow.test.ts` and `test/ci/trusted-fixture-artifact.test.mjs` read the workflow and action files and assert on their command text, so routing those steps through `pnpm gate <id>` moved what they were matching. They are the two suites the manifest cannot help with: it proves a gate is still run, not that a test asserting on how CI spells a command was updated with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * fix(ci): credit gates by execution shape, and keep every guard Three ways the manifest could report a gate as owned when it does not run. 1. Crediting was a substring scan over `run:`, which #1429 explicitly rules out — "do not infer reachability from a command name merely appearing in workflow text". `false && pnpm gate x`, a gate inside `if false; then … fi`, one named in a heredoc, and `echo pnpm gate x` all credited it. There is a live instance: conformance-regenerate.yml's "Fail if regeneration changed anything" step names `pnpm gate maestro-regenerate` inside an error message telling a human to run it, and that credited the gate. A gate now counts only as the first command segment of a line, and a body carrying shell structure earns nothing. Reachability inside a script is not decidable, so this does not try: unrecognised shape means no credit and the check reports unowned. `VAR=$(pnpm gate x …)` is read, since the assignment form is unambiguous and the gate runs. 2. Job-level `if:` was not modelled at all, though six live jobs carry one, so a job that cannot run still credited every gate inside it. Two conditions on the mutation lanes are now declared. 3. A caller's `if:` REPLACED the guard on a nested composite-action step (`guard[0] ?? step.condition`), so an outer `always()` erased an inner `if: false`. Steps carry every guard between the lane and the step. Also corrects two source comments that still claimed project code run outside the runner fails the manifest. It does not: such a step earns no credit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * ci: add the run-gate action that names a gate structurally The seam the ownership proof will read instead of shell. A lane says which gate it runs in `with.gate`, a typed input the manifest reads straight out of the YAML and validates against CHECK_CATALOG. Nothing here is wired yet — the ~60 call sites and the model change follow. Added first so the target of that conversion is reviewable on its own. `args` cannot select which gate runs; it is appended after the id, so the worst a wrong value does is fail the gate it already named. There is no `|| true` and no output capture: the gate's exit code is the step's exit code, so a gate cannot run without being able to fail its lane. Part of #1429. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * merge: main (#1770) and route its three new steps through the runner #1770 landed the orphan-check fix on main, wiring `check:tmpdir-leaks`, `check:tmpdir-leaks:test` and `test:fixture-cache` into Coverage, Layering Guard and Integration Tests. This branch had wired the same three through `pnpm gate`, so the merge produced two steps per check rather than a conflict — each check ran twice. Kept main's steps, with the placement and reasoning reviewed on #1770, and changed only their `run:` line to the canonical runner. Dropped this branch's duplicates. Net effect on CI is unchanged: the same three checks, in the same three lanes, once each. Gate manifest green after the merge: 47 checks wired across 33 lanes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * fix(ci): address review — suite detection, freerange, glob, vacuous skip-list Six review findings plus the mutation blocker. [bug] `registered` was shape-only, so a `test:*` script running `node src/bin.ts test <dir>` resolved to a `script:` leaf and was invisible. Four `test:replay:*` scripts were owned only because someone hand-registered them; `test:replay:android` was neither registered nor reported while the nightly ran the same six .ad files by inlining them. A `test:*` script is now a suite by name. `replay-android` is registered, and the nightly runs the script instead of re-listing its files so the two cannot drift. The nightly invokes it inside `reactivecircus/android-emulator-runner`'s `script:` input — shell handed to a third-party action this loader does not read — so the suite executes but cannot be credited. Recorded in UNPROVABLE_OWNERS with that exact reason rather than assumed. The fixed detector also found a second orphan the review did not name: `test:integration:progress`. That one is a reporter whose `--check` sibling is the registered gate, so it is declared in REPORTING_SCRIPTS — a declaration that itself fails when inert. [bug] `freerange` defaulted to localRunnable, so fail-open ran `fr` (a Bun binary) on the pre-push path. Now false. [suggestion] The `--run` skip-list asserted `build:android-snapshot-helper`, a name `android-helpers` no longer uses, so it could not fail. Derived from the catalog instead. [suggestion] `matchesGlob` joined `**` splits with `.*`, making the adjacent slash mandatory — GitHub's `**` matches zero directories, so `src/**/*.test.ts` did not match `src/a.test.ts`. Pinned against `packages/*/src/**/*.test.ts`. [suggestion] Deleted the unwired `run-gate` action. It had no callers, was absent from GATE_ACTIONS, and its comment described a system that had not shipped. It returns with the rewiring, not before. [suggestion] Collapsed the module headers that narrated discarded designs. Mutation: `daemon entrypoint publishes HTTP metadata and cleans up on shutdown` is the only test here that spawns a real daemon process. It takes ~1.1s alone but exceeds Vitest's 5s default inside Stryker's dry run, which aborts the sweep before a single mutant runs. Given 30s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * fix(mutation): order sandbox aliases longest-first so subpaths resolve Every shard of the mutation sweep aborted in Stryker's dry run with: Cannot find package '@agent-device/selectors/engine' imported from .tmp/stryker/sandbox-*/src/core/selector-pipeline.ts The alias was generated correctly; it just never won. Vite matches a STRING alias by prefix and takes the first hit, and `workspaceSpecifierTargets` emitted the bare `@agent-device/selectors` ahead of the subpath entries. The bare entry therefore captured `@agent-device/selectors/engine` and rewrote it to `…/src/index.ts/engine`, which does not exist; Node fell back to real package resolution, could not find the subpath inside the sandbox, and the dry run failed before a single mutant ran — so the shard uploaded an empty envelope instead of a report and the ratchet failed for want of one. Sorting longest specifier first makes the most specific alias win: @agent-device/selectors/engine -> packages/selectors/src/engine.ts @agent-device/selectors/ast -> packages/selectors/src/ast.ts @agent-device/selectors -> packages/selectors/src/index.ts `/ast` never tripped this because nothing in a related test set imported it; `selector-pipeline.ts` introduced the first subpath import that mattered (#1744), so the mutation lane has been unable to run since that landed. Any PR touching `scripts/mutation/**` — which fails open into the full sweep — would have hit it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * refactor: derive gate ownership from workflow structure * fix: run gates without optional arguments * fix: resolve mutation workspace subpaths exactly --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-08-12 16:02:18 +02:00
"gate": "node --experimental-strip-types scripts/gate/run.ts",
"check:gate-manifest": "node --experimental-strip-types scripts/gate/check.ts",
"check:gate-manifest:test": "node --experimental-strip-types scripts/node-test-tmpdir.ts --experimental-strip-types --test scripts/gate/*.test.ts",
"check:coverage-changed": "node --experimental-strip-types scripts/coverage-changed/run.ts",
fix(test): deterministic temp-dir cleanup across node --test lanes (#1661) * fix(test): deterministic temp-dir cleanup across node --test lanes node --test has no global setup/teardown hook, so unlike Vitest (#1593) every node --test package.json script (maestro:conformance, mutation:test, check:affected:test, check:coverage-changed:test, check:layering, depgraph:test, check:tmpdir-leaks:test, check:contention-retry, test:fixture-cache, test:smoke(:web), test:integration:node, test:concurrency-torture) still created scratch directories against the real, unredirected os.tmpdir(), with cleanup only as reliable as each call site's own try/finally — which a crash, OOM, or timeout kill bypasses entirely. Add scripts/node-test-tmpdir.ts: it wraps the whole `node --test` invocation as a child process, redirecting TMPDIR to one disposable, pid-tagged directory (shared root/prefix with the Vitest lane) and removing it from the process 'exit' event, which fires on normal completion, a thrown error, or a forwarded SIGINT/SIGTERM alike. Every node --test script now runs through it. check-tmpdir-leaks.ts already scans by root/prefix, so it covers both mechanisms with no changes to its detection logic. Verified: a node --test process that mkdtemp's then gets SIGKILL'd leaves a directory behind unwrapped; wrapped and SIGTERM'd, TMPDIR is redirected and the directory is gone with no orphaned processes. All 13 wrapped lanes and the full Vitest suite (5,591 tests) pass with zero residual agent-device-test-run-* directories after the run. Fixes #1595 * test(tmpdir): ratchet every node --test script through the wrapper The 13 lanes wrapped in package.json were a one-time hand sweep with nothing enforcing the pattern going forward — a 14th node --test script added later without scripts/node-test-tmpdir.ts would silently reopen #1595 for that one lane. Add a structural check to scripts/node-test-tmpdir.test.ts (now part of check:tmpdir-leaks:test) that reads package.json and fails if any script invokes `node ... --test` without routing through the wrapper. Dumb string matching over the scripts map, no shell parsing, with an explicit (currently empty) NODE_TEST_WRAPPER_BYPASS_ALLOWLIST for any lane that must legitimately bypass it. Verified it both passes on the current package.json and fails when a synthetic unwrapped `node --test` script is added. * fix(test): preserve the Swift cache and close the raw node --test bypasses Review on #1661 found two gaps: 1. The wrapper only overrode TMPDIR, so it discarded and forced a recompile of the durable Swift compiler cache every run instead of mirroring vitest-tmpdir-global-setup.ts's carve-out for it. Read os.tmpdir() before the child's TMPDIR redirect takes effect and set AGENT_DEVICE_SWIFT_CACHE_DIR from that (only when unset), same as the Vitest lane — the two now share one durable cache instead of each discarding and recompiling their own. Added a probe assertion (scripts/node-test-tmpdir.test.ts) that fails without the fix and passes with it (verified both ways). 2. docs/agents/testing.md documented raw `node --test` commands for the iOS smoke files, and the android/ios/conformance-regenerate/nightly workflows invoked `node --test` directly outside package.json. Routed all of them through scripts/node-test-tmpdir.ts so the documented local commands and CI lanes get the same crash/timeout-safe cleanup the package.json scripts already have.
2026-08-07 13:26:20 +02:00
"check:coverage-changed:test": "node --experimental-strip-types scripts/node-test-tmpdir.ts --experimental-strip-types --test scripts/coverage-changed/model.test.ts scripts/coverage-changed/run.test.ts",
fix(layering): stop double-reporting contracts-authority violations (#1746) * fix(layering): stop double-reporting contracts-authority violations main()'s violation list spread checkContractsImplementationAuthority(sources) twice, so every R11 contracts-implementation-authority finding was printed and ::error-annotated twice on a red run — inflating the headline violation count and producing duplicate GitHub annotations on the same file:line. Verified by planting a `setTimeout` call in a contracts production source: the rule reported 2 identical violations before and 1 after, with the extra annotation gone. `pnpm check:layering` stays green (136/136 policy tests). Nothing in the suite covers main()'s assembly of the violation list — the policy tests all call their rule functions directly — so neither a duplicated nor a dropped entry there is currently detectable. * test(layering): hold main() to wiring every rule exactly once The duplicate this branch removed survived because nothing enumerates the guard's rules: main()'s violation list is hand-written, and the per-policy tests call their rule functions directly, never seeing the wiring. A lost spread is the dangerous version of the same gap — the rule stops being enforced and the run still prints OK. Make the file's own bindings the oracle: every in-scope `check*` value, local or imported, must be spread into main()'s violation list exactly once. That covers both directions plus a third case — a policy written and never wired in. Fails closed if main() or the array is renamed, so the instrument cannot pass by finding nothing. Test-only rather than an R17 inside the guard: a self-referential rule is defeated by dropping its own spread, which is exactly the failure it exists to catch. Verified by mutating the real check.ts in both directions (re-planting the duplicate, then dropping checkZeroDepJobs) — each turns the run red, and the restored file is green at 143/143. * test(layering): discover layering suites by glob instead of by hand check:layering named its 14 test files one by one, so adding a policy test meant remembering to register it — and twice nobody did. Both halves of the R16 record cutover shipped with tests that have never run: scripts/layering/record-runtime-mechanics-policy.test.ts (2 tests) scripts/layering/record-runtime-registry-policy.test.ts (1 test) Their policies are live in the guard; only the tests were dormant. All three pass, so nothing had rotted — the coverage was simply never being collected. Glob the directory the way mutation:test already globs its own, which makes the filesystem the enumeration and retires the registration step. 143 -> 146 tests, still green. This is the same defect as the duplicate spread this branch opened with, one level up: a hand-maintained list with nothing checking it against reality. * refactor(layering): register guard rules in a keyed table Replaces the AST wiring guard with a construction that cannot express the defect, per review on #1746. The parser was the wrong instrument: it reconstructed one array's shape from TypeScript syntax, so it only recognised top-level function declarations and imports whose local name matched /^check[A-Z]/. A const-defined or aliased rule was invisible to it, a helper named checkX was a false positive, and naming and syntax became part of the interface — all to detect a mistake rather than prevent it. Rules now live in a keyed table over a shared context, executed once via Object.values. An object cannot hold a key twice, so double registration is unrepresentable rather than merely detected, and oxlint's no-dupe-keys rejects the attempt at the source. LayeringRuleId makes a missing key a type error, and LAYERING_RULE_IDS gives the catalog to check exhaustiveness against. Call sites and order are unchanged, so grouped output and the success line are byte-identical. One regression test remains, through the production interface: scripts/ is outside tsconfig.json's `include`, so the Record's exhaustiveness is an editor signal rather than a CI gate, and the catalog assertion is what fails the build when wiring goes missing. Verified by mutation: dropping an entry and registering an uncatalogued one both fail the test, a duplicated key fails oxlint, and re-planting the original contracts violation reports it exactly once. Net -133 LOC.
2026-08-11 20:21:56 +02:00
"check:layering": "node --experimental-strip-types scripts/node-test-tmpdir.ts --experimental-strip-types --test scripts/layering/*.test.ts && node --experimental-strip-types scripts/layering/check.ts",
fix(ci): make the two rg-based static checks actually run (#2006) * fix(ci): make the two rg-based static checks actually run ripgrep is never installed on ubuntu-latest, so both `rg` assertions in the Lint & Format job failed with "command not found" (exit 127) on every run. `if rg ...; then ... fi` cannot distinguish that from "no matches" (exit 1) — both read as false, so each step silently passed without its assertion ever executing. The DI-seams check had 7 live violations it never reported. Rewrite both against `grep`, which every runner ships, with match/ no-match/error exit codes handled explicitly so a broken scan fails the lane instead of reading as a pass, plus a zero-tracked-files guard so a renamed directory can't quietly go uncovered. The DI-seam pattern also gets narrower to drop two classes of false positive surfaced by actually running it: `typeof fetch` (fetchImpl?/ fetch? seams inject the one global with no module boundary vi.mock can intercept; auth-session.ts/cloud-profile.ts/daemon-proxy.ts exercise the seam directly in their unit tests, while CLI-level tests use vi.stubGlobal('fetch', ...) where the seam isn't reachable — a deliberate, exercised seam) and `typeof SOME_CONSTANT` in SCREAMING_SNAKE_CASE (derives a literal union type from a constant, e.g. interaction-touch-response.ts's dispatchPath field — not an injectable seam at all). Fixes #1976 * fix(ci): replace the DI-seam name-based allowlist with an explicit per-site one Review on PR #2006 (#1976): the previous revision fixed the exit-code handling but decided which `?: typeof X` matches to ban with a regex that exempted matches by the *spelling* of the typeof target (`typeof fetch` always passed, SCREAMING_SNAKE_CASE targets always passed). That's a name-based semantic allowlist, not ownership: a new, genuinely test-only `typeof fetch` seam anywhere in the tree would have silently passed, while an equally legitimate seam under any other name would still fail. Add scripts/di-seams: a small, tested TypeScript checker that judges each match against an explicit, typed, per-site allowlist (scripts/di-seams/approved.ts) keyed by (file, field name, typeof target) rather than by name. A triple is exempt only because it was individually reviewed and named — never because of how it's spelled — and the gate fails just as hard on a stale approval (one whose triple no longer matches anything, e.g. after a rename) as on an unapproved seam, so the list can't silently drift out of sync with the code it describes. Moves the DI-seams step in ci.yml to run after Setup toolchain (it's no longer a toolchain-free text scan); the Swift trailing-comma check stays where it was. * fix(ci): register di-seams as a real gate and route it through the tmpdir wrapper CI caught two things the local (dependency-free) run couldn't: - oxfmt formatting on the two new files. - scripts/node-test-tmpdir.test.ts's repo-wide audit: every package.json script that invokes `node --test` directly must route through scripts/node-test-tmpdir.ts, or a crash/timeout mid-run leaks its scratch TMPDIR. check:di-seams now does. - check:gate-manifest: a package.json script that runs `node --test` must be covered by a registered CHECK_CATALOG gate, or the audit reports the test suite as run by no lane. Registered 'di-seams' in scripts/check-affected/{model,checks}.ts and wired the CI step through run-gate like every other structural guard in this job, instead of invoking pnpm directly. Verified locally with node_modules installed: check:di-seams, check:gate-manifest, check:gate-manifest:test, check:affected:test, check:layering, check:fallow (scoped to the changed files), format, lint, and typecheck all pass. * fix(ci): close the multiline and duplicate-site gaps in the DI-seam scanner Review round 2 on PR #2006 (#1976): - findSeamMatches scanned line by line, so a declaration split across lines (`field?:` on one line, `typeof X` on the next) was invisible. Matching now runs against each file's whole source in one pass — `\s` matches a real newline in JavaScript regexes with no extra flag needed — with the line number derived from the match's character offset. - checkSeams keyed approval by (file, field, target) alone, so once one occurrence of a triple was approved, any further occurrence of that same triple anywhere in the file passed too. The key now includes the line the match starts on, so an approval names one specific declaration, not a recurring pattern. approved.ts expands from 5 collapsed entries to the 7 exact sites this closes down to. Added regression tests planting both gaps directly (a cross-line declaration, and a second unreviewed fetchImpl?: typeof fetch at a different line in an already-approved file) and verified both against the real tree with injected violations, restored cleanly afterward. Re-ran the full local gate suite (di-seams, gate-manifest, layering, fallow, format, lint, typecheck) — all green. * fix(ci): resync approved DI-seam line after merging main Merging main (#2002) removed an unused import above the approved dispatchPath?: typeof MAESTRO_COORDINATE_FALLBACK_PATH declaration in interaction-touch-response.ts, shifting it from line 61 to line 60 — exactly the location-specific-approval staleness the gate is designed to catch, just triggered by an unrelated upstream edit rather than a change in this PR. Updated the approved line to match. * fix(ci): replace the DI-seam positional table with a code-local approval marker Review round 3 on PR #2006 (#1976): CI proved the round-2 fix's core assumption wrong within one push. Keying approval by (file, line, field, target) made a line number the identity — an unrelated edit anywhere earlier in a file shifts every approval below it, and that's exactly what happened: merging main removed an unused import above the approved dispatchPath declaration, and the gate rejected an unchanged, already-reviewed line. Detection is now AST-based (oxc-parser, the same tool scripts/layering/*.ts already uses) instead of a source-text regex: any `{ optional: true, typeAnnotation: TSTypeQuery }` node — a property signature or a bare parameter — is a candidate, which finds a multiline `field?:\n typeof X` declaration for free instead of needing a special case for it. Approval is a `// di-seam-approved: <reason>` comment immediately above the declaration, matching this repo's own `// fallow-ignore-next-line complexity` convention: the marker precedes what it exempts. approved.ts (the external table) is deleted — there is nothing left to keep in sync, since the approval travels with the code it approves. A second, unmarked seam under the same field/target elsewhere still fails; reordering unrelated code around an approved declaration no longer touches it. Added the marker to the 7 real approved sites (fetch-global injection seams in auth-session.ts/cloud-profile.ts/daemon-proxy.ts; the literal-type-derivation false positive in interaction-touch-response.ts) and regression tests proving: a cross-line declaration is still found, a second unmarked occurrence of an approved field/target pair still fails, and an unrelated insertion above an approved declaration no longer breaks it. Verified against the real tree with an injected multi-line unrelated insertion before an approved site — still green. Re-ran the full local gate suite (di-seams, gate-manifest, layering, fallow, format, lint, typecheck, auth-session unit tests) — all green. * fix(ci): reject a di-seam-approved marker with no reason text Review round 4 on PR #2006 (#1976): approvalReason() returned '' (not null) for a bare `// di-seam-approved:` comment with nothing after it, and checkSeams() only filtered out null, so an empty marker silently approved a seam with zero justification — exactly the kind of unreviewed bypass this gate exists to prevent. approvalReason() now returns null when the joined reason text is empty after trimming, so a bare or whitespace-only marker is treated the same as no marker at all. Added tests for both the model-level behavior and the end-to-end checkSeams() result, plus verified against the real tree by injecting a bare-marker declaration and confirming it's flagged, then restored cleanly. --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-08-24 21:31:24 +02:00
"check:di-seams": "node --experimental-strip-types scripts/node-test-tmpdir.ts --experimental-strip-types --test scripts/di-seams/*.test.ts && node --experimental-strip-types scripts/di-seams/check.ts",
feat(scripts): dependency-graph report over the layering gate's model (#1410) * feat(scripts): dependency-graph report over the layering gate's model Reports what the layering gate deliberately does not enforce, as JSON plus a short summary. No renderer: the productive artifact is the JSON. pnpm depgraph # -> .tmp/depgraph/graph.json + summary pnpm depgraph:test Dependency graph: 898 files, 4627 edges, 25 zones value-import cycles (R4): 0 type-only/dynamic cycles (not gate-rejected): 8 spine back-edges (R5): 0 type-only spine inversions (R6): 42 transitively redundant value edges: 1338 The two numbers worth having are the ones CI cannot give you. Transitively redundant value edges — where the target is still reachable at distance >= 2, so the direct import changes nothing about what the module can see — need a real reachability pass, not a grep. And cycle detection over type-only and dynamic edges covers the loops R4 excludes by design. Both are candidate lists, never work lists; at ~1300 the redundancy set is a place to look. It reuses scripts/layering/model.ts, the same module check.ts uses in CI, so the file set, zone partition, edge kinds and cycle definition are the enforced ones. A second extractor would describe a graph nobody gates. Consequence worth having: its R6 count reproduces TYPE_INVERSION_BASELINE, so a mismatch means one of the two is stale. This is the analysis half of a viewer that was built and dropped. The render cost ~2200 lines and needed a Fallow exemption for a 920-line canvas file, and nobody read it. Everything here clears the repo's bar with NO exemption — scripts/depgraph is deliberately absent from ignorePatterns, unlike scripts/layering, scripts/perf and scripts/maestro-conformance. Getting there meant fixing rather than suppressing: extracted `valueSuccessors` (the value-edge adjacency was built identically in two places — a real clone), split `buildGraph` into four named aggregation steps, split `reachableBeyondDirectEdge` out of `markRedundantEdges`, extracted `compareZoneEdges`/`crossedZonePair`, extracted `edgeKindCode`/`edgeFlags` from a nested ternary scoring CRAP 42, and deleted `fileGroup` plus the `group` node field once the cluster layout went. Two additive exports on scripts/layering/model.ts: `zoneRank` and `targetDagZone` (previously module-private). The gate's behaviour is unchanged. `pnpm check` green, 4488 unit tests, 5 model tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bfu8HofkhybiAm5LECfqur * ci(layering): assert the depgraph report reproduces the gate's baseline The report reads the same model as the gate, so its inversion count must equal TYPE_INVERSION_BASELINE. That agreement was previously a nice property nobody checked; the Layering Guard job now runs scripts/depgraph/model.test.ts, so the two cannot be green independently. Verified by bumping a baseline entry by one and confirming the job fails with a message naming the fix. The count feeding the check is computed by `typeInversionsByPair`, which applies the gate's rule — once per FILE pair, over the raw resolved edges — rather than reading the collapsed edge list. That matters: `collapseEdges` keeps one edge per pair with the strongest kind winning, and `dynamic` outranks `type`, so a module imported both lazily and for its types would collapse to `dynamic` and drop out of the count. No such pair exists today (measured: 0 of 42 inverting pairs), but a number wired into a CI equality check must not be able to drift for a reason unrelated to layering. Stated honestly in the README and the test: this is a cross-check of the report's extraction and the baseline against the real tree, not two independent algorithms. The gate remains the authority — if they disagree, the baseline or the tree is wrong, never the test. TYPE_INVERSION_BASELINE is now exported for this purpose. Not done here, deliberately: the ~1338 transitively redundant value edges are a candidate for a loose growth-only ratchet later. They are a candidate list, not a work list, and a hard count would be noise. `pnpm check` green, 4488 unit tests, 6 depgraph model tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bfu8HofkhybiAm5LECfqur * fix(depgraph): make the source reviewable, and stop overclaiming removability Four review findings, all of them real. P1 - the implementation was binary. scripts/depgraph/model.ts contained two raw NUL bytes used as map-key delimiters, so Git classified a ~346-line file as binary and hid its entire diff behind `- -`. Replaced with a unicode escape: identical at runtime, textual on disk. I had seen the symptom repeatedly - every grep on that file printed "binary file matches" - and worked around it with python instead of asking why, which is how it survived to review. Guarded repo-wide rather than for this one file: a new test asserts no tracked .ts under src/ or scripts/ contains a raw NUL, verified by reintroducing one and watching it fail. Nothing else would catch a recurrence, and the failure mode is silent - the code works, the review does not. P1 - "transitively redundant" claimed removability it cannot support. Module reachability does not carry bindings: if `a` imports `{ c }` while `b` only re-exports it as `{ c as b }`, the path a -> b -> c exists and deleting a -> c still breaks `a`. The fixture in model.test.ts is exactly that shape and its comment said "removable". Reachability also says nothing about when a module's side effects run. Renamed throughout to what it measures - `transitivelyReachable`, `markTransitivelyReachableEdges`, and a summary line reading "value edges whose target is also reachable at distance >= 2 (reachability only - not a removability claim)". The caveats and the counterexample are now stated in the marker function, the fixture comment and the README, and symbol-level analysis is named as what deciding any individual edge would actually require. P2 - build.ts had no coverage. Every test exercised model.ts, so the CLI could break its output path, wire shape or summary silently. Added three subprocess tests: default path plus summary-agrees-with-payload, `--out` honoured and valid JSON written, and a trailing `--out` falling back rather than crashing (pinned so it is a decision, not an accident). `pnpm depgraph:test` now runs inside `check:tooling`, so `pnpm check` covers it. P2 - README was wrong three ways: it queried `.tmp/depgraph/index.json` after the output moved to `graph.json` (the documented command failed as written), it derived inversions from collapsed `zoneEdges`, which can undercount, and it claimed both that the report runs in CI and that nothing here runs in CI. The query now reads `typeInversions` and was run verbatim; the CI sentence names exactly which single test runs and states that nothing else gates a merge. pnpm check green, 4488 unit tests, 10 depgraph tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bfu8HofkhybiAm5LECfqur --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-07-27 13:59:53 +02:00
"depgraph": "node --experimental-strip-types scripts/depgraph/build.ts",
fix(test): deterministic temp-dir cleanup across node --test lanes (#1661) * fix(test): deterministic temp-dir cleanup across node --test lanes node --test has no global setup/teardown hook, so unlike Vitest (#1593) every node --test package.json script (maestro:conformance, mutation:test, check:affected:test, check:coverage-changed:test, check:layering, depgraph:test, check:tmpdir-leaks:test, check:contention-retry, test:fixture-cache, test:smoke(:web), test:integration:node, test:concurrency-torture) still created scratch directories against the real, unredirected os.tmpdir(), with cleanup only as reliable as each call site's own try/finally — which a crash, OOM, or timeout kill bypasses entirely. Add scripts/node-test-tmpdir.ts: it wraps the whole `node --test` invocation as a child process, redirecting TMPDIR to one disposable, pid-tagged directory (shared root/prefix with the Vitest lane) and removing it from the process 'exit' event, which fires on normal completion, a thrown error, or a forwarded SIGINT/SIGTERM alike. Every node --test script now runs through it. check-tmpdir-leaks.ts already scans by root/prefix, so it covers both mechanisms with no changes to its detection logic. Verified: a node --test process that mkdtemp's then gets SIGKILL'd leaves a directory behind unwrapped; wrapped and SIGTERM'd, TMPDIR is redirected and the directory is gone with no orphaned processes. All 13 wrapped lanes and the full Vitest suite (5,591 tests) pass with zero residual agent-device-test-run-* directories after the run. Fixes #1595 * test(tmpdir): ratchet every node --test script through the wrapper The 13 lanes wrapped in package.json were a one-time hand sweep with nothing enforcing the pattern going forward — a 14th node --test script added later without scripts/node-test-tmpdir.ts would silently reopen #1595 for that one lane. Add a structural check to scripts/node-test-tmpdir.test.ts (now part of check:tmpdir-leaks:test) that reads package.json and fails if any script invokes `node ... --test` without routing through the wrapper. Dumb string matching over the scripts map, no shell parsing, with an explicit (currently empty) NODE_TEST_WRAPPER_BYPASS_ALLOWLIST for any lane that must legitimately bypass it. Verified it both passes on the current package.json and fails when a synthetic unwrapped `node --test` script is added. * fix(test): preserve the Swift cache and close the raw node --test bypasses Review on #1661 found two gaps: 1. The wrapper only overrode TMPDIR, so it discarded and forced a recompile of the durable Swift compiler cache every run instead of mirroring vitest-tmpdir-global-setup.ts's carve-out for it. Read os.tmpdir() before the child's TMPDIR redirect takes effect and set AGENT_DEVICE_SWIFT_CACHE_DIR from that (only when unset), same as the Vitest lane — the two now share one durable cache instead of each discarding and recompiling their own. Added a probe assertion (scripts/node-test-tmpdir.test.ts) that fails without the fix and passes with it (verified both ways). 2. docs/agents/testing.md documented raw `node --test` commands for the iOS smoke files, and the android/ios/conformance-regenerate/nightly workflows invoked `node --test` directly outside package.json. Routed all of them through scripts/node-test-tmpdir.ts so the documented local commands and CI lanes get the same crash/timeout-safe cleanup the package.json scripts already have.
2026-08-07 13:26:20 +02:00
"depgraph:test": "node --experimental-strip-types scripts/node-test-tmpdir.ts --experimental-strip-types --test scripts/depgraph/model.test.ts scripts/depgraph/affected.test.ts",
chore: baseline-free production-exports cleanup (#1276) (#1282) * chore: baseline-free production-exports cleanup (#1276) Classify and burn down the 32 baseline-tolerated unused production exports. - Live seams: annotate with @internal JSDoc visibility tags (test hooks, introspection helpers, public install-source constant) so fallow no longer treats them as dead production exports. - Wrappers: collapse re-export wrappers in commands/index.ts (ref/selector) and daemon/lease-context.ts (buildLeaseDiagnosticsContext); update all importers to pull directly from the source module. - Stale baseline entry: remove the non-existent resetAndroidMultiTouchHelperInstallCache entry. - Empty fallow-baselines/production-unused-exports.json so check:production-exports now fails loudly on any new dead export. Fixes #1276 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * chore: address review feedback on production-exports cleanup (#1276) - CONTRIBUTING.md: document that intentional non-production exports should use JSDoc @internal with a short justification, treated as a reviewed baseline entry. - isPlatform: fix JSDoc tag to "@internal" and remove conflicting "public" wording. - ARCHIVE_EXTENSIONS: re-export from src/sdk/install-source.ts so the public install-source subpath has a real consumer story for the constant. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * chore: make production-exports check truly baseline-free (#1276) - Drop --baseline from pnpm check:production-exports and remove the check:production-exports:baseline generation script. - Delete fallow-baselines/production-unused-exports.json. - Update CONTRIBUTING.md to describe the baseline-free behavior and remove references to reviewed baseline entries for production unused exports. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Michał Pierzchała <thymikee@gmail.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-16 11:44:07 +02:00
"check:production-exports": "fallow dead-code --config fallow-production-exports.json --production --unused-exports --fail-on-issues",
refactor: keep command and daemon-route owner-file claims tooling-only (#1178) (#1192) * refactor(command-descriptor): keep owner-file claims tooling-only Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor(daemon): keep daemon-route owner-file claims tooling-only Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(daemon): guard against re-adding owner-file paths to the production route chain Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor(command-descriptor): derive owner-file projection from colocated RAW_COMMAND_DESCRIPTORS - Keep ownerFiles on each RAW_COMMAND_DESCRIPTORS entry as the source of truth. - Add tooling-only __OWNER_FILES__ build flag so production bundles omit the ownerFiles properties entirely. - Derive COMMAND_OWNER_FILES from RAW_COMMAND_DESCRIPTORS instead of a hand-maintained parallel table. - Guard command-explain tests against leaking ownerFiles into production descriptor objects. - Enable treeshake.propertyReadSideEffects: false in tsdown to help drop the dead ownerFiles branch from production bundles. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * chore: apply oxfmt formatting Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(build): guard tooling metadata exclusion Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor(command-descriptor): drop global treeshake option and add bundle guard - Remove treeshake.propertyReadSideEffects from tsdown.config.ts; the __OWNER_FILES__ define + conditional spread already keeps owner files out of the bundle, so the global DCE lever is unnecessary and scope-creeping. - Add a comment on the __OWNER_FILES__ global declaration explaining the deliberate type-versus-runtime mismatch. - Add test/output-economy/owner-files-no-leak.test.ts to build dist and assert that no command or daemon-route owner-file path appears in the emitted JS. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(build): remove owner metadata property reads Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(command-descriptor): enforce owner claim totality Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Michał Pierzchała <thymikee@gmail.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-11 07:46:00 +02:00
"check:bundle-owner-files": "node --experimental-strip-types scripts/check-bundle-owner-files.ts",
build: verify the published tarball instead of grepping the bundle (#1578) * build: verify the published tarball instead of grepping the bundle Replaces the bundle-dependency grep with one gate that packs the tarball npm would publish and proves it sound from a clean consumer install: publint and attw on the tarball, a two-way dependency-closure audit, an import of every `exports` subpath, and the CLI smoke run — all from outside the workspace, where no pnpm link can mask an unresolvable specifier. Also stops the build from emitting a publishable bundle in the first place: a missing workspace link now fails `pnpm build` instead of warning and exiting 0, which is how 0.20.4 shipped an unresolvable `@agent-device/ad-script` import. publint found 12 real defects in the current package — every `exports` entry listed `types` after `import`, so TypeScript resolved declarations by accident rather than by condition. The dependency audit found `pngjs` declared as a runtime dependency while tsdown inlines it, an install every user paid for and no shipped code reached; it moves to devDependencies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(ci): run the package gate without pnpm on the Node floor pnpm 11.17 requires Node >= 22.13, so `pnpm check:package` could not start on the 22.12 floor the Packaged CLI job exists to cover. The gate needs only `node` and `npm`, so the job invokes the script directly. Splits the dependency-closure audit into a collector and a message builder to clear Fallow's complexity threshold, and classifies both packaging linters in ignoreDependencies: they are subprocess CLIs with no importable API here, which dependency analysis cannot follow to an import. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(publishing): read every literal resolution form in the closure audit The dependency-closure audit derived shipped imports from the ESM module record alone, so it could not see a package resolved through `require` or a `createRequire` result: neither produces a module-record entry. A lazy `createRequire('@agent-device/…')` would therefore clear the audit, the all-export probe and the exercised CLI paths, reintroducing the 0.20.4 published-install failure class for another command. Measuring the built bundle turned up a second, larger hole in the same reader. The shipped files are minified, and the minifier rewrites every string literal to a no-substitution template literal, so the dynamic-import extraction — which accepted quoted strings only — matched 0 of the 99 dynamic imports the bundle contains. The lazy `import()` path that broke 0.20.4 was reported as covered while checking nothing. Specifiers now come from the module record plus an AST walk over every literal runtime-resolution form: `import()`, `require()`, `require.resolve()`, an immediately-invoked `createRequire(...)`, and calls through a `createRequire` result under any import or minified alias. Both spellings of a string literal count everywhere, and `.cjs` joins the scanned extensions. Computed specifiers stay explicitly out of scope, and are pinned as such. Rejecting them is not available: minifiers reuse short identifiers across scopes, and the packed bundle really does contain an unrelated `a(h[t],f,g,l,e,m)` that no name-based match can distinguish from a require call. Those are covered by the gate's runtime half instead, which resolves them for real. Bare-identifier calls need the one-string-argument shape for the same reason. The audit moves to scripts/lib/shipped-imports.ts so fixture packages can exercise it. The gate needs a real `npm pack` behind minutes of Swift and Android builds, so every check that runs it can only watch a healthy package pass — which is how a reader that matched nothing looked covered. The new fixtures assert the failure direction per resolution form: 16 of the 22 fail against the previous reader, and the 6 that pass are the quoted-spelling and pinned-limitation cases. A wiring assertion keeps the audit and both runtime probes attached to the gate, since fixtures alone would stay green if the call were deleted. Verified against the real built bundle: the closure resolves to exactly the two declared dependencies, so the stricter reader adds no false positives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 12:28:05 +02:00
"check:package": "node --experimental-strip-types scripts/check-package.ts",
"check:command-docs": "vitest run --project unit-core src/__tests__/command-doc-coverage.test.ts",
"check:agent-guidance": "node --experimental-strip-types scripts/node-test-tmpdir.ts --experimental-strip-types --test scripts/__tests__/agent-guidance-contract.test.ts",
test: frozen replay-compat corpus with expected verdicts (#1417) (#1436) * test: frozen replay-compat corpus with expected verdicts (#1417) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test: pin replay-compat corpus bytes to released blobs and assert via parseReplayInput Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test: lock replay-compat provenance kind by corpus area and verify it in CI Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * docs: describe corpus provenance-kind lock and CI job Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test: prune replay-compat corpus to minimal witnesses per shipped form Reviewer feedback on #1436: the mechanism earns its place, the dataset did not. Drop the 30 corpus entries whose bytes repeat a syntactic form or a migration refusal another entry already witnesses (platform twins and adjacent-release re-recordings), leaving 22 deliberate entries; make note required and state per entry which form or refusal it is the sole witness of. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test: address corpus review nits (typed coverage list, cap rationale, derived-citation note) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * docs: split corpus rule — form from the release, verdict from today's parser Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * docs: format corpus README emphasis markers Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Michał Pierzchała <thymikee@gmail.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-28 10:06:00 +02:00
"check:replay-compat": "node --experimental-strip-types scripts/check-replay-compat-provenance.ts",
test: add daemon RPC wire-surface compatibility gate (#1717) * test: gate daemon RPC wire compatibility against the last released tag (#1432) ADR 0006 fixes exactly when DAEMON_RPC_PROTOCOL_VERSION must be bumped, and nothing checked that it was. The runtime guard (readRemoteDaemonHealth) refuses a mismatched peer, but only fires when someone remembered the bump — a wire change that skipped it left both sides advertising protocol 2 while parsing different payloads, which is the failure ADR 0006 exists to prevent. Local daemons cannot skew (isReusableDaemonInfo takes over on any package version mismatch). Cross-machine is skewed by design — proxy, cloud/limrun, a remote macOS host — and ADR 0006 explicitly rules package version out as the compatibility gate there, so the one boundary where skew is intended was the one boundary with no gate. test/wire-compat/surface.ts declares the wire surface grouped by the ADR bullet each group serves, quoting it, with an `uncovered` note where a bullet is only partly digestible (the /health and /rpc literals inside http-server.ts stay reviewer-owned: a moved route 404s at connect time rather than misparsing). ledger.json records what each declaration hashes to, at which protocol version. Two gates, split for the same reason the replay-compat corpus splits: - unit-core holds the ledger to its source and prints the digest to paste; - Released-Surface Compatibility reads the ledger at the last RELEASED tag and requires the drift since then to carry a bump or a compatibleChanges ack. From one commit a bumped ledger and an unbumped one are both just an edited file, so only a released baseline can tell them apart. Acks are keyed by the digest they cover, so one "added an optional field" cannot launder later changes. Digests ignore comments and formatting; the manifest's closure is derived from the AST, so a field typed by an unlisted sibling fails rather than sitting outside the gate. CI cost: one added job (checkout + toolchain + two node scripts, ~1 min), mirroring the existing full-history replay-compat job. * test: close wire-surface overclaim and make the closure fail closed (#1432) Addresses both review P1s on #1717. P1 — the manifest materially overclaimed ADR 0006 coverage. It quoted all four bullets while digesting only the payload TYPES, so the producer and consumer seams could break a skewed peer without moving a listed digest. Now listed on both sides of every boundary: JSON-RPC method sets and the projections that turn each method's params into a DaemonRequest, createRpcError/sendJson/ writeRpcResponseEnvelope, resolveToken and the auth-hook types, upload preflight/finalize/308 handlers and the resumable ticket shape, artifact route and download/inventory framing, REST error mapping, and the client's own payload builder, lease-method mapping, response parser and error projection. 57 -> 117 declarations. What stays out is now named rather than implied: createDaemonHttpServer's dispatch wiring and the /health and /rpc literals inside it. Everything it dispatches WITH is digested individually, and a moved route 404s at connect time rather than misparsing — the loud failure, not the silent one. P1 — imported and re-exported payload shapes escaped the closure. declarationHomes() scanned only the manifest's own files and the walk continued silently when a name could not be placed, so a listed type could gain foo?: ImportedShape from a new module and stay green. Resolution is now explicit and fails closed: relative imports, workspace specifiers (through the owning package's own exports map, so a re-pointed export cannot drop a type), and facade re-export chains. Every referenced name must land on a listed declaration, a waiver with a written reason, a declared external module, or the TS/Node global set. Fixed two extractor blind spots the walk exposed: a declaration's own generic parameters and `as const` were being reported as references. Planted-red proofs (wire-mutations.test.ts): 13 cases independently mutate method naming, response serialization, response parsing, auth projection, upload ticket shape, 308 framing, artifact framing, REST error mapping, and progress framing, each asserting the digest moves; 3 probes prove the closure really reaches across a package boundary, a facade re-export, and a plain relative import. Mutations apply inside the declaration's own span — a whole-file replace silently hit a sibling sharing the substring, which is how the first draft of one case passed vacuously. The largest waiver pair (InternalRequestOptions, CommandFlags) rests on ADR 0006's own additive rule: they reach the peer inside DaemonRequest's untyped flags/input bags, and the decision says a new flag needs no bump. Digesting them would fire the gate on every new CLI flag and train reviewers to rubber-stamp acks. * test: list the consumer half of the auxiliary HTTP boundaries (#1432) Addresses the remaining review P1 on #1717. The manifest claimed both sides of response/upload/artifact framing while listing nothing from upload-client.ts, daemon-artifacts.ts, or the health consumer in daemon-client-transport.ts, so those parsers could narrow without moving a listed digest or protocol 2. Now listed (117 -> 141 declarations): - /health consumer: RemoteDaemonHealth, readHealthPayload, readDaemonHttpHealth, readRemoteDaemonHealth. This is the sharpest of the three — narrowing the reader or the comparison disables the very refusal ADR 0006 exists to guarantee, and nothing else in the repo would notice. - /upload consumer: UploadResponse, UploadPreflightResponse, UploadPreflightResult, parseUploadPreflightResult, requestUploadPreflight, uploadDirectArtifact, tryDirectUploadWithResume, shouldRetryDirectUpload, finalizeDirectUpload, uploadLegacyArtifact, ARTIFACT_HASH_ALGORITHM, isStringRecord, and PreparedUploadArtifact — whose sha256/sizeBytes/fileName/artifactType/ contentType fields ARE the preflight body the daemon parses. - /artifacts/* consumer: DaemonArtifactEndpoint, buildDaemonArtifactUrl, isRemoteDaemon, DownloadRemoteArtifactParams, downloadRemoteArtifact, materializeRemoteArtifacts, resolveMaterializedArtifactPath. Running the closure fail-closed over the new files surfaced three more stops, each decided rather than skipped: PreparedUploadArtifact listed (it is payload), UploadProgressSink waived (client-local rendering, never leaves the process), and src/daemon/types.ts#DaemonArtifact waived as a re-export alias of the listed kernel type, matching its DaemonRequest/DaemonResponse siblings. 10 more planted-red mutations cover the new seams: health version-read and mismatch-refusal defeated, RemoteDaemonHealth field dropped, preflight parser narrowed, preflight/legacy response shapes narrowed, finalize body key renamed, ticket field renamed, artifact tenant header dropped, artifact URL moved. A fourth closure probe proves the upload-consumer files are genuinely reached by the walk rather than merely listed. 22 -> 33 tests. The README now states the coverage as a producer/consumer table per boundary, so the claim is checkable at a glance instead of asserted in prose. * test: list the client half of the resumable 308 contract (#1432) Addresses the third review P1 on #1717. Listing the daemon's handleResumableUpload proved it still PRODUCES 308; nothing proved the client still CONSUMES the released one. src/remote/upload-stream.ts owns that half and was entirely outside the manifest, so a newer client could stop accepting `upload-offset`, change how it reads `Range: bytes=0-N`, or emit a different resumed `Content-Range` without moving one of the 141 listed digests. Now listed (141 -> 151): UploadStreamResponse, streamFileToHttpRequest, streamFileToHttpRequestAttempt, buildUploadRequestHeaders, isUploadResumeStatus, isUploadRedirectStatus, parseUploadResumeOffset, parseNonNegativeIntegerHeader, firstHeaderValue, MAX_UPLOAD_REDIRECTS. streamFileToHttpRequestAttempt is listed despite its size, unlike createDaemonHttpServer which stays in `uncovered`. The distinction is stated at the declaration: the HTTP server only dispatches to handlers that are each digested, while the attempt loop IS the resume state machine — it decides whether a 308 continues the upload and what the next request carries, so its sequencing alone can break a released daemon while every helper keeps its digest. 6 new planted-red mutations prove the client half moves the ledger: a dropped `upload-offset` fallback, narrowed Range parsing, a changed resumed Content-Range, 308 no longer treated as continue, a narrowed UploadStreamResponse, and dropped header-value coercion. 33 -> 39 tests. Closure fail-closed surfaced two more stops: UploadStreamProgressOptions waived (local byte-progress rendering) and URL/URLSearchParams added to the global set. README now carries a `/upload` resume row in the producer/consumer table, and names the pattern behind three rounds of review: the coverage sentence kept getting written ahead of the coverage, so the table and the `uncovered` notes are the claims to trust — they are checkable against surface.ts, prose is not. --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-08-10 20:52:29 +02:00
"check:daemon-wire-compat": "node --experimental-strip-types scripts/wire-compat/run.ts",
"check:daemon-wire-compat:test": "node --experimental-strip-types scripts/node-test-tmpdir.ts --experimental-strip-types --test scripts/wire-compat/model.test.ts",
"check:xctest-selection": "node --experimental-strip-types scripts/check-xctest-selection.ts",
fix: stop the unit suite from leaking temp directories (#1593) * fix: stop the unit suite from leaking temp directories ~650 test call sites across the unit suite create scratch directories via fs.mkdtemp(path.join(os.tmpdir(), ...)) or shared factories (makeSessionStore) with no cleanup, ever. Over time this accumulated 1.16M+ orphaned directories in the real system tmpdir, slow enough to make tools that enumerate $TMPDIR at startup (e.g. opencode) take 1-2 minutes to launch. Rather than migrate every call site, redirect os.tmpdir() itself for the lifetime of the whole `vitest run` invocation: scripts/vitest-tmpdir-global-setup.ts wires in as vitest's globalSetup/globalTeardown, points TMPDIR at one /tmp-rooted directory (verified: env mutations here propagate to every forked worker, confirmed empirically), and removes it in one recursive rm after every worker across every project finishes. Since os.tmpdir() reads TMPDIR on every call, this covers all ~650 call sites without touching any of them. Rooted at /tmp rather than nested inside the current (already deep, on macOS) os.tmpdir(): that broke real AF_UNIX socket tests (runner-usbmux.test.ts) by pushing socket paths past the 104-byte sun_path limit. A per-file afterAll hook was tried first but proved unreliable — 5 of 7 workers in one run never ran it before their process was torn down; the global setup/teardown pair (one process, confirmed single execution) is the mechanism that's actually guaranteed to run once. Also adds: - scripts/check-tmpdir-leaks.ts: CI/local guard asserting no agent-device-test-run-* directory survives a run (a leftover one means a worker was killed before cleanup could run). - src/__tests__/test-utils/tmp-dir.ts: documented mkdtempForTest / mkdtempForTestSync helpers, the discoverable way to get a scratch dir going forward (mirrors the src/utils/exec.ts pattern for node:child_process). - scripts/check-test-tmpdir-helper.ts: ratchet guard capping raw fs.mkdtemp/mkdtempSync call sites in test files at today's count (632); it can only shrink as call sites migrate to the helper. * fix: make check-tmpdir-leaks scan the same root the fix actually uses check-tmpdir-leaks.ts was scanning os.tmpdir() for leftover run directories, but vitest-tmpdir-global-setup.ts creates them under a hard-coded /tmp. On macOS those are different paths (TMPDIR is a deep per-user /var/folders/.../T/ directory) — the guard could never find a leak on the exact platform the original leak happened on, only on Linux CI where os.tmpdir() already is /tmp. Export TEST_RUN_TMP_ROOT and TEST_RUN_TMP_PREFIX from the global-setup module and import them in the leak check instead of recomputing a path that can drift. Switched from a fixed pid-based directory name to fs.mkdtempSync so a same-named leftover from a prior killed run (or, on a shared machine, another user) can't collide with a live run. Also fixes two stale comments (in this file and ci.yml) that still described the per-file afterAll hook design that was abandoned in favor of the global setup/teardown pair, and notes the check only covers vitest runs, not the node --test lanes (test:smoke, test:integration:node). Verified live: with the old code the guard reported no leaks even with a real orphaned /tmp/agent-device-test-run-* directory present (left by a command that got killed mid-run); with this fix it correctly found and reported it. * simplify: drop the tmpdir ratchet guard, keep the leak check local-only Two guards were more than this needed: - check:test-tmpdir-helper (ratchet on raw fs.mkdtemp call counts) protects nothing a bug could actually trigger — the leak is already fixed architecturally regardless of call-site count, so this was pure style/discoverability nudging. Dropped the script and its check:tooling/CI wiring; kept mkdtempForTest/mkdtempForTestSync in tmp-dir.ts as the documented option without enforcing it. - check:tmpdir-leaks in CI added little: GitHub-hosted runners are destroyed after each job, so a leftover directory there is harmless by construction, and a worker getting killed mid-run would already surface as a job failure some other way. Its real value is local, on the long-lived dev machines where the original leak actually accumulated — kept it wired into check:unit, dropped the CI step. * fix: don't flag a concurrent vitest run's tmpdir as a leak check-tmpdir-leaks.ts reported every agent-device-test-run-* directory as a leak, but a concurrent vitest run in another worktree legitimately keeps its own directory present until its own teardown finishes. On a machine that regularly runs several worktrees at once, that made check:unit fail on unrelated in-progress work. Embed the owning process's pid in the directory name (still random- suffixed via mkdtempSync, so same-pid reuse across separate runs can't collide) and have the leak check skip any directory whose pid is still alive (process.kill(pid, 0)) — only directories whose owning process already exited without running its globalTeardown are real leaks. Split the pure logic into check-tmpdir-leaks-model.ts (findLeakedRunDirectories, with an injectable liveness check for testing) so it has a real regression suite, including the concurrent-run case, instead of only being exercised by hand. * refactor: migrate raw fs.mkdtemp call sites to mkdtempForTest(Sync) Migrates 629 raw fs.mkdtemp(Sync)(path.join(os.tmpdir(), PREFIX)) call sites across 168 test files to the mkdtempForTest / mkdtempForTestSync helpers (src/__tests__/test-utils/tmp-dir.ts), so there's one documented, discoverable way to get a scratch dir in a test — cleanup already didn't depend on the call-site shape (the global TMPDIR redirect covers any of them), this is purely for consistency and discoverability, same reasoning as src/utils/exec.ts for node:child_process. Existing manual per-test cleanup (fs.rm in finally/afterEach/onTestFinished blocks) is untouched — the global teardown is a fallback for killed workers, not a replacement for tests cleaning up after themselves. Migrated with a one-off AST-based codemod (oxc-parser, since regex mismatched multi-line calls and complex prefix expressions like `options?.tempPrefix ?? 'default-'`) rather than by hand across 168 files. The codemod isn't included — it doesn't need to survive this commit. Caught and fixed one real bug in it during review: a small number of files declare a second import statement later in the file, after some of the matched call sites, which broke a naive "insert after the textually-last ImportDeclaration" placement; fixed to insert after the top contiguous import block instead, plus a self-check that re-parses every generated file before writing it. Also fixes 5 fallow dead-code findings the branch introduced: the vitest globalSetup functions (setup/teardown) are only referenced by the config-string path vitest.config.ts hands to globalSetup, invisible to static analysis — suppressed with the documented convention. isProcessAlive didn't need to be exported (nothing outside the module uses it). And dropped a barrel re-export of the new helpers from test-utils/index.ts: nothing actually imports through the barrel (matching the existing makeSessionStore convention, which is imported directly from store-factory.ts everywhere despite also being barrel-exported), so the re-export was genuinely dead. Documents the convention in docs/agents/testing.md. Verified: full unit suite (5308 tests) passes except the one pre-existing, unrelated package-exports.test.ts failure; typecheck, lint, and format all clean; fallow audit clean against the PR base. * fix: correct fallow suppression token and drop unused barrel re-export These were meant to be part of 397cdd6d7 (verified locally before that commit) but didn't actually get staged — caught by CI's Fallow Code Quality check re-running against the pushed commit, which still had the plural 'unused-exports' token (fallow expects singular 'unused-export') and the dead barrel re-export. * fix: migrate the two mkdtemp call sites the rebase silently reintroduced Rebasing onto main pulled in #1594's two new test cases in this file, added independently of this branch's migration, still using raw fs.mkdtempSync(path.join(os.tmpdir(), ...)). Git's line-based merge found no textual conflict with this branch's removal of the os import (the changes touch non-overlapping regions), so it silently produced a file that doesn't typecheck. Migrated both to mkdtempForTestSync for consistency with the rest of the file, caught by CI's Typecheck, Fallow Code Quality, and FreeRange checks re-running against the pushed commit. * test: pin Vitest tmpdir lifecycle * fix: preserve the Swift cache across test runs
2026-08-05 08:56:39 +02:00
"check:tmpdir-leaks": "node --experimental-strip-types scripts/check-tmpdir-leaks.ts",
"check:tmpdir-leaks:test": "node --experimental-strip-types scripts/node-test-tmpdir.ts --experimental-strip-types --test scripts/check-tmpdir-leaks-model.test.ts scripts/vitest-tmpdir-global-setup.test.ts scripts/node-test-tmpdir.test.ts scripts/swift-toolchain-tmpdir.test.ts",
"check:freerange": "fr",
"check:quick": "pnpm lint && pnpm typecheck",
2026-05-11 17:28:04 +02:00
"sync:mcp-metadata": "node scripts/sync-mcp-metadata.mjs",
"check:mcp-metadata": "node scripts/sync-mcp-metadata.mjs --check",
"version": "pnpm sync:mcp-metadata && git add server.json",
"check:tooling": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm check:layering && pnpm depgraph:test && pnpm check:gate-manifest:test && pnpm check:gate-manifest && pnpm check:production-exports && pnpm check:tmpdir-leaks:test && pnpm check:xctest-selection && pnpm check:mcp-metadata && pnpm build && pnpm check:bundle-owner-files && pnpm check:package",
chore(test): remove the contention retry and shrink the subprocess-stub project (#1781 A4) (#1827) The enumerated single-retry policy (#1419) has fired zero times since it landed on 2026-07-29: 0 of 234 sampled Coverage-job lane envelopes (2026-08-11 to 2026-08-18) have retryCount > 0, and none of 17 recent failed runs was retried (5 refused "outside the enumerated retry list", 4 refused "unhandled error"). All three trackers its entries pointed at (#1098, #1414, #1419) are closed. It cost ~1,454 LOC, a per-run secret marker threaded through a setup file on every Vitest project, and a standing obligation for every future gate reporter to call the blocker bus. Delete the scripts, tests and fixtures, the check:contention-retry script and gate, the envelope artifact upload, and the runner-timeout setup file; test:coverage:ci is a plain `vitest run --coverage` again. lane-envelope.ts stays: the mutation, fuzz and concurrency-torture lanes build their envelopes from it. run-blocker-bus.ts goes: its only consumer was the retry's failure sink, and its only publisher already fails the run by setting process.exitCode. Keep the subprocess-stub project for the three files that really spawn (client-metro, fuzz harness, fuzz corpus-replay) and drop the three that run in 31/212/277ms in CI, which cannot contend for anything. The list is now a plain array in vitest.config.ts with the reason at each entry. Membership and the project's kill criterion live in #1823. Because test:coverage:ci is a bare vitest run, the gate manifest reads its projects directly, so OPAQUE_RUNNERS no longer needs it and an unrun Vitest project becomes unrepresentable rather than detected; the audit test now constructs that state by project-scoping the script.
2026-08-18 15:35:25 +02:00
"check:unit": "pnpm test:unit && pnpm check:tmpdir-leaks && pnpm test:smoke",
"check": "pnpm check:tooling && pnpm check:fallow && pnpm check:unit",
"prepack": "pnpm check:mcp-metadata && pnpm package:npm",
refactor(platforms): sink the shared src/platforms root files into their substrate homes (#2100) * refactor(platforms): sink the shared src/platforms root files into their substrate homes (#2082 W3) The shared files left directly under src/platforms move onto a declared owner: provisioning mechanics (install-source family, toolchain probing, boot-failure classification, app-resolution caching) form @agent-device/provision-kit above capture-kit; host mechanics resolve to host-kit's seams; kernel takes the pure numeric helpers; contracts keeps vocabulary only. Settings parsing, command-attempt rendering, and the unsupported-interactor factory stay with their families rather than pooling in a substrate package: android and apple settings each own their parsing, and the unsupported-interactor factory lives in root core with a vega-local copy. A platforms-root-shape rule rejects any new shared file or directory appearing directly under src/platforms, and the provision-kit direction gates (no platform imports in, no capture-kit importer) are planted red. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * test: cover the family-owned parsers and the unsupported-interactor factories The settings parsers and the unsupported-interactor factory arrived without owning tests, so their branches rode on callers. Each now has one: the appearance/state parsers over every accepted spelling and their rejections, the attempt summarizer over its arg join and stderr budget, and both interactor factories over the whole operation surface and the per-instance label. Also drops the duplicate ./snapshot-desktop-projection export key that a rebase left in capture-kit's manifest, where JSON silently keeps the last, and the root-shape docblock the violation message already states. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * docs: keep tool directives only in the touched files Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-08-28 07:46:50 +02:00
"typecheck": "tsc -b packages/xml packages/kernel packages/contracts packages/host-kit packages/capture-kit packages/provision-kit packages/platform-apple packages/platform-android packages/platform-harmonyos packages/platform-vega packages/platform-linux packages/platform-web packages/ad-script packages/selectors packages/ad-replay packages/maestro packages/replay-test packages/provider-webdriver packages/provider-limrun && tsc -p tsconfig.json && tsc -p examples/sdk/tsconfig.json",
test: replace the hand-typed Maestro fixture with a generated conformance oracle (#1289) * test: replace the hand-typed Maestro fixture with a generated conformance oracle Closes #1274. The old harness (scripts/maestro-conformance*) compared 5 hand-authored flows against a hand-typed transcription of Maestro 2.5.1's command model. It proved parser self-consistency, not conformance: all four bug classes that cost #1217 days of live debugging slipped past it by construction, and it verified no upstream SHAs despite parsing them. Every expected value here is generated from the pinned upstream artifacts. dev.mobile:maestro-orchestra:2.5.1 is published on Maven Central, so the harness runs the real parser and reads the real bytecode — no full Maestro source build. Layer 1 (parser): a Gradle/Kotlin harness drives the pinned YamlCommandReader over a corpus of 42 vendored maestro-test flows (sha256-recorded) plus authored bug-class, coverage, and invalid flows, capturing each parse. The verifier parses each flow with the live engine and classifies it identical / both-reject / we-reject / mismatch / we-are-lenient. Every non-identical outcome must be a declared divergence, so the 17 we-reject entries in expected-divergence.ts are the mechanical parity backlog (assertTrue, clipboard, travel, killApp, and option-level gaps) rather than silent drift. Layer 2 (semantics): ASM reads static-final constants straight from the pinned bytecode without initializing driver classes (MAX_RETRIES_ALLOWED=3, SCREENSHOT_DIFF_THRESHOLD=0.005, ANIMATION_TIMEOUT_MS=15000, erase cap, and the iOS pre-tap gate we intentionally omit), plus the parser-observed 400ms swipe default. Each is cross-checked against MAESTRO_COMPATIBILITY_PRESETS. Layer 3 (differential): scheduled device scenarios. Cross-engine comparison is outcome parity only and says so; finer behavior is asserted engine-side via invariants over replay-timing.ndjson. Bug class 4's detector — a tap must not consume the whole settle budget, since a full-budget tap means the stability loop never latched while the flow still passes — is pure and unit-tested against synthetic traces; only the device run is scheduled-only. regenerate.mjs verifies the pinned jar SHA-256s before trusting output and is byte-deterministic across runs. Layers 1-2 verify in normal CI via node --test with no Java (the job installs deps: unlike the layering guard it copies, the verifier parses with the live engine, which imports the `yaml` package). Acceptance: the four bug classes each have a fixture; every command in SUPPORTED_MAESTRO_COMMAND_NAMES (the parser's own dispatch table, now exported as the single source of truth) is corpus-covered or listed unverified; the five documented deviations are expected-divergence entries. * fix: address review findings on the conformance oracle P1 — layer-3 scenarios could never run. They pointed at layer-1 corpus flows, which exist only to be PARSED: they name a fictional com.example.app and elements that exist on no device. A device run would have failed before exercising any runtime behavior, making bug class 4's detector silently vacuous. Layer 3 now has its own flows under differential/flows/ driving the real fixture app (examples/test-app, com.callstack.agentdevicelab); the workflow builds and installs it and hard-fails if it is missing. A test enforces the separation so a scenario can never point back at the parse corpus. Nothing else in this repo builds or installs the Expo fixture app, so those steps are new and unproven. The workflow is therefore dispatch-only: the cron is removed until a supervised first run proves the path. A nightly job that fails at 05:00 every day teaches nothing. P2 — layer 3 installed whatever version the online installer served. It now pins MAESTRO_VERSION from pinned-upstream.json, so layer 3 cannot drift from the version layers 1-2 claim, and asserts `maestro --version` matches. P2 — fixture content was not bound to regeneration. CI compared only the embedded upstream metadata, so a hand edit to a captured command or constant passed: the transcription failure mode this oracle exists to remove. Two-layer fix, because per-PR CI must stay Java-free and cannot re-derive: - Each fixture now carries a contentHash seal that the verifier recomputes, so editing a capture breaks the build. Tamper-evident, and tested by actually tampering rather than assuming a hash comparison works. - New scheduled conformance-regenerate job re-runs the harness against the pinned jars and fails on any byte difference. Forgery cannot survive a real re-derivation. This is what makes "generated from upstream" enforced. P3 — boot-ios-test-simulator requires runtime-version; now passed alongside preferred-device-name, as the other iOS workflows do. * tmp: trigger layer-3 differential on this branch to prove the device path workflow_dispatch cannot run pre-merge (it registers from the default branch), so this temporary push trigger exists only to execute the never-run device path on the PR head and capture evidence. Removed before merge. * fix(ci): install the fixture app unfrozen for the layer-3 device run First live run of the device path failed at the very first step: ERR_PNPM_LOCKFILE_CONFIG_MISMATCH. CI implies --frozen-lockfile and the fixture app's lockfile is out of sync with its package.json overrides. No CI job has ever built examples/test-app, so that drift was never surfaced. * fix: drop --ignore-workspace from test-app:install (defeats #649 security overrides) The first live run of the layer-3 device path failed at ERR_PNPM_LOCKFILE_CONFIG_MISMATCH, and the cause is a real latent bug rather than a stale lockfile. #649 moved the fixture app's `overrides` into examples/test-app/pnpm-workspace.yaml precisely because pnpm only honors overrides from a workspace root — they pin transitive deps (ws, brace-expansion, xmldom, postcss, uuid, shell-quote) to versions that clear Dependabot alerts. But `test-app:install` passes --ignore-workspace, which ignores that very file, so the overrides are dropped and no longer match the lockfile that has them baked in. It goes unnoticed locally because interactive installs are not frozen, and no CI job has ever installed this app. Dropping --ignore-workspace makes examples/test-app resolve as its own workspace root (it has its own pnpm-workspace.yaml and is not a member of the repo-root workspace), so the overrides apply and a frozen install succeeds. Verified both directions locally: with the flag + --frozen-lockfile reproduces the CI failure; without it, a frozen install completes and the lockfile's overrides stay intact. Note the workaround this replaces would have been actively harmful: installing with --no-frozen-lockfile resolves the mismatch by regenerating the lockfile WITHOUT the overrides, silently reverting the app to the vulnerable transitive versions #649 pinned away. * fix: make layer-3 scenarios prove what they claim, and parse the Maestro version Run 3 (29497919702) got the whole device path working: Expo build (30m), app installed, simctl check, pinned Maestro CLI install. Only the version ASSERTION failed — `maestro --version` prints an analytics banner before the version, and `tr -d '[:space:]'` mashed banner+version into one string. The CLI was correctly 2.5.1. Match the semver line instead, and set MAESTRO_CLI_NO_ANALYTICS (CI should not phone home). Verified the parse against the exact CI output: banner and clean forms both yield 2.5.1, wrong/empty still fail. tap-retry-if-no-change was vacuous: it tapped a navigating control, so the first tap always succeeded and retryIfNoChange never ran — it passed while proving nothing. It now taps the app's non-interactive title so the screen cannot change and the retry path is forced, and asserts tapRetries >= 1 from the trace (MaestroRuntimeMetrics already records it per step). A new metricAtLeast invariant kind carries the assertion; a test reproduces the old vacuity. percent-swipe no longer claims bug class 1. Truncation vs rounding is a <=1px delta that no app-observable device outcome can distinguish, so pass/pass could never back that claim up. The runtime half is instead pinned exactly by a pure unit test of resolveMaestroCoordinate (it short-circuits on a known viewport, so no device is needed) — verified to catch the regression by flipping trunc->round, which turns 3 of 6 tests red. Truncation had no test coverage at all before this. A test now forbids any device scenario from re-claiming bug class 1. * fix(ci): pass --maestro and match the fixture app's real UI in layer-3 flows Run 4 (29500262301) reached the differential itself — build, install, simctl check and the pinned Maestro 2.5.1 verification all passed — and surfaced two real bugs, both mine: 1. The runner invoked `agent-device test <flow>` without --maestro, so every scenario failed with "test does not support this file type". The repo's own scripts/run-test-app-maestro-suite.mjs passes it; the flag is what routes a .yaml through the Maestro compat engine. 2. settle-after-tap and percent-swipe assumed home-open-form is on screen at launch. It is not: real Maestro reported "Element not found: home-open-form", and the app's own helper flow scrolls it into view first. settle-after-tap now scrolls before tapping, mirroring that helper; percent-swipe no longer navigates at all and swipes the scrollable home screen, so it tests the conversion and nothing else. The remaining two flows already reported maestro=pass, so only the agent-device invocation was wrong for those. Note the settle invariant correctly reported "no-data: no completed tapOn steps" and FAILED rather than passing — a detector that cannot run is a failure, as intended. * feat: declare layer-3 divergences and schedule the differential Layer 3 ran both engines for the first time (29504440599) and immediately found a real engine bug. Blocking the measurement instrument on repairing what it just measured inverts the dependency, so layer 3 now gets the contract layer 1 already had: every divergence is a decision on the record. Adds `knownDivergence: { reason, tracking }` to the scenario type — the layer-3 twin of FLOW_DIVERGENCES. A declared divergence keeps the run green; only UNDECLARED ones fail. Two rules stop that from rotting, both enforced mechanically rather than by prose discipline: - `tracking` is required and must be a real issue URL (run.test.ts), because a declaration with nothing behind it is how "temporarily expected" becomes permanent without anyone deciding to. - a stale declaration FAILS: if a declared-divergent scenario starts passing, the run goes red until the declaration is removed. The fix PR must delete it, and the differential then enforces the gap stays closed — the oracle is the acceptance test for its own findings. Declared: - settle-after-tap -> #1299. Our scrollUntilVisible times out finding home-open-form where Maestro 2.5.1 scrolls to it and passes. Real engine correctness bug in an advertised command, found by this differential. Blocks bug class 4's device detector until fixed. - tap-retry-if-no-change -> #1300. The invariant caught the scenario being vacuous: both engines pass but tapRetries was 0, so retryIfNoChange never ran. Needs an inert fixture control; a scenario defect, not an engine one. Proven green on both engines and enforced now: percent-swipe, optional-warned-not-failed — the latter is real device-verified warned-vs-failed parity. With declarations in place the differential is green, so the schedule goes in (cron 05:00) per #1274. A green run still prints what it is not proving. * fix: park the flaky retry scenario instead of declaring it a divergence Run 29510020718 fired the stale-declaration guard on its first outing and caught my own mistake. tap-retry-if-no-change measured tapRetries=0 in run 29504440599 and tapRetries=1 in 29510020718 — same flow, same commit. So it is not vacuous as #1300 originally claimed: it is NON-DETERMINISTIC. The tap sometimes holds the hierarchy signature still and sometimes does not, because the fixture home screen carries live content. That exposes a real limit of the mechanism added in the previous commit: knownDivergence assumes the divergence REPRODUCES. A declared-but-flaky scenario flips between known-divergence (green) and stale-declaration (red) at random — a coin-flip scheduled job, which is worse than no scenario because it teaches people to ignore the differential. So the scenario is parked, not declared. The flow and the tapRetries invariant stay implemented and unit-tested, so the fix PR only re-adds the scenario once the fixture has an inert control. retryIfNoChange therefore has NO device coverage right now — tracked in #1300 and stated plainly rather than disguised by a green run. A test keeps it out of the active set until then. #1300 updated with the corrected diagnosis and both runs' evidence. Active differential: settle-after-tap (declared divergence, #1299), percent-swipe and optional-warned-not-failed (both enforced, pass/pass on real devices). * fix: make a knownDivergence waiver cover exactly one failure, not any failure P1 from re-review, and a real flaw: the code did not do what its own comment claimed. runScenario() collapsed every unexpected outcome and every invariant failure into `misbehaved`, then turned ANY of them green if the scenario carried a declaration. So while the #1299 scrollUntilVisible waiver is open, upstream Maestro could start failing too — or a different invariant could break — and the scheduled job would still report known-divergence and pass. A waiver for one bug was silently amnesty for the next. That is the exact failure this oracle exists to prevent, committed one commit after building the guard against it. knownDivergence now requires an `expected` signature: both engines' outcomes plus each declared invariant's status. The runner matches it exactly — - matches -> known-divergence (green, tracked) - misbehaves differently -> failed (red): not the failure the waiver covers - stops misbehaving -> stale-declaration (red): remove the declaration #1299's signature pins what runs 29504440599/29510020718 actually observed: maestro=pass, agent-device=fail, settle invariant no-data. Tests prove unrelated failures stay red under an open waiver: upstream also failing, our engine unexpectedly passing, a different invariant status, and a new invariant appearing are each NOT covered. A signature where both engines pass is rejected outright as describing no divergence. Also retains replay-timing.ndjson as a run artifact (review evidence note): the invariants are computed from that trace, so a report saying "tapRetries was 0" cannot be audited once the runner is gone without it. * perf(ci): cache the fixture app build for the layer-3 differential The differential job took ~30 minutes, of which 1331s (22 min, 79%) was building the Expo fixture app and only 347s was the differential itself — rebuilt from scratch on every run for an app that changes almost never. Cache the built .app, keyed on everything that can change the binary: the app's sources, native config, dependency graph, the build step itself, the iOS runtime, and the Xcode version. Mirrors the existing setup-apple-replay prebuilt-runner cache (same action pin, same Xcode-key + source-hash shape). On a hit the build is skipped entirely and the bundle is installed straight onto the booted simulator (~seconds), taking the job to roughly 8 minutes. On a miss it falls back to exactly the previous behaviour and repopulates, so the worst case is unchanged. The existing simctl verification still gates both paths, so a bad cache cannot produce a vacuous green: if the app is not installed, the job fails loudly rather than running scenarios against nothing. Note the first run after this lands is necessarily a miss. * refactor(ci): extract setup-fixture-app so any job can use the cached app The fixture-app build + cache was inline in the differential workflow, so nothing else could reach it. Extracted to a composite action mirroring setup-apple-replay, because the capability is what #320 has been missing: it wants replay coverage moved off Apple system apps onto a controlled fixture with stable ids, and that fixture (examples/test-app) already exists — CI just had no way to build and install it. The cache is genuinely shared. GitHub caches are per-repository and readable across workflows, and a run restores from its own branch or the default branch, so once a run on main populates it every workflow gets the hit and only the first one pays the ~22 minutes. The key is computed inside the action from a fixed input list and deliberately contains nothing caller-specific — folding a caller's workflow path into it would silently unshare the cache. Also removes a duplication risk: the action reads the bundle id from the built app's Info.plist rather than hardcoding it, so it cannot drift from what was actually built, and it fails loudly if the app is not installed. The conformance workflow keeps its own narrower assertion — that the installed id is the one its scenarios target — since that is its concern, not the action's. Usage: - uses: ./.github/actions/setup-fixture-app with: runtime-version: ${{ env.IOS_RUNTIME_VERSION }} # outputs: app-path, app-id, cache-hit * chore(ci): remove the temporary branch push trigger Run 29519848340 on this head executed both engines against the real fixture app and came back green, so the trigger that existed only to prove the never-run device path has done its job. Merged config is now cron (05:00) + workflow_dispatch, as required by #1274. known-divergence settle-after-tap maestro=pass agent-device=fail (#1299) ok percent-swipe maestro=pass agent-device=pass ok optional-warned-not-failed maestro=pass agent-device=pass This commit will not itself trigger a run: GitHub evaluates triggers at the pushed commit, and the push trigger is gone in it.
2026-07-16 20:22:28 +02:00
"test-app:install": "pnpm install --dir examples/test-app",
2026-04-26 20:49:59 -04:00
"test-app:start": "pnpm --dir examples/test-app start",
"test-app:ios": "pnpm --dir examples/test-app ios",
"test-app:android": "pnpm --dir examples/test-app android",
"test-app:typecheck": "pnpm --dir examples/test-app typecheck",
"test-app:replay:ios": "pnpm ad test examples/test-app/replays --platform ios --artifacts-dir .tmp/test-app-replay/ios",
"test-app:replay:android": "pnpm ad test examples/test-app/replays --platform android --artifacts-dir .tmp/test-app-replay/android",
"test-app:maestro": "node scripts/run-test-app-maestro-suite.mjs",
"test-app:maestro:ios": "pnpm test-app:maestro --platform ios",
"test-app:maestro:android": "pnpm test-app:maestro --platform android",
"test": "pnpm test:unit",
"test:unit": "vitest run --project unit-core --project fuzz-worker",
"test:maestro-compat": "vitest run --project unit-core packages/maestro src/daemon/adapters/maestro src/compat/__tests__/replay-input.test.ts",
fix(ci): run the fuzz corpus replay outside the coverage lane (#1994) The Coverage job intermittently ends with no failing test and one file's results missing: Test Files 1070 passed (1071) Errors 1 error Error: [vitest-pool]: Worker forks emitted error. Caused by: Error: Worker exited unexpectedly This is shape (B) of #1824 — the half #1854 did not fix. Scanning every failed Coverage job across the 120 CI runs since #1854 merged finds the signature five times, and the vanished file is scripts/fuzz/corpus-replay.test.ts all five (six for six with #1866's occurrence): 23% of Coverage failures in that window, ~4% of all CI runs. The ~40s gap before the error is coverage report generation, not test time — the pool surfaces its AggregateError only once every task settles. Control, from a green attempt of the same run: the file passes in 3152ms at 09:37:35.9 and the summary prints at 09:38:12.5. So the file is not slow in CI, nothing else is in flight when it dies, and neither a missed per-case budget nor STARTUP_BUDGET_MS is implicated. Partial test counts (3/11 and 9/11 reported) place the death mid-file, inside runCases. So the corpus replay gets its own serialized project that the coverage run skips, and a second uninstrumented Vitest invocation in `test:coverage:ci` runs it, keeping the tests on every PR. Measured against two full runs, this costs zero coverage: the cases execute in worker threads, a separate isolate the fork's inspector never instruments, so the lines reported are identical with and without it. Membership is by demonstrated failure, not by a property of the code: `session-replay-runtime-maestro.test.ts` also constructs a node:worker_threads Worker and stays in unit-core, instrumented and green, so "nests a Worker" is explicitly not the criterion. The second leg goes through `test:fuzz-worker`, which blanks AGENT_DEVICE_COVERAGE_SHARD and AGENT_DEVICE_COVERAGE_MERGE. ci.yml sets those as job-level env over a single `gate: unit-ci` step, so both legs would otherwise inherit them and the shard would die: Vitest refuses `--shard=1/2` over this one-file project, and the blob reporter overwrites the instrumented shard's report on its way out. Verified on the merged tree — shard 1/2 (549 files), shard 2/2 (548), and the merge job (1097 files, 90.38% lines) all pass, and the leg still fails without the blanking. Refs #1824
2026-08-24 17:03:25 +02:00
"test:coverage": "vitest run --coverage --project=!fuzz-worker && pnpm test:fuzz-worker",
"test:coverage:ci": "vitest run --coverage --project=!fuzz-worker && pnpm test:fuzz-worker",
"test:fuzz-worker": "vitest run --project fuzz-worker",
refactor: add provider-first device lab tests (#542) * refactor: add provider-first device lab tests * refactor: tighten device lab provider seams * test: cover provider lab contracts * docs: record device lab harness direction * ci: run device lab integration tests * test: move device lab under integration * test: extract device lab helpers * refactor: centralize apps filter defaults * test: drop lab-covered unit tests * test: fold platform happy paths into device lab * test: reuse device lab helpers * test: move device lab to in-process harness * test: replace session handler cases with device lab * test: harden device lab scenario contracts * docs: define unit test retention policy * test: expand provider device lab coverage * test: harden provider device lab coverage * test: cover manifest install and runner session contracts * chore: remove unused provider cleanup code * test: split android find device lab scenario * test: track provider lab architecture progress * test: clarify provider lab roadmap progress * test: advance provider lab session coverage * test: move menubar click routing to device lab * test: move menubar snapshots to device lab * refactor: centralize screenshot flag plumbing * refactor: colocate screenshot flag metadata * test: cover all public commands in device lab * test: move macos wait success to device lab * test: drop redundant perf and diff units * test: move push payload paths to device lab * test: move network parsing to device lab * test: move log cleanup to device lab * test: move log restart and boot to device lab * test: move ios physical boot to device lab * test: cover perf startup in device lab * test: extract android and ios device lab worlds * test: trim device lab world surface * test: split snapshot capture unit coverage * test: deepen device lab coverage and trim handler units * test: clean up device lab migration scaffolding * test: report device lab public command coverage * refactor: make Apple provider seams semantic * refactor: tighten device inventory and Linux provider seams * refactor: tighten request provider scoping * refactor: add semantic macos host provider * test: broaden device lab find coverage * test: cover workflow flags in device lab * refactor: promote linux input provider seam * test: clarify device lab flag coverage * test: classify snapshot force-full progress * test: enforce device lab progress in ci * test: stabilize device lab ci * test: move packaged metro smoke to integration * test: drop stale provider seam coverage * test: harden provider scope regression coverage * refactor: remove stale platform barrels * refactor: keep linux clipboard and screenshots semantic * refactor: move macos host tools behind provider * fix: honor remote artifact output paths * test: deepen runtime coverage for daemon and runner paths * test: share loopback test helpers * refactor: make daemon runtime importable * fix: honor replay target metadata * chore: tighten final device lab quality gates * test: share device lab setup helpers * test: remove generic apple lab fallback * test: deduplicate device lab helpers * chore: tighten fallow duplication signal * refactor: share apple diagnostic helpers * fix: detect active android ime during fill verification * test: consolidate provider-backed integration suite * ci: fix fallow and iOS smoke setup * chore: consolidate cleanup after ci fixes * test: split vitest unit and integration projects * docs: mention MCP discovery metadata * docs: add agent skills context pointers * fix: close provider recording coverage gaps * fix: restore mcp compatibility smoke * test: cover provider edge regressions * test: consolidate loopback helpers * docs: remove stale provider routing reference * fix: harden final provider review issues * chore: defer mcp cleanup from provider refactor
2026-05-18 14:50:52 +02:00
"test:integration:provider": "vitest run --project provider-integration",
"test:integration:progress": "node --experimental-strip-types scripts/integration-progress.ts",
"test:integration:progress:check": "node --experimental-strip-types scripts/integration-progress.ts --check",
fix(test): deterministic temp-dir cleanup across node --test lanes (#1661) * fix(test): deterministic temp-dir cleanup across node --test lanes node --test has no global setup/teardown hook, so unlike Vitest (#1593) every node --test package.json script (maestro:conformance, mutation:test, check:affected:test, check:coverage-changed:test, check:layering, depgraph:test, check:tmpdir-leaks:test, check:contention-retry, test:fixture-cache, test:smoke(:web), test:integration:node, test:concurrency-torture) still created scratch directories against the real, unredirected os.tmpdir(), with cleanup only as reliable as each call site's own try/finally — which a crash, OOM, or timeout kill bypasses entirely. Add scripts/node-test-tmpdir.ts: it wraps the whole `node --test` invocation as a child process, redirecting TMPDIR to one disposable, pid-tagged directory (shared root/prefix with the Vitest lane) and removing it from the process 'exit' event, which fires on normal completion, a thrown error, or a forwarded SIGINT/SIGTERM alike. Every node --test script now runs through it. check-tmpdir-leaks.ts already scans by root/prefix, so it covers both mechanisms with no changes to its detection logic. Verified: a node --test process that mkdtemp's then gets SIGKILL'd leaves a directory behind unwrapped; wrapped and SIGTERM'd, TMPDIR is redirected and the directory is gone with no orphaned processes. All 13 wrapped lanes and the full Vitest suite (5,591 tests) pass with zero residual agent-device-test-run-* directories after the run. Fixes #1595 * test(tmpdir): ratchet every node --test script through the wrapper The 13 lanes wrapped in package.json were a one-time hand sweep with nothing enforcing the pattern going forward — a 14th node --test script added later without scripts/node-test-tmpdir.ts would silently reopen #1595 for that one lane. Add a structural check to scripts/node-test-tmpdir.test.ts (now part of check:tmpdir-leaks:test) that reads package.json and fails if any script invokes `node ... --test` without routing through the wrapper. Dumb string matching over the scripts map, no shell parsing, with an explicit (currently empty) NODE_TEST_WRAPPER_BYPASS_ALLOWLIST for any lane that must legitimately bypass it. Verified it both passes on the current package.json and fails when a synthetic unwrapped `node --test` script is added. * fix(test): preserve the Swift cache and close the raw node --test bypasses Review on #1661 found two gaps: 1. The wrapper only overrode TMPDIR, so it discarded and forced a recompile of the durable Swift compiler cache every run instead of mirroring vitest-tmpdir-global-setup.ts's carve-out for it. Read os.tmpdir() before the child's TMPDIR redirect takes effect and set AGENT_DEVICE_SWIFT_CACHE_DIR from that (only when unset), same as the Vitest lane — the two now share one durable cache instead of each discarding and recompiling their own. Added a probe assertion (scripts/node-test-tmpdir.test.ts) that fails without the fix and passes with it (verified both ways). 2. docs/agents/testing.md documented raw `node --test` commands for the iOS smoke files, and the android/ios/conformance-regenerate/nightly workflows invoked `node --test` directly outside package.json. Routed all of them through scripts/node-test-tmpdir.ts so the documented local commands and CI lanes get the same crash/timeout-safe cleanup the package.json scripts already have.
2026-08-07 13:26:20 +02:00
"test:fixture-cache": "node --experimental-strip-types scripts/node-test-tmpdir.ts --test test/ci/trusted-fixture-artifact.test.mjs",
refactor(ci): make gate ownership structural (#1429) (#1753) * test(ci): prove every registered gate is owned and reachable (#1429) A check that silently stops running looks exactly like a green build. Two suites had already stopped: `check:tmpdir-leaks` (with its model tests) and `test:fixture-cache` are real package scripts that no workflow ran, reachable only through the `check:unit` aggregate CI never invokes. `CHECK_CATALOG` becomes the registry of every check and `pnpm gate <id>` the only way CI runs one, so finding what a lane runs is a scan for `pnpm gate` rather than an attempt to interpret shell. `pnpm check:gate-manifest` then asserts against the real workflows that every registered check is run by some qualifying lane (per unit, not per script name), that every check the real selector activates for a path is run by a lane that path would start (#1420's class), and that every Vitest project and suite script belongs to a check. The wiring that keeps those honest is asserted too: a gate id must name a registered check, an `if:` must be ruled on in GATE_CONDITIONS so `if: false` unowns what it guards, an action declared to run a gate is proven to, and a job whose steps the loader cannot open fails closed. It deliberately does not try to prove CI runs project code only through `pnpm gate`. Whether a shell block executes project code is not decidable from its text, so shell this model does not recognise earns no ownership credit — the failure direction is a check reported unowned, never one waved through. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * test(ci): update the two suites that assert on rewired workflow text `scripts/mutation/workflow.test.ts` and `test/ci/trusted-fixture-artifact.test.mjs` read the workflow and action files and assert on their command text, so routing those steps through `pnpm gate <id>` moved what they were matching. They are the two suites the manifest cannot help with: it proves a gate is still run, not that a test asserting on how CI spells a command was updated with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * fix(ci): credit gates by execution shape, and keep every guard Three ways the manifest could report a gate as owned when it does not run. 1. Crediting was a substring scan over `run:`, which #1429 explicitly rules out — "do not infer reachability from a command name merely appearing in workflow text". `false && pnpm gate x`, a gate inside `if false; then … fi`, one named in a heredoc, and `echo pnpm gate x` all credited it. There is a live instance: conformance-regenerate.yml's "Fail if regeneration changed anything" step names `pnpm gate maestro-regenerate` inside an error message telling a human to run it, and that credited the gate. A gate now counts only as the first command segment of a line, and a body carrying shell structure earns nothing. Reachability inside a script is not decidable, so this does not try: unrecognised shape means no credit and the check reports unowned. `VAR=$(pnpm gate x …)` is read, since the assignment form is unambiguous and the gate runs. 2. Job-level `if:` was not modelled at all, though six live jobs carry one, so a job that cannot run still credited every gate inside it. Two conditions on the mutation lanes are now declared. 3. A caller's `if:` REPLACED the guard on a nested composite-action step (`guard[0] ?? step.condition`), so an outer `always()` erased an inner `if: false`. Steps carry every guard between the lane and the step. Also corrects two source comments that still claimed project code run outside the runner fails the manifest. It does not: such a step earns no credit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * ci: add the run-gate action that names a gate structurally The seam the ownership proof will read instead of shell. A lane says which gate it runs in `with.gate`, a typed input the manifest reads straight out of the YAML and validates against CHECK_CATALOG. Nothing here is wired yet — the ~60 call sites and the model change follow. Added first so the target of that conversion is reviewable on its own. `args` cannot select which gate runs; it is appended after the id, so the worst a wrong value does is fail the gate it already named. There is no `|| true` and no output capture: the gate's exit code is the step's exit code, so a gate cannot run without being able to fail its lane. Part of #1429. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * merge: main (#1770) and route its three new steps through the runner #1770 landed the orphan-check fix on main, wiring `check:tmpdir-leaks`, `check:tmpdir-leaks:test` and `test:fixture-cache` into Coverage, Layering Guard and Integration Tests. This branch had wired the same three through `pnpm gate`, so the merge produced two steps per check rather than a conflict — each check ran twice. Kept main's steps, with the placement and reasoning reviewed on #1770, and changed only their `run:` line to the canonical runner. Dropped this branch's duplicates. Net effect on CI is unchanged: the same three checks, in the same three lanes, once each. Gate manifest green after the merge: 47 checks wired across 33 lanes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * fix(ci): address review — suite detection, freerange, glob, vacuous skip-list Six review findings plus the mutation blocker. [bug] `registered` was shape-only, so a `test:*` script running `node src/bin.ts test <dir>` resolved to a `script:` leaf and was invisible. Four `test:replay:*` scripts were owned only because someone hand-registered them; `test:replay:android` was neither registered nor reported while the nightly ran the same six .ad files by inlining them. A `test:*` script is now a suite by name. `replay-android` is registered, and the nightly runs the script instead of re-listing its files so the two cannot drift. The nightly invokes it inside `reactivecircus/android-emulator-runner`'s `script:` input — shell handed to a third-party action this loader does not read — so the suite executes but cannot be credited. Recorded in UNPROVABLE_OWNERS with that exact reason rather than assumed. The fixed detector also found a second orphan the review did not name: `test:integration:progress`. That one is a reporter whose `--check` sibling is the registered gate, so it is declared in REPORTING_SCRIPTS — a declaration that itself fails when inert. [bug] `freerange` defaulted to localRunnable, so fail-open ran `fr` (a Bun binary) on the pre-push path. Now false. [suggestion] The `--run` skip-list asserted `build:android-snapshot-helper`, a name `android-helpers` no longer uses, so it could not fail. Derived from the catalog instead. [suggestion] `matchesGlob` joined `**` splits with `.*`, making the adjacent slash mandatory — GitHub's `**` matches zero directories, so `src/**/*.test.ts` did not match `src/a.test.ts`. Pinned against `packages/*/src/**/*.test.ts`. [suggestion] Deleted the unwired `run-gate` action. It had no callers, was absent from GATE_ACTIONS, and its comment described a system that had not shipped. It returns with the rewiring, not before. [suggestion] Collapsed the module headers that narrated discarded designs. Mutation: `daemon entrypoint publishes HTTP metadata and cleans up on shutdown` is the only test here that spawns a real daemon process. It takes ~1.1s alone but exceeds Vitest's 5s default inside Stryker's dry run, which aborts the sweep before a single mutant runs. Given 30s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * fix(mutation): order sandbox aliases longest-first so subpaths resolve Every shard of the mutation sweep aborted in Stryker's dry run with: Cannot find package '@agent-device/selectors/engine' imported from .tmp/stryker/sandbox-*/src/core/selector-pipeline.ts The alias was generated correctly; it just never won. Vite matches a STRING alias by prefix and takes the first hit, and `workspaceSpecifierTargets` emitted the bare `@agent-device/selectors` ahead of the subpath entries. The bare entry therefore captured `@agent-device/selectors/engine` and rewrote it to `…/src/index.ts/engine`, which does not exist; Node fell back to real package resolution, could not find the subpath inside the sandbox, and the dry run failed before a single mutant ran — so the shard uploaded an empty envelope instead of a report and the ratchet failed for want of one. Sorting longest specifier first makes the most specific alias win: @agent-device/selectors/engine -> packages/selectors/src/engine.ts @agent-device/selectors/ast -> packages/selectors/src/ast.ts @agent-device/selectors -> packages/selectors/src/index.ts `/ast` never tripped this because nothing in a related test set imported it; `selector-pipeline.ts` introduced the first subpath import that mattered (#1744), so the mutation lane has been unable to run since that landed. Any PR touching `scripts/mutation/**` — which fails open into the full sweep — would have hit it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * refactor: derive gate ownership from workflow structure * fix: run gates without optional arguments * fix: resolve mutation workspace subpaths exactly --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-08-12 16:02:18 +02:00
"test:fixture-fallback": "sh ./test/scripts/setup-fixture-app-fallback-smoke.sh",
chore: add agent-efficiency regression guards (#1174) * chore: ratchet architecture dependency graph Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test: ratchet agent-facing output economy Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * feat: derive command navigation explanations Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: keep efficiency checks fallow-clean Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(layering): enforce back-edge ceiling monotonicity and cover root src files Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * ci(layering): pin back-edge-ceiling ratchet to PR merge-base Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(output-economy): baseline-independent actionability floors, policy-derived error, like-for-like screenshot surfaces Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(explain): resolve true CLI aliases, canonical usage, and derived owners Surface true CLI aliases from parser normalization (long-press, metrics, tap, launch, relaunch) distinct from catalog keys, preserving implied-flag semantics (relaunch => open --relaunch). Extract the canonical single-line usage builder to src/utils/cli-usage.ts so schemas without usageOverride include positionals and flags. Replace guessed handler paths with a completeness-checked daemon-route owner map keyed by the closed DaemonCommandRoute union, fixing silently-dropped non-kebab routes (reactNative, recordTrace) and generic dispatch. Add table-driven coverage for aliases, synthesized usage, split-family/route-variant/dispatch owners, structured output, and explain:command CLI exit/stdout/stderr. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: enforce exact ratchets and compact command explain Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: colocate command ownership metadata Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: bind daemon owners to production routes Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: preserve generic dispatch bundling Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: enforce monotonic output budgets Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Michał Pierzchała <thymikee@gmail.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-10 11:54:09 +02:00
"test:output-economy": "vitest run --project output-economy",
fix(test): deterministic temp-dir cleanup across node --test lanes (#1661) * fix(test): deterministic temp-dir cleanup across node --test lanes node --test has no global setup/teardown hook, so unlike Vitest (#1593) every node --test package.json script (maestro:conformance, mutation:test, check:affected:test, check:coverage-changed:test, check:layering, depgraph:test, check:tmpdir-leaks:test, check:contention-retry, test:fixture-cache, test:smoke(:web), test:integration:node, test:concurrency-torture) still created scratch directories against the real, unredirected os.tmpdir(), with cleanup only as reliable as each call site's own try/finally — which a crash, OOM, or timeout kill bypasses entirely. Add scripts/node-test-tmpdir.ts: it wraps the whole `node --test` invocation as a child process, redirecting TMPDIR to one disposable, pid-tagged directory (shared root/prefix with the Vitest lane) and removing it from the process 'exit' event, which fires on normal completion, a thrown error, or a forwarded SIGINT/SIGTERM alike. Every node --test script now runs through it. check-tmpdir-leaks.ts already scans by root/prefix, so it covers both mechanisms with no changes to its detection logic. Verified: a node --test process that mkdtemp's then gets SIGKILL'd leaves a directory behind unwrapped; wrapped and SIGTERM'd, TMPDIR is redirected and the directory is gone with no orphaned processes. All 13 wrapped lanes and the full Vitest suite (5,591 tests) pass with zero residual agent-device-test-run-* directories after the run. Fixes #1595 * test(tmpdir): ratchet every node --test script through the wrapper The 13 lanes wrapped in package.json were a one-time hand sweep with nothing enforcing the pattern going forward — a 14th node --test script added later without scripts/node-test-tmpdir.ts would silently reopen #1595 for that one lane. Add a structural check to scripts/node-test-tmpdir.test.ts (now part of check:tmpdir-leaks:test) that reads package.json and fails if any script invokes `node ... --test` without routing through the wrapper. Dumb string matching over the scripts map, no shell parsing, with an explicit (currently empty) NODE_TEST_WRAPPER_BYPASS_ALLOWLIST for any lane that must legitimately bypass it. Verified it both passes on the current package.json and fails when a synthetic unwrapped `node --test` script is added. * fix(test): preserve the Swift cache and close the raw node --test bypasses Review on #1661 found two gaps: 1. The wrapper only overrode TMPDIR, so it discarded and forced a recompile of the durable Swift compiler cache every run instead of mirroring vitest-tmpdir-global-setup.ts's carve-out for it. Read os.tmpdir() before the child's TMPDIR redirect takes effect and set AGENT_DEVICE_SWIFT_CACHE_DIR from that (only when unset), same as the Vitest lane — the two now share one durable cache instead of each discarding and recompiling their own. Added a probe assertion (scripts/node-test-tmpdir.test.ts) that fails without the fix and passes with it (verified both ways). 2. docs/agents/testing.md documented raw `node --test` commands for the iOS smoke files, and the android/ios/conformance-regenerate/nightly workflows invoked `node --test` directly outside package.json. Routed all of them through scripts/node-test-tmpdir.ts so the documented local commands and CI lanes get the same crash/timeout-safe cleanup the package.json scripts already have.
2026-08-07 13:26:20 +02:00
"test:smoke:web": "pnpm build && node --experimental-strip-types scripts/node-test-tmpdir.ts --test test/integration/smoke-web-platform.test.ts",
"test:smoke": "node --experimental-strip-types scripts/node-test-tmpdir.ts --test test/integration/smoke-*.test.ts",
"test:integration:node": "node --experimental-strip-types scripts/node-test-tmpdir.ts --test --experimental-test-isolation=none test/integration/*.test.ts",
"test:integration:macos-coverage": "node --experimental-strip-types scripts/node-test-tmpdir.ts --test test/integration/smoke-macos-coverage.test.ts",
refactor: add provider-first device lab tests (#542) * refactor: add provider-first device lab tests * refactor: tighten device lab provider seams * test: cover provider lab contracts * docs: record device lab harness direction * ci: run device lab integration tests * test: move device lab under integration * test: extract device lab helpers * refactor: centralize apps filter defaults * test: drop lab-covered unit tests * test: fold platform happy paths into device lab * test: reuse device lab helpers * test: move device lab to in-process harness * test: replace session handler cases with device lab * test: harden device lab scenario contracts * docs: define unit test retention policy * test: expand provider device lab coverage * test: harden provider device lab coverage * test: cover manifest install and runner session contracts * chore: remove unused provider cleanup code * test: split android find device lab scenario * test: track provider lab architecture progress * test: clarify provider lab roadmap progress * test: advance provider lab session coverage * test: move menubar click routing to device lab * test: move menubar snapshots to device lab * refactor: centralize screenshot flag plumbing * refactor: colocate screenshot flag metadata * test: cover all public commands in device lab * test: move macos wait success to device lab * test: drop redundant perf and diff units * test: move push payload paths to device lab * test: move network parsing to device lab * test: move log cleanup to device lab * test: move log restart and boot to device lab * test: move ios physical boot to device lab * test: cover perf startup in device lab * test: extract android and ios device lab worlds * test: trim device lab world surface * test: split snapshot capture unit coverage * test: deepen device lab coverage and trim handler units * test: clean up device lab migration scaffolding * test: report device lab public command coverage * refactor: make Apple provider seams semantic * refactor: tighten device inventory and Linux provider seams * refactor: tighten request provider scoping * refactor: add semantic macos host provider * test: broaden device lab find coverage * test: cover workflow flags in device lab * refactor: promote linux input provider seam * test: clarify device lab flag coverage * test: classify snapshot force-full progress * test: enforce device lab progress in ci * test: stabilize device lab ci * test: move packaged metro smoke to integration * test: drop stale provider seam coverage * test: harden provider scope regression coverage * refactor: remove stale platform barrels * refactor: keep linux clipboard and screenshots semantic * refactor: move macos host tools behind provider * fix: honor remote artifact output paths * test: deepen runtime coverage for daemon and runner paths * test: share loopback test helpers * refactor: make daemon runtime importable * fix: honor replay target metadata * chore: tighten final device lab quality gates * test: share device lab setup helpers * test: remove generic apple lab fallback * test: deduplicate device lab helpers * chore: tighten fallow duplication signal * refactor: share apple diagnostic helpers * fix: detect active android ime during fill verification * test: consolidate provider-backed integration suite * ci: fix fallow and iOS smoke setup * chore: consolidate cleanup after ci fixes * test: split vitest unit and integration projects * docs: mention MCP discovery metadata * docs: add agent skills context pointers * fix: close provider recording coverage gaps * fix: restore mcp compatibility smoke * test: cover provider edge regressions * test: consolidate loopback helpers * docs: remove stale provider routing reference * fix: harden final provider review issues * chore: defer mcp cleanup from provider refactor
2026-05-18 14:50:52 +02:00
"test:integration": "pnpm test:integration:node && pnpm test:integration:provider",
fix(test): deterministic temp-dir cleanup across node --test lanes (#1661) * fix(test): deterministic temp-dir cleanup across node --test lanes node --test has no global setup/teardown hook, so unlike Vitest (#1593) every node --test package.json script (maestro:conformance, mutation:test, check:affected:test, check:coverage-changed:test, check:layering, depgraph:test, check:tmpdir-leaks:test, check:contention-retry, test:fixture-cache, test:smoke(:web), test:integration:node, test:concurrency-torture) still created scratch directories against the real, unredirected os.tmpdir(), with cleanup only as reliable as each call site's own try/finally — which a crash, OOM, or timeout kill bypasses entirely. Add scripts/node-test-tmpdir.ts: it wraps the whole `node --test` invocation as a child process, redirecting TMPDIR to one disposable, pid-tagged directory (shared root/prefix with the Vitest lane) and removing it from the process 'exit' event, which fires on normal completion, a thrown error, or a forwarded SIGINT/SIGTERM alike. Every node --test script now runs through it. check-tmpdir-leaks.ts already scans by root/prefix, so it covers both mechanisms with no changes to its detection logic. Verified: a node --test process that mkdtemp's then gets SIGKILL'd leaves a directory behind unwrapped; wrapped and SIGTERM'd, TMPDIR is redirected and the directory is gone with no orphaned processes. All 13 wrapped lanes and the full Vitest suite (5,591 tests) pass with zero residual agent-device-test-run-* directories after the run. Fixes #1595 * test(tmpdir): ratchet every node --test script through the wrapper The 13 lanes wrapped in package.json were a one-time hand sweep with nothing enforcing the pattern going forward — a 14th node --test script added later without scripts/node-test-tmpdir.ts would silently reopen #1595 for that one lane. Add a structural check to scripts/node-test-tmpdir.test.ts (now part of check:tmpdir-leaks:test) that reads package.json and fails if any script invokes `node ... --test` without routing through the wrapper. Dumb string matching over the scripts map, no shell parsing, with an explicit (currently empty) NODE_TEST_WRAPPER_BYPASS_ALLOWLIST for any lane that must legitimately bypass it. Verified it both passes on the current package.json and fails when a synthetic unwrapped `node --test` script is added. * fix(test): preserve the Swift cache and close the raw node --test bypasses Review on #1661 found two gaps: 1. The wrapper only overrode TMPDIR, so it discarded and forced a recompile of the durable Swift compiler cache every run instead of mirroring vitest-tmpdir-global-setup.ts's carve-out for it. Read os.tmpdir() before the child's TMPDIR redirect takes effect and set AGENT_DEVICE_SWIFT_CACHE_DIR from that (only when unset), same as the Vitest lane — the two now share one durable cache instead of each discarding and recompiling their own. Added a probe assertion (scripts/node-test-tmpdir.test.ts) that fails without the fix and passes with it (verified both ways). 2. docs/agents/testing.md documented raw `node --test` commands for the iOS smoke files, and the android/ios/conformance-regenerate/nightly workflows invoked `node --test` directly outside package.json. Routed all of them through scripts/node-test-tmpdir.ts so the documented local commands and CI lanes get the same crash/timeout-safe cleanup the package.json scripts already have.
2026-08-07 13:26:20 +02:00
"test:concurrency-torture": "node --experimental-strip-types scripts/node-test-tmpdir.ts --test test/integration/nightly/concurrency-torture.test.ts",
"test:replay:ios": "node --experimental-strip-types src/bin.ts test test/integration/replays/ios/simulator",
"test:replay:ios-device": "node --experimental-strip-types src/bin.ts test test/integration/replays/ios/device",
fix(scroll): keep saturated scroll gestures out of the status bar; gate Android replays from android/emulator (#1781 A1) (#1820) * fix(scroll): keep saturated scroll gestures out of the status bar; gate Android replays from android/emulator (#1781 A1) `pnpm gate replay-android` failed 4/8 whenever it ran after the full-tier Android E2E (replays-nightly run 32107665052, job 95620294899): 05-app-lifecycle, 06-swipe-gestures and both fixture replays diverged under "A system surface covers the app". The E2E was not the cause. Reproduced on a pixel_7 / API 36 AVD with the same cutout geometry CI's `avdmanager --device pixel_7` produces (status bar 136px, not the 63px of a plain 1080x2400 skin): - `03-scroll-discovery.ad` runs `scroll up 3`. The scroll planner clamps travel to the viewport minus a 5% band, so the touch-down landed at y=120 — inside the 136px status bar — and pulled the notification shade instead of scrolling. On API 36 the app window is edge-to-edge, so the reported viewport starts at y=0 and includes that bar. - The shade then covered every replay until `04`'s `back` closed it. Native readdir order on the runner (03, 05, 06, fixture/02, fixture/01, 04, 01, 02) put four files in that window; the last green run (2026-07-30) had 04 right after 03, so the pull was masked. Fix in the product, not the lane: DEFAULT_EDGE_PADDING_FRACTION 0.05 -> 0.1 in the TS scroll planner and its Swift port. Every real Pixel has a cutout (5.7% of a Pixel 7's height) and an iPhone's Dynamic Island status bar is 6.9%, so any saturated `scroll up` opened the shade / Notification Center for real agents too. Parity vectors updated in both suites plus a Pixel 7 regression vector (1080x2400, amount 3 -> touch-down y=240 > 136). Second contamination the same order exposed once the shade was gone: `fixture/02-selector-routes-covered-diagnosis.ad` is a #1715 reproduction recipe that FAILS BY DESIGN at step 9 (covered-target refusal) and leaves the device in landscape, yet the gate enumerated `test/integration/replays/android` recursively. iOS keeps gate replays in `replays/ios/simulator` and fixture recipes in `replays/ios/fixture`; Android now mirrors that: the six Settings replays move to `replays/android/emulator`, `test:replay:android` points there, and `fixture/` stays E2E-owned (`full:fixture-replays` already runs 01 by path). android.yml and the workflow-evidence fixture follow the path; the replay-compat manifest keeps the historical paths it pins at released tags. Verified live (Pixel 7 geometry, API 36, --retries 0): control run at main head in CI order reproduces exactly CI's 4/8; with the fix, `pnpm gate replay-android` 6/6 in both native and CI order, and `03` leaves Settings on screen (scroll up 3 now touches down at y=240). * test(scroll): drive the TS and Swift scroll-plan parity vectors from one table (#1820 review) The two suites hand-mirrored the same vectors and #1820 had to edit both by hand — the drift class the repo already closes for the tap-point rule via contracts/fixtures/tap-point-policy.json. The scroll vectors (plus both planner constants, pinned behaviourally on a 1000px axis) now live in contracts/fixtures/scroll-gesture.json; scroll-gesture.test.ts and RunnerTests+ScrollGesture.swift iterate it. Verified: vitest 10/10; the four XCTests run on an iOS 26.2 simulator with the unit flag on (Executed 4 tests, 0 failures). Also: test/ci/android-workflow-evidence.json says what it guards. Follow-up for content-safe viewport bounds + discovery order: #1821.
2026-08-18 14:31:54 +02:00
"test:replay:android": "node --experimental-strip-types src/bin.ts test test/integration/replays/android/emulator",
feat: add Linux desktop automation support via AT-SPI2 (#356) * feat: add Linux desktop automation support via AT-SPI2 (Phase 1+2) Add Linux as a first-class platform using AT-SPI2 accessibility framework via node-gtk for accessibility tree snapshots. This mirrors the macOS desktop automation approach using accessibility snapshots. New files: - src/platforms/linux/atspi-bridge.ts: Core AT-SPI2 bridge using node-gtk with lazy loading, recursive tree traversal (max 1500 nodes, depth 12) - src/platforms/linux/role-map.ts: AT-SPI2 role normalization (~100 roles mapped to existing snapshot type conventions) - src/platforms/linux/snapshot.ts: Snapshot entry point with surface, scope, depth, and interactive-only filtering support - src/platforms/linux/devices.ts: Local device discovery for Linux - src/platforms/linux/node-gtk.d.ts: Type declarations for node-gtk Integration: - Extended Platform type with 'linux', backend union with 'linux-atspi' - Wired snapshot into dispatch.ts and snapshot-capture.ts - Added Linux device discovery to dispatch-resolve.ts - Added stub interactor (input actions deferred to Phase 3) - Added 'linux' to CLI --platform flag - node-gtk added as optional dependency (only installs on Linux) https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT * refactor: code review cleanup for Linux platform support - Extract SnapshotBackend type alias to replace repeated string union across 5 files (snapshot.ts, snapshot-capture.ts, session-replay-heal.ts, interaction.test.ts) - Remove duplicate scope/interactive/depth filtering from linux/snapshot.ts — let the existing buildSnapshotState pipeline handle it, same as Android - Extract isDesktopBackend() helper in snapshot-capture.ts to consolidate the "skip mobile semantics" pattern for macos-helper and linux-atspi - Collapse 17 repetitive throw statements in Linux interactor stubs into a linuxStub() factory function https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT * feat: Linux input synthesis, screenshots, and app lifecycle (Phase 3+4) Add xdotool/ydotool input actions (tap, swipe, scroll, type, fill, right/middle click, long press, double click), screenshot capture via grim/scrot, and app lifecycle management (open, close, back, home). Wire Linux interactors with real implementations and fix device discovery order so Linux doesn't displace Android in auto-selection. https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT * refactor: consolidate Linux env detection, simplify input actions - Extract linux-env.ts with cached display server + input tool detection so every action avoids repeated `which` lookups - Add moveTo/clickButton/sendKey helpers to eliminate repeated mousemove boilerplate across 5 mouse actions - Make scrollLinux respect amount/pixels options instead of hardcoded scroll count - Have backLinux/homeLinux reuse sendKey instead of duplicating tool detection https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT * feat: add Linux CI smoke test with Xvfb and AT-SPI2 Add GitHub Actions workflow that boots a virtual X11 display (Xvfb), installs AT-SPI2 accessibility tooling and xdotool, opens gnome-calculator, takes screenshots, and captures an accessibility snapshot. Screenshots are uploaded as artifacts for visual verification. Also adds 'linux' to replay script metadata platforms and a test:replay:linux script to package.json. https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT * fix: remove pre-session screenshot from Linux replay test The replay runner requires an active session before any commands can run. Move the screenshot after the open command that creates the session. https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT * fix: Linux CI — add missing node-gtk build deps and AT-SPI2 env - Add gobject-introspection, libcairo2-dev, build-essential for node-gtk native compilation - Split AT-SPI2 registry start into its own step so it picks up DBUS_SESSION_BUS_ADDRESS from GITHUB_ENV - Set GTK_A11Y=atspi, GTK_MODULES=gail:atk-bridge, NO_AT_BRIDGE=0 to ensure GTK apps expose their accessibility tree on headless CI - Set GSETTINGS_BACKEND=memory to avoid dconf failures - Add node-gtk verification step to catch build failures early https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT * fix: explicitly rebuild node-gtk native module in Linux CI pnpm install silently skips failed optional dependency builds and the pnpm cache may not include the native binary. Force a rebuild after install to ensure the node-gtk .node binding is compiled against the system GI/cairo headers. https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT * fix: use node-pre-gyp directly to build node-gtk from source pnpm rebuild doesn't trigger node-pre-gyp properly for optional deps. Run node-pre-gyp install --fallback-to-build --update-binary directly inside the node-gtk package directory to force compilation when no prebuilt binary exists for the current Node ABI (v127 / Node 22). https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT * refactor: replace node-gtk with Python subprocess for AT-SPI2 node-gtk is a native C++ addon that requires compilation against specific Node ABI versions and GObject Introspection headers. This proved unreliable on CI (no prebuilt binaries for Node 22 ABI v127, silent optional dep build failures, pnpm cache staleness). Replace it with a Python helper script (atspi-dump.py) that uses PyGObject — the reference GObject Introspection consumer. python3-gi is trivially installable on any Linux distro with no compilation step. The Node bridge spawns `python3 atspi-dump.py` and parses JSON output. - Remove node-gtk from optionalDependencies - Remove node-gtk.d.ts type stub - Add atspi-dump.py (~200 lines) doing the same tree traversal - Rewrite atspi-bridge.ts to use subprocess instead of in-process GI - Simplify CI workflow: no more native build deps or rebuild steps https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT * chore: drop pre-installed packages from Linux CI apt-get python3-gi, gir1.2-atspi-2.0, at-spi2-core, and dbus-x11 are already present on Ubuntu GitHub Actions runners. https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT * feat: surface support for Linux, unit tests, stronger CI assertions - Allow --surface desktop and --surface frontmost-app on Linux (previously only macOS could use --surface) - Add unit tests for atspi-bridge (9 tests: JSON parsing, role normalization, null coercion, error handling, arg forwarding) - Add unit tests for role-map (3 tests: common roles, case normalization, PascalCase fallback) - Improve .py script path resolution (walk upward instead of hardcoded relative paths) - CI replay test now asserts snapshot contains calculator UI nodes via is-exists https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT * docs: add cross-platform snapshot traversal contract Document the shared schema, traversal rules, surface semantics, and normalized role types that all snapshot backends (Swift, Python, Android) must conform to. This serves as the single source of truth when adding or modifying platform backends. https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT * fix: update lockfile after removing node-gtk optional dependency pnpm-lock.yaml still referenced node-gtk after it was removed from package.json, causing pnpm install --frozen-lockfile to fail in CI. https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT * fix: address review findings in Linux platform code - atspi-dump.py: use ctx dict for traversal limits instead of globals, fix rect filter (width/height <= 0 should use `or`), add surface validation - input-actions.ts: make sendKey scancodes required to prevent silent no-op on ydotool, fix ydotool longPress/swipe to use click --down/--up - app-lifecycle.ts: use pkill -x (exact match) instead of pkill -f - linux-env.ts: emit diagnostic warning when falling back to xdotool on Wayland https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT * fix(ci): explicitly install all Linux a11y dependencies Ubuntu runners may not have at-spi2-core, python3-gi, gir1.2-atspi-2.0, or dbus-x11 pre-installed. Install them explicitly instead of assuming they exist. Also make the verify step's tree dump non-fatal since no apps are running at that point. https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT * fix: quote multi-word role value in Linux smoke test selector The selector parser tokenizes on whitespace, so `role=push button` was split into two tokens causing a parse failure. Use single quotes inside the selector: `role='push button'`. https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT * fix: use valid selector keys in Linux smoke test appName is not a valid selector key. The supported keys are: id, role, text, label, value, visible, hidden, editable, selected, enabled, hittable. Simplified to use label and role only. https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT * chore: cleanup pass — menubar warning, fix contract doc example - snapshot.ts: emit diagnostic warning when menubar surface is requested on Linux (falls back to desktop silently otherwise) - SNAPSHOT_CONTRACT.md: fix unmapped role example to use a role that isn't actually mapped (was "color chooser" which maps to Dialog) https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT * chore: add Python bytecache to gitignore https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT * feat: harden Linux platform — capability matrix, CI, error handling P0: Add explicit Linux capability matrix with 3-way platform routing (Apple/Linux/Android) in isCommandSupportedOnDevice. Linux now correctly blocks unsupported commands (clipboard, rotate, scrollIntoView, etc.) at capability level rather than throwing at runtime. Includes tests. P0: Expand Linux CI to run typecheck + unit tests before smoke tests. Add AT-SPI2 registry health probe with fail-fast on missing registry. P1: Harden atspi-dump.py — arg parsing now produces JSON errors on bad int values, and a top-level catch wraps unexpected exceptions in JSON. P1: Add 10s per-action timeout to xdotool/ydotool input commands to prevent indefinite hangs. P1: Tighten smoke test selectors to calculator-specific signals (digit labels) instead of generic role='push button'. P2: Document Linux surface mapping, supported commands, and known limitations in SNAPSHOT_CONTRACT.md. https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT * fix: apply depth/interactive filtering to Linux snapshots Linux snapshots were bypassing snapshotInteractiveOnly and snapshotDepth filtering that macOS-helper gets via shapeDesktopSurfaceSnapshot. Route Linux through the same function so snapshot -i and --depth flags work. Renamed shapeMacOsSurfaceSnapshot → shapeDesktopSurfaceSnapshot since it's now shared between macOS and Linux desktop backends. https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT * fix: address review findings — error reporting, Wayland, timeout - app-lifecycle.ts: emit diagnostic on fire-and-forget app launch failure instead of silently swallowing errors - linux-env.ts: make xdotool on Wayland a hard error instead of a broken fallback (xdotool doesn't work on Wayland) - atspi-bridge.ts: increase Python subprocess timeout from 15s to 30s for safety on slow/loaded systems with large a11y trees https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT * feat: appName/windowTitle selectors, clipboard, input-action tests Selectors: - Add appname and windowtitle as selector keys for desktop platforms. Both macOS and Linux snapshots already populate these fields — now they're usable in selector expressions (e.g., "label=OK appname=Calc"). Keys are case-insensitive. Clipboard: - Implement readLinuxClipboard/writeLinuxClipboard using xclip/xsel (X11) or wl-copy/wl-paste (Wayland) with descriptive TOOL_MISSING errors. Enable clipboard in Linux capability matrix. 7 unit tests. Input action tests: - Add 18 unit tests covering xdotool and ydotool code paths: press, right/middle click, double click, sendKey, type, scroll, swipe, focus, fill. Tests mock runCmd and verify correct tool + args. https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT * chore: cache tool detection for screenshot/clipboard, extract get_app_info helper Avoid repeated `which` calls on every screenshot/clipboard operation by caching the resolved tool on first use, matching the input-action pattern. Extract duplicated app_name/pid retrieval in atspi-dump.py into get_app_info. https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-04-04 13:14:04 +02:00
"test:replay:macos": "node --experimental-strip-types src/bin.ts test test/integration/replays/macos",
"test:replay:linux": "node --experimental-strip-types src/bin.ts test test/integration/replays/linux",
"test:linux:command-evidence": "node --experimental-strip-types test/integration/linux-e2e/live-runner.ts"
2026-01-30 19:41:00 +01:00
},
"files": [
"bin",
"dist",
"apple/macos-helper",
"!apple/macos-helper/**/.build",
"android/snapshot-helper/dist",
"!android/snapshot-helper/dist/*.idsig",
"!android/snapshot-helper/README.md",
"android/ime-helper/dist",
"!android/ime-helper/dist/*.idsig",
"!android/ime-helper/README.md",
"linux/atspi-dump.py",
2026-05-11 17:28:04 +02:00
"server.json",
"smithery.yaml",
2026-01-30 19:41:00 +01:00
"README.md",
"LICENSE"
],
"keywords": [
"agent",
"device",
"cli",
"automation",
2026-01-30 19:41:00 +01:00
"adb",
"simctl",
"devicectl",
"ios",
"android",
"tvos",
"macos",
"web",
"browser",
"react-native",
"observability",
"diagnostics",
"network",
"profiling",
2026-05-11 17:28:04 +02:00
"performance",
"mcp",
"model-context-protocol",
"mcp-server",
"ai-agent",
"mobile-automation",
"ios-simulator",
"android-emulator",
"xcuitest",
"e2e-testing",
"cursor",
"claude-code",
"expo",
"mobile-testing",
"qa-automation",
"ai-testing",
"ios-automation",
"android-automation",
"simulator",
"emulator",
"appium",
"maestro",
"detox"
2026-01-30 19:41:00 +01:00
],
"dependencies": {
"@limrun/api": "^0.24.5",
"ipaddr.js": "^2.5.0",
"tar-stream": "^3.2.0",
"undici": "7.29.0",
"yaml": "^2.9.0",
"yauzl": "^3.4.0"
},
feat(ai-sdk): add agent-device/ai-sdk tool set and document the MCP zero-code path (#1804) * feat(ai-sdk): add agent-device/ai-sdk tool set and document the MCP zero-code path Adds `createAgentDeviceTools()` under a new `agent-device/ai-sdk` subpath, built from the same command registry the MCP server uses so both stay in lockstep without a hand-maintained tool list. Introduces a `frameworkTier` descriptor facet ('core' | 'extended') so the factory can default to a curated perceive/act loop instead of handing a model dozens of tools. `ai` is wired as an optional peer dependency, imported lazily inside the factory rather than at module scope, so importing the subpath itself never requires `ai` to be installed - only calling it does. The package's own publishing gate (scripts/lib/shipped-imports.ts) is extended to recognize peerDependencies as a valid resolution source, since this is the first optional peer this package has shipped. Also restructures the AI SDK doc around three tiers (zero-code via @ai-sdk/mcp, the new typed tool set, hand-written tools) and fixes a stale `needsApproval` reference in favor of the current `toolApproval` API. * fix(layering): classify src/ai-sdk as a rank-4 zone The layering guard requires every src/<folder>/ to be explicitly ranked or unranked; the new src/ai-sdk/ subpath (added in the prior commit) was left unclassified, failing CI's Layering Guard job. It sits at the same tier as client/compat/daemon-server/metro/remote/sdk - a public integration surface consuming mcp (3) and core (2), imported by nothing else in the tree. * fix(ci): cover, exempt, and pack the new ai-sdk subpath Fixes the remaining CI failures on the ai-sdk subpath commit: - Coverage: src/ai-sdk/index.ts had no dedicated unit test (only manual/ integration verification), so changed-line coverage sat at 6.9% against the 70% gate. Adds src/ai-sdk/__tests__/index.test.ts (core vs 'all' tool filtering, session/platform pinning and schema hiding, error normalization, toolApproval passthrough) with createCommandToolExecutor and createAgentDeviceClient mocked the same way command-tools.test.ts does, plus a dedicated missing-peer-dependency.test.ts that mocks `ai` itself to throw, isolated to its own file so it doesn't affect the other tests' use of the real, installed `ai` package. Changed-line coverage is now 29/29 (100%). - Fallow Code Quality: src/ai-sdk/index.ts and examples/sdk/ai-sdk-tools.ts are entry points with no in-repo importer (reached only via package.json exports / run directly), and the new subpath's exports are unused internally by design - both need the same treatment src/sdk/*.ts and its examples already have in .fallowrc.json. - Integration Tests: test/integration/installed-package-metro.test.ts and src/__tests__/package-exports.test.ts each hand-list every published subpath and smoke-check it from a real packed install; added ./ai-sdk to both so the new subpath is actually exercised, not just silently passing. * fix(ai-sdk): hide MCP transport/config fields from the model too createAgentDeviceTools() only removed session and mcpOutputFormat from tool schemas. stateDir was still model-visible and reached the shared executor as client configuration, letting a tool call redirect into a different daemon state directory - defeating the "one pinned session" guarantee the factory exists to provide. includeCost and responseLevel are MCP tool-config knobs in the same category, irrelevant to this adapter. Widens the hidden-field set to session/stateDir/mcpOutputFormat/ includeCost/responseLevel, and now strips them from the runtime input inside execute() too (not just the schema), so the guarantee holds even if a caller bypasses schema validation. The schema-properties filter and the input filter now share one omitHidden() helper instead of two near- duplicate implementations. Addresses the P1 review comment on #1804.
2026-08-18 11:57:34 +02:00
"peerDependencies": {
"ai": "^6.0.0 || ^7.0.0"
},
"peerDependenciesMeta": {
"ai": {
"optional": true
}
},
2026-01-30 19:41:00 +01:00
"devDependencies": {
refactor(daemon): extract native .ad replay to packages/ad-replay (#1478 P5) (#1555) * refactor(replay): move the dependency-free engine leaves into packages/ad-replay Stage A of the #1478 P5 extraction: vars, plan-digest (+canonical-json, sole consumer), the target-identity classification core, report-action, and suggestion-ranking move verbatim; imports updated. The package facade temporarily re-exports the moved symbols so root consumers keep compiling; a later stage narrows it to inspectAdReplay/runAdReplay only. * chore(layering): register packages/ad-replay in the workspace and DAG * refactor(replay): define the three-operation replay selector port with dual adapters (#1478 P5) * refactor(daemon): route replay handlers through the selector port (#1478 P5) * refactor(replay): split target verification into engine policy and daemon authority (#1478 P5) * refactor(replay): move the .ad step loop behind inspectAdReplay/runAdReplay (#1478 P5) * refactor(replay): lock the ad-replay façade to its real consumers (#1478 P5) * test(replay): prove shared-id demotion on both selector-port adapters (#1555 review) * fix(replay): restore invalid replayBackend rejection on the native path (#1555 review) * refactor(replay): move shared .ad vocabulary to its owner, packages/ad-script (#1555 review) * refactor(replay): neutral step/run outcomes and digest/resume behind inspectAdReplay (#1555 review) P1 "do not smuggle daemon wire failures through a generic": drop the TResponse generic from AdReplayStepRuntime/runAdReplay. executeStep and handleActionFailure now return neutral tagged AdReplayStepOutcome/ AdReplayStepFailure values (kind/message/artifactPaths only); runAdReplay returns a neutral completed/failed AdReplayRunOutcome. The engine never holds or returns a DaemonResponse. The daemon adapter (createAdReplayStepRuntime, session-replay-runtime.ts) keeps its real wire response in a local side-map as it builds each neutral outcome, and runReplayScriptFile reads it back once runAdReplay reports which step failed, so the final response is byte-identical to before this split. P1 "parsing/planning/digest/resume must also occur behind runAdReplay": relocate computeReplayPlanDigest's call site and the --from/--plan-digest resume-point math (resolveReplayEntryIndex) behind inspectAdReplay's manifest as planDigest and a resolveEntryIndex closure. Neither is a new top-level export -- inspectAdReplay/runAdReplay stay the only two. Timing is preserved exactly (still called eagerly in prepareReplayPlan, before prepareReplaySession's coordinator-mutating side effects) since moving resume validation to run inside runAdReplay itself would let a rejected --from request mutate coordinator/session state first -- a real ordering hazard, not just a cosmetic one. computeReplayPlanDigest/ReplayPlanDigestMetadata/resolveReplayEntryIndex leave the ad-replay façade; request-router-repair-expired.test.ts and prepareReplayPlan read the digest/resume result off the manifest instead. * refactor(replay): relocate classifyTargetBindingMatch and pin the ad-replay façade (#1555 review) P1 "complete the binding façade instead of documenting deviations": classifyTargetBindingMatch never had a real consumer reachable through inspectAdReplay/runAdReplay -- both its callers (the daemon's record-time self-check in session-target-evidence.ts and its replay-time classification wrapper in session-replay-target-classification.ts) are daemon files that imported it directly. It interprets TargetAnnotationV1 evidence semantics shared beyond the engine, so it moves to packages/ad-script alongside target-annotation-identity.ts (new target-annotation-classification.ts + its test), and both daemon call sites now import it from there instead of @agent-device/ad-replay. One deviation remains and is reported rather than papered over per the review's own instruction: the four target-verification policy functions (planPreDispatchTargetVerification, planPostResolutionTargetVerification, deriveReplayTargetGuardMismatchEvidence, deriveWaitLandmarkMismatchEvidence) and the ReplaySelectorPort type family stay exported. Their sole caller, session-replay-target-verification.ts, interleaves these pure decisions with daemon-only async work (capture, SessionStore, coordinator/resume stamping, wire shaping) that must stay outside the engine by design; moving their call sites to live only behind runAdReplay would require restructuring that whole orchestration into new fine-grained AdReplayStepRuntime capabilities, which is out of scope for this pass. See packages/ad-replay/src/index.ts's header comment for the full reasoning. P1 "add the reviewer-required exact exported-symbol gate": adds readNamedExports (scripts/layering/package-boundaries.ts), a small parser over a façade's `export { .. } from`, `export type { .. } from`, and direct-declaration forms, and pins @agent-device/ad-replay's exact 21-symbol export list in package-boundaries.test.ts. Plant-verified: a stray `export const` addition failed the assertion; removed it and the gate went green again. * refactor(replay): drive target verification from the engine step loop (#1555 review) Moves the verify-then-dispatch decision flow into packages/ad-replay's step loop so the four target-verification policy functions (plan{PostResolution,PreDispatch}TargetVerification, derive{ReplayTargetGuardMismatch,WaitLandmark}MismatchEvidence) become engine-private and leave the ad-replay façade. The daemon (session-replay-target-verification.ts) shrinks to the narrow AdReplayStepRuntime capabilities the engine drives: routing (beginTargetVerification), capture (captureObservation), classification (classifyTarget), dispatch (dispatchStep), and wire-building (buildRecordedUnverifiableFailure, buildTargetBindingFailure, buildPostDispatchTargetBindingFailure). Wire output and replay-compat stay byte-identical; the exact-symbol façade gate is updated to the shrunken export list. * refactor(daemon): decompose the replay adapter's two over-threshold functions (#1555) * refactor(replay): fold #1554's keep-session terminal-lifecycle policy into the ad-replay engine Rebasing p5/extract-ad-replay onto main pulled in #1554's --keep-session feature, which had grown its own daemon-side terminal-close-suppression predicate (session-replay-terminal-lifecycle.ts's resolveSuppressedTerminalCloseIndex/countExecutedReplayActions) independently of this branch's own engine-side one (step-loop.ts's isRepairArmedTerminalCloseAction). Both are the same decision family — replay --keep-session and an active --save-script repair now share ONE structural resolution (resolveSuppressedTerminalCloseIndex, generalized to "terminal among EXECUTABLE actions" rather than the old physical-last-index check) and one suppression check inside runAdReplay, gated on keepSession OR runtime.isRepairArmed(). AdReplayRunRequest grew a keepSession field; the neutral 'replayed' count in AdReplayRunOutcome is now computed inline in the loop instead of the daemon's old actions.length - entryIndex approximation. requireLiveSessionForKeepSession (the --keep-session live-session postcondition) stays daemon-side, inlined into session-replay-runtime.ts, since it inspects SessionStore state the engine never sees. The daemon-only session-replay-terminal-lifecycle.ts this arrived with is deleted entirely — its isExecutableReplayAction was a duplicate of the engine's own. runReplayScriptFile's Maestro-format routing (including the new --keep-session Maestro rejection) was extracted into routeMaestroReplay to keep the function under fallow's complexity threshold after re-threading keepSession through it. Added packages/ad-replay/src/internal/__tests__/step-loop.test.ts covering the unified suppression decision (both keepSession and repair-armed) directly against runAdReplay, including the terminal-among-executable-actions case with a trailing nested replay marker. The daemon-level integration tests (6 tests in session-replay-terminal-lifecycle.test.ts, exercising the same behavior through runReplayScriptFile) and the SDK provider-scenario test (active-session-script-publication.test.ts) needed no changes and pass unmodified. * refactor(daemon): decompose session-replay-runtime.ts into three modules (#1555) Splits the ~1096-line replay runtime into cohesive pieces, keeping session-replay-runtime.ts as thin orchestration (~240 LOC): - session-replay-runtime-engine-adapter.ts: the AdReplayStepRuntime adapter (createAdReplayStepRuntime, the build*Failure capability implementations, and the lastResponse/lastObservation side-map mechanics), extracted verbatim. - session-replay-runtime-plan.ts: extended with the plan-side helpers (validateReplayBackendFlag, inspectReplayPlanManifest, resolveReplayPlanEntryIndex, prepareReplayPlan, routeMaestroReplay) alongside the buildReplayMetadataFlags helper already there — buildReplayMetadataFlags is now module-private since its one caller moved into the same file. Also introduces ReplayScriptFileParams, named here (instead of derived via Parameters<typeof runReplayScriptFile>) so routeMaestroReplay can reference the shape without importing back from session-replay-runtime.ts. - session-replay-runtime-session.ts (new): session preparation (prepareReplaySession and its coordinator arming/repair-preflight helpers), extracted verbatim. Coordinator ownership is unchanged: createReplayCoordinator is still constructed only in session-replay-runtime.ts, matching replay-coordinator-ownership.test.ts's allowlist as-is — every extracted module receives the already-constructed ReplayCoordinator as a parameter. Pure move; no behavior change. * test(replay): cover pre-step artifact ordering and resume-before-mutation (#1555) Two invariants found during the P5 decomposition pass now have direct counterfactual-verified coverage: - packages/ad-replay/src/internal/__tests__/step-loop.test.ts: a post-dispatch target-binding mismatch (dispatchWithGuard) must report the accumulated PRE-step artifact snapshot it was called with, never the artifacts the failed dispatch itself produced. Verified red by swapping the buildPostDispatchTargetBindingFailure call to outcome.artifactPaths. - src/daemon/handlers/__tests__/session-replay-runtime-plan.test.ts: a rejected --from/--plan-digest resume must never reach prepareReplaySession's coordinator-mutating writes (the R2 ordering invariant) — a pre-armed repair transaction and corrective-resume watermark are asserted byte-for-byte unchanged after rejection. Verified red by calling prepareReplaySession before honoring the plan-validation rejection. * fix(ad-replay): enforce the exact two-entrypoint facade (#1555 review P1) packages/ad-replay/src/index.ts now exports exactly two value symbols, inspectAdReplay and runAdReplay, and zero types — formatReplaySuccessMessage (presentation) moves beside its one caller in session-replay-runtime.ts, and every type a root daemon file needs is derived structurally off the two entrypoints in the one new src/daemon/ad-replay-facade-types.ts module instead of being named off the façade. scripts/layering/package-boundaries.ts's readNamedExports is rewritten on oxc-parser's own static-export table instead of a regex, so it can no longer silently miss a widening export form: a bare `export *` re-export or an `export default` now throws (an un-enumerable, and therefore un-pinnable, export), while `export * as ns` and every other enumerable form is still counted. The pinned exact-symbol assertion in package-boundaries.test.ts is narrowed to ['inspectAdReplay', 'runAdReplay']. * fix(ad-replay): translate wire failures before the engine boundary (#1555 review P1) AdReplayDispatchOutcome's guard-mismatch/landmark-mismatch variants carried a generic `details: Record<string, unknown> | undefined` bag straight off the wire response — a daemon wire projection crossing into the engine even though the outcome itself was already a neutral type. The daemon adapter (session-replay-runtime-engine-adapter.ts) now narrows that bag into the typed AdReplayGuardMismatchEvidence/AdReplayLandmarkMismatchEvidence shapes (observed identity, expected/observed structural denotation, ancestry entries, match count) before returning the outcome; the unknown-parsing readers move there with the wire-reading responsibility they always were. target-verification.ts's deriveReplayTargetGuardMismatchEvidence/ deriveWaitLandmarkMismatchEvidence now consume only the typed values — no `unknown`-valued record type remains on any engine-crossing signature. * fix(ad-replay): move variable semantics/planning behind runAdReplay (#1555 review P1) The daemon assembled the `${VAR}` scope (buildPreparedReplayScope) and interpolated actions at two independent call sites: dispatch's own (invokeReplayAction) and target verification's separate one (resolveTargetVerificationEntry) — duplicated orchestration the P5 design assigns to the engine. runAdReplay's request now carries the raw scope INPUTS (varSources: plain builtins/file/shell/cli-env data, plus actionLines/actionSourcePaths/ resolvedPath for interpolation-error location) instead of a built scope; the engine builds the scope and resolves each action exactly once per step, handing the RESOLVED action to dispatchStep/beginTargetVerification while every other capability still receives the ORIGINAL recorded action (a target-binding divergence reports the recorded selector, never an expanded ${VAR}). This is the one resolution site now — session-replay-action-runtime.ts's invokeReplayAction and session-replay-target-verification.ts's resolveTargetVerificationEntry no longer hold a scope or call resolveReplayAction themselves. Scrub-value collection (collectReplayScrubbableVarValues, for divergence-report redaction) is kept single-sourced in the engine too: it's computed from the engine's own live scope and threaded to each build-failure/handleActionFailure capability as an explicit scrubVars argument, rather than the daemon recomputing it from a second scope object (which would have gone stale, since expandedBuiltinNames tracking now only happens engine-side). The Maestro replay path's own daemon-side vars usage is unrelated (a different engine) and is out of scope here. * fix(ad-script): make ${VAR} interpolation a linear scanner CodeQL flagged the interpolation regex's fallback group as js/polynomial-redos once vars.ts moved into packages/ (library-input classification): every ${NAME:- prefix of an unclosed input rescanned to end-of-string, quadratic overall — 1,857 ms measured on 20k repetitions of '${A:-['. Replaced with a single-pass scanner; failed fallback scans emit their span verbatim and resume after it (escape-pair alignment is identical from every candidate start inside the span, so no later candidate can terminate where the failed scan could not). Equivalence: 200k-trial differential fuzz against the retired regex over the adversarial alphabet, zero mismatches; both adversarial shapes now resolve in 1-2 ms. * refactor(ad-replay): typed façade replaces the zero-type rule (#1555 structural-quality review) Reverses the exact-two-value zero-type export shape #1555's second review pass established: it forced every root type derivation through one shim (src/daemon/ad-replay-facade-types.ts) and left four daemon-side twin types (TargetVerificationEntry, TargetClassificationOutcome, TargetBindingFailureEvidence, ReplayVerifiedTargetGuard) plus a toDaemonEvidence copy translator shadowing the engine's own shapes. packages/ad-replay/src/index.ts now exports inspectAdReplay/runAdReplay (unchanged, still the only two values) plus the neutral vocabulary their signatures are built from, by name — following packages/maestro's façade precedent. The exact-symbol gate in scripts/layering/package-boundaries.test.ts is widened to pin the full sorted list (values + types). The four daemon twins are deleted; session-replay-target-verification.ts and session-replay-runtime-engine-adapter.ts now use the engine's own AdReplayVerificationEntry/AdReplayTargetClassification/ AdReplayTargetBindingEvidence/AdReplayVerifiedTargetGuard directly. TargetBindingDivergenceBuilt's array fields are now readonly-compatible, so toDaemonEvidence's copy is gone — evidence flows through unchanged. * fix(ad-replay): honor the selector port's own contract in the parse gate target-verification.ts's planPreDispatchTargetVerification used resolveRecordedTarget (operation 2, resolve) over an empty node tree purely to read its parse-invalid reason — a resolve call standing in for a parse call, even though readSelectorExpression (operation 1, parse) exists to answer exactly that question and was already unused inside the engine. Replaced with port.readSelectorExpression('ordinary', [token]). The mapping is not 'invalid' -> skip: production's 'ordinary'/'wait' grammars only ever record a boundary once it has already parsed, so a single malformed token can only come back 'not-applicable' there ('invalid' is unreachable from this call site on the production adapter). Both non-'expression' outcomes map to skip, matching the historical behavior (a single parse-invalid reason covered both cases). platform dropped from the function's params — it was only ever threaded to the resolve call this replaces. Added a contract-suite cell pinning the exact (diverging) discriminant each adapter reports for a selector-shaped-but-malformed bare token, and why the divergence is harmless for the one real consumer. * refactor(ad-replay): split step-loop.ts and shrink the daemon adapter (#1555 structural-quality review) step-loop.ts (810 LOC) splits three ways, following packages/maestro's own precedent: - internal/runtime-port-types.ts: the AdReplayStepRuntime boundary vocabulary (all the neutral types the engine/daemon exchange). - internal/verify-dispatch.ts: verifyAndDispatchStep + its dispatchNoGuard/ dispatchWithGuard helpers. - internal/step-loop.ts: runAdReplay itself plus the terminal-close/ executable-action structural logic (isExecutableReplayAction, resolveSuppressedTerminalCloseIndex). packages/ad-replay/src/index.ts's type exports now source from runtime-port-types.ts. step-loop.test.ts's AdReplayStepRuntime import moves to the new path (no assertion changes). src/daemon/handlers/session-replay-runtime-engine-adapter.ts (553 LOC after item 1's twin removal) shrinks to 294 via two further extractions: - session-replay-dispatch-narrowing.ts: the wire `details` bag -> typed evidence narrowing and dispatch-failure classification. - session-replay-runtime-step-support.ts: ReplayStepContext (moved here to avoid a cycle with the adapter, which re-exports it by name) plus the failure-wrapping/diagnostics-support helpers. Final LOC: adapter 294, dispatch-narrowing 148, step-support 153, step-loop 225, verify-dispatch 246, runtime-port-types 374. * test(ad-replay): package-local tests for resume.ts/target-verification.ts + terminal-lifecycle test rename resume.test.ts covers resolveReplayEntryIndex directly (previously only exercised transitively through the daemon's session-replay-runtime-plan tests): no --from/--plan-digest, the paired-flags requirement, in-range --from, out-of-range rejection, stale-digest rejection, the authorized empty-tail boundary (actionCount + 1) gated on a matching watermark, and the unperformed-record-and-heal growth check. Counterfactual run and restored: widening describeOutOfRangeResumeFrom's bound turns the out-of-range/ empty-tail-without-watermark assertions red (2 failures observed). target-verification.test.ts covers all four engine policy functions directly: the two plan* pre-capture gates and the two derive* post-dispatch evidence builders, including item 2's own new decision surface (a fake ReplaySelectorPort proving both non-'expression' readSelectorExpression outcomes map to skip). Counterfactual run and restored: narrowing the check to the literal `'invalid' -> skip` reading turns the 'not-applicable' case red (reports recorded-unverifiable instead of skip). session-replay-terminal-lifecycle.test.ts renamed to session-replay-runtime-keep-session.test.ts: its production module (session-replay-terminal-lifecycle.ts) was already deleted by the #1554 fold-in, and its six cases drive the full runReplayScriptFile round trip against a real SessionStore (including daemon-only postconditions the engine's step loop never reaches) rather than testing engine policy through the façade in isolation — the engine's own terminal-close-suppression decision already has direct, cheaper coverage in step-loop.test.ts. No assertion changes; both files' header comments cross-reference the split. * refactor(ad-replay): compute scrub values once per step, one name end to end collectReplayScrubbableVarValues(scope) was called fresh at 5 separate return points inside one verifyAndDispatchStep invocation plus once more in handleActionFailure — always the same result, since nothing between them mutates scope. step-loop.ts's runAdReplay now computes scrubVars ONCE per step, right after resolveReplayAction (the one call that can grow the scope's expanded-builtins set), and threads it as a plain readonly AdReplayScrubValue[] value; verify-dispatch.ts no longer imports ReplayVarScope or collectReplayScrubbableVarValues at all. "One name" end to end: the daemon's TargetBindingDivergenceContext.scrubVars and withReplayFailureDiagnostics's scrubVars param used a separately-derived ReturnType<typeof collectReplayScrubbableVarValues> (mutable array) instead of the engine's own AdReplayScrubValue, requiring a [...scrubVars] copy at every daemon call site to satisfy the mutable-array type. Both now use readonly AdReplayScrubValue[]/readonly ReplayVarScrubEntry[] (structurally identical, already readonly-safe downstream — scrubReplayVarValues and createReplayDivergenceSanitizer already accepted readonly arrays), so the four [...scrubVars] copies in session-replay-runtime-engine-adapter.ts are gone. * fix(daemon): make lastObservation genuinely per-step, not per-run createAdReplayStepRuntime's lastObservation closure lives for the whole replay run (one factory call covers every step), but was never reset between steps. Every current buildTargetBindingFailure call site happens to be preceded by this same step's own captureObservation, so the `lastObservation ?? { reason: 'observation-missing' }` fallback could never actually fire — but if it ever did (a future call path reaching buildTargetBindingFailure without capturing first), it would silently attach the PREVIOUS step's screen instead of reporting the missing-capture condition the fallback message claims. armStep runs exactly once per step, before any of that step's other capabilities (verified against step-loop.ts's runAdReplay loop order) — the natural per-step boundary. It now clears lastObservation first. No behavior change on any reachable path today (full daemon + ad-replay suite: 1766/1766 green); an unrelated device-claim-prune contention flake was observed once and did not reproduce on isolated or full-suite reruns. * docs(ad-replay): fix decayed review-changelog comments naming defunct symbols Four comments named symbols/paths that no longer exist, left behind by earlier review passes describing PR history rather than the current constraint: - session-replay-runtime-step-support.ts / session-replay-runtime.ts (2 sites): referenced a function called executeStep, which was never reintroduced under that name after the P5 split — the actual mechanism is the runtime's dispatch/build-failure capabilities recording into the lastResponse side-map. - session-replay-runtime.ts: referenced an engine collectArtifactPaths capability that does not exist — artifactPaths is a daemon-side Set the adapter mutates via collectReplayActionArtifactPaths. - packages/ad-replay/src/internal/selector-port.ts: pointed at ./testing/in-memory-selector-port.ts, the in-memory adapter's pre-stage-D location — it has lived at src/__tests__/test-utils/in-memory-replay-selector-port.ts since. - session-replay-repair-hint.ts / session-replay-runtime-step-support.ts (2 sites): named target-identity.ts, which does not exist (the real file is target-identity-node.ts); the second site additionally mislabeled classifyReplayTarget as engine-side when it is daemon-side (session-replay-target-classification.ts). Comment-only; no behavior change. * refactor(ad-script): move declaredScriptPlatform to its natural shared owner packages/ad-replay/src/internal/inspect.ts's declaredScriptPlatform and src/daemon/replay-device-selection.ts's readScriptReplaySelection each kept their own copy of the same "platform declared before the first open" scan over runtime/open actions — .ad script semantics, not engine or daemon policy, needed independently by ad-replay's plan-digest precedence and the daemon's device-selection platform resolution. Verified this was a genuine duplicate (not the single-sourced state I initially reported): readScriptReplaySelection's platform-tracking loop computes the identical result via a differently-shaped traversal fused with its own app-target scan. resolveDeclaredScriptPlatform now lives in packages/ad-script (its natural owner: the one package both ad-replay and the daemon already depend on, avoiding the R11 issue that justified the original duplication). The daemon's app-target scan stays its own separate pass; fusing it back into the shared function would smuggle a daemon-only concern into ad-script for no measurable cost (the actions array is small, and the shared function already stops at the same point the app-target scan needs to look). * docs(ad-replay): fix package.json description to match the current façade Described "target-identity, variable substitution, plan-digest, and report primitives" — the wide pre-#1555-review façade shape. Vars/identity/report vocabulary moved to ad-script/daemon across the P5 and #1555 review passes; the package now exports exactly inspectAdReplay/runAdReplay plus the neutral AdReplayStepRuntime vocabulary. Description updated to match. * refactor(daemon): fold the step-support fragment back into the engine adapter A simplicity audit judged session-replay-runtime-step-support.ts a size-target fragment, not a concern boundary: four unrelated concerns, one consumer, and a header comment admitting it existed to satisfy the <300 LOC metric. Folded back; the previously-exported helpers are module-private again; the adapter's honest size is renegotiated from the plan metric (dispatch-narrowing stays extracted — it has one nameable job).
2026-08-03 17:25:13 +02:00
"@agent-device/ad-replay": "workspace:*",
refactor: extract the .ad script codec into packages/ad-script (#1478) (#1536) * refactor: extract the .ad script codec into packages/ad-script Moves the mutually-coupled .ad read/write codec (script.ts, script-utils.ts, script-formatting.ts, open-script.ts) plus the target-v1 annotation SERDE slice of target-identity.ts into a new private leaf package, @agent-device/ad-script, exporting only `.`. This is option 1 from the P5 scoping dossier on #1478: the codec is shared by the daemon's session-script publication writer, the future replay engine, the CLI's `replay export`, and Maestro's failure-label formatting, so it can no longer live in root src/ once packages/ad-replay lands (R11 forbids a package reaching into root src), and a second export subpath or writer-half duplication are both ruled out by existing gates/tests. target-identity.ts keeps only the record/replay-shared classification core (classifyTargetBindingMatch, local-identity/ancestry-prefix matching), importing its shared types from the new package. Every real consumer (re-derived by grep, not the dossier's list alone) is rewired to @agent-device/ad-script. Refs #1478 Co-Authored-By: Claude <noreply@anthropic.com> * refactor: trim the ad-script façade to real consumers, lock the one-export boundary - packages/ad-script/src/index.ts: drop parseReplaySeriesFlags, formatTargetAnnotationCommentLine, parseTargetAnnotationCommentLine, TargetAnnotationLineParseResult, and TargetRect from the public façade — none has a consumer outside the package (re-swept every remaining export by grep; everything else kept has at least one real external importer). The functions/types stay exported from their declaring internal modules for the package's own internal use (script.ts, script-formatting.ts). - scripts/layering/package-boundaries.test.ts: add the parallel R11 assertions "the real tree parses, declares, and passes R11" already makes for maestro/provider-webdriver/provider-limrun/xml — ad-script exports exactly `.`, depends on exactly contracts+kernel, and is declared in root package.json — plus ad-script entries in the deep-resolution rejection coverage. Verified the lock catches a regression: temporarily added a fake `./codec` export to packages/ad-script/package.json and confirmed both the export-key-list assertion and the deep-resolution-rejection assertion fail; removed the plant and reconfirmed green. Refs #1478 Co-Authored-By: Claude <noreply@anthropic.com> * fix: remove polynomial-redos ambiguity from the target-v1 annotation line regex CodeQL js/polynomial-redos flagged TARGET_ANNOTATION_LINE_RE (packages/ad-script/src/internal/target-annotation-serde.ts): the payload group's `\s+(.*)` let `\s+` and the unconstrained `.*` both match whitespace, so a run of separator whitespace that ultimately fails to complete the match has many `\s+`/`.*` splits to backtrack through before concluding failure. Anchor the payload group on `\S` (the exact complement of `\s`), so the mandatory `\s+` separator and the payload's first character can never overlap — the split point becomes unique and no backtracking is possible. Behavior-preserving: the only caller (parseTargetAnnotationCommentLine) always matches against an already-.trim()-ed line, whose last character (whenever the tag matches at all) is never whitespace — so a payload section `\S.*` would reject (content that is entirely whitespace) can never reach this regex through the real call path. Verified against the frozen replay-compat corpus and the full serde/parser test suites, unmodified. Added a regression test with the exact adversarial shape CodeQL/the reviewer cited (many tab pairs after the version digits), asserting sub-second parse. Refs #1478 Co-Authored-By: Claude <noreply@anthropic.com> * test(ad-script): pin the annotation-line pattern's linear rejection directly The entry-point adversarial case matched greedily even with the retired regex (trim strips edge whitespace and per-line input carries no newline), so it proved nothing about the pattern. The regression surface is the pattern itself: an interior tab run with an x-newline tail fails the match, which the retired form re-split quadratically (3.7s at 100k tabs) and the \S anchor rejects in one attempt. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-08-01 20:21:38 +02:00
"@agent-device/ad-script": "workspace:*",
feat(ai-sdk): add agent-device/ai-sdk tool set and document the MCP zero-code path (#1804) * feat(ai-sdk): add agent-device/ai-sdk tool set and document the MCP zero-code path Adds `createAgentDeviceTools()` under a new `agent-device/ai-sdk` subpath, built from the same command registry the MCP server uses so both stay in lockstep without a hand-maintained tool list. Introduces a `frameworkTier` descriptor facet ('core' | 'extended') so the factory can default to a curated perceive/act loop instead of handing a model dozens of tools. `ai` is wired as an optional peer dependency, imported lazily inside the factory rather than at module scope, so importing the subpath itself never requires `ai` to be installed - only calling it does. The package's own publishing gate (scripts/lib/shipped-imports.ts) is extended to recognize peerDependencies as a valid resolution source, since this is the first optional peer this package has shipped. Also restructures the AI SDK doc around three tiers (zero-code via @ai-sdk/mcp, the new typed tool set, hand-written tools) and fixes a stale `needsApproval` reference in favor of the current `toolApproval` API. * fix(layering): classify src/ai-sdk as a rank-4 zone The layering guard requires every src/<folder>/ to be explicitly ranked or unranked; the new src/ai-sdk/ subpath (added in the prior commit) was left unclassified, failing CI's Layering Guard job. It sits at the same tier as client/compat/daemon-server/metro/remote/sdk - a public integration surface consuming mcp (3) and core (2), imported by nothing else in the tree. * fix(ci): cover, exempt, and pack the new ai-sdk subpath Fixes the remaining CI failures on the ai-sdk subpath commit: - Coverage: src/ai-sdk/index.ts had no dedicated unit test (only manual/ integration verification), so changed-line coverage sat at 6.9% against the 70% gate. Adds src/ai-sdk/__tests__/index.test.ts (core vs 'all' tool filtering, session/platform pinning and schema hiding, error normalization, toolApproval passthrough) with createCommandToolExecutor and createAgentDeviceClient mocked the same way command-tools.test.ts does, plus a dedicated missing-peer-dependency.test.ts that mocks `ai` itself to throw, isolated to its own file so it doesn't affect the other tests' use of the real, installed `ai` package. Changed-line coverage is now 29/29 (100%). - Fallow Code Quality: src/ai-sdk/index.ts and examples/sdk/ai-sdk-tools.ts are entry points with no in-repo importer (reached only via package.json exports / run directly), and the new subpath's exports are unused internally by design - both need the same treatment src/sdk/*.ts and its examples already have in .fallowrc.json. - Integration Tests: test/integration/installed-package-metro.test.ts and src/__tests__/package-exports.test.ts each hand-list every published subpath and smoke-check it from a real packed install; added ./ai-sdk to both so the new subpath is actually exercised, not just silently passing. * fix(ai-sdk): hide MCP transport/config fields from the model too createAgentDeviceTools() only removed session and mcpOutputFormat from tool schemas. stateDir was still model-visible and reached the shared executor as client configuration, letting a tool call redirect into a different daemon state directory - defeating the "one pinned session" guarantee the factory exists to provide. includeCost and responseLevel are MCP tool-config knobs in the same category, irrelevant to this adapter. Widens the hidden-field set to session/stateDir/mcpOutputFormat/ includeCost/responseLevel, and now strips them from the runtime input inside execute() too (not just the schema), so the guarantee holds even if a caller bypasses schema validation. The schema-properties filter and the input filter now share one omitHidden() helper instead of two near- duplicate implementations. Addresses the P1 review comment on #1804.
2026-08-18 11:57:34 +02:00
"@agent-device/capture-kit": "workspace:*",
"@agent-device/contracts": "workspace:*",
refactor(substrate): split host mechanics into @agent-device/host-kit capability ports (#2088) * refactor: split generic host mechanics into @agent-device/host-kit (#2082 W1) The shared src/utils closure that blocked the platform-family moves lands on declared owners: generic host mechanics form a new private @agent-device/host-kit package between kernel and capture-kit, and capture-kit keeps capture, snapshot, and recording behavior, depending on host-kit for the mechanics it needs. tar-stream and yauzl move with the archive code. Every seam's exported subpaths are pinned in package-boundaries.test.ts, the layering model ranks the new zone, R13's allow-list names it, and each seam carries an exact eager-closure row. ADR-0019's substrate amendment describes the layout. Tests that mocked two of the moved modules separately became duplicate same-seam vi.mock factories, where the second silently replaced the first; those are merged, and the mocks that production code reaches past are pinned at their injection points instead. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * refactor(host-kit): one narrow capability port per export The four technical barrels (exec/fs/values/request) grouped by category rather than by capability, so a consumer needing one mechanic evaluated unrelated ones. Each export is now a single capability over the host machine: command, process, diagnostics, retry, archive, file, request, version. A port re-exports only what a consumer of that capability uses, and every port carries its own eager-closure row. Most of the old values barrel was never host mechanics. Pure record readers, config-source values, result text, memoization, async scoping, coordinate validation, and device-scope parsing touch no process, file, or environment, so they join kernel's other primitives instead. Closures fall accordingly: capture-kit's png-worker-client from 20 to 10, png-resize from 28 to 18, session-teardown from 79 to 68, and the CLI from 386 to 380. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * chore: drop the migration inventories and trim the touched comments Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * docs: trim the touched host-kit and mutation-lane comments Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * docs: keep tool directives only in the touched files Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * docs: keep tool directives only across the touched tree Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * fix: point the Swift parity comment at the real TS twin and test The W1 move rewrote this citation to packages/contracts/src/mobile-snapshot-semantics.ts, which does not exist: the module went to capture-kit while isTapPointInsideViewport itself went to packages/contracts/src/snapshot-visibility.ts. The TS test line was left pointing at the pre-move path. Both now resolve. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * fix: repoint comment citations at the homes this refactor moved them to The W1 move left ~20 comment citations pointing at src/utils/*.ts and src/request/*.ts paths that no longer exist. Each now names the capability port that owns the symbol, which survives further file moves: exec -> host-kit/command host-process, owner-identity -> host-kit/process diagnostics -> host-kit/diagnostics atomic-file, process-lock -> host-kit/file retry -> host-kit/retry request progress/cancel -> host-kit/request version -> host-kit/version ttl-memo, source-value, parsing, device-isolation, keyed-lock, success-text -> kernel subpaths Comment-only; no closure, budget, or behavior change. ADR citations are left as written, being dated records of the decision rather than live references. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-08-28 07:46:48 +02:00
"@agent-device/host-kit": "workspace:*",
refactor: pnpm workspace + @agent-device/kernel pilot (#1490 W0) (#1494) * refactor: pnpm workspace + @agent-device/kernel pilot (#1490 W0) Extend the workspace with packages/* and move the kernel behind an enforced public API: packages/kernel with nine consumer-earned subpath exports (errors, device, snapshot, contracts, collections, rect, redaction, daemon-error, bounds — the last absorbed from utils as Rect vocabulary). Every kernel import repo-wide becomes the @agent-device/kernel/<sub> specifier; kernel tests move to src/__tests__/kernel/ and exercise the package surface. The root declares the package in devDependencies (workspace:*), tsdown bundles it (noExternal) so the published artifact and its runtime dependency manifest are unchanged. Gate rewiring in the same change, per the W0 brief: - R1 kernel-sink retires (physically subsumed); new R11 package-boundaries guards no-root-back-imports, relative tunnelling past exports maps, undeclared workspace deps, and non-exported subpaths, with runtime resolution pins via import.meta.resolve. - resolveImportEdges and mutation ownership follow workspace specifiers through exports maps, keeping R4 cycle checks, depgraph, and derived test ownership connected across the seam (kernel-errors still owns 495 tests). listSourceFiles includes packages/*/src. - kernel becomes an unranked zone; mutation registry, stryker mutate globs, and the mutation-affected workflow path filter move to packages/kernel/src/errors.ts. - check:affected gains packages/ ownership (manifests fail open); vitest and coverage include packages/*/src; fallow ignores packages/** (its resolver cannot follow workspace specifiers). - The affected-selector CI job installs dependencies: its closure now crosses workspace specifiers, and the R8 relative exception is unsafe for production src files (Node ESM does not realpath, so dual specifier/relative loads would instantiate modules twice). The R8 zero-dep set is pinned empty with that rationale. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep * fix: address W0 review — mutation sandbox, exports-map resolution, tsc -b Review findings on #1494, all five: 1. contracts-schema-public.test.ts reads the kernel source at its packages/ path (fs access invisible to the codemod and typecheck). 2. Mutation lane: Stryker sandboxes the tree but pnpm's node_modules symlink resolves @agent-device/* back to the real repo, so mutants in the sandbox never load and vitest.related finds no tests. vitest.mutation.config.ts now aliases each EXPORTED specifier to its source (derived from exports maps, never a wildcard), keeping resolution inside the mutated tree. Validated: kernel-errors module runs end to end (dry run 3,984 tests, mutants killed, exit 0). 3. Layering/depgraph resolve workspace specifiers through the exports-derived map (workspaceSpecifierTargets) instead of reconstructing paths, so '.'-facade packages resolve; the positional fallback remains only for map-less fixtures (P0 pin). 4. Per-package project references implemented: packages/kernel is composite (emitDeclarationOnly -> dist-types, gitignored), the root references it, and typecheck becomes tsc -b — probed to catch type errors on both sides under TypeScript 7 native. 5. R11's relative-route exception now requires membership in an actual R8 zero-dep job closure (zeroDepClosureFiles walks entries), not mere scripts/ placement — closing the dual-instantiation bypass. Also from review discussion: daemon-error moves out of the kernel package to src/client/ — its consumers (cli, client facade) rehydrate wire DaemonErrors client-side; the daemon only produces them. Kernel drops to 8 exported subpaths before any of them ship. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep * refactor: one exports-map reader for mutation alias and ownership Fallow flagged workspaceExportAliases (cognitive 15, CRAP 90). The manifest-reading logic already exists as workspaceSpecifierTargets in scripts/layering/package-boundaries.ts, so both the Stryker sandbox alias table and the mutation ownership walker now consume it instead of carrying near-clones. Behavior unchanged; mutation suite 45/45 and changed-code fallow green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep * fix: composite kernel without a root references edge FreeRange runs plain `tsc -p tsconfig.json`, and a root `references` entry makes non-build-mode TypeScript demand the referenced project's built declarations (TS6305) — a standing "build first" tax on every plain -p consumer (fr, editors). Keep the per-package composite project and build it in typecheck (`tsc -b packages/kernel` before the root and examples/sdk passes), but drop the root references edge: root consumption resolves through exports to source, identical to runtime and to the bundler. Probed: plain -p green with no prebuilt output; kernel-side type errors still caught by its own build. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep * fix: R11 uses the layering parser; mutation config is a fallow entry Review blockers on #1494: - R11's private single-quote regex could miss a double-quoted or re-export route into packages/*/src. specifierSites now delegates to the layering model's parseImports (both quote styles, side-effect imports, re-exports, dynamic imports), with direct regressions for each formerly-invisible form. - vitest.mutation.config.ts becomes a declared fallow entry instead of a tolerated unused-file finding: the full-repo audit now reports it reachable (unused files 2 -> 1; the remainder predates this PR). FreeRange clean-checkout evidence: with packages/kernel/dist-types and every *.tsbuildinfo deleted, `pnpm check:freerange` reports 0 findings on this head — the TS6305 topology died with the root references edge in the previous commit; check:freerange has no build precondition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-07-30 12:12:46 +02:00
"@agent-device/kernel": "workspace:*",
"@agent-device/maestro": "workspace:*",
"@agent-device/platform-android": "workspace:*",
"@agent-device/platform-apple": "workspace:*",
"@agent-device/platform-harmonyos": "workspace:*",
"@agent-device/platform-linux": "workspace:*",
"@agent-device/platform-vega": "workspace:*",
"@agent-device/platform-web": "workspace:*",
"@agent-device/provider-limrun": "workspace:*",
"@agent-device/provider-webdriver": "workspace:*",
refactor(platforms): sink the shared src/platforms root files into their substrate homes (#2100) * refactor(platforms): sink the shared src/platforms root files into their substrate homes (#2082 W3) The shared files left directly under src/platforms move onto a declared owner: provisioning mechanics (install-source family, toolchain probing, boot-failure classification, app-resolution caching) form @agent-device/provision-kit above capture-kit; host mechanics resolve to host-kit's seams; kernel takes the pure numeric helpers; contracts keeps vocabulary only. Settings parsing, command-attempt rendering, and the unsupported-interactor factory stay with their families rather than pooling in a substrate package: android and apple settings each own their parsing, and the unsupported-interactor factory lives in root core with a vega-local copy. A platforms-root-shape rule rejects any new shared file or directory appearing directly under src/platforms, and the provision-kit direction gates (no platform imports in, no capture-kit importer) are planted red. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * test: cover the family-owned parsers and the unsupported-interactor factories The settings parsers and the unsupported-interactor factory arrived without owning tests, so their branches rode on callers. Each now has one: the appearance/state parsers over every accepted spelling and their rejections, the attempt summarizer over its arg join and stderr budget, and both interactor factories over the whole operation surface and the per-instance label. Also drops the duplicate ./snapshot-desktop-projection export key that a rebase left in capture-kit's manifest, where JSON silently keeps the last, and the root-shape docblock the violation message already states. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * docs: keep tool directives only in the touched files Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-08-28 07:46:50 +02:00
"@agent-device/provision-kit": "workspace:*",
refactor(replay-test): extract packages/replay-test (#1478 P3b) (#1525) * refactor(replay-test): source the manifest device vocabulary from the kernel `session-test-types.ts` reached `ReplayScriptMetadata['platform']` and `['target']` through `replay/script.ts` — the native `.ad` engine. A format-neutral scheduler must not name an engine module, and P5 relocates that engine into `packages/ad-replay` regardless, so the import had to go before the scheduler can move. Both members already resolve to neutral kernel types (`Exclude<PlatformSelector, 'web'>` and `DeviceTarget` from `@agent-device/kernel/device`), so this re-sources them directly and the manifest shape is unchanged. Only the import direction differs. First increment of P3b; the scheduler still has request-global, engine and daemon imports to port before the physical move. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8 * refactor(replay-test): inject the progress sink instead of reading a request global The scheduler called `emitRequestProgress` in eight places, which reads a sink out of a request-global `AsyncLocalStorage`. That is ambient authority a format-neutral scheduler cannot hold once it lives in `packages/replay-test`, and #1505 recorded it as a shrink-only R10 entry. The host now injects the capability through the existing `ReplayTestRuntimeDependencies` seam established in P3a, so no new seam is invented. `session-replay.ts` supplies `emitProgress: emitRequestProgress`; `src/request/progress.ts` keeps the sink and its AsyncLocalStorage binding for every other caller. The port is deliberately narrower than `RequestProgressSink`: it accepts only `ReplayTestSuiteProgressEvent | ReplayTestProgressEvent`, so the scheduler is not handed the ability to emit `CommandProgressEvent`. Authority narrows again one hop down: `runReplayTestAttempt` spread the whole dependency bag but uses three of its members and never publishes progress, so it now takes `Pick<..., 'runReplay' | 'cleanupSession' | 'finalizeAttempt'>`. That is why no runtime test fixture needed changing — the attempt runtime never gained the capability in the first place. Also drops the last two `replay/script.ts` type references from `session-test-runtime.ts`, so the engine import is gone from that file too. Reporter contract preserved: `session-test-reporter-values.test.ts` and `session-test-reporter-values-maestro.test.ts` both pass unmodified (27 tests green across the five scheduler suites). Typecheck clean. Remaining scheduler boundary for P3b: `request/cancel.ts`, `replay/format.ts`, `replay/script.ts` in discovery, `session-store.ts`, `daemon/types.ts`, `replay-source-discovery.ts`, `core/dispatch*`, `utils/diagnostics.ts`. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8 * refactor(replay-test): ask the host whether the suite is canceled The scheduler called `isRequestCanceled(requestId)` in five places. That both reaches a request-global registry and forces the scheduler to name a daemon request id as the cancellation key — neither survives the move into `packages/replay-test`. The host now binds the predicate to its own request and passes `isCanceled: () => boolean`. The scheduler asks a question it is entitled to ask and learns nothing about how cancellation is tracked. `shouldStopReplayTestExecution` takes the capability rather than a request id, so no scheduler function threads a daemon identifier for this purpose any more. `session-test-attempt.ts` and `session-test.ts` no longer import `request/cancel.ts` at all. It remains in `session-test-runtime.ts`, which does something different — `registerRequestAbort`, `markRequestCanceled` and the parent-abort relay are cancellation *binding*, which the brief assigns to the daemon adapter, so that split is its own step. Behavior preserved: both pinned reporter characterizations pass unmodified, 32/33 across the five scheduler suites. The one failure is the pre-existing P2/#1506 discovery-ordering regression, unrelated and untouched here. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8 * refactor(replay-test): drop the dead request-tracking call from attempt ids `buildReplayTestAttemptRequestId` wrapped its template in `resolveRequestTrackingId`, pulling `request/cancel.ts` into the scheduler. That wrapper substitutes a generated id only when its first argument is an empty string. The template here always contains `:test:`, so it is never empty and the wrapper always returned it unchanged — the call is unreachable in this path. Probed all three shapes (explicit request id, suite-id fallback with a shard, and degenerate empty inputs); every one returns the template verbatim. Removing it takes `request/cancel.ts` out of discovery without altering a single produced id. The scheduler mints attempt identity itself, which is what the brief asks for. Evidence the ids are byte-identical: the pinned reporter characterizations assert exact session strings such as `default:test:suite-reporter:1-02-retry:attempt-1` and pass unmodified — 30 tests green across the reporter, suite and discovery suites. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8 * refactor(replay-test): move cancellation binding and diagnostics to the host `session-test-runtime.ts` held the last two request-globals in the scheduler: `request/cancel.ts` (registerRequestAbort, markRequestCanceled, clearRequestCanceled, plus the parent-abort relay) and `utils/diagnostics.ts`. These are different in kind from the earlier ports. The brief gives the daemon adapter the job of mapping an attempt id to daemon request identifiers and *binding cancellation*, while timeout policy stays scheduler-owned. So the scheduler now receives a per-attempt capability with exactly two verbs — `cancel()` on timeout and `release()` when the attempt settles — and every registry interaction, including `relayReplayTestAbortFromParent`, moved to `session-replay.ts` next to the rest of the adapter. Diagnostics became a narrow publish capability for the same reason: `emitDiagnostic` reads a request-global scope. The level vocabulary is spelled out at the seam rather than imported, so nothing engine- or daemon-shaped crosses it. The runtime fixtures drive the real exported host binding rather than a stub. They assert cancellation through `isRequestCanceled`, and a stubbed binding would have kept those assertions passing while proving nothing. 24 tests green across the runtime, suite and both reporter characterizations, which pass unmodified. Typecheck, lint and oxfmt clean. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8 * refactor(replay-test): split discovery into host inspection and scheduler policy discoverReplayTestEntries expanded paths, read every file, and called both engines — readReplayScriptMetadata for .ad, inspectMaestroFlow for Maestro — plus resolveReplayFormat to choose between them. Four imports a format-neutral scheduler cannot hold. Inspection is now the host's discoverSources capability. What stays in the scheduler is the genuinely neutral half: which sources a --platform filter runs, which it skips and with what message, and the empty-suite error. The manifest carries exactly the four fields the scheduler consumes (platform, target, retries, timeoutMs) plus the reporter's title, per the brief's instruction not to add more without a demonstrated call site. The platform tag is what removes the last format leak. The filter used to ask resolveReplayFormat(...) === 'maestro' to decide whether a missing platform was disqualifying. It now reads a tag: caller-bound means the invocation supplies the platform, unspecified means the source declared none. Maestro is what caller-bound looks like from the scheduler's side, and the format cannot be recovered from it. Discovery tests drive the real inspection capability, writing actual .ad and Maestro sources — a stubbed host half would have kept them green while proving nothing about the composition they exist to pin. 35 tests green across discovery, suite, runtime and both reporter characterizations, which pass unmodified. The Maestro one is the direct check that titles still flow, since they now arrive via the manifest. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8 * refactor(replay-test): build attempt ids from named segments; trim comments Review feedback on the attempt-id builder: the comment explained a deletion that git already records, and it sat above an opaque template literal. The id is now a segment list joined on ':', so its shape is readable without prose. Output is byte-identical — the reporter characterizations assert exact session and attempt strings and pass unmodified. Applied the same standard to four other docblocks in this PR that narrated what the code used to do rather than what it does. The durable 'why' stays: which side of the seam owns what, and why the vocabulary is neutral. The migration history goes, since git carries it and these docblocks will outlive the migration. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8 * refactor(replay-test): move shard device binding to the host buildReplayTestShardPlan called listDeviceInventory to discover what to shard across, and buildReplayTestShardFlags constructed daemon CommandFlags for the nested request. Inventory enumeration, allowlists, simulator set paths, explicit --device selectors and the too-few-devices error are host concerns; what is scheduler-owned is deciding how many shards exist and which entries each one runs. The scheduler now receives resolved shard targets through a capability. The target is neutral: id and name for session labels and progress metadata, plus platform and target, which are already kernel vocabulary. DeviceInfo no longer crosses into scheduling. One behavior note: an explicit --device selector could in principle name a web target, which is not a shardable device. That is now rejected with INVALID_ARGS rather than widening the neutral platform vocabulary to carry something the scheduler can never run. Implicit selection already filtered to mobile. 919 of 920 handler tests pass. The one failure, session-test-runner.test.ts 'binds each replay script to its declared platform metadata', fails identically on clean origin/main in this container and is unrelated: directory discovery walks with opendirSync/readSync and directory results are deduped but not sorted, while glob results are sorted, so suite order is filesystem-dependent. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8 * refactor(replay-test): extract packages/replay-test behind a façade Completes the P3b extraction. The scheduler, attempt runtime, discovery policy, sharding distribution, artifacts and neutral types now live in packages/replay-test/src/internal/, with one package-root export. The façade takes a neutral ReplayTestSuiteRequest and returns a tagged ReplayTestSuiteOutcome. DaemonRequest, DaemonResponse and CommandFlags no longer reach the scheduler; the adapter translates flags and meta in, and the outcome back to a daemon response. Eight flags were read by the scheduler and each became a field it owns. Host work moved to daemon adapters: source inspection (both engines and format routing), shard device binding and shard-flag parsing, and artifacts-dir home expansion, which is why the package can resolve paths without SessionStore. The one remaining shared concern was the timing trace: the host writes video lifecycle events into the same trace the scheduler owns. Rather than export a writer from the façade, each attempt hands the host an appendTimingEvent closure, so the trace format stays private and the authority is scoped to that attempt. Tests mirror the topology. Discovery tests split along the seam they now cross: ordering, traversal and routing are pinned host-side against real files, filtering policy is pinned in the package against fake sources. The runtime tests assert the scheduler's cancellation obligation (cancel once on timeout, always release) against a recording binding, and a new daemon test pins the adapter's half — registry entries, the parent-abort relay, and detach on release — so that coverage moved rather than disappeared. R10 retargeted to packages/replay-test/src/ and the zone ranked alongside maestro. R11 confirms zero root-src imports from the package. 914 of 915 handler and package tests pass. The one failure, session-test-runner 'binds each replay script to its declared platform metadata', fails identically on clean main here: directory discovery walks with opendirSync and dedupes without sorting, while globs sort, so suite order is filesystem-dependent. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8 * refactor(daemon): simplify replay-test request translation Fallow flagged toReplayTestSuiteRequest at 14 cyclomatic in 18 lines. The branches were self-inflicted: every req.flags?.x is one, and each optional field was written as a conditional spread to avoid setting an undefined key. exactOptionalPropertyTypes is not enabled, so assigning undefined to an optional field is equivalent and the spreads bought nothing. Destructuring flags once and extracting two flag readers removes most of the rest. One correctness note on the simplification itself: the first version used `artifactsDir && expandHome(...)`, which returns '' for an empty-string flag where the previous code called expandHome(''). Replaced with an explicit undefined check so the empty-string path is unchanged. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8 * test(live): share the replay test-suite harness across iOS and Android Both live journeys invoked the public test command and then re-derived the same value-contract assertions by hand — suite totals, per-script status, replay counts, non-empty JUnit. Those are claims about the published suite result and are identical on every platform, and they had already drifted: iOS iterated with readReplayCommands inline, Android cast data.tests at the call site. The shared helper owns exactly that boundary. It takes the caller's runStep rather than binding a context type, so it is not a platform-configured runner and cannot template a platform's journey. Everything a platform genuinely differs on stays with the caller: which scripts run, the retry policy (iOS 2, Android none — itself a claim worth keeping), which commands each script exercises, and the behavioral evidence. Both callers keep every verify* call they had. 67 lines removed, 15 added. Residual risk: this container has no iOS or Android devices, so the live suites could not be executed here. Typecheck and lint pass; the harness needs a run on real targets before the claim that behavior is unchanged is evidence rather than inference. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8 * test: pin directory enumeration in the platform-binding suite test The test wrote two scripts into a temp directory and assumed discovery would return them in creation order. Directory expansion deliberately preserves filesystem order to match Maestro — only glob expansion sorts, and 'preserves Maestro directory filesystem order' pins that with a mocked opendirSync. So the ordering contract is correct; this test's assumption about enumeration was not. It passes on CI, where small directories usually enumerate in creation order, and fails on filesystems that do not — identically on clean main, where the platform-to-script binding appears reversed. Pinning enumeration the way the discovery tests already do keeps the subject intact (each script binds to ITS declared platform, and session numbering follows discovery order) without depending on the host filesystem. The fs import became a default import because vi.spyOn cannot redefine an ESM namespace export. 915 of 915 handler and package tests now pass here. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8 * test: scope the enumeration spy and restore it in a finally The spy I added restored only on the happy path and asserted on its argument inside the mock implementation. Either would misfire for anything else sharing the worker: an assertion thrown from inside fs, or a leaked global opendirSync, surfaces as a worker crash with no failed test rather than a readable failure. It now delegates to the real implementation for any directory but this suite's own, and restores in a finally. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8 * fix(replay-test): put package tests where they are actually run Review found the moved package tests were neither executed nor typechecked. They sat under packages/replay-test/test/, but vitest's unit-core lane includes packages/*/src/**/*.test.ts, and neither the root nor the package tsconfig covers a top-level test directory. A plain unit-core run discovered zero files under the package. That is why they looked green: my earlier runs passed those paths explicitly on the command line, which masked that the default run skipped them. The count is the proof — 550 files/4741 tests before, 553/4753 now, and the delta is exactly the three files and twelve tests that were being skipped. The runtime test also imported runReplayTestAttempt from the package specifier, which the facade does not export. It would have failed the moment it was discovered. It now imports internally, like the rest of the internal tests. Also removed replayTestAttemptFailure from the facade: zero consumers outside the package, so exporting it widened the boundary for nothing. P3 asks for a one-function facade. 553 test files and 4753 tests pass; lint and the layering guard are clean. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8 * style: format the facade after removing the unused export A scripted edit removed the export line but left a stray blank line; oxfmt was not re-run on that file afterward, so Lint & Format caught what pnpm lint alone does not. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8 * fix(replay-test): typecheck the package and fix a type-only import Review found the moved package tests were transpiled by vitest but never typechecked: the root typecheck script builds six packages via tsc -b and packages/replay-test was not among them, so its tsconfig was never used. That hid a real TS2459. session-test-runtime.test.ts imported ReplayTestAttemptOutcome from ../session-test-runtime.ts, which imports that type but does not re-export it. It now imports from ../session-test-types.ts, where the type is defined. Adding the package to the tsc -b list closes the gap. Verified empirically rather than assumed: planting a string-to-number error in a package test makes typecheck fail, and removing it makes it pass. This is the second finding of the same shape on this PR — first the tests were not discovered by vitest, now they were not covered by typecheck — so the gate was confirmed to reach the files rather than trusted to. 12 package tests pass, lint, format and the layering guard are clean, and typecheck is clean with the package included. Refs #1478 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8 --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-07-31 20:16:18 +02:00
"@agent-device/replay-test": "workspace:*",
"@agent-device/selectors": "workspace:*",
"@agent-device/xml": "workspace:*",
build: verify the published tarball instead of grepping the bundle (#1578) * build: verify the published tarball instead of grepping the bundle Replaces the bundle-dependency grep with one gate that packs the tarball npm would publish and proves it sound from a clean consumer install: publint and attw on the tarball, a two-way dependency-closure audit, an import of every `exports` subpath, and the CLI smoke run — all from outside the workspace, where no pnpm link can mask an unresolvable specifier. Also stops the build from emitting a publishable bundle in the first place: a missing workspace link now fails `pnpm build` instead of warning and exiting 0, which is how 0.20.4 shipped an unresolvable `@agent-device/ad-script` import. publint found 12 real defects in the current package — every `exports` entry listed `types` after `import`, so TypeScript resolved declarations by accident rather than by condition. The dependency audit found `pngjs` declared as a runtime dependency while tsdown inlines it, an install every user paid for and no shipped code reached; it moves to devDependencies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(ci): run the package gate without pnpm on the Node floor pnpm 11.17 requires Node >= 22.13, so `pnpm check:package` could not start on the 22.12 floor the Packaged CLI job exists to cover. The gate needs only `node` and `npm`, so the job invokes the script directly. Splits the dependency-closure audit into a collector and a message builder to clear Fallow's complexity threshold, and classifies both packaging linters in ignoreDependencies: they are subprocess CLIs with no importable API here, which dependency analysis cannot follow to an import. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(publishing): read every literal resolution form in the closure audit The dependency-closure audit derived shipped imports from the ESM module record alone, so it could not see a package resolved through `require` or a `createRequire` result: neither produces a module-record entry. A lazy `createRequire('@agent-device/…')` would therefore clear the audit, the all-export probe and the exercised CLI paths, reintroducing the 0.20.4 published-install failure class for another command. Measuring the built bundle turned up a second, larger hole in the same reader. The shipped files are minified, and the minifier rewrites every string literal to a no-substitution template literal, so the dynamic-import extraction — which accepted quoted strings only — matched 0 of the 99 dynamic imports the bundle contains. The lazy `import()` path that broke 0.20.4 was reported as covered while checking nothing. Specifiers now come from the module record plus an AST walk over every literal runtime-resolution form: `import()`, `require()`, `require.resolve()`, an immediately-invoked `createRequire(...)`, and calls through a `createRequire` result under any import or minified alias. Both spellings of a string literal count everywhere, and `.cjs` joins the scanned extensions. Computed specifiers stay explicitly out of scope, and are pinned as such. Rejecting them is not available: minifiers reuse short identifiers across scopes, and the packed bundle really does contain an unrelated `a(h[t],f,g,l,e,m)` that no name-based match can distinguish from a require call. Those are covered by the gate's runtime half instead, which resolves them for real. Bare-identifier calls need the one-string-argument shape for the same reason. The audit moves to scripts/lib/shipped-imports.ts so fixture packages can exercise it. The gate needs a real `npm pack` behind minutes of Swift and Android builds, so every check that runs it can only watch a healthy package pass — which is how a reader that matched nothing looked covered. The new fixtures assert the failure direction per resolution form: 16 of the 22 fail against the previous reader, and the 6 that pass are the quoted-spelling and pinned-limitation cases. A wiring assertion keeps the audit and both runtime probes attached to the gate, since fixtures alone would stay green if the call were deleted. Verified against the real built bundle: the closure resolves to exactly the two declared dependencies, so the stricter reader adds no false positives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 12:28:05 +02:00
"@arethetypeswrong/cli": "^0.18.5",
"@chenglou/freerange": "^0.0.4",
"@nkzw/eslint-plugin": "^2.0.0",
"@nkzw/oxlint-config": "^1.3.0",
"@stryker-mutator/core": "10.0.0",
"@stryker-mutator/vitest-runner": "10.0.0",
feat(ai-sdk): add agent-device/ai-sdk tool set and document the MCP zero-code path (#1804) * feat(ai-sdk): add agent-device/ai-sdk tool set and document the MCP zero-code path Adds `createAgentDeviceTools()` under a new `agent-device/ai-sdk` subpath, built from the same command registry the MCP server uses so both stay in lockstep without a hand-maintained tool list. Introduces a `frameworkTier` descriptor facet ('core' | 'extended') so the factory can default to a curated perceive/act loop instead of handing a model dozens of tools. `ai` is wired as an optional peer dependency, imported lazily inside the factory rather than at module scope, so importing the subpath itself never requires `ai` to be installed - only calling it does. The package's own publishing gate (scripts/lib/shipped-imports.ts) is extended to recognize peerDependencies as a valid resolution source, since this is the first optional peer this package has shipped. Also restructures the AI SDK doc around three tiers (zero-code via @ai-sdk/mcp, the new typed tool set, hand-written tools) and fixes a stale `needsApproval` reference in favor of the current `toolApproval` API. * fix(layering): classify src/ai-sdk as a rank-4 zone The layering guard requires every src/<folder>/ to be explicitly ranked or unranked; the new src/ai-sdk/ subpath (added in the prior commit) was left unclassified, failing CI's Layering Guard job. It sits at the same tier as client/compat/daemon-server/metro/remote/sdk - a public integration surface consuming mcp (3) and core (2), imported by nothing else in the tree. * fix(ci): cover, exempt, and pack the new ai-sdk subpath Fixes the remaining CI failures on the ai-sdk subpath commit: - Coverage: src/ai-sdk/index.ts had no dedicated unit test (only manual/ integration verification), so changed-line coverage sat at 6.9% against the 70% gate. Adds src/ai-sdk/__tests__/index.test.ts (core vs 'all' tool filtering, session/platform pinning and schema hiding, error normalization, toolApproval passthrough) with createCommandToolExecutor and createAgentDeviceClient mocked the same way command-tools.test.ts does, plus a dedicated missing-peer-dependency.test.ts that mocks `ai` itself to throw, isolated to its own file so it doesn't affect the other tests' use of the real, installed `ai` package. Changed-line coverage is now 29/29 (100%). - Fallow Code Quality: src/ai-sdk/index.ts and examples/sdk/ai-sdk-tools.ts are entry points with no in-repo importer (reached only via package.json exports / run directly), and the new subpath's exports are unused internally by design - both need the same treatment src/sdk/*.ts and its examples already have in .fallowrc.json. - Integration Tests: test/integration/installed-package-metro.test.ts and src/__tests__/package-exports.test.ts each hand-list every published subpath and smoke-check it from a real packed install; added ./ai-sdk to both so the new subpath is actually exercised, not just silently passing. * fix(ai-sdk): hide MCP transport/config fields from the model too createAgentDeviceTools() only removed session and mcpOutputFormat from tool schemas. stateDir was still model-visible and reached the shared executor as client configuration, letting a tool call redirect into a different daemon state directory - defeating the "one pinned session" guarantee the factory exists to provide. includeCost and responseLevel are MCP tool-config knobs in the same category, irrelevant to this adapter. Widens the hidden-field set to session/stateDir/mcpOutputFormat/ includeCost/responseLevel, and now strips them from the runtime input inside execute() too (not just the schema), so the guarantee holds even if a caller bypasses schema validation. The schema-properties filter and the input filter now share one omitHidden() helper instead of two near- duplicate implementations. Addresses the P1 review comment on #1804.
2026-08-18 11:57:34 +02:00
"@types/json-schema": "^7.0.15",
"@types/node": "^22.20.1",
2026-07-30 14:12:50 +02:00
"@types/pngjs": "^6.0.5",
"@types/tar-stream": "^3.1.4",
"@types/yauzl": "^3.4.0",
"@vitest/coverage-v8": "4.1.11",
"ai": "^7.0.68",
"eslint-plugin-no-only-tests": "^3.4.0",
"eslint-plugin-perfectionist": "^5.10.1",
"fallow": "^2.104.0",
"fast-check": "^4.9.0",
"oxc-parser": "^0.146.0",
"oxfmt": "^0.64.0",
"oxlint": "^1.79.0",
build: verify the published tarball instead of grepping the bundle (#1578) * build: verify the published tarball instead of grepping the bundle Replaces the bundle-dependency grep with one gate that packs the tarball npm would publish and proves it sound from a clean consumer install: publint and attw on the tarball, a two-way dependency-closure audit, an import of every `exports` subpath, and the CLI smoke run — all from outside the workspace, where no pnpm link can mask an unresolvable specifier. Also stops the build from emitting a publishable bundle in the first place: a missing workspace link now fails `pnpm build` instead of warning and exiting 0, which is how 0.20.4 shipped an unresolvable `@agent-device/ad-script` import. publint found 12 real defects in the current package — every `exports` entry listed `types` after `import`, so TypeScript resolved declarations by accident rather than by condition. The dependency audit found `pngjs` declared as a runtime dependency while tsdown inlines it, an install every user paid for and no shipped code reached; it moves to devDependencies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(ci): run the package gate without pnpm on the Node floor pnpm 11.17 requires Node >= 22.13, so `pnpm check:package` could not start on the 22.12 floor the Packaged CLI job exists to cover. The gate needs only `node` and `npm`, so the job invokes the script directly. Splits the dependency-closure audit into a collector and a message builder to clear Fallow's complexity threshold, and classifies both packaging linters in ignoreDependencies: they are subprocess CLIs with no importable API here, which dependency analysis cannot follow to an import. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD * fix(publishing): read every literal resolution form in the closure audit The dependency-closure audit derived shipped imports from the ESM module record alone, so it could not see a package resolved through `require` or a `createRequire` result: neither produces a module-record entry. A lazy `createRequire('@agent-device/…')` would therefore clear the audit, the all-export probe and the exercised CLI paths, reintroducing the 0.20.4 published-install failure class for another command. Measuring the built bundle turned up a second, larger hole in the same reader. The shipped files are minified, and the minifier rewrites every string literal to a no-substitution template literal, so the dynamic-import extraction — which accepted quoted strings only — matched 0 of the 99 dynamic imports the bundle contains. The lazy `import()` path that broke 0.20.4 was reported as covered while checking nothing. Specifiers now come from the module record plus an AST walk over every literal runtime-resolution form: `import()`, `require()`, `require.resolve()`, an immediately-invoked `createRequire(...)`, and calls through a `createRequire` result under any import or minified alias. Both spellings of a string literal count everywhere, and `.cjs` joins the scanned extensions. Computed specifiers stay explicitly out of scope, and are pinned as such. Rejecting them is not available: minifiers reuse short identifiers across scopes, and the packed bundle really does contain an unrelated `a(h[t],f,g,l,e,m)` that no name-based match can distinguish from a require call. Those are covered by the gate's runtime half instead, which resolves them for real. Bare-identifier calls need the one-string-argument shape for the same reason. The audit moves to scripts/lib/shipped-imports.ts so fixture packages can exercise it. The gate needs a real `npm pack` behind minutes of Swift and Android builds, so every check that runs it can only watch a healthy package pass — which is how a reader that matched nothing looked covered. The new fixtures assert the failure direction per resolution form: 16 of the 22 fail against the previous reader, and the 6 that pass are the quoted-spelling and pinned-limitation cases. A wiring assertion keeps the audit and both runtime probes attached to the gate, since fixtures alone would stay green if the call were deleted. Verified against the real built bundle: the closure resolves to exactly the two declared dependencies, so the stricter reader adds no false positives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NamFJUgn9DGHrT2za11JbD --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-08-04 12:28:05 +02:00
"pngjs": "^7.0.0",
"publint": "^0.3.24",
"tsdown": "^0.22.14",
"typescript": "^7.0.2",
"vite": "^8.2.1",
"vitest": "^4.1.11"
2026-01-30 19:41:00 +01:00
}
}