feat(icons): no-frame wordmarks, squircle BrandMarks, on-demand generation

- website wordmarks (logo.png/logo-dark.png) drop the black/white frame:
  the girl alone on transparency, black for light navbar / white for dark
  (docusaurus srcDark stays)
- BrandMark marks are now the app-icon squircle itself (transparent
  corners, 824px safe-zone composition) instead of plain tiles; the
  components no longer paint a tile/rounding
- generated icon outputs are NOT committed anymore: all 35 targets are
  gitignored and regenerated on demand by the consuming pipelines via the
  new scripts/generate-icons.mjs (website prebuild, desktop prebuild+dev,
  installer prebuild, web prebuild)
- freshness lane switched from byte-compare to structural verification
  (sizes, squircle corner transparency, ICO frame sets via header parse) —
  no more windows/ubuntu byte drift dance
- marks saved as 8-bit palette PNGs (FASTOCTREE quantize keeps per-index
  alpha tRNS -> 2.5KB, AA edges preserved); compose_svg renders in the
  background's native space so resvg scales per output size
This commit is contained in:
ethernet
2026-09-06 21:49:26 -04:00
parent 1c729b39f7
commit f5b56a2746
50 changed files with 657 additions and 556 deletions

View File

@@ -2,11 +2,12 @@ name: Icon Assets Freshness
# Guard for the icon pipeline. The sources of truth are the nous-girl SVGs # Guard for the icon pipeline. The sources of truth are the nous-girl SVGs
# (assets/nous-girl-*.svg) and the per-platform backgrounds # (assets/nous-girl-*.svg) and the per-platform backgrounds
# (assets/backgrounds/*.svg); assets/icon-master*.svg and every raster target # (assets/backgrounds/*.svg). Generated outputs are NOT committed — every
# are generated from those (see scripts/generate_icons.py). This check # consuming pipeline regenerates them via scripts/generate-icons.mjs — so
# regenerates everything and byte-compares against the committed files. The # this lane runs --check, which regenerates every target in memory and
# repo can never hold a hand-edited or stale generated icon, and an art or # asserts structural invariants (sizes, transparent squircle corners,
# background change can never land without its regenerated outputs. # container frame sets). A source change can never land broken. See
# scripts/generate_icons.py.
# #
# Byte-determinism note: resvg renders the same SVG to identical bytes on any # Byte-determinism note: resvg renders the same SVG to identical bytes on any
# host for pure-shape artwork. If the master ever contains <text>, font # host for pure-shape artwork. If the master ever contains <text>, font

746
.gitignore vendored
View File

@@ -1,359 +1,389 @@
!apps/desktop/src/global.d.ts !apps/desktop/src/global.d.ts
!apps/desktop/src/plugins/*/plugin.js !apps/desktop/src/plugins/*/plugin.js
!apps/desktop/src/vite-env.d.ts !apps/desktop/src/vite-env.d.ts
!hermes_cli/data/ !hermes_cli/data/
!hermes_cli/data/plugin_index.json !hermes_cli/data/plugin_index.json
# — ignore so `git status` stays clean and update's autostash skips them. # — ignore so `git status` stays clean and update's autostash skips them.
# (launch-time stale-bytecode sweep). Runtime state, never a code change. # (launch-time stale-bytecode sweep). Runtime state, never a code change.
# `data/` pattern above would otherwise swallow it. # `data/` pattern above would otherwise swallow it.
# `npm run sync-assets` (see web/package.json). # `npm run sync-assets` (see web/package.json).
# also created in-repo when an agent operates in this checkout). Plans, audit # also created in-repo when an agent operates in this checkout). Plans, audit
# and `hermes update`'s untracked autostash does not treat it as a local edit (#66189 / #54855). # and `hermes update`'s untracked autostash does not treat it as a local edit (#66189 / #54855).
# automation-blueprints-index.json is a build artifact emitted by # automation-blueprints-index.json is a build artifact emitted by
# bootstrap installer. It is Hermes-managed runtime state, never a code change — # bootstrap installer. It is Hermes-managed runtime state, never a code change —
# Bundled community plugin index seed (shipped as package data) — the bare # Bundled community plugin index seed (shipped as package data) — the bare
# by `hermes update` / launch-time self-heal. Runtime state, never a code change # by `hermes update` / launch-time self-heal. Runtime state, never a code change
# by accident via 3a69e34702, removed in the #72002 salvage). # by accident via 3a69e34702, removed in the #72002 salvage).
# Checkout fingerprint the __pycache__ tree was last validated against # Checkout fingerprint the __pycache__ tree was last validated against
# CLI config (may contain sensitive SSH paths) # CLI config (may contain sensitive SSH paths)
# committed to the repo root. See the hermes-release skill. # committed to the repo root. See the hermes-release skill.
# Cross-process web UI build lock (flock target, always empty) # Cross-process web UI build lock (flock target, always empty)
# cut (passed to `gh release create --notes-file`); the GitHub Release itself # cut (passed to `gh release create --notes-file`); the GitHub Release itself
# Desktop demo-run scratch output (hermes writes demo/*.txt during recorded # Desktop demo-run scratch output (hermes writes demo/*.txt during recorded
# Desktop/bootstrap install marker written into the managed checkout root by the # Desktop/bootstrap install marker written into the managed checkout root by the
# e.g. hermes-bots): plugin.js IS the source, not tsc output. No .tsx # e.g. hermes-bots): plugin.js IS the source, not tsc output. No .tsx
# every build). # every build).
# EXCEPT bundled plain-ESM plugin entries (adopted SDK-consumer plugins, # EXCEPT bundled plain-ESM plugin entries (adopted SDK-consumer plugins,
# git for the same reason as skills-index.json (large, generated, change # git for the same reason as skills-index.json (large, generated, change
# ignore it so `hermes update`'s `git stash push --include-untracked` does not # ignore it so `hermes update`'s `git stash push --include-untracked` does not
# image-provider (fal.media) URL — they are NEVER committed to the repo. The # image-provider (fal.media) URL — they are NEVER committed to the repo. The
# infographic-check CI job is what actually enforces this. # infographic-check CI job is what actually enforces this.
# Installer-written method stamp in the managed checkout root (scripts/install.sh). # Installer-written method stamp in the managed checkout root (scripts/install.sh).
# interrupted; consumed by launch-time recovery. Never commit it (was tracked # interrupted; consumed by launch-time recovery. Never commit it (was tracked
# Interrupted-update breadcrumb + recovery lock written next to the shared venv # Interrupted-update breadcrumb + recovery lock written next to the shared venv
# Local editor / agent tooling (machine-specific; keep in global config, not the repo) # Local editor / agent tooling (machine-specific; keep in global config, not the repo)
# logs, and per-session caches are never artifacts of the codebase. # logs, and per-session caches are never artifacts of the codebase.
# Nix # Nix
# No trailing slash: also matches node_modules SYMLINKS (worktrees often # No trailing slash: also matches node_modules SYMLINKS (worktrees often
# Per-release changelog drafts. These exist only transiently during a release # Per-release changelog drafts. These exist only transiently during a release
# Persistent dev sandbox dir (scripts/dev-sandbox.sh --persistent) # Persistent dev sandbox dir (scripts/dev-sandbox.sh --persistent)
# Playwright visual regression baselines — cached from main in CI, not committed # Playwright visual regression baselines — cached from main in CI, not committed
# PR body is the archive. See the hermes-agent-dev skill's # PR body is the archive. See the hermes-agent-dev skill's
# PR infographics are rendered locally and embedded in PR descriptions via the # PR infographics are rendered locally and embedded in PR descriptions via the
# pr-infographic-workflow reference (storage rule + lapse #8 / #COMMIT-1). # pr-infographic-workflow reference (storage rule + lapse #8 / #COMMIT-1).
# Private keys # Private keys
# regenerated by scripts/tool_search_livetest.py. Never an artifact of the repo. # regenerated by scripts/tool_search_livetest.py. Never an artifact of the repo.
# Release script temp files # Release script temp files
# Repo-root build/debug artifacts that must never be committed # Repo-root build/debug artifacts that must never be committed
# resolves the stale .js OVER the .tsx — never track these) # resolves the stale .js OVER the .tsx — never track these)
# Runtime marker written by hermes update when a lazy dependency refresh is # Runtime marker written by hermes update when a lazy dependency refresh is
# Runtime metadata only — never a code change. Ignore so `git status` stays clean # Runtime metadata only — never a code change. Ignore so `git status` stays clean
# Sandbox dirs used by the install/update E2E (tests/install/). The suffix is # Sandbox dirs used by the install/update E2E (tests/install/). The suffix is
# sibling exists, so the stale-shadow hazard above cannot apply. # sibling exists, so the stale-shadow hazard above cannot apply.
# sidestepped by an `infograficos/` directory (#70552). .gitignore is only # sidestepped by an `infograficos/` directory (#70552). .gitignore is only
# Skills Hub state (lives in ~/.hermes/skills/.hub/ at runtime, but just in case) # Skills Hub state (lives in ~/.hermes/skills/.hub/ at runtime, but just in case)
# skills.json + skills-meta.json are build artifacts emitted by # skills.json + skills-meta.json are build artifacts emitted by
# slip into a commit and break `npm ci` on CI with ENOTDIR). # slip into a commit and break `npm ci` on CI with ENOTDIR).
# Spelling variants are listed because a single `infographic/` pattern was # Spelling variants are listed because a single `infographic/` pattern was
# stores the published notes. They are not a build artifact and must never be # stores the published notes. They are not a build artifact and must never be
# symlink node_modules to the main checkout; the dir-only pattern let one # symlink node_modules to the main checkout; the dir-only pattern let one
# the first line of defence and cannot stop `git add -f` at all — the # the first line of defence and cannot stop `git add -f` at all — the
# the route name, so each route gets its own tree and two can run at once. # the route name, so each route gets its own tree and two can run at once.
# Tool Search live-test harness output — non-deterministic model transcripts, # Tool Search live-test harness output — non-deterministic model transcripts,
# treat it as a local edit and autostash it on every run (#38529). # treat it as a local edit and autostash it on every run (#38529).
# tsc-emitted artifacts (a stray `tsc -b` compiles into src/, and vite then # tsc-emitted artifacts (a stray `tsc -b` compiles into src/, and vite then
# walkthroughs). Throwaway artifacts, never part of the app. # walkthroughs). Throwaway artifacts, never part of the app.
# Web UI assets — synced from @nous-research/ui at build time via # Web UI assets — synced from @nous-research/ui at build time via
# Web UI build output # Web UI build output
# website/scripts/extract-automation-blueprints.py during prebuild. # website/scripts/extract-automation-blueprints.py during prebuild.
# website/scripts/extract-skills.py during prebuild — keep them out of # website/scripts/extract-skills.py during prebuild — keep them out of
# Working directory for the Hermes Agent's session state (~/.hermes/ at runtime; # Working directory for the Hermes Agent's session state (~/.hermes/ at runtime;
# #
%SystemDrive%/ %SystemDrive%/
*.pem *.pem
*.ppk *.ppk
*.pyc* *.pyc*
*.tsbuildinfo *.tsbuildinfo
*-snapshots/ *-snapshots/
.act-sandbox-agent.* .act-sandbox-agent.*
.bytecode-fingerprint .bytecode-fingerprint
.bytecode-fingerprint.tmp .bytecode-fingerprint.tmp
.codex/ .codex/
.cursor/ .cursor/
.direnv/ .direnv/
.DS_Store .DS_Store
.env .env
.env.development .env.development
.env.development.local .env.development.local
.env.local .env.local
.env.production.local .env.production.local
.env.test .env.test
.env.test.local .env.test.local
.gemini/ .gemini/
.hermes/ .hermes/
.hermes-bootstrap-complete .hermes-bootstrap-complete
.hermes-docker/ .hermes-docker/
.hermes-sandbox/ .hermes-sandbox/
.hermes-sandbox-e2e*/ .hermes-sandbox-e2e*/
.lazy-refresh-incomplete .lazy-refresh-incomplete
.mcp.json .mcp.json
.nix-stamps/ .nix-stamps/
.notebooklm-cli-venv/ .notebooklm-cli-venv/
.notebooklm-home/ .notebooklm-home/
.notebooklm-playwright/ .notebooklm-playwright/
.op.env .op.env
.pip-cache/ .pip-cache/
.pytest_cache/ .pytest_cache/
.pytest-cache/ .pytest-cache/
.release_notes.md .release_notes.md
.skills_prompt_snapshot.json .skills_prompt_snapshot.json
.update-incomplete .update-incomplete
.update-incomplete.lock .update-incomplete.lock
.uv-cache/ .uv-cache/
.venv .venv
.venv/ .venv/
.vscode/ .vscode/
.web_ui_build.lock .web_ui_build.lock
.worktrees/ .worktrees/
.zed/ .zed/
/*.png.bak /*.png.bak
/.hermes-runtime/ /.hermes-runtime/
/.install_method /.install_method
/_pycache/ /_pycache/
/bin/ /bin/
/default.tar.gz /default.tar.gz
/log.txt /log.txt
/sqlite_leak_fix.png /sqlite_leak_fix.png
/venv.old/ /venv.old/
/venv.stale.runtime-*/ /venv.stale.runtime-*/
/venv/ /venv/
__pycache__/ __pycache__/
__pycache__/model_tools.cpython-310.pyc __pycache__/model_tools.cpython-310.pyc
__pycache__/web_tools.cpython-310.pyc __pycache__/web_tools.cpython-310.pyc
act/ act/
agent-browser/ agent-browser/
apps/desktop/build/ apps/desktop/build/
apps/desktop/demo/ apps/desktop/demo/
apps/desktop/dist/ apps/desktop/dist/
apps/desktop/release/ apps/desktop/release/
apps/desktop/src/**/*.d.ts apps/desktop/src/**/*.d.ts
apps/desktop/src/**/*.js apps/desktop/src/**/*.js
apps/desktop/src/**/*.js.map apps/desktop/src/**/*.js.map
apps/shared/src/**/*.d.ts apps/shared/src/**/*.d.ts
apps/shared/src/**/*.js apps/shared/src/**/*.js
apps/shared/src/**/*.js.map apps/shared/src/**/*.js.map
browser-use/ browser-use/
cli-config.yaml cli-config.yaml
compose.hermes.local.yml compose.hermes.local.yml
config/mcporter.json config/mcporter.json
data/ data/
data/* data/*
docs/superpowers/* docs/superpowers/*
environments/benchmarks/evals/ environments/benchmarks/evals/
examples/ examples/
export* export*
hermes-*/* hermes-*/*
hermes_agent.egg-info/ hermes_agent.egg-info/
hermes_cli/scripts/ hermes_cli/scripts/
hermes_cli/tui_dist/* hermes_cli/tui_dist/*
hermes_cli/web_dist/ hermes_cli/web_dist/
ignored/ ignored/
images/ images/
infografico/ infografico/
infograficos/ infograficos/
infographic/ infographic/
infographics/ infographics/
logs/ logs/
mini-swe-agent/ mini-swe-agent/
models-dev-upstream/ models-dev-upstream/
native/fts5_cjk/*.so native/fts5_cjk/*.so
node_modules node_modules
opencode.json opencode.json
playwright-report/ playwright-report/
privvy* privvy*
RELEASE_v*.md RELEASE_v*.md
result result
run_datagen_kimik2-thinking.sh run_datagen_kimik2-thinking.sh
run_datagen_megascience_glm4-6.sh run_datagen_megascience_glm4-6.sh
run_datagen_sonnet.sh run_datagen_sonnet.sh
source-data/* source-data/*
run_datagen_megascience_glm4-6.sh run_datagen_megascience_glm4-6.sh
data/* data/*
# No trailing slash: also matches node_modules SYMLINKS (worktrees often # No trailing slash: also matches node_modules SYMLINKS (worktrees often
# symlink node_modules to the main checkout; the dir-only pattern let one # symlink node_modules to the main checkout; the dir-only pattern let one
# slip into a commit and break `npm ci` on CI with ENOTDIR). # slip into a commit and break `npm ci` on CI with ENOTDIR).
node_modules node_modules
browser-use/ browser-use/
agent-browser/ agent-browser/
# Private keys # Private keys
*.ppk *.ppk
*.pem *.pem
privvy* privvy*
images/ images/
__pycache__/ __pycache__/
hermes_agent.egg-info/ hermes_agent.egg-info/
wandb/ wandb/
testlogs testlogs
playwright-report/ playwright-report/
test-results/ test-results/
# Playwright visual regression baselines — cached from main in CI, not committed # Playwright visual regression baselines — cached from main in CI, not committed
*-snapshots/ *-snapshots/
# CLI config (may contain sensitive SSH paths) # CLI config (may contain sensitive SSH paths)
cli-config.yaml cli-config.yaml
# Skills Hub state (lives in ~/.hermes/skills/.hub/ at runtime, but just in case) # Skills Hub state (lives in ~/.hermes/skills/.hub/ at runtime, but just in case)
skills/.hub/ skills/.hub/
ignored/ ignored/
.worktrees/ .worktrees/
environments/benchmarks/evals/ environments/benchmarks/evals/
# Web UI build output # Web UI build output
hermes_cli/web_dist/ hermes_cli/web_dist/
# Cross-process web UI build lock (flock target, always empty) # Cross-process web UI build lock (flock target, always empty)
.web_ui_build.lock .web_ui_build.lock
apps/desktop/build/ apps/desktop/build/
apps/desktop/dist/ apps/desktop/dist/
# tsc-emitted artifacts (a stray `tsc -b` compiles into src/, and vite then # tsc-emitted artifacts (a stray `tsc -b` compiles into src/, and vite then
# resolves the stale .js OVER the .tsx — never track these) # resolves the stale .js OVER the .tsx — never track these)
apps/desktop/src/**/*.js apps/desktop/src/**/*.js
apps/desktop/src/**/*.js.map apps/desktop/src/**/*.js.map
apps/desktop/src/**/*.d.ts apps/desktop/src/**/*.d.ts
# EXCEPT bundled plain-ESM plugin entries (adopted SDK-consumer plugins, # EXCEPT bundled plain-ESM plugin entries (adopted SDK-consumer plugins,
# e.g. hermes-bots): plugin.js IS the source, not tsc output. No .tsx # e.g. hermes-bots): plugin.js IS the source, not tsc output. No .tsx
# sibling exists, so the stale-shadow hazard above cannot apply. # sibling exists, so the stale-shadow hazard above cannot apply.
!apps/desktop/src/plugins/*/plugin.js !apps/desktop/src/plugins/*/plugin.js
!apps/desktop/src/global.d.ts !apps/desktop/src/global.d.ts
!apps/desktop/src/vite-env.d.ts !apps/desktop/src/vite-env.d.ts
# Build/debug artifacts that must never be committed # Build/debug artifacts that must never be committed
/log.txt /log.txt
/sqlite_leak_fix.png /sqlite_leak_fix.png
/*.png.bak /*.png.bak
*.tar.gz *.tar.gz
*.tgz *.tgz
apps/shared/src/**/*.js apps/shared/src/**/*.js
apps/shared/src/**/*.js.map apps/shared/src/**/*.js.map
apps/shared/src/**/*.d.ts apps/shared/src/**/*.d.ts
apps/desktop/release/ apps/desktop/release/
# stage-and-swap Desktop rebuild output (#86443); removed after the swap, but # stage-and-swap Desktop rebuild output (#86443); removed after the swap, but
# a killed build must not leave the checkout dirty # a killed build must not leave the checkout dirty
apps/desktop/.staging-*/ apps/desktop/.staging-*/
*.tsbuildinfo *.tsbuildinfo
# Web UI assets — synced from @nous-research/ui at build time via # Web UI assets — synced from @nous-research/ui at build time via
# `npm run sync-assets` (see web/package.json). # `npm run sync-assets` (see web/package.json).
web/public/fonts/ web/public/fonts/
web/public/ds-assets/ web/public/ds-assets/
# Release script temp files # Release script temp files
.release_notes.md .release_notes.md
mini-swe-agent/ mini-swe-agent/
# Nix # Nix
.direnv/ .direnv/
.nix-stamps/ .nix-stamps/
result result
website/static/api/skills-index.json website/static/api/skills-index.json
# skills.json + skills-meta.json are build artifacts emitted by # skills.json + skills-meta.json are build artifacts emitted by
# website/scripts/extract-skills.py during prebuild — keep them out of # website/scripts/extract-skills.py during prebuild — keep them out of
# git for the same reason as skills-index.json (large, generated, change # git for the same reason as skills-index.json (large, generated, change
# every build). # every build).
website/static/api/skills.json website/static/api/skills.json
website/static/api/skills-meta.json
# automation-blueprints-index.json is a build artifact emitted by
# website/scripts/extract-automation-blueprints.py during prebuild.
website/static/api/automation-blueprints-index.json
models-dev-upstream/
# Local editor / agent tooling (machine-specific; keep in global config, not the repo)
.codex/
.cursor/
.gemini/
.zed/
.mcp.json
opencode.json
config/mcporter.json
hermes_cli/tui_dist/*
hermes_cli/scripts/
docs/superpowers/*
# Working directory for the Hermes Agent's session state (~/.hermes/ at runtime;
# also created in-repo when an agent operates in this checkout). Plans, audit
# logs, and per-session caches are never artifacts of the codebase.
.hermes/
# Desktop/bootstrap install marker written into the managed checkout root by the
# bootstrap installer. It is Hermes-managed runtime state, never a code change —
# ignore it so `hermes update`'s `git stash push --include-untracked` does not
# treat it as a local edit and autostash it on every run (#38529).
.hermes-bootstrap-complete
# Persistent dev sandbox dir (scripts/dev-sandbox.sh --persistent)
.hermes-sandbox/
# Sandbox dirs used by the install/update E2E (tests/install/). The suffix is
# the route name, so each route gets its own tree and two can run at once.
.hermes-sandbox-e2e*/
# Interrupted-update breadcrumb + recovery lock written next to the shared venv
# by `hermes update` / launch-time self-heal. Runtime state, never a code change
# — ignore so `git status` stays clean and update's autostash skips them.
.update-incomplete
.update-incomplete.lock
# Checkout fingerprint the __pycache__ tree was last validated against
# (launch-time stale-bytecode sweep). Runtime state, never a code change.
.bytecode-fingerprint
.bytecode-fingerprint.tmp
# Installer-written method stamp in the managed checkout root (scripts/install.sh).
# Runtime metadata only — never a code change. Ignore so `git status` stays clean
# and `hermes update`'s untracked autostash does not treat it as a local edit (#66189 / #54855).
/.install_method
# Tool Search live-test harness output — non-deterministic model transcripts,
# regenerated by scripts/tool_search_livetest.py. Never an artifact of the repo.
scripts/out/
# Per-release changelog drafts. These exist only transiently during a release
# cut (passed to `gh release create --notes-file`); the GitHub Release itself
# stores the published notes. They are not a build artifact and must never be
# committed to the repo root. See the hermes-release skill.
# Desktop demo-run scratch output (hermes writes demo/*.txt during recorded
# walkthroughs). Throwaway artifacts, never part of the app.
# PR infographics are rendered locally and embedded in PR descriptions via the
# image-provider (fal.media) URL — they are NEVER committed to the repo. The
# PR body is the archive. See the hermes-agent-dev skill's
# pr-infographic-workflow reference (storage rule + lapse #8 / #COMMIT-1).
#
# Spelling variants are listed because a single `infographic/` pattern was
# sidestepped by an `infograficos/` directory (#70552). .gitignore is only
# the first line of defence and cannot stop `git add -f` at all — the
# infographic-check CI job is what actually enforces this.
# Runtime marker written by hermes update when a lazy dependency refresh is
# interrupted; consumed by launch-time recovery. Never commit it (was tracked
# by accident via 3a69e34702, removed in the #72002 salvage).
# Disposable profile created by scripts/probe_active_session_exclusivity.py
.probe-home/
skills/.hub/
source-data/*
temp_vision_images/
test_durations.json
testlogs
test-results/
tests/quick_test_dataset.jsonl
tests/sample_dataset.jsonl
tmp/
wandb/
web/public/ds-assets/
web/public/fonts/
website/static/api/automation-blueprints-index.json
website/static/api/skills.json
website/static/api/skills-index.json
website/static/api/skills-meta.json website/static/api/skills-meta.json
# automation-blueprints-index.json is a build artifact emitted by # ── generated icon assets (see scripts/generate_icons.py) ──────────────────
# website/scripts/extract-automation-blueprints.py during prebuild. # Regenerated on demand by every consuming pipeline (website/desktop/
website/static/api/automation-blueprints-index.json # installer/web prebuild via scripts/generate-icons.mjs). Sources of truth:
models-dev-upstream/ # assets/nous-girl-*.svg + assets/backgrounds/.
assets/icon-master.svg
# Local editor / agent tooling (machine-specific; keep in global config, not the repo) assets/icon-master-dark.svg
.codex/ apps/desktop/assets/icon.png
.cursor/ apps/desktop/assets/icon.ico
.gemini/ apps/desktop/assets/icon.icns
.zed/ apps/desktop/assets/icon-dark.png
.mcp.json apps/desktop/assets/icon-dark.ico
opencode.json apps/desktop/assets/icon-dark.icns
config/mcporter.json apps/desktop/assets/appx/*.png
apps/desktop/public/apple-touch-icon.png
hermes_cli/tui_dist/* apps/desktop/public/nous-girl.png
hermes_cli/scripts/ apps/desktop/public/nous-girl-dark.png
docs/superpowers/* apps/bootstrap-installer/src-tauri/icons/*.png
# Working directory for the Hermes Agent's session state (~/.hermes/ at runtime; apps/bootstrap-installer/src-tauri/icons/icon.ico
# also created in-repo when an agent operates in this checkout). Plans, audit apps/bootstrap-installer/src-tauri/icons/icon.icns
# logs, and per-session caches are never artifacts of the codebase. apps/bootstrap-installer/public/nous-girl.png
.hermes/ website/static/img/logo.png
website/static/img/logo-dark.png
# Desktop/bootstrap install marker written into the managed checkout root by the website/static/img/nous-logo.png
# bootstrap installer. It is Hermes-managed runtime state, never a code change — website/static/img/nous-logo-dark.png
# ignore it so `hermes update`'s `git stash push --include-untracked` does not website/static/img/favicon-16x16.png
# treat it as a local edit and autostash it on every run (#38529). website/static/img/favicon-32x32.png
.hermes-bootstrap-complete website/static/img/apple-touch-icon.png
website/static/img/favicon.ico
# Persistent dev sandbox dir (scripts/dev-sandbox.sh --persistent) website/static/img/favicon.svg
.hermes-sandbox/ web/public/favicon.ico
# Sandbox dirs used by the install/update E2E (tests/install/). The suffix is
# the route name, so each route gets its own tree and two can run at once.
.hermes-sandbox-e2e*/
# Interrupted-update breadcrumb + recovery lock written next to the shared venv
# by `hermes update` / launch-time self-heal. Runtime state, never a code change
# — ignore so `git status` stays clean and update's autostash skips them.
.update-incomplete
.update-incomplete.lock
# Checkout fingerprint the __pycache__ tree was last validated against
# (launch-time stale-bytecode sweep). Runtime state, never a code change.
.bytecode-fingerprint
.bytecode-fingerprint.tmp
# Installer-written method stamp in the managed checkout root (scripts/install.sh).
# Runtime metadata only — never a code change. Ignore so `git status` stays clean
# and `hermes update`'s untracked autostash does not treat it as a local edit (#66189 / #54855).
/.install_method
# Tool Search live-test harness output — non-deterministic model transcripts,
# regenerated by scripts/tool_search_livetest.py. Never an artifact of the repo.
scripts/out/
# Per-release changelog drafts. These exist only transiently during a release
# cut (passed to `gh release create --notes-file`); the GitHub Release itself
# stores the published notes. They are not a build artifact and must never be
# committed to the repo root. See the hermes-release skill.
# Desktop demo-run scratch output (hermes writes demo/*.txt during recorded
# walkthroughs). Throwaway artifacts, never part of the app.
# PR infographics are rendered locally and embedded in PR descriptions via the
# image-provider (fal.media) URL — they are NEVER committed to the repo. The
# PR body is the archive. See the hermes-agent-dev skill's
# pr-infographic-workflow reference (storage rule + lapse #8 / #COMMIT-1).
#
# Spelling variants are listed because a single `infographic/` pattern was
# sidestepped by an `infograficos/` directory (#70552). .gitignore is only
# the first line of defence and cannot stop `git add -f` at all — the
# infographic-check CI job is what actually enforces this.
# Runtime marker written by hermes update when a lazy dependency refresh is
# interrupted; consumed by launch-time recovery. Never commit it (was tracked
# by accident via 3a69e34702, removed in the #72002 salvage).
# Disposable profile created by scripts/probe_active_session_exclusivity.py
.probe-home/
skills/.hub/
source-data/*
temp_vision_images/
test_durations.json
testlogs
test-results/
tests/quick_test_dataset.jsonl
tests/sample_dataset.jsonl
tmp/
wandb/
web/public/ds-assets/
web/public/fonts/
website/static/api/automation-blueprints-index.json
website/static/api/skills.json
website/static/api/skills-index.json
website/static/api/skills-meta.json

View File

@@ -6,6 +6,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host 127.0.0.1 --port 5175", "dev": "vite --host 127.0.0.1 --port 5175",
"prebuild": "node ../../scripts/generate-icons.mjs",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"preview": "vite preview", "preview": "vite preview",
"tauri": "tauri", "tauri": "tauri",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -2,12 +2,12 @@ import { cn } from '../lib/utils'
const assetPath = (path: string) => `${import.meta.env.BASE_URL}${path.replace(/^\/+/, '')}` const assetPath = (path: string) => `${import.meta.env.BASE_URL}${path.replace(/^\/+/, '')}`
// Brand badge: nous-girl mark on a white tile (the light BrandMark variant). // Brand badge: the nous-girl mark in the app-icon squircle (light variant).
// Ported from apps/desktop's BrandMark; asset lives in this app's public/. // Ported from apps/desktop's BrandMark; the installer has no theme system so
// The installer has no theme system, so it always uses the light asset. // it always uses the light squircle asset. Generated by generate_icons.py.
export function BrandMark({ className, ...props }: React.ComponentProps<'span'>) { export function BrandMark({ className, ...props }: React.ComponentProps<'span'>) {
return ( return (
<span className={cn('inline-flex size-14 shrink-0 items-center justify-center bg-white', className)} {...props}> <span className={cn('inline-flex size-14 shrink-0 items-center justify-center', className)} {...props}>
<img alt="" className="size-full object-contain" src={assetPath('nous-girl.png')} /> <img alt="" className="size-full object-contain" src={assetPath('nous-girl.png')} />
</span> </span>
) )

View File

@@ -258,7 +258,7 @@ Sizes: `default`, `xs`, `overlay` (titlebar glyph counts).
mode, white girl on the `#0d1117` dark tile in dark mode (keyed off mode, white girl on the `#0d1117` dark tile in dark mode (keyed off
`renderedMode` so bright surfaces in dark skins stay on the light mark). `renderedMode` so bright surfaces in dark skins stay on the light mark).
The marks are generated by `scripts/generate_icons.py` from the girl SVGs The marks are generated by `scripts/generate_icons.py` from the girl SVGs
+ tile backgrounds (`assets/nous-girl-*.svg` × `assets/backgrounds/tile-*.svg`). + squircle backgrounds (`assets/nous-girl-*.svg` × `assets/backgrounds/`).
It replaced scattered Sparkles glyphs in updates / onboarding / about. Use it It replaced scattered Sparkles glyphs in updates / onboarding / about. Use it
for hero/brand moments; don't reintroduce decorative star/sparkle icons. for hero/brand moments; don't reintroduce decorative star/sparkle icons.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -19,7 +19,7 @@
"clean:e2e": "tsc --build tsconfig.e2e.json --clean", "clean:e2e": "tsc --build tsconfig.e2e.json --clean",
"clean:renderer": "tsc --build tsconfig.json --clean ", "clean:renderer": "tsc --build tsconfig.json --clean ",
"clean:electron": "tsc --build tsconfig.electron.json --clean", "clean:electron": "tsc --build tsconfig.electron.json --clean",
"dev": "concurrently -k \"npm:dev:renderer\" \"npm:dev:electron\"", "dev": "node ../../scripts/generate-icons.mjs && concurrently -k \"npm:dev:renderer\" \"npm:dev:electron\"",
"dev:fake-boot": "cross-env HERMES_DESKTOP_BOOT_FAKE=1 HERMES_DESKTOP_BOOT_FAKE_STEP_MS=650 npm run dev", "dev:fake-boot": "cross-env HERMES_DESKTOP_BOOT_FAKE=1 HERMES_DESKTOP_BOOT_FAKE_STEP_MS=650 npm run dev",
"dev:mock": "node scripts/dev-mock.mjs", "dev:mock": "node scripts/dev-mock.mjs",
"dev:renderer": "node scripts/assert-root-install.mjs && npm run clean:renderer && vite --host 127.0.0.1 --port 5174", "dev:renderer": "node scripts/assert-root-install.mjs && npm run clean:renderer && vite --host 127.0.0.1 --port 5174",
@@ -27,7 +27,7 @@
"profile:main": "tsc --build tsconfig.electron.json && wait-on http://127.0.0.1:5174 && node scripts/bundle-electron-main.mjs --dev && cross-env XCURSOR_SIZE=24 HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron --inspect=9229 .", "profile:main": "tsc --build tsconfig.electron.json && wait-on http://127.0.0.1:5174 && node scripts/bundle-electron-main.mjs --dev && cross-env XCURSOR_SIZE=24 HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron --inspect=9229 .",
"profile:main:cpu": "tsc --build tsconfig.electron.json && wait-on http://127.0.0.1:5174 && node scripts/bundle-electron-main.mjs --dev && cross-env XCURSOR_SIZE=24 NODE_OPTIONS=--cpu-prof HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron .", "profile:main:cpu": "tsc --build tsconfig.electron.json && wait-on http://127.0.0.1:5174 && node scripts/bundle-electron-main.mjs --dev && cross-env XCURSOR_SIZE=24 NODE_OPTIONS=--cpu-prof HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron .",
"start": "npm run build && electron .", "start": "npm run build && electron .",
"prebuild": "node scripts/assert-root-install.mjs && npm run clean", "prebuild": "node ../../scripts/generate-icons.mjs && node scripts/assert-root-install.mjs && npm run clean",
"build": "node scripts/assert-root-install.mjs && node scripts/write-build-stamp.mjs && vite build && node scripts/bundle-electron-main.mjs && node scripts/stage-native-deps.mjs", "build": "node scripts/assert-root-install.mjs && node scripts/write-build-stamp.mjs && vite build && node scripts/bundle-electron-main.mjs && node scripts/stage-native-deps.mjs",
"postbuild": "node scripts/assert-dist-built.mjs", "postbuild": "node scripts/assert-dist-built.mjs",
"prebuilder": "node scripts/patch-electron-builder-mac-binary.mjs", "prebuilder": "node scripts/patch-electron-builder-mac-binary.mjs",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -3,22 +3,18 @@ import { useTheme } from '@/themes'
const assetPath = (path: string) => `${import.meta.env.BASE_URL}${path.replace(/^\/+/, '')}` const assetPath = (path: string) => `${import.meta.env.BASE_URL}${path.replace(/^\/+/, '')}`
// Brand badge: nous-girl mark on a fixed tile, theme-aware — light mode shows // Brand badge: the nous-girl mark in the app-icon squircle, theme-aware —
// the black girl on white, dark mode the white girl on the nous dark surface. // light mode shows the black girl on the white squircle, dark mode the white
// Fills the tile (softly rounded); size via className (default size-14). // girl on the #0d1117 squircle. The mark PNG carries the rounded shape and
// transparent corners, so no tile/rounding classes here. Generated by
// scripts/generate_icons.py from the girl SVGs + squircle backgrounds; size
// via className (default size-14).
export function BrandMark({ className, ...props }: React.ComponentProps<'span'>) { export function BrandMark({ className, ...props }: React.ComponentProps<'span'>) {
const { renderedMode } = useTheme() const { renderedMode } = useTheme()
const dark = renderedMode === 'dark' const dark = renderedMode === 'dark'
return ( return (
<span <span className={cn('inline-flex size-14 shrink-0 items-center justify-center', className)} {...props}>
className={cn(
'inline-flex size-14 shrink-0 items-center justify-center overflow-hidden rounded-md bg-white',
dark && 'bg-[#0d1117]',
className
)}
{...props}
>
<img alt="" className="size-full object-contain" src={assetPath(dark ? 'nous-girl-dark.png' : 'nous-girl.png')} /> <img alt="" className="size-full object-contain" src={assetPath(dark ? 'nous-girl-dark.png' : 'nous-girl.png')} />
</span> </span>
) )

View File

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1772 1799">
<!-- website wordmark background, dark: white frame (L/R 14, T 17, B 16) + #0d1117 interior -->
<rect x="0" y="0" width="1772" height="1799" rx="2" fill="#ffffff"/>
<rect x="14" y="17" width="1743" height="1766" fill="#0d1117"/>
</svg>

Before

Width:  |  Height:  |  Size: 307 B

View File

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1772 1799">
<!-- website wordmark background, light: black frame (L/R 14, T 17, B 16) + white interior -->
<rect x="0" y="0" width="1772" height="1799" rx="2" fill="#000000"/>
<rect x="14" y="17" width="1743" height="1766" fill="#ffffff"/>
</svg>

Before

Width:  |  Height:  |  Size: 306 B

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<!-- BrandMark tile, dark: solid #0d1117 -->
<rect x="0" y="0" width="256" height="256" fill="#0d1117"/>
</svg>

Before

Width:  |  Height:  |  Size: 179 B

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<!-- BrandMark tile, light: solid white -->
<rect x="0" y="0" width="256" height="256" fill="#ffffff"/>
</svg>

Before

Width:  |  Height:  |  Size: 178 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 134 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 113 KiB

View File

@@ -0,0 +1,57 @@
#!/usr/bin/env node
/**
* Generate the icon assets on demand for the pipeline that needs them.
*
* Generated icons are NOT committed (see scripts/generate_icons.py) — every
* consuming pipeline regenerates them before building:
* - website: website/scripts/prebuild.mjs (docusaurus prebuild)
* - desktop: apps/desktop/package.json prebuild + predev
* - installer: apps/bootstrap-installer/package.json prebuild
* - web: web/package.json prebuild
*
* Finds the project venv python (resvg-py rides the dev extra) and runs the
* generator from the repo root. Fails loudly if the toolchain is missing so
* a build can never silently ship without icons.
*/
import { spawnSync } from 'node:child_process'
import { existsSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
function findPython() {
const candidates = [
path.join(repoRoot, '.venv', 'Scripts', 'python.exe'), // windows
path.join(repoRoot, '.venv', 'bin', 'python'), // posix
path.join(repoRoot, 'venv', 'Scripts', 'python.exe'),
path.join(repoRoot, 'venv', 'bin', 'python'),
]
for (const candidate of candidates) {
if (existsSync(candidate)) return candidate
}
return null
}
function main() {
const python = findPython()
const generator = path.join(repoRoot, 'scripts', 'generate_icons.py')
let result
if (python) {
result = spawnSync(python, [generator], { cwd: repoRoot, stdio: 'inherit' })
} else if (process.env.VIRTUAL_ENV) {
const py = process.platform === 'win32' ? 'python.exe' : 'python'
result = spawnSync(py, [generator], { cwd: repoRoot, stdio: 'inherit', env: process.env })
} else {
// Fall back to uv-managed python (CI installs the dev extra).
result = spawnSync('uv', ['run', 'python', generator], { cwd: repoRoot, stdio: 'inherit' })
}
if (result.error) {
console.error('[generate-icons] failed to launch icon generator:', result.error.message)
console.error('[generate-icons] install the dev extra with: uv sync --extra dev')
process.exit(1)
}
process.exit(result.status ?? 1)
}
main()

View File

@@ -3,7 +3,7 @@
Usage (from repo root): Usage (from repo root):
.venv/Scripts/python.exe scripts/generate_icons.py # write .venv/Scripts/python.exe scripts/generate_icons.py # write
.venv/Scripts/python.exe scripts/generate_icons.py --check # verify only .venv/Scripts/python.exe scripts/generate_icons.py --check # verify structure
Sources of truth — two axes, composed per target: Sources of truth — two axes, composed per target:
Girl art (vector): assets/nous-girl-black.svg (black positive space) Girl art (vector): assets/nous-girl-black.svg (black positive space)
@@ -12,27 +12,26 @@ Sources of truth — two axes, composed per target:
5487^2 viewBox, one path each). 5487^2 viewBox, one path each).
Backgrounds (per platform surface, light/dark): Backgrounds (per platform surface, light/dark):
assets/backgrounds/squircle-light.svg white rounded assets/backgrounds/squircle-light.svg white rounded
assets/backgrounds/squircle-dark.svg #0d1117 rounded assets/backgrounds/squircle-dark.svg #0d1117 rounded
assets/backgrounds/logo-frame.svg black frame +
white interior (website)
assets/backgrounds/logo-frame-dark.svg white frame +
#0d1117 interior
assets/backgrounds/tile-light.svg solid white (BrandMark)
assets/backgrounds/tile-dark.svg solid #0d1117
The master SVGs (assets/icon-master.svg light, assets/icon-master-dark.svg The master SVGs (assets/icon-master.svg light, assets/icon-master-dark.svg
dark) are GENERATED artifacts — squircle background + girl nested into the dark) are GENERATED artifacts — squircle background + girl nested into the
824px HIG content safe zone — not hand-edited sources. The 1024 master is 824px HIG content safe zone. The light master drives every squircle target;
what every squircle target renders from. the dark master drives the dark-appearance targets.
If the girl SVGs are missing a red-circle placeholder master is written so a The girl is nested via its art bbox as viewBox, so it always lands centered in
partial checkout still generates; --check then flags the drift. the box (824 safe zone for squircles / height-fitted for the marks) without
distortion. The girl art corners sit ~185px from the squircle arc centers vs
the 245px radius, so no art touches the rounded corners on any OS mask.
--check mode regenerates every target in memory and byte-compares it GENERATED OUTPUTS ARE NOT COMMITTED. Everything this script writes is
against the committed file, exiting nonzero on any drift. This is what gitignored and regenerated on demand by the consuming pipelines (website
CI runs (see .github/workflows/icons-freshness-check.yml) so the repo prebuild, desktop prebuild/predev, installer prebuild, web prebuild — via
can never hold a hand-edited or stale generated icon. scripts/generate-icons.mjs). The freshness lane (icons-freshness-check.yml)
runs --check, which regenerates in memory and asserts structural invariants
(sizes, transparency, container frame sets) — there are no committed bytes to
byte-compare against.
Rendering: resvg (resvg-py) for SVG -> PNG fidelity at every size. Rendering: resvg (resvg-py) for SVG -> PNG fidelity at every size.
Containers: Pillow for multi-size .ico and .icns. Containers: Pillow for multi-size .ico and .icns.
@@ -41,7 +40,7 @@ Deps:
Pillow (core dependency), resvg-py (dev extra): Pillow (core dependency), resvg-py (dev extra):
uv sync --extra dev uv sync --extra dev
Outputs (35 files): Outputs (30 files):
assets/icon-master.svg generated light master assets/icon-master.svg generated light master
assets/icon-master-dark.svg generated dark master assets/icon-master-dark.svg generated dark master
apps/desktop/assets/icon.png 1024x1024 squircle (light) apps/desktop/assets/icon.png 1024x1024 squircle (light)
@@ -54,21 +53,18 @@ Outputs (35 files):
apps/desktop/assets/appx/StoreLogo.png 50x50 squircle apps/desktop/assets/appx/StoreLogo.png 50x50 squircle
apps/desktop/assets/appx/Square44x44Logo.png 44x44 squircle apps/desktop/assets/appx/Square44x44Logo.png 44x44 squircle
apps/desktop/assets/appx/Square150x150Logo.png 150x150 squircle apps/desktop/assets/appx/Square150x150Logo.png 150x150 squircle
apps/desktop/assets/appx/*-dark.png dark-appearance logos (ready apps/desktop/assets/appx/*-dark.png dark-appearance logos
to wire into the MSIX
manifest when app-builder-lib
supports contrast images)
apps/desktop/public/apple-touch-icon.png 1024x1024 squircle apps/desktop/public/apple-touch-icon.png 1024x1024 squircle
apps/desktop/public/nous-girl.png 256x256 PNG, black girl on white (light) apps/desktop/public/nous-girl.png 256x256 squircle, black girl (light mark)
apps/desktop/public/nous-girl-dark.png 256x256 PNG, white girl on #0d1117 (dark) apps/desktop/public/nous-girl-dark.png 256x256 squircle, white girl (dark mark)
apps/bootstrap-installer/src-tauri/icons/32x32.png 32x32 apps/bootstrap-installer/src-tauri/icons/32x32.png 32x32
apps/bootstrap-installer/src-tauri/icons/128x128.png 128x128 apps/bootstrap-installer/src-tauri/icons/128x128.png 128x128
apps/bootstrap-installer/src-tauri/icons/128x128@2x.png 256x256 apps/bootstrap-installer/src-tauri/icons/128x128@2x.png 256x256
apps/bootstrap-installer/src-tauri/icons/icon.ico 16,32,64,128,256 apps/bootstrap-installer/src-tauri/icons/icon.ico 16,32,64,128,256
apps/bootstrap-installer/src-tauri/icons/icon.icns 16..1024 apps/bootstrap-installer/src-tauri/icons/icon.icns 16..1024
apps/bootstrap-installer/public/nous-girl.png 256x256 PNG, black girl on white (light) apps/bootstrap-installer/public/nous-girl.png 256x256 squircle mark (light)
website/static/img/logo.png 1772x1799 black-frame wordmark website/static/img/logo.png 1772x1799 girl alone, transparent (light)
website/static/img/logo-dark.png 1772x1799 white-frame wordmark website/static/img/logo-dark.png 1772x1799 girl alone, transparent (dark)
website/static/img/nous-logo.png 150x150 on white (opaque) website/static/img/nous-logo.png 150x150 on white (opaque)
website/static/img/nous-logo-dark.png 150x150 on #0d1117 (opaque) website/static/img/nous-logo-dark.png 150x150 on #0d1117 (opaque)
website/static/img/favicon-16x16.png 16x16 website/static/img/favicon-16x16.png 16x16
@@ -113,24 +109,41 @@ DARK_RGB = (13, 17, 23)
# Girl placement per background: (x, y, w, h) in that background's coordinate # Girl placement per background: (x, y, w, h) in that background's coordinate
# space. Squircle backgrounds put the girl in the 824px HIG content safe zone # space. Squircle backgrounds put the girl in the 824px HIG content safe zone
# (centered, 100px pad on a 1024 canvas); logo frames fill the white/dark # (centered, 100px pad on a 1024 canvas). Marks reuse the same squircles.
# interior at ~1.7x; BrandMark tiles take the girl height-fitted (meet).
GIRL_BOXES = { GIRL_BOXES = {
"squircle-light.svg": (100, 100, 824, 824), "squircle-light.svg": (100, 100, 824, 824),
"squircle-dark.svg": (100, 100, 824, 824), "squircle-dark.svg": (100, 100, 824, 824),
"logo-frame.svg": (14, 17, 1743, 1766),
"logo-frame-dark.svg": (14, 17, 1743, 1766),
"tile-light.svg": (0, 0, 256, 256),
"tile-dark.svg": (0, 0, 256, 256),
} }
# The brand-kit SVG canvas (both girl svgs share this viewBox). # The brand-kit SVG canvas (both girl svgs share this viewBox).
GIRL_VIEWBOX = 5487.0615 GIRL_VIEWBOX = 5487.0615
PLACEHOLDER = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"> # Target sizes for --check's structural verification: relpath -> (format, size)
<rect x="0" y="0" width="1024" height="1024" rx="{SQUIRCLE_RADIUS}" fill="#ffffff"/> CHECK_SIZES: dict[str, tuple[str, tuple[int, int]]] = {
<circle cx="512" cy="512" r="420" fill="#e34c4c"/> "apps/desktop/assets/icon.png": ("PNG", (1024, 1024)),
</svg> "apps/desktop/assets/icon-dark.png": ("PNG", (1024, 1024)),
''' "apps/desktop/assets/appx/Wide310x150Logo.png": ("PNG", (310, 150)),
"apps/desktop/assets/appx/Wide310x150Logo-dark.png": ("PNG", (310, 150)),
"apps/desktop/assets/appx/StoreLogo.png": ("PNG", (50, 50)),
"apps/desktop/assets/appx/StoreLogo-dark.png": ("PNG", (50, 50)),
"apps/desktop/assets/appx/Square44x44Logo.png": ("PNG", (44, 44)),
"apps/desktop/assets/appx/Square44x44Logo-dark.png": ("PNG", (44, 44)),
"apps/desktop/assets/appx/Square150x150Logo.png": ("PNG", (150, 150)),
"apps/desktop/assets/appx/Square150x150Logo-dark.png": ("PNG", (150, 150)),
"apps/desktop/public/apple-touch-icon.png": ("PNG", (1024, 1024)),
"apps/desktop/public/nous-girl.png": ("PNG", (256, 256)),
"apps/desktop/public/nous-girl-dark.png": ("PNG", (256, 256)),
"apps/bootstrap-installer/src-tauri/icons/32x32.png": ("PNG", (32, 32)),
"apps/bootstrap-installer/src-tauri/icons/128x128.png": ("PNG", (128, 128)),
"apps/bootstrap-installer/src-tauri/icons/128x128@2x.png": ("PNG", (256, 256)),
"apps/bootstrap-installer/public/nous-girl.png": ("PNG", (256, 256)),
"website/static/img/logo.png": ("PNG", (1772, 1799)),
"website/static/img/logo-dark.png": ("PNG", (1772, 1799)),
"website/static/img/nous-logo.png": ("PNG", (150, 150)),
"website/static/img/nous-logo-dark.png": ("PNG", (150, 150)),
"website/static/img/favicon-16x16.png": ("PNG", (16, 16)),
"website/static/img/favicon-32x32.png": ("PNG", (32, 32)),
"website/static/img/apple-touch-icon.png": ("PNG", (180, 180)),
}
# (relpath, kind, arg) # (relpath, kind, arg)
TARGETS: list[tuple[str, str, object]] = [ TARGETS: list[tuple[str, str, object]] = [
@@ -216,7 +229,7 @@ def girl_layer(girl: str, box: tuple[float, float, float, float]) -> str:
def background_inner(name: str) -> tuple[str, int, int]: def background_inner(name: str) -> tuple[str, int, int]:
"""Inner content + (width, height) of a background SVG asset.""" """Inner content + (width, height) of a background SVG asset."""
text = (BACKGROUNDS / name).read_text(encoding="utf-8") text = (BACKGROUNDS / name).read_text(encoding="utf-8")
m = re.search(r"<svg\b[^>]*viewBox=\"0 0 (\d+(?:\.\d+)?) (\d+(?:\.\d+)?)\"[^>]*>", text) m = re.search(r'<svg\b[^>]*viewBox="0 0 (\d+(?:\.\d+)?) (\d+(?:\.\d+)?)"[^>]*>', text)
assert m, f"cannot parse viewBox of {name}" assert m, f"cannot parse viewBox of {name}"
w, h = float(m.group(1)), float(m.group(2)) w, h = float(m.group(1)), float(m.group(2))
inner = re.sub(r"^.*?>\s*", "", text, count=1, flags=re.S) inner = re.sub(r"^.*?>\s*", "", text, count=1, flags=re.S)
@@ -224,14 +237,15 @@ def background_inner(name: str) -> tuple[str, int, int]:
return inner, int(w), int(h) return inner, int(w), int(h)
def compose_master(girl: str, bg: str) -> str: def compose_svg(girl: str, bg: str) -> str:
"""Composed master SVG: background + girl layer.""" """Full svg text: background + girl layer, in the background's native
coordinate space (resvg scales to whatever output size is requested, so
the composition is size-agnostic — no manual box scaling)."""
inner, w, h = background_inner(bg) inner, w, h = background_inner(bg)
x, y, bx, bh = GIRL_BOXES[bg]
return ( return (
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}">\n' f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}">\n'
f" {inner.strip()}\n" f" {inner.strip()}\n"
f" {girl_layer(girl, (x, y, bx, bh))}\n" f" {girl_layer(girl, GIRL_BOXES[bg])}\n"
"</svg>\n" "</svg>\n"
) )
@@ -243,12 +257,17 @@ def ensure_masters(check: bool = False) -> None:
if missing: if missing:
if check: if check:
sys.exit(f"[check] girl art missing: {[p.name for p in missing]}") sys.exit(f"[check] girl art missing: {[p.name for p in missing]}")
for path, content in ((MASTER, PLACEHOLDER), (MASTER_DARK, PLACEHOLDER)): placeholder = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
path.write_text(content, encoding="utf-8") <rect x="0" y="0" width="1024" height="1024" rx="{SQUIRCLE_RADIUS}" fill="#ffffff"/>
<circle cx="512" cy="512" r="420" fill="#e34c4c"/>
</svg>
'''
for path in (MASTER, MASTER_DARK):
path.write_text(placeholder, encoding="utf-8")
print(f"[master] wrote placeholder masters ({[p.name for p in missing]})") print(f"[master] wrote placeholder masters ({[p.name for p in missing]})")
return return
MASTER.write_text(compose_master("black", "squircle-light.svg"), encoding="utf-8") MASTER.write_text(compose_svg("black", "squircle-light.svg"), encoding="utf-8")
MASTER_DARK.write_text(compose_master("white", "squircle-dark.svg"), encoding="utf-8") MASTER_DARK.write_text(compose_svg("white", "squircle-dark.svg"), encoding="utf-8")
# ─── rendering ────────────────────────────────────────────────────────────── # ─── rendering ──────────────────────────────────────────────────────────────
@@ -261,67 +280,56 @@ def render(master: Path, size: int, *, background: str | None = None) -> Image.I
return Image.open(io.BytesIO(data)).convert("RGBA") return Image.open(io.BytesIO(data)).convert("RGBA")
def render_svg(svg: str, size: int | tuple[int, int]) -> Image.Image:
if isinstance(size, int):
w = h = size
else:
w, h = size
data = resvg_py.svg_to_bytes(svg_string=svg, width=w, height=h)
return Image.open(io.BytesIO(data)).convert("RGBA")
def paste_centered(canvas: Image.Image, img: Image.Image) -> None: def paste_centered(canvas: Image.Image, img: Image.Image) -> None:
x = (canvas.width - img.width) // 2 x = (canvas.width - img.width) // 2
y = (canvas.height - img.height) // 2 y = (canvas.height - img.height) // 2
canvas.alpha_composite(img, (x, y)) canvas.alpha_composite(img, (x, y))
def girl_mark(kind: str, size: int) -> Image.Image: def save_png(img: Image.Image, buf: io.BytesIO) -> None:
"""Composite the girl SVG onto a fixed tile (BrandMark asset), rendered """Save with alpha preserved. Flat art quantizes losslessly to an 8-bit
straight from the vector art (crisp at any size). palette (tRNS per-index alpha keeps the AA edges), so use that when the
palette round-trips pixel-identically; fall back to RGBA otherwise."""
girl_light: black girl on white tile. girl_dark: white girl on #0d1117. if img.mode != "RGBA":
""" img.convert("RGBA").save(buf, "PNG", optimize=True)
if kind == "girl_light": return
girl, bg = "black", "tile-light.svg" quantized = img.quantize(colors=256, method=Image.FASTOCTREE, dither=Image.NONE)
if quantized.convert("RGBA").tobytes() == img.tobytes():
quantized.save(buf, "PNG", optimize=True)
else: else:
girl, bg = "white", "tile-dark.svg" img.save(buf, "PNG", optimize=True)
def girl_mark(kind: str, size: int) -> Image.Image:
"""The girl in the app-icon squircle (BrandMark asset) — the mark IS the
icon shape. girl_light: black girl on white squircle. girl_dark: white
girl on #0d1117 squircle."""
if kind == "girl_light":
girl, bg = "black", "squircle-light.svg"
else:
girl, bg = "white", "squircle-dark.svg"
if not GIRLS[girl].exists(): if not GIRLS[girl].exists():
print(f" [girl] {GIRLS[girl].name} missing — placeholder fallback") print(f" [girl] {GIRLS[girl].name} missing — placeholder fallback")
return render(MASTER if kind == "girl_light" else MASTER_DARK, size) return render(MASTER if kind == "girl_light" else MASTER_DARK, size)
return render_svg(compose_svg(girl, bg), size)
inner, w, h = background_inner(bg)
svg = (
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}">\n'
f" {inner.strip()}\n"
f" {girl_layer(girl, GIRL_BOXES[bg])}\n"
"</svg>\n"
)
data = resvg_py.svg_to_bytes(svg_string=svg, width=size, height=size)
return Image.open(io.BytesIO(data)).convert("RGBA")
def build_logo_image(master: Path, dark: bool = False) -> Image.Image: def build_logo_image(master: Path, dark: bool = False) -> Image.Image:
"""1772x1799 wordmark: frame (light: black / dark: white, ~14-17px) + """1772x1799 wordmark: the girl alone on transparency (no frame), centered.
interior (light: white / dark: #0d1117) + art at ~1.7x. Light = black girl, dark = white girl — the consuming surface's background
(navbar light/dark) shows through."""
Measured from the real logo.png: border L/R 14, T 17, B 16; interior
spans (15,18)..(1758,1784) = 1743x1766.
"""
from PIL import ImageDraw
W, H = 1772, 1799 W, H = 1772, 1799
art_w, art_h = 1743, 1766 svg = f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}">\n {girl_layer("white" if dark else "black", (0, 0, W, H))}\n</svg>\n'
border = 14 return render_svg(svg, (W, H))
top, bottom = 17, 16
if dark:
frame, interior = DARK_RGB, (255, 255, 255)
else:
frame, interior = (0, 0, 0), (255, 255, 255)
canvas = Image.new("RGBA", (W, H), (0, 0, 0, 0))
draw = ImageDraw.Draw(canvas)
draw.rounded_rectangle((0, 0, W - 1, H - 1), radius=2, fill=frame)
draw.rectangle(
(border, top, W - 1 - border, H - 1 - bottom),
fill=interior,
)
# Art: render the master square at ~1.7x, tiny vertical stretch to fill.
art = render(master, art_w).resize((art_w, art_h), Image.LANCZOS)
canvas.alpha_composite(art, (border, top))
return canvas
def target_bytes(kind: str, arg: object) -> bytes: def target_bytes(kind: str, arg: object) -> bytes:
@@ -335,23 +343,15 @@ def target_bytes(kind: str, arg: object) -> bytes:
buf = io.BytesIO() buf = io.BytesIO()
if kind == "png": if kind == "png":
render(MASTER, arg).save(buf, "PNG") save_png(render(MASTER, arg), buf)
elif kind == "png_dark": elif kind == "png_dark":
render(MASTER_DARK, arg).save(buf, "PNG") save_png(render(MASTER_DARK, arg), buf)
elif kind == "png_white": elif kind == "png_white":
render(MASTER, arg, background="#ffffff").save(buf, "PNG") render(MASTER, arg, background="#ffffff").convert("RGB").save(buf, "PNG", optimize=True)
elif kind == "png_dark_white": elif kind == "png_dark_white":
render(MASTER_DARK, arg, background=DARK_HEX).save(buf, "PNG") render(MASTER_DARK, arg, background=DARK_HEX).convert("RGB").save(buf, "PNG", optimize=True)
elif kind == "girl_light" or kind == "girl_dark": elif kind in ("girl_light", "girl_dark"):
# Flat 2-tone vector-style art: <256 distinct colors, so an 8-bit save_png(girl_mark(kind, arg), buf)
# palette PNG holds it pixel-identically. Saved with compress_level=0
# (stored deflate blocks): the icons-freshness lane byte-compares
# against an ubuntu regen, and compressed PNG bytes drift per host
# zlib — stored blocks are spec-locked, identical on every platform.
# ~66KB vs ~15KB compressed is a non-issue for a bundled asset.
girl_mark(kind, arg).convert("RGB").convert("P", palette=Image.ADAPTIVE, colors=256).save(
buf, "PNG", compress_level=0
)
elif kind == "ico": elif kind == "ico":
img = render(MASTER, max(arg)) img = render(MASTER, max(arg))
img.save(buf, format="ICO", sizes=[(s, s) for s in arg]) img.save(buf, format="ICO", sizes=[(s, s) for s in arg])
@@ -424,33 +424,84 @@ def cmd_write() -> None:
def cmd_check() -> int: def cmd_check() -> int:
"""Structural verification: regenerate every target in memory and assert
the invariants that actually matter (there are no committed bytes to
byte-compare — outputs are generated on demand)."""
ensure_masters(check=True) ensure_masters(check=True)
drifted: list[str] = [] problems: list[str] = []
# every target must generate without error
for rel, kind, arg in TARGETS: for rel, kind, arg in TARGETS:
try:
target_bytes(kind, arg)
except Exception as exc: # noqa: BLE001
problems.append(f"{rel}: REGENERATE FAILED ({exc})")
# PNG targets must have the expected format + size
for rel, (fmt, size) in CHECK_SIZES.items():
path = ROOT / rel path = ROOT / rel
if not path.exists(): if not path.exists():
drifted.append(f"{rel}: MISSING (expected generated file)") problems.append(f"{rel}: MISSING (expected generated file)")
continue continue
try: try:
expected = target_bytes(kind, arg) im = Image.open(path)
if im.format != fmt or im.size != size:
problems.append(f"{rel}: got {im.format} {im.size}, expected {fmt} {size}")
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
drifted.append(f"{rel}: REGENERATE FAILED ({exc})") problems.append(f"{rel}: UNREADABLE ({exc})")
continue
actual = path.read_bytes()
if actual != expected:
drifted.append(f"{rel}: drift ({len(actual)} bytes on disk vs {len(expected)} generated)")
if not drifted: # squircles must keep transparent corners (alpha extrema include 0)
print(f"[ok] all {len(TARGETS)} generated icons match the girl art + backgrounds") for rel in (
"apps/desktop/assets/icon.png",
"apps/desktop/assets/icon-dark.png",
"apps/desktop/public/nous-girl.png",
"apps/desktop/public/nous-girl-dark.png",
"apps/desktop/public/apple-touch-icon.png",
):
path = ROOT / rel
if not path.exists():
continue
alpha = Image.open(path).convert("RGBA").getchannel("A")
lo, hi = alpha.getextrema()
if lo != 0 or hi != 255:
problems.append(f"{rel}: alpha extrema {alpha.getextrema()}, expected (0, 255) transparent corners")
# containers must have the right frame sets (parse ICO headers directly —
# PIL's ICO n_frames is unreliable across versions)
def ico_sizes(path: Path) -> list[int]:
data = path.read_bytes()
count = int.from_bytes(data[4:6], "little")
sizes = []
for i in range(count):
entry = data[6 + i * 16 : 6 + (i + 1) * 16]
w = entry[0] or 256
h = entry[1] or 256
sizes.append(w)
return sorted(set(sizes))
for rel, sizes in (
("apps/desktop/assets/icon.ico", [16, 24, 32, 48, 64, 128, 256]),
("apps/desktop/assets/icon-dark.ico", [16, 24, 32, 48, 64, 128, 256]),
("apps/bootstrap-installer/src-tauri/icons/icon.ico", [16, 32, 64, 128, 256]),
):
path = ROOT / rel
if not path.exists():
problems.append(f"{rel}: MISSING")
continue
try:
got = ico_sizes(path)
if got != sizes:
problems.append(f"{rel}: ICO frames {got}, expected {sizes}")
except Exception as exc: # noqa: BLE001
problems.append(f"{rel}: UNREADABLE ({exc})")
if not problems:
print(f"[ok] all {len(TARGETS)} targets generate and pass structural checks")
return 0 return 0
print(f"[check] {len(drifted)} file(s) out of sync:") print(f"[check] {len(problems)} problem(s):")
for line in drifted: for line in problems:
print(f" - {line}") print(f" - {line}")
print(
"\nFix: run `.venv/Scripts/python.exe scripts/generate_icons.py` "
"and commit the regenerated files."
)
return 1 return 1

View File

@@ -5,6 +5,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"prebuild": "node ../scripts/generate-icons.mjs",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"lint": "eslint .", "lint": "eslint .",
"lint:fix": "eslint . --fix", "lint:fix": "eslint . --fix",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -29,9 +29,11 @@ import { fileURLToPath } from "node:url";
const scriptDir = dirname(fileURLToPath(import.meta.url)); const scriptDir = dirname(fileURLToPath(import.meta.url));
const websiteDir = resolve(scriptDir, ".."); const websiteDir = resolve(scriptDir, "..");
const repoRoot = resolve(websiteDir, "..");
const extractScript = join(scriptDir, "extract-skills.py"); const extractScript = join(scriptDir, "extract-skills.py");
const llmsScript = join(scriptDir, "generate-llms-txt.py"); const llmsScript = join(scriptDir, "generate-llms-txt.py");
const cronBlueprintsScript = join(scriptDir, "extract-automation-blueprints.py"); const cronBlueprintsScript = join(scriptDir, "extract-automation-blueprints.py");
const iconGenScript = join(repoRoot, "scripts", "generate-icons.mjs");
const outputFile = join(websiteDir, "static", "api", "skills.json"); const outputFile = join(websiteDir, "static", "api", "skills.json");
const unifiedIndexFile = join(websiteDir, "static", "api", "skills-index.json"); const unifiedIndexFile = join(websiteDir, "static", "api", "skills-index.json");
const UNIFIED_INDEX_URL = const UNIFIED_INDEX_URL =
@@ -119,7 +121,19 @@ async function ensureUnifiedIndex() {
} }
} }
// 0) Pull unified index if we don't have a fresh one. // 0) Icon assets — the navbar logo, favicons, and apple-touch-icon are
// generated (not committed). This must fail loudly: a docs build without the
// logo ships a broken navbar.
console.log("[prebuild] generating icon assets…");
{
const r = spawnSync("node", [iconGenScript], { stdio: "inherit", cwd: repoRoot });
if (r.status !== 0) {
console.error("[prebuild] icon generation failed — install the dev extra with `uv sync --extra dev`");
process.exit(1);
}
}
// 0b) Pull unified index if we don't have a fresh one.
await ensureUnifiedIndex(); await ensureUnifiedIndex();
// 1) skills.json — required for the Skills Hub page. // 1) skills.json — required for the Skills Hub page.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 491 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB