diff --git a/.github/workflows/icons-freshness-check.yml b/.github/workflows/icons-freshness-check.yml index d3d1fe9f5c..7881afac19 100644 --- a/.github/workflows/icons-freshness-check.yml +++ b/.github/workflows/icons-freshness-check.yml @@ -2,11 +2,12 @@ name: Icon Assets Freshness # Guard for the icon pipeline. The sources of truth are the nous-girl SVGs # (assets/nous-girl-*.svg) and the per-platform backgrounds -# (assets/backgrounds/*.svg); assets/icon-master*.svg and every raster target -# are generated from those (see scripts/generate_icons.py). This check -# regenerates everything and byte-compares against the committed files. The -# repo can never hold a hand-edited or stale generated icon, and an art or -# background change can never land without its regenerated outputs. +# (assets/backgrounds/*.svg). Generated outputs are NOT committed — every +# consuming pipeline regenerates them via scripts/generate-icons.mjs — so +# this lane runs --check, which regenerates every target in memory and +# asserts structural invariants (sizes, transparent squircle corners, +# 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 # host for pure-shape artwork. If the master ever contains , font diff --git a/.gitignore b/.gitignore index 86d1fa906f..f545754285 100644 --- a/.gitignore +++ b/.gitignore @@ -1,359 +1,389 @@ - -!apps/desktop/src/global.d.ts -!apps/desktop/src/plugins/*/plugin.js -!apps/desktop/src/vite-env.d.ts -!hermes_cli/data/ -!hermes_cli/data/plugin_index.json -# — ignore so `git status` stays clean and update's autostash skips them. -# (launch-time stale-bytecode sweep). Runtime state, never a code change. -# `data/` pattern above would otherwise swallow it. -# `npm run sync-assets` (see web/package.json). -# 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). -# automation-blueprints-index.json is a build artifact emitted by -# bootstrap installer. It is Hermes-managed runtime state, never a code change — -# 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 accident via 3a69e34702, removed in the #72002 salvage). -# Checkout fingerprint the __pycache__ tree was last validated against -# CLI config (may contain sensitive SSH paths) -# committed to the repo root. See the hermes-release skill. -# Cross-process web UI build lock (flock target, always empty) -# cut (passed to `gh release create --notes-file`); the GitHub Release itself -# Desktop demo-run scratch output (hermes writes demo/*.txt during recorded -# 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 -# every build). -# EXCEPT bundled plain-ESM plugin entries (adopted SDK-consumer plugins, -# 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 -# image-provider (fal.media) URL — they are NEVER committed to the repo. The -# infographic-check CI job is what actually enforces this. -# Installer-written method stamp in the managed checkout root (scripts/install.sh). -# interrupted; consumed by launch-time recovery. Never commit it (was tracked -# Interrupted-update breadcrumb + recovery lock written next to the shared venv -# Local editor / agent tooling (machine-specific; keep in global config, not the repo) -# logs, and per-session caches are never artifacts of the codebase. -# Nix -# No trailing slash: also matches node_modules SYMLINKS (worktrees often -# Per-release changelog drafts. These exist only transiently during a release -# Persistent dev sandbox dir (scripts/dev-sandbox.sh --persistent) -# Playwright visual regression baselines — cached from main in CI, not committed -# 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-infographic-workflow reference (storage rule + lapse #8 / #COMMIT-1). -# Private keys -# regenerated by scripts/tool_search_livetest.py. Never an artifact of the repo. -# Release script temp files -# Repo-root build/debug artifacts that must never be committed -# resolves the stale .js OVER the .tsx — never track these) -# 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 -# Sandbox dirs used by the install/update E2E (tests/install/). The suffix is -# sibling exists, so the stale-shadow hazard above cannot apply. -# sidestepped by an `infograficos/` directory (#70552). .gitignore is only -# Skills Hub state (lives in ~/.hermes/skills/.hub/ at runtime, but just in case) -# skills.json + skills-meta.json are build artifacts emitted by -# slip into a commit and break `npm ci` on CI with ENOTDIR). -# Spelling variants are listed because a single `infographic/` pattern was -# 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 -# 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. -# Tool Search live-test harness output — non-deterministic model transcripts, -# 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 -# walkthroughs). Throwaway artifacts, never part of the app. -# Web UI assets — synced from @nous-research/ui at build time via -# Web UI build output -# website/scripts/extract-automation-blueprints.py during prebuild. -# website/scripts/extract-skills.py during prebuild — keep them out of -# Working directory for the Hermes Agent's session state (~/.hermes/ at runtime; -# -%SystemDrive%/ -*.pem -*.ppk -*.pyc* -*.tsbuildinfo -*-snapshots/ -.act-sandbox-agent.* -.bytecode-fingerprint -.bytecode-fingerprint.tmp -.codex/ -.cursor/ -.direnv/ -.DS_Store -.env -.env.development -.env.development.local -.env.local -.env.production.local -.env.test -.env.test.local -.gemini/ -.hermes/ -.hermes-bootstrap-complete -.hermes-docker/ -.hermes-sandbox/ -.hermes-sandbox-e2e*/ -.lazy-refresh-incomplete -.mcp.json -.nix-stamps/ -.notebooklm-cli-venv/ -.notebooklm-home/ -.notebooklm-playwright/ -.op.env -.pip-cache/ -.pytest_cache/ -.pytest-cache/ -.release_notes.md -.skills_prompt_snapshot.json -.update-incomplete -.update-incomplete.lock -.uv-cache/ -.venv -.venv/ -.vscode/ -.web_ui_build.lock -.worktrees/ -.zed/ -/*.png.bak -/.hermes-runtime/ -/.install_method -/_pycache/ -/bin/ -/default.tar.gz -/log.txt -/sqlite_leak_fix.png -/venv.old/ -/venv.stale.runtime-*/ -/venv/ -__pycache__/ -__pycache__/model_tools.cpython-310.pyc -__pycache__/web_tools.cpython-310.pyc -act/ -agent-browser/ -apps/desktop/build/ -apps/desktop/demo/ -apps/desktop/dist/ -apps/desktop/release/ -apps/desktop/src/**/*.d.ts -apps/desktop/src/**/*.js -apps/desktop/src/**/*.js.map -apps/shared/src/**/*.d.ts -apps/shared/src/**/*.js -apps/shared/src/**/*.js.map -browser-use/ -cli-config.yaml -compose.hermes.local.yml -config/mcporter.json -data/ -data/* -docs/superpowers/* -environments/benchmarks/evals/ -examples/ -export* -hermes-*/* -hermes_agent.egg-info/ -hermes_cli/scripts/ -hermes_cli/tui_dist/* -hermes_cli/web_dist/ -ignored/ -images/ -infografico/ -infograficos/ -infographic/ -infographics/ -logs/ -mini-swe-agent/ -models-dev-upstream/ -native/fts5_cjk/*.so -node_modules -opencode.json -playwright-report/ -privvy* -RELEASE_v*.md -result -run_datagen_kimik2-thinking.sh -run_datagen_megascience_glm4-6.sh -run_datagen_sonnet.sh -source-data/* -run_datagen_megascience_glm4-6.sh -data/* -# No trailing slash: also matches node_modules SYMLINKS (worktrees often -# 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). -node_modules -browser-use/ -agent-browser/ -# Private keys -*.ppk -*.pem -privvy* -images/ -__pycache__/ -hermes_agent.egg-info/ -wandb/ -testlogs -playwright-report/ -test-results/ -# Playwright visual regression baselines — cached from main in CI, not committed -*-snapshots/ - -# CLI config (may contain sensitive SSH paths) -cli-config.yaml - -# Skills Hub state (lives in ~/.hermes/skills/.hub/ at runtime, but just in case) -skills/.hub/ -ignored/ -.worktrees/ -environments/benchmarks/evals/ - -# Web UI build output -hermes_cli/web_dist/ -# Cross-process web UI build lock (flock target, always empty) -.web_ui_build.lock -apps/desktop/build/ -apps/desktop/dist/ - -# tsc-emitted artifacts (a stray `tsc -b` compiles into src/, and vite then -# resolves the stale .js OVER the .tsx — never track these) -apps/desktop/src/**/*.js -apps/desktop/src/**/*.js.map -apps/desktop/src/**/*.d.ts -# EXCEPT bundled plain-ESM plugin entries (adopted SDK-consumer plugins, -# e.g. hermes-bots): plugin.js IS the source, not tsc output. No .tsx -# sibling exists, so the stale-shadow hazard above cannot apply. -!apps/desktop/src/plugins/*/plugin.js -!apps/desktop/src/global.d.ts -!apps/desktop/src/vite-env.d.ts - -# Build/debug artifacts that must never be committed -/log.txt -/sqlite_leak_fix.png -/*.png.bak -*.tar.gz -*.tgz -apps/shared/src/**/*.js -apps/shared/src/**/*.js.map -apps/shared/src/**/*.d.ts -apps/desktop/release/ -# stage-and-swap Desktop rebuild output (#86443); removed after the swap, but -# a killed build must not leave the checkout dirty -apps/desktop/.staging-*/ -*.tsbuildinfo - -# Web UI assets — synced from @nous-research/ui at build time via -# `npm run sync-assets` (see web/package.json). -web/public/fonts/ -web/public/ds-assets/ - -# Release script temp files -.release_notes.md -mini-swe-agent/ - -# Nix -.direnv/ -.nix-stamps/ -result -website/static/api/skills-index.json -# skills.json + skills-meta.json are build artifacts emitted by -# website/scripts/extract-skills.py during prebuild — keep them out of -# git for the same reason as skills-index.json (large, generated, change -# every build). -website/static/api/skills.json + +!apps/desktop/src/global.d.ts +!apps/desktop/src/plugins/*/plugin.js +!apps/desktop/src/vite-env.d.ts +!hermes_cli/data/ +!hermes_cli/data/plugin_index.json +# — ignore so `git status` stays clean and update's autostash skips them. +# (launch-time stale-bytecode sweep). Runtime state, never a code change. +# `data/` pattern above would otherwise swallow it. +# `npm run sync-assets` (see web/package.json). +# 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). +# automation-blueprints-index.json is a build artifact emitted by +# bootstrap installer. It is Hermes-managed runtime state, never a code change — +# 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 accident via 3a69e34702, removed in the #72002 salvage). +# Checkout fingerprint the __pycache__ tree was last validated against +# CLI config (may contain sensitive SSH paths) +# committed to the repo root. See the hermes-release skill. +# Cross-process web UI build lock (flock target, always empty) +# cut (passed to `gh release create --notes-file`); the GitHub Release itself +# Desktop demo-run scratch output (hermes writes demo/*.txt during recorded +# 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 +# every build). +# EXCEPT bundled plain-ESM plugin entries (adopted SDK-consumer plugins, +# 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 +# image-provider (fal.media) URL — they are NEVER committed to the repo. The +# infographic-check CI job is what actually enforces this. +# Installer-written method stamp in the managed checkout root (scripts/install.sh). +# interrupted; consumed by launch-time recovery. Never commit it (was tracked +# Interrupted-update breadcrumb + recovery lock written next to the shared venv +# Local editor / agent tooling (machine-specific; keep in global config, not the repo) +# logs, and per-session caches are never artifacts of the codebase. +# Nix +# No trailing slash: also matches node_modules SYMLINKS (worktrees often +# Per-release changelog drafts. These exist only transiently during a release +# Persistent dev sandbox dir (scripts/dev-sandbox.sh --persistent) +# Playwright visual regression baselines — cached from main in CI, not committed +# 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-infographic-workflow reference (storage rule + lapse #8 / #COMMIT-1). +# Private keys +# regenerated by scripts/tool_search_livetest.py. Never an artifact of the repo. +# Release script temp files +# Repo-root build/debug artifacts that must never be committed +# resolves the stale .js OVER the .tsx — never track these) +# 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 +# Sandbox dirs used by the install/update E2E (tests/install/). The suffix is +# sibling exists, so the stale-shadow hazard above cannot apply. +# sidestepped by an `infograficos/` directory (#70552). .gitignore is only +# Skills Hub state (lives in ~/.hermes/skills/.hub/ at runtime, but just in case) +# skills.json + skills-meta.json are build artifacts emitted by +# slip into a commit and break `npm ci` on CI with ENOTDIR). +# Spelling variants are listed because a single `infographic/` pattern was +# 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 +# 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. +# Tool Search live-test harness output — non-deterministic model transcripts, +# 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 +# walkthroughs). Throwaway artifacts, never part of the app. +# Web UI assets — synced from @nous-research/ui at build time via +# Web UI build output +# website/scripts/extract-automation-blueprints.py during prebuild. +# website/scripts/extract-skills.py during prebuild — keep them out of +# Working directory for the Hermes Agent's session state (~/.hermes/ at runtime; +# +%SystemDrive%/ +*.pem +*.ppk +*.pyc* +*.tsbuildinfo +*-snapshots/ +.act-sandbox-agent.* +.bytecode-fingerprint +.bytecode-fingerprint.tmp +.codex/ +.cursor/ +.direnv/ +.DS_Store +.env +.env.development +.env.development.local +.env.local +.env.production.local +.env.test +.env.test.local +.gemini/ +.hermes/ +.hermes-bootstrap-complete +.hermes-docker/ +.hermes-sandbox/ +.hermes-sandbox-e2e*/ +.lazy-refresh-incomplete +.mcp.json +.nix-stamps/ +.notebooklm-cli-venv/ +.notebooklm-home/ +.notebooklm-playwright/ +.op.env +.pip-cache/ +.pytest_cache/ +.pytest-cache/ +.release_notes.md +.skills_prompt_snapshot.json +.update-incomplete +.update-incomplete.lock +.uv-cache/ +.venv +.venv/ +.vscode/ +.web_ui_build.lock +.worktrees/ +.zed/ +/*.png.bak +/.hermes-runtime/ +/.install_method +/_pycache/ +/bin/ +/default.tar.gz +/log.txt +/sqlite_leak_fix.png +/venv.old/ +/venv.stale.runtime-*/ +/venv/ +__pycache__/ +__pycache__/model_tools.cpython-310.pyc +__pycache__/web_tools.cpython-310.pyc +act/ +agent-browser/ +apps/desktop/build/ +apps/desktop/demo/ +apps/desktop/dist/ +apps/desktop/release/ +apps/desktop/src/**/*.d.ts +apps/desktop/src/**/*.js +apps/desktop/src/**/*.js.map +apps/shared/src/**/*.d.ts +apps/shared/src/**/*.js +apps/shared/src/**/*.js.map +browser-use/ +cli-config.yaml +compose.hermes.local.yml +config/mcporter.json +data/ +data/* +docs/superpowers/* +environments/benchmarks/evals/ +examples/ +export* +hermes-*/* +hermes_agent.egg-info/ +hermes_cli/scripts/ +hermes_cli/tui_dist/* +hermes_cli/web_dist/ +ignored/ +images/ +infografico/ +infograficos/ +infographic/ +infographics/ +logs/ +mini-swe-agent/ +models-dev-upstream/ +native/fts5_cjk/*.so +node_modules +opencode.json +playwright-report/ +privvy* +RELEASE_v*.md +result +run_datagen_kimik2-thinking.sh +run_datagen_megascience_glm4-6.sh +run_datagen_sonnet.sh +source-data/* +run_datagen_megascience_glm4-6.sh +data/* +# No trailing slash: also matches node_modules SYMLINKS (worktrees often +# 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). +node_modules +browser-use/ +agent-browser/ +# Private keys +*.ppk +*.pem +privvy* +images/ +__pycache__/ +hermes_agent.egg-info/ +wandb/ +testlogs +playwright-report/ +test-results/ +# Playwright visual regression baselines — cached from main in CI, not committed +*-snapshots/ + +# CLI config (may contain sensitive SSH paths) +cli-config.yaml + +# Skills Hub state (lives in ~/.hermes/skills/.hub/ at runtime, but just in case) +skills/.hub/ +ignored/ +.worktrees/ +environments/benchmarks/evals/ + +# Web UI build output +hermes_cli/web_dist/ +# Cross-process web UI build lock (flock target, always empty) +.web_ui_build.lock +apps/desktop/build/ +apps/desktop/dist/ + +# tsc-emitted artifacts (a stray `tsc -b` compiles into src/, and vite then +# resolves the stale .js OVER the .tsx — never track these) +apps/desktop/src/**/*.js +apps/desktop/src/**/*.js.map +apps/desktop/src/**/*.d.ts +# EXCEPT bundled plain-ESM plugin entries (adopted SDK-consumer plugins, +# e.g. hermes-bots): plugin.js IS the source, not tsc output. No .tsx +# sibling exists, so the stale-shadow hazard above cannot apply. +!apps/desktop/src/plugins/*/plugin.js +!apps/desktop/src/global.d.ts +!apps/desktop/src/vite-env.d.ts + +# Build/debug artifacts that must never be committed +/log.txt +/sqlite_leak_fix.png +/*.png.bak +*.tar.gz +*.tgz +apps/shared/src/**/*.js +apps/shared/src/**/*.js.map +apps/shared/src/**/*.d.ts +apps/desktop/release/ +# stage-and-swap Desktop rebuild output (#86443); removed after the swap, but +# a killed build must not leave the checkout dirty +apps/desktop/.staging-*/ +*.tsbuildinfo + +# Web UI assets — synced from @nous-research/ui at build time via +# `npm run sync-assets` (see web/package.json). +web/public/fonts/ +web/public/ds-assets/ + +# Release script temp files +.release_notes.md +mini-swe-agent/ + +# Nix +.direnv/ +.nix-stamps/ +result +website/static/api/skills-index.json +# skills.json + skills-meta.json are build artifacts emitted by +# website/scripts/extract-skills.py during prebuild — keep them out of +# git for the same reason as skills-index.json (large, generated, change +# every build). +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 -# 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 \ No newline at end of file +# ── generated icon assets (see scripts/generate_icons.py) ────────────────── +# Regenerated on demand by every consuming pipeline (website/desktop/ +# installer/web prebuild via scripts/generate-icons.mjs). Sources of truth: +# assets/nous-girl-*.svg + assets/backgrounds/. +assets/icon-master.svg +assets/icon-master-dark.svg +apps/desktop/assets/icon.png +apps/desktop/assets/icon.ico +apps/desktop/assets/icon.icns +apps/desktop/assets/icon-dark.png +apps/desktop/assets/icon-dark.ico +apps/desktop/assets/icon-dark.icns +apps/desktop/assets/appx/*.png +apps/desktop/public/apple-touch-icon.png +apps/desktop/public/nous-girl.png +apps/desktop/public/nous-girl-dark.png +apps/bootstrap-installer/src-tauri/icons/*.png +apps/bootstrap-installer/src-tauri/icons/icon.ico +apps/bootstrap-installer/src-tauri/icons/icon.icns +apps/bootstrap-installer/public/nous-girl.png +website/static/img/logo.png +website/static/img/logo-dark.png +website/static/img/nous-logo.png +website/static/img/nous-logo-dark.png +website/static/img/favicon-16x16.png +website/static/img/favicon-32x32.png +website/static/img/apple-touch-icon.png +website/static/img/favicon.ico +website/static/img/favicon.svg +web/public/favicon.ico diff --git a/apps/bootstrap-installer/package.json b/apps/bootstrap-installer/package.json index c385f9b108..d94bc17e41 100644 --- a/apps/bootstrap-installer/package.json +++ b/apps/bootstrap-installer/package.json @@ -6,6 +6,7 @@ "type": "module", "scripts": { "dev": "vite --host 127.0.0.1 --port 5175", + "prebuild": "node ../../scripts/generate-icons.mjs", "build": "tsc -b && vite build", "preview": "vite preview", "tauri": "tauri", diff --git a/apps/bootstrap-installer/public/nous-girl.png b/apps/bootstrap-installer/public/nous-girl.png deleted file mode 100644 index d6d763b391..0000000000 Binary files a/apps/bootstrap-installer/public/nous-girl.png and /dev/null differ diff --git a/apps/bootstrap-installer/src-tauri/icons/128x128.png b/apps/bootstrap-installer/src-tauri/icons/128x128.png deleted file mode 100644 index 550a891650..0000000000 Binary files a/apps/bootstrap-installer/src-tauri/icons/128x128.png and /dev/null differ diff --git a/apps/bootstrap-installer/src-tauri/icons/128x128@2x.png b/apps/bootstrap-installer/src-tauri/icons/128x128@2x.png deleted file mode 100644 index 8da9354cd6..0000000000 Binary files a/apps/bootstrap-installer/src-tauri/icons/128x128@2x.png and /dev/null differ diff --git a/apps/bootstrap-installer/src-tauri/icons/32x32.png b/apps/bootstrap-installer/src-tauri/icons/32x32.png deleted file mode 100644 index aa63f12148..0000000000 Binary files a/apps/bootstrap-installer/src-tauri/icons/32x32.png and /dev/null differ diff --git a/apps/bootstrap-installer/src-tauri/icons/icon.icns b/apps/bootstrap-installer/src-tauri/icons/icon.icns deleted file mode 100644 index 53910bb695..0000000000 Binary files a/apps/bootstrap-installer/src-tauri/icons/icon.icns and /dev/null differ diff --git a/apps/bootstrap-installer/src-tauri/icons/icon.ico b/apps/bootstrap-installer/src-tauri/icons/icon.ico deleted file mode 100644 index ab346bae6a..0000000000 Binary files a/apps/bootstrap-installer/src-tauri/icons/icon.ico and /dev/null differ diff --git a/apps/bootstrap-installer/src/components/brand-mark.tsx b/apps/bootstrap-installer/src/components/brand-mark.tsx index 11f96f9632..a37d6887ad 100644 --- a/apps/bootstrap-installer/src/components/brand-mark.tsx +++ b/apps/bootstrap-installer/src/components/brand-mark.tsx @@ -2,12 +2,12 @@ import { cn } from '../lib/utils' const assetPath = (path: string) => `${import.meta.env.BASE_URL}${path.replace(/^\/+/, '')}` -// Brand badge: nous-girl mark on a white tile (the light BrandMark variant). -// Ported from apps/desktop's BrandMark; asset lives in this app's public/. -// The installer has no theme system, so it always uses the light asset. +// Brand badge: the nous-girl mark in the app-icon squircle (light variant). +// Ported from apps/desktop's BrandMark; the installer has no theme system so +// it always uses the light squircle asset. Generated by generate_icons.py. export function BrandMark({ className, ...props }: React.ComponentProps<'span'>) { return ( - + ) diff --git a/apps/desktop/DESIGN.md b/apps/desktop/DESIGN.md index a535f0a416..264b131725 100644 --- a/apps/desktop/DESIGN.md +++ b/apps/desktop/DESIGN.md @@ -258,7 +258,7 @@ Sizes: `default`, `xs`, `overlay` (titlebar glyph counts). 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). 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 for hero/brand moments; don't reintroduce decorative star/sparkle icons. diff --git a/apps/desktop/assets/appx/Square150x150Logo-dark.png b/apps/desktop/assets/appx/Square150x150Logo-dark.png deleted file mode 100644 index 123b78521b..0000000000 Binary files a/apps/desktop/assets/appx/Square150x150Logo-dark.png and /dev/null differ diff --git a/apps/desktop/assets/appx/Square150x150Logo.png b/apps/desktop/assets/appx/Square150x150Logo.png deleted file mode 100644 index 5e696d3916..0000000000 Binary files a/apps/desktop/assets/appx/Square150x150Logo.png and /dev/null differ diff --git a/apps/desktop/assets/appx/Square44x44Logo-dark.png b/apps/desktop/assets/appx/Square44x44Logo-dark.png deleted file mode 100644 index b2f26d1add..0000000000 Binary files a/apps/desktop/assets/appx/Square44x44Logo-dark.png and /dev/null differ diff --git a/apps/desktop/assets/appx/Square44x44Logo.png b/apps/desktop/assets/appx/Square44x44Logo.png deleted file mode 100644 index a8b986cc28..0000000000 Binary files a/apps/desktop/assets/appx/Square44x44Logo.png and /dev/null differ diff --git a/apps/desktop/assets/appx/StoreLogo-dark.png b/apps/desktop/assets/appx/StoreLogo-dark.png deleted file mode 100644 index f9ebe3b3dc..0000000000 Binary files a/apps/desktop/assets/appx/StoreLogo-dark.png and /dev/null differ diff --git a/apps/desktop/assets/appx/StoreLogo.png b/apps/desktop/assets/appx/StoreLogo.png deleted file mode 100644 index 421efbe7b2..0000000000 Binary files a/apps/desktop/assets/appx/StoreLogo.png and /dev/null differ diff --git a/apps/desktop/assets/appx/Wide310x150Logo-dark.png b/apps/desktop/assets/appx/Wide310x150Logo-dark.png deleted file mode 100644 index a8aef27799..0000000000 Binary files a/apps/desktop/assets/appx/Wide310x150Logo-dark.png and /dev/null differ diff --git a/apps/desktop/assets/appx/Wide310x150Logo.png b/apps/desktop/assets/appx/Wide310x150Logo.png deleted file mode 100644 index 8817e46aef..0000000000 Binary files a/apps/desktop/assets/appx/Wide310x150Logo.png and /dev/null differ diff --git a/apps/desktop/assets/icon-dark.icns b/apps/desktop/assets/icon-dark.icns deleted file mode 100644 index 56ddcc6c59..0000000000 Binary files a/apps/desktop/assets/icon-dark.icns and /dev/null differ diff --git a/apps/desktop/assets/icon-dark.ico b/apps/desktop/assets/icon-dark.ico deleted file mode 100644 index 4138db45b5..0000000000 Binary files a/apps/desktop/assets/icon-dark.ico and /dev/null differ diff --git a/apps/desktop/assets/icon-dark.png b/apps/desktop/assets/icon-dark.png deleted file mode 100644 index d628cee4a0..0000000000 Binary files a/apps/desktop/assets/icon-dark.png and /dev/null differ diff --git a/apps/desktop/assets/icon.icns b/apps/desktop/assets/icon.icns deleted file mode 100644 index 53910bb695..0000000000 Binary files a/apps/desktop/assets/icon.icns and /dev/null differ diff --git a/apps/desktop/assets/icon.ico b/apps/desktop/assets/icon.ico deleted file mode 100644 index b2e0360e06..0000000000 Binary files a/apps/desktop/assets/icon.ico and /dev/null differ diff --git a/apps/desktop/assets/icon.png b/apps/desktop/assets/icon.png deleted file mode 100644 index 9d981c804c..0000000000 Binary files a/apps/desktop/assets/icon.png and /dev/null differ diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 7546a1e41f..3eeb3e58f1 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -19,7 +19,7 @@ "clean:e2e": "tsc --build tsconfig.e2e.json --clean", "clean:renderer": "tsc --build tsconfig.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: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", @@ -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: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 .", - "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", "postbuild": "node scripts/assert-dist-built.mjs", "prebuilder": "node scripts/patch-electron-builder-mac-binary.mjs", diff --git a/apps/desktop/public/apple-touch-icon.png b/apps/desktop/public/apple-touch-icon.png deleted file mode 100644 index 9d981c804c..0000000000 Binary files a/apps/desktop/public/apple-touch-icon.png and /dev/null differ diff --git a/apps/desktop/public/nous-girl-dark.png b/apps/desktop/public/nous-girl-dark.png deleted file mode 100644 index c4bf2652d0..0000000000 Binary files a/apps/desktop/public/nous-girl-dark.png and /dev/null differ diff --git a/apps/desktop/public/nous-girl.png b/apps/desktop/public/nous-girl.png deleted file mode 100644 index d6d763b391..0000000000 Binary files a/apps/desktop/public/nous-girl.png and /dev/null differ diff --git a/apps/desktop/src/components/brand-mark.tsx b/apps/desktop/src/components/brand-mark.tsx index 337e89533a..fb71b33b88 100644 --- a/apps/desktop/src/components/brand-mark.tsx +++ b/apps/desktop/src/components/brand-mark.tsx @@ -3,22 +3,18 @@ import { useTheme } from '@/themes' 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 -// the black girl on white, dark mode the white girl on the nous dark surface. -// Fills the tile (softly rounded); size via className (default size-14). +// Brand badge: the nous-girl mark in the app-icon squircle, theme-aware — +// light mode shows the black girl on the white squircle, dark mode the white +// 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'>) { const { renderedMode } = useTheme() const dark = renderedMode === 'dark' return ( - + ) diff --git a/assets/backgrounds/logo-frame-dark.svg b/assets/backgrounds/logo-frame-dark.svg deleted file mode 100644 index 1bcb2146a8..0000000000 --- a/assets/backgrounds/logo-frame-dark.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/backgrounds/logo-frame.svg b/assets/backgrounds/logo-frame.svg deleted file mode 100644 index be76ca60e9..0000000000 --- a/assets/backgrounds/logo-frame.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/backgrounds/tile-dark.svg b/assets/backgrounds/tile-dark.svg deleted file mode 100644 index 01638791e5..0000000000 --- a/assets/backgrounds/tile-dark.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/backgrounds/tile-light.svg b/assets/backgrounds/tile-light.svg deleted file mode 100644 index b2890cf878..0000000000 --- a/assets/backgrounds/tile-light.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icon-master-dark.svg b/assets/icon-master-dark.svg deleted file mode 100644 index 2d2b744655..0000000000 --- a/assets/icon-master-dark.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/assets/icon-master.svg b/assets/icon-master.svg deleted file mode 100644 index d5fd2200b7..0000000000 --- a/assets/icon-master.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/scripts/generate-icons.mjs b/scripts/generate-icons.mjs new file mode 100644 index 0000000000..82a9a1252e --- /dev/null +++ b/scripts/generate-icons.mjs @@ -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() diff --git a/scripts/generate_icons.py b/scripts/generate_icons.py index e664e57206..09311e29fd 100644 --- a/scripts/generate_icons.py +++ b/scripts/generate_icons.py @@ -3,7 +3,7 @@ Usage (from repo root): .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: 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). Backgrounds (per platform surface, light/dark): - assets/backgrounds/squircle-light.svg white 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 + assets/backgrounds/squircle-light.svg white rounded + assets/backgrounds/squircle-dark.svg #0d1117 rounded The master SVGs (assets/icon-master.svg light, assets/icon-master-dark.svg dark) are GENERATED artifacts — squircle background + girl nested into the - 824px HIG content safe zone — not hand-edited sources. The 1024 master is - what every squircle target renders from. + 824px HIG content safe zone. The light master drives every squircle target; + the dark master drives the dark-appearance targets. -If the girl SVGs are missing a red-circle placeholder master is written so a -partial checkout still generates; --check then flags the drift. +The girl is nested via its art bbox as viewBox, so it always lands centered in +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 -against the committed file, exiting nonzero on any drift. This is what -CI runs (see .github/workflows/icons-freshness-check.yml) so the repo -can never hold a hand-edited or stale generated icon. +GENERATED OUTPUTS ARE NOT COMMITTED. Everything this script writes is +gitignored and regenerated on demand by the consuming pipelines (website +prebuild, desktop prebuild/predev, installer prebuild, web prebuild — via +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. Containers: Pillow for multi-size .ico and .icns. @@ -41,7 +40,7 @@ Deps: Pillow (core dependency), resvg-py (dev extra): uv sync --extra dev -Outputs (35 files): +Outputs (30 files): assets/icon-master.svg generated light master assets/icon-master-dark.svg generated dark master 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/Square44x44Logo.png 44x44 squircle apps/desktop/assets/appx/Square150x150Logo.png 150x150 squircle - apps/desktop/assets/appx/*-dark.png dark-appearance logos (ready - to wire into the MSIX - manifest when app-builder-lib - supports contrast images) + apps/desktop/assets/appx/*-dark.png dark-appearance logos 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-dark.png 256x256 PNG, white girl on #0d1117 (dark) + apps/desktop/public/nous-girl.png 256x256 squircle, black girl (light mark) + 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/128x128.png 128x128 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.icns 16..1024 - apps/bootstrap-installer/public/nous-girl.png 256x256 PNG, black girl on white (light) - website/static/img/logo.png 1772x1799 black-frame wordmark - website/static/img/logo-dark.png 1772x1799 white-frame wordmark + apps/bootstrap-installer/public/nous-girl.png 256x256 squircle mark (light) + website/static/img/logo.png 1772x1799 girl alone, transparent (light) + 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-dark.png 150x150 on #0d1117 (opaque) 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 # 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 -# interior at ~1.7x; BrandMark tiles take the girl height-fitted (meet). +# (centered, 100px pad on a 1024 canvas). Marks reuse the same squircles. GIRL_BOXES = { "squircle-light.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). GIRL_VIEWBOX = 5487.0615 -PLACEHOLDER = f''' - - - -''' +# Target sizes for --check's structural verification: relpath -> (format, size) +CHECK_SIZES: dict[str, tuple[str, tuple[int, int]]] = { + "apps/desktop/assets/icon.png": ("PNG", (1024, 1024)), + "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) 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]: """Inner content + (width, height) of a background SVG asset.""" text = (BACKGROUNDS / name).read_text(encoding="utf-8") - m = re.search(r"]*viewBox=\"0 0 (\d+(?:\.\d+)?) (\d+(?:\.\d+)?)\"[^>]*>", text) + m = re.search(r']*viewBox="0 0 (\d+(?:\.\d+)?) (\d+(?:\.\d+)?)"[^>]*>', text) assert m, f"cannot parse viewBox of {name}" w, h = float(m.group(1)), float(m.group(2)) 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) -def compose_master(girl: str, bg: str) -> str: - """Composed master SVG: background + girl layer.""" +def compose_svg(girl: str, bg: str) -> str: + """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) - x, y, bx, bh = GIRL_BOXES[bg] return ( f'\n' f" {inner.strip()}\n" - f" {girl_layer(girl, (x, y, bx, bh))}\n" + f" {girl_layer(girl, GIRL_BOXES[bg])}\n" "\n" ) @@ -243,12 +257,17 @@ def ensure_masters(check: bool = False) -> None: if missing: if check: sys.exit(f"[check] girl art missing: {[p.name for p in missing]}") - for path, content in ((MASTER, PLACEHOLDER), (MASTER_DARK, PLACEHOLDER)): - path.write_text(content, encoding="utf-8") + placeholder = f''' + + + +''' + 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]})") return - MASTER.write_text(compose_master("black", "squircle-light.svg"), encoding="utf-8") - MASTER_DARK.write_text(compose_master("white", "squircle-dark.svg"), encoding="utf-8") + MASTER.write_text(compose_svg("black", "squircle-light.svg"), encoding="utf-8") + MASTER_DARK.write_text(compose_svg("white", "squircle-dark.svg"), encoding="utf-8") # ─── 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") +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: x = (canvas.width - img.width) // 2 y = (canvas.height - img.height) // 2 canvas.alpha_composite(img, (x, y)) -def girl_mark(kind: str, size: int) -> Image.Image: - """Composite the girl SVG onto a fixed tile (BrandMark asset), rendered - straight from the vector art (crisp at any size). - - girl_light: black girl on white tile. girl_dark: white girl on #0d1117. - """ - if kind == "girl_light": - girl, bg = "black", "tile-light.svg" +def save_png(img: Image.Image, buf: io.BytesIO) -> None: + """Save with alpha preserved. Flat art quantizes losslessly to an 8-bit + palette (tRNS per-index alpha keeps the AA edges), so use that when the + palette round-trips pixel-identically; fall back to RGBA otherwise.""" + if img.mode != "RGBA": + img.convert("RGBA").save(buf, "PNG", optimize=True) + return + 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: - 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(): print(f" [girl] {GIRLS[girl].name} missing — placeholder fallback") return render(MASTER if kind == "girl_light" else MASTER_DARK, size) - - inner, w, h = background_inner(bg) - svg = ( - f'\n' - f" {inner.strip()}\n" - f" {girl_layer(girl, GIRL_BOXES[bg])}\n" - "\n" - ) - data = resvg_py.svg_to_bytes(svg_string=svg, width=size, height=size) - return Image.open(io.BytesIO(data)).convert("RGBA") + return render_svg(compose_svg(girl, bg), size) def build_logo_image(master: Path, dark: bool = False) -> Image.Image: - """1772x1799 wordmark: frame (light: black / dark: white, ~14-17px) + - interior (light: white / dark: #0d1117) + art at ~1.7x. - - 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 - + """1772x1799 wordmark: the girl alone on transparency (no frame), centered. + Light = black girl, dark = white girl — the consuming surface's background + (navbar light/dark) shows through.""" W, H = 1772, 1799 - art_w, art_h = 1743, 1766 - border = 14 - 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 + svg = f'\n {girl_layer("white" if dark else "black", (0, 0, W, H))}\n\n' + return render_svg(svg, (W, H)) def target_bytes(kind: str, arg: object) -> bytes: @@ -335,23 +343,15 @@ def target_bytes(kind: str, arg: object) -> bytes: buf = io.BytesIO() if kind == "png": - render(MASTER, arg).save(buf, "PNG") + save_png(render(MASTER, arg), buf) elif kind == "png_dark": - render(MASTER_DARK, arg).save(buf, "PNG") + save_png(render(MASTER_DARK, arg), buf) 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": - render(MASTER_DARK, arg, background=DARK_HEX).save(buf, "PNG") - elif kind == "girl_light" or kind == "girl_dark": - # Flat 2-tone vector-style art: <256 distinct colors, so an 8-bit - # 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 - ) + render(MASTER_DARK, arg, background=DARK_HEX).convert("RGB").save(buf, "PNG", optimize=True) + elif kind in ("girl_light", "girl_dark"): + save_png(girl_mark(kind, arg), buf) elif kind == "ico": img = render(MASTER, max(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: + """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) - drifted: list[str] = [] + problems: list[str] = [] + + # every target must generate without error 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 if not path.exists(): - drifted.append(f"{rel}: MISSING (expected generated file)") + problems.append(f"{rel}: MISSING (expected generated file)") continue 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 - drifted.append(f"{rel}: REGENERATE FAILED ({exc})") - continue - actual = path.read_bytes() - if actual != expected: - drifted.append(f"{rel}: drift ({len(actual)} bytes on disk vs {len(expected)} generated)") + problems.append(f"{rel}: UNREADABLE ({exc})") - if not drifted: - print(f"[ok] all {len(TARGETS)} generated icons match the girl art + backgrounds") + # squircles must keep transparent corners (alpha extrema include 0) + 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 - print(f"[check] {len(drifted)} file(s) out of sync:") - for line in drifted: + print(f"[check] {len(problems)} problem(s):") + for line in problems: print(f" - {line}") - print( - "\nFix: run `.venv/Scripts/python.exe scripts/generate_icons.py` " - "and commit the regenerated files." - ) return 1 diff --git a/web/package.json b/web/package.json index 74b5f3cb8b..ab6154ea92 100644 --- a/web/package.json +++ b/web/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "dev": "vite", + "prebuild": "node ../scripts/generate-icons.mjs", "build": "tsc -b && vite build", "lint": "eslint .", "lint:fix": "eslint . --fix", diff --git a/web/public/favicon.ico b/web/public/favicon.ico deleted file mode 100644 index 6c5fa23c97..0000000000 Binary files a/web/public/favicon.ico and /dev/null differ diff --git a/website/scripts/prebuild.mjs b/website/scripts/prebuild.mjs index b873a9b208..a577520f75 100644 --- a/website/scripts/prebuild.mjs +++ b/website/scripts/prebuild.mjs @@ -29,9 +29,11 @@ import { fileURLToPath } from "node:url"; const scriptDir = dirname(fileURLToPath(import.meta.url)); const websiteDir = resolve(scriptDir, ".."); +const repoRoot = resolve(websiteDir, ".."); const extractScript = join(scriptDir, "extract-skills.py"); const llmsScript = join(scriptDir, "generate-llms-txt.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 unifiedIndexFile = join(websiteDir, "static", "api", "skills-index.json"); 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(); // 1) skills.json — required for the Skills Hub page. diff --git a/website/static/img/apple-touch-icon.png b/website/static/img/apple-touch-icon.png deleted file mode 100644 index f79504af77..0000000000 Binary files a/website/static/img/apple-touch-icon.png and /dev/null differ diff --git a/website/static/img/favicon-16x16.png b/website/static/img/favicon-16x16.png deleted file mode 100644 index f5abd8087a..0000000000 Binary files a/website/static/img/favicon-16x16.png and /dev/null differ diff --git a/website/static/img/favicon-32x32.png b/website/static/img/favicon-32x32.png deleted file mode 100644 index aa63f12148..0000000000 Binary files a/website/static/img/favicon-32x32.png and /dev/null differ diff --git a/website/static/img/favicon.ico b/website/static/img/favicon.ico deleted file mode 100644 index 6c5fa23c97..0000000000 Binary files a/website/static/img/favicon.ico and /dev/null differ diff --git a/website/static/img/favicon.svg b/website/static/img/favicon.svg deleted file mode 100644 index d5fd2200b7..0000000000 --- a/website/static/img/favicon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/website/static/img/logo-dark.png b/website/static/img/logo-dark.png deleted file mode 100644 index 76d3571637..0000000000 Binary files a/website/static/img/logo-dark.png and /dev/null differ diff --git a/website/static/img/logo.png b/website/static/img/logo.png deleted file mode 100644 index 6fbce441e8..0000000000 Binary files a/website/static/img/logo.png and /dev/null differ diff --git a/website/static/img/nous-logo-dark.png b/website/static/img/nous-logo-dark.png deleted file mode 100644 index 4591fe066b..0000000000 Binary files a/website/static/img/nous-logo-dark.png and /dev/null differ diff --git a/website/static/img/nous-logo.png b/website/static/img/nous-logo.png deleted file mode 100644 index 6ff9e81d76..0000000000 Binary files a/website/static/img/nous-logo.png and /dev/null differ