Skip to content

refactor(substrate): split host mechanics into @agent-device/host-kit capability ports - #2088

Merged
thymikee merged 8 commits into
mainfrom
refactor/platform-substrate-wave
Aug 28, 2026
Merged

refactor(substrate): split host mechanics into @agent-device/host-kit capability ports#2088
thymikee merged 8 commits into
mainfrom
refactor/platform-substrate-wave

Conversation

@thymikee

@thymikee thymikee commented Aug 27, 2026

Copy link
Copy Markdown
Member

Summary

Nothing changes for operators, clients, or command authors: no CLI surface, wire shape, or platform behavior moves. This is the substrate step of #2082 that unblocks the platform-family moves, and its reviewable claim is where shared code is allowed to live.

@agent-device/host-kit is a new private package between kernel and capture-kit holding only mechanics that act on the host machine. Each export is one narrow capability, not a technical category:

port owns closure
command running host commands, argument quoting 7
process process liveness, identity, ownership records, reaping 12
diagnostics diagnostic emission and scopes 3
retry deadline, backoff policy, sleep 6
archive bounded extraction, entry safety, byte limits 9
file atomic publishes, process locks, path resolution 12
request request-scoped cancellation and progress 5
version the installed version off disk 4

capture-kit keeps capture, snapshot and recording behavior and depends on host-kit; tar-stream and yauzl move with the archive code. Helpers that touch no process, file, or environment are not host mechanics and went to kernel instead (record, source-value, success-text, ttl-memo, scoped-provider, location-coordinates, device-isolation) — the same shape as #2106 sinking screenshot-geometry there. Enforced direction: kernel < contracts < host-kit < capture-kit < platform/provider/daemon.

Three earlier shapes were rejected: four category barrels (exec/fs/values, which put unrelated concerns behind one specifier and measurably grew closures), deep façades over the modules in place (hides the surface without moving ownership), and one file per subpath (dozens of shallow generic subpaths with no capability boundary).

Tradeoff: consumers now evaluate a port file where they used to import a module directly. src/cli.ts goes 369 → 380 and src/daemon/session-teardown.ts 60 → 68; for the CLI the +11 is exactly the 11 façade files it reaches. Packed size is neutral (JS gzip −270 B).

Scope: 604 files, nearly all mechanical specifier rewrites; no command family was touched.

Validation

Exact head f87c0fe has 32 substantive SUCCESS checks plus one expected build-cache matrix SKIP: Coverage, Repo Guards, Integration Tests, Typecheck & Package, Lint & Format, Compatibility & Provenance, Bundle Size, command-docs-gate, Ownership and context budgets, Resolve native fingerprint, every CodeQL analysis, all five native Smoke Tests jobs, and the full mutation lane — ten shards plus the aggregate, which passed at 21:19:43 with every shard present and green. The skip is matrix.name, which drops out when the native fingerprint is unchanged.

The previous head bb53b0e had the same 32 SUCCESS plus one SKIP. The delta between them is one comment-only commit (11 files, 26 lines) repairing references this PR broke: the move orphaned ~20 comment citations pointing at src/utils/*.ts and src/request/*.ts paths it deleted — all of which resolved at merge-base 77b5be8 — including one rewritten to packages/contracts/src/mobile-snapshot-semantics.ts, a file that has never existed. Each now names the owning capability port instead of a path.

package-boundaries.test.ts pins the eight subpath keys and the dependency set, the layering model ranks the new zone, and R13's allow-list names host-kit. Every port and every sunk kernel module carries an exact eager-closure row, so a port that grows unrelated mechanics fails the gate.

Two lanes were red on earlier heads; both are resolved and neither was caused by this diff:

  • Affected decision-kernel mutants, three times, always with kernel-errors and selectors-2 cancelled. Those two run ~19½ minutes against the other eight's 5½–10, so a push landing ~17 minutes into a run kills exactly that pair, and the aggregate then correctly refuses to publish a pass over an incomplete shard set. Left undisturbed the lane takes ~27 minutes and passes, as it now has on two consecutive heads. No shard ever failed on its own.
  • Android smoke, the smoke:automation-system native-alert step. It reproduced byte-identically on unrelated PRs while main was green, and it passed on c8b6eb1 then failed on a7c8df6, whose only non-comment difference is } catch {\n} reformatted to } catch {}. It has since passed on bb53b0e and f87c0fe and on both stacked PRs. Investigating it did surface a real scenario-side bug — alert dismiss returns without confirming the dialog closed, and falls back to Back, which cannot produce Alert result: cancelled — but the fix lives outside this refactor and is proposed in the comments rather than pushed.

Follow-up: the size report's CLI-help median swings widely between runs on identical content (71.8→98.6ms on one, 81.0→83.3ms on the next, −2.4ms later), so that figure looks noisier than the effect it would measure.

🤖 Generated with Claude Code

https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-08-28 05:47 UTC

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 2.48 MB 2.48 MB +43 B
JS gzip 834.3 kB 834.0 kB -270 B
npm tarball 957.5 kB 957.3 kB -226 B
npm unpacked 3.32 MB 3.32 MB +185 B

npm unpacked components

Component Base Current Diff
JS / dist source 2.64 MB 2.64 MB +116 B
Apple runner source/project 581.1 kB 581.1 kB +6 B
macOS helper source 54.8 kB 54.8 kB 0 B
Android helper artifacts 0 B 0 B 0 B
Other package files 45.3 kB 45.3 kB +63 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 29.9 ms 30.3 ms +0.4 ms
CLI --help 80.5 ms 81.0 ms +0.5 ms

Top changed chunks:

Chunk Raw diff Gzip diff
dist/src/internal/daemon.js -2.7 kB -869 B
dist/src/sdk-batch-runner.js +336 B +99 B
dist/src/session2.js -281 B -68 B
dist/src/perf-runtime-plan.js -60 B -19 B
dist/src/runtime.js +21 B +13 B

Top changed packed files

Packed file Base Current Diff
dist/src/archive.js 0 B 8.2 kB +8.2 kB
dist/src/byte-limit-stream.js 8.2 kB 0 B -8.2 kB
dist/src/process.js 0 B 4.7 kB +4.7 kB
dist/src/owner-identity.js 751 B 3.8 kB +3.1 kB
dist/src/internal/daemon.js 110.0 kB 107.3 kB -2.7 kB
dist/src/host-process.js 2.5 kB 0 B -2.5 kB
dist/src/snapshot-quality-verdict.js 0 B 2.1 kB +2.1 kB
dist/src/verdict.js 2.1 kB 0 B -2.1 kB
dist/src/owned-process-reaper.js 2.0 kB 0 B -2.0 kB
dist/src/process-lock.js 1.4 kB 0 B -1.4 kB

@thymikee

Copy link
Copy Markdown
Member Author

[P1] Keep moved decision kernels owned by the mutation lane. This head deletes/moves src/snapshot/snapshot-occlusion.ts and src/utils/__tests__/png-worker.test.ts, but KERNEL_MODULES and the thread-hostile test-scope expectation still name those old paths. Select affected kernels therefore fails three structural assertions and emits no mutants for the moved snapshot decision kernel. Update the registry to packages/contracts/src/snapshot-occlusion.ts, re-derive/validate its owning tests, update the moved PNG expectation, and run the mutation-model gate.

[P2] Prove the new project-root behavior, not only memoization. findProjectRoot() now deliberately walks past @agent-device/capture-kit to the agent-device manifest because source daemon spawn/version reads otherwise resolve the package root, but the moved test only checks that a walk occurred and was memoized. Add an exact source-layout assertion that fails with the prior nearest-manifest behavior and verifies the resulting version/daemon-spawn loading shape. Remove the explanatory history comments whose invariant should be expressed by the symbols and planted-red test; likewise remove the new Maestro history comment.

The W1 package boundary itself is directionally sound and the 1311+/909- change is move-dominated with itemized growth, but this head is not ready while the owning structural gate is red and the behavior change lacks planted-red proof.

@thymikee

Copy link
Copy Markdown
Member Author

Re-reviewed 903f3aafe1: the mutation-registry/test-scope P1 is resolved in code; the affected mutation check is still pending. The P2 remains: findProjectRoot() now deliberately skips the capture-kit manifest to recover the source agent-device root, but version.test.ts still only proves walk memoization, not the returned root or daemon-spawn/version loading shape. Add a planted-red exact source-layout assertion that fails with nearest-manifest behavior. Remove the new version/Maestro history comments by making that invariant obvious in symbols and the regression test. Not ready at this head while this behavior change is unproven and CI/mergeability are pending.

Copy link
Copy Markdown
Member Author

Both points addressed on the current head (19dd627):

P1 landed in 7999c04 (your comment raced the push): the snapshot-occlusion kernel row and the stryker mutate list now point at packages/contracts/src/snapshot-occlusion.ts, the thread-hostile expectation follows png-worker.test.ts into capture-kit, and the mutation model suite runs 39/39 — ownership derives the moved owning test, and mutants generate for the kernel again.

P2 in 19dd627: findProjectRoot now takes an explicit start directory, and version.test.ts plants the two behaviors it promises — a fixture layout where a workspace-package manifest sits between the module and the agent-device manifest (resolves the root, and readVersion serves the root's version, not 0.0.0), and a no-agent-device tree that falls back to the nearest manifest. Both assertions were observed red against the prior nearest-manifest walk before the fix went back in; a third real-tree assertion pins the resolved manifest's name and the version it serves. The history comments in version.ts and maestro's shared.ts are gone — the doc comment now states only the standing invariant, and the tests carry the rest.


Generated by Claude Code

@thymikee

Copy link
Copy Markdown
Member Author

Re-reviewed 19dd627a10: the synthetic workspace, vendored fallback, and real-source assertions now substantively prove the project-root fix. One P2 remains: the test makes exported findProjectRoot accept and memoize an arbitrary startDirectory, although every production caller needs the zero-argument owning-root operation. This widens the declared capture-kit/version surface solely as a test hook and lets downstream code select a different project root. Keep findProjectRoot(): string narrow and extract a package-internal resolver for fixture tests (or use a test-only seam). Remove the explanatory docblock/test history comment; the resolver name and exact planted-red assertions should own the invariant. The PR is also currently conflicting with main and checks are pending, so it is not ready.

@thymikee
thymikee force-pushed the refactor/platform-substrate-wave branch from 47da869 to 39af141 Compare August 27, 2026 13:26

Copy link
Copy Markdown
Member Author

Addressed on 39af141 (rebased onto main, so #2081's cutover retirement is now the base):

  • Narrow surface: findProjectRoot(): string is zero-argument again; the walk moved to package-internal project-root.ts, whose resolveAgentDeviceProjectRoot(startDirectory) the fixture tests exercise directly. Nothing new appears in the declared ./version surface, and downstream code cannot select a different root.
  • Comments: the version docblock and the test history comment are gone; the resolver name and the planted-red assertions carry the invariant (re-observed red against the resolver after the extraction).
  • Conflict: resolved in packages/contracts/package.json only — the sorted union of your record-runtime-cutover → record-runtime-execution rename and this PR's four snapshot entries (116 entries, no duplicates). Layering, lint, typecheck, format, fallow, and the mutation/depgraph/affected model suites (123/123) are green on the rebased head; the full unit suite is finishing now and I'll follow up if it surfaces anything.

Generated by Claude Code

@thymikee

Copy link
Copy Markdown
Member Author

Still not ready at 39af1410. The public findProjectRoot() surface is now narrow and the previous design concern is resolved, but the exact-head snapshot-occlusion mutation job is an owner-action failure: Stryker mutates the moved contracts source, finds no related tests, executes none, and emits no report; the aggregate correctly fails too. Repair the mutation source-to-owning-test relationship and rerun. The touched internal resolver/tests also still contain explanatory implementation/history comments that conflict with the newly merged AGENTS rule; remove those rather than preserving the rationale in code. Other substantive CI, including iOS smoke, is green.

Copy link
Copy Markdown
Member Author

Fixed on 6be4299 — the shard failure was two regressions from the move plus one latent defect it exposed:

  • The owning test never moved. The wave's mover listed src/snapshot/snapshot-occlusion.test.ts but the file lives under __tests__/; the git mv crashed there and the recovery missed it, so the test stayed in root with its import rewritten to the package specifier. It now sits at packages/contracts/src/snapshot-occlusion.test.ts importing ./snapshot-occlusion.ts relatively, like every other in-package owning test.
  • The sandbox alias table has always been empty. workspaceSourceAliases derives from tracked manifests, and Stryker's sandbox is an untracked copy — git ls-files returns nothing there, so every @agent-device/* specifier escaped through the node_modules symlink to the real unmutated tree. That's why in-sandbox relatedness only ever traced relative imports (the green package shards each happen to have one — e.g. device-selector-flags.test.ts./errors.ts carries kernel-errors). The alias reader now falls back to the copied manifests on disk when the tracked route is empty, which restores specifier relatedness for every package kernel, not just this one.

Verified: the shard runs end-to-end locally — dry run executes 2,265 tests, 287 mutants tested, report written — and the mutation model suite (39/39), layering, typecheck, and the moved test all pass. Remaining explanatory comments trimmed to bare directives per the new AGENTS rule.


Generated by Claude Code

@thymikee
thymikee force-pushed the refactor/platform-substrate-wave branch from 6be4299 to 10009d5 Compare August 27, 2026 14:30
@thymikee

Copy link
Copy Markdown
Member Author

Not ready.

[P1] This wave contradicts ADR-0019 on both sides: it turns capture-kit from the durable-capture domain into generic platform-common, and moves ~900 lines of snapshot presentation/occlusion/codec behavior into contracts despite the ADR limiting contracts to vocabulary/plans. Split behind cohesive owners or explicitly amend and enforce the architecture before later family waves depend on it.

[P2] The Stryker disk-manifest fallback infers sandbox mode from an empty tracked result and has no regression that exercises that branch; make the manifest source explicit and add planted-red sandbox proof.

Remove the remaining touched implementation/history narration per current AGENTS policy. Exact-head kernel-errors mutation also times out before producing a report; selectors-2 is pending.

Copy link
Copy Markdown
Member Author

Addressed on d6442d7:

P1 — the ~1,000 lines of snapshot behavior (snapshot-occlusion, mobile-snapshot-semantics, snapshot-quality-verdict, snapshot-quality-backend-capabilities) leave contracts for @agent-device/capture-kit, where the ADR already sends capture parsers; contracts is back to vocabulary/plans. For the capture-kit side I took the amend-and-enforce option: ADR-0019 now carries a #2082 amendment naming the substrate layout — capture-kit owns host-process and capture mechanics (which is what the W1 utils set is), provision-kit (W3) owns provisioning, contracts stays vocabulary/plans/pure-classification — and its enforcement: per-package export pins in package-boundaries.test.ts, the standing contracts mechanics gate, and W3's platforms-root-shape rule. The mutation registry, stryker mutate list, pins, and budget rows follow the occlusion kernel to its new home.

P2workspaceSourceAliases(repoRoot, source) now requires 'tracked-manifests' | 'disk-manifests'; the sandbox-mode inference from an empty tracked result is gone. The mutation vitest config declares disk manifests when its root sits under the pinned Stryker temp dir, and a new regression builds an untracked sandbox-shaped fixture inside the repo tree and asserts the tracked reader sees nothing there while the disk reader resolves the specifier exactly — observed red against the prior inference implementation before trusting it.

kernel-errors timeout — root-caused, not flake: the alias fix made the dry run genuinely transform the mutated tree in-sandbox (before, specifier imports escaped to the real unmutated tree, which is what the default five-minute dry-run budget was calibrated against). stryker.config.json now sets dryRunTimeoutMinutes: 15; locally the 861-file dry run completes in 2m47s. A complete local sweep is blocked only by the two known root-container chmod tests failing the dry run as uid 0 — they are green in unprivileged CI, where the exact-head shard reruns now.

Remaining resolver/version narration trimmed to bare constraint statements.


Generated by Claude Code

@thymikee

Copy link
Copy Markdown
Member Author

Not ready. The snapshot-behavior move and explicit mutation-source fix are sound, but the architecture blocker remains. The ADR amendment relabels capture-kit as owner of process execution, archives, PNG, atomic files/locks, request plumbing, retry/path/env/version utilities, and other unrelated mechanics, exposing dozens of shallow subpaths. That is the generic platform-common grab bag ADR-0019 was intended to remove; pinning exports freezes the surface but does not create a cohesive owning domain. Keep capture and snapshot behavior here, but move generic host/process/request utilities behind a focused deep interface rather than using ADR prose to bless the workaround. Remove the added implementation/history narration, and require the kernel-errors mutation shard to finish green.

Copy link
Copy Markdown
Member Author

Two of the three asks are done on 134f6d4: the alias-source docblock and mutation-config narration are trimmed to bare directives, and the kernel-errors shard finished green on the prior head d6442d7 (20m end-to-end under the explicit dry-run budget; snapshot-occlusion green on its capture-kit path too — the comment-only push reruns both).

On the capture-kit P1, before a third relocation of the same ~38 modules I want to agree the target shape, because "focused deep interface" fixes two different things and the blast radius differs by an order of magnitude:

  1. New host package with a deep surface (full remedy): @agent-device/host-kit at kernel < contracts < host-kit < capture-kit < provision-kit < … owning the generic mechanics (exec/diagnostics/timeouts/retry, path/env/shell-quote/version, atomic-file/locks, ttl-memo/scoped-provider/source-value/success-text/parsing, request-cancel/progress), exported as a handful of cohesive deep modules (roughly host-kit/exec, host-kit/fs, host-kit/values, host-kit/request) instead of dozens of one-file subpaths; capture-kit keeps capture behavior (PNG, archives?, process supervision for capture, snapshot behavior, recording/audio/app-log mechanics) and depends on host-kit. This re-touches most of W1's ~500 import sites a second time and re-shapes the consumer surface W4–W6 would code against, so I'd want it agreed before it lands.
  2. Deep façades in place (surface remedy only): keep the modules where they are but collapse the exported surface — the same few deep entry points, with the one-file subpaths retired and the pins shrinking accordingly. Small diff, kills the "dozens of shallow subpaths" symptom, but the ownership label you object to stays.

I read your comment as asking for (1). Confirm — or pick (2) or another cut — and I'll implement it across the stack; the matching ADR text then describes whichever layout actually ships rather than blessing the current one.


Generated by Claude Code

Copy link
Copy Markdown
Member Author

The exact-head iOS smoke failure on 134f6d4 is not this PR's by construction: 134f6d4 differs from d6442d7 — where the identical lane ran fully green at 15:58, gesture-pan replay included — by two comment-only trims (git diff d6442d7..134f6d4 touches no executable line). This run failed in the same window in which the sibling stack heads' iOS lanes also failed under triple-stack runner load, the same gesture-pan/lease shape recorded on #2100 (where the retry-blocked-by-stale-claim harness defect is noted). I have no job-rerun permission from this session; the shard reruns with the next code push, and the PR stays watched until green.


Generated by Claude Code

@thymikee

Copy link
Copy Markdown
Member Author

The snapshot behavior and mutation-source work are sound, but the broader capture-kit ownership blocker remains unchanged on this comment-only head. Confirming the full remedy you outlined: use option 1, a focused host-mechanics owner with a small deep surface (exec, fs, values, request or similarly cohesive seams); keep actual capture/snapshot/recording behavior in capture-kit. Do not use deep façades in place merely to hide dozens of generic subpaths. Remove the remaining resolver/Stryker narration. Exact-head iOS is red and must rerun green or receive a same-load base comparison. Not ready.

@thymikee

Copy link
Copy Markdown
Member Author

Capture-kit is narrower, but the requested host owner is still a generic platform-common utility package hidden behind four technical barrels. exec, fs, and especially values combine unrelated process ownership, diagnostics, archives/locks, parsing, location validation, result presentation, memoization, and version lookup. The measurable result is worse eager closure (CLI 369→386; session teardown 60→79) and CLI-help median 82.8→103.5ms. Split around cohesive owners/narrow capability ports and enforce their dependency shape; remove moved narration. This head is also CONFLICTING/DIRTY with iOS/mutation checks unfinished. Not ready.

Copy link
Copy Markdown
Member Author

Option 1 is implemented on fccf8c1.

@agent-device/host-kit is a new private package between kernel and capture-kit owning the generic host mechanics, exported behind exactly four deep seams — exec, fs, values, request — with every internal module under src/internal/ and unreachable except through a seam. capture-kit keeps capture, snapshot and recording behavior only (PNG tooling, density/pixel diff, occlusion, mobile snapshot semantics, quality verdicts) and now depends on host-kit; tar-stream/yauzl moved with the archive mechanics. The enforced direction is kernel < contracts < host-kit < capture-kit < platform/provider/daemon. No deep façades in place: the modules actually moved.

The seams are pinned rather than open-ended — package-boundaries.test.ts asserts the exact four subpath keys (a fifth fails the gate) and host-kit's dependency set, R13's allow-list names host-kit explicitly, and the layering model ranks the new zone. Each seam carries an exact eager-closure row (exec 14, fs 19, values 12, request 5); the four consumer rows that grew because a consumer now closes over a whole seam instead of one module moved to their measured numbers in this PR, which is the honest cost of the deep surface you asked for.

The ADR-0019 amendment is rewritten to describe this layout instead of blessing capture-kit as the host owner, and the remaining alias-source and Stryker narration is gone.

Two things the seam merge surfaced and this push fixes, both real bugs rather than mechanics: eight test files had per-module vi.mock calls that became duplicate same-seam factories, where the second silently replaced the first, and three more mocked a seam whose production callers reach the internal module directly, so the factory never applied. Those are merged into single spread factories and pinned at the injection points instead.

Green locally on fccf8c1: typecheck, lint, check:layering (1500 files), fallow vs the base, pnpm build plus the packaged-install check, and unit-core at 8176 tests with only the two chmod-based daemon tests failing, which assert unwritable-directory behavior that does not bind under uid 0 in this container and are untouched by the diff. W2 and W3 are rebased onto this head.


Generated by Claude Code

…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
@thymikee
thymikee force-pushed the refactor/platform-substrate-wave branch from fccf8c1 to 56039fe Compare August 27, 2026 18:27
@thymikee

Copy link
Copy Markdown
Member Author

Rebase reconciliation did not change the host-kit blocker: four technical barrels still combine unrelated generic mechanics, eager budgets remain CLI 386/session teardown 79, and fresh CLI-help median worsened 71.8→98.6ms. Split around cohesive owners/narrow capability ports and enforce semantic dependency shape; remove moved narration. Update the stale PR body, which still describes the superseded capture-kit layout. Android smoke is red and most CI/mutation lanes are pending. Not ready.

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
@thymikee

Copy link
Copy Markdown
Member Author

The eight capability ports materially resolve the prior generic host-kit blocker: archive/command/diagnostics/file/process/request/retry/version are cohesive, per-port closure is 3–12 files, eager closure improves, and packed size is neutral. Not ready: remove the unused root scratch-exports.json and scratch-symbol-map.json migration inventories (346 unexplained lines; no consumers); update the stale PR body to describe the current eight-port ownership, itemize current growth, and record why the rejected four-barrel/smaller façade did not preserve capability boundaries; trim touched implementation/enforcement narration including packages/kernel/src/scoped-provider.ts and the package-boundary/eager-budget comments. Coverage, native smoke, and affected mutation evidence must also finish.

Copy link
Copy Markdown
Member Author

Rebased onto main (77b5be8) and restructured on 794fe4b.

The barrels are gone. You were right that exec/fs/values grouped by technical category rather than by capability. Each export is now one narrow capability over the host machine, and a port re-exports only what a consumer of that capability actually uses (the generated surfaces re-exported internal-only symbols at first; fallow caught them and they are trimmed):

port owns
command running host commands, argument quoting
process process liveness, identity, ownership records, reaping
diagnostics diagnostic emission and scopes
retry deadline, backoff policy, sleep
archive bounded extraction, entry safety, byte limits
file atomic publishes, process locks, path resolution
request request-scoped cancellation and progress
version the installed version off disk

Most of values was never host mechanics at all. Record readers, config-source values, result text, memoization, async scoping, coordinate validation and device-scope parsing touch no process, file, or environment, so they are not behind a host port under a different name — they are in @agent-device/kernel beside its other primitives, as kernel/record, kernel/source-value, kernel/success-text, kernel/ttl-memo, kernel/scoped-provider, kernel/location-coordinates, kernel/device-isolation. That is the same shape as #2106 sinking screenshot-geometry into kernel.

The closure numbers you cited move the right way. Every port carries its own eager-closure row, so a port that grows unrelated mechanics fails the gate:

entry before ports now on main
capture-kit/png-worker-client 20 10 9
capture-kit/png-resize 28 18 17
daemon/session-teardown 79 68 60
src/cli.ts 386 380 369

The ports themselves measure 3–12 (diagnostics 3, version 4, request 5, retry 6, command 7, archive 9, file 12, process 12) against the old barrels' 14/19/12. The residue over main is accounted for: src/cli.ts reaches 6 ports and 5 kernel modules, and 380 − 369 = 11 is exactly those 11 façade files — the cost of the specifier existing at all, not of unrelated mechanics loading.

The ADR amendment now states the rule the ports have to keep passing, rather than describing the layout: a port may only hold mechanics a consumer of that capability needs, and a helper that touches no process, file, or environment does not belong in host-kit. The resolver/Stryker narration is gone.

Green on 794fe4b: typecheck, lint, check:layering (1501 files), fallow, pnpm build + packaged-install, and unit-core at 8204 tests — the only failures are the two daemon tests that prove unwritable-directory behavior with chmod 0o500, which does not bind under uid 0 in this container and is untouched by the diff.

On the red Android lane, see the investigation below — it reproduces identically on unrelated PRs while main is green.


Generated by Claude Code

Copy link
Copy Markdown
Member Author

Android Smoke Tests: not this PR's, and not this stack's.

The failure is the same on every red run: the smoke:automation-system scenario times out waiting 10s for Alert result: cancelled after dismissing the native alert, with the surface still reporting a Native alert label.

✖ live Android emulator fixture E2E (103602ms)
  step: wait for Alert result: cancelled
  reason: wait_target_absent, waitedMs: 10001, readableCaptures: 10
  currentSurface.labels: [..., "Last input: longpress", "Long presses: 1", "Native alert"]
  at assertAutomationSystem (android-emulator-e2e/live-automation-scenario.ts:160)

Evidence it is load-borne rather than caused by any diff:

  • main is green on the same lane — run at 18:10 UTC, android.yml, success.
  • An unrelated PR fails identically. fix(daemon): refuse host path install sources on the HTTP surface #2102's branch (claude/agent-device-2097-cd747a, a daemon install-source fix that touches none of this) failed at 18:31 with byte-identical output: same scenario, same Native alert surface, same live-automation-scenario.ts:160 frame. That same branch passed at 17:45 and again at 18:15.
  • This branch passed at 17:59 and failed at 18:27 with the intervening change being the rebase onto main, not anything in the alert path.
  • Five branches went red on that lane inside a 45-minute window (fix/agent-device-2096, claude/agent-device-2097-cd747a, and all three of this stack) while others passed in the same window — the signature of concurrent emulator jobs, not of a shared code change.

Nothing in this stack touches gesture handling, alert dismissal, or the wait loop; W1 is a mechanics move, W2 is Android app-state ownership, W3 is provisioning and settings placement.

No fix exists to port: #2105 addresses device-claim supersession on a retried open, which is a different failure. The scenario's own 10s budget after an alert dismissal is what is too tight under load; a fix belongs in live-automation-scenario.ts (either waiting on the alert's disappearance before waiting on the app text, or taking the budget from the same scaling the other live steps use), which is outside this refactor's scope and would widen it. I have no job-rerun permission from this session, so the lane re-runs on the next push; I am keeping the PR watched until it is green.


Generated by Claude Code

@thymikee thymikee changed the title refactor(substrate): sink the platform-blocking utils/request/snapshot closure into packages refactor(substrate): split host mechanics into @agent-device/host-kit capability ports Aug 27, 2026
@thymikee

Copy link
Copy Markdown
Member Author

Re-reviewed bc8ae932b6: the scratch inventories are removed, the body now provides credible eight-port ownership, growth/closure accounting, rejected smaller alternatives, and enforcement; those prior blockers are resolved.

Not ready yet. Remove the remaining touched implementation/history narration under current AGENTS policy: packages/host-kit/src/internal/project-root.ts (resolver docblock/catch narration), internal/version.ts (memo and catch rationale), scripts/mutation/workspace-aliases.test.ts (Stryker-history block), and the extended history/control-flow blocks in vitest.mutation.config.ts. Names, the explicit alias-source type, and planted-red tests already own these invariants. Also normalize the body to the required concise Summary/Validation template. Exact-head Coverage, Repo Guards, native smoke, and mutation shards remain pending/queued; do not label until they complete without owner-action failures.

Copy link
Copy Markdown
Member Author

All four addressed on bc8ae93.

Scratch inventories gone. scratch-exports.json and scratch-symbol-map.json were the symbol→destination tables the one-shot migration script read; they had no consumers and should never have been committed. Deleted.

PR body rewritten to the eight-port ownership: what each port owns with its measured closure, the itemized growth against main (CLI +11, session-teardown +8, the two capture-kit rows +1 each) with the CLI's +11 attributed to the 11 façade files it reaches, and a section on why the three rejected shapes — four category barrels, deep façades in place, one file per subpath — each failed to keep a capability boundary.

Narration trimmed: the consumer roster on kernel/scoped-provider's withMethodScope (the mechanism sentence stays, the "shared by X and Y seams" list goes), and the pin/budget comments that restated what the assertion already fails on.

On "Affected decision-kernel mutants": that failure is an artifact of my own push, not a mutation regression. Two shard jobs — Mutants (selectors-2) and Mutants (kernel-errors) — were cancelled at 18:56:09/18:56:10 when I force-pushed 794fe4b over the 56039fe the run was measuring. The aggregate then found their artifacts missing and failed exactly as designed: assertCompleteSweep refuses to publish a pass envelope over an incomplete shard set. The shards that did finish came back green, and the snapshot-occlusion survivors printed in that log are the lane's report table, which by design never gates. The lane re-runs on this head.

Coverage, native smoke, and the mutation lane all re-run on bc8ae93; I'm watching until they land.


Generated by Claude Code

Copy link
Copy Markdown
Member Author

Both addressed on c8b6eb1.

Narration trimmed at all four sites: project-root.ts (docblock gone, catch reduced to the bare condition), version.ts (memo comment gone, catch rationale down to one clause), workspace-aliases.test.ts (the sandbox-history block down to the one constraint the fixture has to satisfy), and both extended blocks in vitest.mutation.config.ts.

PR body rewritten to ## Summary / ## Validation per docs/agents/pull-requests.md — behavior first, the tradeoff and its numbers called out, touched-file count and scope noted, evidence in prose.

On the mutation lane — I can now name the mechanism, and it is mine. Both failures have the identical shape: the kernel-errors and selectors-2 shards were cancelled mid-run (18:56:09/18:56:10, then 19:16:04/19:16:04), and the aggregate correctly refused to publish a pass envelope over an incomplete shard set. Those two are the last shards to start and the longest to run, so they are always the pair still in flight ~16 minutes in — which is exactly when each of my pushes superseded the run. Every completed shard was green both times, snapshot-occlusion included.

So this is not a lane defect and not a mutation regression; it is my push cadence against a ~19-minute lane. c8b6eb1 is my last planned push on this branch, so the run it starts should be the first to reach the aggregate with all ten shards present. If it fails again with shards that completed, that is a real finding and I will treat it as one.


Generated by Claude Code

Copy link
Copy Markdown
Member Author

Correction to my Android write-up above: I said "I have no job-rerun permission from this session, so the lane re-runs on the next push." That was wrong. I had assumed it rather than testing it. I can re-run failed jobs, and I have just used that on #2100's Android smoke (run 33110065044), which hit the same smoke:automation-system native-alert step again at 39d5c1a.

That is the one re-run the standing-down comment entitles the failure to; if it comes back red on the re-run I will treat it as this stack's problem rather than as load, and root-cause the alert-dismissal path instead of comparing runs.

Nothing else in that write-up changes: the failure still reproduces byte-identically on unrelated PRs while main is green on the same lane, and the Android smoke passed on this PR's current head c8b6eb1.


Generated by Claude Code

Copy link
Copy Markdown
Member Author

CI is green on c8b6eb1, mutation lane included.

The mutation lane passed with all ten shards present (aggregate at 20:00:09). That closes the thread I have commented on three times, and the timings confirm the mechanism rather than just being consistent with it:

shard duration
selectors-2 17m54s
kernel-errors 16m19s
the other eight 4m30s – 10m47s

Those two run two to four times longer than everything else, so any push landing ~17 minutes into a run kills exactly that pair and nothing else — which is what happened on 56039fe, 794fe4b and bc8ae93, each cancelled at the 17-minute mark by my next push. Left alone, the lane takes about 22 minutes end to end and passes. No shard ever failed on its own.

One correction to my earlier account, since I stated it as fact without checking: I described those two as "the last shards to start". They are not — they start first (19:41:31 and 19:42:11, ahead of the other eight). They are simply the longest-running. The conclusion is unaffected, but the detail was wrong.

Also green on this head: Coverage, Repo Guards, Integration Tests, Typecheck & Package, Lint & Format, Compatibility & Provenance, Bundle Size, every CodeQL analysis, and all five Smoke Tests jobs — the Android native-alert step included, with no change to the alert path.


Generated by Claude Code

@thymikee

Copy link
Copy Markdown
Member Author

Re-reviewed c8b6eb19ef: host-kit’s eight-port ownership remains sound and all exact-head checks are green, including every native lane and mutation shard/aggregate. No functional blocker.

Not ready under current AGENTS comment policy. The cleanup shortened but retained explanatory narration in internal/project-root.ts, internal/version.ts, workspace-aliases.test.ts, and vitest.mutation.config.ts; the full touched tree also carries prose in host-kit/internal/tmp-dir.fixtures.ts, kernel/collections.ts, and related moved tests. Keep tool directives only; names and planted tests should own the invariants. Correct stale body claims too: the PR changes 598 files, not ~440, and current Android/mutation lanes are green rather than known-failing. Once comments/body are corrected, code review is clean.

Copy link
Copy Markdown
Member Author

Both addressed on a7c8df6.

Comments are tool directives only in the touched tree. Removed: the memo/catch rationale in internal/version.ts, the header and both docblocks in internal/tmp-dir.fixtures.ts (its fallow-ignore-next-line stays), the sandbox-fixture note in workspace-aliases.test.ts, and both remaining blocks in vitest.mutation.config.ts.

One judgement call worth flagging rather than burying: in kernel/collections.ts I kept a single line — // Linear-time trim; the regex form trips CodeQL js/polynomial-redos. — rather than deleting it. The policy allows "a brief citation to an external constraint that cannot be encoded", and this is one: without it the obvious simplification back to /^-+|-+$/g reintroduces a CodeQL finding, and no name or test states that. If you read the policy as excluding it too, say so and it goes.

A near-miss from this cleanup, for the record. Stripping the comment out of project-root.ts's empty catch left a bare } catch {}, which I first "tidied" into } catch { continue; } — that skips the current = path.dirname(current) step and makes the walk-up spin on the same directory instead of climbing. I caught it before committing; the catch is now genuinely empty, which lint accepts and which preserves the fall-through. Worth knowing that this particular empty block is load-bearing.

Body corrected: the file count now reads 598, and the Validation section states both lanes as green with the failures described as historical. The mutation paragraph now carries the shard timings that explain the three cancellations (kernel-errors 16m19s and selectors-2 17m54s against 4m30s–10m47s for the other eight), rather than the "known-failing" framing.


Generated by Claude Code

@thymikee

Copy link
Copy Markdown
Member Author

Re-reviewed a7c8df6e8b: the six-file delta removes the previously named host-kit/mutation narration and fixes the body’s 598-file and historical Android/mutation claims. No functional or ownership regression.

Not ready yet. The full touched tree still contains implementation/test-tour narration at packages/capture-kit/src/tmp-dir.fixtures.ts:1,7-12,17, packages/kernel/src/scoped-provider.ts:29-32, src/__tests__/test-utils/host-process-mock.ts:5-24,32-33, and src/platforms/web/agent-browser-lifecycle.test.ts:21-22; remove it under the current AGENTS policy. (fallow-ignore directives and the brief CodeQL constraint in collections.ts are legitimate exceptions.) Exact-head native, core, and mutation checks have restarted and remain incomplete, so the body must not claim this head is green until they finish. Architecture/code otherwise remains clean.

Copy link
Copy Markdown
Member Author

All four sites cleared on bb53b0e; the stack is rebased onto it (#2090 50b9ac6, #2100 974632f).

Removed: the header and both docblocks in capture-kit/src/tmp-dir.fixtures.ts (its fallow-ignore directive stays), the withMethodScope docblock in kernel/scoped-provider.ts, the whole factory docblock and consumer note in test-utils/host-process-mock.ts, and the mock note in web/agent-browser-lifecycle.test.ts. The collections.ts CodeQL line stays, per your read of it as a legitimate exception.

Body corrected on the green claim. The Validation section now says exact-head CI is still running and does not assert this head is green; it separates local evidence at this head from what completed on c8b6eb1, and notes that every head since differs from it only in comments.

On the Android smoke failure at a7c8df6 — that head is a comment-only change from c8b6eb1, where the same lane passed. git diff c8b6eb1..a7c8df6 has exactly one non-comment line:

-      } catch {
-      }
+      } catch {}

An empty block reformatted onto one line. Identical executable behavior, one run green and the next red, which is as clean a demonstration as this lane will produce that the failure is not caused by the change.

Worth recording from that same edit: stripping the comment out of that catch left it empty, and my first attempt to "tidy" it wrote } catch { continue; } — which skips the current = path.dirname(current) step and makes the project-root walk spin on one directory instead of climbing. I caught it before committing. The empty block is load-bearing and should stay empty.


Generated by Claude Code

claude added 2 commits August 27, 2026 20:39
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
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

Copy link
Copy Markdown
Member Author

bb53b0e went fully green — 33/33, mutation lane included. The aggregate passed at 20:51:57 with all ten shards present and each one green. Shard timings confirm the mechanism I described: kernel-errors 19m28s and selectors-2 18m59s against 4m23s–10m28s for the other eight. I held two finished commits unpushed for ~6 minutes rather than cancel that pair at the 15-minute mark for a fourth time.

Also green on that head: Coverage, Repo Guards, Integration Tests, Typecheck & Package, Lint & Format, Compatibility & Provenance, Bundle Size, every CodeQL analysis, and all five Smoke Tests jobs.

New head is f87c0fe; the stack is rebased onto it (#2090 55d3cfe, #2100 60347c3). The delta from bb53b0e is 11 files, 26 lines, comments only.

A broken reference this PR introduced

Following up the iOS lane led me to something I had wrong. packages/platform-apple/src/runner/ is the Node-side transport client; the XCUITest runner is Swift, under apple/runner/. This PR does touch one Swift file, and the line W1 rewrote points at a file that does not exist:

-//   TS twin: src/snapshot/mobile-snapshot-semantics.ts#isTapPointInsideViewport
+//   TS twin: packages/contracts/src/mobile-snapshot-semantics.ts#isTapPointInsideViewport

There is no packages/contracts/src/mobile-snapshot-semantics.ts — the module went to capture-kit, while isTapPointInsideViewport went to packages/contracts/src/snapshot-visibility.ts:138. The adjacent TS test: line was left at its pre-move path too.

That prompted a repo-wide sweep, and the problem is systemic rather than isolated: ~20 comment citations across 11 files point at src/utils/*.ts and src/request/*.ts paths this PR deleted. The densest cluster is packages/platform-apple/src/runner/host.ts, where all 12 section labels on the host-seam type are dead references. I confirmed every one resolved at merge-base 77b5be8, so the move orphaned them. Each now names the owning capability port (@agent-device/host-kit/command, @agent-device/kernel/ttl-memo, …), which survives further moves better than a path. ADR citations are left alone as dated records. src/platforms/boot-diagnostics.ts is still correct here — W3 moves it later — so it is untouched.

Verified at f87c0fe: typecheck, lint, format:check, check:layering (1501 files), check:fallow, eager-closure-budgets (356), plus the two guard tests whose assertion messages changed.

Two corrections to my Android write-up

Both were wrong, and I am flagging them rather than quietly dropping them.

"The surface still reported a Native alert label, so the dialog was up." No. Native alert is the fixture's own SectionCard title (AutomationLabScreen.tsx:214), on screen whenever that section is in view. The dialog's text is Automation confirmation, and its absence from that list leans the other way. The list is also capped to the top 6 interactive texts, and Alert result: … is a plain <Text> that would never appear in it — so it proves nothing either way.

"Take the budget from the same scaling the other live steps use." There is no such scaling anywhere in the repo — no CI factor, no env override. The failing step already uses the same shared 10000 literal as every sibling wait, which is also the product default.

The actual defect: alert dismiss (src/platforms/android/alert.ts:90-117) taps the button coordinates and returns handled: true with no confirmation the dialog closed, and silently falls back to Back when it cannot find a Cancel-like button — and Back does not run RN's cancel onPress, so Alert result: cancelled can never appear regardless of budget. That is a real bug, but the fix belongs in the scenario (a local waitForAlertClosed poll on alert get, plus asserting the dismiss returned button: 'Cancel'), which is outside this refactor. Proposing rather than pushing it.

One thing that weakens my iOS argument

I said this stack does not touch the iOS runner path. The TS side holds — nothing under packages/platform-apple/src/runner/ imports the moved modules. But the runner cache fingerprint (runner-cache-metadata.ts:245-264) hashes file bytes of every Swift source, so the comment-only Swift edit invalidates the cached artifact and forces a cold xcodebuild. That cannot change runner behavior, but it can produce the host load that trips TEXT_INPUT_COMMIT_NOT_OBSERVED. Worth stating plainly since it cuts against my own "does not touch that path" claim.

If that lane goes red again, the runner.log [DEBUG-1874] poll lines settle which it is: one line with a large t means an observation-latency race in the commit deadline; many lines with observedLen=7 means a genuine character drop the check correctly caught.


Generated by Claude Code

@thymikee

Copy link
Copy Markdown
Member Author

Re-reviewed exact head f87c0fe4fe: the narration blockers are removed, and the 11-file citation repair is behavior-neutral and points to valid owning host-kit/kernel/contract seams. The eight-port architecture, closure/size evidence, and updated PR body remain sound. No code finding.

Code-clean and ready for human review, but not yet merge-ready: exact-head iOS smoke and the mutation shards/aggregate are still pending. Reconfirm once those finish green.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Aug 27, 2026

Copy link
Copy Markdown
Member Author

Reconfirming as you asked: the exact head f87c0fe is 33/33 green, and the whole stack is green on its exact heads.

PR head result
#2088 f87c0fe 33/33 — mutation aggregate passed 21:19:43, all ten shards present and green
#2090 55d3cfe 18/18
#2100 60347c3 21/21

The iOS smoke and mutation shards you were waiting on both finished green. All five native Smoke Tests jobs passed here, alongside Coverage, Repo Guards, Integration Tests, Typecheck & Package, Lint & Format, Compatibility & Provenance, Bundle Size, command-docs-gate, Ownership and context budgets, and every CodeQL analysis.

The mutation timings held to the pattern on a second independent run — kernel-errors 19m30s and selectors-2 19m21s against 5m27s–10m07s for the other eight. That is now two consecutive undisturbed runs reaching the aggregate with a full shard set, which retires the cancellation thread for good: the lane was never failing, my pushes were killing the long tail.

All three Validation sections now state the completed exact-head evidence rather than saying CI is running.

One item deliberately left out of this stack, recorded so it is not lost: alert dismiss (src/platforms/android/alert.ts:90-117) taps and returns without confirming the dialog closed, and silently falls back to Back when it cannot find a Cancel-like button — and Back does not run RN's cancel onPress, so Alert result: cancelled can never appear regardless of the wait budget. The fix is a local waitForAlertClosed poll on alert get in live-automation-scenario.ts plus asserting the dismiss returned button: 'Cancel'. It belongs in its own PR off main; happy to open one if you want it.


Generated by Claude Code

@thymikee
thymikee merged commit e832325 into main Aug 28, 2026
33 checks passed
@thymikee
thymikee deleted the refactor/platform-substrate-wave branch August 28, 2026 05:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants