diff --git a/.gitignore b/.gitignore index e08be399..46ea91aa 100644 --- a/.gitignore +++ b/.gitignore @@ -7,10 +7,9 @@ htmlcov/ *.bak.* .lock .memsearch/ -.quirk/isles/ # git worktrees .worktrees/ -# local Agent Isles install (copied from npx cache, has `live`) +# node dependencies node_modules/ diff --git a/.quirk/shot.tmp.mjs b/.quirk/shot.tmp.mjs new file mode 100644 index 00000000..e69de29b diff --git a/BUGS.md b/BUGS.md index 3e8415c3..dc494a8d 100644 --- a/BUGS.md +++ b/BUGS.md @@ -133,25 +133,156 @@ entries' IDs; manual edits to fix typos are fine. - **Severity**: medium - **Proposed fix**: Skip link destinations and autolinks when applying the underscore guard, as scenario-example.tsx now does. -## BUG-15: SKILL.md and orchestration.md disagree on which dispatches carry the mutation-probe line +## BUG-15: pi-watch crashes at import: unpinned pi-coding-agent no longer exports AuthStorage +- **Observed**: 2026-10-13 +- **File**: skills/pi-dev/scripts/pi-watch/pi-watch.mjs:23 +- **Description**: skills/pi-dev/scripts/pi-watch/package.json declares @earendil-works/pi-coding-agent and pi-ai as "*". The installed version (pi 0.87.1) no longer exports AuthStorage, so pi-watch --check codex|gemini|opus exits 1 with: SyntaxError: The requested module '@earendil-works/pi-coding-agent' does not provide an export named 'AuthStorage'. Every pi-backed path (SDD pi implementers, adversarial-review reviewers) is dead; the SDD run on zpyoung/writing-artifacts fell back to Claude Agent reviewers with reduced independence. +- **Severity**: high +- **Proposed fix**: Pin both deps to a known-good version with a frozen lockfile install, and adapt to the new export if AuthStorage moved. + +## BUG-16: Kit elements render unstyled in tier-2 apps +- **Observed**: 2026-10-14 +- **File**: skills/writing-html-artifacts/vendor/kit/1.0.0/kit.css:715 +- **Description**: kit.css relies on Simple.css for pre, button, select and summary styling. In a tier-2 build (Astryx reset.css, no Simple.css), q-code's
body has no background, border or padding and sits outside the header bar's box; q-board's Reset / Copy as Markdown buttons and Move to… selects look like plain text; q-chart's Table view summary has no affordance. Tier-1 report pages look correct. Repro: build-app kit-showcase-app, then The change > Rollback tab, and the Follow-ups tab. +- **Severity**: medium +- **Proposed fix**: Style the kit's own inserted controls and the q-code pre in kit.css instead of inheriting from the host stylesheet; add a tier-2 verify screenshot of a non-first route so this is caught. + +## BUG-17: q-flow browser tests time out when Chrome is launched from a Python parent +- **Observed**: 2026-10-17 +- **File**: tests/fixtures/html-kit/harness.mjs:156 +- **Description**: The harness waitForFunction (rAF polling) for data-q-flow-ready times out at 10s and also at 30s, though the page is ready ~120ms after load when the same harness.mjs runs directly from zsh or perl (<1s total). Any Python parent (rye 3.11, homebrew, /usr/bin) reproduces it, including os.system; close_fds, start_new_session, stdin DEVNULL and the shell's __CF_USER_TEXT_ENCODING made no difference. Interval polling and Chrome's --disable-renderer-backgrounding/--disable-background-timer-throttling/--disable-backgrounding-occluded-windows flags also made no difference: the page itself takes 10-25s to become ready. Verified: relaying the harness call through a node server started from zsh (so Chrome has no Python ancestor) makes the same pytest tests pass in about 3s, so the cause is the Python ancestry, not CPU load. Affects the q-flow tests in tests/test_html_kit_elements.py (runner _run_harness at :91), e.g. test_flow_renders_at_rest_with_controls_closed_list_and_sync_async_edges and test_flow_pulse_travels_at_one_speed_and_streams_before_the_last_one_lands, on zpyoung/q-flow-shapes at efa903b, unmodified. Blocks local browser verification; CI unknown. +- **Severity**: medium + +## BUG-18: Verify's connected-navigation timeout test reports network passed under the local relay +- **Observed**: 2026-10-17 +- **File**: tests/test_html_verify.py:300 +- **Description**: test_connected_navigation_timeout_is_a_network_failure_with_a_report fails locally: verify reports network passed=True where the test expects a navigation-timeout network failure. Reproduces at a36dcfc (before the q-flow traces work) when run through the BUG-17 node relay shim, so the traces branch did not cause it. The relay or the local network may change how the connected-navigation timeout surfaces. Found 2026-10-17 during the q-flow traces wave-1 gate. +- **Severity**: low + +## BUG-19: A q-flow label, kind or detail with an escaped lone surrogate breaks the page build +- **Observed**: 2026-10-17 +- **File**: skills/writing-html-artifacts/scripts/htmlkit/kitcheck.py:1284 +- **Description**: The no-JS fallback that inject_flow_fallbacks writes for acopies node labels (and other graph strings) from the JSON. A JSON escape such as "\ud800" decodes to a lone surrogate that passes the kit check, and the assembled page then raises UnicodeEncodeError when written as UTF-8. Reproduced with a node label "A \ud800". Trace values get the same treatment in the q-flow traces branch, where they fail the check as not JSON; labels, kinds, group labels, edge labels and details predate that and still crash. Found 2026-10-17 in the q-flow traces final review. +- **Severity**: low +- **Status**: fixed on branch zpyoung/q-flow-traces (graph-level UTF-8 check) + +## BUG-20: q-flow keeps pulsing when reduced motion is switched on mid-pulse +- **Observed**: 2026-10-17 +- **File**: skills/writing-html-artifacts/vendor/kit/1.0.0/kit.js:5840 +- **Description**: Reduced motion is read only when a trace step is shown (and when a node is focused). If the reader turns on prefers-reduced-motion while a trace edge or a focused path is pulsing, the pulse keeps running until the next step, a new focus, or trace mode closes; there is no change listener on the media query. Focus mode had the same gap before traces. Found 2026-10-17 by the q-flow traces final review (HX1), logged instead of a fourth review round. +- **Severity**: low +- **Proposed fix**: Listen for changes to the reduced-motion query while pulsing; stop the pulse and apply the static 3.5px lit style. + +## BUG-21: q-flow's no-JS fallback crashes the build on very deep group nesting +- **Observed**: 2026-10-17 (q-schema SDD checkpoint review) +- **File**: skills/writing-html-artifacts/scripts/htmlkit/kitcheck.py:2086 +- **Description**: `_render_flow_fallback_html` renders groups with a recursive `render_group`, so a q-flow graph whose groups nest about 1,000 deep raises an uncaught RecursionError during fallback injection and aborts the build instead of reporting a check failure. q-schema had the same pattern and now uses an explicit stack (commit 6f59893). +- **Introduced by**: unknown (q-flow fallback, before d6895d2) +- **Severity**: low +- **Proposed fix**: render the group tree with an explicit stack, as `_render_schema_fallback_html` does. +- **Blocker for**: nothing in practice; only pathological input + +## BUG-22: Text "data-q-flow-list" inside q-flow data suppresses the no-JS fallback +- **Observed**: 2026-10-17 (q-schema SDD checkpoint review) +- **File**: skills/writing-html-artifacts/scripts/htmlkit/kitcheck.py:2120 +- **Description**: `inject_flow_fallbacks` skips any block whose raw text contains "data-q-flow-list", including that text inside a node label or detail in the JSON, so such a graph ships with no no-JS list. q-schema had the same guard and now checks for a real element attribute outside the JSON script (commit 6f59893). +- **Introduced by**: unknown (q-flow fallback, before d6895d2) +- **Severity**: low +- **Proposed fix**: skip only when a tag outside the JSON script carries a data-q-flow-list attribute, as `inject_schema_fallbacks` does. +- **Blocker for**: no-JS readers of a graph whose data mentions the attribute name + +## BUG-23: q-flow keys its id maps on plain objects, so ids like "constructor" misbehave +- **Observed**: 2026-10-17 (q-schema SDD checkpoint review) +- **File**: skills/writing-html-artifacts/vendor/kit/1.0.0/kit.js:3849 +- **Description**: QFlow's parse, layout and focus code index author-supplied node and group ids into plain `{}` objects (`seenIds`, `groupMap`, `nodeMap`, `outByNode`, `nodeSizes`, ...). An id such as `constructor`, `toString` or `__proto__` resolves to an inherited property. The same pattern in q-schema crashed layout before readiness was set; q-flow was not reproduced here and is fenced from this branch. q-schema now uses prototype-free maps (commit 76cd7b9). +- **Introduced by**: unknown (q-flow runtime, before d6895d2) +- **Severity**: medium +- **Proposed fix**: use `Object.create(null)` or `Map` for every QFlow dictionary keyed by an author id, and add a browser test with those ids. +- **Blocker for**: graphs whose node or group ids collide with Object.prototype names + +## BUG-24: Text " " inside q-flow data stops the no-JS fallback injection early +- **Observed**: 2026-10-17 (q-schema SDD final review) +- **File**: skills/writing-html-artifacts/scripts/htmlkit/kitcheck.py:2109 +- **Description**: `inject_flow_fallbacks` finds each element with the non-greedy regex `_FLOW_BLOCK`, which stops at the text `` when it appears inside the JSON script (valid JSON; browsers end a script only at ` detail may hold only paragraphs, lists, code and links' instead of the JSON-child message. The page still fails the check, so only the diagnostic is wrong. Found 2026-10-17 by the q-sequence closure review (CL1). +- **Severity**: low +- **Proposed fix**: Raise a distinct error from the constant hook and report the JSON-child message for it before the forbidden-markup search. + +## BUG-31: The diagram list disclosure renders as plain text in tier-2 app builds +- **Observed**: 2026-10-18 +- **File**: skills/writing-html-artifacts/vendor/kit/1.0.0/kit.css:1282 +- **Description**: In tier-2 app builds the "Graph as a list" and "Diagram as a list" disclosures under q-flow and q-sequence render as plain unstyled text, with no bordered box or chevron. Tier-1 report and components pages show a bordered disclosure with a chevron. The kit's own rules (kit.css:1282 for q-flow and q-schema, :1668 for q-sequence) only set height, layout and cursor; the box and chevron on tier-1 pages come from the base stylesheet (Simple.css or Web Awesome), which the app's Astryx theme doesn't supply. Seen 2026-10-18 building templates/app/examples/explainer and reference at 4fe1e65 and screenshotting at 1280px light. +- **Severity**: low +- **Proposed fix**: Give the kit's diagram disclosures their own border, padding and marker so they look the same without a base stylesheet, or add matching details styles to the app theme. + +## BUG-32: SKILL.md and orchestration.md disagree on which dispatches carry the mutation-probe line - **Observed**: 2026-10-18 - **File**: skills/full-code-review/SKILL.md:318 - **Description**: SKILL.md Step 4 says to state the mutation-probe line in every dispatch, but orchestration.md's 'Every specialist prompt also says' paragraph says the coordinator adds 'mutation probe: enabled' only to the three test lenses, and only with consent. A coordinator can't tell whether every dispatch needs a mutation-probe line (disabled for the non-test lenses) or only the test lenses do. Doc-only. Found while fixing the gaps the MR !722 re-review exposed. - **Severity**: low - **Proposed fix**: Pick one rule: either every dispatch states the probe status (enabled or disabled), or only the test lenses get the line. Then make both files say the same thing. -## BUG-16: full-code-review lets the coordinator skip the lens fan-out on small diffs +## BUG-33: full-code-review lets the coordinator skip the lens fan-out on small diffs - **Observed**: 2026-10-19 - **File**: skills/full-code-review/SKILL.md:292 - **Description**: In a headless smoke-corpus run (claude -p "/quirk:full-code-review --local --no-open", Sonnet 5.5 default, 2026-10-19), all 5 smoke cases reviewed the 2-9 line diff inline instead of dispatching lenses and wrote no report.json (one used the ReportFindings tool, one wrote a Markdown report). One also overrode a pre-exported REVIEW_OUT_DIR with its own mktemp despite the ${REVIEW_OUT_DIR:-...} default in Setup. SKILL.md never says the fan-out runs regardless of diff size, so the smoke corpus can't run without a prompt override and real tiny-diff reviews silently drop to one inline pass. - **Severity**: medium - **Proposed fix**: State in SKILL.md (Setup + Step 4) that the lens fan-out and report.json are mandatory at any diff size with no inline shortcut, and that a pre-set REVIEW_OUT_DIR is kept; add a test pinning that wording. -- **Resolved**: 2026-10-19. Wording alone (fan-out at any size, keep REVIEW_OUT_DIR) fixed the out-dir override but still left 3 of 5 plain-prompt runs skipping, now citing "headless run on a one-line diff". Closing those excuses by name (one Agent per lens; skip only on run_when, policy, or failed dispatch; diff size, headless, no forge, or cost is never a skip reason) brought all 5 to a full fan-out, report.json in the caller's dir, and a passing smoke_check. Residual lens-packing is BUG-17. +- **Resolved**: 2026-10-19. Wording alone (fan-out at any size, keep REVIEW_OUT_DIR) fixed the out-dir override but still left 3 of 5 plain-prompt runs skipping, now citing "headless run on a one-line diff". Closing those excuses by name (one Agent per lens; skip only on run_when, policy, or failed dispatch; diff size, headless, no forge, or cost is never a skip reason) brought all 5 to a full fan-out, report.json in the caller's dir, and a passing smoke_check. Residual lens-packing is BUG-34. -## BUG-17: full-code-review coordinator occasionally packs several lenses into one agent +## BUG-34: full-code-review coordinator occasionally packs several lenses into one agent - **Observed**: 2026-10-19 - **File**: skills/full-code-review/SKILL.md:298 -- **Description**: In the 2026-10-19 plain-prompt smoke run after the BUG-16 fix, 1 of 54 lens dispatches (correctness case) combined comments, db and observability into one agent despite SKILL.md Step 4 saying one Agent per lens. A packed agent shares one context across checklists, so a lens can get a shallower pass than its own dispatch would give, and run.models then reports per-lens models for a single dispatch. Earlier rounds also packed lenses (test-adequacy case, 2 multi-lens agents). +- **Description**: In the 2026-10-19 plain-prompt smoke run after the BUG-33 fix, 1 of 54 lens dispatches (correctness case) combined comments, db and observability into one agent despite SKILL.md Step 4 saying one Agent per lens. A packed agent shares one context across checklists, so a lens can get a shallower pass than its own dispatch would give, and run.models then reports per-lens models for a single dispatch. Earlier rounds also packed lenses (test-adequacy case, 2 multi-lens agents). - **Severity**: low - **Proposed fix**: Re-run the smoke corpus a few times to measure the rate; if it persists, have the coordinator check its dispatch count against policy.json's runnable lenses before Step 5, or record per-lens agent ids in run so a packed dispatch is visible in the report. diff --git a/README.md b/README.md index 7752d3e1..42a62188 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Version: **2026.7.9** ## What ships -- **26 skills** under `skills/` covering TDD, systematic debugging, brainstorming (which produces a logic spec), deep-research/brainstorming exploration (with a `--involve` user-inclusion dial: checkpoints, mid-run steering, and idea-gate co-creation), divergent ideation, the two-document spec rubric (logic spec + tech spec) and plan writing, scannable-prose revision, code review, a full multi-lens pre-merge review for GitHub and GitLab, adversarial review (typed profiles, a promote/refute two-stage, and an evidence gate), parallel agent dispatch, guided bug/feature/code-change filing with provenance-marked artifacts, gating simplification behind a correctness check, and more. +- **27 skills** under `skills/` covering TDD, systematic debugging, brainstorming (which produces a logic spec), deep-research/brainstorming exploration (with a `--involve` user-inclusion dial: checkpoints, mid-run steering, and idea-gate co-creation), divergent ideation, the two-document spec rubric (logic spec + tech spec) and plan writing, scannable-prose revision, code review, a full multi-lens pre-merge review for GitHub and GitLab, adversarial review (typed profiles, a promote/refute two-stage, and an evidence gate), parallel agent dispatch, guided bug/feature/code-change filing with provenance-marked artifacts, gating simplification behind a correctness check, and more. - **Typed artifacts** (added in 5.1.0) — surface-routing for observations Claude cannot act on. See below. ## Installation @@ -70,16 +70,13 @@ generate `logic.md`, and use a local `review.html` for decisions and sign-off. The page and decision exports are not committed. See [the interactive rubric](skills/writing-specs/interactive-logic-spec.md). -## Agent Isles bridge +## HTML artifacts and the visual companion -Quirk can author Agent Isles-aware artifacts while keeping Markdown canonical and Agent Isles optional. The stdlib-only helper in `bin/agent_isles.py` detects a local `isles` binary, an `isles` on `PATH`, or an explicit `npx agent-isles@next` fallback; it never silently installs packages. - -```bash -python3 bin/agent_isles.py doctor -python3 bin/agent_isles.py command examples/agent-isles/typed-artifacts-summary.md -``` - -The experimental local component pack lives in `packs/quirk/` and covers typed artifact summaries, TDD cycle reports, plan readiness, and review findings. Examples live in `examples/agent-isles/`. See `docs/agent-isles-artifacts.md` for the authoring contract: Markdown source is canonical, generated HTML under `.quirk/isles/` is disposable by default, and the Quirk pack is trusted local code. +The `writing-html-artifacts` skill builds offline, single-file HTML pages from templates and +vendored libraries, and runs the brainstorming visual companion: a small local Python server +that shows option screens in a browser and sends clicks back to the agent. Brainstorming and +exploring-ideas both use it. Markdown stays the canonical source for specs; the companion needs +Python 3 only. ## Development diff --git a/bin/agent_isles.py b/bin/agent_isles.py deleted file mode 100755 index 28ccf61b..00000000 --- a/bin/agent_isles.py +++ /dev/null @@ -1,506 +0,0 @@ -#!/usr/bin/env python3 -"""Stdlib-only Agent Isles bridge for Quirk artifacts. - -The helper constructs deterministic Agent Isles commands without making Agent -Isles a hidden hard dependency. It prefers a repo-local binary, then PATH, then -an explicit npx fallback when allowed. -""" -from __future__ import annotations - -import argparse -import json -import os -import shlex -import shutil -import subprocess -import sys -import time -from dataclasses import dataclass -from pathlib import Path -from typing import Callable - -DEFAULT_OUTPUT_DIR = Path(".quirk") / "isles" -STATE_DIRNAME = "state" -EVENTS_FILENAME = "events" -DEFAULT_NPX_PACKAGE = "agent-isles@next" -# `isles live` is not on the published npm tag yet (it lives on github main). Until a -# release with `live` ships to npm, the live npx fallback targets the github spec. -LIVE_NPX_PACKAGE = "github:zpyoung/agent-isles" - - -class AgentIslesUnavailable(RuntimeError): - """Raised when no usable Agent Isles command path can be constructed.""" - - -@dataclass(frozen=True) -class CommandPlan: - argv: list[str] - output: Path | None - note: str | None = None - - -def _repo_local_isles(repo_root: Path) -> Path | None: - candidate = repo_root / "node_modules" / ".bin" / ("isles.cmd" if os.name == "nt" else "isles") - if candidate.exists() and os.access(candidate, os.X_OK): - return candidate - return None - - -def _which(name: str) -> str | None: - found = shutil.which(name) - return found if found else None - - -def detect_runner( - repo_root: Path, *, no_npx: bool = False, npx_package: str = DEFAULT_NPX_PACKAGE -) -> tuple[list[str], str | None]: - """Return the executable prefix and optional note for Agent Isles.""" - local = _repo_local_isles(repo_root) - if local is not None: - return [str(local)], None - path_isles = _which("isles") - if path_isles: - return [path_isles], None - if not no_npx: - npx = _which("npx") - if npx: - return [npx, npx_package], f"npx fallback is explicit; it may download {npx_package} when executed" - raise AgentIslesUnavailable( - "No Agent Isles executable found. Install/use a repo-local node_modules/.bin/isles, " - "put isles on PATH, or rerun without --no-npx to print/use the explicit npx agent-isles@next fallback." - ) - - -def default_output_for(artifact: Path, repo_root: Path) -> Path: - stem = artifact.stem or "artifact" - return repo_root / DEFAULT_OUTPUT_DIR / f"{stem}.html" - - -def build_command( - action: str, - artifact: Path, - *, - repo_root: Path | None = None, - output: Path | None = None, - no_npx: bool = False, - with_user_packs: bool = False, - no_quirk_pack: bool = False, - extra_packs: list[Path] | None = None, - execute: bool = False, -) -> CommandPlan: - """Build a render/preview command. Does not create files unless execute=True.""" - if action not in {"render", "preview"}: - raise ValueError(f"unsupported action: {action}") - repo = (repo_root or Path.cwd()).resolve() - artifact_path = artifact if artifact.is_absolute() else (Path.cwd() / artifact) - artifact_path = artifact_path.resolve() - if not artifact_path.exists(): - raise FileNotFoundError(f"artifact not found: {artifact_path}") - - runner, note = detect_runner(repo, no_npx=no_npx) - argv = [*runner, action, str(artifact_path)] - - output_path: Path | None = None - if action == "render": - output_path = (output if output is not None else default_output_for(artifact_path, repo)).resolve() - argv.extend(["--output", str(output_path)]) - - pack_paths: list[Path] = [] - quirk_pack = repo / "packs" / "quirk" - if not no_quirk_pack and quirk_pack.exists(): - pack_paths.append(quirk_pack) - pack_paths.extend(extra_packs or []) - for pack in pack_paths: - argv.extend(["--pack", str(pack.resolve())]) - if not with_user_packs: - argv.append("--no-user-packs") - - if execute and output_path is not None: - output_path.parent.mkdir(parents=True, exist_ok=True) - return CommandPlan(argv=argv, output=output_path, note=note) - - -def build_live_command( - screen_dir: Path, - *, - repo_root: Path | None = None, - stop: bool = False, - port: int | None = None, - host: str | None = None, - url_host: str | None = None, - idle_timeout: float | None = None, - owner_pid: int | None = None, - no_npx: bool = False, - execute: bool = False, -) -> CommandPlan: - """Build an `isles live` command for the brainstorming visual companion. - - Unlike render/preview, `isles live` resolves packs internally (it renders with - project-dir discovery and no user packs) and REJECTS `--pack`/`--no-user-packs` - as unknown options. So this builder never adds them. - """ - repo = (repo_root or Path.cwd()).resolve() - screen = screen_dir if screen_dir.is_absolute() else (Path.cwd() / screen_dir) - screen = screen.resolve() - - runner, note = detect_runner(repo, no_npx=no_npx, npx_package=LIVE_NPX_PACKAGE) - argv = [*runner, "live", str(screen)] - - if stop: - argv.append("--stop") - return CommandPlan(argv=argv, output=None, note=note) - - if port is not None: - argv.extend(["--port", str(port)]) - if host: - argv.extend(["--host", host]) - if url_host: - argv.extend(["--url-host", url_host]) - if idle_timeout is not None: - argv.extend(["--idle-timeout", str(idle_timeout)]) - if owner_pid is not None: - argv.extend(["--owner-pid", str(owner_pid)]) - - if execute: - screen.mkdir(parents=True, exist_ok=True) - return CommandPlan(argv=argv, output=None, note=note) - - -def _read_events(events_path: Path) -> list[dict]: - """Parse the live server's JSONL events file; skip blank/malformed lines. - - Returns [] if the file is absent or vanishes mid-read (the live server may - clear/rotate it between our existence check and the read). - """ - try: - raw = events_path.read_text(encoding="utf-8", errors="replace") - except FileNotFoundError: - return [] - records: list[dict] = [] - for line in raw.splitlines(): - line = line.strip() - if not line: - continue - try: - obj = json.loads(line) - except json.JSONDecodeError: - continue - if isinstance(obj, dict): - records.append(obj) - return records - - -def _event_ts(event: dict) -> float: - """The record's epoch-seconds timestamp, or 0.0 if absent/non-numeric.""" - ts = event.get("timestamp") - return float(ts) if isinstance(ts, (int, float)) and not isinstance(ts, bool) else 0.0 - - -def _screen_matches(event: dict, screen: str | None) -> bool: - """Lenient screen match: no filter, or the record predates the screen nonce - (no ``screen`` field — older Agent Isles), or it matches. Stays compatible - with builds that don't stamp a screen, while rejecting stale-tab clicks that - carry a *different* screen.""" - if screen is None: - return True - recorded = event.get("screen") - return recorded is None or recorded == screen - - -def latest_proceed_event( - events_path: Path, *, since: float = 0.0, screen: str | None = None -) -> dict | None: - """Newest proceed with ``timestamp >= since`` and a matching screen, or None.""" - proceeds = [ - e for e in _read_events(events_path) - if e.get("type") == "proceed" and _event_ts(e) >= since and _screen_matches(e, screen) - ] - return proceeds[-1] if proceeds else None - - -def make_file_transport( - events_path: Path, *, since: float = 0.0, screen: str | None = None -) -> Callable[[], dict | None]: - """Build the file-poll transport: the newest proceed at/after ``since``. - - ``since`` is the current screen's start time (epoch seconds), so a stale - proceed left over from a PREVIOUS screen has an older timestamp and is - ignored — it can't false-advance even if the live server's best-effort - clearEvents never ran. Crucially ``since`` is SCREEN-scoped, not wait-scoped: - re-running ``wait`` after a timeout with the same ``since`` is a true - continuation, so a click that arrives between turns is never dropped (a - count/start-time baseline would lose it). - - ``screen`` adds a second, exact guard: only proceeds stamped with that screen - (the Agent Isles nonce) match, so a click from a stale, not-yet-reloaded tab - is rejected even within the same timestamp second. Lenient toward older - Agent Isles builds that don't stamp a screen (see :func:`_screen_matches`). - - This is the swappable seam. Phase 2's channel transport returns a pushed - proceed with the same "for this screen" semantics — the wait loop and event - shape stay identical. - """ - - def poll() -> dict | None: - return latest_proceed_event(events_path, since=since, screen=screen) - - return poll - - -def wait_for_proceed( - transport: Callable[[], dict | None], - *, - timeout: float, - poll_interval: float, - _clock: Callable[[], float] = time.monotonic, - _sleep: Callable[[float], None] = time.sleep, -) -> dict | None: - """Block until ``transport()`` yields a proceed event; return None on timeout. - - ``timeout=0`` checks the transport exactly once (no blocking). - """ - deadline = _clock() + timeout - while True: - event = transport() - if event is not None: - return event - if _clock() >= deadline: - return None - _sleep(poll_interval) - - -def resolve_state_dir(screen_dir: Path, state_dir: Path | None = None) -> Path: - """Where the live server writes events: `` /state`` unless overridden.""" - return state_dir if state_dir is not None else screen_dir / STATE_DIRNAME - - -def newest_screen_mtime(screen_dir: Path) -> int: - """Floor of the newest ``*.md`` screen mtime (epoch seconds), or 0 if none. - - Used as the default proceed cutoff so ``wait`` only honors clicks for the - current screen. Floored to match the server's integer-second timestamps. - """ - newest = 0.0 - try: - for path in screen_dir.glob("*.md"): - try: - mtime = path.stat().st_mtime - except OSError: - continue - if mtime > newest: - newest = mtime - except OSError: - pass - return int(newest) - - -def newest_screen_name(screen_dir: Path) -> str | None: - """Basename of the newest ``*.md`` screen, or None — the default screen nonce.""" - newest_name: str | None = None - newest_mtime = -1.0 - try: - for path in screen_dir.glob("*.md"): - try: - mtime = path.stat().st_mtime - except OSError: - continue - if mtime > newest_mtime: - newest_mtime = mtime - newest_name = path.name - except OSError: - pass - return newest_name - - -def server_running(state_dir: Path) -> bool: - """Best-effort: a live server is up for this dir (info present, not stopped, PID alive).""" - if (state_dir / "server-stopped").exists(): - return False - try: - data = json.loads((state_dir / "server-info").read_text(encoding="utf-8")) - except (FileNotFoundError, ValueError): - return False - pid = data.get("pid") - if not isinstance(pid, int): - return True # info present but no PID — assume running - try: - os.kill(pid, 0) - except ProcessLookupError: - return False - except (PermissionError, OSError): - return True # exists but not signalable by us → treat as alive - return True - - -def doctor(repo_root: Path, *, no_npx: bool = False) -> str: - repo = repo_root.resolve() - lines = ["Agent Isles bridge doctor", f"repo root: {repo}"] - local = _repo_local_isles(repo) - lines.append(f"repo-local isles: {local if local else 'not found'}") - path_isles = _which("isles") - lines.append(f"PATH isles: {path_isles if path_isles else 'not found'}") - npx = _which("npx") - lines.append(f"npx: {npx if npx else 'not found'}") - pack = repo / "packs" / "quirk" - lines.append(f"Quirk pack: {pack if pack.exists() else 'not found'}") - lines.append(f"default output: {repo / DEFAULT_OUTPUT_DIR}/") - try: - runner, note = detect_runner(repo, no_npx=no_npx) - lines.append("recommended runner: " + " ".join(shlex.quote(part) for part in runner)) - if note: - lines.append("note: " + note) - except AgentIslesUnavailable as exc: - lines.append("recommended runner: unavailable") - lines.append("reason: " + str(exc)) - lines.append("No packages were installed and no files were written.") - return "\n".join(lines) + "\n" - - -def _add_common_options(parser: argparse.ArgumentParser) -> None: - parser.add_argument("--repo-root", type=Path, default=Path.cwd(), help="Repository root; defaults to cwd.") - - -def _parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser(description="Quirk Agent Isles bridge helper.") - _add_common_options(parser) - sub = parser.add_subparsers(dest="command", required=True) - - doctor_p = sub.add_parser("doctor", help="Report detected Agent Isles command paths.") - _add_common_options(doctor_p) - doctor_p.add_argument("--no-npx", action="store_true", help="Do not consider npx fallback.") - - for name in ("render", "preview", "command"): - p = sub.add_parser(name, help=("Print command" if name == "command" else f"Build and run Agent Isles {name}.")) - _add_common_options(p) - p.add_argument("artifact", type=Path) - p.add_argument("--output", type=Path, help="Render output path; render only.") - p.add_argument("--no-npx", action="store_true", help="Disable explicit npx fallback.") - p.add_argument("--with-user-packs", action="store_true", help="Allow Agent Isles user packs.") - p.add_argument("--no-quirk-pack", action="store_true", help="Do not add packs/quirk automatically.") - p.add_argument("--pack", action="append", type=Path, default=[], help="Additional local trusted pack path.") - if name in {"render", "preview"}: - p.add_argument("--print-command", action="store_true", help="Print the command instead of executing it.") - - live_p = sub.add_parser("live", help="Start/stop the Agent Isles live brainstorming companion server.") - _add_common_options(live_p) - live_p.add_argument("dir", type=Path, help="Screen directory; agent writes *.md screens here, events land in /state/events.") - live_p.add_argument("--stop", action="store_true", help="Stop a running live server for .") - live_p.add_argument("--port", type=int, help="Bind to a fixed port (default: ephemeral).") - live_p.add_argument("--host", help="Bind host (default 127.0.0.1).") - live_p.add_argument("--url-host", help="Hostname to print in the served URL.") - live_p.add_argument("--idle-timeout", type=float, help="Idle minutes before auto-shutdown (default 30).") - live_p.add_argument("--owner-pid", type=int, help="Shut down when this PID exits.") - live_p.add_argument("--no-npx", action="store_true", help="Disable explicit npx fallback.") - live_p.add_argument("--print-command", action="store_true", help="Print the command instead of executing it.") - - wait_p = sub.add_parser( - "wait", - help="Block until a browser 'proceed' event for , then print the selection JSON.", - ) - _add_common_options(wait_p) - wait_p.add_argument("dir", type=Path, help="Screen directory passed to `live`; events are read from /state/events.") - wait_p.add_argument("--timeout", type=float, default=110.0, help="Max seconds to block (default 110, under the 120s Bash-tool default). 0 = check once.") - wait_p.add_argument("--poll-interval", type=float, default=0.5, help="Seconds between file polls (default 0.5; floored at 0.05).") - wait_p.add_argument("--state-dir", type=Path, help="Override the events state dir (default /state).") - wait_p.add_argument("--since", type=float, help="Only honor proceeds with timestamp >= this epoch (default: newest screen mtime). Pass the same value across re-runs to continue waiting on one screen.") - wait_p.add_argument("--screen", help="Only honor proceeds stamped with this screen basename (default: newest *.md). Rejects stale-tab clicks; lenient toward Agent Isles builds that don't stamp a screen.") - return parser - - -def main(argv: list[str] | None = None) -> int: - parser = _parser() - args = parser.parse_args(argv) - repo_root = args.repo_root.resolve() - if args.command == "doctor": - print(doctor(repo_root, no_npx=args.no_npx), end="") - return 0 - - if args.command == "wait": - screen = args.dir if args.dir.is_absolute() else (Path.cwd() / args.dir) - screen = screen.resolve() - state_dir = resolve_state_dir(screen, args.state_dir.resolve() if args.state_dir else None) - if not server_running(state_dir): - print( - f"agent_isles.py: no live Agent Isles server running for {screen}; " - "start one with `live` first (nothing can deliver a proceed).", - file=sys.stderr, - ) - return 2 - events_path = state_dir / EVENTS_FILENAME - since = args.since if args.since is not None else float(newest_screen_mtime(screen)) - screen_nonce = args.screen if args.screen is not None else newest_screen_name(screen) - transport = make_file_transport(events_path, since=since, screen=screen_nonce) - event = wait_for_proceed(transport, timeout=args.timeout, poll_interval=max(args.poll_interval, 0.05)) - if event is None: - print( - f"agent_isles.py: no proceed event within {args.timeout}s (timed out); " - f"re-run wait to keep waiting (pass --since {since:.0f} to resume the same " - "screen), or fall back to terminal-text advance.", - file=sys.stderr, - ) - return 1 - print(json.dumps(event)) - return 0 - - if args.command == "live": - execute = not args.print_command - try: - plan = build_live_command( - args.dir, - repo_root=repo_root, - stop=args.stop, - port=args.port, - host=args.host, - url_host=args.url_host, - idle_timeout=args.idle_timeout, - owner_pid=args.owner_pid, - no_npx=args.no_npx, - execute=execute, - ) - except AgentIslesUnavailable as exc: - print(f"agent_isles.py: {exc}", file=sys.stderr) - return 2 - quoted = " ".join(shlex.quote(part) for part in plan.argv) - if args.print_command: - print(quoted) - if plan.note: - print(f"# {plan.note}") - return 0 - if plan.note: - print(plan.note, file=sys.stderr) - completed = subprocess.run(plan.argv) - return completed.returncode - - action = "render" if args.command == "command" else args.command - execute = args.command in {"render", "preview"} and not getattr(args, "print_command", False) - try: - plan = build_command( - action, - args.artifact, - repo_root=repo_root, - output=args.output, - no_npx=args.no_npx, - with_user_packs=args.with_user_packs, - no_quirk_pack=args.no_quirk_pack, - extra_packs=args.pack, - execute=execute, - ) - except (AgentIslesUnavailable, FileNotFoundError, ValueError) as exc: - print(f"agent_isles.py: {exc}", file=sys.stderr) - return 2 - - quoted = " ".join(shlex.quote(part) for part in plan.argv) - if args.command == "command" or getattr(args, "print_command", False): - print(quoted) - if plan.note: - print(f"# {plan.note}") - return 0 - - if plan.note: - print(plan.note, file=sys.stderr) - completed = subprocess.run(plan.argv) - return completed.returncode - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/docs/agent-isles-artifacts.md b/docs/agent-isles-artifacts.md deleted file mode 100644 index 1a8f4d69..00000000 --- a/docs/agent-isles-artifacts.md +++ /dev/null @@ -1,69 +0,0 @@ -# Agent Isles-aware Quirk artifacts - -Quirk is the authoring and workflow discipline layer. Agent Isles is the optional rendering and presentation layer. This contract keeps normal Quirk skill use lightweight while making richer rendered reports available when Agent Isles is present. - -## Core contract - -- Markdown source is canonical. Review, edit, diff, and archive the `.md` files. -- generated HTML is disposable by default. The bridge writes to `.quirk/isles/`, which is ignored and can be deleted or regenerated at any time. -- Agent Isles is optional. Quirk must remain useful when Node, npm, npx, and Agent Isles are absent. -- Any built-in Agent Isles island may be used when it improves artifact structure, scanning, or readability. -- Quirk-specific component-pack tags represent Quirk-owned semantics such as typed artifacts, TDD cycles, plan readiness, and review findings. -- Prefer plain Markdown when a heading, list, or table communicates the artifact more clearly than a component. - -## Bridge helper - -Use the stdlib-only helper to inspect or run the optional renderer: - -```bash -python3 bin/agent_isles.py doctor -python3 bin/agent_isles.py command examples/agent-isles/typed-artifacts-summary.md -python3 bin/agent_isles.py render examples/agent-isles/typed-artifacts-summary.md -``` - -The helper chooses command paths in this order: - -1. repo-local `node_modules/.bin/isles`, -2. `isles` on `PATH`, -3. explicit `npx agent-isles@next ...` when `npx` exists and `--no-npx` is not set. - -It never silently installs packages. The npx fallback is printed or executed only as an explicit command path and may download `agent-isles@next` according to npx behavior. - -By default the helper adds `--pack packs/quirk --no-user-packs` when the local Quirk pack exists. Use `--with-user-packs`, `--no-quirk-pack`, and repeated `--pack ` only when a workflow intentionally changes that reproducibility boundary. - -## Authoring rules - -- Keep a useful Plain Markdown fallback near every island or custom element. A reader should understand the artifact in a terminal, code review, or raw GitHub view. -- Use no self-closing custom elements. Write ` ... `, not ``. -- Keep raw HTML blocks continuous when nesting custom elements; many Markdown parsers stop raw HTML parsing after blank lines or indentation changes. -- Put durable data in Markdown text and safe attributes. Do not rely on generated DOM state as the source of truth. -- Use sanitized mode for untrusted Markdown renders. The Quirk pack manifest declares sanitized-mode attributes and intentionally omits unsafe attributes such as `style`, `srcdoc`, and event handlers. -- Treat component packs as trusted local code. Even in sanitized mode, local pack JavaScript executes in the rendered page; review pack changes like code. -- Avoid gratuitous components. Built-in ` ` islands and Quirk ` ` tags are views, not a replacement for clear prose. - -## When to use each surface - -- Use built-in ` ` islands for generic presentation: status boards, decisions, risks, timelines, action lists, comparisons, and callouts. -- Use ` ` for Quirk typed artifact rollups across `BUGS.md`, `DEFERRED.md`, `TEST_BACKLOG.md`, `proposals.md`, and ADR files. -- Use ` ` for red/green/refactor state, verification evidence, and deferred/skipped test debt. -- Use ` ` for plan readiness gates: scope, clarity, files, tests, risks, and handoff readiness. -- Use ` ` for review findings that need severity, path, recommendation, and status. - -## Compatibility - -This integration targets the current prerelease Agent Isles command surface exposed through `agent-isles@next` and local `isles` binaries. Default Quirk tests do not require Agent Isles or Node. Developers who have Agent Isles available can smoke-test with: - -```bash -python3 bin/agent_isles.py doctor -python3 bin/agent_isles.py render examples/agent-isles/integrated-workflow.md --print-command -python3 bin/agent_isles.py render examples/agent-isles/integrated-workflow.md -``` - -Expected reproducibility flags for direct Agent Isles commands are: - -```bash -isles packs resolve examples/agent-isles/integrated-workflow.md --pack packs/quirk --no-user-packs -isles render examples/agent-isles/integrated-workflow.md --pack packs/quirk --no-user-packs --output .quirk/isles/integrated-workflow.html -``` - -Generated files under `.quirk/isles/` are non-canonical views and should not be committed unless a future publishing issue deliberately changes that policy. diff --git a/docs/quirk/specs/2026-10-13-writing-html-artifacts/logic.md b/docs/quirk/specs/2026-10-13-writing-html-artifacts/logic.md new file mode 100644 index 00000000..5153f498 --- /dev/null +++ b/docs/quirk/specs/2026-10-13-writing-html-artifacts/logic.md @@ -0,0 +1,534 @@ +# Logic spec: `writing-html-artifacts` + +**Status**: Approved, 2026-10-13 · Tech spec: [tech.md](tech.md) (gate fired: multi-session, several subsystems) + +A quirk skill that has agents in Claude Code, pi and Oh My Pi build HTML files that open anywhere, +look deliberate, and come with proof that they work. The same machinery gives brainstorming and +exploring-ideas a live visual companion, which replaces quirk's Agent Isles integration. Users and +projects can add their own templates. + +Derived from two research documents: *Cross-Agent Skills for Interactive Single-File HTML +Artifacts* (dual-path builder, bundle and QA contracts, host-neutral delivery) and *Deep research on +libraries like Astryx, TanStack* (library landscape, routing policy, two-tier architecture). + +This spec covers three pieces of work that the user chose to keep in one spec: the single-file +builder (tier 1), the React builder (tier 2), and the companion. + +## Conceptual model + +Everything the skill produces comes from a **template**. Built-in pages, a project's branded +reports, the companion's screens and the React scaffold all go through the same pipeline, so a +custom template gets every guarantee a built-in one has. + +The units: + +- **Template.** A manifest plus fixed chrome plus one to three freeform slots. The manifest holds: + - a name and a "use when" description the agent matches requests against + - the tier (1 or 2) + - the libraries it needs + - token overrides for the house theme + - a rules addendum layered on the design reference + - a filled-in example, which is also its verify fixture + + Templates are built-in, user-level or project-level. +- **Library.** A vendored file set, pinned to a version, checksummed, and carrying its license. + v1 ships Simple.css, Alpine.js, Web Awesome, one chart library and Mermaid. A template can bring + more. +- **House theme.** One token set with light and dark pairs, plus the design reference that says how + to use it. Project templates override its tokens. +- **Artifact.** A built `.html` file. It may have a sidecar data file and, for tier 2, a source + folder. A header inside the file records the template, tier, mode and library versions. +- **Companion session.** A local server plus a directory of screens. Each screen is an artifact + built from the `companion` template. +- **Verify report.** The result of running the checks on the final file, with screenshot paths. + +Slots are deliberately coarse. The chrome is fixed (theme, toggle, license notices, header) and the +agent writes freely inside the slots. Consistency comes from the shared chrome and tokens, and +quality comes from verifying the output rather than from a rigid slot grammar. + +## Data flow + +A request comes in and the agent matches it against template descriptions, looking in the project +first, then the user's templates, then the built-ins. It states which template it picked and which +tier, and why. + +The agent fills the slots by following the design reference and the template's rules addendum. It +also declares the artifact's interactions and its mode, so the later checks know what to expect. + +The build turns template plus slot content into the deliverable. Tier 1 assembles chrome, slots +and tokens, inlines the libraries, and adds license notices and the header. Tier 2 copies the +React scaffold into a source folder, installs from the template's frozen lockfile, and bundles to +one file. Either way the output lands in `.quirk/html/`. + +Static checks run on the built file, then browser verify runs on that same file if a browser is +reachable. Because the report must come from the final file, any edit after verify means verify +runs again. + +The agent then delivers a note with the path, the facts about the build and the verify result. It +offers to open or publish the file and does neither on its own. + +For the companion, brainstorming starts the local server, writes one screen per question into the +screen directory, and waits. The user's clicks travel back as an event stream. Proceed on the +current screen ends the wait and hands the agent the selection. + +## Key decisions and rationale + +- **One template engine for everything.** The user asked for custom templates that can be either + tier and can add libraries. A token-overlay design can't express those, and research showed + overlays break silently on upgrades: Shoelace's move to Web Awesome renamed every `--sl-*` token + to `--wa-*` and inverted the color scale. Making the companion and the React scaffold templates + too means project branding reaches them for free. +- **Coarse slots with freeform content.** A practitioner who shipped 21 rigid slot templates called + the slot grammar "the bottleneck" and moved to one freeform body inside shared chrome. +- **Verification over instruction.** Of 101 "tests pass" claims one developer audited, 35 were + false, and 34 of those were stale: the check ran before the last edit. Verify therefore runs on + the final file and the note points at its output. +- **Pinned, vendored, checksummed libraries.** An unpinned dependency in Anthropic's own artifact + builder resolved to a new major version. The build exited 0 and the page threw in the browser. + Vendoring also lets tier 1 build with no network. +- **Simple.css instead of Pico CSS.** Pico is archived; its last release was v2.1.1 in March 2025. + Simple.css is classless, MIT-licensed, maintained (last pushed July 2026), built on CSS variables + and has automatic dark mode, so house tokens can override it. Open Props was rejected because it + is a token library, not a classless stylesheet. +- **A stdlib Python server for the companion.** A page opened from `file://` can't send anything + back, so the companion needs a process that outlives the agent's tool call. Python stdlib keeps + Node out of the brainstorming path and matches quirk's existing `bin/` tooling. +- **The spec is the record.** Companion screens and events are transient. Decisions live in the + spec that brainstorming writes, as they did with Isles. +- **Quirk builds; publishing is separate.** The skill writes a local file. Claude's own artifact + skills and the Artifact tool apply only if the user later chooses to publish it. + +## Behavior + +### Triggering + +- The skill runs when the user asks for HTML, a page, a report as a page, or an interactive tool. +- When an answer "has shape" (a comparison, a dashboard, a flow, a table someone will explore), the + agent offers a page in one line and builds it only on a yes. + +### Picking a template and a tier + +1. The agent matches the request against template descriptions in lookup order: project + `.quirk/html-templates/`, then user `~/.quirk/html-templates/`, then built-in. A name that + exists at two levels resolves to the higher one. The user can name a template directly. +2. Tier 2 is chosen when any of these signals holds: + - the artifact has multiple views or routes + - it has an editable data grid + - state is shared across views + - the user asks for an app or a project + + The agent may also escalate on its own judgment, and must then say why. Otherwise the artifact + is tier 1. +3. The agent states the template, its source level and the tier before building. + +### Building + +- **Tier 1** writes `.quirk/html/ .html`. The file contains the chrome, slot content, tokens, + inlined libraries, one license notice per library, and the header. +- **Tier 2** writes the source project to `.quirk/html/ /` and the bundle to + `.quirk/html/ .html`. It installs from the template's lockfile with a frozen install. The + first build needs network for that install; the output never does. +- **Tier 2 failure.** If Node is missing or the install or build fails, the agent builds a tier-1 + version instead. The delivery note lists every feature that was cut. +- **Editing an existing artifact** rebuilds it with the library versions recorded in its header, + unless the user asks to upgrade. +- `.quirk/html/` is gitignored. + +### Modes + +- **Offline** (default): the file makes zero network requests. Fonts come from the system stack. +- **Connected**: chosen only when the artifact must call a real API, and labelled as connected in + the delivery note. Libraries are still inlined; only the page's own data calls go to the network. +- There is no CDN mode. + +### Static checks + +These run on every build, companion screens included: + +- in offline mode, no external references (scripts, stylesheets, fonts, images, fetches) +- size: a warning above 2 MB, a failure above 10 MB +- every inlined library matches its checksum, and its license notice is present +- the header is present and complete + +When embedded data would push the file over budget, the data moves to a sidecar `.json` or `.csv` +next to the page. The page loads it through a file picker and says so on screen. + +### Browser verify + +When a browser is reachable, one quirk verify script runs these checks on the final file: + +- no console errors +- screenshots at 375px wide and at desktop width +- light and dark themes both legible +- zero network requests (offline mode only; connected mode reports the requests it saw) + +When no browser is reachable, the delivery note says "not browser-verified" and lists the static +checks that did run. Verify never installs a browser without asking. + +### Delivery + +The agent's note lists: + +- the path and size, plus the sidecar and source folder if any +- the template and where it came from +- the tier and the signal (or judgment) that chose it +- the mode +- the libraries and their versions +- the verify result and screenshot paths + +It ends with a one-line offer to open or publish the file. Neither happens automatically. + +### Templates + +- **Built-ins:** + - `report`: Simple.css + Alpine, for reports, explainers, forms and small tools + - `components`: Web Awesome + Alpine, for component-heavy pages (dialogs, tabs, comboboxes) + - `companion`: the brainstorming screen shell + - `app`: tier 2, React with Vite single-file bundling, Astryx and TanStack, with its own lockfile + + Any template can request the chart library or Mermaid. +- **Custom templates** can do everything a built-in does: either tier, token overrides, a rules + addendum, extra libraries, and an example. A tier-2 custom template ships its own lockfile. +- **Project tokens apply to the companion.** A project template's token overrides reach the + companion shell too, so brainstorming screens look like the project. +- **Creating a template** is a guided flow. The user starts from scratch or from an existing + artifact. The flow writes the manifest, skeleton and example, then runs verify on the example. + The template is usable only once its example passes. +- **An invalid template** (bad manifest, failed checksum, missing example) is skipped with a + visible warning, and the next match in lookup order is used. + +### Companion + +- Brainstorming and exploring-ideas start the server. It prints its URL and directories as JSON, + runs in the background, and exits after an idle timeout. +- For each question, the agent writes the question text, the options, and any mockup or diagram + into the `companion` template. The server always serves the newest screen. Screens have 2 to 4 + options and never reuse a filename. +- The shell provides single- and multi-select choice sets, a Proceed button, the theme toggle, the + project's tokens, and a "superseded" banner on stale tabs. +- Every click is recorded in a full event stream, tagged with the nonce of the screen it came from. + Clicks from a screen that is no longer current are rejected. +- `wait` blocks until Proceed is clicked on the current screen, then returns the selection. If the + host kills the wait, the agent re-runs it and no click is lost. With no server running, `wait` + fails with a distinct exit code. +- The agent reads the stream and the Proceed payload. Anything the user types in the terminal takes + priority over clicks. +- Screens and events live under `.quirk/brainstorm/`, which is gitignored. + +### Isles removal + +- Removed from quirk: `bin/agent_isles.py`, `packs/quirk/`, `examples/agent-isles/`, + `docs/agent-isles-artifacts.md`, and the three Isles test files. +- Rewritten: both `visual-companion.md` guides and the companion offers in the brainstorming and + exploring-ideas skills, which currently name Agent Isles and Node. +- The separately installed agent-isles plugin is left alone. Quirk just stops referencing it. +- The README skill count goes from 25 to 26. + +### Hosts + +- v1 supports Claude Code, pi and Oh My Pi. +- Scripts are found relative to the skill's own directory, so the skill works wherever it is + installed or symlinked. +- Build, the server and static checks need only Python stdlib. Browser verify and tier 2 need Node. + +### Scenarios + +1. **A report from data.** The user asks for a page summarizing test-run timings. The agent picks + `report` (built-in), tier 1, offline, and adds the chart library. It builds + `.quirk/html/test-timings.html` at 410 KB, verify passes at both widths and both themes, and the + note offers to open it. +2. **A branded project report.** The project has `.quirk/html-templates/report/`. The same request + resolves to the project's `report`, the note says "project template", and the page uses the + project's tokens and rules addendum. +3. **An app.** The user asks for a tool to edit a list of feature flags and preview them in two + views. Two signals fire (editable grid, multiple views), so the agent picks `app`, tier 2. The + source lands in `.quirk/html/flag-editor/` and the bundle in `.quirk/html/flag-editor.html`. +4. **Tier 2 without Node.** Same request on a machine without Node. The agent builds a tier-1 + version with one view and a read-only table, and the note lists both cuts. +5. **Too much data.** The data would make a 14 MB file. It moves to `flag-data.json` next to the + page, the page shows a file picker, and the note names both files. +6. **No browser.** Verify can't find a browser. The note says "not browser-verified" and lists the + static checks that passed. +7. **A broken custom template.** A user template's library checksum doesn't match. The agent warns, + skips it, and uses the built-in of the same name, and the note says so. +8. **A companion session.** Brainstorming starts the server and shows a screen with three layout + options in the project's colors. The user clicks two, changes their mind, clicks Proceed. The + agent gets the full click stream and the final selection. A click on an older tab is rejected + and that tab shows the "superseded" banner. +9. **A killed wait.** The host times out the agent's `wait` mid-question. The agent re-runs it, the + user's earlier clicks are still in the stream, and Proceed returns normally. +10. **Editing an old artifact.** A file built against an older Web Awesome is edited. It rebuilds + with the versions in its header, not the current pins. + +## Scope and non-goals + +**Success criteria:** + +- every built-in template has a worked example that passes verify, and there is a tier-2 example +- trigger evals: about 10 prompts that should fire the skill and about 10 near-misses that should + not +- a brainstorming session runs end to end on the new companion in Claude Code and in pi or OMP +- the server, builder and static checks are covered by pytest, which runs in CI + +**Non-goals for v1:** + +- Codex, Gemini CLI and Copilot CLI +- handoffs into other skills (code review, finishing a branch, the pm dashboard) +- changes to the writing-specs interactive app +- automatic opening or publishing +- a CDN mode +- webfonts +- browser verify in CI +- automated pin-bump PRs +- keyboard, axe and per-interaction smoke checks in verify +- archiving companion screens as an audit trail + +## Decisions Locked + +**Scope and shape** + +- Two tiers: inlined single-file HTML, and a React project bundled to one file. +- Curated, inlined library stack rather than per-artifact choices. +- Replace Agent Isles as quirk's companion. +- Skill name `writing-html-artifacts`. +- All three pieces (tier 1, tier 2, companion) in one spec. + +**Tier escalation** + +- Named signal list plus agent judgment; the agent states the signal or the reason. +- Tier 2 leaves the source project and the bundle. +- Tier 2 installs from a shipped lockfile with a frozen install. +- Tier 2 failure falls back to tier 1 and lists what was cut. + +**Offline semantics** + +- Offline by default, opt-in connected mode, no CDN mode. +- System font stack only. +- Warn above 2 MB, fail above 10 MB. +- Over-budget data goes to a sidecar loaded through a file picker. + +**Companion protocol** + +- Quirk's own stdlib Python server. +- Content goes into a fixed shell with freeform regions. +- The agent reads the full click stream plus the Proceed payload. +- A killed wait is re-entered and loses no clicks. +- Stale clicks are rejected and old tabs show a "superseded" banner. +- Isles code is removed in the same change. +- The spec is the record; screens and events are transient. +- The external agent-isles plugin is left alone. + +**Verification** + +- Browser QA is required when a browser is reachable; otherwise the note says "not + browser-verified". +- One quirk verify script for every host. +- Checks: console, 375px and desktop viewports, light and dark, network. +- Done means verify ran on the final file, with report and screenshot paths in the note. +- The companion shell is tested in pytest; screens get static checks only. + +**Design quality** + +- Quirk ships its own full design reference, informed by web and Reddit research. +- One house theme shared by artifacts and the companion. +- Light and dark both required, following the OS, with a toggle. + +**Libraries and pins** + +- v1 libraries: a classless stylesheet with Alpine, Web Awesome with Alpine, a chart library, + Mermaid. +- Pico CSS is replaced by a maintained classless library; the design names Simple.css. +- Libraries are vendored with checksums. +- Pins are bumped by hand, and a check flags outdated pins or pins with advisories. +- Old artifacts keep their recorded versions unless the user asks to upgrade. +- Each inlined library carries a license notice. +- Python stdlib for build, server and static checks; Node for verify and tier 2. + +**Hosts and delivery** + +- Hosts: Claude Code, pi, Oh My Pi. +- Scripts resolve relative to the skill's directory. +- Output goes to `.quirk/html/ .html`, gitignored. +- Report the path and the delivery note, then offer to open or publish; never automatic. +- Trigger when HTML, a page or a tool is asked for; offer in one line when an answer has shape. +- No handoffs beyond the companion in v1. +- The writing-specs app is left as is. +- Quirk builds the file; Claude's artifact skills apply only if the user publishes. + +**Templates** + +- Contents: skeleton with slots, token overrides, rules addendum, extra libraries, example. +- Lookup: project `.quirk/html-templates/` (committed), then user `~/.quirk/html-templates/`, then + built-in. +- Custom templates can be either tier. +- Project tokens apply to the companion. +- The agent matches the request to descriptions and states its pick; the user can name one. +- Templates are created through a guided flow, from scratch or from an existing artifact. +- An invalid template is skipped with a warning and the next match is used. + +**Approach** + +- Approach A: one template engine with coarse slots, borrowing freeform regions and + verify-over-rules from the agent-assembled approach. + +## Industry Insights + +Research ran in four phases: context research, adhd gray-area discovery, a deep dive on the design +reference, and validation of the three approaches. The Brave and reddit-research skills were +unavailable, so agents used WebSearch and WebFetch, and Reddit came from Arctic Shift. + +**Templates and slots** + +- Rigid slot grammars became "the bottleneck" for a 21-template HTML skill; one freeform body over + shared chrome kept pages consistent. + [Muninn](https://muninn.austegard.com/blog/the-unreasonable-effectiveness-of-html-and-the-skill-that-tries-not-to-ruin-it.html) +- dogum/html-artifacts v2.0.0 dropped HTML teaching in favor of judgment, headless-render checks + and evals against a no-skill baseline. [dogum/html-artifacts](https://github.com/dogum/html-artifacts) +- Template precedence needs one explicit rule, and collisions need qualified names. + [Hugo lookup order](https://gohugo.io/templates/lookup-order/), + [shadcn namespaces](https://ui.shadcn.com/docs/registry/namespace) +- Templates sprawl and rot unless versioned and tested; Backstage's official templates are + unversioned and Copier updates pass drift through unmarked. + [backstage/software-templates](https://github.com/backstage/software-templates), + [Red Hat](https://developers.redhat.com/articles/2025/03/17/10-tips-better-backstage-software-templates), + [fastmcp-server-template #653](https://github.com/pvliesdonk/fastmcp-server-template/issues/653) +- Manifests with descriptions and typed kinds keep growing template sets findable. + [shadcn registry-item.json](https://ui.shadcn.com/docs/registry/registry-item-json) + +**Theming** + +- Token overrides stop at component internals; Web Awesome needs component properties or + `::part()` for shadow DOM. [Web Awesome theming](https://webawesome.com/docs/theming-overview/) +- Upgrades break silently through renames: Shoelace to Web Awesome renamed `--sl-*` to `--wa-*` and + inverted the color scale. + [migration guide](https://webawesome.com/docs/resources/migrating-from-shoelace/) +- Light and dark pairs are usually hand-duplicated; Pico requires dark variables twice. The DTCG + 2025.10 resolver models themes as contexts. + [Pico CSS variables](https://picocss.com/docs/css-variables), + [DTCG resolver](https://www.designtokens.org/tr/2025.10/resolver/) +- Dark themes fail contrast near the threshold (4.43:1 against 4.5:1), so each theme is checked + separately. [gh-aw-cao #9249](https://github.com/githubnext/gh-aw-cao/issues/9249) + +**Building and dependencies** + +- An unpinned `react-resizable-panels` resolved to v4 in Anthropic's artifact builder; the build + exited 0 and the browser threw React #130. + [anthropics/skills #1893](https://github.com/anthropics/skills/issues/1893) +- vite-plugin-singlefile is the simplest single-file route; it doesn't inline `public/` files or + SVG. [vite-plugin-singlefile](https://github.com/richardtallent/vite-plugin-singlefile) +- Hidden CDN and font dependencies break offline pages. + [livepage #161](https://github.com/glennfaison/livepage/issues/161) +- Inlined widgets add up fast: ten self-contained widgets totaled 4.1 MB. + [bundle plan](https://glama.ai/mcp/servers/@mikechao/brave-search-mcp/blob/9db552a665dc2c1e7b81f16dd87301daddd1c88f/docs/reduce-bundle-plan.md) +- Anthropic's web-artifacts-builder is a heavy React/shadcn stack "not for simple single-file + artifacts", and defers testing. + [web-artifacts-builder](https://github.com/anthropics/skills/blob/main/skills/web-artifacts-builder/SKILL.md) + +**Verification** + +- In an audit of 101 "tests pass" claims, 35 were false and 34 of those were stale. + [dev.to audit](https://dev.to/vinzenz_eiberger/i-checked-101-tests-pass-claims-from-my-ai-coding-agents-35-werent-true-h6n) +- dogum verifies at 1280x800 in light and dark and at 400x800, checks the console, and doesn't + install a browser without asking. + [verify-before-delivering.md](https://raw.githubusercontent.com/dogum/html-artifacts/main/skills/html-artifacts/references/verify-before-delivering.md) +- Agent-written browser tests can pass by accident when they only confirm page load. + [Currents](https://currents.dev/posts/state-of-playwright-ai-ecosystem-in-2026) + +**Design reference** + +- Models converge on a default look ("distributional convergence": Inter, purple gradients); + Anthropic names Space Grotesk as the next default to avoid. + [Anthropic cookbook](https://platform.claude.com/cookbook/coding-prompting-for-frontend-aesthetics), + [Anthropic blog](https://claude.com/blog/improving-frontend-design-through-skills) +- dogum's banned list (gray card grids, gradient heroes, emoji headers, glassmorphism, centered + everything) with "rebuild at three or more" is the most usable artifact-specific list. + [matching-your-style.md](https://raw.githubusercontent.com/dogum/html-artifacts/main/skills/html-artifacts/references/matching-your-style.md) +- frontend-design gives the most concrete rules: a 4 to 6 color plan, one or two families, measure + under 80 characters, named device bans. + [frontend-design](https://raw.githubusercontent.com/anthropics/skills/main/skills/frontend-design/SKILL.md) +- Recurring defaults: body 16 to 18px, line-height 1.5 to 1.6, measure 60 to 75ch, one type ratio + (1.25 or 1.333), 4px spacing base, 4.5:1 text and 3:1 UI contrast, 2px focus outline with 2px + offset, a named radius scale, 5 to 8 categorical chart series (Okabe-Ito). + [accessibility-and-print.md](https://raw.githubusercontent.com/dogum/html-artifacts/main/skills/html-artifacts/references/accessibility-and-print.md), + [noqta](https://noqta.tn/en/blog/ai-design-slop-overused-ui-patterns-fix-2026) +- DESIGN.md (Google Labs, alpha) pairs YAML tokens with prose and lints contrast, but doesn't model + light and dark. [design.md](https://github.com/google-labs-code/design.md) +- The superpowers brainstorming companion has a ready vocabulary (options, cards, mockup, split, + pros-cons) and the rule of 2 to 4 options per screen. + [visual-companion.md](https://raw.githubusercontent.com/obra/superpowers/main/skills/brainstorming/visual-companion.md) +- Reddit: people spot "AI purple" and "Claude sad beige", but clarity and usability matter more + than avoiding the look. [r/webdev](https://www.reddit.com/r/webdev/comments/1trfxp1/), + [r/ClaudeAI](https://www.reddit.com/r/ClaudeAI/comments/1x22sap/), + [r/UI_Design](https://www.reddit.com/r/UI_Design/comments/1wnh9z5/) +- No source measures concrete rules against vague taste guidance; the reference ships numbers and + checkable rules, and evals test them. + [arXiv 2505.17037](https://arxiv.org/html/2505.17037v1) + +**Skill packaging** + +- Only `name` and `description` frontmatter is portable across hosts. + [Agent Skills spec](https://agentskills.io/specification) +- Descriptions are the only trigger signal; test about 20 queries, half should-fire and half + near-miss. [agentskills.io](https://agentskills.io/skill-creation/optimizing-descriptions) + +## Deferred Ideas + +Out of scope for v1: + +- handoffs from code review, finishing a branch and the pm dashboard into a page +- browser verify in CI +- Codex, Gemini CLI and Copilot CLI support +- automated pin-bump PRs +- keyboard, axe and per-interaction smoke checks in verify +- a pinned-CDN mode and inlined webfonts +- Preact + HTM and Bulma as libraries +- archiving companion screens alongside the spec + +Open for the tech spec: + +- `[tech-spec]` Which chart library ships. +- `[tech-spec]` How the companion keeps Mermaid (over 1 MB inlined) from bloating every screen. +- `[tech-spec]` The manifest schema, its version field, and how slots are marked in a skeleton. +- `[tech-spec]` The header's format and how an edit reads versions back from it. +- `[tech-spec]` How verify finds a browser, how it runs Playwright, and where screenshots go. +- `[tech-spec]` How the pin check finds outdated pins and advisories. +- `[tech-spec]` Companion details carried from Isles: idle timeout, `wait` chunk length, the + no-server exit code, and the event record shape. +- `[tech-spec]` How "create from an existing artifact" extracts a skeleton and tokens. +- `[tech-spec]` How trigger evals are run and scored. +- `[tech-spec]` The design reference's section list (the research proposed 14 sections). + +## Glossary + +- **Artifact**: a built `.html` file the skill delivers, with its header, plus any sidecar or + source folder. +- **Chrome**: the fixed part of a template (theme, toggle, notices, header) that the agent doesn't + write. +- **Companion**: the live page brainstorming and exploring-ideas show in a browser to ask visual + questions. +- **Connected mode**: an artifact allowed to call a real API at runtime. Libraries are still + inlined. +- **Header**: a comment block inside the artifact recording template, tier, mode and library + versions. +- **House theme**: quirk's default token set, with light and dark pairs. +- **Library**: a vendored, pinned, checksummed file set with its license, such as Alpine or + Simple.css. +- **Nonce**: a per-screen token that ties each click to the screen it came from. +- **Offline mode**: the default; the artifact makes zero network requests. +- **Proceed payload**: what `wait` returns when the user clicks Proceed: the current selection. +- **Sidecar**: a data file shipped next to an artifact and loaded through a file picker. +- **Slot**: a freeform region of a template the agent fills. +- **Template**: a manifest, chrome, slots, tokens, rules addendum, libraries and example. Built-in, + user or project. +- **Tier 1**: a single file assembled from a template with inlined libraries; no build tool. +- **Tier 2**: a React project bundled to one file, with the source kept. +- **Tokens**: CSS custom properties for color, type, spacing and radius. +- **Verify**: the browser checks run on the final file. + +## Status & amendments + +**Status**: Approved by the user, 2026-10-13. Handed to subagent-driven-development. + +**Amendments** + +None yet. diff --git a/docs/quirk/specs/2026-10-13-writing-html-artifacts/tech.md b/docs/quirk/specs/2026-10-13-writing-html-artifacts/tech.md new file mode 100644 index 00000000..9cb5c17e --- /dev/null +++ b/docs/quirk/specs/2026-10-13-writing-html-artifacts/tech.md @@ -0,0 +1,625 @@ +# writing-html-artifacts: tech spec + +**Status**: Approved by the user, 2026-10-13; amended 2026-10-13 (see end) · Logic spec: [logic.md](logic.md) + +This document says where to work and what must hold. Paths are relative to the repo root. `$SKILL` +means `skills/writing-html-artifacts/`. Line anchors are against `ac902bd`. + +Every script is Python 3.9+ stdlib (`pyproject.toml:4`) except the Node pieces named in §1. Python +files open with `from __future__ import annotations`, like every in-skill script today. + +## 1. Files and ownership + +Back-link: [Scope and non-goals](logic.md#scope-and-non-goals). + +| Area | Files | Change | +|---|---|---| +| Skill entry | `$SKILL/SKILL.md` | new: workflow, delivery note, links to references | +| References | `$SKILL/references/design-reference.md`, `templates.md`, `companion.md` | new | +| Builder CLI | `$SKILL/scripts/quirk_html.py` | new: `build`, `build-app`, `check`, `verify`, `note`, `templates` subcommands | +| Builder library | `$SKILL/scripts/htmlkit/` (`__init__.py`, `manifest.py`, `resolve.py`, `tokens.py`, `assemble.py`, `checks.py`, `header.py`, `vendor.py`, `app.py`, `fsutil.py`) | new | +| Companion | `$SKILL/scripts/companion.py` | new: `start`, `show`, `wait`, `events`, `stop` | +| Pins | `$SKILL/scripts/pins.py` | new: `verify` (offline), `check` (network) | +| Verify (Node) | `$SKILL/scripts/verify/{package.json,pnpm-lock.yaml,verify.mjs}` | new | +| Vendor build (Node, maintainer only) | `$SKILL/scripts/vendor-build/{package.json,pnpm-lock.yaml,build-webawesome.mjs}` | new | +| Vendored libraries | `$SKILL/vendor/libraries.json`, `$SKILL/vendor/ / /…` | new | +| House theme | `$SKILL/theme/{house.tokens.json,base.css,chrome.js}` | new | +| Built-in templates | `$SKILL/templates/{report,components,companion,app}/` | new | +| Evals | `$SKILL/evals/trigger-evals.json` | new | +| Brainstorming | `skills/brainstorming/visual-companion.md` (rewrite; it must say terminal input outranks clicks), `skills/brainstorming/SKILL.md:357-374` (offer text) | modify | +| Exploring ideas | `skills/exploring-ideas/visual-companion.md` (rewrite; same terminal-priority rule), `skills/exploring-ideas/SKILL.md:178,188-196`, `skills/exploring-ideas/references/interaction-model.md:110` | modify | +| Isles removal | `bin/agent_isles.py`, `packs/quirk/`, `examples/agent-isles/`, `docs/agent-isles-artifacts.md`, `tests/test_agent_isles_{bridge,wait,pack}.py` | delete | +| Repo docs | `README.md:9` (count 25→26), `README.md:62-71` (Isles section), `.gitignore:10,15` | modify | +| Tests | `tests/test_writing_html_artifacts_skill.py`, `tests/test_html_builder.py`, `tests/test_html_companion.py`, `tests/test_html_vendor.py`, `tests/test_html_verify.py`, `tests/test_isles_removed.py` | new | + +## 2. Architecture + +Back-link: [Data flow](logic.md#data-flow). + +- `quirk_html.py` and `companion.py` are thin CLIs. They put `Path(__file__).resolve().parent` on + `sys.path` (the filing-scripts pattern, `skills/filing-requests/scripts/template_resolve.py:18-23`) + and import `htmlkit`. Every subcommand writes exactly one JSON object to stdout, the same shape + convention as `skills/adversarial-review/SKILL.md:65-68`. Diagnostics go to stderr. +- `htmlkit` owns all logic so tests import it directly. Module responsibilities: + - `manifest.py`: parse and validate `template.json` and `libraries.json` + - `resolve.py`: template lookup across levels, invalid-template skipping + - `tokens.py`: merge token layers, emit the `:root` CSS + - `assemble.py`: tier-1 build (source file → HTML) + - `checks.py`: static checks over a built file + - `header.py`: write and read the artifact header and license block + - `vendor.py`: locate a library version, verify checksums, produce inline blocks + - `app.py`: tier-2 scaffold, install, build, post-process + - `fsutil.py`: atomic writes and self-ignoring directories +- `companion.py` builds screens with `htmlkit.assemble` against the `companion` template, then runs + an `http.server.ThreadingHTTPServer` in a detached process. +- Node runs only in `scripts/verify/` (Playwright), in tier-2 builds (`pnpm`), and in + `scripts/vendor-build/` (maintainer-time esbuild). Python never imports from them; it shells out. +- Agents locate scripts relative to the skill directory. SKILL.md and both companion guides say: + `SKILL_DIR` is the directory holding this skill's SKILL.md, which the host shows when the skill + loads. The brainstorming and exploring-ideas guides reach the companion as + ` /../writing-html-artifacts/scripts/companion.py`. No `$CLAUDE_PLUGIN_ROOT` in + any new or rewritten text. + +## 3. Data models + +### 3.1 Template manifest: `template.json` + +Back-link: [Templates](logic.md#templates). + +SCHEMA: + +``` +{ + "schema": 1, // int; only 1 is valid + "name": "report", // must equal the template directory name; [a-z0-9-]{1,40} + "description": "Use when ...", // 30..400 chars, starts with "Use when" + "tier": 1, // 1 | 2 + "libraries": ["simple-css", "alpine"], // tier 1: names in libraries.json, or in this template's vendor/ + "optional_libraries": ["chartjs", "mermaid"], // tier 1: a source file may request these + "slots": { "main": {"required": true, "description": "..."} }, // tier 1: 1..3 entries + "skeleton": "skeleton.html", // tier 1, relative path + "tokens": "tokens.json", // optional, relative path (§3.3 shape) + "rules": "rules.md", // optional design-rules addendum + "example": "example.src.html", // tier 1: a source file (§3.4); tier 2: a directory overlay + "project": "project/" // tier 2 only: Vite project with package.json + pnpm-lock.yaml +} +``` + +- A library name is resolved first in the template's own `vendor/libraries.json`, then in + `$SKILL/vendor/libraries.json`. A template-level entry with the same name as a built-in shadows it + for that template only. +- Custom templates (project or user level) also need `verified.json` (§3.6). Built-ins don't. + +### 3.2 Vendored libraries: `vendor/libraries.json` + +Back-link: [Conceptual model](logic.md#conceptual-model). + +SCHEMA: + +``` +{ + "schema": 1, + "libraries": { + " ": { + "current": " ", // version new builds use + "versions": { + " ": { + "package": " ", // used by pins.py check + "license": "MIT", // SPDX id + "license_file": "LICENSE", // relative to vendor/ / / + "source": " ", // where the files came from + "files": [ + {"path": "cdn.min.js", "kind": "js" | "css", + "sha256": " ", + "inline_sha256": " ", + "position": "head" | "body-end", + "inline": true} // false: served by the companion only, never inlined + ] + } + } + } + } +} +``` + +v1 contents (exact versions; the implementer records sha256 values from the downloaded files): + +| name | package@version | files | notes | +|---|---|---|---| +| `simple-css` | `simpledotcss@2.3.7` | `simple.min.css` | no manual dark toggle; `theme/base.css` adds one | +| `alpine` | `alpinejs@3.17.4` | `dist/cdn.min.js`, saved as `alpine.min.js`, position `body-end` | self-starts; must come after markup and after any Alpine plugin | +| `webawesome` | `@awesome.me/webawesome@3.14.0` | `webawesome.bundle.js` (IIFE), `webawesome.css` | produced by `scripts/vendor-build/build-webawesome.mjs`; free components only | +| `chartjs` | `chart.js@4.5.1` | `dist/chart.umd.js`, saved as `chart.umd.js` | already minified | +| `mermaid` | `mermaid@12.2.0` | `dist/mermaid.min.js` (about 5.2 MB IIFE) | never use the `.mjs` build; it loads chunks | + +**Old versions are never deleted.** A pin bump adds a `versions` entry and moves `current`; the +previous directory stays so frozen artifacts rebuild. `pins.py verify` enforces that every listed +version's files exist and match. + +### 3.3 Tokens: `house.tokens.json` and template `tokens.json` + +Back-link: [Conceptual model](logic.md#conceptual-model), [Templates](logic.md#templates). + +SCHEMA: + +``` +{ + "schema": 1, + "color": { " ": [" ", " "] }, // roles: bg, surface, text, text-muted, border, + // accent, accent-text, good, warn, critical, focus + "font": { "sans": " ", "mono": " ", "display": " " }, + "size": { " ": " " }, // body, small, h1..h4 + "space": { " ": " " }, // 1..8 on a 4px base + "radius": { "sm": "4px", "md": "6px", "pill": "999px" } +} +``` + +- Every color is a light/dark pair. A single string is invalid. +- `tokens.py` merges layers in this order, later wins per key: house → user + `~/.quirk/html-templates/tokens.json` → project `.quirk/html-templates/tokens.json` → the + template's own `tokens` file. The two shared files are how project tokens reach built-in + templates, the companion included. +- Emitted CSS uses `--q- - ` names. It defines light values on `:root`, dark values under + `@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) {…} }`, and dark values + again under `:root[data-theme="dark"]`. +- `theme/base.css` maps `--q-*` onto Simple.css's variables (`--bg`, `--accent-bg`, `--text`, + `--text-light`, `--border`, `--accent`, `--accent-hover`, `--accent-text`, `--code`, + `--preformatted`, `--marked`, `--disabled`) and onto Web Awesome's brand and surface variables. + It must override Simple.css's light values explicitly too, because Simple.css's own dark media + block would otherwise win on a dark OS when the user picked light. +- System font stacks only. No `@font-face`, no `url()` fonts. + +### 3.4 Tier-1 source file: ` .src.html` + +Back-link: [Picking a template and a tier](logic.md#picking-a-template-and-a-tier), [Building](logic.md#building). + +The agent writes this file; the builder reads it. Format: + +- Line 1 is a header comment containing one JSON object: + `` + - `template` (required), `title` (required), `mode` (`offline` default | `connected`), + `libraries` (subset of the template's `optional_libraries`), `interactions` (list of strings, + may be empty), `data` (optional path to a JSON or CSV file, relative to the source file), + `tier_reason` (optional; set when the agent overrode the signal list). +- The rest is slot sections. Each starts with a line that is exactly ``. + Content runs to the next such line or end of file. Text before the first slot line, other than + the header, is an error. +- REGEX: `^$` (one per line, matched with `re.M`) + +### 3.5 Artifact header and license block + +Back-link: [Building](logic.md#building), [Delivery](logic.md#delivery). + +- The built file starts `` and the next line is the header: + ``. + +SCHEMA: + +``` +{ + "schema": 1, + "template": "report", "template_level": "builtin" | "user" | "project", + "tier": 1 | 2, "tier_signal": " '>" | null, + "mode": "offline" | "connected", + "libraries": { " ": " " }, + "sidecar": " " | null, + "built_at": " ", + "quirk_version": " " +} +``` + +- `--` inside any JSON string is written as `--` so the comment can't close early. +- Directly after the header, one comment per inlined library: + ``. +- Each inlined library is wrapped as `", "browser": true | false, "hashes": {" ": " "}}` +covering `template.json`, the skeleton, tokens, rules, every file under `vendor/`, and the example. +For tier 2 it covers `template.json` and every file under `project/` and `example/` except +`node_modules` and `dist`; for tier 1 it also covers the example's data file. A +missing file or a hash mismatch makes the template invalid with reason `unverified`. + +### 3.7 Companion screen spec: `screen.json` + +Back-link: [Companion](logic.md#companion). + +SCHEMA: + +``` +{ + "title": "Layout", // short; becomes the + "question": "Which layout fits the dashboard?", + "select": "single" | "multi", + "options": [ {"id": "two-col", "label": "Two column", "description": "...", "html": " "} ], // 2..4 + "content": " ", + "diagram": " " +} +``` + +- Every screen has 2 to 4 options, as the logic spec requires, with unique `id`s matching `[a-z0-9-]{1,40}`. A screen that only shows something still offers choices about it. +- `diagram` makes the screen reference the server-served Mermaid build (§4.4). + +### 3.8 Companion state directory + +Back-link: [Companion](logic.md#companion). + +` /` is under `.quirk/brainstorm/`, for brainstorming and exploring-ideas alike. + +| Path | Writer | Content | +|---|---|---| +| `screens/NNNN- .html` | `show` | the built screen; `NNNN` is a zero-padded counter, never reused | +| `state/current.json` | `show` | `{"screen": "NNNN- ", "nonce": " ", "shown_at": }` | +| `state/server-info` | `start` | the `server-started` JSON | +| `state/server-stopped` | server on exit, or a client that finds the recorded pid dead | `{"reason": "idle" | "stop" | "signal" | "stale", "at": }`; `stale` is written by a client only when the recorded pid is dead | +| `state/events` | server | JSONL, one record per accepted event | + +Event record SCHEMA: +`{"type": "click" | "proceed", "screen": "NNNN- ", "nonce": " ", "choice": " " | null, "text": "