* 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>
Replay-compat corpus
.ad scripts as released versions of agent-device wrote them, each paired with the verdict
today's parser owes it: parses, or fails with an error code and the migration hint substring.
corpus.test.ts runs in unit-core and asserts every verdict through parseReplayInput — the same
composition replay/test use — so a grammar change cannot quietly alter which historical script
surface still loads. #1417.
The freeze rule
Never edit a script under scripts/ to make a parser change pass.
A corpus script is a recording that already exists in the wild; editing it deletes the evidence the gate is here to keep, and the pinned identities below make an edited script fail rather than pass. When a grammar change flips a verdict:
- Leave the script byte-identical.
- Update its
verdictinmanifest.tsin the same PR. - Say in the PR description which historical surface broke, and why the new refusal is the intended migration rather than a regression.
A verdict that changes from parses to fails is a compatibility break by definition. That is
allowed — the corpus only insists it be deliberate, reviewed, and paired with a migration hint good
enough to put in front of whoever recorded the script.
What is in the corpus
scripts/integration/andscripts/examples/— mined from the git history oftest/integration/replaysandexamples/test-app/replays.scripts/docs/— surfaces the released grammar wrote that those two suites never exercised (${VAR}parameterization, reserved/orderingenvrefusals,wait stableand landmark waits,target-v1annotations, retiredgesture rotatevelocity andgesture swipedurationMs).
What earns an entry
This is a parser-compatibility corpus, not a device matrix. An entry has to be the only witness of something:
- a shipped syntactic form no other entry's bytes contain, or
- a distinct migration refusal (error code + hint) no other entry already provokes.
So the same flow recorded on another platform, or re-recorded at an adjacent release with only
coordinates and labels changed, does not get an entry: it costs maintenance and adds no parser
leverage. Each entry's note says which form or refusal it is the sole witness of; if a new entry
cannot claim one, it does not belong here. Keep the corpus in the tens of entries, not a mirror of
the replay-fixture tree.
How "frozen" and "released" are enforced, not asserted
Provenance lives in manifest.ts. recordedBy is the released tag whose grammar produced the
surface, and provenance pins the bytes:
minedentries carry the git object id of the historical blob.corpus.test.tshashes the checked-in bytes and must reproduce that id, so a rewritten script cannot be made green by editing the manifest to match — the id is only obtainable from the released content.derivedentries (scripts/docs/) have no historical blob to point at, so their bytes are pinned by SHA-256. Only the bytes are machine-checked: thefromcitation naming the released grammar or doc that emitted the surface is reviewer-verified, so a derived entry has to be reviewed against its cited source at that tag (git show <tag>:<path>) before it lands.
Two sources, never invention. The form comes from the cited release — the script bytes are what
that tag's grammar or docs emitted. The verdict comes from today's parser, because a migration
refusal for a retired form did not exist at the tag that emitted it (v0.16.8 wrote
gesture rotate <deg> <x> <y> <velocity>; the hint telling you to drop the velocity is current
output). So copy the form from the release and paste the code/hint from an observed run — never
paraphrase either.
The kind is not a free choice: provenance-rules.ts fixes it by corpus area (scripts/integration/
and scripts/examples/ must stay mined, scripts/docs/ is derived), so an edited script cannot
be relabelled derived, re-pinned by digest, and thereby skipped by the history check. Both the unit
test and the verifier enforce that rule, and a new area must declare its kind before entries can live
under it.
pnpm check:replay-compat closes the other half: it re-derives every mined id from git history
(git rev-parse <recordedBy>:<path>) and checks every cited tag against git tag --list, so an
entry cannot claim a version that was never cut. It needs full history and tags, so it runs in its
own Replay-Compat Provenance CI job (fetch-depth: 0) rather than inside the shallow-clone-safe
unit lane, and any change under test/replay-compat/ selects it in pnpm check:affected.
Released surfaces only. A grammar state that only ever existed between commits is not compat
surface — git tag --contains <commit> decides, and unreleased shapes stay out (AGENTS.md,
"Unreleased API surface dies free"). The corpus is baselined against tags up to v0.20.0.
Adding an entry
- Find the surface at a release tag (
git show <tag>:<path>, or the released grammar/docs that emitted it) and copy it verbatim intoscripts/<area>/<name>.<tag>.ad. - Add a
manifest.tsentry withrecordedBy,provenance,covers, and anotenaming the form or refusal it is the sole witness of. For a mined entry the blob id isgit rev-parse <tag>:<path>; for a derived one it isshasum -a 256 <file>. Add the tag toREPLAY_COMPAT_RELEASED_TAGSif it is new. - Take the
verdictfrom what the current parser does with those bytes: start fromverdict: { kind: 'parses' }, run the suite, and if it refuses, paste the reported code and hint substring verbatim. Do not transcribe a hint from a release, an issue, or memory — the refusal is produced by today's parser, not by the tag that emitted the form. - Run
pnpm exec vitest run --project unit-core test/replay-compatandpnpm check:replay-compat.
Add an entry when a change retires, renames, or narrows a .ad form — the corpus is the record of
what that costs someone with a saved recording.