Skip to content

refactor: pnpm workspace + @agent-device/kernel pilot (#1490 W0) - #1494

Merged
thymikee merged 5 commits into
mainfrom
claude/agent-device-scope-review-bmrfyc
Jul 30, 2026
Merged

refactor: pnpm workspace + @agent-device/kernel pilot (#1490 W0)#1494
thymikee merged 5 commits into
mainfrom
claude/agent-device-scope-review-bmrfyc

Conversation

@thymikee

Copy link
Copy Markdown
Member

Summary

W0 of #1490: the workspace gains packages/*, and the kernel becomes the pilot package with an enforced public API — proving the whole toolchain on the module the layering spine already certified dependency-free.

  • packages/kernel (@agent-device/kernel, private: true): nine consumer-earned subpath exports (errors, device, snapshot, contracts, collections, rect, redaction, daemon-error, bounds), each types+default pointing at source — NodeNext resolves them for tsc, Node loads them under --experimental-strip-types (the pnpm symlink realpaths outside node_modules), and tsdown bundles them (noExternal). utils/bounds.ts moves in as Rect vocabulary per the W0 audit; timeouts/xml/version/exec stay root-side (audit verdicts in the issue thread — none of them is honestly kernel vocabulary).
  • Repo-wide codemod: every kernel import becomes @agent-device/kernel/<sub> (~800 files, mechanical). Kernel tests move to src/__tests__/kernel/ and exercise the package's public surface (they need root test-utils/replay fixtures, which a package may not import).
  • R1 retires, R11 arrives: R1 kernel-sink is physically subsumed (a package cannot import root src/ at all). New scripts/layering/package-boundaries.ts (R11) guards the four bypasses resolution alone can't see: package→root imports, relative tunnelling into packages/*/src (the sole R8-script exception derives permitted targets from the exports map), undeclared workspace deps, and non-exported subpaths — plus runtime import.meta.resolve pins that deep specifiers are resolution errors.
  • Graphs stay connected across the seam: resolveImportEdges and mutation ownership resolve workspace specifiers through exports maps (derived, never hand-listed) — without this, kernel-errors would have silently lost all 495 of its owned tests and the P0-pinned depgraph contract (test: pin daemon modularity migration contracts #1487) stays satisfied. listSourceFiles includes packages/*/src, kernel classifies as a deliberately unranked zone, and the mutation registry/stryker globs/mutation-affected.yml path filter follow the move (caught by the workflow-coverage test).
  • Selector/config wiring: check:affected gains packages/ ownership with manifests failing open; vitest + coverage include packages/*/src; fallow ignores packages/** (its resolver cannot follow workspace specifiers); Testing Matrix row added.
  • One CI job change: affected-selector now installs dependencies. Its entry closure reaches workspace specifiers through src/utils, and the R8 relative-import exception is unsafe for production files — Node's ESM loader does not realpath, so a module loaded both relatively and via specifier instantiates twice in one process (duplicate AppError, broken instanceof). That leaves zero zero-dep jobs; the R8 vacuousness guard pins the empty set with this rationale instead of passing silently.

Disclosed deviation from the brief: per-package TypeScript project references are not used — allowImportingTsExtensions requires noEmit, which conflicts with composite declaration emit. The root tsconfig includes packages/ in one graph instead (exports maps still resolve per-package); revisit when a package needs an independent build.

Validation

  • pnpm check:layering — R2–R11 green: 939 source files (packages included), R6 at 7, R9 at 102, R10 unchanged, R11: 1 package behind 9 exported subpaths, zero root back-imports; 46 layering model/policy/R11 tests pass, including the runtime resolution pins.
  • Full unit suite (unit-core + subprocess-stub) green on package specifiers; mutation suite 45/45 (including the workflow path-filter coverage test that caught the mutation-affected.yml update); depgraph 22/22 against the P0-pinned contract; selector suites 36/36.
  • pnpm build + check:bundle-owner-files + pnpm test:smoke (24 passed, incl. packaged-CLI and every-export-subpath smokes): zero @agent-device strings in emitted dist/ JS, runtime dependency manifest unchanged (@limrun/api, yaml only — the workspace dep is a devDependency).
  • check:fallow (no issues, 858 changed files) and check:production-exports green; format/lint/typecheck (root + examples/sdk) green.
  • Full pnpm check aggregate was still completing at push time; every constituent gate above passed individually. Size report should read ≈ 0 — worth eyeballing the chunk table, since the four kernel sdk-chunk regexes moved to packages/ paths.

Part of #1490 (W0 checkbox). Next: W1a (@agent-device/contracts + ProviderDeviceRuntime inversion) once this lands.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep


Generated by Claude Code

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
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-07-30 10:14 UTC

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 1.88 MB 1.88 MB 0 B
JS gzip 605.4 kB 605.4 kB -1 B
npm tarball 724.0 kB 724.1 kB +34 B
npm unpacked 2.53 MB 2.53 MB +149 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 28.1 ms 27.6 ms -0.5 ms
CLI --help 59.1 ms 58.3 ms -0.9 ms

Top changed chunks: no changes in the largest emitted chunks.

…c -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

Copy link
Copy Markdown
Member Author

All five review findings addressed in the latest push:

  1. [P1] Source-inspection testcontracts-schema-public.test.ts reads the kernel source at packages/kernel/src/errors.ts. This was the Coverage failure and the initial-suite failure behind every non-kernel mutation shard.
  2. [P1] Mutation discovery — root cause was the Stryker sandbox: pnpm's node_modules symlink resolves @agent-device/* back to the real repo, so sandbox mutants never load and vitest.related finds zero tests. vitest.mutation.config.ts now aliases each exported specifier to its source file — entries derived from the exports maps, never a wildcard, so the lane cannot resolve internals R11 forbids. Validated end-to-end locally: mutation:run --modules kernel-errors dry-runs 3,984 tests from the 438-file derived scope and completes with mutants killed, exit 0.
  3. [P1] Exports-map resolutionresolveImportEdges takes an exports-derived workspaceSpecifierTargets map (check.ts and depgraph pass it), so a bare .-facade specifier like the planned @agent-device/provider-* resolves instead of silently dropping out of layering/depgraph analysis. The positional fallback survives only for map-less fixtures (the test: pin daemon modularity migration contracts #1487 P0 pin).
  4. [P1] Scope blocker resolved by implementation, not spec changepackages/kernel/tsconfig.json is now composite (emitDeclarationOnly → gitignored dist-types/), the root references it, and typecheck is tsc -b. Probed under TypeScript 7 native: deliberate type errors on both the root side and the kernel side are caught. The PR-body deviation note is void.
  5. [P2] R8 exception scoped to reality — the relative route into packages/*/src now requires membership in an actual zero-dep job closure (zeroDepClosureFiles walks the derived entries), not scripts/ placement. With zero zero-dep jobs today, no file qualifies — the dual-instantiation bypass is closed and pinned by test.

Also from the review discussion: daemon-error left the kernel package for src/client/ — its consumers (cli.ts, agent-device-client.ts) rehydrate wire DaemonErrors client-side; the daemon only produces them. Kernel ships with 8 subpaths, corrected before any of them are released surface.

Local gates on the new head: full unit suite 4,868 passed, layering chain 46/46 with R11 at 8 subpaths / zero back-imports, depgraph 22/22, mutation suite 45/45, typecheck (tsc -b + examples/sdk), lint/format, check:fallow + check:production-exports green.


Generated by Claude Code

claude added 2 commits July 30, 2026 09:01
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
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
@thymikee

Copy link
Copy Markdown
Member Author

Not ready. The package extraction direction is sound, but two blockers remain. First, R11's specifier scanner only recognizes single-quoted from/import sites; a double-quoted relative import or re-export into packages/*/src never reaches checkRootSites, so pnpm check:layering can pass a route the new rule claims to forbid. Parse specifiers through the existing import graph/AST machinery or make the scanner complete, with direct double-quote and re-export regressions. Second, the fresh-checkout FreeRange topology is still broken by project-reference declaration outputs: check:freerange runs before typecheck, so missing packages/kernel/dist-types/*.d.ts produces TS6305 errors. The latest commit does not change that wiring. Ensure every standalone CI gate can resolve/build the package from a clean checkout, then rerun exact-head CI. Also verify Fallow no longer treats vitest.mutation.config.ts as unreachable rather than suppressing the audit.

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

Copy link
Copy Markdown
Member Author

Both blockers addressed at 0892048:

R11 scanner completenessspecifierSites now delegates to the layering model's own parseImports instead of a private single-quote regex, so both quote styles, side-effect imports, re-exports, and dynamic imports all reach checkRootSites/checkPackageInternalSites through the one parser the rest of the gate already trusts. Direct regressions added for each formerly-invisible form: double-quoted import, double-quoted export … from re-export, double-quoted dynamic import into packages/*/src, and a double-quoted export * escape from inside a package (10/10 in the R11 suite, 47/47 across the layering chain).

FreeRange / fresh-checkout topology — this was fixed by 1e4795d (the comment appears to have raced that push): the root references edge is gone, so plain tsc -p — fr's mode — resolves kernel exports to source with no build precondition; the composite kernel project is built by tsc -b packages/kernel inside typecheck only. Clean-state evidence on this head: with packages/kernel/dist-types/ and every *.tsbuildinfo deleted, pnpm check:freerange reports 0 findings — no TS6305 is possible because nothing references declaration outputs anymore. CI's FreeRange re-run on 1e4795d was already in progress when this landed; 0892048 re-runs it again on exact head.

Fallow reachabilityvitest.mutation.config.ts is now a declared entry in .fallowrc.json, not a tolerated finding: the full-repo audit's unused-files count drops 2 → 1, with the remaining one (scripts/lib/run-as-main.ts) pre-existing on main and untouched by this PR. The changed-code gate is green with zero suppressions added.

Local gates on 0892048: layering 47/47 (R11 at 8 subpaths, zero back-imports), format/lint, tsc -b packages/kernel + root + examples/sdk, check:fallow clean.


Generated by Claude Code

@thymikee

Copy link
Copy Markdown
Member Author

Clean re-review at 0892048. R11 now uses the shared layering import parser, with regressions for both quote styles, static/dynamic imports and re-exports, so the tunnelling bypass is closed. Root project references were removed while the kernel remains composite and explicitly built by typecheck, restoring standalone clean-checkout FreeRange behavior. vitest.mutation.config.ts is a declared Fallow entry rather than a suppression. Exact-head Layering, FreeRange, Fallow, typecheck/build/package, integration/coverage/platform smoke, and all mutation lanes—including kernel-errors—are green. The package remains a narrow private leaf with exports-derived enforcement; no remaining findings, ready for human merge.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Jul 30, 2026
@thymikee
thymikee merged commit 76453ad into main Jul 30, 2026
40 checks passed
@thymikee
thymikee deleted the claude/agent-device-scope-review-bmrfyc branch July 30, 2026 10:12
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