os.homedir() reads USERPROFILE on Windows and never HOME, so the
isolation setup and every test that moved home were still resolving the
developer's real home directory. Fixtures came back empty, real sessions
leaked into counts, and the code under test wrote plans, receipts and a
sync ledger into the real ~/.config/codeburn during a run.
USERPROFILE now joins the redirected variables and a shared setHome()
helper moves HOME and USERPROFILE together; every spawn env carries both.
The remaining Windows-only failures were POSIX-shaped fixtures: chmod
tricks that only toggle a read-only attribute on NTFS, pid 1 as an
always-alive lock holder, splitting paths on '/', bare tar, mode-bit
expectations, and per-file timeouts too short for the cost of a tsx spawn.
Each gets a platform-aware equivalent so nothing is skipped on Windows and
the POSIX branches stay byte-for-byte what they were.
Full run on Windows: 267 files passed, 2 skipped, 3592 tests passed.
Deep-review probes proved three crash paths where a plugin could kill the entire push: an exporter exiting before reading stdin raised an unhandled EPIPE stream error; exporter stdout of a bare JSON string passed the null check and threw in enrichment; and a call record without a tools array crashed the turn-context build inside the promise executor. All three now cost only that plugin's contribution, with a stderr notice, pinned by regression tests mirroring the probes. Remote install additionally caps the manifest read at 64 KB.
- Add buildTurnContextMap() helper to compute turn context (turnId, category, retries, hasEdits, oneShot) for calls grouped by session
- Include optional turn field in exporter stdin JSON when call is resolvable to a turn
- Create CODEBURN_PLUGIN_STATE_DIR (~/.config/codeburn/plugin-state/<plugin-name>) before spawning exporters
- Pass CODEBURN_PLUGIN_STATE_DIR to exporter processes; plugins write state outside their signed tree
- Add tests for turn context inclusion, state directory creation, and calls without turns
Per-call attributes and span attributes now pass through filterPluginAttributes to enforce the #1128 sanitizer, rejecting credential/path-like stringValues and dropping non-scalar values (array, object). Fixes security regression where raw strings bypassed sanitization. When multiple plugins contribute attributes for the same deduplicationKey, they now merge into one list instead of overwriting, enabling multi-plugin enrichment of the same call. Tests added for sanitization of path-like values, rejection of arrayValues, and merging across plugins.
Loaded plugins with exporters/sync.mjs now enrich sync pushes with per-call attributes and declared extra spans (CB-3 phase 2). CLI spawns exporter child processes, passes batch calls via stdin, collects JSON output with perCall Map and extra spans array. All enrichment is guarded: perCall attrs attached to matching deduplicationKey, extra spans filtered by declared spanKinds, attributes filtered by declared syncAttributes, core keys never overwritten. Exporter timeout (30s), size cap (8MB stdout), and failures are handled gracefully - plugin contributes nothing on crash/timeout/bad JSON, stderr notice printed, push completes. Extra spans are capped at 2x calls.length and >64KB spans dropped. No ledger on CLI side for extra spans (plugin's responsibility). Dry-run shows exporter contributions. Wire is byte-identical when no exporters present.