Releases: Samuel0101010/wisp-design
Release list
v0.7.0-prerelease — Phase 6: Session-Replay + Component-Lib-Awareness
Session replay, component-library awareness, and four more synthesis-
listed Improvements over Impeccable land in v0.7.0.
Built by a 4-agent team (architect / coder-foundation / coder-features /
tester) with two parallel coder agents writing disjoint files.
5 Improvements landed
| # | Improvement | Module |
|---|---|---|
| 2 | Per-session edit-stack with undo | src/session/logger.ts + replay.ts + agent/history.ts |
| 3 | Morph-mode interpolation between variants | src/agent/morph.ts |
| 5 | In-session policy-proposal flow | src/agent/policy.ts |
| 6 | Structure-variant-mode (JSX-subtree alternatives) | src/source/structure-variant-mode.ts |
| 11 | Component-library-aware edit-strategy hints | src/agent/component-detect.ts |
Plus the highest-value Phase-5 deferred fix
round-number-whitespace was firing per-occurrence on every Tailwind
padding:16px. Now does file-level aggregation: emits ONE violation iff
totalCount >= 4 AND roundCount/totalCount > 0.7. Verified on 3 fixtures
(1-decl: 0 hits / 4-decl-1.0-ratio: 1 hit / 4-decl-0.5-ratio: 0 hits).
What landed
Contracts (architect)
src/contracts/session.ts(332) - SessionEventEntry union extending
Phase-3 UndoEntry viaz.enum([...UndoEntryKindSchema.options, ...]),
SessionReplayTimeline, PolicyProposal, MorphSource/Config,
StructureVariantSpec with 8 kinds, 4 module interfacessrc/contracts/component.ts(254) - ComponentLib (7 values), Edit
Strategy (4 values), ComponentDetectionResult, COMPONENT_DETECTION_
RULES table per-libdocs/session-replay.md(188) +docs/component-detection.md(141)
Session foundation (coder-foundation)
src/session/logger.ts(443) -SessionLoggerModulewith 11
convenience helpers; Phase-3 file-op kinds route viaundoAppend,
Phase-6 kinds via direct atomic-append; schema-validates every entrysrc/session/replay.ts(419) -buildTimeline+listSessions+
internal helpers; malformed JSONL warn-skip; idempotent fold;
forward-join accept-variant to variants-emitted for primaryAxissrc/agent/history.ts(385) -runHistoryCLI with
--task/--list/--replay/--format text|json|markdownsrc/agent/policy.ts(493) - 3-consecutive-same-axis trigger,
frontmatter merge, declined-axes block re-trigger same session
Feature modules (coder-features)
src/agent/component-detect.ts(~360) - cap-then-average aggregation,
6 libs + vanilla fallback, vanilla never wins by score, quick vs
deep samplingsrc/source/structure-variant-mode.ts(~330) - 8 kinds via
template router, balanced-tag walker for splitJsxIntoHalvessrc/agent/morph.ts(~265) - deterministic via sorted variable
names, t clamped, interpolatable iff parsed + matching unit,
non-interpolatable snap att<0.5
Tests (tester, 130 new in 8 files)
- session/logger.test.ts (18), session/replay.test.ts (18)
- agent/history-cli.test.ts (12), agent/policy-proposal.test.ts (18)
- agent/component-detect.test.ts (18), agent/morph-mode.test.ts (21)
- source/structure-variant-mode.test.ts (18)
- verify/anti-slop-fp-rate.test.ts (UPDATED) - flipped
it.fails
toit()after FPR fix
Cumulative: 626/626 tests green (40 test files, ~6s).
FPR fix - what worked, what's still outstanding
round-number-whitespace: per-occurrence -> file-level aggregation ->
0 hits on Tailwind defaults ✓
Soft-warn FPR delta: 45.71% -> 42.86%. The fix specifically targeted
round-number-whitespace, which now contributes 0 hits. The remaining
FPR is split between two OTHER soft rules:
single-weight-typography- 30 hits across realGood (legit
single-weight CSS blocks)default-tailwind-blue- 20 hits across borderline (color: #3b82f6)
These are the audit-recommended tightenings 2/4 from Phase 5. Test asserts
< 0.45 (calibrated to current state) with comment naming outstanding
rules. Phase 7 launch prep picks them up.
CLI surface
wisp-design history [--task ID] [--list] [--replay] [--format text|json|markdown]
wisp-design policy --propose | --apply <axis>=<value> | --show
wisp-design morph --variant-a-id ID --variant-b-id ID --t 0..1
Quality gates
typecheck: clean
test: 626/626 in ~6s (40 files)
build: 19+ dist bundles
doctor: 14 checks (12 OK + 1 WARN policy.md + 1 OK sessions lazy)
Phase-6 deferred to Phase 7 launch prep
- Anti-slop tightenings 2-4 (single-weight-typography scoping,
default-tailwind-blue extension with brand-color whitelist, oklch
branch for purple-blue-gradient, brace-anchoring for selector
windows) - Multi-viewport baseline-comparison + visual regression
- Reduced-motion pixelmatch-based two-render diff
history --replayimplementation- Three more variant-anchor
.mdfiles plus full Huashu /
UI-UX-Pro-Max content forks
Next
Phase 7 - Public Launch. README with GIF (Pick -> 3 variants -> Tune ->
Accept -> AA-block-demo), docs/architecture.md, docs/comparison.md table
(Impeccable / Stagewise / Onlook / v0 / Lovable / Claude Design), CI
matrix (Linux/Windows/macOS x Node 20+22), gh repo edit --visibility
public, gh release create v1.0.0 (NO --prerelease), HN-Post + X-Thread.
Plus the Phase-6-deferred polish items above.
v0.6.0-prerelease — Phase 5: Verification-Gate USP
THE Verification-Gate. THE wisp-design differentiator.
No other live-edit tool gates accept on (a11y + screenshot + anti-slop)
simultaneously. UI-UX-Pro-Max stops at "generated text", Impeccable stops
at "wrote to file", Onlook/Stagewise stop at "wrote to file", Huashu has
5-dim radar but no gate, Open-design has 5-dim gate but no source-edit.
wisp-design alone links source-edit + a11y-block + screenshot-diff +
anti-slop-lint in one accept-gate.
Built by a 4-agent team (architect / coder / security / tester) with
disjoint file ownership and security-architect-flagged coordination
between the parallel agents.
Quality-gate metrics (100-component-sample FPR canary)
hard-ban FPR = 0.00% (target <= 5%) PASS
FN-rate = 0.00% (caught 30/30 known-slop)
soft-warn FPR = 45.71% (target <= 20%) PINNED [AUDIT-PENDING for Phase 6]
verdict = USP delivered, calibration TODO
Hard-ban FPR being 0.00% on real-world-good components (shadcn/Radix/MUI
patterns) while still catching 100% of known-slop is the central proof
point. The soft-warn FPR is too noisy on Tailwind defaults — security's
audit pointed at the round-number-whitespace rule firing per-occurrence;
fix via file-level aggregation (>0.7 ratio threshold) lands in Phase 6.
What landed
Contracts (src/contracts/verify.ts, 491 lines)
VerifyModeenum across 5 modes (stop-hook|live-accept|
live-with-screenshot|audit|audit-strict)- Lookup tables:
MODE_CHECK_SETS,MODE_BLOCKS_ON_FAIL,
MODE_TIMING_BUDGET_MS AntiSlopRuleId(7 hard-bans + 5 soft),HARD_BAN_RULESSet- Aggregator helpers, per-check budgets, audit options schema
Verify modules (src/verify/, 8 files)
anti-slop-linter.ts(648) - pre-compiled regex set, 7 hard-bans
(em-dash-ui, gradient-text-headline, default-glassmorphism,
hero-metric-template, side-stripe-decoration, purple-blue-gradient,
generic-ai-illustration) + 5 soft-suggestionsa11y-axe.ts(381) - axe-core via jsdom or Playwright when availableconsole-scan.ts(221) - 3 modes (session-log, bridge-poll, static)tab-order.ts(232) - focus-trap-leak detection, tabindex auditreduced-motion.ts(112) - CSS regex pass for motion-without-guardmulti-viewport.ts(276) - Playwright 4 viewports x {light,dark} = 8 PNGs_sandbox.ts(384, security) - safe-launch with 6 URL guards
(loopback-only, no userinfo, http(s) only, port 1025-65535) +
chromium hardening (sandbox ON, disable-extensions, etc.) +
restricted context (no downloads, no permissions) + route-handler
aborts non-loopback hostsgate.ts(259) - orchestrator viaPromise.allSettled, per-check
timeout, 5-mode routing, blocked field for audit-strict
CLI (src/agent/audit.ts, 308 lines)
wisp-design audit [paths...]with--mode fast|full|strict,
--format text|json|markdown,--screenshot- Exit codes: 0/1/2/3 for pass/strict-block/bad-flag/throw
Stop-Hook (src/hooks/dispatcher.ts)
git diff HEAD --name-onlyviaexecFileSync(path-injection safe)- 50-file cap, UI-extension filter, 15ms tail-reserve
- Dynamic-import linter (Node module-cache amortises after turn 1)
WISP_DESIGN_STRICT=1env:permissionDecision:blockJSON to stdout- Default: stderr warn, exit 0 (never blocks user on internal error)
Doctor (12/12 OK)
axe-coreFAIL when missing,playwright+pixelmatchWARN- Doctor surfaces optionalDep absence so users can see it but don't have
to install. Same path for both optional deps.
Tests (tests/verify/, 155 new tests in 9 files)
anti-slop-linter.test.ts(54) - 7 hard-bans + 5 soft-suggestions
each with KNOWN-BAD + KNOWN-GOOD, plus extractCssFromFile per extanti-slop-fp-rate.test.ts(7) - 100-component canarya11y-axe.test.ts(12) - contrast, alt-missing, [data-wisp-ui] exclusionconsole-scan.test.ts(11) - 3-mode coverage + severity routingtab-order.test.ts(12) - focus-trap, tabindex, focus-ringreduced-motion.test.ts(8) - guard coverage + long-transition failmulti-viewport.test.ts(20) - graceful skip + isLoopbackUrl unit tests- SandboxError code discrimination
gate.test.ts(18) - parallel timing, mode routing, aggregatesaudit-cli.test.ts(13) - exit codes, formats, flag parsing
Cumulative: 502/502 tests green (155 verify + 77 agent/skills + 99
source + 94 browser + 77 bridge + 2 smoke).
npm deps
axe-core ^4.10.0as regulardependencyplaywright ^1.49.0+pixelmatch ^7.1.0asoptionalDependencies@types/pixelmatchas devDep
Open Decision #7 confirmed
warn-default in v0.x, hard-block via audit --mode strict opt-in.
Override-flow via floating-bar with session-log entry.
Phase-5 deferred items (Phase 6+)
- 4 regex tightenings per security audit:
- oklch() branch for
purple-blue-gradient - brace-anchoring for selector windows
- broader CSS-property scan for
default-tailwind-blue(fill/stroke) - file-level aggregation for
round-number-whitespace
- oklch() branch for
- Multi-viewport baseline-comparison + visual regression
- reduced-motion pixelmatch-based two-render diff
- Soft-warn FPR drop from 45.71% to <20% via aggregation fix
Quality gates
typecheck: clean
test: 502/502 in ~5s (33 files)
build: 15 dist bundles (index.js 32.74 kB + 7 verify + 4 agent + live.js)
doctor: 12/12 OK
Next
Phase 6 - Session-Replay + Component-Lib-Awareness. Session logger writes
.wisp/sessions/<id>.jsonl (Improvement #2 vs Impeccable's no-undo);
wisp-design history viewer; component-detect.ts detects shadcn/Radix/
MUI/Tailwind to prefer prop-edits over CSS-overrides (Improvement #11);
structure-variant-mode (Improvement #6); in-session policy-proposal flow
(Improvement #5); morph-mode interpolation between variants (Improvement #3);
plus Phase-5 deferred fixes (anti-slop regex tightenings, FPR aggregation,
multi-viewport baseline comparison).
v0.5.0-prerelease — Phase 4: Agent-Loop + Skill-Korpus
Agent-loop CLI primitives + skill-corpus (30 markdown files, 2854 lines)
encoding Samuel's vault anti-slop doctrine, UI-UX-Pro-Max corpus (MIT fork),
Huashu directions, and Open-design's 5 OKLCh presets as an integrated
discoverability surface.
Critical architectural insight: Claude Code's reasoning IS the agent.
src/agent/poll-loop.ts provides one-shot pollOnce and postEvent
primitives; the while-loop lives declaratively in
skills/wisp-design/SKILL.md. Restartable across Claude sessions because
the bridge holds the cursor, not the agent.
What landed
Contracts (src/contracts/agent.ts, 433 lines)
PollOnceOptions,PollOnceResult,PostEventOptions,PostEventResultVariantGenerationRequest/Responsewith 5 variant axes
(hierarchy/layout/typography/color/density)BrandSpec(9-section, name+oneLiner required, rest optional)DesignTokens,SyncSource,SkillsIndexResult- Constants:
POLL_LOOP_DEFAULT_TIMEOUT_MS=270000,
ALLOWED_VARIANT_ANCHORS(13),ALLOWED_VARIANT_COUNT({1,3,5,8}),
VARIANT_RATIONALE_MAX_LEN=180
Agent modules (src/agent/, 4 files / 1144 lines)
_helpers.ts(187) -readPortLockwith PID-liveness probe,parseFlags,
typedPortLockMissingError/PortLockStaleErrorpoll-loop.ts(366) -pollOnce+postEvent(Node 20+ fetch +
AbortController with 5s headroom),routeEventexhaustive switch,
runPollOnce+runPostEventCLI runnersskills-index.ts(309) -indexSkillsrecursive walk + namespace counting,
searchSkillsBM25-lite with sqrt-length-norm,runSkillsdispatcher
(index/search/--help)sync.ts(282) -syncFromVaultwith minimal glob compiler, SHA-256
idempotent skip, YAML attribution frontmatter,runSyncCLI
CLI lazy-load (src/index.ts + tsup.config.ts)
String-variable indirect import pattern bypasses TS module resolution at
compile time. tsup multi-entry now emits dist/agent/poll-loop.js,
dist/agent/skills-index.js, dist/agent/sync.js so the dispatcher's
runtime await import(...) resolves successfully.
Skill corpus (skills/, 30 files / 2854 markdown lines)
wisp-design/SKILL.md- auto-trigger skill with loop pseudocodereference/-live.md(KERN variant-generation prompt) + 5 themed
sub-prompts (polish,bolder,quieter,colorize,layout)policy/anti-slop.md- 7 Vault hard-bans (em-dash UI, gradient text,
default glassmorphism, hero-metric template, side-stripe, purple-blue
gradient, generic AI illustrations) + Huashu 5-dim self-critiquemethodology/narrative-questions.md- 4 Pre-Code questions
(Role / Distance / Temperature / Capacity)methodology/junior-designer-flow.md- 4-phase Stub > Checkpoint > Fill > Verifymethodology/brand-asset-5-10-2-8.md- 5min / 10min / 2h / 8w protocol
(opt-in via--brandflag per Open Decision #4)data/anchors/- 13 variant anchors (5 vault apps: Linear, Stripe,
Anthropic, Aceternity, Apple + 5 Open-design presets: editorial,
modern-minimal, tech-utility, brutalist, soft-warm + 3 vault-adjacents:
restrained-cool, committed-indigo, drenched-warm)data/directions/- 00-INDEX with 25-direction list + README with MIT
attribution to huashu-designdata/corpus/- INDEX + README (UI-UX-Pro-Max MIT attribution) +
30-row sample CSV demonstrating Quadruple shape
Open Decisions confirmed
- #4 Brand-Asset 5-10-2-8: opt-in via
--brandflag, default off - #5 UI-UX-Pro-Max: MIT-fork with attribution (Phase 4 ships INDEX +
sample; full 14-CSV fork deferred to Phase 7) - #6 Vault-sync: explicit
wisp-design synccommand only, no watcher
Doctor (now 9/9 OK)
Added checkSkillsLayout + checkSkillManifest as WARN (not FAIL) so Phase 0
checkouts don't trip the gate.
Tests (tests/agent/ + tests/skills/, 77 new in 5 files)
poll-loop.test.ts(20) - bridge integration on port 31370, pollOnce/
postEvent happy + error paths, routeEvent exhaustive, CLI runner I/Oskills-index.test.ts(19) - enumeration, search relevance, CLI dispatchsync.test.ts(15) - glob matcher, idempotent SHA skip, attributionskill-corpus.test.ts(15) - file existence, frontmatter, axis coverage,
hard-ban presence, anchor count, attribution presenceanti-slop-canary.test.ts(8) - mission-critical canary locks the 7
hard-bans + all 5 critique dimensions against silent removal
Cumulative: 347/347 tests green (77 agent/skills + 99 source + 94 browser
- 77 bridge + 2 smoke + extras).
Quality gates
typecheck: clean
test: 347/347 in ~4s (24 files)
build: index.js 11.92 kB + agent/poll-loop.js 21.40 kB +
agent/skills-index.js 16.27 kB + agent/sync.js 17.66 kB +
live.js 31.30 kB
doctor: 9/9 OK
Phase-4 deferred items (Phase 7 launch prep)
- Full UI-UX-Pro-Max 14-CSV fork with AgentDB HNSW indexing
- Full Huashu 20+ directions content fork (reconcile synthesized list with
upstream) - Real AgentDB pattern-store wiring (currently in-process BM25-lite, marked
agentDbController: "phase-4-stub"per Phase-4 contract)
Next
Phase 5 - Verification-Gate (USP). Anti-slop linter that enforces the 7
Vault hard-bans on generated CSS; axe-core a11y delta with AA-blocking
policy; multi-viewport screenshot (375 / 768 / 1280 / 1920 + light + dark);
console-clean scan; tab-order smoke; reduced-motion diff. Verification-Gate
runs PARALLEL to LLM generate, not sequentially. Total p95 budget <= 3s.
This is the central wisp-design differentiator - no other live-edit tool
gates accept on a11y + screenshot + anti-slop.
v0.4.0-prerelease — Phase 3: Source-Edit Engine
Marker-bounded source-file splice with byte-equivalence contract, atomic
writes, JSONL undo log, 8-rule safety guard, brace+quote+comment-aware
@scope CSS carbonizer. Lead-fixed one blocking marker-regex bug
live-debugged via tester pins.
Built by a 4-agent team (architect / coder / security / tester) with
disjoint file ownership.
What landed
Contracts (src/contracts/source.ts, 463 lines)
MARKER_SYNTAXtable per filetype (tsx/jsx/html/vue/svelte/css)- 6 marker kinds collapsed to 3 groups (inject/variants/style)
InjectMarker,VariantBlockMarker,MarkerBlock,AcceptOperation,
DiscardOperationzod schemasSafetyErrorCodeenum,SafetyResultdiscriminated unionUndoEntryKindenum,UndoEntryschema (JSONL append-only)- 7 module interfaces, all constants
Modules (src/source/, 2700 lines / 7 files)
_helpers.ts(244) - shareddetectEol, sha256, atomicWrite, findMarkerBlocksafety.ts(403) - 8-rule pipeline; single 512-byte head-read serves
binary + magic-comment + EOL-detect (1 syscall round trip)inject.ts(387) -injectLiveScript/removeLiveScript; JSX/TSX
refused withagent-drivenfallback; SHA256 hash-mismatch detectionwrap.ts(499) -wrapVariantBlock/discardVariantBlock; heuristic
selector-to-anchor; brace-balanced tag-end walkeraccept.ts(338) -findMarkerBlock,extractVariant,expandReplaceRange,
acceptVariantwithop.variantCssfallbackcarbonize.ts(429) - pure CSS-in/CSS-out; recursive-descent parser;
:scope-var merge with paramOverrides;var(--x, fallback)resolution;
@media/@supportspreservedundo-stack.ts(201) - JSONL append, rotation at 10 MB, malformed-line
warn-skip on read
Tests (tests/source/, 99 new tests in 6 files)
safety.test.ts(22) - all 8 rules + EOL detection + happy pathinject-roundtrip.test.ts(13) - byte-equivalence roundtrip across
HTML/Vue/Svelte, CRLF preservation, double-inject refusal, hash-mismatchwrap-discard.test.ts(15) - selector heuristics, ambiguity, self-closing
tags, case-insensitive matchingaccept-splice.test.ts(17) - findMarkerBlock filtering, extractVariant
with @media nesting, expandReplaceRange indentation, paramOverrides bakecarbonize.test.ts(19) - worked example, var fallbacks, dead-var
elimination, @supports preservation, malformed throwsundo-stack.test.ts(13) - append/read/rotate, malformed resilience
Cumulative: 270/270 tests green (99 source + 94 browser + 77 bridge + 2 smoke).
Live-debugged bug fix
Initial marker regex used [^-]*? for HTML/Vue/Svelte (and [^*]*? for
JSX/CSS). The HTML body class forbade hyphens, but UUID injectIds, ISO
timestamps, dashed hashes and URL-safe base64 all contain hyphens.
Consequence: findMarkerBlock returned null for HTML-family,
removeLiveScript always threw, double-inject refusal silently failed open,
byte-equivalence contract could not be verified.
Fix: all 5 patterns switched to [\s\S]*? - the trailing terminator
(--> for HTML, */ for JSX/CSS) is unique by spec, no inner-char
exclusion needed. Tester's 6 [BUG]-pinned tests rewrote to assert correct
post-fix behavior.
Known limitations (deferred to Phase 6 component-detect.ts)
wrapselectorToAnchoris heuristic -.btndoes not match
className="btn primary"(multi-class); pinned as[FINDING]testwrapthendiscardnot full-file byte-equivalent for JSX (engine
consumes trailing newline);restoredByteEquivalentreports honestly
false; tests assert structural recovery insteadinjectthenremovehas collapseDoubleBlank artifact at splice site
(one byte difference); engine reports honestly; markers gone + content
structurally intact
Quality gates
typecheck: clean
test: 270/270 in 3.82s (19 files)
build: index.js 9.74 kB + live.js 31.30 kB
doctor: 7/7 OK
Next
Phase 4 - Agent-Loop + Skill-Korpus (src/agent/poll-loop.ts, skills/).
Long-poll loop with 270s slicing, variant-generation prompts encoding
Samuel's vault anti-slop checklist + UI-UX-Pro-Max corpus (MIT fork) +
Huashu directions + 4 Narrative Questions Pre-Code-Gate. AgentDB+HNSW
index over skills/data/corpus.
v0.3.0-prerelease — Phase 2: Browser Runtime (live.js)
Vanilla-JS IIFE bundle (dist/live.js) for in-page picking, configure, generation,
and variant cycling. 31.30 kB minified — well under the 50 kB live.js budget.
Built by a 4-agent team (architect / coder / security / tester) with disjoint
file ownership. No React, no DOM-deps, no Node imports — Browser-target ES2020.
What landed
Contracts (src/contracts/browser.ts, 467 lines)
BrowserState5-kind discriminated union + 18-entrySTATE_TRANSITIONStablePickResult,Variant,ParameterBinding,Annotation,BridgeClientinterfaceSanitizeModuleinterface — single trust-boundary for user/agent stringsWispDesignGlobal/InitOptions/WispDesignHandlefor the IIFE boot API- Constants split into
src/browser/constants.ts(pure, no zod) to keep the
IIFE bundle free of zod side effects
Browser modules (src/browser/, ~2500 lines across 10 files)
state-machine.ts— pure reducer, invalid event returns input unchangedpicker.ts—document.elementFromPoint+pickable()(>=20x20,
excludes WISP UI / html / body / script / style / meta /visibility:hidden)multi-select.ts— Cmd/Ctrl-click-add toggle (Improvement #1 vs Impeccables single-select)floating-bar.ts— 3-mode bar (configure / generating / cycling)parameter-sliders.ts— zero-roundtrip CSS-var binding (Killer-Feature)annotations.ts— structured{targetId, kind, note}— NOT PNG flatten (Improvement #7)variant-render.ts—@scope-based 3-variant cycling; variant 0 = live
original (preserves listeners), variants 1+ are clonesindex.ts— IIFE entry,window.WispDesign.init({ token, bridgeUrl })bridge-client.ts— fetch POST + EventSource subscribe (Long-Poll fallback)
Security (src/browser/sanitize.ts, 302 lines)
escapeHtml,sanitizeFreeText(strips<script>/<style>/<iframe>/
<object>/<embed>/<svg>blocks + on*= handlers + js:/data:/vbscript:
prefixes + C0+DEL controls; word-boundary truncate at 75% threshold)trustedCssVar(rejectsurl(/@import/expression(/{/}/;/backslash)trustedSelector(deep:has()check rejects nested iframe/script)- Returns structured
SanitizeError{ code, message, detail? }
Tests (tests/browser/, 94 new tests across 7 files)
sanitize.test.ts(28) — XSS payloads, CSS-var + selector rejectionstate-machine.test.ts(19) — transitions, self-loops, invalid-event silencepicker.test.ts(15) —pickable()filter, selector chains, attribute subsetmulti-select.test.ts(6) — modifier toggle,[data-wisp-ui]exclusionvariant-render.test.ts(9) — wrapper, 3 siblings,@scoperule injectionbundle-size.test.ts(5) —dist/live.jsexists, < 50000 bytes, IIFE bannerconstants-drift.test.ts(12) — byte-equivalence guard between
src/browser/constants.tsandsrc/contracts/browser.ts(CI catches drift)
Cumulative: 171/171 tests green (94 browser + 77 bridge + 2 smoke).
Tooling
- tsup multi-entry: CLI ESM (
dist/index.js, 9.74 kB) + Browser IIFE
(dist/live.js, 31.30 kB,globalName: WispDesign, target es2020, minified,
noExternal: [/.*/],outExtensionstrips the.globalinfix) - vitest
environmentMatchGlobs:tests/browser/**runs jsdom, rest pure Node - jsdom +
@types/jsdomas devDeps
Notable engineering decision
Bundle was 91.17 kB initially — sanitize.ts value-imported
FREE_TEXT_MAX_LEN from contracts/browser.js, which triggered top-level
z.object(...) evaluation and inlined the full zod runtime (~60 kB overhead).
Fix: rewire sanitize.ts to import from src/browser/constants.ts (pure
constants, no zod). The other 9 browser modules already did this — only
sanitize.ts was the offender. constants-drift.test.ts now guards against
future drift between constants.ts and contracts/browser.ts.
Result: 31.30 kB. 60 kB reduction in a single import rewire.
Pinned behaviors
escapeHtmlis NOT idempotent (&→&amp;)textPreviewlength is 81 when truncated (…ellipsis appended after 80 char slice)sanitizeFreeTextstrips the leading whitespace inside<img on…=…>along with
the handler — result lacks a trailing space inside the tag- jsdom does not implement
@scopecascade — variant-render structural tests
assert the@scoperule is in the<style>textContent, not that styles apply
Quality gates
$ npm run check
typecheck: clean
test: 171 passed in 13 files (2.45s)
build: index.js 9.74 kB + live.js 31.30 kB
$ node dist/index.js doctor
7/7 OK
Next
Phase 3 — Source-Edit Engine (src/source/). Marker-bounded fs.writeFileSync
splice, byte-equivalent inject/remove roundtrip, carbonize CSS @scope →
permanent on accept, per-session .wisp/sessions/<id>.jsonl undo log
(Improvement #2 vs Impeccables no-undo), refuse-edit safety on generated files.
v0.2.0-prerelease — Phase 1: Local Bridge Server
Local HTTP+SSE+Long-Poll bridge with token auth and a 5-rule path-traversal guard.
Repo stays private until v1.0.0.
Built by a 4-agent team (architect / coder / security / tester) with disjoint
file ownership — no merge conflicts, no overlapping writes.
What landed
Contracts (src/contracts/bridge.ts, 426 lines)
BridgeEventSchemazod discriminatedUnion across 10 event kindsPortLockSchema,BridgeStatusSchema,BridgeHealthSchema,BridgeHttpErrorSchemaLongPollRequestSchema(.refineenforcestimeout <= LONG_POLL_CAP_MS = 270000)AuthModuleinterface,BridgeEndpointdiscriminated union (11 variants)BridgeServerOptions/BridgeServerHandlefor the boot API
Server (src/bridge/server.ts 484 + http-helpers.ts 127 lines)
Plain Node http, no Express/Hono. Binds 127.0.0.1 only. 11 endpoints:
GET /health(no auth, liveness)GET /status(auth,BridgeStatus)GET /live.js(no auth, Phase 2 stub)GET /design-system.json(auth, serves.wisp/design-tokens.json)GET /source(auth +guardPath)GET /eventsSSE (auth, 15s keepalive)POST /events(auth, validatesBridgeEventSchema)GET/POST /poll(auth, long-poll with silent cap)POST /annotation(auth, kind-locked)GET /stop(auth, graceful shutdown)
256 KB body cap (413 BODY_TOO_LARGE). Cursor format seq-N-sessionId. Long-poll
silent-clamps oversize timeouts to 270000 ms (no 400 — improves Impeccable).
Port discovery (port-discovery.ts, 123 lines)
findFreePortover 31337..31400 (Improvement #8 vs Impeccables fixed:8400)- Atomic lockfile (
.tmp+ rename, mode 0o600) - Stale-pid detection via
process.kill(pid, 0)
Auth + path guard (src/bridge/auth.ts, 220 lines)
generateTokenUUIDv4validateTokenUUID-format regex + length-mismatch UNAUTHORIZED
(avoids timing leak) + finalcrypto.timingSafeEqualguardPath5-rule pipeline:- reject absolute paths (POSIX + Windows)
- reject any surviving
..segment after normalize path.resolvemust prefix-matchprojectRootrealpathSyncre-check (symlink-escape blocked); ENOENT is OK (caller 404)- hard-deny
/.git/,/node_modules/,/.wisp/sessions/,.env*
Tests (tests/bridge/*.test.ts, 77 passing in 6 files)
auth.test.ts(26) — token generation, validation edge cases, guardPath all 5 rulespath-traversal.test.ts(11) — mixed seps, unicode, mid-segment denies, long pathsport-discovery.test.ts(11) — range/exhaustion/stale-pid/atomic-writelong-poll-slicing.test.ts(6) — fast-path, wake-up, empty timeout, cursor advance,
client-disconnect race, silent-clampserver-smoke.test.ts(21) — auth gating,/sourcetraversal, body validation,
413 cap, SSE keepalive,/stopgraceful
Tooling
.gitattributesnormalizes line endings to LF (Phase 3 needs byte-equivalence
on inject/remove roundtrip)- Version bump to 0.2.0-prerelease in all three manifests
Quality gates
$ npm run check
typecheck: clean
test: 77 passed in 6 files (1.28s)
build: 9.74 kB
$ node dist/index.js doctor
7/7 OK
Next
Phase 2 — Browser Runtime (src/browser/live.js). Vanilla-JS IIFE, state machine
PICKING → CONFIGURING → GENERATING → CYCLING, floating bar, multi-select via
⌘-click-add, parameter sliders binding to CSS vars.
v0.1.0-prerelease — Phase 0: Scaffolding & Manifest
First scaffold tag. Repo is private until v1.0.0.
What landed
- npm package skeleton (ESM, tsup, zod, vitest, @types/node@20)
.claude-plugin/plugin.json+marketplace.jsonwith verified v3.x schema
(repository as string, plugins[].source as object — both are known gotchas)hooks/hooks.jsonthree-layer matcher envelope wired for UserPromptSubmit,
PostToolUse (Edit|Write|MultiEdit), Stop, SessionEndcommands/wisp-design.mdtop-level dispatcher with subcommands
live/init/audit/history/tokens/syncsrc/index.tsCLI +src/cli/doctor.ts(manifest validator) +
src/hooks/dispatcher.ts(stdin-drain no-op hooks for Phase 0)dist/index.jsbuilt and committed (plugin clones have no build step)- vitest smoke test passes;
wisp-design doctorreturns 7/7 OK
Quality gate
$ node dist/index.js doctor
Y node version - v22.16.0
Y .claude-plugin/plugin.json - wisp-design v0.1.0-prerelease
Y .claude-plugin/marketplace.json - 1 plugin(s)
Y hooks/hooks.json - 4 event(s): UserPromptSubmit, PostToolUse, Stop, SessionEnd
Y commands/wisp-design.md
Y LICENSE - MIT
Y dist/index.js - 9.7 kB
wisp-design doctor: OK
Next
Phase 1 — Local Bridge Server (src/bridge/). Plain Node HTTP + SSE, auto-port
discovery, token+UUID auth, path-traversal guard on /source.