Skip to content

Commit 31af377

Browse files
committed
Merge remote-tracking branch 'origin/main' into HEAD
* origin/main: perf: bundle runtime dependencies and report full install size (#2310) ci: avoid unrelated Apple runner cache invalidation (#2303) fix(web): preserve the backend ref so snapshot refs match actionable refs (#2283) test(daemon): session-open-url-prewarm through the request seam (#2304) test(daemon): session-devices-batch-runtime through the request seam (#2305) chore(gates): layering baselines ratchet against merge-base (#2299) test(daemon): one typed conformance helper for the daemon runtime suites (#2298) chore(layering): derive the contracts export inventory from package.json (#2297) perf: bundle tar-stream to reduce install footprint (#2286)
2 parents c077a28 + 7a2d48d commit 31af377

53 files changed

Lines changed: 2161 additions & 1697 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/actions/setup-apple-runner-build/action.yml

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ runs:
4444
id: source-hash
4545
run: |
4646
set -euo pipefail
47-
echo "value=${{ hashFiles('apple/runner/**', 'apple/snapshot-presentation/**', 'scripts/build-xcuitest-apple.sh', 'scripts/patch-xcuitest-runner-icon.ts', 'scripts/write-xcuitest-cache-metadata.mjs', 'packages/platform-apple/src/runner/apple-runner-platform.ts', 'packages/platform-apple/src/runner/runner-cache-metadata.ts', 'packages/platform-apple/src/runner/runner-icon.ts', 'packages/platform-apple/src/runner/runner-xctestrun.ts', 'packages/platform-apple/src/runner/runner-xctestrun-products.ts', '.github/actions/setup-apple-runner-build/action.yml', 'package.json', 'pnpm-lock.yaml') }}" >> "$GITHUB_OUTPUT"
47+
echo "value=${{ hashFiles('apple/runner/**', 'apple/snapshot-presentation/**', 'scripts/build-xcuitest-apple.sh', 'scripts/swift-toolchain-tmpdir.ts', 'scripts/patch-xcuitest-runner-icon.ts', 'scripts/write-xcuitest-cache-metadata.mjs', 'packages/platform-apple/src/runner/apple-runner-platform.ts', 'packages/platform-apple/src/runner/runner-cache-metadata.ts', 'packages/platform-apple/src/runner/runner-icon.ts', 'packages/platform-apple/src/runner/runner-xctestrun.ts', 'packages/platform-apple/src/runner/runner-xctestrun-products.ts', '.github/actions/setup-apple-runner-build/action.yml') }}" >> "$GITHUB_OUTPUT"
4848
shell: bash
4949

5050
- name: Resolve Apple runner build variant
@@ -55,12 +55,19 @@ runs:
5555
INPUT_XCUITEST_DESTINATION: ${{ inputs.xcuitest-destination }}
5656
run: |
5757
set -euo pipefail
58+
BUILD_COMMANDS="$(node -p 'JSON.stringify(Object.entries(require("./package.json").scripts).filter(([name]) => name.startsWith("build:xcuitest:")))')"
5859
VARIANT="$(
5960
printf '%s\n' \
6061
"$INPUT_GATE" \
62+
"$BUILD_COMMANDS" \
63+
"$(uname -m)" \
6164
"$INPUT_XCUITEST_PLATFORM" \
6265
"$INPUT_XCUITEST_DESTINATION" \
6366
"${AGENT_DEVICE_XCUITEST_INCLUDE_UNIT_TESTS:-0}" \
67+
"${AGENT_DEVICE_XCUITEST_ARCHS:-}" \
68+
"${AGENT_DEVICE_IOS_BUNDLE_ID:-}" \
69+
"${AGENT_DEVICE_IOS_RUNNER_APP_BUNDLE_ID:-}" \
70+
"${AGENT_DEVICE_IOS_RUNNER_TEST_BUNDLE_ID:-}" \
6471
| shasum -a 256 \
6572
| cut -c1-16
6673
)"
@@ -89,4 +96,23 @@ runs:
8996
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.2.3
9097
with:
9198
path: ${{ inputs.derived-path }}
92-
key: ${{ inputs.cache-key-prefix }}-${{ steps.xcode.outputs.key }}${{ inputs.cache-key-suffix }}-${{ steps.build-variant.outputs.key }}-${{ steps.source-hash.outputs.value }}
99+
key: ${{ steps.restore-runner-build.outputs.cache-primary-key }}
100+
101+
- name: Report Apple runner build cache
102+
env:
103+
CACHE_HIT: ${{ steps.restore-runner-build.outputs.cache-hit }}
104+
CACHE_KEY: ${{ steps.restore-runner-build.outputs.cache-primary-key }}
105+
BUILD_GATE: ${{ inputs.gate }}
106+
run: |
107+
set -euo pipefail
108+
if [ "$CACHE_HIT" = 'true' ]; then
109+
RESULT='restored exact prebuilt runner; compilation skipped'
110+
else
111+
RESULT='cache miss; built runner and attempted cache save'
112+
fi
113+
{
114+
printf '### Apple runner cache (%s)\n\n' "$BUILD_GATE"
115+
printf '%s\n\n' "$RESULT"
116+
printf 'Key: `%s`\n' "$CACHE_KEY"
117+
} >> "$GITHUB_STEP_SUMMARY"
118+
shell: bash

.github/workflows/ci.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,12 @@ jobs:
100100
runs-on: ubuntu-latest
101101
timeout-minutes: 15
102102
steps:
103+
# The layering ratchets (R6/R9/R10) measure the merge-base with origin/main, which a
104+
# shallow checkout cannot reach.
103105
- name: Checkout
104106
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
107+
with:
108+
fetch-depth: 0
105109

106110
# The layering gate parses production sources with `oxc-parser`, so
107111
# dependencies are required; keep install-deps enabled.
@@ -117,7 +121,7 @@ jobs:
117121

118122
# Model tests for the dependency-graph report and its blast-radius query. The report
119123
# reads the gate's model (scripts/layering/model.ts) and applies the gate's own R6
120-
# counting rule, so it is not a second measurement of TYPE_INVERSION_BASELINE.
124+
# counting rule, so it is not a second measurement of the R6 ratchet.
121125
- name: Check the depgraph report model
122126
uses: ./.github/actions/run-gate
123127
with: { gate: depgraph }

.github/workflows/size.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,15 @@ jobs:
9999
--startup-runs 7 \
100100
--json .tmp/size-report.json \
101101
--markdown .tmp/size-report.md
102+
cp /tmp/agent-device-size-base.json .tmp/size-report-base.json
103+
104+
- name: Upload detailed size reports
105+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
106+
with:
107+
name: size-report
108+
path: .tmp/size-report*.json
109+
include-hidden-files: true
110+
if-no-files-found: error
102111

103112
- name: Add job summary
104113
run: cat .tmp/size-report.md >> "$GITHUB_STEP_SUMMARY"

docs/dependency-graph-findings.md

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ const files = listSourceFiles();
2222
const sources = new Map(files.map((f) => [f, fs.readFileSync(f, 'utf8')]));
2323
const edges = resolveImportEdges(sources);
2424

25-
// e.g. R6 inversions per zone pair, deduplicated by file pair — reproduces
26-
// TYPE_INVERSION_BASELINE, so a mismatch means one of the two is stale.
25+
// e.g. R6 inversions per zone pair, deduplicated by file pair — the same count the gate
26+
// ratchets against the merge-base with origin/main.
2727
const seen = new Set<string>();
2828
const byPair = new Map<string, number>();
2929
for (const edge of edges) {
@@ -126,8 +126,9 @@ narrow name replaced both.
126126
from `daemon-command-registry.ts` to key an exhaustive owner-file map; that remaining inversion
127127
is the commands-zone consumer, not a second source of truth for the union.
128128

129-
All remaining inversions are argued at `TYPE_INVERSION_BASELINE` in `scripts/layering/check.ts`, next
130-
to the numbers they explain.
129+
All remaining inversions are argued here. R6 (`scripts/layering/type-inversion-ratchet.ts`) records
130+
no numbers of its own: its reference is the same count taken at the merge-base with `origin/main`,
131+
so a zone pair can only shrink.
131132

132133
## 0b. The biggest structural finding is not an inversion
133134

@@ -156,13 +157,13 @@ but it is a comprehension one, and it is the single largest obstacle to reading
156157
isolation. At the current measured commit it spans `commands` (33), `daemon-server` (30),
157158
`platforms` (19), `core` (12), root composition (5), `contracts` (2), and `client` (1).
158159

159-
Now ratcheted for growth by **R9** (`TYPE_CYCLE_BASELINE`, derived from the zone ceilings in
160-
`scripts/layering/daemon-modularity.ts`), so it cannot get worse
160+
Now ratcheted for growth by **R9** (`scripts/layering/daemon-modularity.ts`), so it cannot get worse
161161
while nobody is looking — a type-only import that closes a new loop fails the gate, verified by
162162
adding one type-only import that closes a loop and watching the gate reject it. It was growth-only
163-
here; #1781 A6 made it an equality pin, so a baseline left above the measured size fails too and a
164-
shrink is banked by the change that earns it. The refactor itself is still deliberately not
165-
attempted; it starts at those four hubs.
163+
here; #1781 A6 made it an equality pin, and the pin is now the merge-base's own measurement
164+
(`scripts/layering/ratchet-reference.ts`), so a shrink is banked the moment it merges and there is
165+
no slack left to spend. The refactor itself is still deliberately not attempted; it starts at those
166+
four hubs.
166167

167168
### The facade cycle: investigated, no narrower port exists
168169

@@ -183,14 +184,15 @@ duplicate the public API shape — a second source of truth for it — or derive
183184
carry the same dependency.
184185

185186
Those four files are therefore the minimum number of naming sites, not an accident: they are the
186-
choke point. Accepted as a position, argued at `TYPE_INVERSION_BASELINE`. The option this section
187+
choke point. Accepted as a position, argued in §0 above. The option this section
187188
used to hold open — moving `NAVIGATION_COMMAND_PROJECTIONS` out of `commands/` — was answered by
188189
deleting it: five direct signatures replaced the registry, so there is no longer a projection
189190
registry whose home is in question.
190191

191192
## 1. The two remaining type-inversion clusters
192193

193-
`TYPE_INVERSION_BASELINE` in `scripts/layering/check.ts` holds both, with the reasoning inline.
194+
§0 above holds both, with the reasoning inline; the gate measures them against the merge-base
195+
rather than recording them.
194196

195197
**28 + 1 edges → `client/client-types.ts`***done, mostly.* Now 5 edges. The vocabulary moved into
196198
the `contracts/client-*.ts` family files — one file per command/domain family, largest 137 LOC —

package.json

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -265,14 +265,6 @@
265265
"maestro",
266266
"detox"
267267
],
268-
"dependencies": {
269-
"@limrun/api": "^0.24.5",
270-
"ipaddr.js": "^2.5.0",
271-
"tar-stream": "^3.2.0",
272-
"undici": "7.29.0",
273-
"yaml": "^2.9.0",
274-
"yauzl": "^3.4.0"
275-
},
276268
"peerDependencies": {
277269
"ai": "^6.0.0 || ^7.0.0"
278270
},
@@ -303,6 +295,7 @@
303295
"@agent-device/xml": "workspace:*",
304296
"@arethetypeswrong/cli": "^0.18.5",
305297
"@chenglou/freerange": "^0.0.4",
298+
"@limrun/api": "^0.24.5",
306299
"@nkzw/eslint-plugin": "^2.0.0",
307300
"@nkzw/oxlint-config": "^1.3.0",
308301
"@stryker-mutator/core": "10.0.0",
@@ -318,14 +311,19 @@
318311
"eslint-plugin-perfectionist": "^5.10.1",
319312
"fallow": "^2.104.0",
320313
"fast-check": "^4.9.0",
314+
"ipaddr.js": "^2.5.0",
321315
"oxc-parser": "^0.146.0",
322316
"oxfmt": "^0.64.0",
323317
"oxlint": "^1.79.0",
324318
"pngjs": "^7.0.0",
325319
"publint": "^0.3.24",
320+
"tar-stream": "^3.2.0",
326321
"tsdown": "^0.22.14",
327322
"typescript": "^7.0.2",
323+
"undici": "7.29.0",
328324
"vite": "^8.2.1",
329-
"vitest": "^4.1.11"
325+
"vitest": "^4.1.11",
326+
"yaml": "^2.9.0",
327+
"yauzl": "^3.4.0"
330328
}
331329
}

packages/host-kit/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
"description": "Private host-mechanics package: each export is one narrow capability port over the host machine \u2014 running commands, observing and owning processes, diagnostics, retry/deadline, archive extraction, durable files and locks, request-scoped plumbing, and the installed version.",
77
"dependencies": {
88
"@agent-device/contracts": "workspace:*",
9-
"@agent-device/kernel": "workspace:*",
10-
"tar-stream": "^3.2.0",
11-
"yauzl": "^3.4.0"
9+
"@agent-device/kernel": "workspace:*"
1210
},
1311
"devDependencies": {
1412
"@types/tar-stream": "^3.1.4",
15-
"@types/yauzl": "^3.4.0"
13+
"@types/yauzl": "^3.4.0",
14+
"tar-stream": "^3.2.0",
15+
"yauzl": "^3.4.0"
1616
},
1717
"exports": {
1818
"./archive": {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { describe, expect, test } from 'vitest';
2+
import { attachRefs, type RawSnapshotNode } from './snapshot.ts';
3+
4+
const node = (index: number, extra: Partial<RawSnapshotNode> = {}): RawSnapshotNode => ({
5+
index,
6+
...extra,
7+
});
8+
9+
describe('attachRefs', () => {
10+
test('preserves a backend-minted ref instead of re-minting a dense positional one', () => {
11+
// The web/agent-browser backend mints refs in tree order (non-dense — non-interactive
12+
// nodes are skipped). The first node's backend ref is e2 because e1 went to a node
13+
// that did not appear in the projected tree. Preserving it keeps the ref the agent
14+
// reads off the snapshot identical to the ref the backend resolves on the next action.
15+
const attached = attachRefs([node(0, { ref: 'e2' }), node(1, { ref: 'e3' })]);
16+
expect(attached.map((n) => n.ref)).toEqual(['e2', 'e3']);
17+
// index is preserved untouched — the positional identity is orthogonal to the ref.
18+
expect(attached.map((n) => n.index)).toEqual([0, 1]);
19+
});
20+
21+
test('re-mints dense positional refs for nodes without a backend ref', () => {
22+
const attached = attachRefs([node(0), node(1, { ref: 'e7' }), node(2)]);
23+
expect(attached.map((n) => n.ref)).toEqual(['e1', 'e7', 'e3']);
24+
});
25+
});

packages/kernel/src/snapshot.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,15 @@ export type RawSnapshotNode = {
120120
hiddenContentBelow?: boolean;
121121
interactionBlocked?: 'covered';
122122
presentationHints?: string[];
123+
/**
124+
* Backend-minted ref for this node, when the capture backend already assigns a
125+
* stable, actionable ref (e.g. the web/agent-browser backend resolves actions
126+
* against its own `@eN` refs). `attachRefs` preserves this instead of re-minting
127+
* a dense positional ref, so the ref an agent sees in the snapshot is the same
128+
* ref the backend can resolve on the next action. Absent for backends that do
129+
* not mint refs — those fall back to dense `e${index}` numbering.
130+
*/
131+
ref?: string;
123132
/**
124133
* Accessibility custom actions the element exposes (iOS
125134
* `UIAccessibilityCustomAction`, React Native `accessibilityActions`). Merged
@@ -273,8 +282,15 @@ export type ScreenshotOverlayRef = {
273282
center: Point;
274283
};
275284

285+
/**
286+
* Assign a display ref to every node. A node that already carries a backend-minted
287+
* `ref` keeps it (see `RawSnapshotNode.ref`) — the web/agent-browser backend resolves
288+
* actions against its own refs, so re-minting a dense positional ref here would make
289+
* the snapshot show one ref while actions act on a different element. Backends that do
290+
* not mint refs get dense `e${index}` numbering, matching the historical behavior.
291+
*/
276292
export function attachRefs(nodes: RawSnapshotNode[]): SnapshotNode[] {
277-
return nodes.map((node, idx) => ({ ...node, ref: `e${idx + 1}` }));
293+
return nodes.map((node, idx) => ({ ...node, ref: node.ref ?? `e${idx + 1}` }));
278294
}
279295

280296
/**

packages/maestro/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88
"dependencies": {
99
"@agent-device/contracts": "workspace:*",
1010
"@agent-device/kernel": "workspace:*",
11-
"@agent-device/selectors": "workspace:*",
12-
"yaml": "^2.9.0"
11+
"@agent-device/selectors": "workspace:*"
1312
},
1413
"exports": {
1514
".": {
1615
"types": "./src/index.ts",
1716
"default": "./src/index.ts"
1817
}
18+
},
19+
"devDependencies": {
20+
"yaml": "^2.9.0"
1921
}
2022
}

packages/platform-web/src/__tests__/agent-browser-snapshot.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,34 @@ describe('normalizeAgentBrowserSnapshot', () => {
5959

6060
expect(result.nodes[0]?.enabled).toBe(true);
6161
});
62+
63+
test('preserves the backend ref so the displayed ref is the actionable one', async () => {
64+
// agent-browser mints refs in tree order and skips non-interactive nodes
65+
// (the leading `generic` container never gets a ref), so the refs are NOT dense:
66+
// e1 = generic (skipped here, no ref on it)
67+
// e2 = username textbox
68+
// e3 = passcode textbox <-- agent presses @e3 expecting username
69+
// e4 = sign-in button
70+
// The username textbox is the 2nd NODE in the snapshot but its backend ref is e2.
71+
// If agent-device re-mints dense positional refs, the snapshot would show the
72+
// username as @e2 while the passcode (node 3) shows @e3 — and an agent that
73+
// reads "@e3 = passcode" would land in the passcode field when it meant the
74+
// username, because the backend's own e3 resolves to a different element.
75+
// Preserving the backend ref keeps display ref == actionable ref.
76+
const result = await normalizeAgentBrowserSnapshot({
77+
snapshot: [
78+
'- textbox "Username" [ref=e2]',
79+
'- textbox "Passcode" [ref=e3]',
80+
'- button "Sign in" [ref=e4]',
81+
].join('\n'),
82+
refs: {
83+
e2: { role: 'textbox', name: 'Username' },
84+
e3: { role: 'textbox', name: 'Passcode' },
85+
e4: { role: 'button', name: 'Sign in' },
86+
},
87+
});
88+
89+
expect(result.nodes.map((node) => node.label)).toEqual(['Username', 'Passcode', 'Sign in']);
90+
expect(result.nodes.map((node) => node.ref)).toEqual(['e2', 'e3', 'e4']);
91+
});
6292
});

0 commit comments

Comments
 (0)