Reorganize contributor docs

This commit is contained in:
Paul Bakaus
2026-06-09 15:18:53 -07:00
parent 5fbe37c97c
commit 324cec73a4
10 changed files with 13 additions and 265 deletions
+217
View File
@@ -0,0 +1,217 @@
# Developer Guide
Documentation for contributors to Impeccable.
## Architecture
The skill at `skill/` is transformed into provider-specific formats by a config-driven factory. Each provider is defined as a config object in `scripts/lib/transformers/providers.js` -- adding a new provider requires only a new config entry.
For detailed harness capabilities (which frontmatter fields each supports, placeholder systems, directory structures), see [HARNESSES.md](HARNESSES.md).
## Source Format
### Skill (`skill/SKILL.src.md`)
```yaml
---
name: skill-name
description: What this skill provides
argument-hint: "[target]"
user-invocable: true
license: License info (optional)
compatibility: Environment requirements (optional)
---
Your skill instructions here...
```
**Frontmatter fields** (based on [Agent Skills spec](https://agentskills.io/specification)):
- `name` (required): Skill identifier (1-64 chars, lowercase/numbers/hyphens)
- `description` (required): What the skill provides (1-1024 chars)
- `user-invocable` (optional): Boolean -- if `true`, the skill can be invoked as a slash command
- `argument-hint` (optional): Hint shown during autocomplete (e.g., `[target]`, `[area (feature, page...)]`)
- `license` (optional): License/attribution info
- `compatibility` (optional): Environment requirements (1-500 chars)
- `metadata` (optional): Arbitrary key-value pairs
- `allowed-tools` (optional, experimental): Pre-approved tools list
**Body placeholders** (replaced per-provider during build):
- `{{model}}` -- Provider-specific model name (e.g., "Claude", "Gemini", "GPT")
- `{{config_file}}` -- Provider-specific config file (e.g., "CLAUDE.md", ".cursorrules")
- `{{ask_instruction}}` -- How to ask the user for clarification
- `{{command_prefix}}` -- Slash command prefix (`/` for most, `$` for Codex)
- `{{available_commands}}` -- Comma-separated list of user-invocable commands
## Building
### Prerequisites
- Bun (fast JavaScript runtime and package manager)
- No external dependencies required
### Commands
```bash
# Build all provider formats
bun run build
# Clean dist folder
bun run clean
# Rebuild from scratch
bun run rebuild
```
### What Gets Generated
```
source/ -> dist/
skills/{name}/SKILL.md {provider}/{configDir}/skills/{name}/SKILL.md
```
Each provider gets its own output directory.
## Build System Details
The build system uses a factory pattern under `scripts/`:
```
scripts/
build.js # Main orchestrator
lib/
utils.js # Frontmatter parsing, placeholder replacement, YAML generation
zip.js # ZIP bundle generation
transformers/
factory.js # createTransformer() -- generates transformer functions from config
providers.js # PROVIDERS config map -- one entry per provider
index.js # Re-exports factory-generated transformer functions
```
### Adding a New Provider
1. Add a placeholder config to `PROVIDER_PLACEHOLDERS` in `scripts/lib/utils.js`:
```javascript
'my-provider': {
model: 'MyModel',
config_file: 'CONFIG.md',
ask_instruction: 'ask the user directly to clarify.',
command_prefix: '/'
}
```
2. Add a provider config to `PROVIDERS` in `scripts/lib/transformers/providers.js`:
```javascript
'my-provider': {
provider: 'my-provider',
configDir: '.my-provider',
displayName: 'My Provider',
frontmatterFields: ['user-invocable', 'argument-hint', 'license'],
}
```
3. Run `bun run build` -- the provider is automatically picked up by the build loop.
4. Update `HARNESSES.md` with the provider's capabilities.
### Provider Config Options
| Field | Description |
|-------|-------------|
| `provider` | Key for output directory and placeholder lookup |
| `configDir` | Dot-directory name (e.g., `.claude`) |
| `displayName` | Human-readable name for build logs |
| `frontmatterFields` | Which optional fields to emit (see `factory.js` FIELD_SPECS) |
| `bodyTransform` | Optional `(body, skill) => body` function for post-processing |
| `placeholderProvider` | Override which PROVIDER_PLACEHOLDERS key to use (for variants sharing config) |
### Key Functions
- `createTransformer(config)`: Factory that returns a transformer function from a provider config
- `parseFrontmatter()`: Extracts YAML frontmatter and body from SKILL.md files
- `readSourceFiles()`: Reads `skill/SKILL.src.md` plus its `reference/` and `scripts/` siblings
- `replacePlaceholders()`: Substitutes `{{model}}`, `{{config_file}}`, etc. per provider
- `generateYamlFrontmatter()`: Serializes objects to YAML frontmatter (auto-quotes values starting with `[` or `{`)
## Testing
```bash
bun run test # Default suite — unit + static fixtures (no API keys needed)
bun run test:live-e2e # Opt-in — full-cycle live-mode E2E across framework fixtures (~2 min, needs `npx playwright install chromium` once)
bun run test:skill-behavior # Opt-in — LLM-backed checks that the SKILL.md Setup flow actually drives the agent (~5 min, costs cents, needs `.env`)
```
The skill-behavior suite runs three providers (claude-haiku-4-5, gpt-5.4-mini, gemini-3.1-flash-lite — the cheapest tier of each, every run) with the source `skill/SKILL.src.md` inlined as the system prompt and a workspace-scoped `bash`/`read`/`write`/`list` tool set. It then asserts on the tool-call trace, not on free-form output. Use it whenever you edit `skill/SKILL.src.md`'s Setup section, `skill/scripts/context.mjs`, or any Setup-touching reference (`teach.md`, `document.md`, `brand.md`, `product.md`, sub-command refs). Per-scenario assertions and the current baseline (21-22/24) live in `tests/skill-behavior/README.md`. Provider keys live in repo-root `.env` (gitignored); missing keys skip cleanly.
## Best Practices
### Skill Writing
1. **Focused scope**: One clear domain per skill
2. **Clear descriptions**: Make purpose obvious
3. **Clear instructions**: LLM should understand exactly what to do
4. **Include examples**: Where they clarify intent
5. **State constraints**: What NOT to do as clearly as what to do
6. **Test across providers**: Verify it works in multiple contexts. For Setup-related edits to `skill/`, `bun run test:skill-behavior` automates this across three providers.
## Reference Documentation
- [Agent Skills Specification](https://agentskills.io/specification) - Open standard
- [HARNESSES.md](HARNESSES.md) - Provider capabilities matrix
- [Cursor Skills](https://cursor.com/docs/context/skills)
- [Claude Code Skills](https://code.claude.com/docs/en/skills)
- [Gemini CLI Skills](https://geminicli.com/docs/cli/skills/)
- [Codex CLI Skills](https://developers.openai.com/codex/skills/)
- [VS Code Copilot Skills](https://code.visualstudio.com/docs/copilot/customization/agent-skills)
- [Kiro Skills](https://kiro.dev/docs/skills/)
- [OpenCode Skills](https://opencode.ai/docs/skills/)
- [Pi Skills](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/skills.md)
- [Qoder Skills](https://docs.qoder.com/extensions/skills)
## Repository Structure
```
impeccable/
source/ # Edit these! Source of truth
skills/ # Skill definitions
frontend-design/
SKILL.md
reference/*.md # Domain-specific references
audit/SKILL.md
polish/SKILL.md
...
dist/ # Generated output (gitignored)
scripts/
build.js # Main orchestrator
lib/
utils.js # Shared utilities
zip.js # ZIP generation
transformers/
factory.js # Config-driven transformer factory
providers.js # Provider config map
index.js # Re-exports
tests/ # Bun test suite
docs/
HARNESSES.md # Provider capabilities reference
DEVELOP.md # This file
README.md # User documentation
```
## Troubleshooting
### Build fails with YAML parsing errors
- Check frontmatter indentation (YAML is indent-sensitive)
- Ensure `---` delimiters are on their own lines
- Values starting with `[` or `{` are auto-quoted; other special YAML chars may need manual quoting
### Output doesn't match expectations
- Check the provider config in `scripts/lib/transformers/providers.js`
- Verify source file has correct frontmatter structure
- Run `bun run rebuild` to ensure clean build
### Provider doesn't recognize the files
- Check installation path for your provider
- Verify file naming matches provider requirements
- Consult [HARNESSES.md](HARNESSES.md) for provider-specific details
## Questions?
Open an issue or submit a PR!
+97
View File
@@ -0,0 +1,97 @@
# Harness Skills Capabilities Reference
Source of truth for what each AI coding harness supports in terms of agent skills.
Used to inform provider configs in `scripts/lib/transformers/providers.js`.
Last verified: 2026-04-28
## Official Documentation
| Harness | Docs URL |
|---------|----------|
| Claude Code | https://code.claude.com/docs/en/skills |
| Cursor | https://cursor.com/docs/context/skills |
| Gemini CLI | https://geminicli.com/docs/cli/skills/ |
| Codex CLI | https://developers.openai.com/codex/skills |
| GitHub Copilot (Agents) | https://code.visualstudio.com/docs/copilot/customization/agent-skills |
| Kiro | https://kiro.dev/docs/skills/ |
| OpenCode | https://opencode.ai/docs/skills/ |
| Pi | https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/skills.md |
| Qoder | https://docs.qoder.com/extensions/skills |
| Trae | TBD (no official skills docs found yet) |
| Rovo Dev | https://support.atlassian.com/rovo/docs/extend-rovo-dev-cli-with-agent-skills |
## Spec Compliance
All harnesses follow the [Agent Skills specification](https://agentskills.io/specification) to varying degrees. The spec defines these frontmatter fields: `name`, `description`, `license`, `compatibility`, `metadata`, `allowed-tools`.
Provider-specific extensions beyond the spec: `user-invocable`, `argument-hint`, `disable-model-invocation`, `allowed-tools` (extended syntax), `model`, `effort`, `context`, `agent`, `hooks`, `subtask`, `mcp`.
## Frontmatter Support
Fields marked with * are spec-standard. Others are provider extensions.
| Field | Claude Code | Cursor | Gemini | Codex | Copilot | Kiro | OpenCode | Pi | Qoder | Rovo Dev |
|-------|:-----------:|:------:|:------:|:-----:|:-------:|:----:|:--------:|:--:|:-----:|:--------:|
| `name`* | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| `description`* | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| `license`* | Yes | Yes | Ignored | No | Yes | Yes | Yes | Yes | Yes | Yes |
| `compatibility`* | Yes | Yes | Ignored | No | Yes | Yes | Yes | Yes | Yes | Yes |
| `metadata`* | Yes | Yes | Ignored | No | Yes | Yes | Yes | Yes | Yes | Yes |
| `allowed-tools`* | Yes | No | Ignored | No | No | No | Yes | Yes | Yes | Yes |
| `user-invocable` | Yes | No | No | No | Yes | No | Yes | No | Yes | Yes |
| `argument-hint` | Yes | No | No | No | Yes | No | Yes | No | Yes | Yes |
| `disable-model-invocation` | Yes | Yes | No | No | Yes | No | Yes | Yes | TBD | TBD |
| `model` | Yes | No | No | No | No | No | Yes | No | No | No |
| `effort` | Yes | No | No | No | No | No | No | No | No | No |
| `context` | Yes | No | No | No | No | No | No | No | No | No |
| `agent` | Yes | No | No | No | No | No | Yes | No | No | No |
| `hooks` | Yes | No | No | No | No | No | No | No | No | No |
Notes:
- Gemini CLI validates only `name` and `description`; other spec fields are parsed but ignored.
- Codex CLI uses a separate `agents/openai.yaml` sidecar for skill metadata (icons, branding, MCP tools, invocation control). Codex also auto-discovers subagents bundled inside an installed skill's `agents/` folder (TOML), which is how Impeccable ships its asset-producer. Standalone custom agents can still live under `.codex/agents/` or `~/.codex/agents/`, but Impeccable no longer installs anything there.
- Kiro recognizes `user-invocable` and `disable-model-invocation` per community reports but does not formally document them.
- Unknown fields are silently ignored by all harnesses.
## Skill Directory Structure
| Harness | Native directory | Also reads |
|---------|-----------------|------------|
| Claude Code | `.claude/skills/` | - |
| Cursor | `.cursor/skills/` | `.agents/skills/`, `.claude/skills/` |
| Gemini CLI | `.gemini/skills/` | `.agents/skills/` |
| Codex CLI | `.agents/skills/` (primary) | - |
| GitHub Copilot | `.github/skills/` | `.agents/skills/`, `.claude/skills/` |
| Kiro | `.kiro/skills/` | - |
| OpenCode | `.opencode/skills/` | `.agents/skills/`, `.claude/skills/` |
| Pi | `.pi/skills/` | `.agents/skills/` |
| Qoder | `.qoder/skills/` | `~/.qoder/skills/` (user-level) |
| Trae China | `.trae-cn/skills/` | TBD |
| Trae International | `.trae/skills/` | TBD |
| Rovo Dev | `.rovodev/skills/` | `~/.rovodev/skills/` (user-level) |
All harnesses support the `{skill-name}/SKILL.md` directory structure with optional `reference/`, `scripts/`, and `assets/` subdirectories.
## Native Subagent Directory Structure
| Harness | Native directory | File format |
|---------|------------------|-------------|
| Claude Code | `.claude/agents/` (installed plugin) | Markdown with YAML frontmatter |
| Codex CLI | `<skill>/agents/` (nested, auto-discovered) | TOML |
Impeccable keeps canonical agent prompts under `skill/agents/` and emits provider-native files only for harnesses with documented subagent formats. Claude reads its agents from the installed plugin; Codex auto-discovers the TOML bundled inside the installed skill's own `agents/` folder, so the normal skills install carries it with no separate sidecar.
## Placeholder / Variable Substitution
Claude Code supports runtime variable substitution directly in SKILL.md bodies: `$ARGUMENTS`, `$0`-`$N`, `${CLAUDE_SKILL_DIR}`, `${CLAUDE_SESSION_ID}`. No other harness supports substitution in skills.
Some harnesses have separate "custom commands" systems (distinct from skills) with their own substitution:
| Harness | Command system | Substitution syntax |
|---------|---------------|-------------------|
| Gemini CLI | `.gemini/commands/` (TOML) | `{{args}}`, `!{shell}`, `@{file}` |
| Codex CLI | `.codex/prompts/` | `$ARGNAME` |
| OpenCode | `.opencode/commands/` | `$ARGUMENTS`, `$1`-`$N`, `` !`shell` `` |
Our build system handles cross-provider placeholders at compile time via `replacePlaceholders()` for `{{model}}`, `{{config_file}}`, `{{ask_instruction}}`, and `{{available_commands}}`.
+103
View File
@@ -0,0 +1,103 @@
# STYLE.md
Editorial brief for impeccable.design. Read this before writing or editing user-facing copy: the homepage, sub-pages, command editorials, tutorials, and READMEs.
The bar: **for every paragraph, point to the sentence that makes it specifically yours.** If you can't, the paragraph is AI by default, even if a human typed it.
## Principles
1. **Open with the reader's wrong belief, your strongest claim, or the example.** No "in this guide", no "let's dive in".
2. **Take a position someone could disagree with.** If the paragraph could be inverted without changing meaning, it has no position. Sign your stance.
3. **Name names. Use numbers.** Real competitors, real customer names, real version numbers, real file paths, real benchmarks. Cut "lightweight"; write "54 KB".
4. **Verbs lead. Nouns follow.** Imperative is fine. Active voice. Cut nominalizations ("the implementation of" → "implementing").
5. **Vary sentence length on purpose.** Long, long, short. Smooth uniform rhythm is the deepest AI tell.
6. **Prose carries the load; structure supports it.** Bullets are for parallel options. Paragraphs are for argument. Don't bullet what would be tighter as a sentence.
7. **Plain words. Technical terms only when something specifically rests on them.** Mixing levels lets the technical terms hit harder.
8. **Allow ungrammatical fragments for rhythm.** Five words. Confidence signal.
9. **Respect the reader's competence.** No "developers should consider"; just "you might not need an effect".
10. **Read it aloud. Fix anything you stumble over.**
11. **Concrete over comprehensive.** Coverage is an AI obsession. Trade coverage for momentum. Leave things out.
12. **Close by handing off the next move.** Don't summarize. End on the strongest sentence, give a directive ("Now do this"), or just stop.
## Denylist
The build's `validateProse` step (in `scripts/build.js`) fails the build on these. The list is the editorial brief, enforced. Add a rule here when you ban a new pattern; remove a rule when the term has earned a real meaning here. **Do not silently allowlist** by working around the regex.
### Stolen-engineer diction
Engineering words that became AI flavor once they leaked into training data around late 2024.
| Banned | Why | Use instead |
|---|---|---|
| `load-bearing` | Almost always vague. The literal sense is rare. | Name the specific thing it does. "The decision that shapes the rest", "carries the brand", "matters specifically". |
| `highest-leverage` | Vague claim of impact. | Say what specifically pays off. "The change that moves the design most". |
| `biggest unlock` | Marketing-speak. | Describe the actual change. |
### Internal jargon leaking out
Words that work in a research notebook and fail in user copy.
| Banned | Why | Use instead |
|---|---|---|
| `reflex defaults` | Eval-team jargon. | "Instincts", "first guesses", "default reaches". |
| `collapses into monoculture` | Eval-paper voice. | Describe what specifically went wrong (e.g. "every model picked the same three fonts"). |
| `data-driven` | Empty marketing adjective. | Cite the data. "Validated against 15 briefs across two models". |
### Marketing voice
Adjectives and verbs that gesture at quality without doing the work.
| Banned | Why | Use instead |
|---|---|---|
| `seamless`, `seamlessly` | Hollow positive. | Say what specifically works without friction. |
| `robust`, `robustness` | Hollow positive. | Cite the failure mode handled. |
| `elevate`, `elevates` | Marketing verb. | Use the specific verb (improve, raise, sharpen). |
| `empower`, `empowers` | Marketing verb. | "Let you", "make possible". |
| `underscore`, `underscores` | AI tell. | "Show", "make clear". |
| `pivotal` | Hollow positive. | "Central", "key", or describe the role. |
| `tapestry` | AI scenery noun. | Cut. |
### Verbs
| Banned | Why | Use instead |
|---|---|---|
| `delve`, `delves`, `delved`, `delving` | The most-flagged AI tell of all. | "Look at", "explore", or just delete the throat-clearing verb. |
### Throat-clearing
Sentences that delay the point. Cut them; almost nothing of value is lost.
| Banned | Why | Use instead |
|---|---|---|
| `in today's …` | Generic opener. | Start at the actual point. |
| `gone are the days` | Cliché opener. | Make the point directly. |
| `whether you're …` | Audience-pandering; addresses no one. | Pick one reader. Write to them. |
| `let's dive in` | Throat-clearing. | Just start. |
### Closers
| Banned | Why | Use instead |
|---|---|---|
| `in summary`, `in conclusion` | Restates what was just said. | End on the strongest sentence. Trust the reader. |
### Transitions
| Banned | Why | Use instead |
|---|---|---|
| `moreover`, `furthermore` | Metronome transition crutch. | Drop, or use "also", or restructure. |
### Punctuation
| Banned | Why | Use instead |
|---|---|---|
| Em dash `—` (and HTML entities `&mdash;`, `&#8212;`, `&#x2014;`) | Decision-avoidance: writer didn't pick a relationship between the clauses. | Comma, colon, semicolon, period, parentheses. Pick the relationship. |
| ` -- ` (double hyphen as em-dash substitute) | Worse than the em dash. Signals failed cleanup. | Real punctuation. |
## Patterns the validator can't catch
The above are the easy wins. The deeper issues require human judgment on every paragraph.
- **Negation pivot.** "It's not just X, it's Y." "Less about X, more about Y." This is now a stronger AI tell than any vocabulary item. Use sparingly. Most instances should be replaced with a direct positive claim.
- **Triadic everything.** Every list exactly three items. Every adjective in groups of three ("fast, simple, and powerful"). Vary count: use 2 or 4. Use 1.
- **The five-paragraph essay shape.** Intro → 3 sections → conclusion, on every page. Mix it up. Lead with the example. Skip the conclusion. Let some sections be one sentence.
- **Uniform paragraph length.** Insert a 4-word sentence. Insert a one-line paragraph.
- **Synthetic balance.** Pros and cons of equal length when one is clearly right. Write the recommendation; note real exceptions briefly.
- **Hollow confidence.** "Powerful" without numbers. Replace with a concrete fact.
- **Hedging stacks.** "It might potentially be useful to consider..." Each hedge is fine; stacked, they sound trained.
- **Interchangeable copy.** Swap "Impeccable" for a competitor name. If nothing becomes false, the copy is generic.
## When in doubt
Read the paragraph aloud. If you stumble, rewrite. If a sentence describes nothing specific to this product, cut it.
-48
View File
@@ -1,48 +0,0 @@
# Issue 150 Live Preview Plan
## Current Bug Summary
Live preview can lose framework state when variants are written directly into watched component source. The Svelte reproduction is a stateful expense row: after adding an expense, generating variants for the row should not reset the component or render raw Svelte expressions such as `{expenses[0].name}`.
The current branch uses **Svelte component injection** for `.svelte` targets: variants are real components under `src/lib/impeccable/<id>/`, mounted in the browser via Svelte 5 `mount()`, and inlined back into the route source on live exit. Accept keeps the mounted component visible immediately while deferring the route write until `live-server stop`.
## Manual test apps (home directory)
Stateful framework repros live outside this repo:
- **Svelte:** `~/impeccable-live-svelte` (see its `README.md`)
- **React:** `~/impeccable-live-react` (see its `README.md`)
Each app includes a copied `.cursor/skills/impeccable` build from the local impeccable branch for live-server / inject / poll.
## Current Status
- Svelte component-injection Accept fix is implemented.
- Svelte manual pass with the user is complete (`~/impeccable-live-svelte`).
- React manual user check and the DeepSeek-backed final run are still pending (`~/impeccable-live-react`).
- Focused live tests, build, and full test suite should pass after harness refresh.
## Svelte Fix Plan
- Keep component injection scoped to `.svelte` targets.
- Extract `propContract` from the picked route markup and author variants as real `.svelte` files with `{propName}` bindings.
- Mount compiled variants in the browser with the app's shared Svelte runtime.
- On Accept, keep the chosen mounted variant visible immediately.
- Defer the real route source inline until live shutdown to avoid accept-time remounts.
- On live shutdown, inline accepted markup + CSS into the route and remove temp component files.
- Keep the connected indicator stable while an event is leased or actively being handled.
## React Parity Test Plan
- Same shape as the Svelte case in `~/impeccable-live-react`.
- Run the same Go, cycle, Accept flow against the React row.
- React already uses direct source wrap + Fast Refresh; keep that path unchanged unless manual testing shows state loss.
## Validation Checklist (impeccable repo)
- [ ] `node --test tests/live-browser-regression.test.mjs tests/live-accept.test.mjs tests/live-poll.test.mjs tests/live-server.test.mjs tests/live-svelte-component.test.mjs`
- [ ] Manual Svelte run in `~/impeccable-live-svelte`
- [ ] `bun run build`
- [ ] `bun run test`
The PR should remain draft until the React manual check in `~/impeccable-live-react` passes.
-132
View File
@@ -1,132 +0,0 @@
# Handoff: Svelte live mode bar stuck at `0/0` (CYCLING with no variants)
Date: 2026-05-30
Branch: `codex/issue-150-svelte-live`
Status: **Unresolved.** Two rounds of fixes landed and were synced to the manual test repo, but the symptom still reproduces.
---
## Symptom
In `~/impeccable-live-svelte`, after running a live action (Polish/Bolder) on the expense row, the floating live bar shows the **CYCLING** layout (prev/next arrows, two faint dots, `✓ Accept`, `✕`) but the counter reads `0/0` and every control is disabled (`opacity: 0.3`, `pointer-events: none`). No variant is mounted. The bar persists across reloads.
Captured DOM (trimmed): `<div id="impeccable-live-bar">``<span>0/0</span>``✓ Accept` (disabled) `✕`. Full markup is in the chat history if needed.
`0/0` = `visibleVariant=0 / arrivedVariants=0`. The two dots = `expectedVariants=2` rendered as "pending" (see `buildDots`, both unfilled because `arrivedVariants=0`).
---
## Context: what this feature is
Issue 150 replaced the Svelte "source-shadow" live preview with **real component injection**. See `docs/issue-150-live-preview-plan.md` and `skill/reference/live.md` (the `svelte-component` paragraph). Key pieces:
- `skill/scripts/live-svelte-component.mjs` — scaffolds `src/lib/impeccable/<id>/` with `manifest.json`, `v1.svelte``vN.svelte`, and a one-time `__runtime.js`. Inlines the accepted variant back into the route on exit.
- `skill/scripts/live-wrap.mjs``.svelte` targets return `previewMode: "svelte-component"`, `file` = manifest path.
- `skill/scripts/live-browser.js` — mounts compiled variants via Svelte 5 `mount()` into a `display:contents` slot that replaces the original element. Cycling = unmount + remount.
- `skill/scripts/live-accept.mjs` / `live-server.mjs` — defer the route source write to `live-server stop`.
The Svelte wrapper in the live DOM is **runtime-injected** and holds a single mount target (`[data-impeccable-component-mount]`), **not** `[data-impeccable-variant]` children like the HTML/JSX path. This distinction is the source of most of the trouble.
---
## What has already been fixed (and synced to the test repo)
All in `skill/scripts/live-browser.js`, rebuilt via `bun run build:skills` and rsync'd to `~/impeccable-live-svelte/.cursor/skills/impeccable/`.
1. **Params sidecar (the original compile error).** Svelte parses `{` inside an attribute value as an expression, so `data-impeccable-params='[{…}]'` broke compilation (`Expected token }`). Params now load from `componentDir/params.json` keyed by variant number:
- `loadSvelteComponentParams(manifest)` fetches `params.json`.
- `parseVariantParams()` reads from `svelteComponentSession.paramsByVariant` for the component path instead of the DOM attribute.
- Agent contract updated in `live-svelte-component.mjs` (`buildSvelteComponentCssAuthoring`) and `skill/reference/live.md`.
2. **Resume guard.** `resumeSession()` now drops an orphaned `svelte-component` wrapper (no live in-memory mount) instead of resuming it into an empty bar. Without this, every reload resumed `arrivedVariants=0`.
3. **Abort-on-failure.** New `abortSvelteComponentInjection(sessionId, message)` resets the bar to PICKING (restores the original element, clears session, toast) when the picked element can't be found OR the initial `mountSvelteComponentVariant` returns false (compile/mount throw). Called from `injectSvelteComponentsFromManifest`.
Tests added in `tests/live-browser-regression.test.mjs` (all green): orphan reset, abort helper, sidecar params. Full focused suite passes: `node --test tests/live-browser-regression.test.mjs tests/live-svelte-component.test.mjs tests/live-accept.test.mjs`.
---
## Why it probably STILL reproduces (hypotheses, in priority order)
### H1. The served `live.js` is stale (verify FIRST)
The browser loads `live.js` from the running live-server, not from disk directly. If the server wasn't restarted, or the `<script src=".../live.js">` is cached, the page is still running the OLD code without the abort/resume guards.
- **Check:** in the page console, search the loaded script for `abortSvelteComponentInjection` / `loadSvelteComponentParams`. If absent, the fix isn't loaded.
- **Fix:** `live-server.mjs --stop` then `--background`, hard-reload (Cmd+Shift+R), reload Cursor.
- Confirm the test repo copy is current: `grep -l abortSvelteComponentInjection ~/impeccable-live-svelte/.cursor/skills/impeccable/scripts/live-browser.js`.
### H2. The variant still fails to compile, and a path other than `injectSvelteComponentsFromManifest` sets CYCLING
The abort only fires inside `injectSvelteComponentsFromManifest`. If the agent-authored `v1.svelte` still has a compile error (another `{` in an attribute, a bad expression, etc.), the mount fails. Confirm the abort path is actually reached:
- Look for the console line `[impeccable] Failed to mount Svelte variant N` and the toast.
- If CYCLING is being set somewhere else, audit every `updateBarContent('cycling')` / `state = 'CYCLING'` site (grep finds ~12) for one that runs with `arrivedVariants===0`.
### H3. Server-side event replay re-injects after reset
`live-server.mjs` redelivers unacknowledged events and persists a session journal. After the browser aborts to PICKING, a redelivered `done` event (SSE reconnect, or the agent reply not acked) could re-trigger injection. The `done` handler (`live-browser.js` ~line 4868) only injects when `state === 'GENERATING'`, so a clean PICKING state should be safe — but verify the state at the moment of replay. The durable session may also need clearing: check `.impeccable/live/sessions/` and `.impeccable/live/deferred-svelte-component-accepts.json` in the test repo.
### H4. Stale localStorage session keeps re-priming the bar
The bar persists `state`/`id`/`visible` to localStorage (keys prefixed `impeccable-live`). A reload can rehydrate CYCLING before any wrapper exists.
- **Check:** `Object.keys(localStorage).filter(k=>k.startsWith('impeccable-live'))` in the console.
- **Clear:** remove those keys and reload (see "Reset procedure" below).
### H5. Defect-class root cause: empty CYCLING is representable at all
The deepest fix is to make `0/0 CYCLING` an impossible state. A cheap, robust guard: in the cycling render path (`buildCyclingRow` / wherever `updateBarContent('cycling')` resolves), if `arrivedVariants === 0`, refuse to render CYCLING and fall back to PICKING (or hide). That self-heals regardless of which upstream path is buggy. Consider adding this as belt-and-suspenders even after the specific path is found.
---
## Reset procedure (clears the currently-stuck bar)
```js
// page console
Object.keys(localStorage).filter(k => k.startsWith('impeccable-live')).forEach(k => localStorage.removeItem(k));
location.reload();
```
```bash
cd ~/impeccable-live-svelte
node .cursor/skills/impeccable/scripts/live-server.mjs --stop
node .cursor/skills/impeccable/scripts/live-server.mjs --background
# also clear any orphaned session state if H3 suspected:
rm -rf .impeccable/live/sessions/* .impeccable/live/deferred-svelte-component-accepts.json
rm -rf src/lib/impeccable/*/ # leftover variant component dirs (keep __runtime.js)
```
Reload Cursor so `/impeccable` picks up the synced skill.
---
## Repro
1. `cd ~/impeccable-live-svelte && npm run dev`
2. New terminal: `node .cursor/skills/impeccable/scripts/live-server.mjs --background`, note the port.
3. `node .cursor/skills/impeccable/scripts/live-inject.mjs --port <port>` then `node .cursor/skills/impeccable/scripts/live-poll.mjs`.
4. In the browser: add an expense, pick the expense row, run Polish/Bolder, let variants generate.
5. Observe whether the bar reaches `1/3` (good) or `0/0` (the bug).
6. With `IMPECCABLE_E2E_DEBUG`-style logging: open devtools console and watch for `[impeccable]` lines during step 4.
The expense row source is `src/routes/+page.svelte` lines ~34-37 (`<article class="expense-row …">{expenses[0].name}…`). `propContract` derives `name` and `amount`.
---
## Suggested investigation order for tomorrow
1. **Confirm the new code is actually live** (H1) — single biggest time-saver. Grep the loaded script + the test-repo file for `abortSvelteComponentInjection`.
2. With confirmed-fresh code, reproduce and **capture the console**. Determine whether `injectSvelteComponentsFromManifest` runs and whether `mountSvelteComponentVariant` throws. Read the actual `src/lib/impeccable/<id>/v1.svelte` the agent wrote — is it valid Svelte?
3. If mount throws: the agent is still authoring an invalid component. Tighten the `live.md` contract / scaffold stub, OR make the inline-accept/scaffold validate. Capture the exact error.
4. If mount succeeds but bar still `0/0`: trace which `state='CYCLING'` site runs with `arrivedVariants=0` (H2) and add the H5 guard.
5. Add an E2E regression once root cause is known. Note `tests/live-e2e.test.mjs` still references the old `source-shadow` markers for the Svelte fixture (lines ~311, ~891, ~918) — that opt-in suite needs updating to the component-injection model regardless.
---
## Key files / line anchors (as of this handoff)
| File | What |
|---|---|
| `skill/scripts/live-browser.js` ~4242 | `injectSvelteComponentsFromManifest` |
| `skill/scripts/live-browser.js` (`abortSvelteComponentInjection`) | clean-reset helper |
| `skill/scripts/live-browser.js` (`mountSvelteComponentVariant`) | dynamic import + `mount()` |
| `skill/scripts/live-browser.js` (`loadSvelteComponentParams`, `parseVariantParams`) | sidecar params |
| `skill/scripts/live-browser.js` (`resumeSession`) | orphan-wrapper guard |
| `skill/scripts/live-browser.js` ~4868 | SSE `done` handler |
| `skill/scripts/live-browser.js` (`buildCyclingRow`, `buildDots`, ~2102/2262) | bar render — candidate for H5 guard |
| `skill/scripts/live-svelte-component.mjs` | scaffold / inline-accept / cssAuthoring contract |
| `skill/reference/live.md` (`svelte-component` paragraph + Parameters §7) | agent contract |
Remember: source of truth is `skill/`. After any edit run `bun run build:skills`, then rsync to `~/impeccable-live-svelte/.cursor/skills/impeccable/`. Do not hand-edit the harness copies.