diff --git a/Dockerfile b/Dockerfile index 3703823326..0d3da72eb7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,26 +21,36 @@ RUN useradd -u 10000 -m -d /opt/data hermes COPY --chmod=0755 --from=gosu_source /gosu /usr/local/bin/ COPY --chmod=0755 --from=uv_source /usr/local/bin/uv /usr/local/bin/uvx /usr/local/bin/ -COPY . /opt/hermes WORKDIR /opt/hermes -# Install Node dependencies and Playwright as root (--with-deps needs apt) +# ---------- Layer-cached dependency install ---------- +# Copy only package manifests first so npm install + Playwright are cached +# unless the lockfiles themselves change. +COPY package.json package-lock.json ./ +COPY scripts/whatsapp-bridge/package.json scripts/whatsapp-bridge/package-lock.json scripts/whatsapp-bridge/ +COPY web/package.json web/package-lock.json web/ + RUN npm install --prefer-offline --no-audit && \ npx playwright install --with-deps chromium --only-shell && \ - cd /opt/hermes/scripts/whatsapp-bridge && \ - npm install --prefer-offline --no-audit && \ + (cd scripts/whatsapp-bridge && npm install --prefer-offline --no-audit) && \ + (cd web && npm install --prefer-offline --no-audit) && \ npm cache clean --force -# Hand ownership to hermes user, then install Python deps in a virtualenv -RUN chown -R hermes:hermes /opt/hermes -USER hermes +# ---------- Source code ---------- +# .dockerignore excludes node_modules, so the installs above survive. +COPY --chown=hermes:hermes . . +# Build web dashboard (Vite outputs to hermes_cli/web_dist/) +RUN cd web && npm run build + +# ---------- Python virtualenv ---------- +RUN chown hermes:hermes /opt/hermes +USER hermes RUN uv venv && \ uv pip install --no-cache-dir -e ".[all]" -USER root -RUN chmod +x /opt/hermes/docker/entrypoint.sh - +# ---------- Runtime ---------- +ENV HERMES_WEB_DIST=/opt/hermes/hermes_cli/web_dist ENV HERMES_HOME=/opt/data VOLUME [ "/opt/data" ] ENTRYPOINT [ "/opt/hermes/docker/entrypoint.sh" ] diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index 568d610922..126f4615dd 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -99,11 +99,48 @@ _FIXED_TEMPERATURE_MODELS: Dict[str, float] = { "kimi-for-coding": 0.6, } +# Moonshot's kimi-for-coding endpoint (api.kimi.com/coding) documents: +# "k2.5 model will use a fixed value 1.0, non-thinking mode will use a fixed +# value 0.6. Any other value will result in an error." The same lock applies +# to the other k2.* models served on that endpoint. Enumerated explicitly so +# non-coding siblings like `kimi-k2-instruct` (variable temperature, served on +# the standard chat API and third parties) are NOT clamped. +# Source: https://platform.kimi.ai/docs/guide/kimi-k2-5-quickstart +_KIMI_INSTANT_MODELS: frozenset = frozenset({ + "kimi-k2.5", + "kimi-k2-turbo-preview", + "kimi-k2-0905-preview", +}) +_KIMI_THINKING_MODELS: frozenset = frozenset({ + "kimi-k2-thinking", + "kimi-k2-thinking-turbo", +}) + def _fixed_temperature_for_model(model: Optional[str]) -> Optional[float]: - """Return a required temperature override for models with strict contracts.""" + """Return a required temperature override for models with strict contracts. + + Moonshot's kimi-for-coding endpoint rejects any non-approved temperature on + the k2.5 family. Non-thinking variants require exactly 0.6; thinking + variants require 1.0. An optional ``vendor/`` prefix (e.g. + ``moonshotai/kimi-k2.5``) is tolerated for aggregator routings. + + Returns ``None`` for every other model, including ``kimi-k2-instruct*`` + which is the separate non-coding K2 family with variable temperature. + """ normalized = (model or "").strip().lower() - return _FIXED_TEMPERATURE_MODELS.get(normalized) + fixed = _FIXED_TEMPERATURE_MODELS.get(normalized) + if fixed is not None: + logger.debug("Forcing temperature=%s for model %r (fixed map)", fixed, model) + return fixed + bare = normalized.rsplit("/", 1)[-1] + if bare in _KIMI_THINKING_MODELS: + logger.debug("Forcing temperature=1.0 for kimi thinking model %r", model) + return 1.0 + if bare in _KIMI_INSTANT_MODELS: + logger.debug("Forcing temperature=0.6 for kimi instant model %r", model) + return 0.6 + return None # Default auxiliary models for direct API-key providers (cheap/fast for side tasks) _API_KEY_PROVIDER_AUX_MODELS: Dict[str, str] = { diff --git a/agent/context_compressor.py b/agent/context_compressor.py index 34ec5091b1..ae8c2c0bd3 100644 --- a/agent/context_compressor.py +++ b/agent/context_compressor.py @@ -63,6 +63,52 @@ _CHARS_PER_TOKEN = 4 _SUMMARY_FAILURE_COOLDOWN_SECONDS = 600 +def _truncate_tool_call_args_json(args: str, head_chars: int = 200) -> str: + """Shrink long string values inside a tool-call arguments JSON blob while + preserving JSON validity. + + The ``function.arguments`` field on a tool call is a JSON-encoded string + passed through to the LLM provider; downstream providers strictly + validate it and return a non-retryable 400 when it is not well-formed. + An earlier implementation sliced the raw JSON at a fixed byte offset and + appended ``...[truncated]`` — which routinely produced strings like:: + + {"path": "/foo/bar", "content": "# long markdown + ...[truncated] + + i.e. an unterminated string and a missing closing brace. MiniMax, for + example, rejects this with ``invalid function arguments json string`` + and the session gets stuck re-sending the same broken history on every + turn. See issue #11762 for the observed loop. + + This helper parses the arguments, shrinks long string leaves inside the + parsed structure, and re-serialises. Non-string values (paths, ints, + booleans) are preserved intact. If the arguments are not valid JSON + to begin with — some model backends use non-JSON tool arguments — the + original string is returned unchanged rather than replaced with + something neither we nor the backend can parse. + """ + try: + parsed = json.loads(args) + except (ValueError, TypeError): + return args + + def _shrink(obj: Any) -> Any: + if isinstance(obj, str): + if len(obj) > head_chars: + return obj[:head_chars] + "...[truncated]" + return obj + if isinstance(obj, dict): + return {k: _shrink(v) for k, v in obj.items()} + if isinstance(obj, list): + return [_shrink(v) for v in obj] + return obj + + shrunken = _shrink(parsed) + # ensure_ascii=False preserves CJK/emoji instead of bloating with \uXXXX + return json.dumps(shrunken, ensure_ascii=False) + + def _summarize_tool_result(tool_name: str, tool_args: str, tool_content: str) -> str: """Create an informative 1-line summary of a tool call + result. @@ -449,6 +495,11 @@ class ContextCompressor(ContextEngine): # Pass 3: Truncate large tool_call arguments in assistant messages # outside the protected tail. write_file with 50KB content, for # example, survives pruning entirely without this. + # + # The shrinking is done inside the parsed JSON structure so the + # result remains valid JSON — otherwise downstream providers 400 + # on every subsequent turn until the broken call falls out of + # the window. See ``_truncate_tool_call_args_json`` docstring. for i in range(prune_boundary): msg = result[i] if msg.get("role") != "assistant" or not msg.get("tool_calls"): @@ -459,8 +510,10 @@ class ContextCompressor(ContextEngine): if isinstance(tc, dict): args = tc.get("function", {}).get("arguments", "") if len(args) > 500: - tc = {**tc, "function": {**tc["function"], "arguments": args[:200] + "...[truncated]"}} - modified = True + new_args = _truncate_tool_call_args_json(args) + if new_args != args: + tc = {**tc, "function": {**tc["function"], "arguments": new_args}} + modified = True new_tcs.append(tc) if modified: result[i] = {**msg, "tool_calls": new_tcs} diff --git a/hermes_cli/main.py b/hermes_cli/main.py index a13a6f88ee..ce02c2e72c 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -6229,8 +6229,9 @@ def cmd_dashboard(args): print(f"Install them with: {sys.executable} -m pip install 'fastapi' 'uvicorn[standard]'") sys.exit(1) - if not _build_web_ui(PROJECT_ROOT / "web", fatal=True): - sys.exit(1) + if "HERMES_WEB_DIST" not in os.environ: + if not _build_web_ui(PROJECT_ROOT / "web", fatal=True): + sys.exit(1) from hermes_cli.web_server import start_server diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 0d0dc4a66b..110b81e4b5 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -59,7 +59,7 @@ except ImportError: f"Install with: {sys.executable} -m pip install 'fastapi' 'uvicorn[standard]'" ) -WEB_DIST = Path(__file__).parent / "web_dist" +WEB_DIST = Path(os.environ["HERMES_WEB_DIST"]) if "HERMES_WEB_DIST" in os.environ else Path(__file__).parent / "web_dist" _log = logging.getLogger(__name__) app = FastAPI(title="Hermes Agent", version=__version__) diff --git a/nix/checks.nix b/nix/checks.nix index ff8e7947c5..984016a4f4 100644 --- a/nix/checks.nix +++ b/nix/checks.nix @@ -37,7 +37,30 @@ json.dump(sorted(leaf_paths(DEFAULT_CONFIG)), sys.stdout, indent=2) in { packages.configKeys = configKeys; - checks = lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux { + checks = { + # Cross-platform evaluation — catches "not supported for interpreter" + # errors (e.g. sphinx dropping python311) without needing a darwin builder. + # Evaluation is pure and instant; it doesn't build anything. + cross-eval = let + targetSystems = builtins.filter + (s: inputs.self.packages ? ${s}) + [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin" ]; + tryEvalPkg = sys: + let pkg = inputs.self.packages.${sys}.default; + in builtins.tryEval (builtins.seq pkg.drvPath true); + results = map (sys: { inherit sys; result = tryEvalPkg sys; }) targetSystems; + failures = builtins.filter (r: !r.result.success) results; + failMsg = lib.concatMapStringsSep "\n" (r: " - ${r.sys}") failures; + in pkgs.runCommand "hermes-cross-eval" { } ( + if failures != [] then + builtins.throw "Package fails to evaluate on:\n${failMsg}" + else '' + echo "PASS: package evaluates on all ${toString (builtins.length targetSystems)} platforms" + mkdir -p $out + echo "ok" > $out/result + '' + ); + } // lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux { # Verify binaries exist and are executable package-contents = pkgs.runCommand "hermes-package-contents" { } '' set -e diff --git a/nix/devShell.nix b/nix/devShell.nix index db39c9d955..63edc59cf1 100644 --- a/nix/devShell.nix +++ b/nix/devShell.nix @@ -12,7 +12,7 @@ devShells.default = pkgs.mkShell { inputsFrom = packages; packages = with pkgs; [ - python311 uv nodejs_22 ripgrep git openssh ffmpeg + python312 uv nodejs_22 ripgrep git openssh ffmpeg ]; shellHook = let diff --git a/nix/nixosModules.nix b/nix/nixosModules.nix index 24a2a1b6dd..3f2709f814 100644 --- a/nix/nixosModules.nix +++ b/nix/nixosModules.nix @@ -148,15 +148,14 @@ su -s /bin/sh "$TARGET_USER" -c 'curl -LsSf https://astral.sh/uv/install.sh | sh' || true fi - # Python 3.11 venv — gives the agent a writable Python with pip. - # Uses uv to install Python 3.11 (Ubuntu 24.04 ships 3.12). + # Python 3.12 venv — gives the agent a writable Python with pip. # --seed includes pip/setuptools so bare `pip install` works. _UV_BIN="$TARGET_HOME/.local/bin/uv" if [ ! -d "$TARGET_HOME/.venv" ] && [ -x "$_UV_BIN" ]; then su -s /bin/sh "$TARGET_USER" -c " export PATH=\"\$HOME/.local/bin:\$PATH\" - uv python install 3.11 - uv venv --python 3.11 --seed \"\$HOME/.venv\" + uv python install 3.12 + uv venv --python 3.12 --seed \"\$HOME/.venv\" " || true fi diff --git a/nix/packages.nix b/nix/packages.nix index 968ad12fb7..912be7843b 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -18,6 +18,10 @@ filter = path: _type: !(pkgs.lib.hasInfix "/index-cache/" path); }; + hermesWeb = pkgs.callPackage ./web.nix { + npm-lockfile-fix = inputs'.npm-lockfile-fix.packages.default; + }; + runtimeDeps = with pkgs; [ nodejs_22 ripgrep @@ -52,6 +56,7 @@ mkdir -p $out/share/hermes-agent $out/bin cp -r ${bundledSkills} $out/share/hermes-agent/skills + cp -r ${hermesWeb} $out/share/hermes-agent/web_dist # copy pre-built TUI (same layout as dev: ui-tui/dist/ + node_modules/) mkdir -p $out/ui-tui @@ -62,6 +67,7 @@ makeWrapper ${hermesVenv}/bin/${name} $out/bin/${name} \ --suffix PATH : "${runtimePath}" \ --set HERMES_BUNDLED_SKILLS $out/share/hermes-agent/skills \ + --set HERMES_WEB_DIST $out/share/hermes-agent/web_dist \ --set HERMES_TUI_DIR $out/ui-tui \ --set HERMES_PYTHON ${hermesVenv}/bin/python3 \ --set HERMES_NODE ${pkgs.nodejs_22}/bin/node @@ -81,7 +87,7 @@ STAMP_VALUE="${pyprojectHash}:${uvLockHash}" if [ ! -f "$STAMP" ] || [ "$(cat "$STAMP")" != "$STAMP_VALUE" ]; then echo "hermes-agent: installing Python dependencies..." - uv venv .venv --python ${pkgs.python311}/bin/python3 2>/dev/null || true + uv venv .venv --python ${pkgs.python312}/bin/python3 2>/dev/null || true source .venv/bin/activate uv pip install -e ".[all]" [ -d mini-swe-agent ] && uv pip install -e ./mini-swe-agent 2>/dev/null || true @@ -104,6 +110,7 @@ }; tui = hermesTui; + web = hermesWeb; }; }; } diff --git a/nix/python.nix b/nix/python.nix index 91411f4d75..0bcd017e76 100644 --- a/nix/python.nix +++ b/nix/python.nix @@ -1,6 +1,6 @@ # nix/python.nix — uv2nix virtual environment builder { - python311, + python312, lib, callPackage, uv2nix, @@ -51,28 +51,30 @@ let pythonPackageOverrides = final: _prev: if isAarch64Darwin then { - numpy = mkPrebuiltOverride final python311.pkgs.numpy { }; + numpy = mkPrebuiltOverride final python312.pkgs.numpy { }; - av = mkPrebuiltOverride final python311.pkgs.av { }; + pyarrow = mkPrebuiltOverride final python312.pkgs.pyarrow { }; - humanfriendly = mkPrebuiltOverride final python311.pkgs.humanfriendly { }; + av = mkPrebuiltOverride final python312.pkgs.av { }; - coloredlogs = mkPrebuiltOverride final python311.pkgs.coloredlogs { + humanfriendly = mkPrebuiltOverride final python312.pkgs.humanfriendly { }; + + coloredlogs = mkPrebuiltOverride final python312.pkgs.coloredlogs { humanfriendly = [ ]; }; - onnxruntime = mkPrebuiltOverride final python311.pkgs.onnxruntime { + onnxruntime = mkPrebuiltOverride final python312.pkgs.onnxruntime { coloredlogs = [ ]; numpy = [ ]; packaging = [ ]; }; - ctranslate2 = mkPrebuiltOverride final python311.pkgs.ctranslate2 { + ctranslate2 = mkPrebuiltOverride final python312.pkgs.ctranslate2 { numpy = [ ]; pyyaml = [ ]; }; - faster-whisper = mkPrebuiltOverride final python311.pkgs.faster-whisper { + faster-whisper = mkPrebuiltOverride final python312.pkgs.faster-whisper { av = [ ]; ctranslate2 = [ ]; huggingface-hub = [ ]; @@ -84,7 +86,7 @@ let pythonSet = (callPackage pyproject-nix.build.packages { - python = python311; + python = python312; }).overrideScope (lib.composeManyExtensions [ pyproject-build-systems.overlays.default diff --git a/nix/web.nix b/nix/web.nix new file mode 100644 index 0000000000..247889753f --- /dev/null +++ b/nix/web.nix @@ -0,0 +1,63 @@ +# nix/web.nix — Hermes Web Dashboard (Vite/React) frontend build +{ pkgs, npm-lockfile-fix, ... }: +let + src = ../web; + npmDeps = pkgs.fetchNpmDeps { + inherit src; + hash = "sha256-Y0pOzdFG8BLjfvCLmsvqYpjxFjAQabXp1i7X9W/cCU4="; + }; + + npmLockHash = builtins.hashString "sha256" (builtins.readFile ../web/package-lock.json); +in +pkgs.buildNpmPackage { + pname = "hermes-web"; + version = "0.0.0"; + inherit src npmDeps; + + doCheck = false; + + buildPhase = '' + npx tsc -b + npx vite build --outDir dist + ''; + + installPhase = '' + runHook preInstall + cp -r dist $out + runHook postInstall + ''; + + nativeBuildInputs = [ + (pkgs.writeShellScriptBin "update_web_lockfile" '' + set -euox pipefail + + REPO_ROOT=$(git rev-parse --show-toplevel) + + cd "$REPO_ROOT/web" + rm -rf node_modules/ + npm cache clean --force + CI=true npm install + ${pkgs.lib.getExe npm-lockfile-fix} ./package-lock.json + + NIX_FILE="$REPO_ROOT/nix/web.nix" + sed -i "s/hash = \"[^\"]*\";/hash = \"\";/" $NIX_FILE + NIX_OUTPUT=$(nix build .#web 2>&1 || true) + NEW_HASH=$(echo "$NIX_OUTPUT" | grep 'got:' | awk '{print $2}') + echo got new hash $NEW_HASH + sed -i "s|hash = \"[^\"]*\";|hash = \"$NEW_HASH\";|" $NIX_FILE + nix build .#web + echo "Updated npm hash in $NIX_FILE to $NEW_HASH" + '') + ]; + + passthru.devShellHook = '' + STAMP=".nix-stamps/hermes-web" + STAMP_VALUE="${npmLockHash}" + if [ ! -f "$STAMP" ] || [ "$(cat "$STAMP")" != "$STAMP_VALUE" ]; then + echo "hermes-web: installing npm dependencies..." + cd web && CI=true npm install --silent --no-fund --no-audit 2>/dev/null && cd .. + mkdir -p .nix-stamps + echo "$STAMP_VALUE" > "$STAMP" + fi + ''; +} diff --git a/scripts/release.py b/scripts/release.py index 372a4802ba..4c32dccfdb 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -74,6 +74,7 @@ AUTHOR_MAP = { "109555139+davetist@users.noreply.github.com": "davetist", "39405770+yyq4193@users.noreply.github.com": "yyq4193", "Asunfly@users.noreply.github.com": "Asunfly", + "2500400+honghua@users.noreply.github.com": "honghua", # contributors (manual mapping from git names) "ahmedsherif95@gmail.com": "asheriif", "liujinkun@bytedance.com": "liujinkun2025", @@ -264,6 +265,7 @@ AUTHOR_MAP = { "asurla@nvidia.com": "anniesurla", "limkuan24@gmail.com": "WideLee", "aviralarora002@gmail.com": "AviArora02-commits", + "junminliu@gmail.com": "JimLiu", } diff --git a/skills/creative/baoyu-infographic/PORT_NOTES.md b/skills/creative/baoyu-infographic/PORT_NOTES.md new file mode 100644 index 0000000000..0a2d86d89c --- /dev/null +++ b/skills/creative/baoyu-infographic/PORT_NOTES.md @@ -0,0 +1,43 @@ +# Port Notes — baoyu-infographic + +Ported from [JimLiu/baoyu-skills](https://github.com/JimLiu/baoyu-skills) v1.56.1. + +## Changes from upstream + +Only `SKILL.md` was modified. All 45 reference files are verbatim copies. + +### SKILL.md adaptations + +| Change | Upstream | Hermes | +|--------|----------|--------| +| Metadata namespace | `openclaw` | `hermes` | +| Trigger | `/baoyu-infographic` slash command | Natural language skill matching | +| User config | EXTEND.md file (project/user/XDG paths) | Removed — not part of Hermes infra | +| User prompts | `AskUserQuestion` (batched) | `clarify` tool (one at a time) | +| Image generation | baoyu-imagine (Bun/TypeScript) | `image_generate` tool | +| Platform support | Linux/macOS/Windows/WSL/PowerShell | Linux/macOS only | +| File operations | Bash commands | Hermes file tools (write_file, read_file) | + +### What was preserved + +- All layout definitions (21 files) +- All style definitions (21 files) +- Core reference files (analysis-framework, base-prompt, structured-content-template) +- Recommended combinations table +- Keyword shortcuts table +- Core principles and workflow structure +- Author, version, homepage attribution + +## Syncing with upstream + +To pull upstream updates: +```bash +# Compare versions +curl -sL https://raw.githubusercontent.com/JimLiu/baoyu-skills/main/skills/baoyu-infographic/SKILL.md | head -5 +# Look for version: line + +# Diff reference files +diff <(curl -sL https://raw.githubusercontent.com/.../references/layouts/bento-grid.md) references/layouts/bento-grid.md +``` + +Reference files can be overwritten directly (they're unchanged from upstream). SKILL.md must be manually merged since it contains Hermes-specific adaptations. diff --git a/skills/creative/baoyu-infographic/SKILL.md b/skills/creative/baoyu-infographic/SKILL.md new file mode 100644 index 0000000000..fea3499cbf --- /dev/null +++ b/skills/creative/baoyu-infographic/SKILL.md @@ -0,0 +1,236 @@ +--- +name: baoyu-infographic +description: Generate professional infographics with 21 layout types and 21 visual styles. Analyzes content, recommends layout×style combinations, and generates publication-ready infographics. Use when user asks to create "infographic", "visual summary", "信息图", "可视化", or "高密度信息大图". +version: 1.56.1 +author: 宝玉 (JimLiu) +license: MIT +metadata: + hermes: + tags: [infographic, visual-summary, creative, image-generation] + homepage: https://github.com/JimLiu/baoyu-skills#baoyu-infographic +--- + +# Infographic Generator + +Adapted from [baoyu-infographic](https://github.com/JimLiu/baoyu-skills) for Hermes Agent's tool ecosystem. + +Two dimensions: **layout** (information structure) × **style** (visual aesthetics). Freely combine any layout with any style. + +## When to Use + +Trigger this skill when the user asks to create an infographic, visual summary, information graphic, or uses terms like "信息图", "可视化", or "高密度信息大图". The user provides content (text, file path, URL, or topic) and optionally specifies layout, style, aspect ratio, or language. + +## Options + +| Option | Values | +|--------|--------| +| Layout | 21 options (see Layout Gallery), default: bento-grid | +| Style | 21 options (see Style Gallery), default: craft-handmade | +| Aspect | Named: landscape (16:9), portrait (9:16), square (1:1). Custom: any W:H ratio (e.g., 3:4, 4:3, 2.35:1) | +| Language | en, zh, ja, etc. | + +## Layout Gallery + +| Layout | Best For | +|--------|----------| +| `linear-progression` | Timelines, processes, tutorials | +| `binary-comparison` | A vs B, before-after, pros-cons | +| `comparison-matrix` | Multi-factor comparisons | +| `hierarchical-layers` | Pyramids, priority levels | +| `tree-branching` | Categories, taxonomies | +| `hub-spoke` | Central concept with related items | +| `structural-breakdown` | Exploded views, cross-sections | +| `bento-grid` | Multiple topics, overview (default) | +| `iceberg` | Surface vs hidden aspects | +| `bridge` | Problem-solution | +| `funnel` | Conversion, filtering | +| `isometric-map` | Spatial relationships | +| `dashboard` | Metrics, KPIs | +| `periodic-table` | Categorized collections | +| `comic-strip` | Narratives, sequences | +| `story-mountain` | Plot structure, tension arcs | +| `jigsaw` | Interconnected parts | +| `venn-diagram` | Overlapping concepts | +| `winding-roadmap` | Journey, milestones | +| `circular-flow` | Cycles, recurring processes | +| `dense-modules` | High-density modules, data-rich guides | + +Full definitions: `references/layouts/.md` + +## Style Gallery + +| Style | Description | +|-------|-------------| +| `craft-handmade` | Hand-drawn, paper craft (default) | +| `claymation` | 3D clay figures, stop-motion | +| `kawaii` | Japanese cute, pastels | +| `storybook-watercolor` | Soft painted, whimsical | +| `chalkboard` | Chalk on black board | +| `cyberpunk-neon` | Neon glow, futuristic | +| `bold-graphic` | Comic style, halftone | +| `aged-academia` | Vintage science, sepia | +| `corporate-memphis` | Flat vector, vibrant | +| `technical-schematic` | Blueprint, engineering | +| `origami` | Folded paper, geometric | +| `pixel-art` | Retro 8-bit | +| `ui-wireframe` | Grayscale interface mockup | +| `subway-map` | Transit diagram | +| `ikea-manual` | Minimal line art | +| `knolling` | Organized flat-lay | +| `lego-brick` | Toy brick construction | +| `pop-laboratory` | Blueprint grid, coordinate markers, lab precision | +| `morandi-journal` | Hand-drawn doodle, warm Morandi tones | +| `retro-pop-grid` | 1970s retro pop art, Swiss grid, thick outlines | +| `hand-drawn-edu` | Macaron pastels, hand-drawn wobble, stick figures | + +Full definitions: `references/styles/