Roadmap — how Wyren was built
import { Badge } from ‘@astrojs/starlight/components’;
Timeline
Section titled “Timeline”| Chunk | Hours | Name | Status |
|---|---|---|---|
| 0 | Pre-build | Documentation site | |
| 1 | 0-6 | Distiller quality gate | |
| 2 | 6-14 | Plugin skeleton + injection | |
| 3 | 14-22 | Distiller wired to Stop hook | |
| 4 | 22-32 | Git sync layer | |
| 5 | 32-44 | Broadcast + polish + demo | |
| Post-ship | 2026-04-23 | Cross-platform installer | |
| Post-ship | 2026-04-23 | Live sync + fault hardening | |
| Post-ship | 2026-04-23 | Code review + live testing polish | |
| Post-ship | 2026-04-24 | Weighted Tier 0 + install polish | |
| Post-ship | 2026-04-24 | Windows CI fix | |
| Post-ship | 2026-04-24 | E2E fixes + CLI polish | |
| Post-ship | 2026-04-24 | Parallel code review + integration | |
| Post-ship | 2026-05-06 | Install file cleanup | |
| Post-ship | 2026-05-08 | Distiller auth fix + SessionStart timeout | |
| Post-ship | 2026-05-09 | Sync integrity: Peer pushed + force-push detection | |
| Post-ship | 2026-05-09 | Code audit + docs consistency pass |
How the build was sequenced
Section titled “How the build was sequenced”- Each chunk had exit criteria. The next chunk did not start until current criteria passed.
- Chunk 1 was the go/no-go gate. If distiller quality had failed, the project would have pivoted to handoff-only. All downstream infra depends on it.
- Living docs discipline. Each chunk ended with a docs update. Docs shipped with code.
Chunk 0 — Documentation site (this site)
Section titled “Chunk 0 — Documentation site (this site)”Goal: every teammate can read this site cold and answer: what is Wyren, what problem, what’s the stack, what ships when, how much it costs, how to install.
Stack: Astro Starlight → GitHub Pages. Markdown content, built-in search, dark mode, Mermaid diagrams.
Exit criteria:
- Deployed URL reachable; sidebar + search + dark mode all work.
- Unseen teammate reads site for 10 min and can answer five core questions unaided.
- Site committed + pushed; Actions green.
Chunk 1 — Distiller quality gate (Hours 0-6) ✅
Section titled “Chunk 1 — Distiller quality gate (Hours 0-6) ✅”Gate passed. distiller.mjs + lib/transcript.mjs + lib/memory.mjs + prompts/distill.md. Tested on a real 828-line planning transcript: 34-line final memory, hygiene test passed (resolved item correctly dropped on incremental pass), blind A/B 3/3 non-obvious facts captured.
Key detail: subprocess runs with claude -p --no-session-persistence — prevents session state from contaminating the distill call. (--bare was removed in v0.4.3: it also stripped OAuth/keychain auth, causing “Not logged in” errors in detached processes.)
Full Chunk 1 detail + test results →
Chunk 2 — Plugin skeleton + injection (Hours 6-14) ✅
Section titled “Chunk 2 — Plugin skeleton + injection (Hours 6-14) ✅”Shipped. Plugin hooks wired via ~/.claude/settings.json. SessionStart hook reads .wyren/memory.md + broadcast files, injects as additionalContext. wyren init sets up .wyren/ structure. 17 unit tests green. E2E verified via hook pipe test.
New files: .claude-plugin/plugin.json, hooks/hooks.json, hooks/run-hook.cmd, hooks/session-start.mjs, hooks/stop.mjs (stub), bin/wyren.mjs, lib/util.mjs.
Chunk 3 — Distiller wired into Stop hook (Hours 14-22) ✅
Section titled “Chunk 3 — Distiller wired into Stop hook (Hours 14-22) ✅”Shipped. stop.mjs spawns distiller detached after 5 turns (or 2min idle since last distillation). Tier 0 regex filter in lib/filter.mjs skips API calls when the transcript slice has no signal words or Edit/Write tool use. Default model changed to Haiku 4.5. distiller_running lock prevents concurrent runs. 29 unit tests green.
Key detail: the Tier 0 regex matches the rendered transcript format ([tool_use Edit]), not raw JSONL — a subtle but important distinction since the distiller operates on rendered prose, not the raw event stream.
Chunk 4 — Git sync layer (Hours 22-32) ✅
Section titled “Chunk 4 — Git sync layer (Hours 22-32) ✅”Shipped. lib/sync.mjs — GitSync with pull() (fetch + scoped checkout of .wyren/ files, 3s timeout, WYREN_SKIP_PULL escape), push() (commit + retry-on-conflict, reset --mixed FETCH_HEAD on conflict so local HEAD stays in sync — no infinite re-conflict loop), lock() (atomic openSync('wx'), 60s stale-steal). Session-start pulls before injecting context; distiller pushes after atomic write. wyren status and wyren distill [--force|--push|--dry-run] CLI commands. 38 unit tests green (including two-machine conflict scenario).
Key implementation detail: conflict resolution uses reset --mixed FETCH_HEAD rather than --theirs + rebase --continue. Safer on Windows (no GIT_EDITOR needed), leaves working tree untouched outside .wyren/, and correctly advances local HEAD to remote tip.
Chunk 5 — Broadcast + polish + demo (Hours 32-44) ✅
Section titled “Chunk 5 — Broadcast + polish + demo (Hours 32-44) ✅”Shipped. Skills/CLAUDE.md broadcast via .wyren/broadcast/. wyren broadcast-skill <name> CLI copies a local skill file to .wyren/broadcast/skills/ for teammates to receive on their next SessionStart. Session-start injects broadcast content as additional context; an acknowledgment instruction prompts Claude to announce loaded skills in its first message. 46 unit tests green (6 broadcast-skill + 9 session-start + rest from prior chunks).
Exit criteria: full scripted demo runs end-to-end in under 4 minutes without intervention.
Post-ship — Deployability v1 (2026-04-23) ✅
Section titled “Post-ship — Deployability v1 (2026-04-23) ✅”Shipped. Cross-platform installer closes the biggest adoption blocker — teammates on macOS had no automated install path.
New files: install.sh (macOS/Linux one-liner), install.ps1 (Windows one-liner), scripts/installer.mjs (shared Node logic — preflight, symlink/junction, settings.json JSONC-tolerant patch, atomic write, verify, update, uninstall, doctor).
New CLI subcommands: wyren install, wyren update, wyren uninstall, wyren doctor. setup.ps1 shrunk to deprecation stub. CI matrix added: ubuntu unit tests + macOS + Windows e2e. Heavy code review caught 3 Important bugs before merge.
Test totals after this work: 79 unit tests + 27 e2e tests = 106 total.
Post-ship — Live sync + fault hardening (2026-04-23) ✅
Section titled “Post-ship — Live sync + fault hardening (2026-04-23) ✅”Shipped. B’s running session now receives A’s new memory automatically — no restart required.
New files: hooks/user-prompt-submit.mjs (UserPromptSubmit hook — pulls .wyren/memory.md on each user turn with a 1.5s fetch cap + 3s hook budget, diffs against a stored snapshot, injects only the delta as additionalContext), lib/diff-memory.mjs (pure section-aware diff + hash utilities, no deps).
State file: .wyren/state/ups-state.json — owned exclusively by the UPS hook (stores snapshot hash + last-pull timestamp). WYREN_SKIP_PULL=1 skips the pull; diff still runs from disk.
Fault injection testing caught two bugs before they reached users: (1) EISDIR crash when .wyren/state/ directory exists but ups-state.json is absent; (2) watermark race between Stop hook and UPS — resolved by giving each hook exclusive ownership of its own state file. windowsHide: true added to remaining spawnSync calls.
New test files: tests/fault-network.test.mjs, tests/fault-corruption.test.mjs, tests/fault-concurrency.test.mjs, tests/fault-e2e-livesync.test.mjs (53 fault tests).
Test totals after this work: 131 unit tests + 32 e2e tests = 163 total.
Post-ship — Code review + live testing polish (2026-04-23) ✅
Section titled “Post-ship — Code review + live testing polish (2026-04-23) ✅”Shipped. Two-machine live test surfaced 9 additional bugs; systematic code review caught 9 more. All fixed before any second user touched the plugin.
Key fixes:
- Installer:
${CLAUDE_PLUGIN_ROOT}doesn’t expand insettings.json(only in pluginhooks.json) — installer now writes absolute repoDir path. UTF-8 BOM insettings.json(written by PowerShell) crashed the JSONC parser — stripped on read.wyren install+wyren updatenow register thewyrenCLI globally vianpm install -g;wyren uninstallderegisters and deletes the clone. - UPS hook: fetch timeout 1s→1.5s, hook budget 2s→3s — fixes timeouts on higher-latency connections.
- Stop hook:
distiller_runningcould get permanently stuck if the OS killed the distiller process mid-flight — PID liveness check added toshouldDistill.WYREN_TURNS_THRESHOLD+WYREN_IDLE_MSenv vars added for test-cycle acceleration. - sync.mjs:
resetWatermarkTurnsused non-atomic write and didn’t cleardistiller_runningon conflict recovery — both fixed. - Tests:
WYREN_ROOTin three fault test files used raw URL pathname (spaces →%20→ ENOENT);fault-concurrencyhad a hardcoded machine path. All fixed withfileURLToPath.
Test totals unchanged: 131 unit + 32 e2e = 163 total (all passing).
Post-ship — Filter upgrade + install polish (2026-04-24) ✅
Section titled “Post-ship — Filter upgrade + install polish (2026-04-24) ✅”Shipped. Tier 0 filter overhauled from a simple presence-check regex to a weighted scoring system. Parallel agent session delivered the changes; reviewer session caught and fixed 4 bugs before merge; installer files polished.
Key changes:
lib/filter.mjs:scoreTier0()— categorized signal weights (1–3), structural scoring on raw JSONL lines (session length, avg user message length, edit count),WYREN_TIER0_THRESHOLDenv var (default 3),MultiEdittool detection.hasTier0Signal()is backwards-compatible, logs score + breakdown to.wyren/log.distiller.mjs: passes rawslicedlines tohasTier0Signalso structural signals score correctly.bin/wyren.mjs:wyren initseedsmemory.mdfromCLAUDE.mdif present (8 KB cap, skips empty/directory, one-time import).- Install files: executable bits set in git (
100644→100755) forbin/wyren.mjs,hooks/run-hook.cmd,install.sh,scripts/installer.mjs;install.shnow respectsWYREN_HOME;install.ps1$Args→$WyrenArgs(PS reserved variable),$clonequoted for space-in-path safety;installer.mjssurfaces npm stderr on CLI registration failure. - Reviewer fixes:
scoreTier0(null/undefined)no longer throws;EISDIRcrash whenCLAUDE.mdis a directory; emptyCLAUDE.mdskipped;test-e2e.mjsH1/H4 assertions updated for absolute-path installer (stale${CLAUDE_PLUGIN_ROOT}check).
Test totals: 137 unit (136 pass, 1 skip POSIX-only) + 32 e2e = 169 total.
Post-ship — Windows CI fix (2026-04-24) ✅
Section titled “Post-ship — Windows CI fix (2026-04-24) ✅”Shipped. Two targeted fixes to make CI green on Windows Server 2022.
-
scripts/installer.mjsinspectLink()— extractedstripWinPathPrefix()helper strips both\\?\(Win32 extended) and\??\(NT namespace) prefixes fromreadlinkSyncoutput. Windows Server 2022 returns the NT-namespace form; the old code only stripped Win32-form, causing junction idempotency false-positives and install failures. -
tests/fault-network.test.mjstest 59 — switched remote URL fromgit://localhost:9/nonexistent.gittofile:///nonexistent-wyren-test-remote. Thegit://scheme spawns network helpers on Windows that held a handle to the temp directory, causingrmSyncto throwEBUSY. Thefile://scheme fails immediately with no helpers spawned.
Test totals unchanged: 169 total, CI green on all platforms.
Post-ship — E2E fixes + CLI polish (2026-04-24) ✅
Section titled “Post-ship — E2E fixes + CLI polish (2026-04-24) ✅”Shipped. Two e2e test fixes plus CLI quality-of-life improvements.
- G18 fix:
spawnStopHooksgets anextraEnvparameter; G18 passesWYREN_TURNS_THRESHOLD: '100'so Windows process-startup stagger cannot accumulate to the default threshold of 5 and trigger a distiller reset mid-test. - H4 fix: old hook seed hardcoded the actual
WYREN_ROOTpath, causing the test to always fail on any dev machine other than the original. Replaced with a fictional path. wyren log [--lines N]: tail the distiller log from any directory (default 50 lines).-nshorthand supported.wyren --version/-v: printwyren <version>frompackage.json.wyren --help/-h: print command reference;wyrenwith no args also shows help (exit 0). Unknown commands printwyren: unknown command '<X>'+ help to stderr (exit 1).
Test totals: 169 total, e2e 32/32.
Post-ship — Parallel code review + integration (2026-04-24) ✅
Section titled “Post-ship — Parallel code review + integration (2026-04-24) ✅”Shipped. Three-agent parallel review pass caught logic, reliability, and quality-of-life issues. 28 additional unit tests added.
Key changes:
sync.mjspush(): stages paths separately so absence of.wyren/broadcast/dir no longer aborts thememory.mdpush._rebase()also checks out.wyren/broadcastfromFETCH_HEAD.stop.mjs: skips settingdistiller_running/ resettingturns_since_distillif spawn produces no PID. Prevents stuck-forever state on failed spawn.lib/filter.mjs: NaN guard onWYREN_TIER0_THRESHOLDparse (falls back to default3).distiller.mjs: lock error handling consolidated — any failure skips push and exits conservatively.wyren status: shows human-readable progress (N/5 turns until next distill); lock line hidden when not held; init hint updated togit add .wyren/.tests/transcript.test.mjs: new file — 17 tests coveringlib/transcript.mjs(previously zero coverage).- 11 additional tests across stop, filter, session-start, and fault-corruption test files.
Test totals: 166 unit (164 pass, 1 skip POSIX-only, 1 flaky-under-load) + 32 e2e.
Post-ship — Install file cleanup (2026-05-06) ✅
Section titled “Post-ship — Install file cleanup (2026-05-06) ✅”Shipped. Installed clone now contains only runtime files — no internal docs, no installer shims.
cleanInstall(): deletesCLAUDE.md,README.md,install.sh,install.ps1from clone after every clone or update. These are not needed at runtime;CLAUDE.mdin particular contains internal project context that should not ship to users.git update-index --skip-worktree: set on every deleted file sogit diff HEADstays clean andwyren updatedoesn’t refuse with “local changes detected.”- Heal step: before the dirty check in
cloneOrUpdate, anyROOT_FILES_TO_REMOVEmissing from disk have skip-worktree applied — handles upgrades from old installs that deleted files without marking them. applySparse(): runs in both the clone and update paths ofcloneOrUpdate. Cone mode excludestests/,docs-site/,.github/. Always called aftergit reset --hard(which clears skip-worktree bits).installer.mjsself-contained:isMaininlined, no import fromlib/util.mjs. Required for bootstrap reliability when onlyscripts/is sparse-materialized.- Bootstrap shims:
install.shandinstall.ps1do a plaingit clone --depth=1. Installer owns the full sparse + cleanup lifecycle via the update path ofcloneOrUpdate.
Post-ship — Distiller auth fix + SessionStart timeout (v0.4.3, 2026-05-08) ✅
Section titled “Post-ship — Distiller auth fix + SessionStart timeout (v0.4.3, 2026-05-08) ✅”Shipped. Two fixes for distiller reliability and hook stability.
distiller.mjs: Removed--bareflag (stripped OAuth/keychain auth → “Not logged in” in detached processes) and--tools ''(flag removed from CC CLI). Now uses--no-session-persistenceonly.scripts/installer.mjs: Raised SessionStart hook timeout from 2s to 4s in the installedsettings.jsonentry, providing a 2s buffer for Node startup + git operations above the 2s internal cap.
Test totals: 185 pass / 187 total (2 skip POSIX-only) + 32 e2e.
Post-ship — Sync integrity (v0.4.4, 2026-05-09) ✅
Section titled “Post-ship — Sync integrity (v0.4.4, 2026-05-09) ✅”Shipped. Two observability and safety features for shared memory sync.
bin/wyren.mjs(wyren status): AddedPeer pushed:line — shows timestamp, author, and age of the last remote commit to.wyren/memory.md. Requires no network call; reads fromorigin/<branch>ref cached by the last fetch.hooks/user-prompt-submit.mjs: Added ancestry check after each pull. If the remotememory.mdcommit is not a descendant of the last-known remote commit, injects a ⚠️ warning into context: “remote memory.md was force-pushed — treat injected context with extra caution.” Fail-open; never blocks the session.tests/wyren-status.test.mjs: New test file — verifiesPeer pushed:line always appears inwyren statusoutput.
Test totals: 185 pass / 187 total (2 skip POSIX-only) + 32 e2e.
Post-ship — Code audit + docs consistency pass (v0.4.5, 2026-05-09) ✅
Section titled “Post-ship — Code audit + docs consistency pass (v0.4.5, 2026-05-09) ✅”Shipped. Multi-agent audit pass across all source and docs files.
hooks/hooks.json: SessionStart timeout corrected 2s→4s — matches what the installer writes tosettings.json. The 2s was too tight for Node startup + git operations above the 1.5s+0.5s internal cap.lib/sync.mjs: Removed staleI1:/I3:/I4:review-label prefixes from inline comments.distiller.mjs: Trimmed multi-line planning note into a single comment line.- Docs site: Purged all stale references —
--bare→--no-session-persistence(Chunk 1);pull --rebase/--theirs→ accurate recovery description (faq.md, how-it-works.md); “Tier 0 regex filter” → “Tier 0 weighted signal filter” (how-it-works.md, faq.md); SessionStart budget 2s→4s (hooks.md);Peer pushed:added to cli.md status example and description. - README.md: Limitation #2 rewritten to describe actual
_recoverNonFastForwardbehavior;wyren statustable entry updated withPeer pushed:.
Test totals: 185 pass / 187 total (2 skip POSIX-only) + 32 e2e.