From 6752da9a7735add1aff6ebc632c7e83fc4005a48 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:32:18 +0530 Subject: [PATCH 01/90] fix(dashboard): clean up upload temp file on client disconnect + pin python-multipart (NS-501) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to #47663 (streaming multipart upload), fixing two issues that landed with it. 1. Temp file leaked on client disconnect. The streaming upload endpoint's except chain caught only HTTPException / PermissionError / OSError — all Exception subclasses. asyncio.CancelledError, raised when a browser aborts a large upload mid-stream (the exact NS-501 scenario), is a BaseException, so it bypassed every except clause and reached a finally that only closed the file handle and never unlinked the temp file. Every aborted large upload orphaned a partial `.{name}.*.upload` file (up to ~100 MB) in the target directory. Cleanup now lives in finally, keyed on a `renamed` success flag, so the temp file is removed on every non-success exit including BaseException paths. Added test_stream_upload_cleans_temp_on_cancellation, which fails on the pre-fix code (leaks the temp file) and passes with the fix. 2. python-multipart pinned to ==0.0.27 instead of ==0.0.20. The package was already resolved at 0.0.27 transitively (via daytona) before #47663; the explicit ==0.0.20 pin in the [web] extra and the tool.dashboard lazy-install set downgraded it. Bumped both to ==0.0.27 and regenerated with `uv lock`, keeping the lockfile coherent. The base dependency stays >=0.0.9,<1. --- hermes_cli/web_server.py | 12 ++++-- pyproject.toml | 2 +- tests/hermes_cli/test_web_server_files.py | 52 +++++++++++++++++++++++ tools/lazy_deps.py | 2 +- uv.lock | 8 ++-- 5 files changed, 67 insertions(+), 9 deletions(-) diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index ed619979bfb..ad82d9fdfef 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -1529,6 +1529,7 @@ async def upload_managed_file_stream( ) tmp_path = Path(tmp_name) total = 0 + renamed = False try: with os.fdopen(tmp_fd, "wb") as out: while True: @@ -1540,16 +1541,21 @@ async def upload_managed_file_stream( raise HTTPException(status_code=413, detail="File is too large") out.write(chunk) os.replace(tmp_path, target) + renamed = True except HTTPException: - tmp_path.unlink(missing_ok=True) raise except PermissionError: - tmp_path.unlink(missing_ok=True) raise HTTPException(status_code=403, detail="File is not writable") except OSError as exc: - tmp_path.unlink(missing_ok=True) raise HTTPException(status_code=500, detail=f"Could not write file: {exc}") finally: + # Clean up the temp file on every non-success exit, including + # BaseException paths the `except` clauses above don't catch — most + # importantly asyncio.CancelledError when a browser aborts a large + # upload mid-stream (the exact NS-501 scenario). os.replace clears + # tmp_path on success, so only unlink when the rename didn't happen. + if not renamed: + tmp_path.unlink(missing_ok=True) await file.close() return { diff --git a/pyproject.toml b/pyproject.toml index 6e371126dd2..cab849dc755 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -258,7 +258,7 @@ youtube = [ # `hermes dashboard` (localhost SPA + API). Not in core to keep the default install lean. # starlette==1.0.1 pinned for CVE-2026-48710 (BadHost) — fastapi pulls Starlette # transitively and pre-1.0.1 is the vulnerable range. See the mcp extra above. -web = ["fastapi==0.133.1", "uvicorn[standard]==0.41.0", "starlette==1.0.1", "python-multipart==0.0.20"] +web = ["fastapi==0.133.1", "uvicorn[standard]==0.41.0", "starlette==1.0.1", "python-multipart==0.0.27"] all = [ # Policy (2026-05-12): `[all]` includes only extras that genuinely # CAN'T be lazy-installed via `tools/lazy_deps.py` — i.e. things every diff --git a/tests/hermes_cli/test_web_server_files.py b/tests/hermes_cli/test_web_server_files.py index 46ba18b1355..b295f0ab998 100644 --- a/tests/hermes_cli/test_web_server_files.py +++ b/tests/hermes_cli/test_web_server_files.py @@ -436,3 +436,55 @@ def test_stream_upload_large_file_under_cap_succeeds(forced_files_client, monkey assert created.status_code == 200 assert file_path.stat().st_size == len(payload) assert file_path.read_bytes() == payload + + +def test_stream_upload_cleans_temp_on_cancellation(forced_files_client): + """A client disconnect mid-stream (asyncio.CancelledError) must not leak a temp file. + + CancelledError is a BaseException, not an Exception, so it bypasses the + endpoint's ``except`` clauses entirely. The cleanup therefore lives in a + ``finally`` keyed on a success flag — without it, every aborted large + upload (the exact NS-501 scenario) would orphan a partial ``.upload`` temp + file in the target directory. We invoke the endpoint coroutine directly so + the BaseException propagates instead of being swallowed by the test client. + """ + import asyncio + + _client, root = forced_files_client + target = root / "out" / "aborted.bin" + target.parent.mkdir(parents=True, exist_ok=True) + + class _AbortingUpload: + """UploadFile stand-in that yields one chunk then aborts like a dropped client.""" + + filename = "aborted.bin" + + def __init__(self): + self._calls = 0 + + async def read(self, _size): + self._calls += 1 + if self._calls == 1: + return b"partial chunk before the client vanished" + raise asyncio.CancelledError() + + async def close(self): + return None + + request = SimpleNamespace() + + with pytest.raises(asyncio.CancelledError): + asyncio.run( + web_server.upload_managed_file_stream( + request=request, + file=_AbortingUpload(), + path=str(target), + overwrite=True, + ) + ) + + # No partial data was promoted into place ... + assert not target.exists() + # ... and no .upload temp file was left behind. + leftovers = [p.name for p in target.parent.iterdir() if ".upload" in p.name] + assert leftovers == [], f"temp upload files leaked on cancellation: {leftovers}" diff --git a/tools/lazy_deps.py b/tools/lazy_deps.py index 98bacbf42a0..4e2159a1a02 100644 --- a/tools/lazy_deps.py +++ b/tools/lazy_deps.py @@ -178,7 +178,7 @@ LAZY_DEPS: dict[str, tuple[str, ...]] = { "fastapi==0.133.1", "uvicorn[standard]==0.41.0", "starlette==1.0.1", # CVE-2026-48710 (BadHost) — keep lazy-install in sync with pyproject [web] - "python-multipart==0.0.20", # FastAPI UploadFile/Form for streaming uploads (NS-501) + "python-multipart==0.0.27", # FastAPI UploadFile/Form for streaming uploads (NS-501) ), # Vision image-resize recovery (Pillow). Pillow is now a CORE dependency # (pyproject `dependencies`), so this entry is a belt-and-suspenders fallback diff --git a/uv.lock b/uv.lock index fc340bdbe89..095b7563311 100644 --- a/uv.lock +++ b/uv.lock @@ -1713,7 +1713,7 @@ requires-dist = [ { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = "==1.3.0" }, { name = "python-dotenv", specifier = "==1.2.2" }, { name = "python-multipart", specifier = ">=0.0.9,<1" }, - { name = "python-multipart", marker = "extra == 'web'", specifier = "==0.0.20" }, + { name = "python-multipart", marker = "extra == 'web'", specifier = "==0.0.27" }, { name = "python-telegram-bot", extras = ["webhooks"], marker = "extra == 'messaging'", specifier = "==22.6" }, { name = "python-telegram-bot", extras = ["webhooks"], marker = "extra == 'termux'", specifier = "==22.6" }, { name = "pywinpty", marker = "sys_platform == 'win32'", specifier = ">=2.0.0,<3" }, @@ -3317,11 +3317,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.20" +version = "0.0.27" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/9b/f23807317a113dc36e74e75eb265a02dd1a4d9082abc3c1064acd22997c4/python_multipart-0.0.27.tar.gz", hash = "sha256:9870a6a8c5a20a5bf4f07c017bd1489006ff8836cff097b6933355ee2b49b602", size = 44043, upload-time = "2026-04-27T10:51:26.649Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, + { url = "https://files.pythonhosted.org/packages/99/78/4126abcbdbd3c559d43e0db7f7b9173fc6befe45d39a2856cc0b8ec2a5a6/python_multipart-0.0.27-py3-none-any.whl", hash = "sha256:6fccfad17a27334bd0193681b369f476eda3409f17381a2d65aa7df3f7275645", size = 29254, upload-time = "2026-04-27T10:51:24.997Z" }, ] [[package]] From b892ee2bcf1b65f3010c7229f4d61e574ada54ad Mon Sep 17 00:00:00 2001 From: xxxigm Date: Tue, 16 Jun 2026 21:20:14 +0700 Subject: [PATCH 02/90] fix(agent): summarize non-retryable API errors so raw HTML never leaks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a non-retryable client error aborts the turn (e.g. a Codex/Cloudflare HTTP 403 "managed challenge" page), the conversation loop returned the failure dict with `error: str(api_error)` — the entire ~60KB HTML page. Downstream consumers deliver that field verbatim: a cron job dumped a Cloudflare challenge page to Discord, where it was split into ~31 messages. The sibling "max retries exhausted" path already collapses such bodies via `_summarize_api_error` (which extracts the / status from HTML error pages). This makes the non-retryable path consistent: compute the summary once and use it for both the status emit and the returned `error`. --- agent/conversation_loop.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/agent/conversation_loop.py b/agent/conversation_loop.py index ef69ac68329..163a508a8cd 100644 --- a/agent/conversation_loop.py +++ b/agent/conversation_loop.py @@ -3197,15 +3197,22 @@ def run_conversation( # Terminal — flush buffered context so the user sees # what was tried before the abort. agent._flush_status_buffer() + # Summarize once: Cloudflare/proxy HTML challenge pages and + # other raw provider bodies must be collapsed to a short + # one-liner here, otherwise the full page leaks into the + # returned ``error`` field and downstream consumers deliver + # it verbatim (e.g. a cron failure notification dumped a + # ~60KB Cloudflare challenge page as 31 Discord messages). + _nonretryable_summary = agent._summarize_api_error(api_error) if classified.reason == FailoverReason.content_policy_blocked: agent._emit_status( f"❌ Provider safety filter blocked this request: " - f"{agent._summarize_api_error(api_error)}" + f"{_nonretryable_summary}" ) else: agent._emit_status( f"❌ Non-retryable error (HTTP {status_code}): " - f"{agent._summarize_api_error(api_error)}" + f"{_nonretryable_summary}" ) agent._vprint(f"{agent.log_prefix}❌ Non-retryable client error (HTTP {status_code}). Aborting.", force=True) agent._vprint(f"{agent.log_prefix} 🔌 Provider: {_provider} Model: {_model}", force=True) @@ -3309,7 +3316,7 @@ def run_conversation( "api_calls": api_call_count, "completed": False, "failed": True, - "error": str(api_error), + "error": _nonretryable_summary, } if retry_count >= max_retries: From f18f31ebf6dda993ade9f9de222fcf7fdfe8952e Mon Sep 17 00:00:00 2001 From: xxxigm <tuancanhnguyen706@gmail.com> Date: Thu, 18 Jun 2026 14:55:38 +0700 Subject: [PATCH 03/90] test(agent): cover non-retryable error HTML summarization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Locks the contract that a non-retryable failure (a Cloudflare 403 "managed challenge" page) returns a short, HTML-free `error` field — guarding the field path where the raw page was dumped to Discord as ~31 messages. The test drives the standard chat-completions path with a concrete model so the turn actually reaches `client.chat.completions.create`, where the mocked 403 is raised. It asserts the create call happened (guarding against a vacuous pass — an empty model on the Codex Responses path would otherwise abort on a validation ValueError before any API call) and that the summarized error includes "403" while excluding <html> / _cf_chl_opt. The non-retryable abort path is provider-agnostic; a Cloudflare managed-challenge 403 can surface on any provider behind Cloudflare. --- .../test_nonretryable_error_html_summary.py | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 tests/run_agent/test_nonretryable_error_html_summary.py diff --git a/tests/run_agent/test_nonretryable_error_html_summary.py b/tests/run_agent/test_nonretryable_error_html_summary.py new file mode 100644 index 00000000000..db765b124f3 --- /dev/null +++ b/tests/run_agent/test_nonretryable_error_html_summary.py @@ -0,0 +1,130 @@ +"""Regression: non-retryable API failures must not leak raw HTML pages. + +A scheduled cron job fell back to the Codex (``chatgpt.com``) provider, which +returned a Cloudflare *challenge* page (HTTP 403) instead of a normal API +response. The conversation loop classified this as a non-retryable client +error and returned the failure dict — but the ``error`` field carried +``str(api_error)``, i.e. the entire ~60 KB Cloudflare HTML page. The cron +scheduler then delivered that verbatim to Discord, where it was split into +~31 messages (the reporter's "31 part discord message which is cloudflares +challenge page"). + +The sibling "max retries exhausted" path already summarized the error via +``_summarize_api_error`` (which collapses HTML pages to a one-liner); the +non-retryable path did not. These tests lock the contract: whichever +terminal path is taken, ``result['error']`` is a short, HTML-free summary. +""" + +from unittest.mock import MagicMock, patch + +import run_agent +from run_agent import AIAgent + + +# A representative Cloudflare "managed challenge" body, matching the shape the +# Codex backend returned in the field report (no <title>, large inline +# ``_cf_chl_opt`` script). Padded so length-based assertions are meaningful. +_CLOUDFLARE_CHALLENGE_HTML = ( + "<!DOCTYPE html>\n<html>\n <head>\n" + ' <meta http-equiv="refresh" content="360"></head>\n' + " <body>\n <div class=\"data\"><noscript>" + "Enable JavaScript and cookies to continue</noscript>" + "<script>(function(){window._cf_chl_opt = {cRay: 'a0ca002c4f91769c'," + "cZone: 'chatgpt.com', cType: 'managed', " + + ("md: '" + "x" * 4000 + "',") + + "};})();</script></div>\n </body>\n</html>\n" +) + + +def _make_403_html_error() -> Exception: + """An exception mimicking a Codex 403 whose body is a Cloudflare page.""" + err = Exception(_CLOUDFLARE_CHALLENGE_HTML) + err.status_code = 403 + return err + + +def _make_agent() -> AIAgent: + # Drive the standard chat-completions path with a concrete model so the + # turn actually reaches ``client.chat.completions.create`` — that is where + # the mocked 403 is raised. The non-retryable abort being exercised lives + # in the shared conversation loop and is provider-agnostic; a Cloudflare + # "managed challenge" 403 can surface on any provider sitting behind + # Cloudflare (it was first reported on the Codex backend). Pinning + # ``api_mode`` + ``model`` here avoids the earlier abort the previous + # revision hit: an empty model on the Codex Responses path raised a + # validation ``ValueError`` *before* any API call, so the test passed + # without ever touching the 403 summarization path. + with ( + patch("run_agent.get_tool_definitions", return_value=[]), + patch("run_agent.check_toolset_requirements", return_value={}), + patch("run_agent.OpenAI"), + ): + a = AIAgent( + api_key="test-key-1234567890", + base_url="https://api.openai.com/v1", + provider="openai", + api_mode="chat_completions", + model="gpt-5.5", + quiet_mode=True, + skip_context_files=True, + skip_memory=True, + ) + a.client = MagicMock() + a._cached_system_prompt = "You are helpful." + a._use_prompt_caching = False + a.tool_delay = 0 + a.compression_enabled = False + a.save_trajectories = False + return a + + +def test_summarize_collapses_cloudflare_challenge_page(): + """``_summarize_api_error`` must never echo the raw HTML body.""" + summary = AIAgent._summarize_api_error(_make_403_html_error()) + + assert "<html" not in summary.lower() + assert "<!doctype" not in summary.lower() + assert "_cf_chl_opt" not in summary + # A one-liner, not a multi-kilobyte page. + assert len(summary) < 200 + # Still informative: the HTTP status survives. + assert "403" in summary + + +def test_non_retryable_failure_error_is_summarized_not_raw_html(): + """The terminal non-retryable dict must carry a short, HTML-free error. + + This is the exact field path: a 403 Cloudflare challenge with no fallback + configured aborts as a non-retryable client error. Before the fix the + returned ``error`` was the full ~60 KB page. + + The mocked 403 is the *only* failure the turn can hit — the agent reaches + ``client.chat.completions.create`` (asserted below), so the test cannot + pass vacuously by aborting on some earlier, unrelated error. + """ + agent = _make_agent() + agent.client.chat.completions.create.side_effect = _make_403_html_error() + + with ( + patch.object(agent, "_persist_session"), + patch.object(agent, "_save_trajectory"), + patch.object(agent, "_cleanup_task_resources"), + ): + result = agent.run_conversation("daily briefing please") + + # Guard against a vacuous pass: the mocked 403 must actually be the + # failure that aborted the turn. (The previous revision never reached + # this call and still "passed".) + assert agent.client.chat.completions.create.called + assert result.get("failed") is True + error = result.get("error") or "" + # The whole point of the fix: no raw HTML / Cloudflare markup leaks. + assert "<html" not in error.lower() + assert "<!doctype" not in error.lower() + assert "_cf_chl_opt" not in error + # Still informative: the summarized 403 status survives into the field + # delivered downstream. + assert "403" in error + # The original page was tens of kilobytes; a summary is short. + assert len(error) < 500 + assert len(error) < len(_CLOUDFLARE_CHALLENGE_HTML) From d0622cafabfbf0acfe8649e4f0390d20d0bc11d6 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:46:47 +0530 Subject: [PATCH 04/90] refactor(agent): reuse hoisted summary in content-policy branch The non-retryable abort path now computes _nonretryable_summary once and reuses it at the emit sites and the returned error field. The content-policy-blocked return branch still recomputed the identical value into a separate _summary local, half-honoring the 'summarize once' intent. _summarize_api_error is a pure staticmethod and api_error is never reassigned in this block, so _summary was provably byte-identical to _nonretryable_summary. Reuse the hoisted value and drop the redundant call. Behavior-preserving. --- agent/conversation_loop.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/agent/conversation_loop.py b/agent/conversation_loop.py index 163a508a8cd..0ccc9649428 100644 --- a/agent/conversation_loop.py +++ b/agent/conversation_loop.py @@ -3297,18 +3297,17 @@ def run_conversation( else: agent._persist_session(messages, conversation_history) if classified.reason == FailoverReason.content_policy_blocked: - _summary = agent._summarize_api_error(api_error) _policy_response = ( "⚠️ The model provider's safety filter blocked this request " "(not a Hermes/gateway failure).\n\n" - f"Provider message: {_summary}\n\n" + f"Provider message: {_nonretryable_summary}\n\n" f"{_CONTENT_POLICY_RECOVERY_HINT}" ) return _content_policy_blocked_result( messages, api_call_count, final_response=_policy_response, - error_detail=_summary, + error_detail=_nonretryable_summary, ) return { "final_response": None, From 245b95b09470bb3887943122a7d0de5bf20da055 Mon Sep 17 00:00:00 2001 From: AhmetArif0 <147827411+AhmetArif0@users.noreply.github.com> Date: Tue, 2 Jun 2026 18:34:26 +0300 Subject: [PATCH 05/90] fix(terminal): block gateway lifecycle commands from inside the gateway process MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit systemctl --user restart hermes-gateway run via the terminal tool is a child of the gateway itself. When systemd delivers SIGTERM the gateway kills this subprocess before it can complete, so the service may never restart — reproducing issue #37453. The hermes gateway restart/stop guard (hermes_cli/gateway.py) and the cron-path guard (hermes_cli/cron.py) already block equivalent commands in their respective paths but the terminal tool had no such defense. Add a hard-block before command execution in terminal_tool: when _HERMES_GATEWAY=1 and the command matches _contains_gateway_lifecycle_command, return an error immediately. force=True cannot bypass it — unlike the normal dangerous-command approval flow, here even a user-approved restart would fail because the SIGTERM propagates to child processes. Also extend _GATEWAY_LIFECYCLE_PATTERNS to match systemctl with flags (e.g. systemctl --user restart) — the previous regex required the action word immediately after systemctl with no flags in between. Adds 9 regression tests: 6 blocked variants (parametrized), force bypass attempt, safe systemctl passthrough, and guard-inactive-outside-gateway. --- hermes_cli/cron.py | 2 +- tests/hermes_cli/test_gateway_restart_loop.py | 107 ++++++++++++++++++ tools/terminal_tool.py | 23 ++++ 3 files changed, 131 insertions(+), 1 deletion(-) diff --git a/hermes_cli/cron.py b/hermes_cli/cron.py index 717c1e97658..86f8e6b09e2 100644 --- a/hermes_cli/cron.py +++ b/hermes_cli/cron.py @@ -25,7 +25,7 @@ _GATEWAY_LIFECYCLE_PATTERNS = re.compile( r"(?i)" r"(hermes\s+gateway\s+(restart|stop|start))" r"|(launchctl\s+(kickstart|unload|load|stop|restart)\s+.*hermes)" - r"|(systemctl\s+(restart|stop|start)\s+.*hermes)" + r"|(systemctl\s+(-\S+\s+)*(restart|stop|start)\s+.*hermes)" r"|(p?kill\s+.*hermes.*gateway)" ) diff --git a/tests/hermes_cli/test_gateway_restart_loop.py b/tests/hermes_cli/test_gateway_restart_loop.py index d6c9bb06cec..74ee9e4934e 100644 --- a/tests/hermes_cli/test_gateway_restart_loop.py +++ b/tests/hermes_cli/test_gateway_restart_loop.py @@ -6,6 +6,7 @@ Covers: - _contains_gateway_lifecycle_command pattern matching """ +import json import os from argparse import Namespace @@ -250,3 +251,109 @@ class TestGatewaySelfTargetingGuard: args = Namespace(gateway_command="restart", all=False, system=False) with pytest.raises(_Reached): gw.gateway_command(args) + + +# --------------------------------------------------------------------------- +# Defense 3: terminal_tool hard-blocks gateway lifecycle commands inside gateway +# --------------------------------------------------------------------------- + +class TestTerminalToolGatewayLifecycleGuard: + """terminal_tool must refuse gateway lifecycle commands when _HERMES_GATEWAY=1. + + Issue #37453: systemctl --user restart hermes-gateway runs as a child of the + gateway process. When systemd delivers SIGTERM the gateway kills its own + restart command mid-execution — the service may never restart. The guard + must fire before execution, unconditionally (force=True cannot bypass it). + """ + + def _make_fake_env(self): + class _FakeEnv: + env = {} + def execute(self, command, **kwargs): # pragma: no cover + raise AssertionError("execute must not be reached") + return _FakeEnv() + + def _minimal_config(self): + return {"env_type": "local", "cwd": "/tmp", "timeout": 60, "lifetime_seconds": 3600} + + def _patch_env(self, monkeypatch, fake_env, *, inside_gateway: bool): + import tools.terminal_tool as tt + eid = "default" + monkeypatch.setattr(tt, "_active_environments", {eid: fake_env}) + monkeypatch.setattr(tt, "_last_activity", {eid: 0.0}) + monkeypatch.setattr(tt, "_task_env_overrides", {}) + monkeypatch.setattr(tt, "_get_env_config", self._minimal_config) + if inside_gateway: + monkeypatch.setenv("_HERMES_GATEWAY", "1") + else: + monkeypatch.delenv("_HERMES_GATEWAY", raising=False) + + @pytest.mark.parametrize("cmd", [ + "systemctl restart hermes-gateway", + "systemctl --user restart hermes-gateway", + "systemctl stop hermes-gateway.service", + "hermes gateway restart", + "launchctl kickstart gui/501/ai.hermes.gateway", + "pkill -f hermes.*gateway", + ]) + def test_blocks_lifecycle_commands_inside_gateway(self, monkeypatch, cmd): + import tools.terminal_tool as tt + self._patch_env(monkeypatch, self._make_fake_env(), inside_gateway=True) + + result = json.loads(tt.terminal_tool(command=cmd)) + + assert result["exit_code"] == 1 + assert "Blocked" in result["error"] + + def test_force_true_cannot_bypass_block(self, monkeypatch): + import tools.terminal_tool as tt + self._patch_env(monkeypatch, self._make_fake_env(), inside_gateway=True) + + result = json.loads(tt.terminal_tool( + command="systemctl restart hermes-gateway", force=True + )) + + assert result["exit_code"] == 1 + assert "Blocked" in result["error"] + + def test_safe_systemctl_commands_pass_through(self, monkeypatch): + """Non-hermes systemctl commands must not be blocked by this guard.""" + import tools.terminal_tool as tt + + calls = [] + + class _FakeEnv: + env = {} + def execute(self, command, **kwargs): + calls.append(command) + return {"output": "Active: running", "returncode": 0} + + self._patch_env(monkeypatch, _FakeEnv(), inside_gateway=True) + monkeypatch.setattr(tt, "_check_all_guards", lambda cmd, env: {"approved": True}) + + result = json.loads(tt.terminal_tool(command="systemctl status nginx")) + + assert result["exit_code"] == 0 + assert calls == ["systemctl status nginx"] + + def test_guard_inactive_outside_gateway(self, monkeypatch): + """Without _HERMES_GATEWAY=1 the lifecycle guard must not fire.""" + import tools.terminal_tool as tt + + calls = [] + + class _FakeEnv: + env = {} + def execute(self, command, **kwargs): + calls.append(command) + return {"output": "restarting...", "returncode": 0} + + self._patch_env(monkeypatch, _FakeEnv(), inside_gateway=False) + monkeypatch.setattr(tt, "_check_all_guards", lambda cmd, env: {"approved": True}) + + result = json.loads(tt.terminal_tool(command="systemctl restart hermes-gateway")) + + # Outside the gateway the lifecycle guard doesn't block — the normal + # approval flow handles it (here mocked as approved). + assert result["exit_code"] == 0 + assert calls == ["systemctl restart hermes-gateway"] diff --git a/tools/terminal_tool.py b/tools/terminal_tool.py index 71907a3a3cc..26d0f425c56 100644 --- a/tools/terminal_tool.py +++ b/tools/terminal_tool.py @@ -2058,6 +2058,29 @@ def terminal_tool( env = new_env logger.info("%s environment ready for task %s", env_type, effective_task_id[:8]) + # Hard-block: gateway lifecycle commands (systemctl/launchctl/hermes + # restart|stop targeting hermes-gateway) must never run inside the + # gateway process itself. The restart would SIGTERM the gateway, which + # kills this very subprocess before it can complete — the service may + # never restart. This mirrors the `hermes gateway restart` guard in + # hermes_cli/gateway.py and the cron-path guard in hermes_cli/cron.py, + # but applies unconditionally (force=True cannot help here). + if os.environ.get("_HERMES_GATEWAY") == "1": + from hermes_cli.cron import _contains_gateway_lifecycle_command + if _contains_gateway_lifecycle_command(command): + return json.dumps({ + "output": "", + "exit_code": 1, + "error": ( + "Blocked: cannot restart or stop the gateway from inside the " + "gateway process. The gateway would kill this command before " + "it could complete (SIGTERM propagates to child processes). " + "Run `hermes gateway restart` from a separate shell outside " + "the running gateway." + ), + "status": "error", + }, ensure_ascii=False) + # Pre-exec security checks (tirith + dangerous command detection) # Skip check if force=True (user has confirmed they want to run it) approval_note = None From a64fc490fe61dfe865e9b189aa5f4c5f1598b285 Mon Sep 17 00:00:00 2001 From: Ben Barclay <ben@nousresearch.com> Date: Fri, 19 Jun 2026 16:30:24 +1000 Subject: [PATCH 06/90] fix(relay): make hosted gateways actually connect AND complete the inbound/outbound round-trip (#48828) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(relay): enable RELAY platform + normalize dial URL so hosted gateways actually connect Three bugs blocked a self-provisioned hosted gateway from ever establishing its inbound relay WS (found while standing up the live staging end-to-end). Each masked the next; all three are needed for inbound to work. 1. RELAY platform never enabled in config.platforms (gateway/config.py). register_relay_adapter() puts the adapter in the platform_registry, but start_gateway()'s connect loop iterates self.config.platforms — which never contained Platform.RELAY. So the adapter was "registered" but never connected (logs showed "relay adapter registered" then "No messaging platforms enabled"). Fix: _apply_env_overrides now enables Platform.RELAY (mirroring relay_url into extra for the connected-checker) when GATEWAY_RELAY_URL (env) or gateway.relay_url (yaml) is set. Absent -> no RELAY entry (direct/ single-tenant gateways unaffected). 2. URL scheme not converted for the WS dial (gateway/relay/ws_transport.py). The relay URL is configured once as the http(s):// base (used as-is for the provision POST), but websockets.connect rejects http(s):// with "scheme isn't ws or wss". Fix: _ws_dial_url converts https->wss / http->ws. 3. /relay path not appended (same helper). The connector mounts its WebSocketServer at path "/relay" and returns HTTP 400 on an upgrade to any other path. GATEWAY_RELAY_URL is the base (no /relay), so the dial hit "/" -> 400. Fix: _ws_dial_url ensures the path ends in /relay. Idempotent — a URL already carrying ws(s):// and/or /relay is unchanged, so provision's _provision_url (which derives /relay/provision from either form) still works. Why the cross-repo E2E missed #2/#3: the stub connector binds ws://host:port and its websockets.serve accepts ANY path, so neither the scheme nor the /relay path was exercised. Real connector needs both. Verified live on staging hermes-agent-stg-automated-perception-5054: after the fixes the gateway logs "Connecting to relay..." -> "✓ relay connected" -> "Gateway running with 1 platform(s)" against wss://gateway-gateway.staging-nousresearch.com/relay, stable. Tests: added _ws_dial_url scheme+path+idempotency cases (test_ws_transport.py) and RELAY-platform-enablement cases for env + yaml + absent (test_config.py). Full gateway/relay + config suites green (191 passed). Relay-adapter lane. EXPERIMENTAL. * fix(relay): re-attach guild_id to outbound so connector egress resolves the tenant The final bug in the hosted-relay round-trip. Inbound worked end to end (Discord -> connector -> bus -> agent WS -> agent runs -> reply), but the reply's egress was declined by the connector: "discord egress declined: target not routed to an onboarded tenant". Cause: the connector's routedEgressGuard resolves the owning tenant from the OUTBOUND action's metadata.guild_id (Discord's routing discriminator). The gateway's generic delivery path builds outbound metadata via run.py _thread_metadata_for_source, which only carries thread_id (and returns None entirely for a non-threaded message) — so guild_id never reached the connector, tenant resolution failed, and the shared bot refused to post. Fix (relay-adapter-local, no perturbation of the generic delivery path or other platforms): RelayAdapter learns chat_id -> guild_id from each inbound event (_capture_scope) and re-attaches it to the outbound action's metadata in send() (_with_scope) when not already present. No-op for chats we never saw inbound (e.g. DMs) and never overwrites an explicit guild_id. Verified live on staging hermes-agent-stg-automated-perception-5054: an @mention in #general now produces a visible bot reply — full multi-tenant relay round-trip (real Discord -> shared connector bot -> tenant routing -> agent WS -> reply egress -> Discord). Tests: _capture_scope/_with_scope reattach, no-scope no-op, explicit-guild_id preserved (test_relay_adapter.py). Full relay + config suites green (160 passed). Relay-adapter lane. EXPERIMENTAL. --- gateway/config.py | 19 +++++++ gateway/relay/adapter.py | 36 ++++++++++++- gateway/relay/ws_transport.py | 31 ++++++++++- tests/gateway/relay/test_relay_adapter.py | 65 +++++++++++++++++++++++ tests/gateway/relay/test_ws_transport.py | 22 ++++++++ tests/gateway/test_config.py | 49 +++++++++++++++++ 6 files changed, 220 insertions(+), 2 deletions(-) diff --git a/gateway/config.py b/gateway/config.py index 0ebf23e12d0..c63b9523d73 100644 --- a/gateway/config.py +++ b/gateway/config.py @@ -2143,5 +2143,24 @@ def _apply_env_overrides(config: GatewayConfig) -> None: except Exception as e: logger.debug("Plugin platform enable pass failed: %s", e) + # Relay (generic connector-fronted platform, EXPERIMENTAL). Enabled when a + # connector relay URL is configured via GATEWAY_RELAY_URL (env) or + # gateway.relay_url (config.yaml). The adapter is registered into the + # platform_registry at gateway startup (gateway.relay.register_relay_adapter) + # and dials OUT to the connector — so, like Telegram/Matrix, it has no public + # inbound port and just needs Platform.RELAY present+enabled in + # config.platforms for start_gateway()'s connect loop to bring it up. The + # connected-checker (Platform.RELAY in _PLATFORM_CONNECTED_CHECKERS) keys on + # extra["relay_url"], so mirror the URL into extra here. + relay_url_env = os.getenv("GATEWAY_RELAY_URL", "").strip() + relay_url_yaml = "" + existing_relay = config.platforms.get(Platform.RELAY) + if existing_relay is not None: + relay_url_yaml = str(existing_relay.extra.get("relay_url") or "").strip() + relay_url_val = relay_url_env or relay_url_yaml + if relay_url_val: + relay_config = _enable_from_env(Platform.RELAY) + relay_config.extra["relay_url"] = relay_url_val.rstrip("/") + for platform_config in config.platforms.values(): platform_config.extra.pop("_enabled_explicit", None) diff --git a/gateway/relay/adapter.py b/gateway/relay/adapter.py index fc4e5f40ee7..a1a7826f8f8 100644 --- a/gateway/relay/adapter.py +++ b/gateway/relay/adapter.py @@ -57,6 +57,13 @@ class RelayAdapter(BasePlatformAdapter): self._transport = transport # Capability surface read by stream_consumer (getattr(..., 4096)). self.MAX_MESSAGE_LENGTH = descriptor.max_message_length + # chat_id -> guild_id (Discord) / workspace scope, learned from inbound + # events. The connector's egress guard resolves the owning tenant from + # the OUTBOUND action's metadata.guild_id; the gateway's generic delivery + # path (run.py _thread_metadata_for_source) only carries thread_id, so we + # re-attach the scope here from what we saw inbound. Keyed by chat_id + # (channel) since that's what send() receives. See routedEgressGuard.ts. + self._scope_by_chat: Dict[str, str] = {} self.supports_code_blocks = descriptor.markdown_dialect not in ("", "plain") # ── capability surface (from descriptor) ───────────────────────────── @@ -108,8 +115,35 @@ class RelayAdapter(BasePlatformAdapter): async def _on_inbound(self, event) -> None: """Bridge a connector-delivered MessageEvent into the normal adapter path.""" + self._capture_scope(event) await self.handle_message(event) + def _capture_scope(self, event) -> None: + """Remember chat_id -> guild scope from an inbound event so our outbound + (the agent's reply) can re-assert it for the connector's egress tenant + resolution. Never raises — scope tracking must not break inbound.""" + try: + src = getattr(event, "source", None) + scope = getattr(src, "guild_id", None) if src else None + chat = getattr(src, "chat_id", None) if src else None + if scope and chat: + self._scope_by_chat[str(chat)] = str(scope) + except Exception: # noqa: BLE001 - scope tracking must never break inbound + pass + + def _with_scope(self, chat_id: str, metadata: Optional[Dict[str, Any]]) -> Dict[str, Any]: + """Ensure the outbound metadata carries guild_id for the connector's + egress tenant resolution. The connector resolves the owning tenant from + metadata.guild_id (Discord); without it egress is declined as + 'target not routed to an onboarded tenant'. No-op when we have no scope + for this chat (e.g. DMs) or it's already present.""" + meta: Dict[str, Any] = dict(metadata or {}) + if not meta.get("guild_id"): + scope = self._scope_by_chat.get(str(chat_id)) + if scope: + meta["guild_id"] = scope + return meta + async def on_interrupt(self, session_key: str, chat_id: str) -> None: """Bridge a connector-delivered /stop into the adapter's interrupt path. @@ -140,7 +174,7 @@ class RelayAdapter(BasePlatformAdapter): "chat_id": chat_id, "content": content, "reply_to": reply_to, - "metadata": metadata or {}, + "metadata": self._with_scope(chat_id, metadata), } ) return SendResult( diff --git a/gateway/relay/ws_transport.py b/gateway/relay/ws_transport.py index b2e8eda09cd..b091d44faa8 100644 --- a/gateway/relay/ws_transport.py +++ b/gateway/relay/ws_transport.py @@ -54,6 +54,35 @@ _HANDSHAKE_TIMEOUT_S = 30.0 _OUTBOUND_TIMEOUT_S = 30.0 +def _ws_dial_url(url: str) -> str: + """Normalize a connector URL to the ``ws(s)://…/relay`` dial target. + + The relay URL is configured once (``GATEWAY_RELAY_URL`` / ``gateway.relay_url``) + as the connector's BASE URL (e.g. ``https://connector.example``) and shared by + both the provision POST (which needs ``http(s)://…/relay/provision`` — see + ``_provision_url``) and the WS dial (which needs ``ws(s)://…/relay``, the path + the connector mounts its ``WebSocketServer`` on). Two normalizations, both + load-bearing: + + - scheme: ``https -> wss``, ``http -> ws`` (``websockets.connect`` raises + "scheme isn't ws or wss" on an http(s) URL). + - path: ensure it ends in ``/relay`` (the connector returns HTTP 400 on an + upgrade to any other path, since the WS server is mounted at ``/relay``). + + Idempotent: an already-``ws(s)://…/relay`` URL is returned unchanged, so a URL + configured WITH the scheme and/or ``/relay`` still works. + """ + raw = (url or "").strip() + if raw.startswith("https://"): + raw = "wss://" + raw[len("https://"):] + elif raw.startswith("http://"): + raw = "ws://" + raw[len("http://"):] + raw = raw.rstrip("/") + if not raw.endswith("/relay"): + raw = f"{raw}/relay" + return raw + + def _event_from_wire(raw: Dict[str, Any]) -> MessageEvent: """Rebuild a MessageEvent from the connector's normalized inbound payload. @@ -118,7 +147,7 @@ class WebSocketRelayTransport: "WebSocketRelayTransport requires the 'websockets' package " "(install the messaging extra)." ) - self._url = url + self._url = _ws_dial_url(url) self._platform = platform self._bot_id = bot_id self._connect_timeout_s = connect_timeout_s diff --git a/tests/gateway/relay/test_relay_adapter.py b/tests/gateway/relay/test_relay_adapter.py index 64d6aab2f86..f176eb5728c 100644 --- a/tests/gateway/relay/test_relay_adapter.py +++ b/tests/gateway/relay/test_relay_adapter.py @@ -75,3 +75,68 @@ async def test_send_without_transport_returns_failure(): result = await a.send("chat1", "hello") assert result.success is False assert result.error == "no transport" + + +class _CaptureTransport: + """Minimal RelayTransport stand-in that records the outbound action.""" + + def __init__(self): + self.sent = None + + def set_inbound_handler(self, h): # noqa: D401 + self._h = h + + async def send_outbound(self, action): + self.sent = action + return {"success": True, "message_id": "m1"} + + +def _make_event(chat_id="chan-1", guild_id="guild-9"): + from gateway.platforms.base import MessageEvent, MessageType + from gateway.session import SessionSource + + src = SessionSource( + platform=Platform.RELAY, + chat_id=chat_id, + chat_type="channel", + guild_id=guild_id, + ) + return MessageEvent(text="hi", source=src, message_type=MessageType.TEXT) + + +@pytest.mark.asyncio +async def test_send_reattaches_guild_id_from_inbound_scope(): + """The connector's egress guard resolves the owning tenant from + metadata.guild_id; the gateway's generic delivery path drops it, so the + relay adapter must re-attach the guild scope learned from the inbound event. + Regression for live 'discord egress declined: target not routed to an + onboarded tenant'.""" + t = _CaptureTransport() + a = RelayAdapter(PlatformConfig(), make_desc(platform="discord"), transport=t) + # Simulate the connector delivering an inbound message in guild-9 / chan-1, + # but don't run the full handle_message pipeline — just the scope capture. + a._capture_scope(_make_event(chat_id="chan-1", guild_id="guild-9")) + + await a.send("chan-1", "the reply") + + assert t.sent["metadata"].get("guild_id") == "guild-9" + + +@pytest.mark.asyncio +async def test_send_without_known_scope_omits_guild_id(): + """A chat we never saw inbound (e.g. a DM) gets no guild_id — no-op, never + invents a scope.""" + t = _CaptureTransport() + a = RelayAdapter(PlatformConfig(), make_desc(platform="discord"), transport=t) + await a.send("unknown-chat", "hi") + assert "guild_id" not in t.sent["metadata"] + + +@pytest.mark.asyncio +async def test_send_preserves_explicit_guild_id(): + """An explicitly-provided metadata.guild_id is never overwritten.""" + t = _CaptureTransport() + a = RelayAdapter(PlatformConfig(), make_desc(platform="discord"), transport=t) + a._capture_scope(_make_event(chat_id="chan-1", guild_id="guild-9")) + await a.send("chan-1", "hi", metadata={"guild_id": "explicit-1"}) + assert t.sent["metadata"]["guild_id"] == "explicit-1" diff --git a/tests/gateway/relay/test_ws_transport.py b/tests/gateway/relay/test_ws_transport.py index dcb3f6c714f..00aa9b43327 100644 --- a/tests/gateway/relay/test_ws_transport.py +++ b/tests/gateway/relay/test_ws_transport.py @@ -177,3 +177,25 @@ async def test_disconnect_fails_pending_waiters_cleanly(server): # After disconnect, an outbound returns a structured failure rather than hanging. result = await t.send_outbound({"op": "send", "chat_id": "c", "content": "x"}) assert result["success"] is False + + +def test_https_url_normalized_to_wss(): + """The relay URL is configured once as the http(s):// BASE (for the provision + POST), but websockets.connect needs ws(s):// and the connector mounts its WS + server at /relay. The transport must convert scheme AND ensure the /relay + path. Regression for the live staging failures 'scheme isn't ws or wss' then + 'server rejected WebSocket connection: HTTP 400' (wrong path).""" + t = WebSocketRelayTransport("https://connector.example", "discord", "b") + assert t._url == "wss://connector.example/relay" + t2 = WebSocketRelayTransport("http://connector.local:8080", "discord", "b") + assert t2._url == "ws://connector.local:8080/relay" + + +def test_ws_dial_url_idempotent_with_scheme_and_path(): + # Already ws(s):// and/or already ending in /relay -> unchanged (no double append). + t = WebSocketRelayTransport("wss://connector.example/relay", "discord", "b") + assert t._url == "wss://connector.example/relay" + t2 = WebSocketRelayTransport("https://connector.example/relay/", "discord", "b") + assert t2._url == "wss://connector.example/relay" + t3 = WebSocketRelayTransport("ws://127.0.0.1:9", "discord", "b") + assert t3._url == "ws://127.0.0.1:9/relay" diff --git a/tests/gateway/test_config.py b/tests/gateway/test_config.py index 9e74dd355ad..9f38f9b8a0d 100644 --- a/tests/gateway/test_config.py +++ b/tests/gateway/test_config.py @@ -311,6 +311,55 @@ class TestLoadGatewayConfig: assert config.quick_commands == {"limits": {"type": "exec", "command": "echo ok"}} + def test_relay_platform_enabled_from_env_url(self, tmp_path, monkeypatch): + """GATEWAY_RELAY_URL must enable Platform.RELAY in config.platforms so + start_gateway()'s connect loop actually dials the connector. Registering + the adapter in the platform_registry is NOT enough — the connect loop + iterates config.platforms, so an un-enabled RELAY never connects (the + 'relay registered but no inbound' bug).""" + hermes_home = tmp_path / ".hermes" + hermes_home.mkdir() + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + monkeypatch.setenv("GATEWAY_RELAY_URL", "https://connector.example/relay/") + + config = load_gateway_config() + + assert Platform.RELAY in config.platforms + relay = config.platforms[Platform.RELAY] + assert relay.enabled is True + # Trailing slash stripped; mirrored into extra for the connected-checker. + assert relay.extra.get("relay_url") == "https://connector.example/relay" + assert Platform.RELAY in config.get_connected_platforms() + + def test_relay_platform_absent_when_url_unset(self, tmp_path, monkeypatch): + """No relay URL -> no RELAY platform, so direct/single-tenant gateways + are unaffected.""" + hermes_home = tmp_path / ".hermes" + hermes_home.mkdir() + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + monkeypatch.delenv("GATEWAY_RELAY_URL", raising=False) + + config = load_gateway_config() + + assert Platform.RELAY not in config.platforms + + def test_relay_platform_enabled_from_config_yaml(self, tmp_path, monkeypatch): + """gateway.relay_url in config.yaml also enables RELAY (env-less path).""" + hermes_home = tmp_path / ".hermes" + hermes_home.mkdir() + config_path = hermes_home / "config.yaml" + config_path.write_text( + "gateway:\n platforms:\n relay:\n extra:\n relay_url: https://connector.example/relay\n", + encoding="utf-8", + ) + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + monkeypatch.delenv("GATEWAY_RELAY_URL", raising=False) + + config = load_gateway_config() + + assert Platform.RELAY in config.platforms + assert config.platforms[Platform.RELAY].enabled is True + def test_bridges_group_sessions_per_user_from_config_yaml(self, tmp_path, monkeypatch): hermes_home = tmp_path / ".hermes" hermes_home.mkdir() From 12dfcfdf73ed0543617ce0f4779aae8a9acb1e33 Mon Sep 17 00:00:00 2001 From: Shannon Sands <shannon.sands.1979@gmail.com> Date: Fri, 19 Jun 2026 16:11:55 +1000 Subject: [PATCH 07/90] fix(tui): restart dashboard chat on idle exit hotkeys --- hermes_cli/web_server.py | 1 + tests/hermes_cli/test_web_server.py | 1 + ui-tui/src/__tests__/gatewayClient.test.ts | 40 +++++++++++++++++++ ui-tui/src/__tests__/gracefulExit.test.ts | 11 +++++ ui-tui/src/__tests__/useInputHandlers.test.ts | 39 +++++++++++++++++- ui-tui/src/app/useInputHandlers.ts | 36 +++++++++++++++-- ui-tui/src/config/env.ts | 8 ++++ ui-tui/src/entry.tsx | 9 ++++- ui-tui/src/gatewayClient.ts | 7 ++++ ui-tui/src/gatewayTypes.ts | 1 + ui-tui/src/lib/gracefulExit.ts | 28 +++++++++++-- web/src/components/ChatSidebar.tsx | 23 ++++++++--- web/src/pages/ChatPage.tsx | 21 +++++++++- 13 files changed, 207 insertions(+), 18 deletions(-) create mode 100644 ui-tui/src/__tests__/gracefulExit.test.ts diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index b2544ce9d77..ba6f4277deb 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -10830,6 +10830,7 @@ def _resolve_chat_argv( # the dashboard PTY path. env.setdefault("HERMES_TUI_DISABLE_MOUSE", "1") env.setdefault("HERMES_TUI_INLINE", "1") + env["HERMES_TUI_DASHBOARD"] = "1" if profile_dir is not None: env["HERMES_HOME"] = str(profile_dir) diff --git a/tests/hermes_cli/test_web_server.py b/tests/hermes_cli/test_web_server.py index e0ad77dfc8a..e65a28101cd 100644 --- a/tests/hermes_cli/test_web_server.py +++ b/tests/hermes_cli/test_web_server.py @@ -5062,6 +5062,7 @@ class TestPtyWebSocket: _argv, _cwd, env = self.ws_module._resolve_chat_argv() + assert env["HERMES_TUI_DASHBOARD"] == "1" assert env["HERMES_TUI_INLINE"] == "1" assert env["HERMES_TUI_DISABLE_MOUSE"] == "1" diff --git a/ui-tui/src/__tests__/gatewayClient.test.ts b/ui-tui/src/__tests__/gatewayClient.test.ts index a872a008ddb..43d96add35a 100644 --- a/ui-tui/src/__tests__/gatewayClient.test.ts +++ b/ui-tui/src/__tests__/gatewayClient.test.ts @@ -187,6 +187,46 @@ describe('GatewayClient websocket attach mode', () => { gw.kill() }) + it('publishes local dashboard-control events to the sidecar websocket', async () => { + process.env.HERMES_TUI_GATEWAY_URL = 'ws://gateway.test/api/ws?token=abc' + process.env.HERMES_TUI_SIDECAR_URL = 'ws://gateway.test/api/pub?token=abc&channel=demo' + + const gw = new GatewayClient() + const seen: string[] = [] + + gw.on('event', ev => seen.push(ev.type)) + gw.start() + + const gatewaySocket = FakeWebSocket.instances[0]! + + gatewaySocket.open() + await vi.waitFor(() => expect(FakeWebSocket.instances).toHaveLength(2)) + + const sidecarSocket = FakeWebSocket.instances[1]! + + sidecarSocket.open() + gw.drain() + + gw.publishLocalEvent({ + payload: { reason: 'idle_exit_hotkey' }, + session_id: 'sid-old', + type: 'dashboard.new_session_requested' + }) + + expect(seen).toContain('dashboard.new_session_requested') + expect(JSON.parse(sidecarSocket.sent.at(-1) ?? '{}')).toEqual({ + jsonrpc: '2.0', + method: 'event', + params: { + payload: { reason: 'idle_exit_hotkey' }, + session_id: 'sid-old', + type: 'dashboard.new_session_requested' + } + }) + + gw.kill() + }) + it('emits exit when attached websocket closes', () => { process.env.HERMES_TUI_GATEWAY_URL = 'ws://gateway.test/api/ws?token=abc' const gw = new GatewayClient() diff --git a/ui-tui/src/__tests__/gracefulExit.test.ts b/ui-tui/src/__tests__/gracefulExit.test.ts new file mode 100644 index 00000000000..6c805dfce7c --- /dev/null +++ b/ui-tui/src/__tests__/gracefulExit.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from 'vitest' + +import { shouldExitForSignal } from '../lib/gracefulExit.js' + +describe('shouldExitForSignal', () => { + it('ignores only the signals explicitly disabled for embedded dashboard chat', () => { + expect(shouldExitForSignal('SIGINT', ['SIGINT'])).toBe(false) + expect(shouldExitForSignal('SIGTERM', ['SIGINT'])).toBe(true) + expect(shouldExitForSignal('SIGHUP', ['SIGINT'])).toBe(true) + }) +}) diff --git a/ui-tui/src/__tests__/useInputHandlers.test.ts b/ui-tui/src/__tests__/useInputHandlers.test.ts index 0d3fd69c1ed..fa9372d5356 100644 --- a/ui-tui/src/__tests__/useInputHandlers.test.ts +++ b/ui-tui/src/__tests__/useInputHandlers.test.ts @@ -1,6 +1,11 @@ import { describe, expect, it, vi } from 'vitest' -import { applyVoiceRecordResponse, shouldFallThroughForScroll } from '../app/useInputHandlers.js' +import { + applyVoiceRecordResponse, + handleIdleHotkeyExit, + shouldAllowIdleHotkeyExit, + shouldFallThroughForScroll +} from '../app/useInputHandlers.js' const baseKey = { downArrow: false, @@ -42,6 +47,38 @@ describe('shouldFallThroughForScroll — keep transcript scrolling alive during }) }) +describe('shouldAllowIdleHotkeyExit', () => { + it('keeps idle exit hotkeys enabled in normal terminals', () => { + expect(shouldAllowIdleHotkeyExit(false)).toBe(true) + }) + + it('disables idle exit hotkeys in dashboard chat', () => { + expect(shouldAllowIdleHotkeyExit(true)).toBe(false) + }) +}) + +describe('handleIdleHotkeyExit', () => { + it('exits in normal terminals', () => { + const actions = { die: vi.fn(), sys: vi.fn() } + + handleIdleHotkeyExit(actions, false) + + expect(actions.die).toHaveBeenCalledTimes(1) + expect(actions.sys).not.toHaveBeenCalled() + }) + + it('asks the dashboard for a fresh chat instead of leaving a ghost session', () => { + const actions = { die: vi.fn(), sys: vi.fn() } + const requestDashboardNewSession = vi.fn() + + handleIdleHotkeyExit(actions, true, requestDashboardNewSession) + + expect(actions.die).not.toHaveBeenCalled() + expect(requestDashboardNewSession).toHaveBeenCalledTimes(1) + expect(actions.sys).toHaveBeenCalledWith('starting a fresh dashboard chat...') + }) +}) + describe('applyVoiceRecordResponse', () => { it('reverts optimistic REC state when the gateway reports voice busy', () => { const setProcessing = vi.fn() diff --git a/ui-tui/src/app/useInputHandlers.ts b/ui-tui/src/app/useInputHandlers.ts index 20d3493f547..f19cccfe5b5 100644 --- a/ui-tui/src/app/useInputHandlers.ts +++ b/ui-tui/src/app/useInputHandlers.ts @@ -2,6 +2,7 @@ import { forceRedraw, useInput } from '@hermes/ink' import { useStore } from '@nanostores/react' import { useEffect, useRef } from 'react' +import { DASHBOARD_TUI_MODE } from '../config/env.js' import { TYPING_IDLE_MS } from '../config/timing.js' import type { ApprovalRespondResponse, @@ -15,13 +16,30 @@ import { computePrecisionWheelStep, initPrecisionWheel } from '../lib/precisionW import { computeWheelStep, initWheelAccelForHost } from '../lib/wheelAccel.js' import { getInputSelection } from './inputSelectionStore.js' -import type { InputHandlerContext, InputHandlerResult } from './interfaces.js' +import type { InputHandlerActions, InputHandlerContext, InputHandlerResult } from './interfaces.js' import { $isBlocked, $overlayState, patchOverlayState } from './overlayStore.js' import { turnController } from './turnController.js' import { patchTurnState } from './turnStore.js' import { getUiState } from './uiStore.js' const isCtrl = (key: { ctrl: boolean }, ch: string, target: string) => key.ctrl && ch.toLowerCase() === target +const DASHBOARD_NEW_SESSION_MESSAGE = 'starting a fresh dashboard chat...' + +export const shouldAllowIdleHotkeyExit = (dashboardTuiMode = DASHBOARD_TUI_MODE) => !dashboardTuiMode + +export function handleIdleHotkeyExit( + actions: Pick<InputHandlerActions, 'die' | 'sys'>, + dashboardTuiMode = DASHBOARD_TUI_MODE, + requestDashboardNewSession?: () => void +) { + if (!shouldAllowIdleHotkeyExit(dashboardTuiMode)) { + requestDashboardNewSession?.() + + return actions.sys(DASHBOARD_NEW_SESSION_MESSAGE) + } + + return actions.die() +} /** * Approval / clarify / confirm overlays mount their own `useInput` handlers @@ -505,11 +523,23 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult { return cActions.clearIn() } - return actions.die() + return handleIdleHotkeyExit(actions, DASHBOARD_TUI_MODE, () => { + gateway.gw.publishLocalEvent({ + payload: { reason: 'idle_exit_hotkey' }, + session_id: live.sid ?? undefined, + type: 'dashboard.new_session_requested' + }) + }) } if (isAction(key, ch, 'd')) { - return actions.die() + return handleIdleHotkeyExit(actions, DASHBOARD_TUI_MODE, () => { + gateway.gw.publishLocalEvent({ + payload: { reason: 'idle_exit_hotkey' }, + session_id: live.sid ?? undefined, + type: 'dashboard.new_session_requested' + }) + }) } if (isAction(key, ch, 'l')) { diff --git a/ui-tui/src/config/env.ts b/ui-tui/src/config/env.ts index 3b5b9bee4d4..843512ed76a 100644 --- a/ui-tui/src/config/env.ts +++ b/ui-tui/src/config/env.ts @@ -1,4 +1,5 @@ import type { MouseTrackingMode } from '@hermes/ink' + import { isTermuxTuiMode } from '../lib/termux.js' const truthy = (v?: string) => /^(?:1|true|yes|on)$/i.test((v ?? '').trim()) @@ -43,12 +44,19 @@ export const STARTUP_IMAGE = (process.env.HERMES_TUI_IMAGE ?? '').trim() // behavior. const mouseTrackingOverride = parseToggle(process.env.HERMES_TUI_MOUSE_TRACKING) const mouseTrackingDisabledLegacy = truthy(process.env.HERMES_TUI_DISABLE_MOUSE) + const resolvedBootMouseEnabled = mouseTrackingOverride ?? (TERMUX_TUI_MODE ? false : !mouseTrackingDisabledLegacy) + export const MOUSE_TRACKING: MouseTrackingMode = resolvedBootMouseEnabled ? 'all' : 'off' export const NO_CONFIRM_DESTRUCTIVE = truthy(process.env.HERMES_TUI_NO_CONFIRM) +// Set by the dashboard PTY launcher. This is intentionally narrower than +// INLINE_MODE: users can opt into inline terminal rendering locally, but the +// browser-embedded TUI has no healthy restart path after an idle exit. +export const DASHBOARD_TUI_MODE = truthy(process.env.HERMES_TUI_DASHBOARD) + // HERMES_DEV_CREDITS — dev-only live-spend readout (Δ status segment + "(dev credits)" // banner). Throwaway dev scaffolding; the whole readout gates on this one flag. export const DEV_CREDITS_MODE = truthy(process.env.HERMES_DEV_CREDITS) diff --git a/ui-tui/src/entry.tsx b/ui-tui/src/entry.tsx index 22fee6bccbd..de60d966760 100644 --- a/ui-tui/src/entry.tsx +++ b/ui-tui/src/entry.tsx @@ -5,7 +5,7 @@ import './lib/forceTruecolor.js' import type { FrameEvent } from '@hermes/ink' -import { TERMUX_TUI_MODE } from './config/env.js' +import { DASHBOARD_TUI_MODE, TERMUX_TUI_MODE } from './config/env.js' import { GatewayClient } from './gatewayClient.js' import { setupGracefulExit } from './lib/gracefulExit.js' import { formatBytes, type HeapDumpResult, performHeapDump } from './lib/memory.js' @@ -76,7 +76,12 @@ setupGracefulExit({ recordParentLifecycle(`graceful-exit received signal=${signal} → killing gateway`) resetTerminalModes() process.stderr.write(`hermes-tui lifecycle: received ${signal}\n`) - } + }, + // The dashboard chat tab has no in-page restart path after the PTY child + // exits. Ignore SIGINT there so Ctrl+C cannot kill the embedded TUI if raw + // mode briefly drops and the terminal driver turns the keystroke into a + // signal instead of input bytes. SIGTERM/SIGHUP still cleanly shut down. + ignoredSignals: DASHBOARD_TUI_MODE ? ['SIGINT'] : [] }) const stopMemoryMonitor = startMemoryMonitor({ diff --git a/ui-tui/src/gatewayClient.ts b/ui-tui/src/gatewayClient.ts index 5dfbe880fb1..88ddc0fcdc3 100644 --- a/ui-tui/src/gatewayClient.ts +++ b/ui-tui/src/gatewayClient.ts @@ -307,6 +307,13 @@ export class GatewayClient extends EventEmitter { } } + publishLocalEvent(ev: GatewayEvent) { + const frame = JSON.stringify({ jsonrpc: '2.0', method: 'event', params: ev }) + + this.mirrorEventToSidecar(frame) + this.publish(ev) + } + private handleWebSocketFrame(raw: unknown) { const text = asWireText(raw) diff --git a/ui-tui/src/gatewayTypes.ts b/ui-tui/src/gatewayTypes.ts index 016171008c1..74a6f7627d1 100644 --- a/ui-tui/src/gatewayTypes.ts +++ b/ui-tui/src/gatewayTypes.ts @@ -634,6 +634,7 @@ export type GatewayEvent = } | { payload?: { state?: 'idle' | 'listening' | 'transcribing' }; session_id?: string; type: 'voice.status' } | { payload?: { no_speech_limit?: boolean; text?: string }; session_id?: string; type: 'voice.transcript' } + | { payload?: { reason?: string }; session_id?: string; type: 'dashboard.new_session_requested' } | { payload: { line: string }; session_id?: string; type: 'gateway.stderr' } | { payload?: { level?: 'info' | 'warn' | 'error'; message?: string } diff --git a/ui-tui/src/lib/gracefulExit.ts b/ui-tui/src/lib/gracefulExit.ts index 2896fd12651..089269ac1ae 100644 --- a/ui-tui/src/lib/gracefulExit.ts +++ b/ui-tui/src/lib/gracefulExit.ts @@ -1,11 +1,16 @@ interface SetupOptions { cleanups?: (() => Promise<void> | void)[] failsafeMs?: number + ignoredSignals?: GracefulSignal[] onError?: (scope: 'uncaughtException' | 'unhandledRejection', err: unknown) => void onSignal?: (signal: NodeJS.Signals) => void } -const SIGNAL_EXIT_CODE: Record<'SIGHUP' | 'SIGINT' | 'SIGTERM', number> = { +export type GracefulSignal = 'SIGHUP' | 'SIGINT' | 'SIGTERM' + +const SIGNALS: readonly GracefulSignal[] = ['SIGINT', 'SIGTERM', 'SIGHUP'] + +const SIGNAL_EXIT_CODE: Record<GracefulSignal, number> = { SIGHUP: 129, SIGINT: 130, SIGTERM: 143 @@ -13,7 +18,16 @@ const SIGNAL_EXIT_CODE: Record<'SIGHUP' | 'SIGINT' | 'SIGTERM', number> = { let wired = false -export function setupGracefulExit({ cleanups = [], failsafeMs = 4000, onError, onSignal }: SetupOptions = {}) { +export const shouldExitForSignal = (signal: GracefulSignal, ignoredSignals: readonly GracefulSignal[] = []) => + !ignoredSignals.includes(signal) + +export function setupGracefulExit({ + cleanups = [], + failsafeMs = 4000, + ignoredSignals = [], + onError, + onSignal +}: SetupOptions = {}) { if (wired) { return } @@ -38,8 +52,14 @@ export function setupGracefulExit({ cleanups = [], failsafeMs = 4000, onError, o void Promise.allSettled(cleanups.map(fn => Promise.resolve().then(fn))).finally(() => process.exit(code)) } - for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP'] as const) { - process.on(sig, () => exit(SIGNAL_EXIT_CODE[sig], sig)) + for (const sig of SIGNALS) { + process.on(sig, () => { + if (!shouldExitForSignal(sig, ignoredSignals)) { + return + } + + exit(SIGNAL_EXIT_CODE[sig], sig) + }) } process.on('uncaughtException', err => onError?.('uncaughtException', err)) diff --git a/web/src/components/ChatSidebar.tsx b/web/src/components/ChatSidebar.tsx index 1a53741d8fd..e6e3437781a 100644 --- a/web/src/components/ChatSidebar.tsx +++ b/web/src/components/ChatSidebar.tsx @@ -74,9 +74,15 @@ interface ChatSidebarProps { /** Management profile from the dashboard switcher — scopes session.create. */ profile?: string; className?: string; + onDashboardNewSessionRequest?: () => void; } -export function ChatSidebar({ channel, profile, className }: ChatSidebarProps) { +export function ChatSidebar({ + channel, + profile, + className, + onDashboardNewSessionRequest, +}: ChatSidebarProps) { // `version` bumps on reconnect; gw is derived so we never call setState // for it inside an effect (React 19's set-state-in-effect rule). The // counter is the dependency on purpose — it's not read in the memo body, @@ -112,9 +118,12 @@ export function ChatSidebar({ channel, profile, className }: ChatSidebarProps) { useEffect(() => { let cancelled = false; - setSessionId(null); - setInfo({}); - setError(null); + queueMicrotask(() => { + if (cancelled) return; + setSessionId(null); + setInfo({}); + setError(null); + }); const offState = gw.onState(setState); const offSessionInfo = gw.on<SessionInfo>("session.info", (ev) => { @@ -233,7 +242,9 @@ export function ChatSidebar({ channel, profile, className }: ChatSidebarProps) { const { type, payload } = frame.params; - if (type === "tool.start") { + if (type === "dashboard.new_session_requested") { + onDashboardNewSessionRequest?.(); + } else if (type === "tool.start") { const p = payload as | { tool_id?: string; name?: string; context?: string } | undefined; @@ -309,7 +320,7 @@ export function ChatSidebar({ channel, profile, className }: ChatSidebarProps) { unmounting = true; ws?.close(); }; - }, [channel, version]); + }, [channel, onDashboardNewSessionRequest, version]); const reconnect = useCallback(() => { setError(null); diff --git a/web/src/pages/ChatPage.tsx b/web/src/pages/ChatPage.tsx index 4e3a6c23151..dcb006e0da2 100644 --- a/web/src/pages/ChatPage.tsx +++ b/web/src/pages/ChatPage.tsx @@ -153,6 +153,15 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) { setBanner(null); setReconnectNonce((n) => n + 1); }, []); + const startFreshDashboardChat = useCallback(() => { + const next = new URLSearchParams(searchParams); + + next.delete("resume"); + setSearchParams(next, { replace: true }); + setSessionEnded(false); + setBanner(null); + setReconnectNonce((n) => n + 1); + }, [searchParams, setSearchParams]); // Raw state for the mobile side-sheet + a derived value that force- // closes whenever the chat tab isn't active. The *derived* value is // what side-effects (body-scroll lock, keydown listener, portal render) @@ -881,7 +890,11 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) { "border-t border-current/10", )} > - <ChatSidebar channel={channel} profile={scopedProfile} /> + <ChatSidebar + channel={channel} + profile={scopedProfile} + onDashboardNewSessionRequest={startFreshDashboardChat} + /> </div> </div> </>, @@ -967,7 +980,11 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) { className="flex min-h-0 shrink-0 flex-col overflow-hidden lg:h-full lg:w-80" > <div className="min-h-0 flex-1 overflow-hidden"> - <ChatSidebar channel={channel} profile={scopedProfile} /> + <ChatSidebar + channel={channel} + profile={scopedProfile} + onDashboardNewSessionRequest={startFreshDashboardChat} + /> </div> </div> )} From f741e70791c1c69b501fdb98da80bec3e4d130c0 Mon Sep 17 00:00:00 2001 From: Shannon Sands <shannon.sands.1979@gmail.com> Date: Fri, 19 Jun 2026 14:27:42 +1000 Subject: [PATCH 08/90] Add Slack allowed users setup field --- hermes_cli/config.py | 7 +++++ hermes_cli/web_server.py | 22 ++++++++++++-- tests/hermes_cli/test_web_server.py | 47 +++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/hermes_cli/config.py b/hermes_cli/config.py index f698c11d5ac..8c790e7e856 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -3439,6 +3439,13 @@ OPTIONAL_ENV_VARS = { "password": True, "category": "messaging", }, + "SLACK_ALLOWED_USERS": { + "description": "Comma-separated Slack member IDs allowed to use Hermes, e.g. U01ABC2DEF3. Without this, Slack may connect but deny messages by default.", + "prompt": "Allowed Slack member IDs", + "url": "https://api.slack.com/apps", + "password": False, + "category": "messaging", + }, "MATTERMOST_URL": { "description": "Mattermost server URL (e.g. https://mm.example.com)", "prompt": "Mattermost server URL", diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 2dbb316d32d..b1320875c53 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -2325,6 +2325,23 @@ def _gateway_display_command(profile: Optional[str], verb: str) -> str: return " ".join(["hermes", *_gateway_subcommand(profile, verb)]) +def _validate_messaging_env_value(platform_id: str, key: str, value: str) -> None: + """Reject platform credentials that are clearly in the wrong field.""" + if platform_id != "slack" or not value: + return + + if key == "SLACK_BOT_TOKEN" and not value.startswith("xoxb-"): + raise HTTPException( + status_code=400, + detail="Slack Bot Token must start with xoxb-. Paste the bot token from OAuth & Permissions.", + ) + if key == "SLACK_APP_TOKEN" and not value.startswith("xapp-"): + raise HTTPException( + status_code=400, + detail="Slack App Token must start with xapp-. Paste the app-level token from Basic Information > App-Level Tokens.", + ) + + def _spawn_gateway_restart(profile: Optional[str] = None) -> Tuple[subprocess.Popen, bool]: """Spawn ``hermes gateway restart``, reusing an in-flight restart. @@ -4155,9 +4172,9 @@ _PLATFORM_OVERRIDES: dict[str, dict[str, Any]] = { }, "slack": { "name": "Slack", - "description": "Use Hermes from Slack via Socket Mode.", + "description": "Use Hermes from Slack via Socket Mode. Add allowed Slack member IDs so connected bots can respond.", "docs_url": "https://api.slack.com/apps", - "env_vars": ("SLACK_BOT_TOKEN", "SLACK_APP_TOKEN"), + "env_vars": ("SLACK_BOT_TOKEN", "SLACK_APP_TOKEN", "SLACK_ALLOWED_USERS"), "required_env": ("SLACK_BOT_TOKEN", "SLACK_APP_TOKEN"), }, "mattermost": { @@ -5221,6 +5238,7 @@ async def update_messaging_platform( ) trimmed = value.strip() if trimmed: + _validate_messaging_env_value(platform_id, key, trimmed) save_env_value(key, trimmed) if body.enabled is not None: diff --git a/tests/hermes_cli/test_web_server.py b/tests/hermes_cli/test_web_server.py index e65a28101cd..3f6ed3e0435 100644 --- a/tests/hermes_cli/test_web_server.py +++ b/tests/hermes_cli/test_web_server.py @@ -1552,6 +1552,24 @@ class TestWebServerEndpoints: assert telegram["enabled"] is False assert any(field["key"] == "TELEGRAM_BOT_TOKEN" and field["required"] for field in telegram["env_vars"]) + def test_slack_messaging_platform_exposes_user_allowlist(self): + resp = self.client.get("/api/messaging/platforms") + + assert resp.status_code == 200 + platforms = resp.json()["platforms"] + slack = next(platform for platform in platforms if platform["id"] == "slack") + fields = {field["key"]: field for field in slack["env_vars"]} + + assert "allowed Slack member IDs" in slack["description"] + assert set(fields) >= { + "SLACK_BOT_TOKEN", + "SLACK_APP_TOKEN", + "SLACK_ALLOWED_USERS", + } + assert fields["SLACK_ALLOWED_USERS"]["prompt"] == "Allowed Slack member IDs" + assert fields["SLACK_ALLOWED_USERS"]["is_password"] is False + assert "member IDs" in fields["SLACK_ALLOWED_USERS"]["description"] + def test_weixin_messaging_metadata_describes_personal_ilink_setup(self): resp = self.client.get("/api/messaging/platforms") @@ -1628,6 +1646,35 @@ class TestWebServerEndpoints: telegram = next(platform for platform in status if platform["id"] == "telegram") assert telegram["enabled"] is False + def test_update_messaging_platform_saves_slack_allowed_users(self): + from hermes_cli.config import load_env + + resp = self.client.put( + "/api/messaging/platforms/slack", + json={"env": {"SLACK_ALLOWED_USERS": "U01ABC2DEF3,U04XYZ5LMN6"}}, + ) + + assert resp.status_code == 200 + assert load_env()["SLACK_ALLOWED_USERS"] == "U01ABC2DEF3,U04XYZ5LMN6" + + def test_update_messaging_platform_rejects_swapped_slack_bot_token(self): + resp = self.client.put( + "/api/messaging/platforms/slack", + json={"env": {"SLACK_BOT_TOKEN": "xapp-wrong-token-type"}}, + ) + + assert resp.status_code == 400 + assert "xoxb-" in resp.json()["detail"] + + def test_update_messaging_platform_rejects_swapped_slack_app_token(self): + resp = self.client.put( + "/api/messaging/platforms/slack", + json={"env": {"SLACK_APP_TOKEN": "xoxb-wrong-token-type"}}, + ) + + assert resp.status_code == 400 + assert "xapp-" in resp.json()["detail"] + def test_messaging_platform_test_reports_missing_required_setup(self): resp = self.client.put("/api/messaging/platforms/discord", json={"enabled": True}) assert resp.status_code == 200 From d9190491a687d7f29fee5e09c2418d66025e9660 Mon Sep 17 00:00:00 2001 From: Shannon Sands <shannon.sands.1979@gmail.com> Date: Fri, 19 Jun 2026 14:37:16 +1000 Subject: [PATCH 09/90] Add Slack setup hints and field validation --- hermes_cli/config.py | 3 + hermes_cli/web_server.py | 13 +++++ tests/hermes_cli/test_web_server.py | 12 ++++ web/src/lib/api.ts | 1 + web/src/pages/ChannelsPage.tsx | 85 ++++++++++++++++++++++++++--- 5 files changed, 106 insertions(+), 8 deletions(-) diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 8c790e7e856..c81df25c03b 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -3426,6 +3426,7 @@ OPTIONAL_ENV_VARS = { "Required scopes: chat:write, app_mentions:read, channels:history, groups:history, " "im:history, im:read, im:write, users:read, files:read, files:write", "prompt": "Slack Bot Token (xoxb-...)", + "help": "In your Slack app, add the required bot scopes, install the app to the workspace, then copy OAuth & Permissions > Bot User OAuth Token.", "url": "https://api.slack.com/apps", "password": True, "category": "messaging", @@ -3435,6 +3436,7 @@ OPTIONAL_ENV_VARS = { "App-Level Tokens. Also ensure Event Subscriptions include: message.im, " "message.channels, message.groups, app_mention", "prompt": "Slack App Token (xapp-...)", + "help": "In your Slack app, enable Socket Mode, then create Basic Information > App-Level Tokens with the connections:write scope.", "url": "https://api.slack.com/apps", "password": True, "category": "messaging", @@ -3442,6 +3444,7 @@ OPTIONAL_ENV_VARS = { "SLACK_ALLOWED_USERS": { "description": "Comma-separated Slack member IDs allowed to use Hermes, e.g. U01ABC2DEF3. Without this, Slack may connect but deny messages by default.", "prompt": "Allowed Slack member IDs", + "help": "In Slack, open your profile, choose More or the three-dot menu, then Copy member ID. Add multiple IDs comma-separated.", "url": "https://api.slack.com/apps", "password": False, "category": "messaging", diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index b1320875c53..b890f68649e 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -2340,6 +2340,18 @@ def _validate_messaging_env_value(platform_id: str, key: str, value: str) -> Non status_code=400, detail="Slack App Token must start with xapp-. Paste the app-level token from Basic Information > App-Level Tokens.", ) + if key == "SLACK_ALLOWED_USERS": + user_ids = [part.strip() for part in value.split(",")] + invalid = [ + user_id + for user_id in user_ids + if not user_id or not re.fullmatch(r"[UW][A-Z0-9]{2,}", user_id) + ] + if invalid: + raise HTTPException( + status_code=400, + detail="Slack allowed user IDs must be comma-separated member IDs like U01ABC2DEF3.", + ) def _spawn_gateway_restart(profile: Optional[str] = None) -> Tuple[subprocess.Popen, bool]: @@ -4659,6 +4671,7 @@ def _messaging_env_info(key: str) -> dict[str, Any]: return { "description": info.get("description", ""), "prompt": info.get("prompt", key), + "help": info.get("help", ""), "url": info.get("url"), "is_password": info.get("password", False), "advanced": info.get("advanced", False), diff --git a/tests/hermes_cli/test_web_server.py b/tests/hermes_cli/test_web_server.py index 3f6ed3e0435..d44c789b3e3 100644 --- a/tests/hermes_cli/test_web_server.py +++ b/tests/hermes_cli/test_web_server.py @@ -1569,6 +1569,9 @@ class TestWebServerEndpoints: assert fields["SLACK_ALLOWED_USERS"]["prompt"] == "Allowed Slack member IDs" assert fields["SLACK_ALLOWED_USERS"]["is_password"] is False assert "member IDs" in fields["SLACK_ALLOWED_USERS"]["description"] + assert "Bot User OAuth Token" in fields["SLACK_BOT_TOKEN"]["help"] + assert "App-Level Tokens" in fields["SLACK_APP_TOKEN"]["help"] + assert "Copy member ID" in fields["SLACK_ALLOWED_USERS"]["help"] def test_weixin_messaging_metadata_describes_personal_ilink_setup(self): resp = self.client.get("/api/messaging/platforms") @@ -1675,6 +1678,15 @@ class TestWebServerEndpoints: assert resp.status_code == 400 assert "xapp-" in resp.json()["detail"] + def test_update_messaging_platform_rejects_invalid_slack_allowed_users(self): + resp = self.client.put( + "/api/messaging/platforms/slack", + json={"env": {"SLACK_ALLOWED_USERS": "U01ABC2DEF3,not-a-user"}}, + ) + + assert resp.status_code == 400 + assert "member IDs" in resp.json()["detail"] + def test_messaging_platform_test_reports_missing_required_setup(self): resp = self.client.put("/api/messaging/platforms/discord", json={"enabled": True}) assert resp.status_code == 200 diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index ec03997b6c6..3955d3324c9 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -1346,6 +1346,7 @@ export interface MessagingPlatformEnvVar { redacted_value: string | null; description: string; prompt: string; + help: string; url: string | null; is_password: boolean; advanced: boolean; diff --git a/web/src/pages/ChannelsPage.tsx b/web/src/pages/ChannelsPage.tsx index d42ab7b9e74..84791738a25 100644 --- a/web/src/pages/ChannelsPage.tsx +++ b/web/src/pages/ChannelsPage.tsx @@ -4,6 +4,7 @@ import { Check, CheckCircle2, ExternalLink, + Info, PlugZap, QrCode, Radio, @@ -55,6 +56,34 @@ function stateBadge(state: string) { } const TELEGRAM_USER_ID_RE = /^\d+$/; +const SLACK_MEMBER_ID_RE = /^[UW][A-Z0-9]{2,}$/; +const SLACK_TOKEN_PREFIXES: Record<string, string> = { + SLACK_BOT_TOKEN: "xoxb-", + SLACK_APP_TOKEN: "xapp-", +}; + +function validateMessagingEnvField(field: MessagingPlatformEnvVar, value: string): string | null { + const trimmed = value.trim(); + if (!trimmed) return null; + + const expectedPrefix = SLACK_TOKEN_PREFIXES[field.key]; + if (expectedPrefix && !trimmed.startsWith(expectedPrefix)) { + return `${field.prompt || field.key} must start with ${expectedPrefix}`; + } + + if (field.key === "SLACK_ALLOWED_USERS") { + const parts = trimmed.split(",").map((part) => part.trim()); + if (parts.some((part) => !part)) { + return "Slack member IDs must be comma-separated without empty entries."; + } + const invalid = parts.find((part) => !SLACK_MEMBER_ID_RE.test(part)); + if (invalid) { + return `${invalid} does not look like a Slack member ID. Use IDs like U01ABC2DEF3.`; + } + } + + return null; +} function formatExpiry(expiresAt: string): string { const ms = Date.parse(expiresAt) - Date.now(); @@ -83,8 +112,12 @@ export default function ChannelsPage() { // Config modal state const [editing, setEditing] = useState<MessagingPlatform | null>(null); const [draftEnv, setDraftEnv] = useState<Record<string, string>>({}); + const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({}); const [saving, setSaving] = useState(false); - const closeEdit = useCallback(() => setEditing(null), []); + const closeEdit = useCallback(() => { + setEditing(null); + setFieldErrors({}); + }, []); const editModalRef = useModalBehavior({ open: editing !== null, onClose: closeEdit }); // Per-card busy + restart-needed tracking @@ -116,6 +149,7 @@ export default function ChannelsPage() { initial[v.key] = ""; }); setDraftEnv(initial); + setFieldErrors({}); setEditing(platform); }; @@ -138,6 +172,16 @@ export default function ChannelsPage() { showToast(`${missing[0].prompt || missing[0].key} is required`, "error"); return; } + const nextFieldErrors: Record<string, string> = {}; + editing.env_vars.forEach((field) => { + const message = validateMessagingEnvField(field, draftEnv[field.key] || ""); + if (message) nextFieldErrors[field.key] = message; + }); + if (Object.keys(nextFieldErrors).length > 0) { + setFieldErrors(nextFieldErrors); + showToast("Fix the highlighted fields before saving.", "error"); + return; + } setSaving(true); try { const body: MessagingPlatformUpdate = { env, enabled: true }; @@ -326,10 +370,22 @@ export default function ChannelsPage() { </p> {editing.env_vars.map((field: MessagingPlatformEnvVar) => ( <div className="grid gap-1.5" key={field.key}> - <Label htmlFor={`field-${field.key}`}> - {field.prompt || field.key} - {field.required ? " *" : ""} - </Label> + <div className="flex items-center gap-1.5"> + <Label htmlFor={`field-${field.key}`}> + {field.prompt || field.key} + {field.required ? " *" : ""} + </Label> + {field.help && ( + <span + aria-label={field.help} + className="inline-flex text-muted-foreground hover:text-foreground" + role="img" + title={field.help} + > + <Info className="h-3.5 w-3.5" /> + </span> + )} + </div> {field.description && ( <span className="text-xs text-muted-foreground"> {field.description} @@ -344,10 +400,23 @@ export default function ChannelsPage() { : field.key } value={draftEnv[field.key] ?? ""} - onChange={(e) => - setDraftEnv((prev) => ({ ...prev, [field.key]: e.target.value })) - } + aria-invalid={Boolean(fieldErrors[field.key])} + onChange={(e) => { + const nextValue = e.target.value; + setDraftEnv((prev) => ({ ...prev, [field.key]: nextValue })); + setFieldErrors((prev) => { + if (!prev[field.key]) return prev; + const next = { ...prev }; + delete next[field.key]; + return next; + }); + }} /> + {fieldErrors[field.key] && ( + <span className="text-xs text-destructive"> + {fieldErrors[field.key]} + </span> + )} </div> ))} From 83c034bd5bc855955a825ff4acd1ed11edab6c3d Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:18:15 +0530 Subject: [PATCH 10/90] fix(dashboard): accept Slack allow-all wildcard in allowed-users validation The new SLACK_ALLOWED_USERS validation rejected '*', but the Slack gateway honors '*' as an allow-all wildcard (gateway/platforms/slack.py DM auth, slash-confirm, and approval-button paths). Accept '*' as a valid list entry in both the API validator and the dashboard form so a value the runtime honors is no longer blocked at setup. --- hermes_cli/web_server.py | 4 +++- tests/hermes_cli/test_web_server.py | 13 +++++++++++++ web/src/pages/ChannelsPage.tsx | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index b890f68649e..316bc154fa4 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -2342,10 +2342,12 @@ def _validate_messaging_env_value(platform_id: str, key: str, value: str) -> Non ) if key == "SLACK_ALLOWED_USERS": user_ids = [part.strip() for part in value.split(",")] + # "*" is the gateway's allow-all wildcard (see gateway/platforms/slack.py), + # so accept it as a valid entry alongside Slack member IDs (U.../W...). invalid = [ user_id for user_id in user_ids - if not user_id or not re.fullmatch(r"[UW][A-Z0-9]{2,}", user_id) + if user_id != "*" and (not user_id or not re.fullmatch(r"[UW][A-Z0-9]{2,}", user_id)) ] if invalid: raise HTTPException( diff --git a/tests/hermes_cli/test_web_server.py b/tests/hermes_cli/test_web_server.py index d44c789b3e3..d7a4dbcbbf9 100644 --- a/tests/hermes_cli/test_web_server.py +++ b/tests/hermes_cli/test_web_server.py @@ -1687,6 +1687,19 @@ class TestWebServerEndpoints: assert resp.status_code == 400 assert "member IDs" in resp.json()["detail"] + def test_update_messaging_platform_accepts_slack_allowed_users_wildcard(self): + # "*" is the gateway's allow-all wildcard (gateway/platforms/slack.py), + # so the dashboard must accept it rather than rejecting it as malformed. + from hermes_cli.config import load_env + + resp = self.client.put( + "/api/messaging/platforms/slack", + json={"env": {"SLACK_ALLOWED_USERS": "*"}}, + ) + + assert resp.status_code == 200 + assert load_env()["SLACK_ALLOWED_USERS"] == "*" + def test_messaging_platform_test_reports_missing_required_setup(self): resp = self.client.put("/api/messaging/platforms/discord", json={"enabled": True}) assert resp.status_code == 200 diff --git a/web/src/pages/ChannelsPage.tsx b/web/src/pages/ChannelsPage.tsx index 84791738a25..db56beb1925 100644 --- a/web/src/pages/ChannelsPage.tsx +++ b/web/src/pages/ChannelsPage.tsx @@ -76,7 +76,7 @@ function validateMessagingEnvField(field: MessagingPlatformEnvVar, value: string if (parts.some((part) => !part)) { return "Slack member IDs must be comma-separated without empty entries."; } - const invalid = parts.find((part) => !SLACK_MEMBER_ID_RE.test(part)); + const invalid = parts.find((part) => part !== "*" && !SLACK_MEMBER_ID_RE.test(part)); if (invalid) { return `${invalid} does not look like a Slack member ID. Use IDs like U01ABC2DEF3.`; } From 1ab6f34791e28559911185b308d8bd1b0be5f393 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:22:30 +0530 Subject: [PATCH 11/90] refactor(dashboard): align Slack allowlist validation with gateway parse - Drop empty entries before validating SLACK_ALLOWED_USERS so a trailing or interior comma (which the gateway silently tolerates in gateway/platforms/slack.py) is no longer rejected at the dashboard. - Hoist the member-ID regex to a module-level _SLACK_MEMBER_ID_RE constant and note it stays in sync with the frontend SLACK_MEMBER_ID_RE. - Add a regression test for the trailing-comma case. --- hermes_cli/web_server.py | 14 ++++++++++---- tests/hermes_cli/test_web_server.py | 13 +++++++++++++ web/src/pages/ChannelsPage.tsx | 11 +++++++---- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 316bc154fa4..b0d51e2481e 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -2325,6 +2325,11 @@ def _gateway_display_command(profile: Optional[str], verb: str) -> str: return " ".join(["hermes", *_gateway_subcommand(profile, verb)]) +# Slack member IDs (users U..., Enterprise Grid W...). Kept in sync with the +# frontend SLACK_MEMBER_ID_RE in web/src/pages/ChannelsPage.tsx. +_SLACK_MEMBER_ID_RE = re.compile(r"[UW][A-Z0-9]{2,}") + + def _validate_messaging_env_value(platform_id: str, key: str, value: str) -> None: """Reject platform credentials that are clearly in the wrong field.""" if platform_id != "slack" or not value: @@ -2341,13 +2346,14 @@ def _validate_messaging_env_value(platform_id: str, key: str, value: str) -> Non detail="Slack App Token must start with xapp-. Paste the app-level token from Basic Information > App-Level Tokens.", ) if key == "SLACK_ALLOWED_USERS": - user_ids = [part.strip() for part in value.split(",")] - # "*" is the gateway's allow-all wildcard (see gateway/platforms/slack.py), - # so accept it as a valid entry alongside Slack member IDs (U.../W...). + # Mirror the gateway's parse (gateway/platforms/slack.py): split on comma, + # strip, and drop empty entries so a trailing/interior comma isn't rejected + # here when the runtime would accept it. "*" is the allow-all wildcard. + user_ids = [part.strip() for part in value.split(",") if part.strip()] invalid = [ user_id for user_id in user_ids - if user_id != "*" and (not user_id or not re.fullmatch(r"[UW][A-Z0-9]{2,}", user_id)) + if user_id != "*" and not _SLACK_MEMBER_ID_RE.fullmatch(user_id) ] if invalid: raise HTTPException( diff --git a/tests/hermes_cli/test_web_server.py b/tests/hermes_cli/test_web_server.py index d7a4dbcbbf9..7416ec0b87a 100644 --- a/tests/hermes_cli/test_web_server.py +++ b/tests/hermes_cli/test_web_server.py @@ -1700,6 +1700,19 @@ class TestWebServerEndpoints: assert resp.status_code == 200 assert load_env()["SLACK_ALLOWED_USERS"] == "*" + def test_update_messaging_platform_accepts_slack_allowed_users_trailing_comma(self): + # The gateway drops empty entries (gateway/platforms/slack.py), so a + # trailing/interior comma must not be rejected by the dashboard. + from hermes_cli.config import load_env + + resp = self.client.put( + "/api/messaging/platforms/slack", + json={"env": {"SLACK_ALLOWED_USERS": "U01ABC2DEF3,,W04XYZ5LMN6,"}}, + ) + + assert resp.status_code == 200 + assert load_env()["SLACK_ALLOWED_USERS"] == "U01ABC2DEF3,,W04XYZ5LMN6," + def test_messaging_platform_test_reports_missing_required_setup(self): resp = self.client.put("/api/messaging/platforms/discord", json={"enabled": True}) assert resp.status_code == 200 diff --git a/web/src/pages/ChannelsPage.tsx b/web/src/pages/ChannelsPage.tsx index db56beb1925..7658c0cd61a 100644 --- a/web/src/pages/ChannelsPage.tsx +++ b/web/src/pages/ChannelsPage.tsx @@ -72,10 +72,13 @@ function validateMessagingEnvField(field: MessagingPlatformEnvVar, value: string } if (field.key === "SLACK_ALLOWED_USERS") { - const parts = trimmed.split(",").map((part) => part.trim()); - if (parts.some((part) => !part)) { - return "Slack member IDs must be comma-separated without empty entries."; - } + // Mirror the gateway's parse (gateway/platforms/slack.py): drop empty + // entries so a trailing/interior comma isn't rejected here. "*" is the + // allow-all wildcard the gateway honors. + const parts = trimmed + .split(",") + .map((part) => part.trim()) + .filter(Boolean); const invalid = parts.find((part) => part !== "*" && !SLACK_MEMBER_ID_RE.test(part)); if (invalid) { return `${invalid} does not look like a Slack member ID. Use IDs like U01ABC2DEF3.`; From c7b7f92ec14a5c43deef844804f0bf6a7f2d992d Mon Sep 17 00:00:00 2001 From: Eurekaxun <eurekaxun@163.com> Date: Tue, 2 Jun 2026 14:33:12 +0800 Subject: [PATCH 12/90] fix(openviking): sync structured turns with tool parts --- plugins/memory/openviking/__init__.py | 339 +++++++++++++++++- tests/openviking_plugin/test_openviking.py | 274 ++++++++++++++ .../memory/test_openviking_provider.py | 47 ++- 3 files changed, 639 insertions(+), 21 deletions(-) diff --git a/plugins/memory/openviking/__init__.py b/plugins/memory/openviking/__init__.py index 7ebe6869a46..c7b05a4864c 100644 --- a/plugins/memory/openviking/__init__.py +++ b/plugins/memory/openviking/__init__.py @@ -70,6 +70,8 @@ _TIMEOUT = 30.0 _SESSION_DRAIN_TIMEOUT = 10.0 _DEFERRED_COMMIT_TIMEOUT = (_TIMEOUT * 2) + 5.0 _REMOTE_RESOURCE_PREFIXES = ("http://", "https://", "git@", "ssh://", "git://") +_SYNC_TRACE_ENV = "HERMES_OPENVIKING_SYNC_TRACE" +_OPENVIKING_RECALL_TOOL_NAMES = {"viking_search", "viking_read", "viking_browse"} # Maps the viking_remember `category` enum to a viking:// subdirectory. # Keep in sync with REMEMBER_SCHEMA.parameters.properties.category.enum. @@ -156,6 +158,18 @@ def _derive_openviking_user_text(content: Any) -> str: return extract_user_instruction_from_skill_message(content) or "" +def _sync_trace_enabled() -> bool: + return os.environ.get(_SYNC_TRACE_ENV, "").strip().lower() in {"1", "true", "yes", "on"} + + +def _preview(value: Any, limit: int = 160) -> str: + text = "" if value is None else str(value) + text = text.replace("\n", "\\n") + if len(text) > limit: + return text[:limit] + "..." + return text + + # --------------------------------------------------------------------------- # Process-level atexit safety net — ensures pending sessions are committed # even if shutdown_memory_provider is never called (e.g. gateway crash, @@ -2221,7 +2235,10 @@ class OpenVikingMemoryProvider(MemoryProvider): def _commit_session(self, sid: str, turn_count: int, *, context: str) -> bool: try: - self._client.post(f"/api/v1/sessions/{sid}/commit") + self._client.post( + f"/api/v1/sessions/{sid}/commit", + {"keep_recent_count": 0}, + ) self._mark_session_committed(sid) logger.info("OpenViking session %s committed %s (%d turns)", sid, context, turn_count) return True @@ -2293,7 +2310,261 @@ class OpenVikingMemoryProvider(MemoryProvider): with self._prefetch_lock: self._prefetch_result = "" - def sync_turn(self, user_content: str, assistant_content: str, *, session_id: str = "") -> None: + @staticmethod + def _message_text(content: Any) -> str: + """Extract text from OpenAI-style string/list content.""" + if isinstance(content, str): + return content + if isinstance(content, list): + chunks = [] + for block in content: + if isinstance(block, str): + chunks.append(block) + elif isinstance(block, dict): + if block.get("type") == "text" and isinstance(block.get("text"), str): + chunks.append(block["text"]) + elif isinstance(block.get("content"), str): + chunks.append(block["content"]) + return "\n".join(chunk for chunk in chunks if chunk) + if content is None: + return "" + return str(content) + + @classmethod + def _message_matches_text(cls, message: Dict[str, Any], expected: Any) -> bool: + expected_text = cls._message_text(expected).strip() + if not expected_text: + return False + actual_text = cls._message_text(message.get("content")).strip() + return actual_text == expected_text + + @classmethod + def _extract_current_turn_messages( + cls, + messages: Optional[List[Dict[str, Any]]], + user_content: str, + assistant_content: str, + ) -> List[Dict[str, Any]]: + """Slice the completed turn out of Hermes' full canonical transcript.""" + if not messages: + return [] + + end_idx: Optional[int] = None + if cls._message_text(assistant_content).strip(): + for idx in range(len(messages) - 1, -1, -1): + message = messages[idx] + if ( + isinstance(message, dict) + and message.get("role") == "assistant" + and cls._message_matches_text(message, assistant_content) + ): + end_idx = idx + break + if end_idx is None: + for idx in range(len(messages) - 1, -1, -1): + message = messages[idx] + if isinstance(message, dict) and message.get("role") == "assistant": + end_idx = idx + break + if end_idx is None: + end_idx = len(messages) - 1 + + start_idx: Optional[int] = None + if cls._message_text(user_content).strip(): + for idx in range(end_idx, -1, -1): + message = messages[idx] + if ( + isinstance(message, dict) + and message.get("role") == "user" + and cls._message_matches_text(message, user_content) + ): + start_idx = idx + break + if start_idx is None: + for idx in range(end_idx, -1, -1): + message = messages[idx] + if isinstance(message, dict) and message.get("role") == "user": + start_idx = idx + break + if start_idx is None: + return [] + + return [message for message in messages[start_idx : end_idx + 1] if isinstance(message, dict)] + + @staticmethod + def _tool_call_id(tool_call: Dict[str, Any]) -> str: + return str(tool_call.get("id") or tool_call.get("tool_call_id") or "") + + @staticmethod + def _tool_call_name(tool_call: Dict[str, Any]) -> str: + function = tool_call.get("function") + if isinstance(function, dict): + return str(function.get("name") or "") + return str(tool_call.get("name") or "") + + @staticmethod + def _is_openviking_recall_tool_name(tool_name: Any) -> bool: + return str(tool_name or "").strip().lower() in _OPENVIKING_RECALL_TOOL_NAMES + + @staticmethod + def _tool_call_input(tool_call: Dict[str, Any]) -> Dict[str, Any]: + function = tool_call.get("function") + raw_args: Any = None + if isinstance(function, dict): + raw_args = function.get("arguments") + if raw_args is None: + raw_args = tool_call.get("args") + if raw_args is None: + return {} + if isinstance(raw_args, dict): + return raw_args + if isinstance(raw_args, str): + if not raw_args.strip(): + return {} + try: + parsed = json.loads(raw_args) + except Exception: + return {"value": raw_args} + if isinstance(parsed, dict): + return parsed + return {"value": parsed} + return {"value": raw_args} + + @classmethod + def _tool_result_status(cls, message: Dict[str, Any]) -> str: + raw_status = str(message.get("status") or message.get("tool_status") or "").lower() + if raw_status in {"error", "failed", "failure"}: + return "error" + if raw_status in {"completed", "complete", "success", "succeeded"}: + return "completed" + + text = cls._message_text(message.get("content")).strip() + if text: + try: + parsed = json.loads(text) + except Exception: + parsed = None + if isinstance(parsed, dict): + status = str(parsed.get("status") or "").lower() + exit_code = parsed.get("exit_code") + if ( + status in {"error", "failed", "failure"} + or parsed.get("success") is False + or bool(parsed.get("error")) + or (isinstance(exit_code, int) and exit_code != 0) + ): + return "error" + return "completed" + + @classmethod + def _messages_to_openviking_batch( + cls, + messages: List[Dict[str, Any]], + ) -> List[Dict[str, Any]]: + """Convert Hermes canonical messages into OpenViking batch payloads.""" + tool_calls_by_id: Dict[str, Dict[str, Any]] = {} + completed_tool_ids: set[str] = set() + skipped_tool_ids: set[str] = set() + for message in messages: + if not isinstance(message, dict): + continue + if message.get("role") == "tool": + tool_id = str(message.get("tool_call_id") or message.get("id") or "") + if tool_id: + completed_tool_ids.add(tool_id) + if cls._is_openviking_recall_tool_name(message.get("name")): + skipped_tool_ids.add(tool_id) + continue + if message.get("role") != "assistant": + continue + for tool_call in message.get("tool_calls") or []: + if not isinstance(tool_call, dict): + continue + tool_id = cls._tool_call_id(tool_call) + tool_name = cls._tool_call_name(tool_call) + if tool_id: + tool_calls_by_id[tool_id] = { + "tool_name": tool_name, + "tool_input": cls._tool_call_input(tool_call), + } + if cls._is_openviking_recall_tool_name(tool_name): + skipped_tool_ids.add(tool_id) + + payload_messages: List[Dict[str, Any]] = [] + pending_tool_parts: List[Dict[str, Any]] = [] + + def flush_tool_parts() -> None: + nonlocal pending_tool_parts + if pending_tool_parts: + payload_messages.append({"role": "user", "parts": pending_tool_parts}) + pending_tool_parts = [] + + for message in messages: + if not isinstance(message, dict): + continue + + role = str(message.get("role") or "") + if role in {"system", "developer"}: + continue + + if role == "tool": + tool_id = str(message.get("tool_call_id") or message.get("id") or "") + prior_call = tool_calls_by_id.get(tool_id, {}) + tool_name = str(message.get("name") or prior_call.get("tool_name") or "") + if tool_id in skipped_tool_ids or cls._is_openviking_recall_tool_name(tool_name): + continue + tool_part = { + "type": "tool", + "tool_id": tool_id, + "tool_name": tool_name, + "tool_input": prior_call.get("tool_input", {}), + "tool_output": cls._message_text(message.get("content")), + "tool_status": cls._tool_result_status(message), + } + pending_tool_parts.append(tool_part) + continue + + if role not in {"user", "assistant"}: + continue + + flush_tool_parts() + parts: List[Dict[str, Any]] = [] + text = cls._message_text(message.get("content")) + if text: + parts.append({"type": "text", "text": text}) + + if role == "assistant": + for tool_call in message.get("tool_calls") or []: + if not isinstance(tool_call, dict): + continue + tool_id = cls._tool_call_id(tool_call) + tool_name = cls._tool_call_name(tool_call) + if tool_id in skipped_tool_ids or cls._is_openviking_recall_tool_name(tool_name): + continue + if tool_id in completed_tool_ids: + continue + parts.append({ + "type": "tool", + "tool_id": tool_id, + "tool_name": tool_name, + "tool_input": cls._tool_call_input(tool_call), + "tool_status": "pending", + }) + + if parts: + payload_messages.append({"role": role, "parts": parts}) + + flush_tool_parts() + return payload_messages + + def sync_turn( + self, + user_content: str, + assistant_content: str, + *, + session_id: str = "", + messages: Optional[List[Dict[str, Any]]] = None, + ) -> None: """Record the conversation turn in OpenViking's session (non-blocking).""" if not self._client: return @@ -2302,6 +2573,37 @@ class OpenVikingMemoryProvider(MemoryProvider): if not user_content: return + turn_messages = ( + self._extract_current_turn_messages(messages, user_content, assistant_content) + if messages is not None + else [] + ) + if turn_messages: + turn_messages = [dict(message) for message in turn_messages] + for message in turn_messages: + if message.get("role") == "user": + message["content"] = user_content + break + batch_messages = self._messages_to_openviking_batch(turn_messages) + + if _sync_trace_enabled(): + logger.info( + "OpenViking sync_turn trace: session_arg=%r cached_session=%r " + "messages_param_supported=true messages_present=%s message_count=%s " + "turn_message_count=%d batch_message_count=%d user_len=%d assistant_len=%d " + "user_preview=%r assistant_preview=%r", + session_id, + self._session_id, + messages is not None, + len(messages) if messages is not None else None, + len(turn_messages), + len(batch_messages), + len(str(user_content or "")), + len(str(assistant_content or "")), + _preview(user_content), + _preview(assistant_content), + ) + # Snapshot the sid and bump the turn counter atomically so a # concurrent on_session_switch/on_session_end can't interleave its # snapshot+reset between the read and the increment (lost turn) and so @@ -2313,24 +2615,39 @@ class OpenVikingMemoryProvider(MemoryProvider): self._turn_count += 1 def _sync(): - try: - client = self._new_client() + def _post_turn(client: _VikingClient) -> None: + if batch_messages: + payload = {"messages": batch_messages} + if _sync_trace_enabled(): + logger.info( + "OpenViking sync_turn trace: POST /api/v1/sessions/%s/messages/batch payload=%s", + sid, + json.dumps(payload, ensure_ascii=False), + ) + try: + client.post(f"/api/v1/sessions/{sid}/messages/batch", payload) + return + except Exception as batch_error: + logger.warning( + "OpenViking structured sync failed; falling back to text sync: %s", + batch_error, + ) + self._post_session_turn( client, sid, user_content[:4000], - assistant_content[:4000], + self._message_text(assistant_content)[:4000], ) + + try: + client = self._new_client() + _post_turn(client) except Exception as e: logger.debug("OpenViking sync_turn failed, reconnecting: %s", e) try: client = self._new_client() - self._post_session_turn( - client, - sid, - user_content[:4000], - assistant_content[:4000], - ) + _post_turn(client) except Exception as retry_error: logger.warning("OpenViking sync_turn failed: %s", retry_error) diff --git a/tests/openviking_plugin/test_openviking.py b/tests/openviking_plugin/test_openviking.py index f10fc502000..ee5d1eb2373 100644 --- a/tests/openviking_plugin/test_openviking.py +++ b/tests/openviking_plugin/test_openviking.py @@ -265,6 +265,280 @@ class TestOpenVikingSkillQuerySafety: assert RecordingVikingClient.calls == [] +class TestOpenVikingTurnConversion: + def test_extract_current_turn_anchors_on_latest_matching_user_and_assistant(self): + messages = [ + {"role": "user", "content": "Please inspect the repository for assemble hooks."}, + {"role": "assistant", "content": "Earlier answer."}, + {"role": "user", "content": "Please inspect the repository for assemble hooks."}, + { + "role": "assistant", + "content": "I will search the codebase.", + "tool_calls": [ + { + "id": "call_rg_1", + "type": "function", + "function": { + "name": "shell_command", + "arguments": json.dumps({"command": "rg assemble"}), + }, + } + ], + }, + { + "role": "tool", + "tool_call_id": "call_rg_1", + "name": "shell_command", + "content": "agent/context_engine.py: no preassemble hook", + }, + {"role": "assistant", "content": "The current main does not expose assemble."}, + ] + + turn = OpenVikingMemoryProvider._extract_current_turn_messages( + messages, + "Please inspect the repository for assemble hooks.", + "The current main does not expose assemble.", + ) + + assert turn == messages[2:] + + def test_messages_to_openviking_batch_coalesces_tool_results(self): + turn = [ + {"role": "user", "content": "Please inspect the repository for assemble hooks."}, + { + "role": "assistant", + "content": "I will search the codebase.", + "tool_calls": [ + { + "id": "call_rg_1", + "type": "function", + "function": { + "name": "shell_command", + "arguments": json.dumps({"command": "rg assemble"}), + }, + } + ], + }, + { + "role": "tool", + "tool_call_id": "call_rg_1", + "name": "shell_command", + "content": "agent/context_engine.py: no preassemble hook", + }, + {"role": "assistant", "content": "The current main does not expose assemble."}, + ] + + batch = OpenVikingMemoryProvider._messages_to_openviking_batch(turn) + + assert [message["role"] for message in batch] == ["user", "assistant", "user", "assistant"] + assert batch[0]["parts"] == [ + {"type": "text", "text": "Please inspect the repository for assemble hooks."} + ] + assert batch[1]["parts"] == [ + {"type": "text", "text": "I will search the codebase."} + ] + assert batch[2]["parts"] == [ + { + "type": "tool", + "tool_id": "call_rg_1", + "tool_name": "shell_command", + "tool_input": {"command": "rg assemble"}, + "tool_output": "agent/context_engine.py: no preassemble hook", + "tool_status": "completed", + } + ] + assert batch[3]["parts"] == [ + {"type": "text", "text": "The current main does not expose assemble."} + ] + + def test_messages_to_openviking_batch_marks_json_tool_error_results(self): + turn = [ + {"role": "user", "content": "Check the file."}, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "call_read_1", + "type": "function", + "function": { + "name": "read_file", + "arguments": json.dumps({"path": "missing.md"}), + }, + } + ], + }, + { + "role": "tool", + "tool_call_id": "call_read_1", + "name": "read_file", + "content": json.dumps({"error": "File not found", "exit_code": 1}), + }, + ] + + batch = OpenVikingMemoryProvider._messages_to_openviking_batch(turn) + + assert batch[1]["parts"] == [ + { + "type": "tool", + "tool_id": "call_read_1", + "tool_name": "read_file", + "tool_input": {"path": "missing.md"}, + "tool_output": json.dumps({"error": "File not found", "exit_code": 1}), + "tool_status": "error", + } + ] + + def test_messages_to_openviking_batch_keeps_pending_tool_call_without_result(self): + turn = [ + {"role": "user", "content": "Start a long running check."}, + { + "role": "assistant", + "content": "Starting it now.", + "tool_calls": [ + { + "id": "call_long_1", + "type": "function", + "function": { + "name": "long_check", + "arguments": json.dumps({"target": "repo"}), + }, + } + ], + }, + ] + + batch = OpenVikingMemoryProvider._messages_to_openviking_batch(turn) + + assert batch[1]["parts"] == [ + {"type": "text", "text": "Starting it now."}, + { + "type": "tool", + "tool_id": "call_long_1", + "tool_name": "long_check", + "tool_input": {"target": "repo"}, + "tool_status": "pending", + }, + ] + + def test_messages_to_openviking_batch_coalesces_adjacent_tool_results(self): + turn = [ + {"role": "user", "content": "Run both tools."}, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "call_a", + "type": "function", + "function": { + "name": "first_tool", + "arguments": json.dumps({"x": 1}), + }, + }, + { + "id": "call_b", + "type": "function", + "function": { + "name": "second_tool", + "arguments": json.dumps({"y": 2}), + }, + }, + ], + }, + {"role": "tool", "tool_call_id": "call_a", "name": "first_tool", "content": "a"}, + {"role": "tool", "tool_call_id": "call_b", "name": "second_tool", "content": "b"}, + {"role": "assistant", "content": "Done."}, + ] + + batch = OpenVikingMemoryProvider._messages_to_openviking_batch(turn) + + assert [message["role"] for message in batch] == ["user", "user", "assistant"] + assert batch[1]["parts"] == [ + { + "type": "tool", + "tool_id": "call_a", + "tool_name": "first_tool", + "tool_input": {"x": 1}, + "tool_output": "a", + "tool_status": "completed", + }, + { + "type": "tool", + "tool_id": "call_b", + "tool_name": "second_tool", + "tool_input": {"y": 2}, + "tool_output": "b", + "tool_status": "completed", + }, + ] + + def test_messages_to_openviking_batch_skips_openviking_recall_tool_results(self): + for recall_tool_name in ("viking_search", "viking_read", "viking_browse"): + turn = [ + {"role": "user", "content": "What did we decide about context assembly?"}, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "call_recall_1", + "type": "function", + "function": { + "name": recall_tool_name, + "arguments": json.dumps({"query": "context assembly decision"}), + }, + }, + { + "id": "call_shell_1", + "type": "function", + "function": { + "name": "shell_command", + "arguments": json.dumps({"command": "rg preassemble"}), + }, + }, + ], + }, + { + "role": "tool", + "tool_call_id": "call_recall_1", + "name": recall_tool_name, + "content": json.dumps({ + "results": [ + { + "uri": "viking://user/hermes/memories/context", + "abstract": "Old OpenViking memory content", + } + ] + }), + }, + { + "role": "tool", + "tool_call_id": "call_shell_1", + "name": "shell_command", + "content": "plugins/memory/openviking/__init__.py", + }, + {"role": "assistant", "content": "We decided to keep sync_turn scoped to ingestion."}, + ] + + batch = OpenVikingMemoryProvider._messages_to_openviking_batch(turn) + + assert [message["role"] for message in batch] == ["user", "user", "assistant"] + assert batch[1]["parts"] == [ + { + "type": "tool", + "tool_id": "call_shell_1", + "tool_name": "shell_command", + "tool_input": {"command": "rg preassemble"}, + "tool_output": "plugins/memory/openviking/__init__.py", + "tool_status": "completed", + } + ] + batch_text = json.dumps(batch) + assert recall_tool_name not in batch_text + assert "Old OpenViking memory content" not in batch_text + + class TestOpenVikingRead: def test_overview_read_normalizes_uri_and_unwraps_result(self): provider = OpenVikingMemoryProvider() diff --git a/tests/plugins/memory/test_openviking_provider.py b/tests/plugins/memory/test_openviking_provider.py index 954385fa54e..2863566b367 100644 --- a/tests/plugins/memory/test_openviking_provider.py +++ b/tests/plugins/memory/test_openviking_provider.py @@ -1975,7 +1975,10 @@ def test_on_session_switch_commits_old_session_and_rotates_id(): provider.on_session_switch("new-sid", parent_session_id="old-sid") - provider._client.post.assert_called_once_with("/api/v1/sessions/old-sid/commit") + provider._client.post.assert_called_once_with( + "/api/v1/sessions/old-sid/commit", + {"keep_recent_count": 0}, + ) assert provider._session_id == "new-sid" assert provider._turn_count == 0 @@ -1998,7 +2001,10 @@ def test_on_session_switch_commits_pending_tokens_without_turn_count(): provider.on_session_switch("new-sid") provider._client.get.assert_called_once_with("/api/v1/sessions/old-sid") - provider._client.post.assert_called_once_with("/api/v1/sessions/old-sid/commit") + provider._client.post.assert_called_once_with( + "/api/v1/sessions/old-sid/commit", + {"keep_recent_count": 0}, + ) assert provider._session_id == "new-sid" assert provider._turn_count == 0 @@ -2051,7 +2057,10 @@ def test_on_session_switch_waits_for_inflight_sync_thread(): provider.on_session_switch("new-sid") assert join_calls, "expected on_session_switch to join the in-flight sync thread" - provider._client.post.assert_called_once_with("/api/v1/sessions/old-sid/commit") + provider._client.post.assert_called_once_with( + "/api/v1/sessions/old-sid/commit", + {"keep_recent_count": 0}, + ) def test_on_session_switch_noop_on_empty_new_id(): @@ -2206,7 +2215,10 @@ def test_on_session_end_marks_session_clean_after_successful_commit(): provider.on_session_end([]) - provider._client.post.assert_called_once_with("/api/v1/sessions/old-sid/commit") + provider._client.post.assert_called_once_with( + "/api/v1/sessions/old-sid/commit", + {"keep_recent_count": 0}, + ) assert provider._turn_count == 0 @@ -2228,7 +2240,10 @@ def test_on_session_end_commits_pending_tokens_without_turn_count(): provider.on_session_end([]) provider._client.get.assert_called_once_with("/api/v1/sessions/old-sid") - provider._client.post.assert_called_once_with("/api/v1/sessions/old-sid/commit") + provider._client.post.assert_called_once_with( + "/api/v1/sessions/old-sid/commit", + {"keep_recent_count": 0}, + ) def test_end_then_switch_does_not_double_commit(): @@ -2241,7 +2256,10 @@ def test_end_then_switch_does_not_double_commit(): provider.on_session_switch("new-sid", parent_session_id="old-sid") # Exactly one commit call, on the OLD session, fired by on_session_end. - provider._client.post.assert_called_once_with("/api/v1/sessions/old-sid/commit") + provider._client.post.assert_called_once_with( + "/api/v1/sessions/old-sid/commit", + {"keep_recent_count": 0}, + ) assert provider._session_id == "new-sid" assert provider._turn_count == 0 @@ -2253,7 +2271,10 @@ def test_end_then_switch_with_pending_tokens_does_not_double_commit(): provider.on_session_end([]) provider.on_session_switch("new-sid", parent_session_id="old-sid") - provider._client.post.assert_called_once_with("/api/v1/sessions/old-sid/commit") + provider._client.post.assert_called_once_with( + "/api/v1/sessions/old-sid/commit", + {"keep_recent_count": 0}, + ) assert provider._session_id == "new-sid" assert provider._turn_count == 0 @@ -2400,7 +2421,10 @@ def test_on_session_switch_does_not_block_caller_on_slow_drain(): # Let the finalizer finish so it doesn't leak past the test. release_drain.set() assert provider._drain_finalizers(timeout=5.0) - provider._client.post.assert_called_once_with("/api/v1/sessions/old-sid/commit") + provider._client.post.assert_called_once_with( + "/api/v1/sessions/old-sid/commit", + {"keep_recent_count": 0}, + ) def test_on_session_switch_defers_old_commit_to_finalizer_thread(): @@ -2415,7 +2439,7 @@ def test_on_session_switch_defers_old_commit_to_finalizer_thread(): committed = threading.Event() drain_timeouts = [] - def fake_post(path): + def fake_post(path, payload=None): committed.set() return {} @@ -2433,7 +2457,10 @@ def test_on_session_switch_defers_old_commit_to_finalizer_thread(): assert provider._turn_count == 0 # The old-session commit lands on the finalizer thread, not inline. assert committed.wait(timeout=5.0), "old session was not finalized off-thread" - provider._client.post.assert_called_once_with("/api/v1/sessions/old-sid/commit") + provider._client.post.assert_called_once_with( + "/api/v1/sessions/old-sid/commit", + {"keep_recent_count": 0}, + ) # The finalizer drains with the deferred (longer) budget, not inline 10s. assert drain_timeouts == [_DEFERRED_COMMIT_TIMEOUT] From d7cd0bc0863cda1a203f00422b1441ca2d9890ed Mon Sep 17 00:00:00 2001 From: Hao Zhe <haozhe4547@gmail.com> Date: Fri, 19 Jun 2026 13:42:36 +0800 Subject: [PATCH 13/90] fix(openviking): preserve structured sync attribution --- agent/codex_runtime.py | 1 + agent/message_content.py | 50 +++++++++++++ plugins/memory/openviking/__init__.py | 36 +++++----- tests/agent/test_message_content.py | 25 +++++++ tests/openviking_plugin/test_openviking.py | 36 +++++++++- .../memory/test_openviking_provider.py | 72 +++++++++++++++++++ .../test_codex_app_server_integration.py | 13 +++- 7 files changed, 210 insertions(+), 23 deletions(-) create mode 100644 agent/message_content.py create mode 100644 tests/agent/test_message_content.py diff --git a/agent/codex_runtime.py b/agent/codex_runtime.py index 7f175fff97f..4ff67871934 100644 --- a/agent/codex_runtime.py +++ b/agent/codex_runtime.py @@ -290,6 +290,7 @@ def run_codex_app_server_turn( original_user_message=original_user_message, final_response=turn.final_text, interrupted=False, + messages=messages, ) except Exception: logger.debug("external memory sync raised", exc_info=True) diff --git a/agent/message_content.py b/agent/message_content.py new file mode 100644 index 00000000000..c42bf408550 --- /dev/null +++ b/agent/message_content.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + + +_NON_TEXT_PART_TYPES = {"image", "image_url", "input_image", "audio", "input_audio"} +_TEXT_KEYS = ("text", "content", "input_text", "output_text", "summary_text") + + +def _field(value: Any, key: str) -> Any: + if isinstance(value, Mapping): + return value.get(key) + return getattr(value, key, None) + + +def _text_from_part(part: Any) -> str: + if part is None: + return "" + if isinstance(part, str): + return part + + part_type = str(_field(part, "type") or "").strip().lower() + if part_type in _NON_TEXT_PART_TYPES: + return "" + + for key in _TEXT_KEYS: + text = _field(part, key) + if isinstance(text, str): + return text + return "" + + +def flatten_message_text(content: Any, *, sep: str = "\n") -> str: + """Return the visible text from common chat/Responses message content shapes.""" + if content is None: + return "" + if isinstance(content, str): + return content + if isinstance(content, list): + chunks = [_text_from_part(part) for part in content] + return sep.join(chunk for chunk in chunks if chunk) + + text = _text_from_part(content) + if text: + return text + try: + return str(content) + except Exception: + return "" diff --git a/plugins/memory/openviking/__init__.py b/plugins/memory/openviking/__init__.py index c7b05a4864c..82f1f26a0a0 100644 --- a/plugins/memory/openviking/__init__.py +++ b/plugins/memory/openviking/__init__.py @@ -45,6 +45,7 @@ from typing import Any, Callable, Dict, List, Optional, Set from urllib.parse import urlparse from urllib.request import url2pathname +from agent.message_content import flatten_message_text from agent.memory_provider import MemoryProvider from agent.skill_commands import extract_user_instruction_from_skill_message from tools.registry import tool_error @@ -2313,22 +2314,7 @@ class OpenVikingMemoryProvider(MemoryProvider): @staticmethod def _message_text(content: Any) -> str: """Extract text from OpenAI-style string/list content.""" - if isinstance(content, str): - return content - if isinstance(content, list): - chunks = [] - for block in content: - if isinstance(block, str): - chunks.append(block) - elif isinstance(block, dict): - if block.get("type") == "text" and isinstance(block.get("text"), str): - chunks.append(block["text"]) - elif isinstance(block.get("content"), str): - chunks.append(block["content"]) - return "\n".join(chunk for chunk in chunks if chunk) - if content is None: - return "" - return str(content) + return flatten_message_text(content) @classmethod def _message_matches_text(cls, message: Dict[str, Any], expected: Any) -> bool: @@ -2460,8 +2446,11 @@ class OpenVikingMemoryProvider(MemoryProvider): def _messages_to_openviking_batch( cls, messages: List[Dict[str, Any]], + *, + assistant_peer_id: str = "", ) -> List[Dict[str, Any]]: """Convert Hermes canonical messages into OpenViking batch payloads.""" + assistant_peer_id = str(assistant_peer_id or "").strip() tool_calls_by_id: Dict[str, Dict[str, Any]] = {} completed_tool_ids: set[str] = set() skipped_tool_ids: set[str] = set() @@ -2493,10 +2482,16 @@ class OpenVikingMemoryProvider(MemoryProvider): payload_messages: List[Dict[str, Any]] = [] pending_tool_parts: List[Dict[str, Any]] = [] + def payload_message(role: str, parts: List[Dict[str, Any]]) -> Dict[str, Any]: + payload: Dict[str, Any] = {"role": role, "parts": parts} + if role == "assistant" and assistant_peer_id: + payload["peer_id"] = assistant_peer_id + return payload + def flush_tool_parts() -> None: nonlocal pending_tool_parts if pending_tool_parts: - payload_messages.append({"role": "user", "parts": pending_tool_parts}) + payload_messages.append(payload_message("assistant", pending_tool_parts)) pending_tool_parts = [] for message in messages: @@ -2552,7 +2547,7 @@ class OpenVikingMemoryProvider(MemoryProvider): }) if parts: - payload_messages.append({"role": role, "parts": parts}) + payload_messages.append(payload_message(role, parts)) flush_tool_parts() return payload_messages @@ -2584,7 +2579,10 @@ class OpenVikingMemoryProvider(MemoryProvider): if message.get("role") == "user": message["content"] = user_content break - batch_messages = self._messages_to_openviking_batch(turn_messages) + batch_messages = self._messages_to_openviking_batch( + turn_messages, + assistant_peer_id=getattr(self, "_agent", _DEFAULT_AGENT), + ) if _sync_trace_enabled(): logger.info( diff --git a/tests/agent/test_message_content.py b/tests/agent/test_message_content.py new file mode 100644 index 00000000000..0207d63600b --- /dev/null +++ b/tests/agent/test_message_content.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from types import SimpleNamespace + +from agent.message_content import flatten_message_text + + +def test_flatten_message_text_accepts_chat_and_responses_text_parts(): + content = [ + {"type": "text", "text": "chat text"}, + {"type": "input_text", "text": "user text"}, + {"type": "output_text", "text": "assistant text"}, + {"type": "summary_text", "text": "summary text"}, + ] + + assert flatten_message_text(content) == "chat text\nuser text\nassistant text\nsummary text" + + +def test_flatten_message_text_accepts_object_parts(): + content = [ + SimpleNamespace(type="output_text", text="object text"), + {"content": "legacy content"}, + ] + + assert flatten_message_text(content) == "object text\nlegacy content" diff --git a/tests/openviking_plugin/test_openviking.py b/tests/openviking_plugin/test_openviking.py index ee5d1eb2373..3a743287672 100644 --- a/tests/openviking_plugin/test_openviking.py +++ b/tests/openviking_plugin/test_openviking.py @@ -330,7 +330,7 @@ class TestOpenVikingTurnConversion: batch = OpenVikingMemoryProvider._messages_to_openviking_batch(turn) - assert [message["role"] for message in batch] == ["user", "assistant", "user", "assistant"] + assert [message["role"] for message in batch] == ["user", "assistant", "assistant", "assistant"] assert batch[0]["parts"] == [ {"type": "text", "text": "Please inspect the repository for assemble hooks."} ] @@ -378,6 +378,7 @@ class TestOpenVikingTurnConversion: batch = OpenVikingMemoryProvider._messages_to_openviking_batch(turn) + assert batch[1]["role"] == "assistant" assert batch[1]["parts"] == [ { "type": "tool", @@ -453,7 +454,7 @@ class TestOpenVikingTurnConversion: batch = OpenVikingMemoryProvider._messages_to_openviking_batch(turn) - assert [message["role"] for message in batch] == ["user", "user", "assistant"] + assert [message["role"] for message in batch] == ["user", "assistant", "assistant"] assert batch[1]["parts"] == [ { "type": "tool", @@ -523,7 +524,7 @@ class TestOpenVikingTurnConversion: batch = OpenVikingMemoryProvider._messages_to_openviking_batch(turn) - assert [message["role"] for message in batch] == ["user", "user", "assistant"] + assert [message["role"] for message in batch] == ["user", "assistant", "assistant"] assert batch[1]["parts"] == [ { "type": "tool", @@ -538,6 +539,35 @@ class TestOpenVikingTurnConversion: assert recall_tool_name not in batch_text assert "Old OpenViking memory content" not in batch_text + def test_messages_to_openviking_batch_preserves_responses_text_parts(self): + turn = [ + {"role": "user", "content": [{"type": "input_text", "text": "hello"}]}, + {"role": "assistant", "content": [{"type": "output_text", "text": "answer"}]}, + ] + + batch = OpenVikingMemoryProvider._messages_to_openviking_batch(turn) + + assert batch == [ + {"role": "user", "parts": [{"type": "text", "text": "hello"}]}, + {"role": "assistant", "parts": [{"type": "text", "text": "answer"}]}, + ] + + def test_messages_to_openviking_batch_adds_assistant_peer_id_when_requested(self): + turn = [ + {"role": "user", "content": "hello"}, + {"role": "assistant", "content": "answer"}, + ] + + batch = OpenVikingMemoryProvider._messages_to_openviking_batch( + turn, + assistant_peer_id="hermes", + ) + + assert batch == [ + {"role": "user", "parts": [{"type": "text", "text": "hello"}]}, + {"role": "assistant", "parts": [{"type": "text", "text": "answer"}], "peer_id": "hermes"}, + ] + class TestOpenVikingRead: def test_overview_read_normalizes_uri_and_unwraps_result(self): diff --git a/tests/plugins/memory/test_openviking_provider.py b/tests/plugins/memory/test_openviking_provider.py index 2863566b367..28f2d8e9d46 100644 --- a/tests/plugins/memory/test_openviking_provider.py +++ b/tests/plugins/memory/test_openviking_provider.py @@ -2195,6 +2195,78 @@ def test_sync_turn_retries_batch_write_with_fresh_client(): )] +def test_sync_turn_structured_messages_include_assistant_peer_id(): + provider = OpenVikingMemoryProvider() + provider._client = MagicMock() + provider._endpoint = "http://test" + provider._api_key = "" + provider._account = "acct" + provider._user = "usr" + provider._agent = "hermes" + provider._session_id = "sid-structured" + + captured = [] + + class StubClient: + def __init__(self, *a, **kw): + pass + + def post(self, path, payload=None, **kwargs): + captured.append((path, payload)) + return {} + + import plugins.memory.openviking as _mod + + real_client_cls = _mod._VikingClient + _mod._VikingClient = StubClient + messages = [ + {"role": "user", "content": [{"type": "input_text", "text": "u"}]}, + { + "role": "assistant", + "content": "Looking.", + "tool_calls": [ + { + "id": "call-1", + "type": "function", + "function": {"name": "shell_command", "arguments": json.dumps({"cmd": "pwd"})}, + } + ], + }, + {"role": "tool", "tool_call_id": "call-1", "name": "shell_command", "content": "ok"}, + {"role": "assistant", "content": [{"type": "output_text", "text": "a"}]}, + ] + try: + provider.sync_turn("u", "a", messages=messages) + assert provider._drain_writers("sid-structured", timeout=2.0) + finally: + _mod._VikingClient = real_client_cls + + assert captured == [( + "/api/v1/sessions/sid-structured/messages/batch", + { + "messages": [ + {"role": "user", "parts": [{"type": "text", "text": "u"}]}, + {"role": "assistant", "parts": [{"type": "text", "text": "Looking."}], "peer_id": "hermes"}, + { + "role": "assistant", + "parts": [ + { + "type": "tool", + "tool_id": "call-1", + "tool_name": "shell_command", + "tool_input": {"cmd": "pwd"}, + "tool_output": "ok", + "tool_status": "completed", + } + ], + "peer_id": "hermes", + }, + {"role": "assistant", "parts": [{"type": "text", "text": "a"}], "peer_id": "hermes"}, + ] + }, + )] + + def test_sync_turn_noop_when_session_id_blank(): provider = OpenVikingMemoryProvider() provider._client = MagicMock() diff --git a/tests/run_agent/test_codex_app_server_integration.py b/tests/run_agent/test_codex_app_server_integration.py index 14c058178b9..b0d2ec23861 100644 --- a/tests/run_agent/test_codex_app_server_integration.py +++ b/tests/run_agent/test_codex_app_server_integration.py @@ -12,7 +12,7 @@ Verifies that: from __future__ import annotations -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest @@ -148,6 +148,17 @@ class TestRunConversationCodexPath: and m.get("content") == "echo: hello"] assert final, f"expected final assistant message in {msgs}" + def test_projected_messages_are_synced_to_external_memory(self, fake_session): + agent = _make_codex_agent() + agent._memory_manager = MagicMock() + agent._memory_manager.build_system_prompt.return_value = "" + + with patch.object(agent, "_spawn_background_review", return_value=None): + result = agent.run_conversation("hello") + + agent._memory_manager.sync_all.assert_called_once() + assert agent._memory_manager.sync_all.call_args.kwargs["messages"] == result["messages"] + def test_nudge_counters_tick(self, fake_session): """The skill nudge counter must accumulate tool_iterations across turns. The memory nudge counter is gated on memory being configured From 15e3b64b7538bb0a38e4bfd91d9c8a4f8110ce8f Mon Sep 17 00:00:00 2001 From: Shannon Sands <shannon.sands.1979@gmail.com> Date: Fri, 19 Jun 2026 11:25:05 +1000 Subject: [PATCH 14/90] fix(tui): keep hosted dashboard chat alive on exit --- .../src/__tests__/createSlashHandler.test.ts | 30 +++++++++++++++++++ ui-tui/src/app/slash/commands/core.ts | 24 ++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/ui-tui/src/__tests__/createSlashHandler.test.ts b/ui-tui/src/__tests__/createSlashHandler.test.ts index a671063e5e9..c0247795af3 100644 --- a/ui-tui/src/__tests__/createSlashHandler.test.ts +++ b/ui-tui/src/__tests__/createSlashHandler.test.ts @@ -9,6 +9,10 @@ describe('createSlashHandler', () => { beforeEach(() => { resetOverlayState() resetUiState() + delete process.env.HERMES_TUI_INLINE + delete process.env.HERMES_HOME + delete process.env.HERMES_WRITE_SAFE_ROOT + delete process.env.HERMES_DISABLE_LAZY_INSTALLS }) it('opens the unified sessions overlay for /resume', () => { @@ -68,6 +72,32 @@ describe('createSlashHandler', () => { expect(ctx.gateway.gw.request).not.toHaveBeenCalled() }) + it('keeps hosted dashboard chat alive for /exit', () => { + process.env.HERMES_TUI_INLINE = '1' + process.env.HERMES_HOME = '/opt/data/profiles/worker' + process.env.HERMES_WRITE_SAFE_ROOT = '/opt/data' + process.env.HERMES_DISABLE_LAZY_INSTALLS = '1' + const ctx = buildCtx() + + expect(createSlashHandler(ctx)('/exit')).toBe(true) + expect(ctx.session.die).not.toHaveBeenCalled() + expect(ctx.gateway.gw.request).not.toHaveBeenCalled() + expect(ctx.transcript.sys).toHaveBeenCalledWith( + 'exit is disabled in hosted dashboard chat — use /new to start a fresh session' + ) + }) + + it('keeps /quit available outside hosted dashboard chat', () => { + process.env.HERMES_TUI_INLINE = '1' + process.env.HERMES_HOME = '/Users/example/.hermes' + process.env.HERMES_WRITE_SAFE_ROOT = '/Users/example/.hermes' + process.env.HERMES_DISABLE_LAZY_INSTALLS = '1' + const ctx = buildCtx() + + expect(createSlashHandler(ctx)('/quit')).toBe(true) + expect(ctx.session.die).toHaveBeenCalledTimes(1) + }) + it('handles /update locally and exits with code 42 via dieWithCode', () => { vi.useFakeTimers() const ctx = buildCtx() diff --git a/ui-tui/src/app/slash/commands/core.ts b/ui-tui/src/app/slash/commands/core.ts index 5c021dbcdf9..b5d72cf7712 100644 --- a/ui-tui/src/app/slash/commands/core.ts +++ b/ui-tui/src/app/slash/commands/core.ts @@ -76,6 +76,20 @@ const DETAILS_USAGE = const DETAILS_SECTION_USAGE = 'usage: /details <section> [hidden|collapsed|expanded|reset]' +const truthyEnv = (v?: string) => /^(?:1|true|yes|on)$/i.test((v ?? '').trim()) + +const hostedInlineDashboardChat = () => { + const hermesHome = (process.env.HERMES_HOME ?? '').trim() + const hostedHome = hermesHome === '/opt/data' || hermesHome.startsWith('/opt/data/') + + return ( + process.env.HERMES_TUI_INLINE === '1' && + hostedHome && + process.env.HERMES_WRITE_SAFE_ROOT === '/opt/data' && + truthyEnv(process.env.HERMES_DISABLE_LAZY_INSTALLS) + ) +} + export const coreCommands: SlashCommand[] = [ { help: 'list commands + hotkeys', @@ -113,7 +127,15 @@ export const coreCommands: SlashCommand[] = [ aliases: ['exit'], help: 'exit hermes', name: 'quit', - run: (_arg, ctx) => ctx.session.die() + run: (_arg, ctx) => { + if (hostedInlineDashboardChat()) { + ctx.transcript.sys('exit is disabled in hosted dashboard chat — use /new to start a fresh session') + + return + } + + ctx.session.die() + } }, { From 3f0e9849e7a2753931ef32c624cae33a7461e653 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:29:19 +0530 Subject: [PATCH 15/90] refactor(tui): reuse DASHBOARD_TUI_MODE for hosted /exit guard Follow-up to the salvaged hosted /exit fix. Instead of a separate 4-env-var fingerprint (HERMES_TUI_INLINE + /opt/data HERMES_HOME + HERMES_WRITE_SAFE_ROOT + HERMES_DISABLE_LAZY_INSTALLS), gate /exit and /quit on the existing DASHBOARD_TUI_MODE flag (HERMES_TUI_DASHBOARD) that the keyboard idle-exit (useInputHandlers) and SIGINT-ignore (entry.tsx) paths already use. One hosted detection mechanism instead of two divergent ones. Extract the refusal text to an exported DASHBOARD_EXIT_DISABLED_MESSAGE so the test asserts the same source of truth as production (no change-detector on the literal). Test mocks only the DASHBOARD_TUI_MODE export via importActual so the other env exports stay real. --- .../src/__tests__/createSlashHandler.test.ts | 35 +++++++++++-------- ui-tui/src/app/slash/commands/core.ts | 30 ++++++++-------- 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/ui-tui/src/__tests__/createSlashHandler.test.ts b/ui-tui/src/__tests__/createSlashHandler.test.ts index c0247795af3..415dd4c0f3c 100644 --- a/ui-tui/src/__tests__/createSlashHandler.test.ts +++ b/ui-tui/src/__tests__/createSlashHandler.test.ts @@ -2,17 +2,30 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createSlashHandler } from '../app/createSlashHandler.js' import { getOverlayState, resetOverlayState } from '../app/overlayStore.js' +import { DASHBOARD_EXIT_DISABLED_MESSAGE } from '../app/slash/commands/core.js' import { getUiState, patchUiState, resetUiState } from '../app/uiStore.js' import { TUI_SESSION_MODEL_FLAG } from '../domain/slash.js' +// DASHBOARD_TUI_MODE resolves once at module load from HERMES_TUI_DASHBOARD, +// so toggling process.env in a test body can't move it. Mock just that one +// export (everything else stays real) and flip the holder per test. +const envState = { dashboardTuiMode: false } +vi.mock('../config/env.js', async importActual => { + const actual = await importActual<typeof import('../config/env.js')>() + + return { + ...actual, + get DASHBOARD_TUI_MODE() { + return envState.dashboardTuiMode + } + } +}) + describe('createSlashHandler', () => { beforeEach(() => { resetOverlayState() resetUiState() - delete process.env.HERMES_TUI_INLINE - delete process.env.HERMES_HOME - delete process.env.HERMES_WRITE_SAFE_ROOT - delete process.env.HERMES_DISABLE_LAZY_INSTALLS + envState.dashboardTuiMode = false }) it('opens the unified sessions overlay for /resume', () => { @@ -73,25 +86,17 @@ describe('createSlashHandler', () => { }) it('keeps hosted dashboard chat alive for /exit', () => { - process.env.HERMES_TUI_INLINE = '1' - process.env.HERMES_HOME = '/opt/data/profiles/worker' - process.env.HERMES_WRITE_SAFE_ROOT = '/opt/data' - process.env.HERMES_DISABLE_LAZY_INSTALLS = '1' + envState.dashboardTuiMode = true const ctx = buildCtx() expect(createSlashHandler(ctx)('/exit')).toBe(true) expect(ctx.session.die).not.toHaveBeenCalled() expect(ctx.gateway.gw.request).not.toHaveBeenCalled() - expect(ctx.transcript.sys).toHaveBeenCalledWith( - 'exit is disabled in hosted dashboard chat — use /new to start a fresh session' - ) + expect(ctx.transcript.sys).toHaveBeenCalledWith(DASHBOARD_EXIT_DISABLED_MESSAGE) }) it('keeps /quit available outside hosted dashboard chat', () => { - process.env.HERMES_TUI_INLINE = '1' - process.env.HERMES_HOME = '/Users/example/.hermes' - process.env.HERMES_WRITE_SAFE_ROOT = '/Users/example/.hermes' - process.env.HERMES_DISABLE_LAZY_INSTALLS = '1' + envState.dashboardTuiMode = false const ctx = buildCtx() expect(createSlashHandler(ctx)('/quit')).toBe(true) diff --git a/ui-tui/src/app/slash/commands/core.ts b/ui-tui/src/app/slash/commands/core.ts index b5d72cf7712..7c5a79505ad 100644 --- a/ui-tui/src/app/slash/commands/core.ts +++ b/ui-tui/src/app/slash/commands/core.ts @@ -1,6 +1,6 @@ import { forceRedraw, type MouseTrackingMode } from '@hermes/ink' -import { NO_CONFIRM_DESTRUCTIVE } from '../../../config/env.js' +import { DASHBOARD_TUI_MODE, NO_CONFIRM_DESTRUCTIVE } from '../../../config/env.js' import { dailyFortune, randomFortune } from '../../../content/fortunes.js' import { HOTKEYS } from '../../../content/hotkeys.js' import { isSectionName, nextDetailsMode, parseDetailsMode, SECTION_NAMES } from '../../../domain/details.js' @@ -76,19 +76,10 @@ const DETAILS_USAGE = const DETAILS_SECTION_USAGE = 'usage: /details <section> [hidden|collapsed|expanded|reset]' -const truthyEnv = (v?: string) => /^(?:1|true|yes|on)$/i.test((v ?? '').trim()) - -const hostedInlineDashboardChat = () => { - const hermesHome = (process.env.HERMES_HOME ?? '').trim() - const hostedHome = hermesHome === '/opt/data' || hermesHome.startsWith('/opt/data/') - - return ( - process.env.HERMES_TUI_INLINE === '1' && - hostedHome && - process.env.HERMES_WRITE_SAFE_ROOT === '/opt/data' && - truthyEnv(process.env.HERMES_DISABLE_LAZY_INSTALLS) - ) -} +// Shown when /exit or /quit is refused in the hosted dashboard chat. Kept as a +// constant so the test asserts against the same source of truth as production. +export const DASHBOARD_EXIT_DISABLED_MESSAGE = + 'exit is disabled in hosted dashboard chat — use /new to start a fresh session' export const coreCommands: SlashCommand[] = [ { @@ -128,8 +119,15 @@ export const coreCommands: SlashCommand[] = [ help: 'exit hermes', name: 'quit', run: (_arg, ctx) => { - if (hostedInlineDashboardChat()) { - ctx.transcript.sys('exit is disabled in hosted dashboard chat — use /new to start a fresh session') + // In the hosted dashboard chat there is no in-page restart path after + // the PTY child exits, so quitting bricks the tab until a refresh. The + // keyboard idle-exit (Ctrl+C / Ctrl+D) and SIGINT handling already refuse + // to die in this mode (see useInputHandlers + entry.tsx); gate /exit and + // /quit on the same DASHBOARD_TUI_MODE flag. Unlike the keyboard path + // (which auto-starts a fresh chat), the explicit quit command refuses and + // instructs the user to run /new themselves. + if (DASHBOARD_TUI_MODE) { + ctx.transcript.sys(DASHBOARD_EXIT_DISABLED_MESSAGE) return } From 5a856bdfa355bb45330a23ecb63abdf9b810e865 Mon Sep 17 00:00:00 2001 From: Hao Zhe <haozhe4547@gmail.com> Date: Fri, 19 Jun 2026 15:38:25 +0800 Subject: [PATCH 16/90] chore(release): add OpenViking contributor attribution --- scripts/release.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/release.py b/scripts/release.py index 6c5d33ec3a1..4e5f8844439 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -1577,6 +1577,7 @@ AUTHOR_MAP = { "sunsky.lau@gmail.com": "liuhao1024", # PR #45494 salvage (claim session slot before auto-resume task; #45456) "andrewdmwalker@gmail.com": "capt-marbles", # PR #38440 salvage (resolve xAI OAuth credentials across profiles; #43589) "infinitycrew39@gmail.com": "infinitycrew39", # PR #47945 salvage (scope langfuse trace state by turn/request ids; #48292) + "eurekaxun@163.com": "huangxun375-stack", # PR #37251 / #48894 structured OpenViking sync } From 9362ce2575e00f5a795285b74e79d54c02e1326c Mon Sep 17 00:00:00 2001 From: Siddharth Balyan <52913345+alt-glitch@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:32:31 +0530 Subject: [PATCH 17/90] feat(skills): add html-artifact skill, fold in sketch + architecture-diagram + concept-diagrams (#48899) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(skills): add html-artifact skill, fold in sketch + architecture-diagram + concept-diagrams Adds a unified `html-artifact` creative skill that produces self-contained, single-file HTML artifacts — concept explainers, implementation plans, status/incident reports, code-review walkthroughs, technical + educational SVG diagrams, multi-variant design comparisons, and throwaway editors that export their state back to the clipboard. Grounded in Anthropic's html-effectiveness gallery (MIT); the house style (token block, serif/sans/ mono split, hand-rolled diffs, inline-SVG diagrams, graceful degradation) is distilled from reading all 20 reference files. Supersedes and removes three overlapping skills, folding their unique value in: - sketch -> the fidelity dial (throwaway vs presentation) + the multi-variant comparison layouts + the browser-vision verify loop (references/fidelity-and-verify.md) - architecture-diagram-> the dark "infra" token variant + double-rect masking + semantic component palette (references/dark-tech.md, templates/diagram.html infra mode) - concept-diagrams -> the 9-ramp educational color system + the concept archetype library (references/concept-archetypes.md, the light design system in templates/diagram.html) Structure: - SKILL.md (description exactly 60 chars), 6 references, 3 templates - templates verified by headless-Chrome render + vision inspection - editor export logic (file://-safe clipboard, Promise-normalized) verified in node Cross-references updated in claude-design (new disambiguation table row drawing the design-taste vs information-artifact boundary), design-md, pretext, spike, and kanban-video-orchestrator. Website skill docs + catalogs regenerated; stale EN/zh-Hans per-skill pages pruned and i18n cross-refs fixed. Not folded (intentionally orthogonal): excalidraw (.excalidraw JSON), p5js (generative canvas), claude-design / popular-web-designs / design-md (visual design taste / brand vocab / token spec). * feat(skills): ship html-effectiveness gallery as fetched reference examples Add scripts/fetch-examples.sh (idempotent clone/pull of Anthropic's MIT html-effectiveness gallery) + references/examples.md mapping each of the 20 example files to a mode so the agent reads the right worked example. The clone lands in references/examples/ and is gitignored (it's a 384KB upstream repo, not vendored). SKILL.md workflow + reference list now point at it; falls back to the distilled pattern references when offline. * feat(skills): make reading a gallery example a required authoring step Reading the matching html-effectiveness example is now workflow step 2 (was an optional aside in step 3): fetch the gallery, read_file the file for your mode, mirror its structure. Models skip optional steps; the examples are the ground truth, so consulting one is mandatory. Added an 'Example' column to the mode->build quick-reference table and a 'don't skip the example' pitfall. Also dogfooded the skill: read 03-code-review-pr.html and 13-flowchart-diagram.html raw and reconciled the distilled references against source — aligned diff-row tint opacity to the source's 0.15 (was 0.18) and added the .ctx/.hunk rows in house-style.md + base.html so they match 03-code-review-pr.html verbatim. * docs(skills): explain the consolidation + bundled-vs-optional rationale The supersession note only stated *what* was folded, not *why* the prune is sound. Expand SKILL.md's intro into a 'Why this skill exists' section: the three former skills emitted the same artifact and overlapped, so consolidating removes which-one-do-I-load ambiguity; and the optional->bundled promotion of concept-diagrams is footprint-safe because this skill has zero deps (only cost is the 60-char description; everything else is progressive-disclosure). States the bundling dividing line explicitly: zero install cost + broadly useful gets bundled, real install cost (hyperframes: Node+FFmpeg+Chromium) stays optional. Regenerated website per-skill page to match. --- .../creative/concept-diagrams/SKILL.md | 362 ----------------- .../apartment-floor-plan-conversion.md | 244 ----------- .../examples/automated-password-reset-flow.md | 276 ------------- .../autonomous-llm-research-agent-flow.md | 240 ----------- .../banana-journey-tree-to-smoothie.md | 161 -------- .../examples/commercial-aircraft-structure.md | 209 ---------- .../examples/cpu-ooo-microarchitecture.md | 236 ----------- .../examples/electricity-grid-flow.md | 182 --------- .../feature-film-production-pipeline.md | 172 -------- .../hospital-emergency-department-flow.md | 165 -------- .../ml-benchmark-grouped-bar-chart.md | 114 ------ .../examples/place-order-uml-sequence.md | 325 --------------- .../examples/smart-city-infrastructure.md | 173 -------- .../examples/smartphone-layer-anatomy.md | 154 ------- .../examples/sn2-reaction-mechanism.md | 247 ------------ .../examples/wind-turbine-structure.md | 338 ---------------- .../references/dashboard-patterns.md | 43 -- .../references/infrastructure-patterns.md | 144 ------- .../references/physical-shape-cookbook.md | 42 -- .../concept-diagrams/templates/template.html | 174 -------- .../kanban-video-orchestrator/SKILL.md | 2 +- .../references/intake.md | 3 +- .../references/role-archetypes.md | 5 +- .../references/tool-matrix.md | 4 +- skills/creative/architecture-diagram/SKILL.md | 148 ------- .../templates/template.html | 319 --------------- skills/creative/claude-design/SKILL.md | 12 +- skills/creative/design-md/SKILL.md | 2 +- skills/creative/html-artifact/SKILL.md | 184 +++++++++ .../html-artifact/references/.gitignore | 3 + .../references/concept-archetypes.md | 94 +++++ .../html-artifact/references/dark-tech.md | 92 +++++ .../html-artifact/references/examples.md | 64 +++ .../references/fidelity-and-verify.md | 78 ++++ .../html-artifact/references/house-style.md | 179 +++++++++ .../html-artifact/references/svg-diagrams.md | 123 ++++++ .../references/throwaway-editors.md | 114 ++++++ .../html-artifact/scripts/fetch-examples.sh | 43 ++ .../html-artifact/templates/base.html | 104 +++++ .../html-artifact/templates/diagram.html | 127 ++++++ .../html-artifact/templates/editor.html | 120 ++++++ skills/creative/pretext/SKILL.md | 2 +- skills/creative/sketch/SKILL.md | 218 ---------- skills/software-development/spike/SKILL.md | 2 +- .../docs/reference/optional-skills-catalog.md | 1 - website/docs/reference/skills-catalog.md | 3 +- .../autonomous-ai-agents-hermes-agent.md | 4 +- .../creative/creative-architecture-diagram.md | 165 -------- .../creative/creative-claude-design.md | 12 +- .../bundled/creative/creative-design-md.md | 2 +- .../creative/creative-html-artifact.md | 202 ++++++++++ .../bundled/creative/creative-pretext.md | 2 +- .../bundled/creative/creative-sketch.md | 238 ----------- .../creative/creative-touchdesigner-mcp.md | 2 +- .../skills/bundled/email/email-himalaya.md | 5 + .../bundled/github/github-github-auth.md | 4 +- .../github/github-github-code-review.md | 4 +- .../bundled/github/github-github-issues.md | 4 +- .../github/github-github-pr-workflow.md | 4 +- .../github/github-github-repo-management.md | 4 +- .../skills/bundled/media/media-gif-search.md | 2 +- .../note-taking/note-taking-obsidian.md | 2 +- .../productivity/productivity-airtable.md | 4 +- .../productivity/productivity-notion.md | 4 +- .../productivity-teams-meeting-pipeline.md | 2 +- .../bundled/research/research-llm-wiki.md | 2 +- .../research-research-paper-writing.md | 2 +- ...tware-development-node-inspect-debugger.md | 2 +- .../software-development-python-debugpy.md | 2 +- .../software-development-spike.md | 2 +- .../autonomous-ai-agents-honcho.md | 4 +- .../blockchain/blockchain-hyperliquid.md | 4 +- .../creative/creative-concept-diagrams.md | 379 ------------------ .../creative-kanban-video-orchestrator.md | 4 +- .../optional/devops/devops-pinggy-tunnel.md | 2 +- .../skills/optional/devops/devops-watchers.md | 2 +- .../skills/optional/mcp/mcp-fastmcp.md | 2 +- .../payments/payments-stripe-projects.md | 2 +- .../productivity/productivity-canvas.md | 2 +- .../productivity/productivity-shopify.md | 2 +- .../productivity/productivity-siyuan.md | 2 +- .../productivity/productivity-telephony.md | 8 +- .../research/research-gitnexus-explorer.md | 2 +- .../skills/optional/research/research-qmd.md | 2 +- .../optional/security/security-1password.md | 2 +- .../optional/security/security-godmode.md | 2 +- ...software-development-rest-graphql-debug.md | 2 +- .../reference/optional-skills-catalog.md | 1 - .../current/reference/skills-catalog.md | 2 - .../creative/creative-architecture-diagram.md | 165 -------- .../creative/creative-claude-design.md | 2 +- .../bundled/creative/creative-design-md.md | 2 +- .../bundled/creative/creative-pretext.md | 2 +- .../bundled/creative/creative-sketch.md | 238 ----------- .../software-development-spike.md | 2 +- .../creative/creative-concept-diagrams.md | 379 ------------------ .../creative-kanban-video-orchestrator.md | 2 +- website/sidebars.ts | 5 +- 98 files changed, 1610 insertions(+), 6336 deletions(-) delete mode 100644 optional-skills/creative/concept-diagrams/SKILL.md delete mode 100644 optional-skills/creative/concept-diagrams/examples/apartment-floor-plan-conversion.md delete mode 100644 optional-skills/creative/concept-diagrams/examples/automated-password-reset-flow.md delete mode 100644 optional-skills/creative/concept-diagrams/examples/autonomous-llm-research-agent-flow.md delete mode 100644 optional-skills/creative/concept-diagrams/examples/banana-journey-tree-to-smoothie.md delete mode 100644 optional-skills/creative/concept-diagrams/examples/commercial-aircraft-structure.md delete mode 100644 optional-skills/creative/concept-diagrams/examples/cpu-ooo-microarchitecture.md delete mode 100644 optional-skills/creative/concept-diagrams/examples/electricity-grid-flow.md delete mode 100644 optional-skills/creative/concept-diagrams/examples/feature-film-production-pipeline.md delete mode 100644 optional-skills/creative/concept-diagrams/examples/hospital-emergency-department-flow.md delete mode 100644 optional-skills/creative/concept-diagrams/examples/ml-benchmark-grouped-bar-chart.md delete mode 100644 optional-skills/creative/concept-diagrams/examples/place-order-uml-sequence.md delete mode 100644 optional-skills/creative/concept-diagrams/examples/smart-city-infrastructure.md delete mode 100644 optional-skills/creative/concept-diagrams/examples/smartphone-layer-anatomy.md delete mode 100644 optional-skills/creative/concept-diagrams/examples/sn2-reaction-mechanism.md delete mode 100644 optional-skills/creative/concept-diagrams/examples/wind-turbine-structure.md delete mode 100644 optional-skills/creative/concept-diagrams/references/dashboard-patterns.md delete mode 100644 optional-skills/creative/concept-diagrams/references/infrastructure-patterns.md delete mode 100644 optional-skills/creative/concept-diagrams/references/physical-shape-cookbook.md delete mode 100644 optional-skills/creative/concept-diagrams/templates/template.html delete mode 100644 skills/creative/architecture-diagram/SKILL.md delete mode 100644 skills/creative/architecture-diagram/templates/template.html create mode 100644 skills/creative/html-artifact/SKILL.md create mode 100644 skills/creative/html-artifact/references/.gitignore create mode 100644 skills/creative/html-artifact/references/concept-archetypes.md create mode 100644 skills/creative/html-artifact/references/dark-tech.md create mode 100644 skills/creative/html-artifact/references/examples.md create mode 100644 skills/creative/html-artifact/references/fidelity-and-verify.md create mode 100644 skills/creative/html-artifact/references/house-style.md create mode 100644 skills/creative/html-artifact/references/svg-diagrams.md create mode 100644 skills/creative/html-artifact/references/throwaway-editors.md create mode 100755 skills/creative/html-artifact/scripts/fetch-examples.sh create mode 100644 skills/creative/html-artifact/templates/base.html create mode 100644 skills/creative/html-artifact/templates/diagram.html create mode 100644 skills/creative/html-artifact/templates/editor.html delete mode 100644 skills/creative/sketch/SKILL.md delete mode 100644 website/docs/user-guide/skills/bundled/creative/creative-architecture-diagram.md create mode 100644 website/docs/user-guide/skills/bundled/creative/creative-html-artifact.md delete mode 100644 website/docs/user-guide/skills/bundled/creative/creative-sketch.md delete mode 100644 website/docs/user-guide/skills/optional/creative/creative-concept-diagrams.md delete mode 100644 website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-architecture-diagram.md delete mode 100644 website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-sketch.md delete mode 100644 website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/optional/creative/creative-concept-diagrams.md diff --git a/optional-skills/creative/concept-diagrams/SKILL.md b/optional-skills/creative/concept-diagrams/SKILL.md deleted file mode 100644 index 6017d4fd121..00000000000 --- a/optional-skills/creative/concept-diagrams/SKILL.md +++ /dev/null @@ -1,362 +0,0 @@ ---- -name: concept-diagrams -description: Generate flat, minimal light/dark-aware SVG diagrams as standalone HTML files, using a unified educational visual language with 9 semantic color ramps, sentence-case typography, and automatic dark mode. Best suited for educational and non-software visuals — physics setups, chemistry mechanisms, math curves, physical objects (aircraft, turbines, smartphones, mechanical watches), anatomy, floor plans, cross-sections, narrative journeys (lifecycle of X, process of Y), hub-spoke system integrations (smart city, IoT), and exploded layer views. If a more specialized skill exists for the subject (dedicated software/cloud architecture, hand-drawn sketches, animated explainers, etc.), prefer that — otherwise this skill can also serve as a general-purpose SVG diagram fallback with a clean educational look. Ships with 15 example diagrams. -version: 0.1.0 -author: v1k22 (original PR), ported into hermes-agent -license: MIT -dependencies: [] -platforms: [linux, macos, windows] -metadata: - hermes: - tags: [diagrams, svg, visualization, education, physics, chemistry, engineering] - related_skills: [architecture-diagram, excalidraw, generative-widgets] ---- - -# Concept Diagrams - -Generate production-quality SVG diagrams with a unified flat, minimal design system. Output is a single self-contained HTML file that renders identically in any modern browser, with automatic light/dark mode. - -## Scope - -**Best suited for:** -- Physics setups, chemistry mechanisms, math curves, biology -- Physical objects (aircraft, turbines, smartphones, mechanical watches, cells) -- Anatomy, cross-sections, exploded layer views -- Floor plans, architectural conversions -- Narrative journeys (lifecycle of X, process of Y) -- Hub-spoke system integrations (smart city, IoT networks, electricity grids) -- Educational / textbook-style visuals in any domain -- Quantitative charts (grouped bars, energy profiles) - -**Look elsewhere first for:** -- Dedicated software / cloud infrastructure architecture with a dark tech aesthetic (consider `architecture-diagram` if available) -- Hand-drawn whiteboard sketches (consider `excalidraw` if available) -- Animated explainers or video output (consider an animation skill) - -If a more specialized skill is available for the subject, prefer that. If none fits, this skill can serve as a general-purpose SVG diagram fallback — the output will carry the clean educational aesthetic described below, which is a reasonable default for almost any subject. - -## Workflow - -1. Decide on the diagram type (see Diagram Types below). -2. Lay out components using the Design System rules. -3. Write the full HTML page using `templates/template.html` as the wrapper — paste your SVG where the template says `<!-- PASTE SVG HERE -->`. -4. Save as a standalone `.html` file (for example `~/my-diagram.html` or `./my-diagram.html`). -5. User opens it directly in a browser — no server, no dependencies. - -Optional: if the user wants a browsable gallery of multiple diagrams, see "Local Preview Server" at the bottom. - -Load the HTML template: -``` -skill_view(name="concept-diagrams", file_path="templates/template.html") -``` - -The template embeds the full CSS design system (`c-*` color classes, text classes, light/dark variables, arrow marker styles). The SVG you generate relies on these classes being present on the hosting page. - ---- - -## Design System - -### Philosophy - -- **Flat**: no gradients, drop shadows, blur, glow, or neon effects. -- **Minimal**: show the essential. No decorative icons inside boxes. -- **Consistent**: same colors, spacing, typography, and stroke widths across every diagram. -- **Dark-mode ready**: all colors auto-adapt via CSS classes — no per-mode SVG. - -### Color Palette - -9 color ramps, each with 7 stops. Put the class name on a `<g>` or shape element; the template CSS handles both modes. - -| Class | 50 (lightest) | 100 | 200 | 400 | 600 | 800 | 900 (darkest) | -|------------|---------------|---------|---------|---------|---------|---------|---------------| -| `c-purple` | #EEEDFE | #CECBF6 | #AFA9EC | #7F77DD | #534AB7 | #3C3489 | #26215C | -| `c-teal` | #E1F5EE | #9FE1CB | #5DCAA5 | #1D9E75 | #0F6E56 | #085041 | #04342C | -| `c-coral` | #FAECE7 | #F5C4B3 | #F0997B | #D85A30 | #993C1D | #712B13 | #4A1B0C | -| `c-pink` | #FBEAF0 | #F4C0D1 | #ED93B1 | #D4537E | #993556 | #72243E | #4B1528 | -| `c-gray` | #F1EFE8 | #D3D1C7 | #B4B2A9 | #888780 | #5F5E5A | #444441 | #2C2C2A | -| `c-blue` | #E6F1FB | #B5D4F4 | #85B7EB | #378ADD | #185FA5 | #0C447C | #042C53 | -| `c-green` | #EAF3DE | #C0DD97 | #97C459 | #639922 | #3B6D11 | #27500A | #173404 | -| `c-amber` | #FAEEDA | #FAC775 | #EF9F27 | #BA7517 | #854F0B | #633806 | #412402 | -| `c-red` | #FCEBEB | #F7C1C1 | #F09595 | #E24B4A | #A32D2D | #791F1F | #501313 | - -#### Color Assignment Rules - -Color encodes **meaning**, not sequence. Never cycle through colors like a rainbow. - -- Group nodes by **category** — all nodes of the same type share one color. -- Use `c-gray` for neutral/structural nodes (start, end, generic steps, users). -- Use **2-3 colors per diagram**, not 6+. -- Prefer `c-purple`, `c-teal`, `c-coral`, `c-pink` for general categories. -- Reserve `c-blue`, `c-green`, `c-amber`, `c-red` for semantic meaning (info, success, warning, error). - -Light/dark stop mapping (handled by the template CSS — just use the class): -- Light mode: 50 fill + 600 stroke + 800 title / 600 subtitle -- Dark mode: 800 fill + 200 stroke + 100 title / 200 subtitle - -### Typography - -Only two font sizes. No exceptions. - -| Class | Size | Weight | Use | -|-------|------|--------|-----| -| `th` | 14px | 500 | Node titles, region labels | -| `ts` | 12px | 400 | Subtitles, descriptions, arrow labels | -| `t` | 14px | 400 | General text | - -- **Sentence case always.** Never Title Case, never ALL CAPS. -- Every `<text>` MUST carry a class (`t`, `ts`, or `th`). No unclassed text. -- `dominant-baseline="central"` on all text inside boxes. -- `text-anchor="middle"` for centered text in boxes. - -**Width estimation (approx):** -- 14px weight 500: ~8px per character -- 12px weight 400: ~6.5px per character -- Always verify: `box_width >= (char_count × px_per_char) + 48` (24px padding each side) - -### Spacing & Layout - -- **ViewBox**: `viewBox="0 0 680 H"` where H = content height + 40px buffer. -- **Safe area**: x=40 to x=640, y=40 to y=(H-40). -- **Between boxes**: 60px minimum gap. -- **Inside boxes**: 24px horizontal padding, 12px vertical padding. -- **Arrowhead gap**: 10px between arrowhead and box edge. -- **Single-line box**: 44px height. -- **Two-line box**: 56px height, 18px between title and subtitle baselines. -- **Container padding**: 20px minimum inside every container. -- **Max nesting**: 2-3 levels deep. Deeper gets unreadable at 680px width. - -### Stroke & Shape - -- **Stroke width**: 0.5px on all node borders. Not 1px, not 2px. -- **Rect rounding**: `rx="8"` for nodes, `rx="12"` for inner containers, `rx="16"` to `rx="20"` for outer containers. -- **Connector paths**: MUST have `fill="none"`. SVG defaults to `fill: black` otherwise. - -### Arrow Marker - -Include this `<defs>` block at the start of **every** SVG: - -```xml -<defs> - <marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" - markerWidth="6" markerHeight="6" orient="auto-start-reverse"> - <path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" - stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> - </marker> -</defs> -``` - -Use `marker-end="url(#arrow)"` on lines. The arrowhead inherits the line color via `context-stroke`. - -### CSS Classes (Provided by the Template) - -The template page provides: - -- Text: `.t`, `.ts`, `.th` -- Neutral: `.box`, `.arr`, `.leader`, `.node` -- Color ramps: `.c-purple`, `.c-teal`, `.c-coral`, `.c-pink`, `.c-gray`, `.c-blue`, `.c-green`, `.c-amber`, `.c-red` (all with automatic light/dark mode) - -You do **not** need to redefine these — just apply them in your SVG. The template file contains the full CSS definitions. - ---- - -## SVG Boilerplate - -Every SVG inside the template page starts with this exact structure: - -```xml -<svg width="100%" viewBox="0 0 680 {HEIGHT}" xmlns="http://www.w3.org/2000/svg"> - <defs> - <marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" - markerWidth="6" markerHeight="6" orient="auto-start-reverse"> - <path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" - stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> - </marker> - </defs> - - <!-- Diagram content here --> - -</svg> -``` - -Replace `{HEIGHT}` with the actual computed height (last element bottom + 40px). - -### Node Patterns - -**Single-line node (44px):** -```xml -<g class="node c-blue"> - <rect x="100" y="20" width="180" height="44" rx="8" stroke-width="0.5"/> - <text class="th" x="190" y="42" text-anchor="middle" dominant-baseline="central">Service name</text> -</g> -``` - -**Two-line node (56px):** -```xml -<g class="node c-teal"> - <rect x="100" y="20" width="200" height="56" rx="8" stroke-width="0.5"/> - <text class="th" x="200" y="38" text-anchor="middle" dominant-baseline="central">Service name</text> - <text class="ts" x="200" y="56" text-anchor="middle" dominant-baseline="central">Short description</text> -</g> -``` - -**Connector (no label):** -```xml -<line x1="200" y1="76" x2="200" y2="120" class="arr" marker-end="url(#arrow)"/> -``` - -**Container (dashed or solid):** -```xml -<g class="c-purple"> - <rect x="40" y="92" width="600" height="300" rx="16" stroke-width="0.5"/> - <text class="th" x="66" y="116">Container label</text> - <text class="ts" x="66" y="134">Subtitle info</text> -</g> -``` - ---- - -## Diagram Types - -Choose the layout that fits the subject: - -1. **Flowchart** — CI/CD pipelines, request lifecycles, approval workflows, data processing. Single-direction flow (top-down or left-right). Max 4-5 nodes per row. -2. **Structural / Containment** — Cloud infrastructure nesting, system architecture with layers. Large outer containers with inner regions. Dashed rects for logical groupings. -3. **API / Endpoint Map** — REST routes, GraphQL schemas. Tree from root, branching to resource groups, each containing endpoint nodes. -4. **Microservice Topology** — Service mesh, event-driven systems. Services as nodes, arrows for communication patterns, message queues between. -5. **Data Flow** — ETL pipelines, streaming architectures. Left-to-right flow from sources through processing to sinks. -6. **Physical / Structural** — Vehicles, buildings, hardware, anatomy. Use shapes that match the physical form — `<path>` for curved bodies, `<polygon>` for tapered shapes, `<ellipse>`/`<circle>` for cylindrical parts, nested `<rect>` for compartments. See `references/physical-shape-cookbook.md`. -7. **Infrastructure / Systems Integration** — Smart cities, IoT networks, multi-domain systems. Hub-spoke layout with central platform connecting subsystems. Semantic line styles (`.data-line`, `.power-line`, `.water-pipe`, `.road`). See `references/infrastructure-patterns.md`. -8. **UI / Dashboard Mockups** — Admin panels, monitoring dashboards. Screen frame with nested chart/gauge/indicator elements. See `references/dashboard-patterns.md`. - -For physical, infrastructure, and dashboard diagrams, load the matching reference file before generating — each one provides ready-made CSS classes and shape primitives. - ---- - -## Validation Checklist - -Before finalizing any SVG, verify ALL of the following: - -1. Every `<text>` has class `t`, `ts`, or `th`. -2. Every `<text>` inside a box has `dominant-baseline="central"`. -3. Every connector `<path>` or `<line>` used as arrow has `fill="none"`. -4. No arrow line crosses through an unrelated box. -5. `box_width >= (longest_label_chars × 8) + 48` for 14px text. -6. `box_width >= (longest_label_chars × 6.5) + 48` for 12px text. -7. ViewBox height = bottom-most element + 40px. -8. All content stays within x=40 to x=640. -9. Color classes (`c-*`) are on `<g>` or shape elements, never on `<path>` connectors. -10. Arrow `<defs>` block is present. -11. No gradients, shadows, blur, or glow effects. -12. Stroke width is 0.5px on all node borders. - ---- - -## Output & Preview - -### Default: standalone HTML file - -Write a single `.html` file the user can open directly. No server, no dependencies, works offline. Pattern: - -```python -# 1. Load the template -template = skill_view("concept-diagrams", "templates/template.html") - -# 2. Fill in title, subtitle, and paste your SVG -html = template.replace( - "<!-- DIAGRAM TITLE HERE -->", "SN2 reaction mechanism" -).replace( - "<!-- OPTIONAL SUBTITLE HERE -->", "Bimolecular nucleophilic substitution" -).replace( - "<!-- PASTE SVG HERE -->", svg_content -) - -# 3. Write to a user-chosen path (or ./ by default) -write_file("./sn2-mechanism.html", html) -``` - -Tell the user how to open it: - -``` -# macOS -open ./sn2-mechanism.html -# Linux -xdg-open ./sn2-mechanism.html -``` - -### Optional: local preview server (multi-diagram gallery) - -Only use this when the user explicitly wants a browsable gallery of multiple diagrams. - -**Rules:** -- Bind to `127.0.0.1` only. Never `0.0.0.0`. Exposing diagrams on all network interfaces is a security hazard on shared networks. -- Pick a free port (do NOT hard-code one) and tell the user the chosen URL. -- The server is optional and opt-in — prefer the standalone HTML file first. - -Recommended pattern (lets the OS pick a free ephemeral port): - -```bash -# Put each diagram in its own folder under .diagrams/ -mkdir -p .diagrams/sn2-mechanism -# ...write .diagrams/sn2-mechanism/index.html... - -# Serve on loopback only, free port -cd .diagrams && python3 -c " -import http.server, socketserver -with socketserver.TCPServer(('127.0.0.1', 0), http.server.SimpleHTTPRequestHandler) as s: - print(f'Serving at http://127.0.0.1:{s.server_address[1]}/') - s.serve_forever() -" & -``` - -If the user insists on a fixed port, use `127.0.0.1:<port>` — still never `0.0.0.0`. Document how to stop the server (`kill %1` or `pkill -f "http.server"`). - ---- - -## Examples Reference - -The `examples/` directory ships 15 complete, tested diagrams. Browse them for working patterns before writing a new diagram of a similar type: - -| File | Type | Demonstrates | -|------|------|--------------| -| `hospital-emergency-department-flow.md` | Flowchart | Priority routing with semantic colors | -| `feature-film-production-pipeline.md` | Flowchart | Phased workflow, horizontal sub-flows | -| `automated-password-reset-flow.md` | Flowchart | Auth flow with error branches | -| `autonomous-llm-research-agent-flow.md` | Flowchart | Loop-back arrows, decision branches | -| `place-order-uml-sequence.md` | Sequence | UML sequence diagram style | -| `commercial-aircraft-structure.md` | Physical | Paths, polygons, ellipses for realistic shapes | -| `wind-turbine-structure.md` | Physical cross-section | Underground/above-ground separation, color coding | -| `smartphone-layer-anatomy.md` | Exploded view | Alternating left/right labels, layered components | -| `apartment-floor-plan-conversion.md` | Floor plan | Walls, doors, proposed changes in dotted red | -| `banana-journey-tree-to-smoothie.md` | Narrative journey | Winding path, progressive state changes | -| `cpu-ooo-microarchitecture.md` | Hardware pipeline | Fan-out, memory hierarchy sidebar | -| `sn2-reaction-mechanism.md` | Chemistry | Molecules, curved arrows, energy profile | -| `smart-city-infrastructure.md` | Hub-spoke | Semantic line styles per system | -| `electricity-grid-flow.md` | Multi-stage flow | Voltage hierarchy, flow markers | -| `ml-benchmark-grouped-bar-chart.md` | Chart | Grouped bars, dual axis | - -Load any example with: -``` -skill_view(name="concept-diagrams", file_path="examples/<filename>") -``` - ---- - -## Quick Reference: What to Use When - -| User says | Diagram type | Suggested colors | -|-----------|--------------|------------------| -| "show the pipeline" | Flowchart | gray start/end, purple steps, red errors, teal deploy | -| "draw the data flow" | Data pipeline (left-right) | gray sources, purple processing, teal sinks | -| "visualize the system" | Structural (containment) | purple container, teal services, coral data | -| "map the endpoints" | API tree | purple root, one ramp per resource group | -| "show the services" | Microservice topology | gray ingress, teal services, purple bus, coral workers | -| "draw the aircraft/vehicle" | Physical | paths, polygons, ellipses for realistic shapes | -| "smart city / IoT" | Hub-spoke integration | semantic line styles per subsystem | -| "show the dashboard" | UI mockup | dark screen, chart colors: teal, purple, coral for alerts | -| "power grid / electricity" | Multi-stage flow | voltage hierarchy (HV/MV/LV line weights) | -| "wind turbine / turbine" | Physical cross-section | foundation + tower cutaway + nacelle color-coded | -| "journey of X / lifecycle" | Narrative journey | winding path, progressive state changes | -| "layers of X / exploded" | Exploded layer view | vertical stack, alternating labels | -| "CPU / pipeline" | Hardware pipeline | vertical stages, fan-out to execution ports | -| "floor plan / apartment" | Floor plan | walls, doors, proposed changes in dotted red | -| "reaction mechanism" | Chemistry | atoms, bonds, curved arrows, transition state, energy profile | diff --git a/optional-skills/creative/concept-diagrams/examples/apartment-floor-plan-conversion.md b/optional-skills/creative/concept-diagrams/examples/apartment-floor-plan-conversion.md deleted file mode 100644 index 7c11d3401e5..00000000000 --- a/optional-skills/creative/concept-diagrams/examples/apartment-floor-plan-conversion.md +++ /dev/null @@ -1,244 +0,0 @@ -# Apartment Floor Plan: 3 BHK to 4 BHK Conversion - -An architectural floor plan showing a 1,500 sq ft apartment with proposed modifications to convert from 3 BHK to 4 BHK. Demonstrates architectural drawing conventions, room layouts, proposed changes with dotted lines, and area comparison tables. - -## Key Patterns Used - -- **Architectural floor plan**: Top-down view with walls, doors, windows -- **Proposed modifications**: Dotted red lines for new walls -- **Room color coding**: Light fills to distinguish room types -- **Circulation paths**: Arrows showing new access routes -- **Data table**: Before/after area comparison with highlighting -- **Architectural symbols**: North arrow, scale bar, door swings - -## Diagram Type - -This is an **architectural floor plan** with: -- **Plan view**: Top-down orthographic projection -- **Overlay technique**: Existing structure + proposed changes -- **Quantitative data**: Area measurements and comparison table - -## Architectural Drawing Elements - -### Wall Styles - -```xml -<!-- Outer walls (thick) --> -<line class="wall" x1="0" y1="0" x2="560" y2="0"/> - -<!-- Internal walls (thinner) --> -<line class="wall-thin" x1="180" y1="0" x2="180" y2="140"/> - -<!-- Proposed new walls (dotted red) --> -<line class="proposed-wall" x1="125" y1="170" x2="125" y2="330"/> -``` - -```css -.wall { stroke: var(--text-primary); stroke-width: 6; fill: none; stroke-linecap: square; } -.wall-thin { stroke: var(--text-primary); stroke-width: 3; fill: none; } -.proposed-wall { stroke: #A32D2D; stroke-width: 4; fill: none; stroke-dasharray: 8 4; } -``` - -### Door Symbols - -```xml -<!-- Door opening with swing arc --> -<rect x="150" y="137" width="25" height="6" fill="var(--bg-primary)"/> -<path class="door" d="M150,140 L150,165"/> -<path class="door-swing" d="M150,140 A25,25 0 0,0 175,140"/> - -<!-- Sliding door (balcony) --> -<rect x="60" y="327" width="60" height="6" fill="var(--bg-primary)" stroke="var(--text-secondary)" stroke-width="1"/> -<line x1="60" y1="330" x2="90" y2="330" stroke="var(--text-secondary)" stroke-width="2"/> -<line x1="90" y1="330" x2="120" y2="330" stroke="var(--text-secondary)" stroke-width="2" stroke-dasharray="3 3"/> - -<!-- Proposed door (dotted) --> -<rect x="143" y="292" width="22" height="6" fill="var(--bg-primary)" stroke="#A32D2D" stroke-width="1" stroke-dasharray="3 2"/> -<path d="M165,295 A22,22 0 0,0 165,273" stroke="#A32D2D" stroke-width="1" stroke-dasharray="3 2" fill="none"/> -``` - -```css -.door { stroke: var(--text-secondary); stroke-width: 1.5; fill: none; } -.door-swing { stroke: var(--text-tertiary); stroke-width: 1; fill: none; stroke-dasharray: 3 2; } -``` - -### Window Symbols - -```xml -<!-- Window with glass indication --> -<rect class="window" x="-3" y="30" width="6" height="50"/> -<line class="window-glass" x1="0" y1="35" x2="0" y2="75"/> - -<!-- Horizontal window (top wall) --> -<rect class="window" x="220" y="-3" width="60" height="6"/> -<line class="window-glass" x1="225" y1="0" x2="275" y2="0"/> -``` - -```css -.window { stroke: var(--text-primary); stroke-width: 1; fill: var(--bg-primary); } -.window-glass { stroke: #378ADD; stroke-width: 2; fill: none; } -``` - -### Room Fills - -```xml -<!-- Different colors for room types --> -<rect class="room-master" x="3" y="3" width="174" height="134" rx="2"/> -<rect class="room-bed2" x="183" y="3" width="134" height="104" rx="2"/> -<rect class="room-living" x="3" y="173" width="554" height="154" rx="2"/> -<rect class="room-kitchen" x="443" y="3" width="114" height="104" rx="2"/> -<rect class="room-bath" x="183" y="113" width="54" height="54" rx="2"/> - -<!-- Proposed new room (highlighted) --> -<rect class="room-new" x="3" y="223" width="120" height="104"/> -``` - -```css -.room-master { fill: rgba(206, 203, 246, 0.3); } /* purple tint */ -.room-bed2 { fill: rgba(159, 225, 203, 0.3); } /* teal tint */ -.room-bed3 { fill: rgba(250, 199, 117, 0.3); } /* amber tint */ -.room-living { fill: rgba(245, 196, 179, 0.3); } /* coral tint */ -.room-kitchen { fill: rgba(237, 147, 177, 0.3); } /* pink tint */ -.room-bath { fill: rgba(133, 183, 235, 0.3); } /* blue tint */ -.room-new { fill: rgba(163, 45, 45, 0.15); } /* red tint for proposed */ -``` - -### Support Fixtures - -```xml -<!-- Kitchen counter hint --> -<rect x="450" y="15" width="50" height="25" fill="none" stroke="var(--text-tertiary)" stroke-width="0.5" rx="2"/> -<text class="tx" x="475" y="30" text-anchor="middle">Counter</text> - -<!-- Balcony (dashed outline) --> -<rect class="balcony-fill" x="3" y="333" width="200" height="50"/> -``` - -```css -.balcony { fill: none; stroke: var(--text-secondary); stroke-width: 2; stroke-dasharray: 6 3; } -.balcony-fill { fill: rgba(93, 202, 165, 0.1); } -``` - -### Room Labels - -```xml -<!-- Room name and area --> -<text class="room-label" x="90" y="65" text-anchor="middle">MASTER</text> -<text class="room-label" x="90" y="78" text-anchor="middle">BEDROOM</text> -<text class="area-label" x="90" y="95" text-anchor="middle">195 sq ft</text> - -<!-- Proposed room (in red) --> -<text class="room-label" x="63" y="268" text-anchor="middle" fill="#A32D2D">BEDROOM 4</text> -<text class="tx" x="63" y="282" text-anchor="middle" fill="#A32D2D">(NEW)</text> -``` - -```css -.room-label { font-family: system-ui; font-size: 11px; fill: var(--text-primary); font-weight: 500; } -.area-label { font-family: system-ui; font-size: 9px; fill: var(--text-tertiary); } -``` - -### Circulation Arrow - -```xml -<defs> - <marker id="circ-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto"> - <path d="M0,0 L10,5 L0,10 Z" class="circulation-fill"/> - </marker> -</defs> - -<path class="circulation" d="M300,250 L200,250 L145,250 L145,280" marker-end="url(#circ-arrow)"/> -<text class="tx" x="250" y="242" fill="#3B6D11" font-weight="500">New corridor access</text> -``` - -```css -.circulation { stroke: #3B6D11; stroke-width: 2; fill: none; } -.circulation-fill { fill: #3B6D11; } -``` - -### North Arrow and Scale Bar - -```xml -<!-- North arrow --> -<g transform="translate(520, 260)"> - <circle cx="0" cy="0" r="20" fill="none" stroke="var(--text-tertiary)" stroke-width="0.5"/> - <polygon points="0,-18 -5,5 0,0 5,5" fill="var(--text-primary)"/> - <text class="tx" x="0" y="-22" text-anchor="middle">N</text> -</g> - -<!-- Scale bar --> -<g transform="translate(420, 300)"> - <line x1="0" y1="0" x2="100" y2="0" stroke="var(--text-primary)" stroke-width="2"/> - <line x1="0" y1="-5" x2="0" y2="5" stroke="var(--text-primary)" stroke-width="1"/> - <line x1="50" y1="-3" x2="50" y2="3" stroke="var(--text-primary)" stroke-width="1"/> - <line x1="100" y1="-5" x2="100" y2="5" stroke="var(--text-primary)" stroke-width="1"/> - <text class="tx" x="0" y="15" text-anchor="middle">0</text> - <text class="tx" x="50" y="15" text-anchor="middle">5'</text> - <text class="tx" x="100" y="15" text-anchor="middle">10'</text> -</g> -``` - -## Area Comparison Table - -### Table Structure - -```xml -<!-- Header row --> -<rect class="table-header" x="0" y="0" width="180" height="28" rx="4 4 0 0"/> -<text class="ts" x="90" y="18" text-anchor="middle" font-weight="500">Room</text> - -<!-- Normal row --> -<rect class="table-row" x="0" y="28" width="180" height="24"/> -<text class="tx" x="10" y="44">Master Bedroom</text> -<text class="tx" x="230" y="44" text-anchor="middle">195</text> - -<!-- Alternating row --> -<rect class="table-row-alt" x="0" y="52" width="180" height="24"/> - -<!-- Highlighted row (for changes) --> -<rect class="table-highlight" x="0" y="100" width="180" height="24"/> -<text class="tx" x="10" y="116" fill="#A32D2D" font-weight="500">Bedroom 4 (NEW)</text> -<text class="tx" x="430" y="116" text-anchor="middle" fill="#3B6D11">+100</text> - -<!-- Total row --> -<rect x="0" y="268" width="180" height="28" fill="var(--bg-secondary)" stroke="var(--border)" stroke-width="1"/> -<text class="ts" x="10" y="286" font-weight="500">TOTAL CARPET AREA</text> -``` - -```css -.table-header { fill: var(--bg-secondary); } -.table-row { fill: var(--bg-primary); stroke: var(--border); stroke-width: 0.5; } -.table-row-alt { fill: var(--bg-tertiary); stroke: var(--border); stroke-width: 0.5; } -.table-highlight { fill: rgba(163, 45, 45, 0.1); stroke: #A32D2D; stroke-width: 0.5; } -``` - -## Layout Notes - -- **ViewBox**: 800×780 (portrait for floor plan + table) -- **Scale**: 10px = 1 foot (apartment ~50ft × 33ft) -- **Floor plan origin**: Offset at (50, 60) for margins -- **Wall thickness**: 6px outer, 3px inner (represents ~6" walls) -- **Room labels**: Centered in each room with area below -- **Table placement**: Below floor plan with full width - -## Color Coding - -| Element | Color | Usage | -|---------|-------|-------| -| Proposed walls | Red (#A32D2D) dotted | New construction | -| New room fill | Red 15% opacity | Bedroom 4 area | -| Circulation | Green (#3B6D11) | New access path | -| Window glass | Blue (#378ADD) | Glass indication | -| Bedrooms | Purple/Teal/Amber tints | Room differentiation | -| Wet areas | Blue tint | Bathrooms | -| Living | Coral tint | Common areas | - -## When to Use This Pattern - -Use this diagram style for: -- Apartment/house floor plans -- Office layout planning -- Renovation proposals showing before/after -- Space planning with area calculations -- Real estate marketing materials -- Interior design presentations -- Building permit documentation diff --git a/optional-skills/creative/concept-diagrams/examples/automated-password-reset-flow.md b/optional-skills/creative/concept-diagrams/examples/automated-password-reset-flow.md deleted file mode 100644 index 86cd1cc0782..00000000000 --- a/optional-skills/creative/concept-diagrams/examples/automated-password-reset-flow.md +++ /dev/null @@ -1,276 +0,0 @@ -# Automated Password Reset Flow - -A two-section flowchart tracing the full user journey for a web application password reset: the initial request phase (forgot password → email check → token generation) and the reset-form phase (link click → new password entry → token/password validation). Demonstrates multi-exit decision diamonds, a three-column branching layout, a loop-back path, and a cross-section separator arrow. - -## Key Patterns Used - -- **Three-column layout**: Left column (error/terminal branches at cx=115), center column (main happy path at cx=340), right column (expired-token branch at cx=552) — allows side branches to live at the same y-level as center nodes without overlap -- **Decision diamonds with `<polygon>`**: Each decision uses a `<g class="decision">` wrapper containing a `<polygon>` and centered `<text>`; the diamond points are computed as `cx±hw, cy±hh` (hw=100, hh=28) -- **Pill-shaped terminals**: Start and end nodes use `rx=22` on their `<rect>` to signal entry/exit points; all mid-flow process nodes use `rx=8` -- **Three-branch decision paths**: Each diamond has a "Yes" branch (down, short `<line>`) and a "No" branch (`<path>` going horizontal then vertical to a side column) -- **Loop-back path**: Mismatch error node loops back to the password-entry node via a routing corridor at x=215 — a 5-px gap between the left column (right edge x=210) and center column (left edge x=220); the path exits the bottom of the error node, drops below it, travels right to x=215, then goes up to the target node's center y, then right 5 px into the node's left edge -- **Section separator**: A dashed horizontal `<line>` at y=452 splits the two phases; the connecting arrow crosses it with a faded label ("user receives email") to preserve flow continuity -- **Italic annotation**: The exact UX copy for the generic message ("If that email exists…") is shown as a faded italic `ts` text block below the left-branch terminal node -- **Legend row**: Five inline swatches (gray, purple, teal, red, amber diamond) at the bottom explain the color-to-role mapping - -## Diagram - -```xml -<svg width="100%" viewBox="0 0 680 960" xmlns="http://www.w3.org/2000/svg"> - <defs> - <marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" - markerWidth="6" markerHeight="6" orient="auto-start-reverse"> - <path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" - stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> - </marker> - </defs> - - <!-- - Column layout (680px viewBox, safe area x=40–640): - Left col : x=20, w=190, cx=115 (error / terminal branches) - Center col: x=220, w=240, cx=340 (main happy path) - Right col: x=465, w=175, cx=552 (expired-token branch) - Loop corridor at x=215 (5-px gap between left and center cols) - --> - - <!-- ═══ SECTION 1 — Forgot password request ═══ --> - <text class="ts" x="40" y="38" opacity=".45">Section 1 — Forgot password request</text> - - <!-- START terminal (pill rx=22 signals start/end) --> - <g class="c-gray"> - <rect x="220" y="46" width="240" height="44" rx="22"/> - <text class="th" x="340" y="68" text-anchor="middle" dominant-baseline="central">User: "Forgot password"</text> - </g> - - <line x1="340" y1="90" x2="340" y2="108" class="arr" marker-end="url(#arrow)"/> - - <!-- N2 · Enter email --> - <g class="c-gray"> - <rect x="220" y="108" width="240" height="44" rx="8"/> - <text class="th" x="340" y="130" text-anchor="middle" dominant-baseline="central">Enter email address</text> - </g> - - <line x1="340" y1="152" x2="340" y2="172" class="arr" marker-end="url(#arrow)"/> - - <!-- D1 · Email in system? diamond: center=(340,200) hw=100 hh=28 --> - <g class="decision"> - <polygon points="340,172 440,200 340,228 240,200"/> - <text class="th" x="340" y="200" text-anchor="middle" dominant-baseline="central">Email in system?</text> - </g> - - <!-- D1 "No" → left column --> - <path d="M 240,200 L 115,200 L 115,248" class="arr" marker-end="url(#arrow)"/> - <text class="ts" x="178" y="193" text-anchor="middle" opacity=".75">No</text> - - <!-- D1 "Yes" → continue down --> - <line x1="340" y1="228" x2="340" y2="248" class="arr" marker-end="url(#arrow)"/> - <text class="ts" x="348" y="242" text-anchor="start" opacity=".75">Yes</text> - - <!-- ── Left branch (D1 = No): generic security message → end ── --> - - <!-- L1 · Generic message (security: never confirm email existence) --> - <g class="c-gray"> - <rect x="20" y="248" width="190" height="56" rx="8"/> - <text class="th" x="115" y="269" text-anchor="middle" dominant-baseline="central">Generic message shown</text> - <text class="ts" x="115" y="287" text-anchor="middle" dominant-baseline="central">Email sent if found</text> - </g> - - <line x1="115" y1="304" x2="115" y2="324" class="arr" marker-end="url(#arrow)"/> - - <!-- L2 · End terminal (left) --> - <g class="c-gray"> - <rect x="20" y="324" width="190" height="44" rx="22"/> - <text class="th" x="115" y="346" text-anchor="middle" dominant-baseline="central">Request handled</text> - </g> - - <!-- Italic annotation: actual UX copy shown below the end node --> - <text class="ts" x="20" y="384" opacity=".45" font-style="italic">"If that email exists, a reset</text> - <text class="ts" x="20" y="398" opacity=".45" font-style="italic">link has been sent."</text> - - <!-- ── Center Yes branch: system generates & sends token ── --> - - <!-- N3 · Generate unique token --> - <g class="c-purple"> - <rect x="220" y="248" width="240" height="56" rx="8"/> - <text class="th" x="340" y="269" text-anchor="middle" dominant-baseline="central">Generate unique token</text> - <text class="ts" x="340" y="287" text-anchor="middle" dominant-baseline="central">Time-limited, cryptographic</text> - </g> - - <line x1="340" y1="304" x2="340" y2="324" class="arr" marker-end="url(#arrow)"/> - - <!-- N4 · Store token + user ID --> - <g class="c-purple"> - <rect x="220" y="324" width="240" height="44" rx="8"/> - <text class="th" x="340" y="346" text-anchor="middle" dominant-baseline="central">Store token + user ID</text> - </g> - - <line x1="340" y1="368" x2="340" y2="388" class="arr" marker-end="url(#arrow)"/> - - <!-- N5 · Send reset email --> - <g class="c-teal"> - <rect x="220" y="388" width="240" height="44" rx="8"/> - <text class="th" x="340" y="410" text-anchor="middle" dominant-baseline="central">Send reset link via email</text> - </g> - - <!-- ═══ Section separator ═══ --> - <line x1="40" y1="452" x2="640" y2="452" - stroke="var(--border)" stroke-width="1" stroke-dasharray="8 5"/> - - <!-- Arrow crossing separator (with inline label) --> - <line x1="340" y1="432" x2="340" y2="472" class="arr" marker-end="url(#arrow)"/> - <text class="ts" x="348" y="448" text-anchor="start" opacity=".55">user receives email</text> - - <text class="ts" x="40" y="464" opacity=".45">Section 2 — Password reset form</text> - - <!-- ═══ SECTION 2 — Password reset form ═══ --> - - <!-- N6 · User clicks reset link --> - <g class="c-gray"> - <rect x="220" y="480" width="240" height="44" rx="8"/> - <text class="th" x="340" y="502" text-anchor="middle" dominant-baseline="central">User clicks reset link</text> - </g> - - <line x1="340" y1="524" x2="340" y2="544" class="arr" marker-end="url(#arrow)"/> - - <!-- N7 · Enter new password ×2 --> - <g class="c-gray"> - <rect x="220" y="544" width="240" height="56" rx="8"/> - <text class="th" x="340" y="565" text-anchor="middle" dominant-baseline="central">Enter new password ×2</text> - <text class="ts" x="340" y="583" text-anchor="middle" dominant-baseline="central">Confirm both passwords match</text> - </g> - - <line x1="340" y1="600" x2="340" y2="620" class="arr" marker-end="url(#arrow)"/> - - <!-- D2 · Token expired? diamond: center=(340,648) hw=100 hh=28 --> - <g class="decision"> - <polygon points="340,620 440,648 340,676 240,648"/> - <text class="th" x="340" y="648" text-anchor="middle" dominant-baseline="central">Token expired?</text> - </g> - - <!-- D2 "Yes" → right column (expired-token branch) --> - <path d="M 440,648 L 552,648 L 552,692" class="arr" marker-end="url(#arrow)"/> - <text class="ts" x="496" y="641" text-anchor="middle" opacity=".75">Yes</text> - - <!-- D2 "No" → down to password-match check --> - <line x1="340" y1="676" x2="340" y2="714" class="arr" marker-end="url(#arrow)"/> - <text class="ts" x="348" y="698" text-anchor="start" opacity=".75">No</text> - - <!-- ── Right branch (D2 = Yes): token expired → dead end ── --> - - <!-- R1 · Token expired error --> - <g class="c-red"> - <rect x="465" y="692" width="175" height="56" rx="8"/> - <text class="th" x="552" y="713" text-anchor="middle" dominant-baseline="central">Token expired</text> - <text class="ts" x="552" y="731" text-anchor="middle" dominant-baseline="central">Show expiry error</text> - </g> - - <line x1="552" y1="748" x2="552" y2="768" class="arr" marker-end="url(#arrow)"/> - - <!-- R2 · End terminal (right) --> - <g class="c-gray"> - <rect x="465" y="768" width="175" height="44" rx="22"/> - <text class="th" x="552" y="790" text-anchor="middle" dominant-baseline="central">End — request again</text> - </g> - - <!-- D3 · Passwords match? diamond: center=(340,742) hw=100 hh=28 --> - <g class="decision"> - <polygon points="340,714 440,742 340,770 240,742"/> - <text class="th" x="340" y="742" text-anchor="middle" dominant-baseline="central">Passwords match?</text> - </g> - - <!-- D3 "No" → left column (mismatch branch) --> - <path d="M 240,742 L 115,742 L 115,786" class="arr" marker-end="url(#arrow)"/> - <text class="ts" x="178" y="735" text-anchor="middle" opacity=".75">No</text> - - <!-- D3 "Yes" → down to reset --> - <line x1="340" y1="770" x2="340" y2="790" class="arr" marker-end="url(#arrow)"/> - <text class="ts" x="348" y="783" text-anchor="start" opacity=".75">Yes</text> - - <!-- ── Left branch (D3 = No): passwords don't match → loop back ── --> - - <!-- L3 · Password mismatch error --> - <g class="c-red"> - <rect x="20" y="786" width="190" height="56" rx="8"/> - <text class="th" x="115" y="807" text-anchor="middle" dominant-baseline="central">Password mismatch</text> - <text class="ts" x="115" y="825" text-anchor="middle" dominant-baseline="central">Passwords do not match</text> - </g> - - <!-- Loop-back arrow: exits L3 bottom → drops to y=862 → - travels right to corridor x=215 → climbs to N7 center y=572 → - enters N7 left edge at (220, 572) pointing right --> - <path d="M 115,842 L 115,862 L 215,862 L 215,572 L 220,572" - class="arr" marker-end="url(#arrow)"/> - <text class="ts" x="224" y="538" text-anchor="start" opacity=".6">retry</text> - - <!-- ── Center Yes branch (D3 = Yes): reset password & invalidate token ── --> - - <!-- N8 · Reset password --> - <g class="c-teal"> - <rect x="220" y="790" width="240" height="56" rx="8"/> - <text class="th" x="340" y="811" text-anchor="middle" dominant-baseline="central">Reset password</text> - <text class="ts" x="340" y="829" text-anchor="middle" dominant-baseline="central">Invalidate used token</text> - </g> - - <line x1="340" y1="846" x2="340" y2="866" class="arr" marker-end="url(#arrow)"/> - - <!-- N9 · Success terminal --> - <g class="c-green"> - <rect x="220" y="866" width="240" height="44" rx="22"/> - <text class="th" x="340" y="888" text-anchor="middle" dominant-baseline="central">Password reset complete</text> - </g> - - <!-- ═══ Legend ═══ --> - <text class="ts" x="40" y="930" opacity=".4">Legend —</text> - <rect x="108" y="920" width="13" height="13" rx="2" fill="#F1EFE8" stroke="#5F5E5A" stroke-width="0.5"/> - <text class="ts" x="126" y="930" opacity=".7">User action</text> - <rect x="210" y="920" width="13" height="13" rx="2" fill="#EEEDFE" stroke="#534AB7" stroke-width="0.5"/> - <text class="ts" x="228" y="930" opacity=".7">System process</text> - <rect x="334" y="920" width="13" height="13" rx="2" fill="#E1F5EE" stroke="#0F6E56" stroke-width="0.5"/> - <text class="ts" x="352" y="930" opacity=".7">Email / success</text> - <rect x="455" y="920" width="13" height="13" rx="2" fill="#FCEBEB" stroke="#A32D2D" stroke-width="0.5"/> - <text class="ts" x="473" y="930" opacity=".7">Error state</text> - <polygon points="556,926 566,932 556,938 546,932" fill="#FAEEDA" stroke="#854F0B" stroke-width="0.5"/> - <text class="ts" x="572" y="932" opacity=".7">Decision</text> - -</svg> -``` - -## Custom CSS - -Add these classes to the hosting page `<style>` block (in addition to the standard skill CSS): - -```css -/* Decision diamond — amber fill, same palette as c-amber */ -.decision > polygon { fill: #FAEEDA; stroke: #854F0B; stroke-width: 0.5; } -.decision > .th { fill: #633806; } - -@media (prefers-color-scheme: dark) { - .decision > polygon { fill: #633806; stroke: #EF9F27; } - .decision > .th { fill: #FAC775; } -} -``` - -## Color Assignments - -| Element | Color | Reason | -|---------|-------|--------| -| Start / end terminals | `c-gray` | Neutral entry and exit points | -| User actions (enter email, click link, enter password) | `c-gray` | User-facing steps with no system processing | -| Generic message + request-handled terminal | `c-gray` | Intentionally neutral — the security message must not reveal data | -| Generate & store token | `c-purple` | Backend system operations | -| Send reset email | `c-teal` | Positive external action (outbound communication) | -| Token expired error | `c-red` | Failure / blocking error state | -| Password mismatch error | `c-red` | Validation failure | -| Reset password + success | `c-teal` / `c-green` | Positive outcome: teal for the action, green pill for the terminal | -| Decision diamonds | `c-amber` (custom `.decision`) | Warning / branch point — matches amber semantic meaning | - -## Layout Notes - -- **ViewBox**: 680×960 — tall flowchart with two phases -- **Three-column structure**: Left (cx=115), center (cx=340), right (cx=552) — each branch stays within its column; only `<path>` arrows cross column boundaries -- **Diamond formula**: `<polygon points="cx,cy-hh cx+hw,cy cx,cy+hh cx-hw,cy"/>` with hw=100, hh=28 gives a 200×56px diamond that sits flush with the center column (x=220–460) -- **Branch routing pattern**: "No" paths use `<path d="M left_point,cy L side_cx,cy L side_cx,node_top">` — one horizontal segment + one vertical segment, no curves needed -- **Loop corridor**: The 5-px gap at x=210–220 between left and center columns provides a clean vertical channel for the loop-back path without any node overlap; the path exits node bottom, drops 20px, goes right to x=215, climbs to target y, enters from left -- **Section separator**: A dashed `<line>` at y=452 with `stroke-dasharray="8 5"` provides a visual phase break; the single connecting arrow crosses it at center, with a faded label on the arrow -- **Pill terminals**: `rx=22` (half the 44px node height) produces a perfect capsule/pill shape — use this consistently for all start/end terminals -- **Error annotation**: The exact UX copy is rendered as faded (`opacity=".45"`) italic `ts` text below the relevant node, keeping it informative without cluttering the flow diff --git a/optional-skills/creative/concept-diagrams/examples/autonomous-llm-research-agent-flow.md b/optional-skills/creative/concept-diagrams/examples/autonomous-llm-research-agent-flow.md deleted file mode 100644 index f0959f003a3..00000000000 --- a/optional-skills/creative/concept-diagrams/examples/autonomous-llm-research-agent-flow.md +++ /dev/null @@ -1,240 +0,0 @@ -# Autonomous LLM Research Agent Flow - -A multi-section flowchart showing Karpathy's autoresearch framework: human-agent handoff, the autonomous experiment loop with keep/discard decision branching, and the modifiable training pipeline. Demonstrates loop-back arrows, convergent decision paths, and semantic color coding for outcomes. - -## Key Patterns Used - -- **Three-section layout**: Setup row, main loop container, and detail container — each visually distinct -- **Neutral dashed containers**: Loop and training pipeline use `var(--bg-secondary)` fill with dashed borders to recede behind colored content nodes -- **Decision branching with convergence**: "val_bpb improved?" splits into Keep (green) and Discard (red), then both converge back to "Log to results.tsv" -- **Loop-back arrow**: Dashed path with rounded corners on the right side of the container showing infinite repetition -- **Semantic color for outcomes**: Green = improvement (keep), Red = no improvement (discard) — not arbitrary decoration -- **Highlighted key step**: "Run training" uses `c-coral` to visually distinguish the most important step from other `c-teal` actions -- **Horizontal pipeline flow**: Training details section uses left-to-right arrow-connected nodes (GPT → MuonAdamW → Evaluation) -- **Footer metadata**: Fixed constraints shown as subtle centered text below the pipeline nodes -- **Legend row**: Color key at the bottom explaining what each color means - -## Diagram - -```xml -<svg width="100%" viewBox="0 0 680 920" xmlns="http://www.w3.org/2000/svg"> - <defs> - <marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" - markerWidth="6" markerHeight="6" orient="auto-start-reverse"> - <path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" - stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> - </marker> - </defs> - - <!-- ========================================== --> - <!-- SECTION 1: SETUP (Human → program.md → AI) --> - <!-- ========================================== --> - - <text class="ts" x="40" y="30" text-anchor="start" opacity=".5">One-time setup</text> - - <!-- Human --> - <g class="node c-gray"> - <rect x="60" y="42" width="140" height="56" rx="8" stroke-width="0.5"/> - <text class="th" x="130" y="62" text-anchor="middle" dominant-baseline="central">Human</text> - <text class="ts" x="130" y="82" text-anchor="middle" dominant-baseline="central">Researcher</text> - </g> - - <!-- Arrow: Human → program.md --> - <line x1="200" y1="70" x2="250" y2="70" class="arr" marker-end="url(#arrow)"/> - - <!-- program.md --> - <g class="node c-gray"> - <rect x="250" y="42" width="180" height="56" rx="8" stroke-width="0.5"/> - <text class="th" x="340" y="62" text-anchor="middle" dominant-baseline="central">program.md</text> - <text class="ts" x="340" y="82" text-anchor="middle" dominant-baseline="central">Agent instructions</text> - </g> - - <!-- Arrow: program.md → AI Agent --> - <line x1="430" y1="70" x2="470" y2="70" class="arr" marker-end="url(#arrow)"/> - - <!-- AI Agent --> - <g class="node c-purple"> - <rect x="470" y="42" width="160" height="56" rx="8" stroke-width="0.5"/> - <text class="th" x="550" y="62" text-anchor="middle" dominant-baseline="central">AI agent</text> - <text class="ts" x="550" y="82" text-anchor="middle" dominant-baseline="central">Claude / Codex</text> - </g> - - <!-- Arrow: Setup row → Loop (from program.md center down) --> - <line x1="340" y1="98" x2="340" y2="142" class="arr" marker-end="url(#arrow)"/> - - <!-- ========================================== --> - <!-- SECTION 2: AUTONOMOUS EXPERIMENT LOOP --> - <!-- ========================================== --> - - <!-- Loop container (neutral dashed) --> - <g> - <rect x="40" y="142" width="600" height="528" rx="16" - stroke-width="1" stroke-dasharray="6 4" - fill="var(--bg-secondary)" stroke="var(--border)"/> - <text class="th" x="66" y="170">Autonomous experiment loop</text> - <text class="ts" x="66" y="188">~12 experiments/hour — runs until manually stopped</text> - </g> - - <!-- Step 1: Read code + past results --> - <g class="node c-teal"> - <rect x="170" y="208" width="280" height="44" rx="8" stroke-width="0.5"/> - <text class="th" x="310" y="230" text-anchor="middle" dominant-baseline="central">Read code + past results</text> - </g> - - <!-- Arrow: S1 → S2 --> - <line x1="310" y1="252" x2="310" y2="274" class="arr" marker-end="url(#arrow)"/> - - <!-- Step 2: Propose + edit train.py --> - <g class="node c-teal"> - <rect x="170" y="274" width="280" height="56" rx="8" stroke-width="0.5"/> - <text class="th" x="310" y="294" text-anchor="middle" dominant-baseline="central">Propose + edit train.py</text> - <text class="ts" x="310" y="314" text-anchor="middle" dominant-baseline="central">Arch, optimizer, hyperparameters</text> - </g> - - <!-- Arrow: S2 → S3 --> - <line x1="310" y1="330" x2="310" y2="352" class="arr" marker-end="url(#arrow)"/> - - <!-- Step 3: Run training (highlighted — key step) --> - <g class="node c-coral"> - <rect x="170" y="352" width="280" height="56" rx="8" stroke-width="0.5"/> - <text class="th" x="310" y="372" text-anchor="middle" dominant-baseline="central">Run training</text> - <text class="ts" x="310" y="392" text-anchor="middle" dominant-baseline="central">uv run train.py (5 min budget)</text> - </g> - - <!-- Arrow: S3 → S4 --> - <line x1="310" y1="408" x2="310" y2="430" class="arr" marker-end="url(#arrow)"/> - - <!-- Step 4: Decision — val_bpb improved? --> - <g class="node c-gray"> - <rect x="170" y="430" width="280" height="44" rx="8" stroke-width="0.5"/> - <text class="th" x="310" y="452" text-anchor="middle" dominant-baseline="central">val_bpb improved?</text> - </g> - - <!-- Decision arrows to Keep / Discard --> - <line x1="240" y1="474" x2="175" y2="508" class="arr" marker-end="url(#arrow)"/> - <line x1="380" y1="474" x2="445" y2="508" class="arr" marker-end="url(#arrow)"/> - - <!-- Decision labels --> - <text class="ts" x="195" y="496" opacity=".6">yes</text> - <text class="ts" x="416" y="496" opacity=".6">no</text> - - <!-- Keep — advance branch --> - <g class="node c-green"> - <rect x="70" y="508" width="210" height="56" rx="8" stroke-width="0.5"/> - <text class="th" x="175" y="528" text-anchor="middle" dominant-baseline="central">Keep</text> - <text class="ts" x="175" y="548" text-anchor="middle" dominant-baseline="central">Advance git branch</text> - </g> - - <!-- Discard — git reset --> - <g class="node c-red"> - <rect x="340" y="508" width="210" height="56" rx="8" stroke-width="0.5"/> - <text class="th" x="445" y="528" text-anchor="middle" dominant-baseline="central">Discard</text> - <text class="ts" x="445" y="548" text-anchor="middle" dominant-baseline="central">Git reset to previous</text> - </g> - - <!-- Converge arrows: Keep → Log, Discard → Log --> - <line x1="175" y1="564" x2="250" y2="590" class="arr" marker-end="url(#arrow)"/> - <line x1="445" y1="564" x2="370" y2="590" class="arr" marker-end="url(#arrow)"/> - - <!-- Step 6: Log to results.tsv --> - <g class="node c-teal"> - <rect x="170" y="590" width="280" height="44" rx="8" stroke-width="0.5"/> - <text class="th" x="310" y="612" text-anchor="middle" dominant-baseline="central">Log to results.tsv</text> - </g> - - <!-- Loop-back arrow (dashed, right side) --> - <path d="M 450 612 L 564 612 Q 576 612 576 600 L 576 242 Q 576 230 564 230 L 450 230" - fill="none" class="arr" stroke-dasharray="4 3" marker-end="url(#arrow)"/> - - <!-- ========================================== --> - <!-- SECTION 3: TRAINING PIPELINE DETAILS --> - <!-- ========================================== --> - - <!-- Connection arrow: Loop → Training details --> - <line x1="310" y1="670" x2="310" y2="710" class="arr" marker-end="url(#arrow)"/> - - <!-- Training container (neutral dashed) --> - <g> - <rect x="40" y="710" width="600" height="170" rx="16" - stroke-width="1" stroke-dasharray="6 4" - fill="var(--bg-secondary)" stroke="var(--border)"/> - <text class="th" x="66" y="738">train.py — modifiable training pipeline</text> - <text class="ts" x="66" y="756">Runs during each training step — single GPU, single file</text> - </g> - - <!-- GPT model --> - <g class="node c-coral"> - <rect x="70" y="774" width="155" height="56" rx="8" stroke-width="0.5"/> - <text class="th" x="147" y="794" text-anchor="middle" dominant-baseline="central">GPT model</text> - <text class="ts" x="147" y="814" text-anchor="middle" dominant-baseline="central">RoPE, FlashAttn3</text> - </g> - - <!-- Arrow: GPT → MuonAdamW --> - <line x1="225" y1="802" x2="260" y2="802" class="arr" marker-end="url(#arrow)"/> - - <!-- MuonAdamW optimizer --> - <g class="node c-coral"> - <rect x="260" y="774" width="155" height="56" rx="8" stroke-width="0.5"/> - <text class="th" x="337" y="794" text-anchor="middle" dominant-baseline="central">MuonAdamW</text> - <text class="ts" x="337" y="814" text-anchor="middle" dominant-baseline="central">Hybrid optimizer</text> - </g> - - <!-- Arrow: MuonAdamW → Evaluation --> - <line x1="415" y1="802" x2="450" y2="802" class="arr" marker-end="url(#arrow)"/> - - <!-- Evaluation --> - <g class="node c-amber"> - <rect x="450" y="774" width="155" height="56" rx="8" stroke-width="0.5"/> - <text class="th" x="527" y="794" text-anchor="middle" dominant-baseline="central">Evaluation</text> - <text class="ts" x="527" y="814" text-anchor="middle" dominant-baseline="central">val_bpb metric</text> - </g> - - <!-- Footer: fixed constraints --> - <text class="ts" x="340" y="856" text-anchor="middle" opacity=".5">climbmix-400b data · 8K BPE vocab · 300s budget · 2048 context</text> - - <!-- ========================================== --> - <!-- LEGEND --> - <!-- ========================================== --> - - <g class="c-teal"><rect x="40" y="890" width="14" height="14" rx="3" stroke-width="0.5"/></g> - <text class="ts" x="62" y="902">Agent actions</text> - - <g class="c-coral"><rect x="170" y="890" width="14" height="14" rx="3" stroke-width="0.5"/></g> - <text class="ts" x="192" y="902">Training run</text> - - <g class="c-green"><rect x="300" y="890" width="14" height="14" rx="3" stroke-width="0.5"/></g> - <text class="ts" x="322" y="902">Improvement</text> - - <g class="c-red"><rect x="430" y="890" width="14" height="14" rx="3" stroke-width="0.5"/></g> - <text class="ts" x="452" y="902">No improvement</text> - -</svg> -``` - -## Color Assignments - -| Element | Color | Reason | -|---------|-------|--------| -| Human, program.md | `c-gray` | Neutral setup / input nodes | -| AI agent | `c-purple` | The active intelligent actor | -| Loop action steps | `c-teal` | Agent's analytical/editing actions | -| Run training | `c-coral` | Highlighted key step — the 5-min training run | -| Decision check | `c-gray` | Neutral evaluation checkpoint | -| Keep (improved) | `c-green` | Semantic success — val_bpb decreased | -| Discard (not improved) | `c-red` | Semantic failure — no improvement | -| Training pipeline nodes | `c-coral` | Training infrastructure components | -| Evaluation node | `c-amber` | Distinct from training — measurement/metric role | -| Containers | Neutral (dashed) | Subtle grouping that recedes behind content | - -## Layout Notes - -- **ViewBox**: 680×920 (standard width, tall for 3 sections) -- **Three sections**: Setup row (y=30–98), loop container (y=142–670), training details (y=710–880) -- **Container style**: Dashed border (`stroke-dasharray="6 4"`), neutral fill (`var(--bg-secondary)`), `stroke-width="1"` — not colored, so inner nodes pop -- **Loop-back arrow**: Dashed `<path>` with quadratic curves (`Q`) at corners for smooth rounded turns, running up the right side of the loop container from "Log" back to "Read code" -- **Decision pattern**: Single question node ("val_bpb improved?") with diagonal arrows to Keep/Discard, then convergent diagonal arrows back to "Log to results.tsv" -- **Decision labels**: "yes"/"no" labels placed along the diagonal arrows with `opacity=".6"` to stay subtle -- **Key step highlight**: "Run training" uses `c-coral` while surrounding steps use `c-teal`, drawing the eye to the most important step -- **Horizontal sub-flow**: Training pipeline uses left-to-right arrow-connected nodes (GPT model → MuonAdamW → Evaluation) -- **Footer metadata**: Fixed constraints (data, vocab, budget, context) shown as a single centered `ts` text line with `opacity=".5"` -- **Legend**: Four color swatches at the bottom explaining the semantic meaning of each color used diff --git a/optional-skills/creative/concept-diagrams/examples/banana-journey-tree-to-smoothie.md b/optional-skills/creative/concept-diagrams/examples/banana-journey-tree-to-smoothie.md deleted file mode 100644 index d4fe3bea159..00000000000 --- a/optional-skills/creative/concept-diagrams/examples/banana-journey-tree-to-smoothie.md +++ /dev/null @@ -1,161 +0,0 @@ -# Journey of a Banana: From Tree to Smoothie - -A narrative journey diagram following a single banana across 3,000 miles and 3 weeks, from harvest in Costa Rica to a smoothie in the consumer's kitchen. Demonstrates storytelling through visualization, winding path layout, and progressive state changes. - -## Key Patterns Used - -- **Winding journey path**: S-curve connecting all stages visually -- **Location markers**: Country flags and place names for geographic context -- **Progressive state changes**: Banana color changes (green → yellow → brown → frozen → smoothie) -- **Narrative details**: Fun elements like spider check, stickers, price tags -- **Timeline**: Bottom timeline showing duration of journey -- **Environmental context**: Ocean waves, gas clouds, store awning - -## New Shape Techniques - -### Banana (curved fruit shape) -```xml -<!-- Green banana --> -<path class="banana-green" d="M 5 0 Q 0 10 3 20 Q 6 25 10 20 Q 13 10 8 0 Z"/> - -<!-- Yellow banana --> -<path class="banana-yellow" d="M 0 5 Q -6 18 0 32 Q 7 40 15 30 Q 20 15 12 5 Z"/> - -<!-- Brown overripe banana with spots --> -<path class="banana-brown" d="M 0 5 Q -5 15 0 28 Q 6 35 14 26 Q 18 14 12 5 Z"/> -<circle class="banana-spots" cx="5" cy="15" r="1.5"/> -<circle class="banana-spots" cx="9" cy="20" r="1"/> -``` - -### Banana Tree -```xml -<!-- Trunk --> -<rect class="tree-trunk" x="55" y="50" width="15" height="60" rx="3"/> -<!-- Leaves (rotated ellipses) --> -<ellipse class="tree-leaf" cx="62" cy="45" rx="40" ry="15" transform="rotate(-20, 62, 45)"/> -<ellipse class="tree-leaf" cx="62" cy="50" rx="35" ry="12" transform="rotate(25, 62, 50)"/> -<!-- Banana bunch hanging --> -<g transform="translate(40, 55)"> - <path class="banana-green" d="M 5 0 Q 0 10 3 20 Q 6 25 10 20 Q 13 10 8 0 Z"/> - <path class="banana-green" d="M 12 2 Q 8 12 11 22 Q 14 27 18 22 Q 21 12 16 2 Z"/> - <rect class="stem" x="8" y="-5" width="12" height="8" rx="2"/> -</g> -``` - -### Cargo Ship -```xml -<!-- Ocean waves --> -<path class="ocean" d="M 0 90 Q 30 85 60 90 Q 90 95 120 90 Q 150 85 180 90 L 180 110 L 0 110 Z" opacity="0.5"/> -<!-- Hull --> -<path class="ship-hull" d="M 20 90 L 30 60 L 160 60 L 170 90 Q 150 95 95 95 Q 40 95 20 90 Z"/> -<!-- Deck --> -<rect class="ship-deck" x="40" y="45" width="110" height="18" rx="2"/> -<!-- Reefer containers --> -<rect class="container" x="45" y="25" width="30" height="22" rx="2"/> -<!-- Refrigeration symbol --> -<text x="60" y="40" text-anchor="middle" fill="#185FA5" style="font-size:10px">❄</text> -<!-- Smoke stack --> -<rect x="145" y="35" width="8" height="15" fill="#444441"/> -``` - -### Inspector Figure -```xml -<!-- Body --> -<rect class="inspector" x="10" y="20" width="25" height="35" rx="3"/> -<!-- Head --> -<circle class="inspector" cx="22" cy="12" r="10"/> -<!-- Hat --> -<rect x="12" y="2" width="20" height="6" rx="2" fill="#534AB7"/> -<!-- Clipboard --> -<rect class="clipboard" x="38" y="28" width="15" height="20" rx="2"/> -<line x1="42" y1="34" x2="50" y2="34" stroke="#888780" stroke-width="1"/> -``` - -### Spider with "No" Symbol -```xml -<circle cx="15" cy="15" r="18" fill="none" stroke="#A32D2D" stroke-width="2"/> -<line x1="3" y1="3" x2="27" y2="27" stroke="#A32D2D" stroke-width="2"/> -<!-- Spider body --> -<ellipse class="spider" cx="15" cy="15" rx="4" ry="5"/> -<ellipse class="spider" cx="15" cy="10" rx="3" ry="3"/> -<!-- Legs --> -<line x1="12" y1="14" x2="5" y2="10" stroke="#2C2C2A" stroke-width="1"/> -<line x1="18" y1="14" x2="25" y2="10" stroke="#2C2C2A" stroke-width="1"/> -``` - -### Blender with Smoothie -```xml -<!-- Blender jar --> -<path class="blender" d="M 5 5 L 0 45 L 35 45 L 30 5 Z"/> -<!-- Smoothie inside (wavy top) --> -<path class="smoothie" d="M 3 20 L 0 45 L 35 45 L 32 20 Q 25 18 17 22 Q 10 18 3 20 Z"/> -<!-- Blender base --> -<rect class="blender" x="-2" y="45" width="40" height="12" rx="3"/> -<!-- Lid --> -<rect x="8" y="0" width="20" height="8" rx="2" fill="#AFA9EC" stroke="#534AB7"/> -<!-- Banana chunks floating --> -<ellipse cx="12" cy="32" rx="4" ry="2" fill="#FAC775"/> -``` - -### Winding Journey Path -```xml -<path class="journey-path" d=" - M 80 100 - L 200 100 - Q 280 100 280 150 - L 280 180 - Q 280 220 320 220 - L 520 220 - Q 560 220 560 260 - L 560 320 - Q 560 360 520 360 - L 280 360 - ... -"/> -``` - -## CSS Classes - -```css -/* Journey */ -.journey-path { stroke: #D3D1C7; stroke-width: 3; fill: none; stroke-linecap: round; } - -/* Banana ripeness stages */ -.banana-green { fill: #97C459; stroke: #3B6D11; stroke-width: 0.5; } -.banana-yellow { fill: #FAC775; stroke: #BA7517; stroke-width: 0.5; } -.banana-brown { fill: #854F0B; stroke: #633806; stroke-width: 0.5; } -.banana-spots { fill: #633806; } - -/* Environment elements */ -.tree-trunk { fill: #854F0B; stroke: #633806; stroke-width: 1; } -.tree-leaf { fill: #97C459; stroke: #3B6D11; stroke-width: 0.5; } -.ocean { fill: #85B7EB; } -.ship-hull { fill: #5F5E5A; stroke: #444441; stroke-width: 1; } -.container { fill: #E6F1FB; stroke: #185FA5; stroke-width: 1; } -.gas-cloud { fill: #C0DD97; stroke: #97C459; stroke-width: 0.5; opacity: 0.6; } - -/* Buildings */ -.packhouse { fill: #F1EFE8; stroke: #5F5E5A; stroke-width: 1; } -.warehouse { fill: #FAEEDA; stroke: #854F0B; stroke-width: 1; } -.store { fill: #E1F5EE; stroke: #0F6E56; stroke-width: 1; } - -/* Kitchen */ -.counter { fill: #FAECE7; stroke: #993C1D; stroke-width: 1; } -.blender { fill: #EEEDFE; stroke: #534AB7; stroke-width: 1; } -.smoothie { fill: #FAC775; } -.freezer { fill: #E6F1FB; stroke: #185FA5; stroke-width: 1; } - -/* Details */ -.sticker { fill: #378ADD; stroke: #185FA5; stroke-width: 0.3; } -.spider { fill: #2C2C2A; stroke: #1a1a18; stroke-width: 0.3; } -``` - -## Layout Notes - -- **ViewBox**: 850×680 (tall for winding path) -- **Path style**: S-curve winding path connects all 7 stages -- **Location labels**: Country flags + place names anchor geographic context -- **State progression**: Same object (banana) shown in different states throughout -- **Timeline**: Horizontal timeline at bottom shows journey duration -- **Narrative elements**: Fun details (spider, stickers, price tags) add storytelling value -- **Environmental context**: Ocean waves, gas clouds, awnings create sense of place diff --git a/optional-skills/creative/concept-diagrams/examples/commercial-aircraft-structure.md b/optional-skills/creative/concept-diagrams/examples/commercial-aircraft-structure.md deleted file mode 100644 index 0e02944d737..00000000000 --- a/optional-skills/creative/concept-diagrams/examples/commercial-aircraft-structure.md +++ /dev/null @@ -1,209 +0,0 @@ -# Commercial Aircraft Structure - -A physical/structural diagram showing an aircraft side profile using appropriate SVG shapes beyond rectangles - paths, polygons, ellipses for realistic representation. - -## Key Patterns Used - -- **Path elements**: Curved fuselage body with nose cone using quadratic bezier curves -- **Polygon elements**: Tapered wing shape, triangular stabilizers, control surfaces -- **Ellipse elements**: Engines (cylinders), wheels (circles) -- **Line elements**: Landing gear struts, leader lines for labels -- **Dashed strokes**: Interior sections (fuel tank), movable control surfaces (rudder, elevator) -- **Layered composition**: Cabin sections drawn inside the fuselage shape -- **Leader lines with labels**: Connect labels to components they describe - -## Diagram - -```xml -<svg width="100%" viewBox="0 0 680 400" xmlns="http://www.w3.org/2000/svg"> - - <!-- FUSELAGE - main body cylinder with nose cone --> - <path class="fuselage" d=" - M 80 180 - Q 40 180 40 200 - Q 40 220 80 220 - L 560 220 - Q 580 220 580 200 - Q 580 180 560 180 - Z - "/> - - <!-- Nose cone --> - <path class="fuselage" d=" - M 80 180 - Q 50 180 35 200 - Q 50 220 80 220 - " fill="none" stroke-width="1"/> - - <!-- COCKPIT windows --> - <path class="cockpit" d=" - M 45 190 - L 75 185 - L 75 200 - L 50 200 - Z - "/> - <line x1="55" y1="188" x2="55" y2="200" stroke="#534AB7" stroke-width="0.5"/> - <line x1="65" y1="186" x2="65" y2="200" stroke="#534AB7" stroke-width="0.5"/> - - <!-- CABIN SECTIONS (inside fuselage) --> - <!-- First class --> - <rect class="first-class" x="85" y="183" width="50" height="34" rx="2"/> - <text class="tl" x="110" y="203" text-anchor="middle">First</text> - - <!-- Business class --> - <rect class="business-class" x="140" y="183" width="80" height="34" rx="2"/> - <text class="tl" x="180" y="203" text-anchor="middle">Business</text> - - <!-- Economy class --> - <rect class="economy-class" x="225" y="183" width="200" height="34" rx="2"/> - <text class="tl" x="325" y="203" text-anchor="middle">Economy</text> - - <!-- CARGO HOLD (lower section indication) --> - <line x1="85" y1="217" x2="520" y2="217" class="leader"/> - <text class="tl" x="300" y="228" text-anchor="middle" opacity=".6">Cargo hold below deck</text> - - <!-- WING - main wing shape --> - <polygon class="wing" points=" - 200,220 - 120,300 - 130,305 - 160,305 - 340,235 - 340,220 - "/> - - <!-- Wing fuel tank (dashed interior) --> - <polygon class="fuel-tank" points=" - 210,225 - 150,280 - 160,283 - 180,283 - 310,232 - 310,225 - "/> - <text class="tl" x="220" y="260" opacity=".7">Fuel</text> - - <!-- Flaps (trailing edge) --> - <polygon class="flap" points=" - 130,300 - 120,305 - 160,310 - 165,305 - "/> - <text class="tl" x="143" y="320">Flaps</text> - - <!-- ENGINE under wing --> - <ellipse class="engine" cx="175" cy="285" rx="25" ry="12"/> - <ellipse cx="155" cy="285" rx="8" ry="10" fill="none" stroke="#993C1D" stroke-width="0.5"/> - <!-- Engine pylon --> - <line x1="175" y1="273" x2="190" y2="245" stroke="#5F5E5A" stroke-width="2"/> - <text class="tl" x="175" y="308" text-anchor="middle">Engine</text> - - <!-- TAIL SECTION --> - <!-- Vertical stabilizer --> - <polygon class="tail-v" points=" - 520,180 - 560,100 - 580,100 - 580,180 - "/> - <text class="tl" x="565" y="150" text-anchor="middle">Vertical</text> - <text class="tl" x="565" y="162" text-anchor="middle">stabilizer</text> - - <!-- Rudder --> - <polygon points="575,105 590,105 590,178 580,178" fill="none" stroke="#185FA5" stroke-width="0.5" stroke-dasharray="3 2"/> - <text class="tl" x="595" y="145" opacity=".6">Rudder</text> - - <!-- Horizontal stabilizer --> - <polygon class="tail-h" points=" - 500,195 - 460,175 - 465,170 - 580,170 - 580,180 - 520,195 - "/> - <text class="tl" x="510" y="166">Horizontal stabilizer</text> - - <!-- Elevator --> - <polygon points="462,174 450,168 455,163 467,169" fill="none" stroke="#185FA5" stroke-width="0.5" stroke-dasharray="3 2"/> - <text class="tl" x="440" y="158" opacity=".6">Elevator</text> - - <!-- LANDING GEAR --> - <!-- Nose gear --> - <line class="gear" x1="100" y1="220" x2="100" y2="260" stroke-width="3"/> - <ellipse class="wheel" cx="100" cy="268" rx="8" ry="10"/> - <text class="tl" x="100" y="290" text-anchor="middle">Nose gear</text> - - <!-- Main gear (under wing/fuselage junction) --> - <line class="gear" x1="280" y1="220" x2="280" y2="270" stroke-width="4"/> - <line class="gear" x1="268" y1="265" x2="292" y2="265" stroke-width="3"/> - <ellipse class="wheel" cx="268" cy="278" rx="10" ry="12"/> - <ellipse class="wheel" cx="292" cy="278" rx="10" ry="12"/> - <text class="tl" x="280" y="302" text-anchor="middle">Main gear</text> - - <!-- LABELS with leader lines --> - <!-- Cockpit label --> - <line class="leader" x1="60" y1="175" x2="60" y2="140"/> - <text class="ts" x="60" y="132" text-anchor="middle">Cockpit</text> - - <!-- Wing label --> - <line class="leader" x1="250" y1="250" x2="290" y2="330"/> - <text class="ts" x="290" y="345" text-anchor="middle">Wing structure</text> - <text class="tl" x="290" y="358" text-anchor="middle">Spars, ribs, skin</text> - - <!-- Fuselage label --> - <line class="leader" x1="400" y1="180" x2="400" y2="140"/> - <text class="ts" x="400" y="132" text-anchor="middle">Fuselage</text> - <text class="tl" x="400" y="145" text-anchor="middle">Pressure vessel</text> - -</svg> -``` - -## CSS Classes for Physical Diagrams - -When creating physical/structural diagrams, define semantic classes for each component type: - -```css -/* Structure shapes */ -.fuselage { fill: #F1EFE8; stroke: #5F5E5A; stroke-width: 1; } -.wing { fill: #E6F1FB; stroke: #185FA5; stroke-width: 1; } -.tail-v { fill: #E6F1FB; stroke: #185FA5; stroke-width: 1; } -.tail-h { fill: #E6F1FB; stroke: #185FA5; stroke-width: 1; } - -/* Interior sections */ -.cockpit { fill: #EEEDFE; stroke: #534AB7; stroke-width: 1; } -.first-class { fill: #FBEAF0; stroke: #993556; stroke-width: 0.5; } -.business-class { fill: #FAECE7; stroke: #993C1D; stroke-width: 0.5; } -.economy-class { fill: #E1F5EE; stroke: #0F6E56; stroke-width: 0.5; } -.cargo { fill: #D3D1C7; stroke: #5F5E5A; stroke-width: 0.5; } - -/* Systems */ -.engine { fill: #FAECE7; stroke: #993C1D; stroke-width: 1; } -.fuel-tank { fill: #FAEEDA; stroke: #854F0B; stroke-width: 0.5; stroke-dasharray: 3 2; } -.flap { fill: #E1F5EE; stroke: #0F6E56; stroke-width: 0.5; } - -/* Mechanical */ -.gear { fill: #444441; stroke: #2C2C2A; stroke-width: 0.5; } -.wheel { fill: #2C2C2A; stroke: #1a1a18; stroke-width: 0.5; } -``` - -## Shape Selection Guide - -| Physical form | SVG element | Example | -|---------------|-------------|---------| -| Curved body | `<path>` with Q (quadratic) or C (cubic) curves | Fuselage, nose cone | -| Tapered/angular | `<polygon>` | Wings, stabilizers | -| Cylindrical | `<ellipse>` | Engines, wheels, tanks | -| Linear structure | `<line>` | Struts, pylons, gear legs | -| Internal sections | `<rect>` inside parent shape | Cabin classes | -| Dashed boundaries | `stroke-dasharray` on any shape | Fuel tanks, control surfaces | - -## Layout Notes - -- **ViewBox**: 680×400 (wider aspect ratio suits side profile) -- **Layering**: Draw outer structures first, then interior details on top -- **Leader lines**: Use `.leader` class (dashed) to connect labels to components -- **Text sizes**: Use `.tl` (10px) for component labels, `.ts` (12px) for section labels -- **Semantic colors**: Group by system (structure=blue, propulsion=coral, fuel=amber, etc.) diff --git a/optional-skills/creative/concept-diagrams/examples/cpu-ooo-microarchitecture.md b/optional-skills/creative/concept-diagrams/examples/cpu-ooo-microarchitecture.md deleted file mode 100644 index 10258129716..00000000000 --- a/optional-skills/creative/concept-diagrams/examples/cpu-ooo-microarchitecture.md +++ /dev/null @@ -1,236 +0,0 @@ -# Out-of-Order CPU Core Microarchitecture - -A structural diagram showing the internal pipeline stages of a modern superscalar out-of-order CPU core. Demonstrates multi-stage vertical flow with parallel paths, fan-out patterns for execution ports, and a separate memory hierarchy sidebar. - -## Key Patterns Used - -- **Multi-stage vertical flow**: Six pipeline stages (Front End → Rename → Schedule → Execute → Retire) -- **Parallel decode paths**: Main decode and µop cache bypass (dashed line for cache hit) -- **Container grouping**: Logical stages grouped in colored containers -- **Fan-out pattern**: Single scheduler dispatching to 6 execution ports -- **Sidebar layout**: Memory hierarchy placed in separate column on right -- **Stage labels**: Left-aligned labels indicating pipeline phase -- **Color-coded semantics**: Different colors for each functional unit category - -## Diagram Type - -This is a **hybrid structural/flow** diagram: -- **Flow aspect**: Instructions move top-to-bottom through pipeline stages -- **Structural aspect**: Components are grouped by function (rename unit, execution cluster) -- **Sidebar**: Memory hierarchy is architecturally separate but connected via data paths - -## Pipeline Stage Breakdown - -### Front End (Purple) -```xml -<!-- Fetch Unit --> -<g class="node c-purple"> - <rect x="40" y="70" width="140" height="56" rx="8" stroke-width="0.5"/> - <text class="th" x="110" y="90" text-anchor="middle" dominant-baseline="central">Fetch unit</text> - <text class="ts" x="110" y="110" text-anchor="middle" dominant-baseline="central">6-wide, 32B/cycle</text> -</g> - -<!-- Branch Predictor (subordinate) --> -<g class="node c-purple"> - <rect x="40" y="140" width="140" height="44" rx="8" stroke-width="0.5"/> - <text class="th" x="110" y="162" text-anchor="middle" dominant-baseline="central">Branch predictor</text> -</g> - -<!-- Decode --> -<g class="node c-purple"> - <rect x="230" y="70" width="160" height="56" rx="8" stroke-width="0.5"/> - <text class="th" x="310" y="90" text-anchor="middle" dominant-baseline="central">Decode</text> - <text class="ts" x="310" y="110" text-anchor="middle" dominant-baseline="central">x86 → µops, 6-wide</text> -</g> -``` - -### µop Cache Bypass Path (Teal) -The µop cache (Decoded Stream Buffer) provides an alternate path that bypasses the complex decoder: - -```xml -<!-- µop Cache parallel to decode --> -<g class="node c-teal"> - <rect x="230" y="150" width="160" height="50" rx="8" stroke-width="0.5"/> - <text class="th" x="310" y="168" text-anchor="middle" dominant-baseline="central">µop cache (DSB)</text> - <text class="ts" x="310" y="186" text-anchor="middle" dominant-baseline="central">4K entries, 8-wide</text> -</g> - -<!-- Dashed bypass path indicating cache hit --> -<path d="M180 110 L205 110 L205 175 L230 175" fill="none" class="arr" - stroke-dasharray="4 3" marker-end="url(#arrow)"/> -<text class="tx" x="164" y="148" opacity=".6">hit</text> -``` - -### Rename/Allocate Container (Coral) -Groups related rename components in a container: - -```xml -<!-- Outer container --> -<g class="c-coral"> - <rect x="40" y="250" width="530" height="130" rx="12" stroke-width="0.5"/> - <text class="th" x="60" y="274">Rename / allocate</text> - <text class="ts" x="60" y="292">Map architectural → physical registers</text> -</g> - -<!-- Inner components --> -<g class="node c-coral"> - <rect x="60" y="310" width="180" height="56" rx="8" stroke-width="0.5"/> - <text class="th" x="150" y="330" text-anchor="middle" dominant-baseline="central">Register alias table</text> - <text class="ts" x="150" y="350" text-anchor="middle" dominant-baseline="central">180 physical regs</text> -</g> -``` - -### Scheduler Fan-Out Pattern (Amber → Teal) -Single unified scheduler dispatching to multiple execution ports: - -```xml -<!-- Unified Scheduler --> -<g class="node c-amber"> - <rect x="140" y="420" width="330" height="50" rx="8" stroke-width="0.5"/> - <text class="th" x="305" y="438" text-anchor="middle" dominant-baseline="central">Unified scheduler</text> - <text class="ts" x="305" y="456" text-anchor="middle" dominant-baseline="central">97 entries, out-of-order dispatch</text> -</g> - -<!-- Fan-out arrows to 6 ports --> -<line x1="170" y1="470" x2="90" y2="540" class="arr" marker-end="url(#arrow)"/> -<line x1="215" y1="470" x2="170" y2="540" class="arr" marker-end="url(#arrow)"/> -<line x1="265" y1="470" x2="250" y2="540" class="arr" marker-end="url(#arrow)"/> -<line x1="305" y1="470" x2="330" y2="540" class="arr" marker-end="url(#arrow)"/> -<line x1="355" y1="470" x2="410" y2="540" class="arr" marker-end="url(#arrow)"/> -<line x1="420" y1="470" x2="490" y2="540" class="arr" marker-end="url(#arrow)"/> -``` - -### Execution Port Box Pattern -Compact boxes showing port number and capabilities: - -```xml -<!-- Execution port with multi-line capability --> -<g class="node c-teal"> - <rect x="55" y="540" width="70" height="64" rx="6" stroke-width="0.5"/> - <text class="th" x="90" y="560" text-anchor="middle" dominant-baseline="central">Port 0</text> - <text class="tx" x="90" y="576" text-anchor="middle" dominant-baseline="central">ALU</text> - <text class="tx" x="90" y="590" text-anchor="middle" dominant-baseline="central">DIV</text> -</g> -``` - -### Reorder Buffer (Pink) -Wide horizontal bar at bottom showing retirement: - -```xml -<g class="c-pink"> - <rect x="40" y="670" width="530" height="40" rx="10" stroke-width="0.5"/> - <text class="th" x="305" y="694" text-anchor="middle" dominant-baseline="central">Reorder buffer (ROB) — 512 entries, 8-wide retire</text> -</g> -``` - -### Memory Hierarchy Sidebar (Blue) -Separate column showing cache levels: - -```xml -<!-- Container --> -<g class="c-blue"> - <rect x="600" y="30" width="190" height="360" rx="16" stroke-width="0.5"/> - <text class="th" x="695" y="54" text-anchor="middle">Memory hierarchy</text> -</g> - -<!-- Cache levels stacked vertically --> -<g class="node c-blue"> - <rect x="620" y="70" width="150" height="50" rx="8" stroke-width="0.5"/> - <text class="th" x="695" y="88" text-anchor="middle" dominant-baseline="central">L1-I cache</text> - <text class="ts" x="695" y="106" text-anchor="middle" dominant-baseline="central">32 KB, 8-way</text> -</g> -<!-- Additional levels follow same pattern --> -``` - -## Connection Patterns - -### Instruction Fetch Path -Horizontal arrow from L1-I cache to fetch unit: -```xml -<path d="M620 95 L200 95" fill="none" class="arr" marker-end="url(#arrow)"/> -<text class="tx" x="410" y="88" text-anchor="middle" opacity=".6">instruction fetch</text> -``` - -### Load/Store Path -Complex path from execution ports to L1-D cache: -```xml -<path d="M250 604 L250 640 L580 640 L580 160 L620 160" fill="none" class="arr" marker-end="url(#arrow)"/> -<text class="tx" x="415" y="652" text-anchor="middle" opacity=".6">load / store</text> -``` - -### Commit Path (dashed) -Dashed line showing write-back from ROB to register file: -```xml -<path d="M550 690 L580 690 L580 445 L595 445" fill="none" class="arr" stroke-dasharray="4 3"/> -<text class="tx" x="590" y="578" opacity=".6" transform="rotate(-90 590 578)">commit</text> -``` - -### Path Merge (Decode + µop Cache) -Two paths converging before rename: -```xml -<line x1="390" y1="98" x2="430" y2="98" class="arr"/> -<line x1="390" y1="175" x2="430" y2="175" class="arr"/> -<path d="M430 98 L430 175" fill="none" stroke="var(--text-secondary)" stroke-width="1.5"/> -<line x1="430" y1="136" x2="470" y2="136" class="arr" marker-end="url(#arrow)"/> -``` - -## Text Classes - -This diagram uses an additional text class for very small labels: - -```css -.tx { font-family: system-ui, -apple-system, sans-serif; font-size: 10px; fill: var(--text-secondary); } -``` - -Used for: -- Execution port capability labels (ALU, Branch, Load, etc.) -- Connection labels (instruction fetch, load/store, commit) -- DRAM latency annotation - -## Color Semantic Mapping - -| Color | Stage | Components | -|-------|-------|------------| -| `c-purple` | Front end | Fetch, Branch predictor, Decode | -| `c-teal` | Execution | µop cache, Execution ports | -| `c-coral` | Rename | RAT, Physical RF, Free list | -| `c-amber` | Schedule | Unified scheduler | -| `c-pink` | Retire | Reorder buffer | -| `c-blue` | Memory | L1-I, L1-D, L2, DRAM | -| `c-gray` | External | Off-chip DRAM | - -## Layout Notes - -- **ViewBox**: 820×720 (taller than wide for vertical pipeline flow) -- **Main pipeline**: x=40 to x=570 (530px width) -- **Memory sidebar**: x=600 to x=790 (190px width) -- **Stage labels**: x=30, left-aligned, 50% opacity -- **Vertical spacing**: ~80-100px between major stages -- **Container padding**: 20px inside containers -- **Port spacing**: 80px between execution port centers -- **Legend**: Bottom-right of memory sidebar, explains color coding - -## Architectural Details Shown - -| Component | Specification | Notes | -|-----------|---------------|-------| -| Fetch | 6-wide, 32B/cycle | Typical modern Intel/AMD | -| Decode | 6-wide, x86→µops | Complex decoder | -| µop Cache | 4K entries, 8-wide | Bypass for hot code | -| RAT | 180 physical regs | Supports deep OoO | -| Scheduler | 97 entries | Unified RS | -| Execution | 6 ports | ALU×2, Load, Store×2, Vector | -| ROB | 512 entries, 8-wide | In-order retirement | -| L1-I | 32 KB, 8-way | Instruction cache | -| L1-D | 48 KB, 12-way | Data cache | -| L2 | 1.25 MB, 20-way | Unified | -| DRAM | DDR5-6400, ~80ns | Off-chip | - -## When to Use This Pattern - -Use this diagram style for: -- CPU/GPU microarchitecture visualization -- Compiler pipeline stages -- Network packet processing pipelines -- Any system with parallel execution units fed by a scheduler -- Hardware designs with multiple functional units diff --git a/optional-skills/creative/concept-diagrams/examples/electricity-grid-flow.md b/optional-skills/creative/concept-diagrams/examples/electricity-grid-flow.md deleted file mode 100644 index 9b6acc66db1..00000000000 --- a/optional-skills/creative/concept-diagrams/examples/electricity-grid-flow.md +++ /dev/null @@ -1,182 +0,0 @@ -# Electricity Grid: Generation to Consumption - -A left-to-right flow diagram showing electricity from multiple generation sources through transmission and distribution networks to end consumers. Demonstrates multi-stage flow layout, voltage level visual hierarchy, and smart grid data overlay. - -## Key Patterns Used - -- **Multi-stage horizontal flow**: Four distinct columns (Generation → Transmission → Distribution → Consumption) -- **Stage dividers**: Vertical dashed lines separating each phase -- **Voltage level hierarchy**: Different line weights/colors for HV, MV, LV -- **Smart grid data overlay**: Dashed data flow lines from control center -- **Capacity labels**: Power ratings on generation sources -- **Multiple source convergence**: Four generators feeding into single transmission grid - -## New Shape Techniques - -### Nuclear Plant (cooling tower + reactor) -```xml -<!-- Cooling tower (hyperbolic curve) --> -<path class="nuclear-tower" d="M 25 80 Q 15 60 20 40 Q 25 20 40 15 Q 55 20 60 40 Q 65 60 55 80 Z"/> -<!-- Steam clouds --> -<ellipse class="nuclear-steam" cx="40" cy="8" rx="12" ry="6"/> -<!-- Reactor dome --> -<rect class="nuclear-building" x="65" y="45" width="40" height="35" rx="3"/> -<ellipse class="nuclear-building" cx="85" cy="45" rx="20" ry="8"/> -``` - -### Gas Peaker Plant (with flames) -```xml -<rect class="gas-plant" x="0" y="25" width="70" height="40" rx="3"/> -<!-- Smokestacks --> -<rect class="gas-stack" x="15" y="5" width="8" height="25" rx="1"/> -<!-- Flame --> -<path class="gas-flame" d="M 19 5 Q 17 0 19 -3 Q 21 0 19 5"/> -<!-- Turbine housing --> -<ellipse class="gas-plant" cx="55" cy="45" rx="12" ry="8"/> -``` - -### Transmission Pylon with Insulators -```xml -<!-- Tapered tower --> -<polygon class="pylon" points="20,0 25,0 30,80 15,80"/> -<!-- Cross arms --> -<line class="pylon-arm" x1="5" y1="10" x2="40" y2="10"/> -<line class="pylon-arm" x1="8" y1="25" x2="37" y2="25"/> -<!-- Insulators (where lines attach) --> -<circle class="insulator" cx="8" cy="10" r="3"/> -<circle class="insulator" cx="37" cy="10" r="3"/> -``` - -### Transformer Symbol -```xml -<!-- Two coils with core --> -<circle class="transformer-coil" cx="25" cy="25" r="12"/> -<circle class="transformer-coil" cx="55" cy="25" r="12"/> -<rect class="transformer-core" x="35" y="15" width="10" height="20" rx="2"/> -<!-- Busbars --> -<line x1="0" y1="15" x2="-10" y2="15" stroke="#EF9F27" stroke-width="3"/> -``` - -### Pole-mounted Transformer -```xml -<rect class="pole" x="18" y="0" width="4" height="60"/> -<line x1="10" y1="8" x2="30" y2="8" stroke="#854F0B" stroke-width="2"/> -<rect class="dist-transformer" x="8" y="15" width="24" height="18" rx="2"/> -<line class="lv-line" x1="20" y1="33" x2="20" y2="60"/> -``` - -### House with Roof -```xml -<rect class="home" x="0" y="25" width="35" height="30" rx="2"/> -<polygon class="home-roof" points="0,25 17,8 35,25"/> -<!-- Door --> -<rect x="8" y="35" width="8" height="15" fill="#085041"/> -<!-- Window --> -<rect x="22" y="32" width="8" height="8" fill="#9FE1CB"/> -``` - -### Factory Building -```xml -<rect class="factory" x="0" y="15" width="90" height="50" rx="3"/> -<!-- Smokestacks --> -<rect class="factory-stack" x="15" y="0" width="10" height="20"/> -<!-- Windows row --> -<rect x="10" y="30" width="15" height="12" fill="#F5C4B3"/> -<rect x="30" y="30" width="15" height="12" fill="#F5C4B3"/> -<!-- Loading dock --> -<rect x="55" y="50" width="30" height="15" fill="#993C1D"/> -``` - -### EV Charger with Car -```xml -<!-- Charging station --> -<rect class="ev-charger" x="20" y="0" width="25" height="45" rx="3"/> -<rect x="24" y="5" width="17" height="12" rx="1" fill="#3C3489"/> -<!-- Cable --> -<path d="M 32 20 Q 32 35 45 40" stroke="#534AB7" stroke-width="2" fill="none"/> -<circle cx="45" cy="40" r="4" fill="#534AB7"/> -<!-- Status light --> -<circle cx="32" cy="38" r="3" fill="#97C459"/> - -<!-- EV Car --> -<path class="ev-car" d="M 5 20 L 5 12 Q 5 5 15 5 L 45 5 Q 55 5 55 12 L 55 20 Z"/> -<!-- Windows --> -<rect x="10" y="8" width="15" height="8" rx="2" fill="#534AB7"/> -<!-- Wheels --> -<circle cx="15" cy="22" r="5" fill="#2C2C2A"/> -<!-- Charging bolt icon --> -<path d="M 28 12 L 32 8 L 30 11 L 34 11 L 30 16 L 32 13 Z" fill="#97C459"/> -``` - -## Voltage Level Line Styles - -```css -/* High voltage (transmission) - thick, bright */ -.hv-line { stroke: #EF9F27; stroke-width: 2.5; fill: none; } - -/* Medium voltage (distribution) - medium */ -.mv-line { stroke: #BA7517; stroke-width: 2; fill: none; } - -/* Low voltage (consumer) - thin, darker */ -.lv-line { stroke: #854F0B; stroke-width: 1.5; fill: none; } - -/* Smart grid data - dashed purple */ -.data-flow { stroke: #7F77DD; stroke-width: 1; fill: none; stroke-dasharray: 3 2; opacity: 0.7; } -``` - -## Flow Arrow Marker - -```xml -<defs> - <marker id="flow-arrow" viewBox="0 0 10 10" refX="9" refY="5" - markerWidth="6" markerHeight="6" orient="auto"> - <path d="M0,0 L10,5 L0,10 Z" fill="#EF9F27"/> - </marker> -</defs> -<!-- Usage --> -<line x1="140" y1="105" x2="210" y2="105" class="hv-line" marker-end="url(#flow-arrow)"/> -``` - -## CSS Classes - -```css -/* Generation */ -.nuclear-tower { fill: #B4B2A9; stroke: #5F5E5A; stroke-width: 1; } -.nuclear-building { fill: #EEEDFE; stroke: #534AB7; stroke-width: 1; } -.solar-panel { fill: #3C3489; stroke: #534AB7; stroke-width: 0.5; } -.wind-tower { fill: #B4B2A9; stroke: #5F5E5A; stroke-width: 1; } -.wind-blade { fill: #F1EFE8; stroke: #888780; stroke-width: 0.5; } -.gas-plant { fill: #FAECE7; stroke: #993C1D; stroke-width: 1; } -.gas-flame { fill: #EF9F27; } - -/* Transmission */ -.pylon { fill: #5F5E5A; stroke: #444441; stroke-width: 0.5; } -.insulator { fill: #FAEEDA; stroke: #854F0B; stroke-width: 0.5; } -.substation { fill: #E6F1FB; stroke: #185FA5; stroke-width: 1; } -.transformer-coil { fill: none; stroke: #185FA5; stroke-width: 1.5; } - -/* Distribution */ -.pole { fill: #854F0B; stroke: #633806; stroke-width: 0.5; } -.dist-transformer { fill: #E1F5EE; stroke: #0F6E56; stroke-width: 1; } - -/* Consumption */ -.home { fill: #E1F5EE; stroke: #0F6E56; stroke-width: 1; } -.home-roof { fill: #0F6E56; stroke: #085041; stroke-width: 0.5; } -.factory { fill: #FAECE7; stroke: #993C1D; stroke-width: 1; } -.ev-charger { fill: #EEEDFE; stroke: #534AB7; stroke-width: 1; } -.ev-car { fill: #3C3489; stroke: #534AB7; stroke-width: 0.5; } - -/* Smart grid */ -.smart-grid { fill: #EEEDFE; stroke: #534AB7; stroke-width: 1.5; } -``` - -## Layout Notes - -- **ViewBox**: 820×520 (wide for 4-column layout) -- **Column widths**: ~200px per stage -- **Stage dividers**: Vertical dashed lines at x=200, 420, 620 -- **Stage labels**: Top of diagram, uppercase for emphasis -- **Flow direction**: Left-to-right with arrows showing power flow -- **Data overlay**: Smart grid data lines use different style (dashed purple) to distinguish from power lines -- **Capacity labels**: Show MW ratings on generators for context -- **Voltage labels**: Show transformation ratios at substations diff --git a/optional-skills/creative/concept-diagrams/examples/feature-film-production-pipeline.md b/optional-skills/creative/concept-diagrams/examples/feature-film-production-pipeline.md deleted file mode 100644 index 76f5f86fc6e..00000000000 --- a/optional-skills/creative/concept-diagrams/examples/feature-film-production-pipeline.md +++ /dev/null @@ -1,172 +0,0 @@ -# Feature Film Production Pipeline - -A phased workflow showing the five stages of filmmaking, using containers with inner nodes and horizontal sub-flows within a phase. - -## Key Patterns Used - -- **Phase containers**: Large rounded rectangles with neutral background and dashed borders -- **Inner task nodes**: Smaller colored nodes inside containers for sub-tasks -- **Horizontal flow within container**: Post-production shows sequential pipeline with arrows (Editing → Color → VFX → Sound → Score) -- **Consistent phase spacing**: ~30px gap between phase containers -- **Phase labels with subtitles**: Each container has title + description - -## Diagram - -```xml -<svg width="100%" viewBox="0 0 680 780" xmlns="http://www.w3.org/2000/svg"> - <defs> - <marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" - markerWidth="6" markerHeight="6" orient="auto-start-reverse"> - <path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" - stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> - </marker> - </defs> - - <!-- Phase 1: Development --> - <g> - <rect x="40" y="30" width="600" height="110" rx="16" stroke-width="1" stroke-dasharray="6 4" fill="var(--bg-secondary)" stroke="var(--border)"/> - <text class="th" x="66" y="56">Development</text> - <text class="ts" x="66" y="74">Concept to greenlight</text> - </g> - <g class="node c-purple"> - <rect x="70" y="90" width="160" height="36" rx="6" stroke-width="0.5"/> - <text class="ts" x="150" y="108" text-anchor="middle" dominant-baseline="central">Script / screenplay</text> - </g> - <g class="node c-purple"> - <rect x="260" y="90" width="160" height="36" rx="6" stroke-width="0.5"/> - <text class="ts" x="340" y="108" text-anchor="middle" dominant-baseline="central">Financing / budget</text> - </g> - <g class="node c-purple"> - <rect x="450" y="90" width="160" height="36" rx="6" stroke-width="0.5"/> - <text class="ts" x="530" y="108" text-anchor="middle" dominant-baseline="central">Casting leads</text> - </g> - - <!-- Arrow to Phase 2 --> - <line x1="340" y1="140" x2="340" y2="170" class="arr" marker-end="url(#arrow)"/> - - <!-- Phase 2: Pre-production --> - <g> - <rect x="40" y="170" width="600" height="110" rx="16" stroke-width="1" stroke-dasharray="6 4" fill="var(--bg-secondary)" stroke="var(--border)"/> - <text class="th" x="66" y="196">Pre-production</text> - <text class="ts" x="66" y="214">Planning and preparation</text> - </g> - <g class="node c-teal"> - <rect x="70" y="230" width="160" height="36" rx="6" stroke-width="0.5"/> - <text class="ts" x="150" y="248" text-anchor="middle" dominant-baseline="central">Storyboards</text> - </g> - <g class="node c-teal"> - <rect x="260" y="230" width="160" height="36" rx="6" stroke-width="0.5"/> - <text class="ts" x="340" y="248" text-anchor="middle" dominant-baseline="central">Location scouting</text> - </g> - <g class="node c-teal"> - <rect x="450" y="230" width="160" height="36" rx="6" stroke-width="0.5"/> - <text class="ts" x="530" y="248" text-anchor="middle" dominant-baseline="central">Crew hiring</text> - </g> - - <!-- Arrow to Phase 3 --> - <line x1="340" y1="280" x2="340" y2="310" class="arr" marker-end="url(#arrow)"/> - - <!-- Phase 3: Production --> - <g> - <rect x="40" y="310" width="600" height="110" rx="16" stroke-width="1" stroke-dasharray="6 4" fill="var(--bg-secondary)" stroke="var(--border)"/> - <text class="th" x="66" y="336">Production</text> - <text class="ts" x="66" y="354">Principal photography</text> - </g> - <g class="node c-coral"> - <rect x="70" y="370" width="160" height="36" rx="6" stroke-width="0.5"/> - <text class="ts" x="150" y="388" text-anchor="middle" dominant-baseline="central">Filming / shooting</text> - </g> - <g class="node c-coral"> - <rect x="260" y="370" width="160" height="36" rx="6" stroke-width="0.5"/> - <text class="ts" x="340" y="388" text-anchor="middle" dominant-baseline="central">Production sound</text> - </g> - <g class="node c-coral"> - <rect x="450" y="370" width="160" height="36" rx="6" stroke-width="0.5"/> - <text class="ts" x="530" y="388" text-anchor="middle" dominant-baseline="central">VFX plates</text> - </g> - - <!-- Arrow to Phase 4 --> - <line x1="340" y1="420" x2="340" y2="450" class="arr" marker-end="url(#arrow)"/> - - <!-- Phase 4: Post-production --> - <g> - <rect x="40" y="450" width="600" height="150" rx="16" stroke-width="1" stroke-dasharray="6 4" fill="var(--bg-secondary)" stroke="var(--border)"/> - <text class="th" x="66" y="476">Post-production</text> - <text class="ts" x="66" y="494">Assembly and finishing</text> - </g> - <g class="node c-amber"> - <rect x="70" y="510" width="110" height="36" rx="6" stroke-width="0.5"/> - <text class="ts" x="125" y="528" text-anchor="middle" dominant-baseline="central">Editing</text> - </g> - <g class="node c-amber"> - <rect x="195" y="510" width="110" height="36" rx="6" stroke-width="0.5"/> - <text class="ts" x="250" y="528" text-anchor="middle" dominant-baseline="central">Color grade</text> - </g> - <g class="node c-amber"> - <rect x="320" y="510" width="90" height="36" rx="6" stroke-width="0.5"/> - <text class="ts" x="365" y="528" text-anchor="middle" dominant-baseline="central">VFX</text> - </g> - <g class="node c-amber"> - <rect x="425" y="510" width="100" height="36" rx="6" stroke-width="0.5"/> - <text class="ts" x="475" y="528" text-anchor="middle" dominant-baseline="central">Sound mix</text> - </g> - <g class="node c-amber"> - <rect x="540" y="510" width="80" height="36" rx="6" stroke-width="0.5"/> - <text class="ts" x="580" y="528" text-anchor="middle" dominant-baseline="central">Score</text> - </g> - <!-- Flow arrows within post --> - <line x1="180" y1="528" x2="195" y2="528" class="arr" marker-end="url(#arrow)"/> - <line x1="305" y1="528" x2="320" y2="528" class="arr" marker-end="url(#arrow)"/> - <line x1="410" y1="528" x2="425" y2="528" class="arr" marker-end="url(#arrow)"/> - <line x1="525" y1="528" x2="540" y2="528" class="arr" marker-end="url(#arrow)"/> - <!-- Final delivery label --> - <g class="node c-amber"> - <rect x="240" y="556" width="200" height="32" rx="6" stroke-width="0.5"/> - <text class="ts" x="340" y="572" text-anchor="middle" dominant-baseline="central">Final master / DCP</text> - </g> - <line x1="340" y1="546" x2="340" y2="556" class="arr" marker-end="url(#arrow)"/> - - <!-- Arrow to Phase 5 --> - <line x1="340" y1="600" x2="340" y2="630" class="arr" marker-end="url(#arrow)"/> - - <!-- Phase 5: Distribution --> - <g> - <rect x="40" y="630" width="600" height="110" rx="16" stroke-width="1" stroke-dasharray="6 4" fill="var(--bg-secondary)" stroke="var(--border)"/> - <text class="th" x="66" y="656">Distribution</text> - <text class="ts" x="66" y="674">Release and exhibition</text> - </g> - <g class="node c-blue"> - <rect x="70" y="690" width="160" height="36" rx="6" stroke-width="0.5"/> - <text class="ts" x="150" y="708" text-anchor="middle" dominant-baseline="central">Film festivals</text> - </g> - <g class="node c-blue"> - <rect x="260" y="690" width="160" height="36" rx="6" stroke-width="0.5"/> - <text class="ts" x="340" y="708" text-anchor="middle" dominant-baseline="central">Theatrical release</text> - </g> - <g class="node c-blue"> - <rect x="450" y="690" width="160" height="36" rx="6" stroke-width="0.5"/> - <text class="ts" x="530" y="708" text-anchor="middle" dominant-baseline="central">Streaming / VOD</text> - </g> -</svg> -``` - -## Color Assignments - -| Element | Color | Reason | -|---------|-------|--------| -| Phase containers | Neutral (dashed) | Subtle grouping, doesn't compete with content | -| Development tasks | `c-purple` | Creative/concept work | -| Pre-production tasks | `c-teal` | Planning and preparation | -| Production tasks | `c-coral` | Active filming (main event) | -| Post-production tasks | `c-amber` | Processing/refinement | -| Distribution tasks | `c-blue` | Outward delivery/release | - -## Layout Notes - -- **ViewBox**: 680×780 (standard width, tall for 5 phases) -- **Container style**: Dashed border (`stroke-dasharray="6 4"`), neutral fill (`var(--bg-secondary)`), `stroke-width="1"` -- **Container height**: 110px for 3-node phases, 150px for post-production (more complex) -- **Inner node dimensions**: 160×36px for standard tasks, variable width for post-production sequential flow -- **Phase gap**: 30px between containers -- **Horizontal sub-flow**: Post-production uses tightly packed nodes with arrows between them to show sequence -- **Convergence node**: "Final master / DCP" sits below the horizontal flow, collecting all post outputs diff --git a/optional-skills/creative/concept-diagrams/examples/hospital-emergency-department-flow.md b/optional-skills/creative/concept-diagrams/examples/hospital-emergency-department-flow.md deleted file mode 100644 index a64c50e5d44..00000000000 --- a/optional-skills/creative/concept-diagrams/examples/hospital-emergency-department-flow.md +++ /dev/null @@ -1,165 +0,0 @@ -# Hospital Emergency Department Flow - -A multi-path flowchart showing patient journey through an emergency department with priority-based routing using semantic colors (red=critical, amber=urgent, green=stable). - -## Key Patterns Used - -- **Semantic color coding**: Red/amber/green for priority levels (not arbitrary decoration) -- **Stage labels**: Left-aligned faded labels marking workflow phases -- **Convergent paths**: Multiple entry points merging, then branching, then converging again -- **Nested containers**: Diagnostics grouped in a container with inner nodes -- **Legend**: Color key at bottom explaining priority levels - -## Diagram - -```xml -<svg width="100%" viewBox="0 0 680 620" xmlns="http://www.w3.org/2000/svg"> - <defs> - <marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" - markerWidth="6" markerHeight="6" orient="auto-start-reverse"> - <path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" - stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> - </marker> - </defs> - - <!-- Stage labels --> - <text class="ts" x="40" y="68" text-anchor="start" opacity=".5">Arrival</text> - <text class="ts" x="40" y="168" text-anchor="start" opacity=".5">Assessment</text> - <text class="ts" x="40" y="288" text-anchor="start" opacity=".5">Priority routing</text> - <text class="ts" x="40" y="418" text-anchor="start" opacity=".5">Diagnostics</text> - <text class="ts" x="40" y="518" text-anchor="start" opacity=".5">Outcome</text> - - <!-- Arrival: Ambulance --> - <g class="node c-gray"> - <rect x="140" y="40" width="160" height="56" rx="8" stroke-width="0.5"/> - <text class="th" x="220" y="60" text-anchor="middle" dominant-baseline="central">Ambulance</text> - <text class="ts" x="220" y="80" text-anchor="middle" dominant-baseline="central">Emergency transport</text> - </g> - - <!-- Arrival: Walk-in --> - <g class="node c-gray"> - <rect x="380" y="40" width="160" height="56" rx="8" stroke-width="0.5"/> - <text class="th" x="460" y="60" text-anchor="middle" dominant-baseline="central">Walk-in</text> - <text class="ts" x="460" y="80" text-anchor="middle" dominant-baseline="central">Self-arrival</text> - </g> - - <!-- Arrows to Triage --> - <line x1="220" y1="96" x2="300" y2="140" class="arr" marker-end="url(#arrow)"/> - <line x1="460" y1="96" x2="380" y2="140" class="arr" marker-end="url(#arrow)"/> - - <!-- Triage --> - <g class="node c-purple"> - <rect x="240" y="140" width="200" height="56" rx="8" stroke-width="0.5"/> - <text class="th" x="340" y="160" text-anchor="middle" dominant-baseline="central">Triage</text> - <text class="ts" x="340" y="180" text-anchor="middle" dominant-baseline="central">Nurse assessment, vitals</text> - </g> - - <!-- Arrows from Triage to Priority --> - <line x1="280" y1="196" x2="140" y2="260" class="arr" marker-end="url(#arrow)"/> - <line x1="340" y1="196" x2="340" y2="260" class="arr" marker-end="url(#arrow)"/> - <line x1="400" y1="196" x2="540" y2="260" class="arr" marker-end="url(#arrow)"/> - - <!-- Priority: Red - Trauma --> - <g class="node c-red"> - <rect x="60" y="260" width="160" height="56" rx="8" stroke-width="0.5"/> - <text class="th" x="140" y="280" text-anchor="middle" dominant-baseline="central">Trauma bay</text> - <text class="ts" x="140" y="300" text-anchor="middle" dominant-baseline="central">Priority: critical</text> - </g> - - <!-- Priority: Yellow - Exam rooms --> - <g class="node c-amber"> - <rect x="260" y="260" width="160" height="56" rx="8" stroke-width="0.5"/> - <text class="th" x="340" y="280" text-anchor="middle" dominant-baseline="central">Exam rooms</text> - <text class="ts" x="340" y="300" text-anchor="middle" dominant-baseline="central">Priority: urgent</text> - </g> - - <!-- Priority: Green - Waiting --> - <g class="node c-green"> - <rect x="460" y="260" width="160" height="56" rx="8" stroke-width="0.5"/> - <text class="th" x="540" y="280" text-anchor="middle" dominant-baseline="central">Waiting area</text> - <text class="ts" x="540" y="300" text-anchor="middle" dominant-baseline="central">Priority: stable</text> - </g> - - <!-- Arrows to Diagnostics --> - <line x1="140" y1="316" x2="220" y2="390" class="arr" marker-end="url(#arrow)"/> - <line x1="340" y1="316" x2="340" y2="390" class="arr" marker-end="url(#arrow)"/> - <line x1="540" y1="316" x2="460" y2="390" class="arr" marker-end="url(#arrow)"/> - - <!-- Diagnostics container --> - <g class="c-teal"> - <rect x="140" y="390" width="400" height="56" rx="12" stroke-width="0.5"/> - </g> - - <!-- Labs --> - <g class="node c-teal"> - <rect x="160" y="400" width="110" height="36" rx="6" stroke-width="0.5"/> - <text class="ts" x="215" y="418" text-anchor="middle" dominant-baseline="central">Labs</text> - </g> - - <!-- Imaging --> - <g class="node c-teal"> - <rect x="285" y="400" width="110" height="36" rx="6" stroke-width="0.5"/> - <text class="ts" x="340" y="418" text-anchor="middle" dominant-baseline="central">Imaging</text> - </g> - - <!-- Diagnosis --> - <g class="node c-teal"> - <rect x="410" y="400" width="110" height="36" rx="6" stroke-width="0.5"/> - <text class="ts" x="465" y="418" text-anchor="middle" dominant-baseline="central">Diagnosis</text> - </g> - - <!-- Arrows to Outcomes --> - <line x1="215" y1="446" x2="160" y2="490" class="arr" marker-end="url(#arrow)"/> - <line x1="340" y1="446" x2="340" y2="490" class="arr" marker-end="url(#arrow)"/> - <line x1="465" y1="446" x2="520" y2="490" class="arr" marker-end="url(#arrow)"/> - - <!-- Outcome: Admission --> - <g class="node c-coral"> - <rect x="80" y="490" width="160" height="56" rx="8" stroke-width="0.5"/> - <text class="th" x="160" y="510" text-anchor="middle" dominant-baseline="central">Admission</text> - <text class="ts" x="160" y="530" text-anchor="middle" dominant-baseline="central">Inpatient ward</text> - </g> - - <!-- Outcome: Surgery --> - <g class="node c-coral"> - <rect x="260" y="490" width="160" height="56" rx="8" stroke-width="0.5"/> - <text class="th" x="340" y="510" text-anchor="middle" dominant-baseline="central">Surgery</text> - <text class="ts" x="340" y="530" text-anchor="middle" dominant-baseline="central">Operating room</text> - </g> - - <!-- Outcome: Discharge --> - <g class="node c-coral"> - <rect x="440" y="490" width="160" height="56" rx="8" stroke-width="0.5"/> - <text class="th" x="520" y="510" text-anchor="middle" dominant-baseline="central">Discharge</text> - <text class="ts" x="520" y="530" text-anchor="middle" dominant-baseline="central">Home with instructions</text> - </g> - - <!-- Legend --> - <text class="ts" x="140" y="580" opacity=".5">Priority levels</text> - <g class="c-red"><rect x="140" y="592" width="14" height="14" rx="3" stroke-width="0.5"/></g> - <text class="ts" x="162" y="604">Critical</text> - <g class="c-amber"><rect x="240" y="592" width="14" height="14" rx="3" stroke-width="0.5"/></g> - <text class="ts" x="262" y="604">Urgent</text> - <g class="c-green"><rect x="340" y="592" width="14" height="14" rx="3" stroke-width="0.5"/></g> - <text class="ts" x="362" y="604">Stable</text> -</svg> -``` - -## Color Assignments - -| Element | Color | Reason | -|---------|-------|--------| -| Entry points (Ambulance, Walk-in) | `c-gray` | Neutral starting points | -| Triage | `c-purple` | Processing/assessment step | -| Trauma bay | `c-red` | Critical priority (semantic) | -| Exam rooms | `c-amber` | Urgent priority (semantic) | -| Waiting area | `c-green` | Stable priority (semantic) | -| Diagnostics | `c-teal` | Clinical services category | -| Outcomes | `c-coral` | Final disposition category | - -## Layout Notes - -- **ViewBox**: 680×620 (standard width, extended height for 5 stages) -- **Stage spacing**: ~110-130px between stage rows -- **Diagonal arrows**: Connect nodes across columns naturally -- **Container with inner nodes**: Diagnostics uses outer `c-teal` rect with inner node rects diff --git a/optional-skills/creative/concept-diagrams/examples/ml-benchmark-grouped-bar-chart.md b/optional-skills/creative/concept-diagrams/examples/ml-benchmark-grouped-bar-chart.md deleted file mode 100644 index be6a4cd1b60..00000000000 --- a/optional-skills/creative/concept-diagrams/examples/ml-benchmark-grouped-bar-chart.md +++ /dev/null @@ -1,114 +0,0 @@ -# ML Benchmark Grouped Bar Chart with Dual Axis - -A quantitative data visualization comparing LLM inference speed across quantization levels with dual Y-axes, threshold markers, and an inset accuracy table. - -## Key Patterns Used - -- **Grouped bars**: Min/max range pairs per category using semantic color pairs (lighter=min, darker=max) -- **Dual Y-axis**: Left axis for primary metric (tok/s), right axis for secondary metric (VRAM GB) -- **Overlay line graph**: `<polyline>` with labeled dots showing VRAM usage across categories -- **Threshold marker**: Dashed red horizontal line indicating hardware limit (24 GB GPU) -- **Zone annotations**: Subtle text labels above/below threshold for context -- **Inset data table**: Alternating row fills below chart with quantitative accuracy data -- **Semantic color coding**: Each quantization level gets its own color from the skill palette (red=OOM, amber=slow, teal=sweet spot, blue=fast) - -## Diagram Type - -This is a **quantitative data chart** with: -- **Grouped vertical bars**: Range bars showing min–max performance per category -- **Secondary axis line**: VRAM usage overlaid as a connected scatter plot -- **Threshold annotation**: Hardware constraint line -- **Inset table**: Supporting accuracy metrics - -## Chart Layout Formula - -``` -Chart area: x=90–590, y=70–410 (500px wide, 340px tall) -Left Y-axis: Primary metric (tok/s) - y = 410 − (val / max_val) × 340 -Right Y-axis: Secondary metric (VRAM GB) - Same formula, different scale labels -Groups: Divide width by number of categories -Bars: Each group → min bar (34px) + 8px gap + max bar (34px) -Line overlay: <polyline> connecting data points across group centers -Threshold: Horizontal dashed line at critical value -Table: Below chart, alternating row fills -``` - -## Data Mapped - -| Quantization | Model Size | Speed (tok/s) | VRAM (GB) | MMLU Pro | Status | -|-------------|-----------|---------------|-----------|----------|--------| -| FP16 | 62 GB | 0.5–2 | 62 | 75.2 | OOM / unusable | -| Q8_0 | 32 GB | 3–5 | 32 | 75.0 | Partial offload | -| Q4_K_M | 16.8 GB | 8–12 | 16.8 | 73.1 | Fits in VRAM ✓ | -| IQ3_M | 12 GB | 12–15 | 12 | 70.5 | Full GPU speed | - -## Bar CSS Classes - -```css -/* Light mode */ -.bar-fp16-min { fill: #FCEBEB; stroke: #A32D2D; stroke-width: 0.75; } -.bar-fp16-max { fill: #F7C1C1; stroke: #A32D2D; stroke-width: 0.75; } -.bar-q8-min { fill: #FAEEDA; stroke: #854F0B; stroke-width: 0.75; } -.bar-q8-max { fill: #FAC775; stroke: #854F0B; stroke-width: 0.75; } -.bar-q4-min { fill: #E1F5EE; stroke: #0F6E56; stroke-width: 0.75; } -.bar-q4-max { fill: #9FE1CB; stroke: #0F6E56; stroke-width: 0.75; } -.bar-iq3-min { fill: #E6F1FB; stroke: #185FA5; stroke-width: 0.75; } -.bar-iq3-max { fill: #B5D4F4; stroke: #185FA5; stroke-width: 0.75; } - -/* Dark mode */ -@media (prefers-color-scheme: dark) { - .bar-fp16-min { fill: #501313; stroke: #F09595; } - .bar-fp16-max { fill: #791F1F; stroke: #F09595; } - .bar-q8-min { fill: #412402; stroke: #EF9F27; } - .bar-q8-max { fill: #633806; stroke: #EF9F27; } - .bar-q4-min { fill: #04342C; stroke: #5DCAA5; } - .bar-q4-max { fill: #085041; stroke: #5DCAA5; } - .bar-iq3-min { fill: #042C53; stroke: #85B7EB; } - .bar-iq3-max { fill: #0C447C; stroke: #85B7EB; } -} -``` - -## Overlay Line CSS - -```css -.vram-line { stroke: #534AB7; stroke-width: 2.5; fill: none; } -.vram-dot { fill: #534AB7; stroke: var(--bg-primary); stroke-width: 2; } -.vram-label { font-family: system-ui, sans-serif; font-size: 10px; fill: #534AB7; font-weight: 500; } -``` - -## Threshold CSS - -```css -.threshold { stroke: #A32D2D; stroke-width: 1; stroke-dasharray: 6 3; fill: none; } -.threshold-label { font-family: system-ui, sans-serif; font-size: 10px; fill: #A32D2D; font-weight: 500; } -``` - -## Table CSS - -```css -.tbl-header { fill: var(--bg-secondary); stroke: var(--border); stroke-width: 0.5; } -.tbl-row { fill: transparent; stroke: var(--border); stroke-width: 0.25; } -.tbl-alt { fill: var(--bg-secondary); stroke: var(--border); stroke-width: 0.25; } -``` - -## Layout Notes - -- **ViewBox**: 680×660 (portrait, chart + legend + table) -- **Chart area**: y=70–410, x=90–590 -- **Legend row**: y=458–470 -- **Inset table**: y=490–620 -- **Bar width**: 34px each, 8px gap between min/max pair -- **Group spacing**: 125px center-to-center -- **Dot halo**: White circle (r=6) behind colored dot (r=5) for legibility over bars/grid - -## When to Use This Pattern - -Use this diagram style for: -- Model benchmark comparisons across quantization levels -- Performance vs. resource usage tradeoff analysis -- Any multi-metric comparison with a hardware/software constraint -- GPU/TPU/accelerator benchmarking dashboards -- Accuracy vs. speed Pareto frontiers -- Hardware requirement sizing charts diff --git a/optional-skills/creative/concept-diagrams/examples/place-order-uml-sequence.md b/optional-skills/creative/concept-diagrams/examples/place-order-uml-sequence.md deleted file mode 100644 index dfb4f6744d9..00000000000 --- a/optional-skills/creative/concept-diagrams/examples/place-order-uml-sequence.md +++ /dev/null @@ -1,325 +0,0 @@ -# Place Order — UML Sequence Diagram - -A UML sequence diagram for the 'Place Order' use case in an e-commerce system. Six lifelines (:Customer, :ShoppingCart, :OrderController, :PaymentGateway, :InventorySystem, :EmailService) interact across 14 numbered messages. An **alt** combined fragment (amber) covers the three conditional outcomes — payment authorized, payment failed, and item unavailable. A **par** combined fragment (teal) nested inside the success branch shows concurrent email confirmation and stock-level update. Demonstrates activation bars, two distinct arrowhead types, UML pentagon fragment tags, and guard conditions. - -## Key Patterns Used - -- **6 lifelines at equal spacing**: Lifeline centers placed at x=90, 190, 290, 390, 490, 590 (100px apart) so the first box left-edge lands at x=40 and the last right-edge lands at x=640 — exactly filling the safe area -- **Two-row actor headers**: Each lifeline box shows `":"` (small, tertiary color) on one line and the class name (slightly larger, bold) on a second line, matching the UML anonymous-instance notation `:ClassName` -- **Two separate arrowhead markers**: `#arr-call` is a filled triangle (`<polygon>`) for synchronous calls; `#arr-ret` is an open chevron (`fill="none"`) for dashed return messages — both use `context-stroke` to inherit line color -- **Activation bars**: Narrow 8px-wide rectangles (`class="activation"`) layered on top of lifeline stems to show object execution periods; OrderController's bar spans the entire interaction; shorter bars mark PaymentGateway, InventorySystem, and EmailService during their active windows -- **Combined fragment pentagon tag**: Each `alt` / `par` frame uses a `<polygon>` dog-eared label shape in the top-left corner — points follow the pattern `(x,y) (x+w,y) (x+w+6,y+6) (x+w+6,y+18) (x,y+18)` creating the characteristic UML notch -- **Nested par inside alt**: The `par` rect (teal) sits inside branch 1 of the `alt` rect (amber); inner rect uses inset x/y (+15/+2) so both borders remain visible and distinguishable -- **Guard conditions**: Italic text in `[square brackets]` placed immediately after each alt frame divider line, or just inside the top frame for branch 1 — rendered with a dedicated `guard-lbl` class (italic, amber color) -- **Alt branch dividers**: Solid horizontal lines (`.frag-alt-div`) span the full alt rect width to separate the three branches; par branch separator uses a dashed line (`.frag-par-div`) per UML spec -- **Lifeline end caps**: Short 14px horizontal tick marks at y=590 (bottom of all lifeline stems) to formally terminate each lifeline -- **Message sequence annotation**: A faint counter row below the legend (①–③ / ④–⑩ / ⑪–⑫ / ⑬–⑭) explains the four message groups without adding noise to the diagram body - -## Diagram - -```xml -<svg width="100%" viewBox="0 0 680 648" xmlns="http://www.w3.org/2000/svg"> - <defs> - <!-- Open chevron arrowhead — return messages --> - <marker id="arr-ret" viewBox="0 0 10 10" refX="8" refY="5" - markerWidth="6" markerHeight="6" orient="auto-start-reverse"> - <path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" - stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> - </marker> - - <!-- Filled triangle arrowhead — synchronous calls --> - <marker id="arr-call" viewBox="0 0 10 10" refX="9" refY="5" - markerWidth="7" markerHeight="7" orient="auto"> - <polygon points="0,1 10,5 0,9" fill="context-stroke"/> - </marker> - </defs> - - <!-- - Lifeline centres (x): - L1 :Customer → 90 - L2 :ShoppingCart → 190 - L3 :OrderController → 290 - L4 :PaymentGateway → 390 - L5 :InventorySystem → 490 - L6 :EmailService → 590 - Actor boxes: x = cx−50, y=20, w=100, h=56, rx=6 - Lifelines: x = cx, y1=76, y2=590 - --> - - <!-- ── 1. LIFELINE DASHED STEMS (drawn first, behind everything) ── --> - <line x1="90" y1="76" x2="90" y2="590" class="lifeline"/> - <line x1="190" y1="76" x2="190" y2="590" class="lifeline"/> - <line x1="290" y1="76" x2="290" y2="590" class="lifeline"/> - <line x1="390" y1="76" x2="390" y2="590" class="lifeline"/> - <line x1="490" y1="76" x2="490" y2="590" class="lifeline"/> - <line x1="590" y1="76" x2="590" y2="590" class="lifeline"/> - - <!-- ── 2. ACTOR HEADER BOXES ── --> - - <!-- :Customer --> - <rect x="40" y="20" width="100" height="56" rx="6" class="actor"/> - <text class="actor-colon" x="90" y="40" text-anchor="middle" dominant-baseline="central">:</text> - <text class="actor-name" x="90" y="58" text-anchor="middle" dominant-baseline="central">Customer</text> - - <!-- :ShoppingCart --> - <rect x="140" y="20" width="100" height="56" rx="6" class="actor"/> - <text class="actor-colon" x="190" y="37" text-anchor="middle" dominant-baseline="central">:</text> - <text class="actor-name" x="190" y="55" text-anchor="middle" dominant-baseline="central">ShoppingCart</text> - - <!-- :OrderController --> - <rect x="240" y="20" width="100" height="56" rx="6" class="actor"/> - <text class="actor-colon" x="290" y="37" text-anchor="middle" dominant-baseline="central">:</text> - <text class="actor-name" x="290" y="55" text-anchor="middle" dominant-baseline="central">OrderController</text> - - <!-- :PaymentGateway --> - <rect x="340" y="20" width="100" height="56" rx="6" class="actor"/> - <text class="actor-colon" x="390" y="37" text-anchor="middle" dominant-baseline="central">:</text> - <text class="actor-name" x="390" y="55" text-anchor="middle" dominant-baseline="central">PaymentGateway</text> - - <!-- :InventorySystem --> - <rect x="440" y="20" width="100" height="56" rx="6" class="actor"/> - <text class="actor-colon" x="490" y="37" text-anchor="middle" dominant-baseline="central">:</text> - <text class="actor-name" x="490" y="55" text-anchor="middle" dominant-baseline="central">InventorySystem</text> - - <!-- :EmailService --> - <rect x="540" y="20" width="100" height="56" rx="6" class="actor"/> - <text class="actor-colon" x="590" y="37" text-anchor="middle" dominant-baseline="central">:</text> - <text class="actor-name" x="590" y="55" text-anchor="middle" dominant-baseline="central">EmailService</text> - - <!-- ── 3. ACTIVATION BARS ── --> - <!-- ShoppingCart: active while forwarding checkout → placeOrder --> - <rect x="186" y="102" width="8" height="26" rx="1" class="activation"/> - <!-- OrderController: active throughout full sequence --> - <rect x="286" y="128" width="8" height="415" rx="1" class="activation"/> - <!-- PaymentGateway: active during auth check (happy-path branch only) --> - <rect x="386" y="154" width="8" height="46" rx="1" class="activation"/> - <!-- InventorySystem: active from reserveItems → updateStockLevels end --> - <rect x="486" y="225" width="8" height="128" rx="1" class="activation"/> - <!-- EmailService: active during confirmation send --> - <rect x="586" y="290" width="8" height="25" rx="1" class="activation"/> - - <!-- ── 4. PRE-ALT MESSAGES ── --> - - <!-- ① checkout() :Customer → :ShoppingCart --> - <line x1="90" y1="102" x2="186" y2="102" class="msg-call" marker-end="url(#arr-call)"/> - <text class="mlbl" x="140" y="97" text-anchor="middle">checkout()</text> - - <!-- ② placeOrder(cartItems) :ShoppingCart → :OrderController --> - <line x1="194" y1="128" x2="286" y2="128" class="msg-call" marker-end="url(#arr-call)"/> - <text class="mlbl" x="242" y="123" text-anchor="middle">placeOrder(cartItems)</text> - - <!-- ③ authorizePayment(amount) :OrderController → :PaymentGateway --> - <line x1="294" y1="154" x2="386" y2="154" class="msg-call" marker-end="url(#arr-call)"/> - <text class="mlbl" x="342" y="149" text-anchor="middle">authorizePayment(amount)</text> - - <!-- ── 5. ALT COMBINED FRAGMENT y=166 → y=563 ── --> - - <!-- Outer alt rectangle --> - <rect x="45" y="166" width="590" height="397" rx="3" class="frag-alt-bg"/> - - <!-- Pentagon "alt" tag: TL corner notch shape --> - <polygon points="45,166 84,166 90,173 90,185 45,185" class="frag-alt-tag"/> - <text class="frag-alt-kw" x="67" y="178" text-anchor="middle" dominant-baseline="central">alt</text> - - <!-- Guard: branch 1 --> - <text class="guard-lbl" x="96" y="179" dominant-baseline="central">[payment authorized]</text> - - <!-- ─── Branch 1: payment authorized ─── --> - - <!-- ④ « authorized » :PaymentGateway → :OrderController (dashed return) --> - <line x1="386" y1="200" x2="294" y2="200" class="msg-ret" marker-end="url(#arr-ret)"/> - <text class="rlbl" x="342" y="195" text-anchor="middle">« authorized »</text> - - <!-- ⑤ reserveItems(cartItems) :OrderController → :InventorySystem --> - <line x1="294" y1="225" x2="486" y2="225" class="msg-call" marker-end="url(#arr-call)"/> - <text class="mlbl" x="392" y="220" text-anchor="middle">reserveItems(cartItems)</text> - - <!-- ⑥ « itemsReserved » :InventorySystem → :OrderController (dashed return) --> - <line x1="486" y1="250" x2="294" y2="250" class="msg-ret" marker-end="url(#arr-ret)"/> - <text class="rlbl" x="392" y="245" text-anchor="middle">« itemsReserved »</text> - - <!-- ── 6. PAR COMBINED FRAGMENT (nested inside alt branch 1) y=266 → y=373 ── --> - - <!-- Inner par rectangle --> - <rect x="60" y="266" width="560" height="107" rx="3" class="frag-par-bg"/> - - <!-- Pentagon "par" tag --> - <polygon points="60,266 97,266 102,272 102,284 60,284" class="frag-par-tag"/> - <text class="frag-par-kw" x="81" y="275" text-anchor="middle" dominant-baseline="central">par</text> - - <!-- Par branch 1: email confirmation --> - - <!-- ⑦ sendConfirmationEmail() :OrderController → :EmailService --> - <line x1="294" y1="295" x2="586" y2="295" class="msg-call" marker-end="url(#arr-call)"/> - <text class="mlbl" x="442" y="290" text-anchor="middle">sendConfirmationEmail()</text> - - <!-- ⑧ « emailQueued » :EmailService → :OrderController (dashed return) --> - <line x1="586" y1="318" x2="294" y2="318" class="msg-ret" marker-end="url(#arr-ret)"/> - <text class="rlbl" x="442" y="313" text-anchor="middle">« emailQueued »</text> - - <!-- Par branch divider (dashed, per UML spec) --> - <line x1="60" y1="336" x2="620" y2="336" class="frag-par-div"/> - - <!-- Par branch 2: stock level update --> - - <!-- ⑨ updateStockLevels() :OrderController → :InventorySystem --> - <line x1="294" y1="355" x2="486" y2="355" class="msg-call" marker-end="url(#arr-call)"/> - <text class="mlbl" x="392" y="350" text-anchor="middle">updateStockLevels()</text> - - <!-- PAR fragment ends at y=373 --> - - <!-- ⑩ « orderPlaced » :OrderController → :Customer (dashed return, after par) --> - <line x1="286" y1="395" x2="90" y2="395" class="msg-ret" marker-end="url(#arr-ret)"/> - <text class="rlbl" x="190" y="390" text-anchor="middle">« orderPlaced »</text> - - <!-- ─── Alt else: [payment failed] ─── --> - - <!-- Alt branch divider 1 (solid line) --> - <line x1="45" y1="415" x2="635" y2="415" class="frag-alt-div"/> - <text class="guard-lbl" x="50" y="429" dominant-baseline="central">[payment failed]</text> - - <!-- ⑪ « authFailed » :PaymentGateway → :OrderController (dashed return) --> - <line x1="390" y1="448" x2="294" y2="448" class="msg-ret" marker-end="url(#arr-ret)"/> - <text class="rlbl" x="344" y="443" text-anchor="middle">« authFailed »</text> - - <!-- ⑫ error(PAYMENT_FAILED) :OrderController → :Customer --> - <line x1="286" y1="470" x2="90" y2="470" class="msg-call" marker-end="url(#arr-call)"/> - <text class="mlbl" x="190" y="465" text-anchor="middle">error(PAYMENT_FAILED)</text> - - <!-- ─── Alt else: [item unavailable] ─── --> - - <!-- Alt branch divider 2 (solid line) --> - <line x1="45" y1="490" x2="635" y2="490" class="frag-alt-div"/> - <text class="guard-lbl" x="50" y="504" dominant-baseline="central">[item unavailable]</text> - - <!-- ⑬ « unavailable » :InventorySystem → :OrderController (dashed return) --> - <line x1="486" y1="523" x2="294" y2="523" class="msg-ret" marker-end="url(#arr-ret)"/> - <text class="rlbl" x="392" y="518" text-anchor="middle">« unavailable »</text> - - <!-- ⑭ error(ITEM_UNAVAILABLE) :OrderController → :Customer --> - <line x1="286" y1="545" x2="90" y2="545" class="msg-call" marker-end="url(#arr-call)"/> - <text class="mlbl" x="190" y="540" text-anchor="middle">error(ITEM_UNAVAILABLE)</text> - - <!-- ALT fragment ends at y=563 --> - - <!-- ── 7. LIFELINE END CAPS (short horizontal tick at y=590) ── --> - <line x1="83" y1="590" x2="97" y2="590" stroke="var(--text-tertiary)" stroke-width="1.5"/> - <line x1="183" y1="590" x2="197" y2="590" stroke="var(--text-tertiary)" stroke-width="1.5"/> - <line x1="283" y1="590" x2="297" y2="590" stroke="var(--text-tertiary)" stroke-width="1.5"/> - <line x1="383" y1="590" x2="397" y2="590" stroke="var(--text-tertiary)" stroke-width="1.5"/> - <line x1="483" y1="590" x2="497" y2="590" stroke="var(--text-tertiary)" stroke-width="1.5"/> - <line x1="583" y1="590" x2="597" y2="590" stroke="var(--text-tertiary)" stroke-width="1.5"/> - - <!-- ── 8. LEGEND ── --> - <text class="ts" x="45" y="612" opacity=".45">Legend —</text> - - <line x1="110" y1="609" x2="148" y2="609" - stroke="var(--text-primary)" stroke-width="1.5" marker-end="url(#arr-call)"/> - <text class="ts" x="154" y="613" opacity=".75">Synchronous call</text> - - <line x1="288" y1="609" x2="326" y2="609" - stroke="var(--text-secondary)" stroke-width="1.5" - stroke-dasharray="5 3" marker-end="url(#arr-ret)"/> - <text class="ts" x="332" y="613" opacity=".75">Return message</text> - - <rect x="458" y="603" width="22" height="13" rx="2" - fill="#FAEEDA" fill-opacity="0.5" stroke="#854F0B" stroke-width="0.75"/> - <text class="ts" x="484" y="613" opacity=".75">alt fragment</text> - - <rect x="558" y="603" width="22" height="13" rx="2" - fill="#E1F5EE" fill-opacity="0.6" stroke="#0F6E56" stroke-width="0.75"/> - <text class="ts" x="584" y="613" opacity=".75">par fragment</text> - - <!-- Message group annotation --> - <text class="ts" x="45" y="632" opacity=".35"> - ①–③ pre-condition · ④–⑩ happy path · ⑪–⑫ payment failure · ⑬–⑭ item unavailable - </text> - -</svg> -``` - -## Custom CSS - -Add these classes to the hosting page `<style>` block (in addition to the standard skill CSS): - -```css -/* ── Actor lifeline header boxes ── */ -.actor { fill: var(--bg-secondary); stroke: var(--text-secondary); stroke-width: 0.5; } -.actor-name { font-family: system-ui, sans-serif; font-size: 11.5px; font-weight: 600; - fill: var(--text-primary); } -.actor-colon { font-family: system-ui, sans-serif; font-size: 10px; fill: var(--text-tertiary); } - -/* ── Lifeline dashed stems ── */ -.lifeline { stroke: var(--text-tertiary); stroke-width: 1; stroke-dasharray: 6 4; fill: none; } - -/* ── Activation bars ── */ -.activation { fill: var(--bg-secondary); stroke: var(--text-secondary); stroke-width: 0.75; } - -/* ── Message arrows ── */ -.msg-call { stroke: var(--text-primary); stroke-width: 1.5; fill: none; } -.msg-ret { stroke: var(--text-secondary); stroke-width: 1.5; fill: none; stroke-dasharray: 6 3; } - -/* ── Message labels ── */ -.mlbl { font-family: system-ui, sans-serif; font-size: 11px; fill: var(--text-primary); } -.rlbl { font-family: system-ui, sans-serif; font-size: 11px; fill: var(--text-secondary); - font-style: italic; } - -/* ── Combined fragment: alt (amber) ── */ -.frag-alt-bg { fill: #FAEEDA; fill-opacity: 0.18; stroke: #854F0B; stroke-width: 1; } -.frag-alt-tag { fill: #FAEEDA; stroke: #854F0B; stroke-width: 0.75; } -.frag-alt-kw { font-family: system-ui, sans-serif; font-size: 11px; font-weight: 700; - fill: #633806; } -.frag-alt-div { stroke: #854F0B; stroke-width: 0.75; fill: none; } -.guard-lbl { font-family: system-ui, sans-serif; font-size: 10.5px; font-style: italic; - fill: #854F0B; } - -/* ── Combined fragment: par (teal) ── */ -.frag-par-bg { fill: #E1F5EE; fill-opacity: 0.35; stroke: #0F6E56; stroke-width: 1; } -.frag-par-tag { fill: #E1F5EE; stroke: #0F6E56; stroke-width: 0.75; } -.frag-par-kw { font-family: system-ui, sans-serif; font-size: 11px; font-weight: 700; - fill: #085041; } -.frag-par-div { stroke: #0F6E56; stroke-width: 0.75; stroke-dasharray: 5 3; fill: none; } - -/* ── Dark mode overrides ── */ -@media (prefers-color-scheme: dark) { - .actor { fill: #2c2c2a; stroke: #b4b2a9; } - .actor-name { fill: #e8e6de; } - .actor-colon { fill: #888780; } - .frag-alt-bg { fill: #633806; fill-opacity: 0.25; stroke: #EF9F27; } - .frag-alt-tag { fill: #633806; stroke: #EF9F27; } - .frag-alt-kw { fill: #FAC775; } - .frag-alt-div { stroke: #EF9F27; } - .guard-lbl { fill: #EF9F27; } - .frag-par-bg { fill: #085041; fill-opacity: 0.35; stroke: #5DCAA5; } - .frag-par-tag { fill: #085041; stroke: #5DCAA5; } - .frag-par-kw { fill: #9FE1CB; } - .frag-par-div { stroke: #5DCAA5; } -} -``` - -## Color Assignments - -| Element | Color | Reason | -|---------|-------|--------| -| Actor header boxes | Neutral (`var(--bg-secondary)`) | Structural / non-semantic — all lifelines share one style | -| Activation bars | Neutral (`var(--bg-secondary)`) | Show execution periods without adding semantic color | -| Synchronous call arrows | `var(--text-primary)` + filled triangle | High contrast for calls — the primary interaction direction | -| Return / dashed arrows | `var(--text-secondary)` + open chevron | Lower contrast for returns — secondary flow direction | -| `alt` fragment | Amber (`#FAEEDA` / `#854F0B`) | Warning / conditional — matches `c-amber` semantic meaning | -| Guard condition text | Amber italic | Belongs visually to the alt fragment | -| `par` fragment | Teal (`#E1F5EE` / `#0F6E56`) | Concurrent success path — matches `c-teal` semantic meaning | -| Alt branch dividers | Amber solid line | Continuity with the alt frame color | -| Par branch divider | Teal dashed line | UML spec: par branches separated by dashed lines | - -## Layout Notes - -- **ViewBox**: 680×648 (standard width; height = lifeline bottom y=590 + legend + annotation + 16px buffer) -- **Lifeline spacing formula**: `(safe_area_width) / (n_lifelines − 1) = 600 / 5 = 120px` — but use `spacing = 100px` starting at `x=90` so that first box left = 40 and last box right = 640 exactly -- **Actor box split-label trick**: Two separate `<text>` elements per box — one for `":"` (10px, tertiary color) and one for the class name (11.5px bold, primary color) — avoids the 14px font needing ~150px+ per box for long names like "OrderController" -- **Pentagon tag formula**: For a fragment starting at `(fx, fy)`, the tag polygon points are `(fx,fy) (fx+w,fy) (fx+w+6,fy+6) (fx+w+6,fy+18) (fx,fy+18)` where `w` = approximate text width of the keyword + 8px padding each side -- **Nested fragment inset**: The `par` rect uses `x = alt_x + 15` and `y = alt_y_current + 2` so both borders remain simultaneously visible — inset enough to separate visually, not so much that it wastes vertical space -- **Activation bar placement**: `x = lifeline_cx − 4`, `width = 8` — centered on the lifeline and narrow enough not to obscure the dashed stem behind it -- **Message label y-offset**: All labels are placed at `y = arrow_y − 5` to sit just above the arrow line; this applies to both left-going and right-going arrows since `text-anchor="middle"` handles horizontal centering automatically -- **Return arrows entering activation bars**: End `x1/x2` at lifeline center (e.g. x=294 for OrderController) rather than the bar edge (x=286) — the small overlap is intentional and clarifies the target object -- **Alt guard label placement**: Branch 1 guard goes at `y = frame_top + 13` to the right of the pentagon tag; subsequent branch guards go at `divider_y + 14` so they sit just inside the new branch -- **Lifeline end cap pattern**: `<line x1="cx−7" y1="590" x2="cx+7" y2="590" stroke-width="1.5"/>` — a simple symmetric tick, no special marker needed diff --git a/optional-skills/creative/concept-diagrams/examples/smart-city-infrastructure.md b/optional-skills/creative/concept-diagrams/examples/smart-city-infrastructure.md deleted file mode 100644 index 4069ede0491..00000000000 --- a/optional-skills/creative/concept-diagrams/examples/smart-city-infrastructure.md +++ /dev/null @@ -1,173 +0,0 @@ -# Smart City Infrastructure - -A multi-system integration diagram showing interconnected city infrastructure (power, water, transport) connected through a central IoT platform with a citizen dashboard on top. Demonstrates hub-spoke layout, diverse physical shapes, and UI mockups. - -## Key Patterns Used - -- **Hub-spoke layout**: Central IoT platform with radiating data connections to subsystems -- **Connection dots**: Visual indicators where data lines attach to the central hub -- **Dashboard/UI mockup**: Screen with mini-charts, gauges, and status indicators -- **Multi-system integration**: Three independent systems unified by central platform -- **Semantic line styles**: Different stroke styles for data (dashed), power, water, roads -- **Physical infrastructure shapes**: Solar panels, wind turbines, dams, pipes, roads, vehicles - -## New Shape Techniques - -### Solar Panels (angled polygons with grid lines) -```xml -<polygon class="solar-panel" points="0,25 35,8 38,12 3,29"/> -<line class="solar-frame" x1="12" y1="22" x2="24" y2="13"/> -<line x1="19" y1="29" x2="19" y2="40" stroke="#5F5E5A" stroke-width="2"/> -``` - -### Wind Turbine (tower + nacelle + blades) -```xml -<!-- Tapered tower --> -<polygon class="wind-tower" points="20,70 30,70 28,25 22,25"/> -<!-- Nacelle --> -<rect class="wind-hub" x="18" y="20" width="14" height="8" rx="2"/> -<!-- Hub --> -<circle class="wind-hub" cx="25" cy="18" r="5"/> -<!-- Blades (rotated ellipses) --> -<ellipse class="wind-blade" cx="25" cy="5" rx="3" ry="13"/> -<ellipse class="wind-blade" cx="14" cy="26" rx="3" ry="13" transform="rotate(-120, 25, 18)"/> -<ellipse class="wind-blade" cx="36" cy="26" rx="3" ry="13" transform="rotate(120, 25, 18)"/> -``` - -### Battery with Charge Level -```xml -<rect class="battery" x="0" y="0" width="45" height="65" rx="5"/> -<!-- Terminals --> -<rect x="10" y="-6" width="10" height="8" rx="2" fill="#27500A"/> -<rect x="25" y="-6" width="10" height="8" rx="2" fill="#27500A"/> -<!-- Charge level fill --> -<rect class="battery-level" x="5" y="12" width="35" height="48" rx="3"/> -<text x="22" y="42" text-anchor="middle" fill="#173404" style="font-size:10px">85%</text> -``` - -### Dam/Reservoir with Water Waves -```xml -<!-- Dam wall --> -<polygon class="reservoir-wall" points="0,60 10,0 70,0 80,60"/> -<!-- Water behind dam --> -<polygon class="water" points="12,10 68,10 68,55 75,55 75,58 5,58 5,55 12,55"/> -<!-- Wave effect --> -<path d="M 15 25 Q 25 22 35 25 Q 45 28 55 25" fill="none" stroke="#378ADD" stroke-width="1" opacity="0.5"/> -``` - -### Pipe Network with Joints and Valves -```xml -<path class="pipe" d="M 80 85 L 110 85"/> -<circle class="pipe-joint" cx="10" cy="30" r="8"/> -<circle class="valve" cx="190" cy="85" r="6"/> -<!-- Distribution branches --> -<path class="pipe-thin" d="M 18 30 L 50 30"/> -<path class="pipe-thin" d="M 10 22 L 10 5 L 50 5"/> -``` - -### Road Intersection with Lane Markings -```xml -<!-- Road surface --> -<line class="road" x1="0" y1="50" x2="170" y2="50"/> -<line class="road-mark" x1="10" y1="50" x2="160" y2="50"/> -<!-- Cross road --> -<line class="road" x1="85" y1="0" x2="85" y2="100"/> -<line class="road-mark" x1="85" y1="10" x2="85" y2="90"/> -<!-- Embedded sensors --> -<circle class="sensor" cx="40" cy="50" r="5"/> -``` - -### Traffic Light with Signal States -```xml -<rect class="traffic-light" x="0" y="0" width="14" height="32" rx="3"/> -<circle class="light-red" cx="7" cy="8" r="4"/> -<circle class="light-off" cx="7" cy="16" r="4"/> -<circle class="light-off" cx="7" cy="24" r="4"/> -``` - -### Bus with Windows and Wheels -```xml -<rect class="bus" x="0" y="0" width="55" height="28" rx="6"/> -<!-- Windows --> -<rect class="bus-window" x="5" y="5" width="12" height="12" rx="2"/> -<rect class="bus-window" x="20" y="5" width="12" height="12" rx="2"/> -<!-- Wheels with hubcaps --> -<circle cx="14" cy="30" r="6" fill="#2C2C2A"/> -<circle cx="14" cy="30" r="3" fill="#5F5E5A"/> -``` - -### Dashboard UI Mockup -```xml -<!-- Monitor frame --> -<rect class="dashboard" x="0" y="0" width="200" height="120" rx="8"/> -<!-- Screen --> -<rect class="screen" x="10" y="10" width="180" height="85" rx="4"/> -<!-- Mini bar chart --> -<rect class="screen-content" x="18" y="18" width="50" height="35" rx="2"/> -<rect class="screen-chart" x="22" y="38" width="8" height="12"/> -<rect class="screen-chart" x="33" y="32" width="8" height="18"/> -<!-- Gauge --> -<circle class="screen-bar" cx="100" cy="35" r="12"/> -<text x="100" y="39" text-anchor="middle" fill="#E8E6DE" style="font-size:8px">78%</text> -<!-- Status indicators --> -<circle cx="35" cy="74" r="6" fill="#97C459"/> -<circle cx="75" cy="74" r="6" fill="#97C459"/> -<circle cx="115" cy="74" r="6" fill="#EF9F27"/> -``` - -### Hexagonal IoT Hub with Connection Points -```xml -<!-- Outer hexagon --> -<polygon class="iot-hex" points="0,-45 39,-22 39,22 0,45 -39,22 -39,-22"/> -<!-- Inner hexagon --> -<polygon class="iot-inner" points="0,-20 17,-10 17,10 0,20 -17,10 -17,-10"/> -<!-- Connection dots on data lines --> -<circle cx="321" cy="248" r="4" fill="#7F77DD"/> -``` - -## CSS Classes for Infrastructure - -```css -/* Power system */ -.solar-panel { fill: #3C3489; stroke: #534AB7; stroke-width: 0.5; } -.solar-frame { fill: none; stroke: #EEEDFE; stroke-width: 0.5; } -.wind-tower { fill: #B4B2A9; stroke: #5F5E5A; stroke-width: 1; } -.wind-blade { fill: #F1EFE8; stroke: #888780; stroke-width: 0.5; } -.battery { fill: #27500A; stroke: #3B6D11; stroke-width: 1.5; } -.battery-level { fill: #97C459; } -.power-line { stroke: #EF9F27; stroke-width: 2; fill: none; } - -/* Water system */ -.reservoir-wall { fill: #B4B2A9; stroke: #5F5E5A; stroke-width: 1; } -.water { fill: #85B7EB; stroke: #378ADD; stroke-width: 0.5; } -.pipe { fill: none; stroke: #378ADD; stroke-width: 4; stroke-linecap: round; } -.pipe-joint { fill: #185FA5; stroke: #0C447C; stroke-width: 1; } -.valve { fill: #0C447C; stroke: #185FA5; stroke-width: 1; } - -/* Transport */ -.road { stroke: #888780; stroke-width: 8; fill: none; stroke-linecap: round; } -.road-mark { stroke: #F1EFE8; stroke-width: 1; fill: none; stroke-dasharray: 6 4; } -.traffic-light { fill: #444441; stroke: #2C2C2A; stroke-width: 0.5; } -.light-red { fill: #E24B4A; } -.light-green { fill: #97C459; } -.light-off { fill: #2C2C2A; } -.bus { fill: #E1F5EE; stroke: #0F6E56; stroke-width: 1.5; } - -/* Data/IoT */ -.data-line { stroke: #7F77DD; stroke-width: 2; fill: none; stroke-dasharray: 4 3; } -.iot-hex { fill: #EEEDFE; stroke: #534AB7; stroke-width: 2; } - -/* Dashboard */ -.dashboard { fill: #F1EFE8; stroke: #5F5E5A; stroke-width: 1.5; } -.screen { fill: #1a1a18; } -.screen-chart { fill: #5DCAA5; } -``` - -## Layout Notes - -- **ViewBox**: 720×620 (wider for three-column system layout) -- **Hub position**: Central IoT at (360, 270) - geometric center -- **Data lines**: Use quadratic curves or L-shaped paths, add connection dots at hub attachment points -- **System spacing**: ~200px width per system section -- **Vertical layers**: Dashboard (top) → IoT Hub (middle) → Systems (bottom) -- **Component grouping**: Use `<g transform="translate(x,y)">` for each major component for easy positioning diff --git a/optional-skills/creative/concept-diagrams/examples/smartphone-layer-anatomy.md b/optional-skills/creative/concept-diagrams/examples/smartphone-layer-anatomy.md deleted file mode 100644 index 101be640b94..00000000000 --- a/optional-skills/creative/concept-diagrams/examples/smartphone-layer-anatomy.md +++ /dev/null @@ -1,154 +0,0 @@ -# Smartphone Layer Anatomy - -An exploded view diagram showing all internal layers of a smartphone from front glass to back, with alternating left/right labels to avoid overlap. Demonstrates layered product teardown visualization and component detail. - -## Key Patterns Used - -- **Exploded vertical stack**: Layers separated vertically to show internal structure -- **Alternating labels**: Left/right label placement prevents text overlap -- **Component detail**: Chips, coils, lenses rendered with realistic shapes -- **Thickness scale**: Measurement indicator on the side -- **Progressive depth**: Each layer slightly offset to create 3D stack effect - -## New Shape Techniques - -### Capacitive Touch Grid -```xml -<rect class="digitizer" x="0" y="0" width="140" height="90" rx="14"/> -<g transform="translate(8, 8)"> - <!-- Horizontal lines --> - <line class="digitizer-grid" x1="0" y1="15" x2="124" y2="15"/> - <line class="digitizer-grid" x1="0" y1="37" x2="124" y2="37"/> - <!-- Vertical lines --> - <line class="digitizer-grid" x1="20" y1="0" x2="20" y2="74"/> - <line class="digitizer-grid" x1="50" y1="0" x2="50" y2="74"/> -</g> -<!-- Touch point indicator --> -<circle cx="70" cy="45" r="12" fill="none" stroke="#7F77DD" stroke-width="2" opacity="0.6"/> -<circle cx="70" cy="45" r="5" fill="#7F77DD" opacity="0.4"/> -``` - -### OLED RGB Subpixels -```xml -<rect class="oled-panel" x="0" y="0" width="140" height="90" rx="12"/> -<g transform="translate(10, 10)"> - <!-- RGB pixel group --> - <rect class="oled-subpixel-r" x="0" y="0" width="2" height="6"/> - <rect class="oled-subpixel-g" x="3" y="0" width="2" height="6"/> - <rect class="oled-subpixel-b" x="6" y="0" width="2" height="6"/> - <!-- Repeat pattern --> - <rect class="oled-subpixel-r" x="11" y="0" width="2" height="6"/> - <rect class="oled-subpixel-g" x="14" y="0" width="2" height="6"/> - <rect class="oled-subpixel-b" x="17" y="0" width="2" height="6"/> -</g> -``` - -### Logic Board with Chips -```xml -<rect class="pcb" x="0" y="0" width="116" height="106" rx="3"/> -<!-- PCB traces --> -<path class="pcb-trace" d="M 8 50 L 30 50 L 30 35"/> - -<!-- CPU chip --> -<rect class="chip-cpu" x="30" y="20" width="55" height="35" rx="3"/> -<text class="chip-label" x="57" y="35" text-anchor="middle">A17 Pro</text> - -<!-- RAM chip --> -<rect class="chip-ram" x="30" y="62" width="35" height="18" rx="2"/> -<text class="chip-label" x="47" y="74" text-anchor="middle">8GB RAM</text> - -<!-- Storage chip --> -<rect class="chip-storage" x="30" y="85" width="55" height="16" rx="2"/> -<text class="chip-label" x="57" y="96" text-anchor="middle">256GB NAND</text> -``` - -### Camera Lens Array -```xml -<!-- Main camera --> -<circle class="camera-lens" cx="20" cy="20" r="18"/> -<circle class="camera-lens-inner" cx="20" cy="20" r="13"/> -<circle class="camera-sensor" cx="20" cy="20" r="8"/> -<circle cx="20" cy="20" r="3" fill="#1a1a18"/> - -<!-- Secondary camera (smaller) --> -<circle class="camera-lens" cx="15" cy="15" r="13"/> -<circle class="camera-lens-inner" cx="15" cy="15" r="9"/> -<circle class="camera-sensor" cx="15" cy="15" r="5"/> -``` - -### Wireless Charging Coil with Magnets -```xml -<!-- Concentric coil rings --> -<circle class="charging-coil-outer" cx="0" cy="0" r="30"/> -<circle class="charging-coil" cx="0" cy="0" r="23"/> -<circle class="charging-coil" cx="0" cy="0" r="16"/> -<circle class="charging-coil" cx="0" cy="0" r="9"/> - -<!-- MagSafe magnet ring --> -<circle class="magnet" cx="0" cy="-35" r="3"/> -<circle class="magnet" cx="25" cy="-25" r="3"/> -<circle class="magnet" cx="35" cy="0" r="3"/> -<circle class="magnet" cx="25" cy="25" r="3"/> -<!-- ... continue around circle --> -``` - -### Battery Cell -```xml -<rect class="battery" x="0" y="0" width="140" height="90" rx="10"/> -<rect class="battery-cell" x="10" y="12" width="120" height="60" rx="6"/> - -<text x="70" y="38" text-anchor="middle" fill="#27500A" style="font-size:9px">Li-Ion Polymer</text> -<text x="70" y="52" text-anchor="middle" fill="#27500A" style="font-size:12px; font-weight:bold">4422 mAh</text> - -<rect class="battery-connector" x="55" y="75" width="30" height="10" rx="2"/> -``` - -## CSS Classes - -```css -/* Glass */ -.front-glass { fill: #E8E6DE; stroke: #888780; stroke-width: 1; opacity: 0.9; } -.back-glass { fill: #2C2C2A; stroke: #444441; stroke-width: 1; } - -/* Touch digitizer */ -.digitizer { fill: #EEEDFE; stroke: #534AB7; stroke-width: 1; } -.digitizer-grid { stroke: #AFA9EC; stroke-width: 0.3; fill: none; } - -/* OLED */ -.oled-panel { fill: #1a1a18; stroke: #444441; stroke-width: 1; } -.oled-subpixel-r { fill: #E24B4A; } -.oled-subpixel-g { fill: #97C459; } -.oled-subpixel-b { fill: #378ADD; } - -/* Midframe */ -.midframe { fill: #B4B2A9; stroke: #5F5E5A; stroke-width: 1.5; } - -/* Logic board */ -.pcb { fill: #0F6E56; stroke: #085041; stroke-width: 1; } -.pcb-trace { stroke: #5DCAA5; stroke-width: 0.3; fill: none; } -.chip-cpu { fill: #3C3489; stroke: #534AB7; stroke-width: 0.5; } -.chip-ram { fill: #185FA5; stroke: #378ADD; stroke-width: 0.5; } -.chip-storage { fill: #27500A; stroke: #3B6D11; stroke-width: 0.5; } - -/* Battery */ -.battery { fill: #EAF3DE; stroke: #3B6D11; stroke-width: 1.5; } -.battery-cell { fill: #97C459; stroke: #639922; stroke-width: 0.5; } - -/* Camera */ -.camera-lens { fill: #0C447C; stroke: #185FA5; stroke-width: 0.5; } -.camera-lens-inner { fill: #1a1a18; stroke: #378ADD; stroke-width: 0.3; } -.camera-sensor { fill: #3C3489; stroke: #534AB7; stroke-width: 0.3; } - -/* Wireless charging */ -.charging-coil { fill: none; stroke: #EF9F27; stroke-width: 1.5; } -.magnet { fill: #5F5E5A; stroke: #444441; stroke-width: 0.5; } -``` - -## Layout Notes - -- **ViewBox**: 900×780 (tall for vertical stack) -- **Layer offset**: Each layer offset 10px right and down for depth effect -- **Label alternation**: Odd layers → RIGHT labels, Even layers → LEFT labels -- **Thickness scale**: Vertical measurement bar on left side -- **Front/Back markers**: Text labels at top and bottom -- **Chip labels**: Use small white text (6px) directly on chip shapes diff --git a/optional-skills/creative/concept-diagrams/examples/sn2-reaction-mechanism.md b/optional-skills/creative/concept-diagrams/examples/sn2-reaction-mechanism.md deleted file mode 100644 index 3f335d85d3d..00000000000 --- a/optional-skills/creative/concept-diagrams/examples/sn2-reaction-mechanism.md +++ /dev/null @@ -1,247 +0,0 @@ -# SN2 Reaction Mechanism - -A chemistry diagram showing the bimolecular nucleophilic substitution (SN2) mechanism between hydroxide ion and methyl bromide. Demonstrates molecular structure rendering, electron movement arrows, transition state notation, and reaction energy profiles. - -## Key Patterns Used - -- **Molecular structures**: Ball-and-stick style atoms with bonds -- **Electron movement**: Curved arrows showing nucleophilic attack -- **Transition state**: Bracketed pentacoordinate intermediate with partial charges -- **Stereochemistry**: Wedge/dash bonds showing 3D configuration -- **Energy profile**: Potential energy vs reaction coordinate plot -- **Annotation boxes**: Key features and mechanistic notes - -## Diagram Type - -This is a **chemistry mechanism diagram** with: -- **Molecular rendering**: Atoms as colored circles with element symbols -- **Bond notation**: Solid, wedge, dash, and partial (dashed) bonds -- **Reaction arrows**: Curved for electron movement, straight for reaction progress -- **Energy landscape**: Quantitative energy profile below mechanism - -## Molecular Structure Elements - -### Atom Rendering - -```xml -<!-- Carbon atom (dark) --> -<circle cx="0" cy="0" r="14" class="carbon"/> -<text class="chem" x="0" y="5" text-anchor="middle" fill="white" font-weight="500">C</text> - -<!-- Oxygen atom (red) --> -<circle cx="0" cy="0" r="14" class="oxygen"/> -<text class="chem" x="0" y="5" text-anchor="middle" fill="white" font-weight="500">O</text> - -<!-- Hydrogen atom (light with border) --> -<circle cx="38" cy="0" r="8" class="hydrogen"/> -<text class="chem-sm" x="38" y="4" text-anchor="middle">H</text> - -<!-- Bromine atom (brown) --> -<circle cx="52" cy="0" r="16" class="bromine"/> -<text class="chem" x="52" y="5" text-anchor="middle" fill="white" font-weight="500">Br</text> -``` - -```css -.carbon { fill: #2C2C2A; } -.hydrogen { fill: #F1EFE8; stroke: #888780; stroke-width: 1; } -.oxygen { fill: #E24B4A; } -.bromine { fill: #993C1D; } -.nitrogen { fill: #378ADD; } /* for other reactions */ -``` - -### Bond Types - -```xml -<!-- Single bond (solid) --> -<line x1="14" y1="0" x2="38" y2="0" class="bond"/> - -<!-- Wedge bond (coming toward viewer) --> -<polygon class="bond-wedge" points="0,-14 -6,-35 6,-35"/> - -<!-- Dash bond (going away from viewer) --> -<line x1="-10" y1="10" x2="-28" y2="28" class="bond-dash"/> - -<!-- Partial bond (forming/breaking) --> -<line x1="-40" y1="0" x2="-14" y2="0" class="bond-partial"/> -``` - -```css -.bond { stroke: var(--text-primary); stroke-width: 2.5; fill: none; stroke-linecap: round; } -.bond-thin { stroke: var(--text-primary); stroke-width: 1.5; fill: none; } -.bond-partial { stroke: var(--text-primary); stroke-width: 2; fill: none; stroke-dasharray: 4 3; } -.bond-wedge { fill: var(--text-primary); stroke: none; } -.bond-dash { stroke: var(--text-primary); stroke-width: 2; fill: none; stroke-dasharray: 2 2; } -``` - -### Lone Pairs and Charges - -```xml -<!-- Lone pair electrons (dots) --> -<circle cx="-8" cy="-18" r="2" fill="var(--text-primary)"/> -<circle cx="0" cy="-18" r="2" fill="var(--text-primary)"/> - -<!-- Formal negative charge --> -<text class="charge" x="12" y="-12" fill="#A32D2D" font-weight="bold">⊖</text> - -<!-- Partial charges (delta notation) --> -<text class="partial" x="0" y="-18" text-anchor="middle" fill="#A32D2D">δ⁻</text> -<text class="partial" x="0" y="-22" text-anchor="middle" fill="#3B6D11">δ⁺</text> -``` - -```css -.charge { font-family: "Times New Roman", Georgia, serif; font-size: 12px; } -.partial { font-family: "Times New Roman", Georgia, serif; font-size: 11px; font-style: italic; } -``` - -### Curved Arrow (Electron Movement) - -```xml -<defs> - <marker id="curved-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto"> - <path d="M0,0 L10,5 L0,10 L3,5 Z" class="arrow-fill"/> - </marker> -</defs> - -<!-- Nucleophilic attack arrow --> -<path d="M -5,15 Q 30,60 70,25" class="arrow-curved" marker-end="url(#curved-arrow)"/> -``` - -```css -.arrow-curved { stroke: #534AB7; stroke-width: 2; fill: none; } -.arrow-fill { fill: #534AB7; } -``` - -### Transition State Brackets - -```xml -<!-- Left bracket --> -<path d="M -75,-70 L -85,-70 L -85,75 L -75,75" class="ts-bracket"/> - -<!-- Right bracket --> -<path d="M 95,-70 L 105,-70 L 105,75 L 95,75" class="ts-bracket"/> - -<!-- Double dagger symbol --> -<text class="chem" x="115" y="-60" fill="var(--text-primary)">‡</text> -``` - -```css -.ts-bracket { stroke: var(--text-primary); stroke-width: 1.5; fill: none; } -``` - -## Energy Profile Diagram - -### Axes - -```xml -<!-- Y-axis (Energy) --> -<line x1="0" y1="280" x2="0" y2="0" class="axis" marker-end="url(#straight-arrow)"/> -<text class="t" x="-15" y="-10" text-anchor="middle" transform="rotate(-90 -15 140)">Potential Energy</text> - -<!-- X-axis (Reaction Coordinate) --> -<line x1="0" y1="280" x2="600" y2="280" class="axis" marker-end="url(#straight-arrow)"/> -<text class="t" x="580" y="305" text-anchor="middle">Reaction Coordinate</text> -``` - -### Energy Curve - -```xml -<!-- Filled area under curve --> -<path class="energy-fill" d=" - M 40,200 - Q 150,200 250,50 - Q 350,200 500,220 - L 500,280 L 40,280 Z -"/> - -<!-- Curve line --> -<path class="energy-curve" d=" - M 40,200 - Q 100,200 150,150 - Q 200,80 250,50 - Q 300,80 350,150 - Q 400,210 500,220 -"/> -``` - -```css -.energy-curve { stroke: #534AB7; stroke-width: 2.5; fill: none; } -.energy-fill { fill: rgba(83, 74, 183, 0.1); } -``` - -### Energy Levels and Annotations - -```xml -<!-- Reactants level --> -<line x1="20" y1="200" x2="80" y2="200" stroke="#3B6D11" stroke-width="2"/> -<text class="ts" x="50" y="218" text-anchor="middle">Reactants</text> - -<!-- Transition state peak --> -<circle cx="250" cy="50" r="5" fill="#534AB7"/> -<line x1="250" y1="50" x2="250" y2="280" class="energy-level"/> -<text class="ts" x="250" y="30" text-anchor="middle" fill="#534AB7" font-weight="500">Transition State [‡]</text> - -<!-- Products level (lower = exergonic) --> -<line x1="470" y1="220" x2="530" y2="220" stroke="#3B6D11" stroke-width="2"/> - -<!-- Activation energy arrow --> -<line x1="100" y1="200" x2="100" y2="55" class="delta-arrow" marker-end="url(#delta-arrow)"/> -<text class="ts" x="85" y="125" text-anchor="end" fill="#3B6D11">E<tspan baseline-shift="sub" font-size="8">a</tspan></text> -``` - -```css -.energy-level { stroke: var(--text-secondary); stroke-width: 1; stroke-dasharray: 4 2; fill: none; } -.delta-arrow { stroke: #3B6D11; stroke-width: 1.5; fill: none; } -.delta-fill { fill: #3B6D11; } -``` - -## Chemistry Text Styles - -```css -/* Chemistry notation (serif font for formulas) */ -.chem { font-family: "Times New Roman", Georgia, serif; font-size: 16px; fill: var(--text-primary); } -.chem-sm { font-family: "Times New Roman", Georgia, serif; font-size: 12px; fill: var(--text-primary); } -.chem-lg { font-family: "Times New Roman", Georgia, serif; font-size: 18px; fill: var(--text-primary); } -``` - -## Subscript/Superscript in SVG - -```xml -<!-- Subscript using tspan --> -<text class="ts">E<tspan baseline-shift="sub" font-size="8">a</tspan></text> - -<!-- Superscript for charges --> -<text class="chem-sm">OH⁻</text> <!-- Using Unicode superscript minus --> -<text class="chem-sm">CH₃Br</text> <!-- Using Unicode subscript 3 --> -``` - -## Color Coding - -| Element | Color | Hex | -|---------|-------|-----| -| Carbon | Dark gray | #2C2C2A | -| Hydrogen | Light cream | #F1EFE8 | -| Oxygen | Red | #E24B4A | -| Bromine | Brown | #993C1D | -| Nitrogen | Blue | #378ADD | -| Electron arrows | Purple | #534AB7 | -| Positive charge | Green | #3B6D11 | -| Negative charge | Red | #A32D2D | - -## Layout Notes - -- **ViewBox**: 800×680 (landscape for mechanism + energy profile) -- **Mechanism section**: y=60-300, showing reactants → TS → products -- **Energy profile**: y=320-630, with axes and curve -- **Atom sizes**: C/O/Br ~12-16px radius, H ~7-8px radius -- **Bond lengths**: ~25-40px between atom centers -- **Spacing**: ~140px between mechanism stages - -## When to Use This Pattern - -Use this diagram style for: -- Organic reaction mechanisms (SN1, SN2, E1, E2, additions, eliminations) -- Reaction energy profiles and kinetics -- Stereochemistry illustrations -- Enzyme mechanism diagrams -- Transition state theory visualization -- Any chemistry concept requiring molecular structures diff --git a/optional-skills/creative/concept-diagrams/examples/wind-turbine-structure.md b/optional-skills/creative/concept-diagrams/examples/wind-turbine-structure.md deleted file mode 100644 index 795b040d1da..00000000000 --- a/optional-skills/creative/concept-diagrams/examples/wind-turbine-structure.md +++ /dev/null @@ -1,338 +0,0 @@ -# Modern Onshore Wind Turbine Structure - -A physical/structural cross-section diagram showing all major components of a modern wind turbine from underground foundation to blade tips. - -## Key Patterns Used - -- **Underground section**: Soil layers, deep concrete foundation with rebar reinforcement grid, spread footing -- **Cross-section view**: Tower wall thickness shown, internal components visible -- **Tapered tower**: Path elements creating realistic tower silhouette that narrows toward top -- **Internal access**: Ladder with rungs, elevator shaft inside tower -- **Cable routing**: Power cables running from nacelle down through tower to transformer -- **Nacelle cutaway**: Gearbox, generator, brake, yaw system all visible inside housing -- **Rotor assembly**: Hub with pitch motors at blade roots, three composite blades with gradient fill -- **Ground level marker**: Clear separation between above/below ground -- **Component color coding**: Each system type has distinct color (blue=generator, gold=gearbox, red=brake, green=yaw, purple=pitch) -- **Legend bar**: Quick reference for color meanings - -## Diagram - -```xml -<svg width="100%" viewBox="0 0 680 920" xmlns="http://www.w3.org/2000/svg"> - <defs> - <marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" - markerWidth="6" markerHeight="6" orient="auto-start-reverse"> - <path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" - stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> - </marker> - <!-- Blade gradient for 3D effect --> - <linearGradient id="bladeGrad" x1="0%" y1="0%" x2="100%" y2="0%"> - <stop offset="0%" style="stop-color:#D3D1C7"/> - <stop offset="50%" style="stop-color:#F1EFE8"/> - <stop offset="100%" style="stop-color:#B4B2A9"/> - </linearGradient> - </defs> - - <!-- ===== GROUND LEVEL LINE ===== --> - <line x1="40" y1="680" x2="640" y2="680" stroke="#3B6D11" stroke-width="2"/> - <text class="tl" x="45" y="675">Ground level</text> - - <!-- ===== UNDERGROUND: FOUNDATION ===== --> - - <!-- Soil layers --> - <rect x="120" y="680" width="300" height="180" class="soil"/> - <rect x="120" y="780" width="300" height="80" class="soil-dark"/> - - <!-- Deep concrete foundation --> - <path d="M170 680 L170 820 L200 850 L340 850 L370 820 L370 680 Z" class="concrete"/> - <!-- Foundation base spread --> - <path d="M140 820 L170 820 L200 850 L340 850 L370 820 L400 820 L400 860 L140 860 Z" class="concrete-dark"/> - - <!-- Rebar reinforcement --> - <g class="rebar"> - <line x1="185" y1="700" x2="185" y2="840"/> - <line x1="210" y1="700" x2="210" y2="845"/> - <line x1="235" y1="700" x2="235" y2="848"/> - <line x1="260" y1="700" x2="260" y2="848"/> - <line x1="285" y1="700" x2="285" y2="848"/> - <line x1="310" y1="700" x2="310" y2="845"/> - <line x1="335" y1="700" x2="335" y2="840"/> - <!-- Horizontal rebar --> - <line x1="175" y1="720" x2="365" y2="720"/> - <line x1="175" y1="760" x2="365" y2="760"/> - <line x1="175" y1="800" x2="365" y2="800"/> - <line x1="155" y1="835" x2="385" y2="835"/> - </g> - - <!-- Foundation labels --> - <line x1="410" y1="770" x2="480" y2="770" class="leader"/> - <text class="ts" x="485" y="766">Deep concrete foundation</text> - <text class="tl" x="485" y="778">Reinforced with steel rebar</text> - <text class="tl" x="485" y="790">15-25m deep typical</text> - - <line x1="400" y1="850" x2="480" y2="870" class="leader"/> - <text class="ts" x="485" y="866">Foundation spread footing</text> - <text class="tl" x="485" y="878">Distributes load to soil</text> - - <!-- ===== TOWER BASE ===== --> - - <!-- Tower base flange --> - <ellipse cx="270" cy="680" rx="70" ry="12" class="concrete-dark"/> - <rect x="200" y="668" width="140" height="12" class="tower"/> - - <!-- Transformer at base --> - <g transform="translate(470, 640)"> - <rect x="0" y="0" width="50" height="40" rx="3" class="transformer"/> - <!-- Cooling fins --> - <rect x="52" y="5" width="4" height="30" class="transformer-fin"/> - <rect x="58" y="5" width="4" height="30" class="transformer-fin"/> - <rect x="64" y="5" width="4" height="30" class="transformer-fin"/> - <!-- Connection box --> - <rect x="10" y="-8" width="30" height="10" rx="2" class="transformer-fin"/> - </g> - <line x1="470" y1="660" x2="430" y2="640" class="leader"/> - <text class="ts" x="385" y="636" text-anchor="end">Transformer</text> - <text class="tl" x="385" y="648" text-anchor="end">Steps up voltage for grid</text> - - <!-- ===== TUBULAR STEEL TOWER ===== --> - - <!-- Tower outer shell (tapered) --> - <path d="M200 680 L220 200 L320 200 L340 680 Z" class="tower"/> - - <!-- Tower inner surface (cutaway) --> - <path d="M215 680 L232 210 L308 210 L325 680 Z" class="tower-inner"/> - - <!-- Tower section joints --> - <line x1="205" y1="550" x2="335" y2="550" class="tower-section"/> - <line x1="210" y1="420" x2="330" y2="420" class="tower-section"/> - <line x1="215" y1="300" x2="325" y2="300" class="tower-section"/> - - <!-- Internal ladder (left side) --> - <g transform="translate(225, 220)"> - <!-- Ladder rails --> - <line x1="0" y1="0" x2="8" y2="450" class="ladder"/> - <line x1="15" y1="0" x2="23" y2="450" class="ladder"/> - <!-- Rungs --> - <g class="ladder-rung"> - <line x1="1" y1="20" x2="22" y2="21"/> - <line x1="1" y1="50" x2="22" y2="52"/> - <line x1="2" y1="80" x2="22" y2="83"/> - <line x1="2" y1="110" x2="23" y2="114"/> - <line x1="2" y1="140" x2="23" y2="145"/> - <line x1="3" y1="170" x2="23" y2="176"/> - <line x1="3" y1="200" x2="24" y2="207"/> - <line x1="3" y1="230" x2="24" y2="238"/> - <line x1="4" y1="260" x2="24" y2="269"/> - <line x1="4" y1="290" x2="25" y2="300"/> - <line x1="4" y1="320" x2="25" y2="331"/> - <line x1="5" y1="350" x2="25" y2="362"/> - <line x1="5" y1="380" x2="26" y2="393"/> - <line x1="6" y1="410" x2="26" y2="424"/> - <line x1="6" y1="440" x2="27" y2="455"/> - </g> - </g> - - <!-- Elevator shaft (right side) --> - <rect x="280" y="230" width="25" height="430" rx="2" class="elevator"/> - <text class="tl" x="292" y="450" text-anchor="middle" transform="rotate(-90, 292, 450)" fill="#185FA5">ELEVATOR</text> - - <!-- Electrical cables running down --> - <path d="M270 220 C270 300 268 400 268 500 C268 600 268 650 310 665 L470 665" class="cable"/> - <path d="M260 225 C258 350 256 500 256 600 C256 650 256 670 256 680" class="cable-thin"/> - - <!-- Tower labels --> - <line x1="340" y1="350" x2="400" y2="320" class="leader"/> - <text class="ts" x="405" y="316">Tubular steel tower</text> - <text class="tl" x="405" y="328">80-120m height typical</text> - <text class="tl" x="405" y="340">Tapered for strength</text> - - <line x1="248" y1="400" x2="130" y2="380" class="leader"/> - <text class="ts" x="125" y="376" text-anchor="end">Internal ladder</text> - <text class="tl" x="125" y="388" text-anchor="end">Service access</text> - - <line x1="305" y1="500" x2="400" y2="520" class="leader"/> - <text class="ts" x="405" y="516">Service elevator</text> - - <line x1="268" y1="580" x2="130" y2="600" class="leader"/> - <text class="ts" x="125" y="596" text-anchor="end">Power cables</text> - <text class="tl" x="125" y="608" text-anchor="end">To transformer</text> - - <!-- ===== NACELLE ===== --> - - <g transform="translate(270, 160)"> - <!-- Nacelle base/bedplate --> - <rect x="-60" y="30" width="120" height="15" class="nacelle"/> - - <!-- Yaw bearing --> - <ellipse cx="0" cy="42" rx="35" ry="6" class="bearing"/> - - <!-- Yaw motors --> - <rect x="-55" y="32" width="12" height="18" rx="2" class="yaw"/> - <rect x="43" y="32" width="12" height="18" rx="2" class="yaw"/> - - <!-- Nacelle housing --> - <path d="M-65 30 L-70 -10 L-65 -35 L70 -35 L85 -10 L85 30 Z" class="nacelle-cover"/> - - <!-- Main shaft --> - <rect x="-90" y="-8" width="35" height="16" rx="2" fill="#888780" stroke="#5F5E5A" stroke-width="0.5"/> - - <!-- Gearbox --> - <rect x="-55" y="-25" width="40" height="45" rx="3" class="gearbox"/> - <text class="tl" x="-35" y="5" text-anchor="middle" fill="#633806">GEAR</text> - - <!-- Generator --> - <rect x="-10" y="-20" width="50" height="38" rx="4" class="generator"/> - <ellipse cx="15" cy="0" rx="15" ry="15" fill="none" stroke="#0C447C" stroke-width="1"/> - <text class="tl" x="15" y="4" text-anchor="middle" fill="#E6F1FB">GEN</text> - - <!-- Brake disc --> - <rect x="45" y="-12" width="8" height="24" rx="1" class="brake"/> - - <!-- Electrical cabinet --> - <rect x="58" y="-25" width="20" height="35" rx="2" fill="#5F5E5A" stroke="#444441" stroke-width="0.5"/> - - <!-- Anemometer on top --> - <line x1="60" y1="-35" x2="60" y2="-50" stroke="#5F5E5A" stroke-width="1"/> - <ellipse cx="60" cy="-52" rx="8" ry="3" fill="#D3D1C7" stroke="#888780" stroke-width="0.5"/> - </g> - - <!-- Nacelle labels --> - <line x1="215" y1="135" x2="130" y2="115" class="leader"/> - <text class="ts" x="125" y="111" text-anchor="end">Gearbox</text> - <text class="tl" x="125" y="123" text-anchor="end">Speed multiplier</text> - - <line x1="285" y1="145" x2="400" y2="125" class="leader"/> - <text class="ts" x="405" y="121">Generator</text> - <text class="tl" x="405" y="133">Converts rotation to electricity</text> - - <line x1="315" y1="155" x2="400" y2="165" class="leader"/> - <text class="ts" x="405" y="161">Brake system</text> - - <line x1="215" y1="200" x2="130" y2="220" class="leader"/> - <text class="ts" x="125" y="216" text-anchor="end">Yaw motors</text> - <text class="tl" x="125" y="228" text-anchor="end">Rotate nacelle to face wind</text> - - <line x1="330" y1="108" x2="400" y2="90" class="leader"/> - <text class="ts" x="405" y="86">Anemometer</text> - <text class="tl" x="405" y="98">Wind speed sensor</text> - - <!-- ===== ROTOR HUB & BLADES ===== --> - - <!-- Hub --> - <g transform="translate(180, 152)"> - <!-- Hub body --> - <ellipse cx="0" cy="0" rx="25" ry="30" class="hub"/> - <!-- Hub nose cone --> - <path d="M-25 -20 Q-50 0 -25 20 Q-30 0 -25 -20" class="hub-cap"/> - - <!-- Blade roots with pitch motors --> - <!-- Blade 1 (up) --> - <g transform="translate(-10, -25) rotate(-80)"> - <ellipse cx="0" cy="0" rx="12" ry="8" class="blade-root"/> - <rect x="-8" y="-5" width="10" height="10" rx="2" class="pitch-motor"/> - </g> - - <!-- Blade 2 (lower left) --> - <g transform="translate(-18, 18) rotate(40)"> - <ellipse cx="0" cy="0" rx="12" ry="8" class="blade-root"/> - <rect x="-8" y="-5" width="10" height="10" rx="2" class="pitch-motor"/> - </g> - - <!-- Blade 3 (lower right) --> - <g transform="translate(5, 22) rotate(160)"> - <ellipse cx="0" cy="0" rx="12" ry="8" class="blade-root"/> - <rect x="-8" y="-5" width="10" height="10" rx="2" class="pitch-motor"/> - </g> - </g> - - <!-- Blade 1 (pointing up-left) --> - <path d="M165 125 Q140 80 130 40 Q125 20 115 15 Q110 18 112 25 Q115 50 125 90 Q140 120 158 128 Z" class="blade" fill="url(#bladeGrad)"/> - - <!-- Blade 2 (pointing down-left) --> - <path d="M158 175 Q120 200 80 230 Q60 245 55 255 Q60 258 68 252 Q95 235 130 210 Q155 190 163 178 Z" class="blade" fill="url(#bladeGrad)"/> - - <!-- Blade 3 (pointing down-right, partially visible) --> - <path d="M188 175 Q195 200 205 230 Q210 250 215 255 Q220 252 218 245 Q212 220 202 195 Q192 175 186 172 Z" class="blade" fill="url(#bladeGrad)"/> - - <!-- Blade labels --> - <line x1="115" y1="35" x2="60" y2="35" class="leader"/> - <text class="ts" x="55" y="31" text-anchor="end">Composite blade</text> - <text class="tl" x="55" y="43" text-anchor="end">Fiberglass/carbon fiber</text> - <text class="tl" x="55" y="55" text-anchor="end">40-80m length each</text> - - <line x1="170" y1="130" x2="130" y2="155" class="leader"/> - <text class="ts" x="85" y="151" text-anchor="end">Pitch motor</text> - <text class="tl" x="85" y="163" text-anchor="end">Adjusts blade angle</text> - - <line x1="180" y1="152" x2="130" y2="180" class="leader"/> - <text class="ts" x="85" y="183" text-anchor="end">Rotor hub</text> - - <!-- ===== LEGEND ===== --> - <g transform="translate(40, 895)"> - <rect x="0" y="-15" width="600" height="30" rx="4" fill="none" stroke="#D3D1C7" stroke-width="0.5"/> - - <rect x="15" y="-5" width="12" height="12" rx="2" class="generator"/> - <text class="tl" x="32" y="5">Generator</text> - - <rect x="95" y="-5" width="12" height="12" rx="2" class="gearbox"/> - <text class="tl" x="112" y="5">Gearbox</text> - - <rect x="170" y="-5" width="12" height="12" rx="2" class="brake"/> - <text class="tl" x="187" y="5">Brake</text> - - <rect x="230" y="-5" width="12" height="12" rx="2" class="yaw"/> - <text class="tl" x="247" y="5">Yaw system</text> - - <rect x="320" y="-5" width="12" height="12" rx="2" class="pitch-motor"/> - <text class="tl" x="337" y="5">Pitch motor</text> - - <line x1="415" y1="1" x2="435" y2="1" class="cable" style="stroke-width:2"/> - <text class="tl" x="440" y="5">Power cable</text> - - <rect x="515" y="-5" width="12" height="12" rx="2" class="transformer"/> - <text class="tl" x="532" y="5">Transformer</text> - </g> - -</svg> -``` - -## CSS Classes - -```css -/* Foundation */ -.concrete { fill: #B4B2A9; stroke: #5F5E5A; stroke-width: 1; } -.concrete-dark { fill: #888780; stroke: #5F5E5A; stroke-width: 1; } -.rebar { stroke: #854F0B; stroke-width: 1.5; fill: none; } -.soil { fill: #8B7355; stroke: #5F5E5A; stroke-width: 0.5; } -.soil-dark { fill: #6B5344; } - -/* Tower */ -.tower { fill: #F1EFE8; stroke: #5F5E5A; stroke-width: 1; } -.tower-inner { fill: #D3D1C7; stroke: #888780; stroke-width: 0.5; } -.tower-section { stroke: #888780; stroke-width: 0.5; stroke-dasharray: 2 4; } -.ladder { stroke: #5F5E5A; stroke-width: 1; fill: none; } -.ladder-rung { stroke: #888780; stroke-width: 0.8; } -.elevator { fill: #E6F1FB; stroke: #185FA5; stroke-width: 0.5; } -.cable { stroke: #E24B4A; stroke-width: 2; fill: none; } -.cable-thin { stroke: #E24B4A; stroke-width: 1.5; fill: none; } - -/* Nacelle */ -.nacelle { fill: #F1EFE8; stroke: #5F5E5A; stroke-width: 1; } -.nacelle-cover { fill: #D3D1C7; stroke: #5F5E5A; stroke-width: 1; } -.gearbox { fill: #BA7517; stroke: #633806; stroke-width: 0.5; } -.generator { fill: #378ADD; stroke: #0C447C; stroke-width: 0.5; } -.brake { fill: #E24B4A; stroke: #791F1F; stroke-width: 0.5; } -.yaw { fill: #5DCAA5; stroke: #085041; stroke-width: 0.5; } -.bearing { fill: #444441; stroke: #2C2C2A; stroke-width: 0.5; } - -/* Rotor */ -.hub { fill: #D3D1C7; stroke: #5F5E5A; stroke-width: 1; } -.hub-cap { fill: #F1EFE8; stroke: #5F5E5A; stroke-width: 1; } -.blade { fill: #F1EFE8; stroke: #888780; stroke-width: 1; } -.blade-root { fill: #D3D1C7; stroke: #5F5E5A; stroke-width: 0.5; } -.pitch-motor { fill: #7F77DD; stroke: #3C3489; stroke-width: 0.5; } - -/* Transformer */ -.transformer { fill: #27500A; stroke: #173404; stroke-width: 1; } -.transformer-fin { fill: #3B6D11; stroke: #27500A; stroke-width: 0.5; } -``` diff --git a/optional-skills/creative/concept-diagrams/references/dashboard-patterns.md b/optional-skills/creative/concept-diagrams/references/dashboard-patterns.md deleted file mode 100644 index 528f185ea7f..00000000000 --- a/optional-skills/creative/concept-diagrams/references/dashboard-patterns.md +++ /dev/null @@ -1,43 +0,0 @@ -# Dashboard Patterns - -Building blocks for UI/dashboard mockups inside a concept diagram — admin panels, monitoring dashboards, control interfaces, status displays. - -## Pattern - -A "screen" is a rounded dark rect inside a lighter "frame" rect, with chart/gauge/indicator elements nested on top. - -```xml -<!-- Monitor frame --> -<rect class="dashboard" x="0" y="0" width="200" height="120" rx="8"/> -<!-- Screen --> -<rect class="screen" x="10" y="10" width="180" height="85" rx="4"/> -<!-- Mini bar chart --> -<rect class="screen-content" x="18" y="18" width="50" height="35" rx="2"/> -<rect class="screen-chart" x="22" y="38" width="8" height="12"/> -<rect class="screen-chart" x="33" y="32" width="8" height="18"/> -<!-- Gauge --> -<circle class="screen-bar" cx="100" cy="35" r="12"/> -<text x="100" y="39" text-anchor="middle" fill="#E8E6DE" style="font-size:8px">78%</text> -<!-- Status indicators --> -<circle cx="35" cy="74" r="6" fill="#97C459"/> <!-- green = ok --> -<circle cx="75" cy="74" r="6" fill="#EF9F27"/> <!-- amber = warning --> -<circle cx="115" cy="74" r="6" fill="#E24B4A"/> <!-- red = alert --> -``` - -## CSS - -```css -.dashboard { fill: #F1EFE8; stroke: #5F5E5A; stroke-width: 1.5; } -.screen { fill: #1a1a18; } -.screen-content { fill: #2C2C2A; } -.screen-chart { fill: #5DCAA5; } -.screen-bar { fill: #7F77DD; } -.screen-alert { fill: #E24B4A; } -``` - -## Tips - -- Dashboard screens stay dark in both light and dark mode — they represent actual monitor glass. -- Keep on-screen text small (`font-size:8px` or `10px`) and high-contrast (near-white fill on dark). -- Use the status triad green/amber/red consistently — OK / warning / alert. -- A single dashboard usually sits on top of an infrastructure hub diagram as a unified view (see `examples/smart-city-infrastructure.md`). diff --git a/optional-skills/creative/concept-diagrams/references/infrastructure-patterns.md b/optional-skills/creative/concept-diagrams/references/infrastructure-patterns.md deleted file mode 100644 index 82c070e57fa..00000000000 --- a/optional-skills/creative/concept-diagrams/references/infrastructure-patterns.md +++ /dev/null @@ -1,144 +0,0 @@ -# Infrastructure Patterns - -Reusable shapes and line styles for infrastructure / systems-integration diagrams (smart cities, IoT networks, industrial systems, multi-domain architectures). - -## Layout pattern: hub-spoke - -- **Central hub**: Hexagon or circle representing the integration platform -- **Radiating connections**: Data lines from hub to each subsystem with connection dots -- **Subsystem sections**: Each system (power, water, transport) in its own region -- **Dashboard on top**: Optional UI mockup showing a unified view (see `dashboard-patterns.md`) - -```xml -<!-- Central hub (hexagon) --> -<polygon class="iot-hex" points="0,-45 39,-22 39,22 0,45 -39,22 -39,-22"/> - -<!-- Data lines with connection dots --> -<path class="data-line" d="M 321 248 L 200 248 L 120 380" stroke-dasharray="4 3"/> -<circle cx="321" cy="248" r="4" fill="#7F77DD"/> -``` - -## Semantic line styles - -Use a dedicated CSS class per subsystem so every diagram reads the same way: - -```css -.data-line { stroke: #7F77DD; stroke-width: 2; fill: none; stroke-dasharray: 4 3; } -.power-line { stroke: #EF9F27; stroke-width: 2; fill: none; } -.water-pipe { stroke: #378ADD; stroke-width: 4; stroke-linecap: round; fill: none; } -.road { stroke: #888780; stroke-width: 8; stroke-linecap: round; fill: none; } -``` - -## Power systems - -**Solar panel (angled):** -```xml -<polygon class="solar-panel" points="0,25 35,8 38,12 3,29"/> -<line class="solar-frame" x1="12" y1="22" x2="24" y2="13"/> -``` - -**Wind turbine:** -```xml -<polygon class="wind-tower" points="20,70 30,70 28,25 22,25"/> -<circle class="wind-hub" cx="25" cy="18" r="5"/> -<ellipse class="wind-blade" cx="25" cy="5" rx="3" ry="13"/> -<ellipse class="wind-blade" cx="14" cy="26" rx="3" ry="13" transform="rotate(-120, 25, 18)"/> -<ellipse class="wind-blade" cx="36" cy="26" rx="3" ry="13" transform="rotate(120, 25, 18)"/> -``` - -**Battery with charge level:** -```xml -<rect class="battery" x="0" y="0" width="45" height="65" rx="5"/> -<rect x="10" y="-6" width="10" height="8" rx="2" fill="#27500A"/> <!-- terminal --> -<rect class="battery-level" x="5" y="12" width="35" height="48" rx="3"/> <!-- fill level --> -``` - -**Power pylon:** -```xml -<polygon class="pylon" points="30,0 35,0 40,60 25,60"/> -<line x1="15" y1="10" x2="45" y2="10" stroke="#5F5E5A" stroke-width="3"/> -<circle cx="18" cy="10" r="3" fill="#FAEEDA" stroke="#854F0B"/> <!-- insulator --> -``` - -## Water systems - -**Reservoir/dam:** -```xml -<polygon class="reservoir-wall" points="0,60 10,0 70,0 80,60"/> -<polygon class="water" points="12,10 68,10 68,55 75,55 75,58 5,58 5,55 12,55"/> -<!-- Wave effect --> -<path d="M 15 25 Q 25 22 35 25 Q 45 28 55 25" fill="none" stroke="#378ADD" opacity="0.5"/> -``` - -**Treatment tank:** -```xml -<ellipse class="treatment-tank" cx="35" cy="45" rx="30" ry="18"/> -<rect class="treatment-tank" x="5" y="20" width="60" height="25"/> -<!-- Bubbles --> -<circle cx="20" cy="32" r="2" fill="#378ADD" opacity="0.6"/> -``` - -**Pipe with joint and valve:** -```xml -<path class="pipe" d="M 80 85 L 110 85"/> -<circle class="pipe-joint" cx="110" cy="85" r="8"/> -<circle class="valve" cx="95" cy="85" r="6"/> -``` - -## Transport systems - -**Road with lane markings:** -```xml -<line class="road" x1="0" y1="50" x2="170" y2="50"/> -<line class="road-mark" x1="10" y1="50" x2="160" y2="50"/> -``` - -**Traffic light:** -```xml -<rect class="traffic-light" x="0" y="0" width="14" height="32" rx="3"/> -<circle class="light-red" cx="7" cy="8" r="4"/> -<circle class="light-off" cx="7" cy="16" r="4"/> -<circle class="light-green" cx="7" cy="24" r="4"/> -``` - -**Bus:** -```xml -<rect class="bus" x="0" y="0" width="55" height="28" rx="6"/> -<rect class="bus-window" x="5" y="5" width="12" height="12" rx="2"/> -<circle cx="14" cy="30" r="6" fill="#2C2C2A"/> <!-- wheel --> -<circle cx="14" cy="30" r="3" fill="#5F5E5A"/> <!-- hubcap --> -``` - -## Full CSS block (add to the host page or inline <style>) - -```css -/* Power */ -.solar-panel { fill: #3C3489; stroke: #534AB7; stroke-width: 0.5; } -.wind-tower { fill: #B4B2A9; stroke: #5F5E5A; stroke-width: 1; } -.wind-blade { fill: #F1EFE8; stroke: #888780; stroke-width: 0.5; } -.battery { fill: #27500A; stroke: #3B6D11; stroke-width: 1.5; } -.battery-level { fill: #97C459; } -.power-line { stroke: #EF9F27; stroke-width: 2; fill: none; } - -/* Water */ -.reservoir-wall { fill: #B4B2A9; stroke: #5F5E5A; stroke-width: 1; } -.water { fill: #85B7EB; stroke: #378ADD; stroke-width: 0.5; } -.pipe { fill: none; stroke: #378ADD; stroke-width: 4; stroke-linecap: round; } -.pipe-joint { fill: #185FA5; stroke: #0C447C; stroke-width: 1; } -.valve { fill: #0C447C; stroke: #185FA5; stroke-width: 1; } - -/* Transport */ -.road { stroke: #888780; stroke-width: 8; fill: none; stroke-linecap: round; } -.road-mark { stroke: #F1EFE8; stroke-width: 1; stroke-dasharray: 6 4; fill: none; } -.traffic-light { fill: #444441; stroke: #2C2C2A; stroke-width: 0.5; } -.light-red { fill: #E24B4A; } -.light-green { fill: #97C459; } -.light-off { fill: #2C2C2A; } -.bus { fill: #E1F5EE; stroke: #0F6E56; stroke-width: 1.5; } -``` - -## Reference examples - -- `examples/smart-city-infrastructure.md` — hub-spoke with multiple subsystems -- `examples/electricity-grid-flow.md` — voltage hierarchy, flow markers -- `examples/wind-turbine-structure.md` — cross-section with legend diff --git a/optional-skills/creative/concept-diagrams/references/physical-shape-cookbook.md b/optional-skills/creative/concept-diagrams/references/physical-shape-cookbook.md deleted file mode 100644 index 1a999203f07..00000000000 --- a/optional-skills/creative/concept-diagrams/references/physical-shape-cookbook.md +++ /dev/null @@ -1,42 +0,0 @@ -# Physical Shape Cookbook - -Guidance for drawing physical objects (vehicles, buildings, hardware, mechanical systems, anatomy) — when rectangles aren't enough. - -## Shape selection - -| Physical form | SVG element | Example use | -|---------------|-------------|-------------| -| Curved bodies | `<path>` with Q/C curves | Fuselage, tanks, pipes | -| Tapered/angular shapes | `<polygon>` | Wings, fins, wedges | -| Cylindrical/round | `<ellipse>`, `<circle>` | Engines, wheels, buttons | -| Linear structures | `<line>` | Struts, beams, connections | -| Internal sections | `<rect>` inside parent | Compartments, rooms | -| Dashed boundaries | `stroke-dasharray` | Hidden parts, fuel tanks | - -## Layering approach - -1. Draw outer structure first (fuselage, frame, hull) -2. Add internal sections on top (cabins, compartments) -3. Add detail elements (engines, wheels, controls) -4. Add leader lines with labels - -## Semantic CSS classes (instead of c-* ramps) - -For physical diagrams, define component-specific classes directly rather than applying `c-*` color classes. This makes each part self-documenting and lets you keep a restrained palette: - -```css -.fuselage { fill: #F1EFE8; stroke: #5F5E5A; stroke-width: 1; } -.wing { fill: #E6F1FB; stroke: #185FA5; stroke-width: 1; } -.engine { fill: #FAECE7; stroke: #993C1D; stroke-width: 1; } -``` - -Add these to a local `<style>` inside the SVG (or extend the host page's `<style>` block). The light-mode/dark-mode pattern still works — use the CSS variables from the template (`var(--bg-secondary)`, `var(--border)`, `var(--text-primary)`) if you want dark-mode awareness. - -## Reference examples - -Look at these example files for working physical-diagram patterns: - -- `examples/commercial-aircraft-structure.md` — fuselage curves + tapered wings + ellipse engines -- `examples/wind-turbine-structure.md` — underground foundation, tubular tower, nacelle cutaway -- `examples/smartphone-layer-anatomy.md` — exploded-view stack with alternating labels -- `examples/apartment-floor-plan-conversion.md` — walls, doors, windows, proposed changes diff --git a/optional-skills/creative/concept-diagrams/templates/template.html b/optional-skills/creative/concept-diagrams/templates/template.html deleted file mode 100644 index 2b48e08d166..00000000000 --- a/optional-skills/creative/concept-diagrams/templates/template.html +++ /dev/null @@ -1,174 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> -<head> -<meta charset="UTF-8"> -<meta name="viewport" content="width=device-width, initial-scale=1.0"> -<title>Concept Diagram - - - -
-

-

- -
- - diff --git a/optional-skills/creative/kanban-video-orchestrator/SKILL.md b/optional-skills/creative/kanban-video-orchestrator/SKILL.md index c5ac2a8c96e..f323406300b 100644 --- a/optional-skills/creative/kanban-video-orchestrator/SKILL.md +++ b/optional-skills/creative/kanban-video-orchestrator/SKILL.md @@ -8,7 +8,7 @@ platforms: [linux, macos, windows] metadata: hermes: tags: [video, kanban, multi-agent, orchestration, production-pipeline] - related_skills: [kanban-orchestrator, kanban-worker, ascii-video, manim-video, p5js, comfyui, touchdesigner-mcp, blender-mcp, pixel-art, ascii-art, songwriting-and-ai-music, heartmula, songsee, spotify, youtube-content, claude-design, excalidraw, architecture-diagram, concept-diagrams, baoyu-comic, baoyu-infographic, humanizer, gif-search, meme-generation] + related_skills: [kanban-orchestrator, kanban-worker, ascii-video, manim-video, p5js, comfyui, touchdesigner-mcp, blender-mcp, pixel-art, ascii-art, songwriting-and-ai-music, heartmula, songsee, spotify, youtube-content, claude-design, excalidraw, html-artifact, baoyu-comic, baoyu-infographic, humanizer, gif-search, meme-generation] credits: | The single-project workspace layout, profile-config patching pattern, SOUL.md-per-profile model, TEAM.md task-graph convention, and diff --git a/optional-skills/creative/kanban-video-orchestrator/references/intake.md b/optional-skills/creative/kanban-video-orchestrator/references/intake.md index d290b606f49..1f817da020b 100644 --- a/optional-skills/creative/kanban-video-orchestrator/references/intake.md +++ b/optional-skills/creative/kanban-video-orchestrator/references/intake.md @@ -96,8 +96,7 @@ texture inside the final scene. - **Terminal-only or with GUI?** - **Voiceover for narration?** - **Diagram support needed?** — Often these benefit from a diagram skill - alongside the screen-capture/render step (`excalidraw`, - `architecture-diagram`, `concept-diagrams`) + alongside the screen-capture/render step (`excalidraw`, `html-artifact`) ### ASCII / terminal art diff --git a/optional-skills/creative/kanban-video-orchestrator/references/role-archetypes.md b/optional-skills/creative/kanban-video-orchestrator/references/role-archetypes.md index 95eaeb33b66..c5e15c06f4b 100644 --- a/optional-skills/creative/kanban-video-orchestrator/references/role-archetypes.md +++ b/optional-skills/creative/kanban-video-orchestrator/references/role-archetypes.md @@ -59,7 +59,7 @@ local skills. - **Toolsets:** kanban, terminal, file - **Skills:** `kanban-worker` plus any project-specific design skill — - `claude-design` (UI/web), `sketch` (quick mockup variants), + `claude-design` (UI/web), `html-artifact` (quick mockup variants, explainers, diagrams), `popular-web-designs` (matching known web aesthetic), `pixel-art` (retro), `ascii-art` (terminal/retro), `excalidraw` (hand-drawn frames), `design-md` (text-based design docs) @@ -72,8 +72,7 @@ film and music video. Often pairs with a diagramming tool. - **Toolsets:** kanban, file - **Skills:** `kanban-worker` plus a diagram skill — `excalidraw` (sketch), - `architecture-diagram` (technical/system), `concept-diagrams` (educational/ - scientific) + `html-artifact` (technical/system + educational/scientific diagrams) - **Outputs:** `storyboard.md` with one row per scene/shot, optional storyboard sketches diff --git a/optional-skills/creative/kanban-video-orchestrator/references/tool-matrix.md b/optional-skills/creative/kanban-video-orchestrator/references/tool-matrix.md index b5e59c31478..2f27ffc41e7 100644 --- a/optional-skills/creative/kanban-video-orchestrator/references/tool-matrix.md +++ b/optional-skills/creative/kanban-video-orchestrator/references/tool-matrix.md @@ -30,10 +30,8 @@ called from the terminal toolset; they don't appear in `always_load`. | `claude-design` | Design one-off HTML artifacts (landing, deck, prototype) | Concept artist for product video style frames; storyboarder for UI-heavy content | | `design-md` | Design markdown docs | Concept artist documenting visual specs | | `popular-web-designs` | Reference patterns for popular web designs | Concept artist; cinematographer when matching a known UI aesthetic | -| `sketch` | Throwaway HTML mockups (2-3 design variants to compare) | Concept artist exploring directions; storyboarder for UI flows | | `excalidraw` | Excalidraw-style hand-drawn diagrams | Storyboarder; concept artist for sketch-style frames | -| `architecture-diagram` | Software architecture diagrams | Storyboarder for technical content; explainer scenes about systems | -| `concept-diagrams` *(optional)* | Flat, minimal SVG diagrams (educational visual language; physics, chemistry, math, anatomy, etc.) | Renderer / storyboarder for explainer scenes with clean educational diagrams | +| `html-artifact` | Self-contained HTML artifacts: throwaway mockup variants, explainers, dark-tech architecture + educational SVG diagrams | Concept artist exploring directions; storyboarder for UI flows + technical/educational explainer scenes | | `pretext` | Mathematical/scientific content authoring | Writer / cinematographer for technical-explainer pretexts | | `creative-ideation` | Constraint-driven project ideation | Director / cinematographer when the brief is wide-open and needs framing | | `humanizer` | Strip AI-isms from text, add real voice | Writer / copywriter post-process to avoid AI-tells in scripts and VO copy | diff --git a/skills/creative/architecture-diagram/SKILL.md b/skills/creative/architecture-diagram/SKILL.md deleted file mode 100644 index 2c813c53c13..00000000000 --- a/skills/creative/architecture-diagram/SKILL.md +++ /dev/null @@ -1,148 +0,0 @@ ---- -name: architecture-diagram -description: "Dark-themed SVG architecture/cloud/infra diagrams as HTML." -version: 1.0.0 -author: Cocoon AI (hello@cocoon-ai.com), ported by Hermes Agent -license: MIT -dependencies: [] -platforms: [linux, macos, windows] -metadata: - hermes: - tags: [architecture, diagrams, SVG, HTML, visualization, infrastructure, cloud] - related_skills: [concept-diagrams, excalidraw] ---- - -# Architecture Diagram Skill - -Generate professional, dark-themed technical architecture diagrams as standalone HTML files with inline SVG graphics. No external tools, no API keys, no rendering libraries — just write the HTML file and open it in a browser. - -## Scope - -**Best suited for:** -- Software system architecture (frontend / backend / database layers) -- Cloud infrastructure (VPC, regions, subnets, managed services) -- Microservice / service-mesh topology -- Database + API map, deployment diagrams -- Anything with a tech-infra subject that fits a dark, grid-backed aesthetic - -**Look elsewhere first for:** -- Physics, chemistry, math, biology, or other scientific subjects -- Physical objects (vehicles, hardware, anatomy, cross-sections) -- Floor plans, narrative journeys, educational / textbook-style visuals -- Hand-drawn whiteboard sketches (consider `excalidraw`) -- Animated explainers (consider an animation skill) - -If a more specialized skill is available for the subject, prefer that. If none fits, this skill can also serve as a general SVG diagram fallback — the output will just carry the dark tech aesthetic described below. - -Based on [Cocoon AI's architecture-diagram-generator](https://github.com/Cocoon-AI/architecture-diagram-generator) (MIT). - -## Workflow - -1. User describes their system architecture (components, connections, technologies) -2. Generate the HTML file following the design system below -3. Save with `write_file` to a `.html` file (e.g. `~/architecture-diagram.html`) -4. User opens in any browser — works offline, no dependencies - -### Output Location - -Save diagrams to a user-specified path, or default to the current working directory: -``` -./[project-name]-architecture.html -``` - -### Preview - -After saving, suggest the user open it: -```bash -# macOS -open ./my-architecture.html -# Linux -xdg-open ./my-architecture.html -``` - -## Design System & Visual Language - -### Color Palette (Semantic Mapping) - -Use specific `rgba` fills and hex strokes to categorize components: - -| Component Type | Fill (rgba) | Stroke (Hex) | -| :--- | :--- | :--- | -| **Frontend** | `rgba(8, 51, 68, 0.4)` | `#22d3ee` (cyan-400) | -| **Backend** | `rgba(6, 78, 59, 0.4)` | `#34d399` (emerald-400) | -| **Database** | `rgba(76, 29, 149, 0.4)` | `#a78bfa` (violet-400) | -| **AWS/Cloud** | `rgba(120, 53, 15, 0.3)` | `#fbbf24` (amber-400) | -| **Security** | `rgba(136, 19, 55, 0.4)` | `#fb7185` (rose-400) | -| **Message Bus** | `rgba(251, 146, 60, 0.3)` | `#fb923c` (orange-400) | -| **External** | `rgba(30, 41, 59, 0.5)` | `#94a3b8` (slate-400) | - -### Typography & Background -- **Font:** JetBrains Mono (Monospace), loaded from Google Fonts -- **Sizes:** 12px (Names), 9px (Sublabels), 8px (Annotations), 7px (Tiny labels) -- **Background:** Slate-950 (`#020617`) with a subtle 40px grid pattern - -```svg - - - - -``` - -## Technical Implementation Details - -### Component Rendering -Components are rounded rectangles (`rx="6"`) with 1.5px strokes. To prevent arrows from showing through semi-transparent fills, use a **double-rect masking technique**: -1. Draw an opaque background rect (`#0f172a`) -2. Draw the semi-transparent styled rect on top - -### Connection Rules -- **Z-Order:** Draw arrows *early* in the SVG (after the grid) so they render behind component boxes -- **Arrowheads:** Defined via SVG markers -- **Security Flows:** Use dashed lines in rose color (`#fb7185`) -- **Boundaries:** - - *Security Groups:* Dashed (`4,4`), rose color - - *Regions:* Large dashed (`8,4`), amber color, `rx="12"` - -### Spacing & Layout Logic -- **Standard Height:** 60px (Services); 80-120px (Large components) -- **Vertical Gap:** Minimum 40px between components -- **Message Buses:** Must be placed *in the gap* between services, not overlapping them -- **Legend Placement:** **CRITICAL.** Must be placed outside all boundary boxes. Calculate the lowest Y-coordinate of all boundaries and place the legend at least 20px below it. - -## Document Structure - -The generated HTML file follows a four-part layout: -1. **Header:** Title with a pulsing dot indicator and subtitle -2. **Main SVG:** The diagram contained within a rounded border card -3. **Summary Cards:** A grid of three cards below the diagram for high-level details -4. **Footer:** Minimal metadata - -### Info Card Pattern -```html -
-
-
-

Title

-
- -
-``` - -## Output Requirements -- **Single File:** One self-contained `.html` file -- **No External Dependencies:** All CSS and SVG must be inline (except Google Fonts) -- **No JavaScript:** Use pure CSS for any animations (like pulsing dots) -- **Compatibility:** Must render correctly in any modern web browser - -## Template Reference - -Load the full HTML template for the exact structure, CSS, and SVG component examples: - -``` -skill_view(name="architecture-diagram", file_path="templates/template.html") -``` - -The template contains working examples of every component type (frontend, backend, database, cloud, security), arrow styles (standard, dashed, curved), security groups, region boundaries, and the legend — use it as your structural reference when generating diagrams. diff --git a/skills/creative/architecture-diagram/templates/template.html b/skills/creative/architecture-diagram/templates/template.html deleted file mode 100644 index f5b32fbe7fd..00000000000 --- a/skills/creative/architecture-diagram/templates/template.html +++ /dev/null @@ -1,319 +0,0 @@ - - - - - - [PROJECT NAME] Architecture Diagram - - - - -
- -
-
-
-

[PROJECT NAME] Architecture

-
-

[Subtitle description]

-
- - -
- - - - - - - - - - - - - - - - - - - Users - Browser/Mobile - - - - Auth Provider - OAuth 2.0 - - - - AWS Region: us-west-2 - - - - CloudFront - CDN - - - - S3 Buckets - • bucket-one - • bucket-two - • bucket-three - OAI Protected - - - - sg-name :port - - - - Load Balancer - HTTPS :443 - - - - API Server - FastAPI :8000 - - - - Database - PostgreSQL - - - - Frontend - React + TypeScript - Additional detail - More info - domain.example.com - - - - - - HTTPS - - - - - - - OAI - - - - - TLS - - - - JWT + PKCE - - - Legend - - - Frontend - - - Backend - - - Cloud Service - - - Database - - - Security - - - Auth Flow - - - Security Group - -
- - -
-
-
-
-

Card Title 1

-
-
    -
  • • Item one
  • -
  • • Item two
  • -
  • • Item three
  • -
  • • Item four
  • -
-
- -
-
-
-

Card Title 2

-
-
    -
  • • Item one
  • -
  • • Item two
  • -
  • • Item three
  • -
  • • Item four
  • -
-
- -
-
-
-

Card Title 3

-
-
    -
  • • Item one
  • -
  • • Item two
  • -
  • • Item three
  • -
  • • Item four
  • -
-
-
- - - -
- - diff --git a/skills/creative/claude-design/SKILL.md b/skills/creative/claude-design/SKILL.md index 673d1ff827a..d61dbcb2f00 100644 --- a/skills/creative/claude-design/SKILL.md +++ b/skills/creative/claude-design/SKILL.md @@ -8,7 +8,7 @@ platforms: [linux, macos, windows] metadata: hermes: tags: [design, html, prototype, ux, ui, creative, artifact, deck, motion, design-system] - related_skills: [design-md, popular-web-designs, excalidraw, architecture-diagram] + related_skills: [html-artifact, design-md, popular-web-designs, excalidraw] --- # Claude Design for CLI/API Agents @@ -19,19 +19,21 @@ The goal is to preserve Claude Design's useful design behavior and taste while r **Before starting, check for other web-design skills like `popular-web-designs` (ready-to-paste design systems for Stripe, Linear, Vercel, Notion, etc.) and `design-md` (Google's DESIGN.md token spec format).** If the user wants a known brand's look, load `popular-web-designs` alongside this one and let it supply the visual vocabulary. If the deliverable is a token spec file rather than a rendered artifact, use `design-md` instead. Full decision table below. -## When To Use This Skill vs `popular-web-designs` vs `design-md` +## When To Use This Skill vs `html-artifact` vs `popular-web-designs` vs `design-md` -Hermes has three design-related skills under `skills/creative/`. They do different jobs — load the right one (or combine them): +Several skills produce HTML — they do different jobs. Load the right one (or combine them): | Skill | What it gives you | Use when the user wants... | |---|---|---| -| **claude-design** (this one) | Design *process and taste* — how to scope a brief, gather context, produce variants, verify a local HTML artifact, avoid AI-design slop | a from-scratch designed artifact (landing page, prototype, deck, component lab, motion study) with no specific brand or token system dictated | +| **claude-design** (this one) | Visual design *process and taste* — how to scope a brief, gather context, produce variants, verify a local HTML artifact, avoid AI-design slop | a from-scratch *designed* artifact (landing page, prototype, deck, component lab, motion study) where the look itself is the point and no specific brand or token system is dictated | +| **html-artifact** | A house style for *information* artifacts — explainers, plans, reports, code reviews, technical/educational diagrams, throwaway editors | to *explain / plan / report / diagram / review* something as a shareable HTML page — the content is the point, not bespoke visual design | | **popular-web-designs** | 54 ready-to-paste design systems — exact colors, typography, components, CSS values for sites like Stripe, Linear, Vercel, Notion, Airbnb | "make it look like Stripe / Linear / Vercel", a page styled after a known brand, or a visual starting point pulled from a real product | | **design-md** | Google's DESIGN.md spec format — author/validate/diff/export design-token files, WCAG contrast checking, Tailwind/DTCG export | a formal, persistent, machine-readable design-system *spec file* (tokens + rationale) that lives in a repo and gets consumed by agents over time | Rule of thumb: -- **Process + taste, one-off artifact** → claude-design +- **Bespoke visual design, taste-driven artifact** → claude-design +- **Explain / plan / report / diagram as a shareable page** → html-artifact - **Match a known brand's look** → popular-web-designs (and let claude-design drive the process) - **Author the tokens spec itself** → design-md diff --git a/skills/creative/design-md/SKILL.md b/skills/creative/design-md/SKILL.md index 6604be1979d..e0534d9ba72 100644 --- a/skills/creative/design-md/SKILL.md +++ b/skills/creative/design-md/SKILL.md @@ -8,7 +8,7 @@ platforms: [linux, macos, windows] metadata: hermes: tags: [design, design-system, tokens, ui, accessibility, wcag, tailwind, dtcg, google] - related_skills: [popular-web-designs, claude-design, excalidraw, architecture-diagram] + related_skills: [popular-web-designs, claude-design, excalidraw, html-artifact] --- # DESIGN.md Skill diff --git a/skills/creative/html-artifact/SKILL.md b/skills/creative/html-artifact/SKILL.md new file mode 100644 index 00000000000..4883e1ff4c1 --- /dev/null +++ b/skills/creative/html-artifact/SKILL.md @@ -0,0 +1,184 @@ +--- +name: html-artifact +description: Build self-contained HTML files to explain, plan, or review. +version: 1.0.0 +author: Anthropic (html-effectiveness gallery, MIT), adapted for Hermes Agent +license: MIT +platforms: [linux, macos, windows] +metadata: + hermes: + tags: [html, artifact, explainer, plan, report, code-review, diagram, svg, design, prototype, editor] + related_skills: [claude-design, popular-web-designs, design-md, excalidraw, p5js] +--- + +# HTML Artifact Skill + +Produce a single self-contained `.html` file — no build step, no dependencies, no +CDN — whenever the deliverable is something a human should *read, share, or poke at*: +a concept explainer, an implementation plan, a status/incident report, a code-review +walkthrough, a technical or educational diagram, a set of design variants, or a +throwaway editor that exports its result back to you. + +HTML beats Markdown once a doc has color, layout, diagrams, tables, code, or +interaction. It opens in any browser, shares as a link, stays readable past 100 +lines, and can carry SVG diagrams and live controls Markdown can't. Default to an +HTML artifact when the user says "make an HTML file/artifact", or asks you to +*explain how X works*, *write up a plan/PR/report*, *diagram* something, *compare* +options, or *prototype* an interaction — even when they don't say "HTML". + +## Why this skill exists (and what it replaced) + +This skill **supersedes** three former skills — `sketch` (throwaway multi-variant +HTML mockups), `architecture-diagram` (dark-tech infra SVG), and `concept-diagrams` +(educational SVG). They were consolidated for a concrete reason: all three emitted +the *same artifact* — a single self-contained HTML file with inline CSS/SVG — and +overlapped heavily (three "diagram" skills, two "compare variants" paths, no shared +token system). Folding them into one mode-switched skill removes the +which-one-do-I-load ambiguity and gives every output the same house style, while +keeping each skill's unique value: the fidelity dial + verify loop (from `sketch`), +the dark infra aesthetic (from `architecture-diagram`), and the 9-ramp educational +system + archetype library (from `concept-diagrams`). + +The consolidation is footprint-safe: this skill has **zero dependencies** (no Node, +FFmpeg, Chromium, or pip packages — it authors plain HTML/CSS/SVG), so even though it +ships **bundled** (active by default) where `concept-diagrams` was optional, the only +always-in-context cost is this skill's one-line description. All references, +templates, and the example gallery load on demand. `concept-diagrams` was optional +because it was niche, not because it had an install cost — promoting that capability +into a general-purpose, zero-dep bundled skill is the right home for it. Diagram-style +work with a *real* install cost (e.g. `hyperframes`: Node + FFmpeg + Chromium) +deliberately stays optional and is **not** folded in here. + +Use a different skill when: matching a known brand's look → `popular-web-designs`; a +formal design-token spec file → `design-md`; a *bespoke visually-designed* artifact +where the look itself is the point → `claude-design`; hand-drawn/whiteboard +`.excalidraw` files → `excalidraw`; generative/animated canvas art → `p5js`. This +skill is for everything else that ships as a readable, shareable HTML page. + +## Reference files (load on demand) + +- `references/house-style.md` — the canonical `:root` token block, type system, + card/table/callout/code-block patterns. **Read this before authoring any artifact.** +- `references/examples.md` — 20 complete reference HTML files (Anthropic's + html-effectiveness gallery, MIT) keyed to each mode, plus the script to fetch them. + Read/fetch one that matches your task to calibrate the house style from a full example. +- `references/svg-diagrams.md` — hand-authored inline SVG: arrow markers, node + groups, decision diamonds, edge semantics, coordinate-grid discipline. Read for + any flowchart / architecture / concept diagram. +- `references/concept-archetypes.md` — the 9-ramp educational color system + a + library of diagram archetypes (timeline, tree, quadrant, layered stack, + before/after, hub-spoke, cross-section). Read for educational / non-software visuals. +- `references/dark-tech.md` — the dark "infra" token variant (carries the old + architecture-diagram aesthetic). Read for cloud/infra/system architecture diagrams. +- `references/throwaway-editors.md` — the single-file editor recipe and the + copy-to-clipboard export pattern that survives `file://`. Read when the artifact + needs interactive controls that export state back to a prompt. +- `references/fidelity-and-verify.md` — the throwaway↔presentation fidelity dial, + the multi-variant comparison layout, and the mandatory browser-vision verify loop. + +## Templates + +- `templates/base.html` — document scaffold with the house-style ` + + +
+

Section · Context

+

Artifact Title

+

One-sentence framing of what this artifact is and who it's for.

+ +

Overview

+

Body copy. Keep paragraphs readable; let layout carry structure.

+ +
+

Metric

42
+

Metric

7
+

Needs attention

3
+

Metric

98%
+
+ +
Note. Use callouts for the one thing the reader must not miss.
+ + + +
+ + diff --git a/skills/creative/html-artifact/templates/diagram.html b/skills/creative/html-artifact/templates/diagram.html new file mode 100644 index 00000000000..93522119d36 --- /dev/null +++ b/skills/creative/html-artifact/templates/diagram.html @@ -0,0 +1,127 @@ + + + + + +Diagram + + + + + +
+

+

+ + +
+ + diff --git a/skills/creative/html-artifact/templates/editor.html b/skills/creative/html-artifact/templates/editor.html new file mode 100644 index 00000000000..88ee378d7a3 --- /dev/null +++ b/skills/creative/html-artifact/templates/editor.html @@ -0,0 +1,120 @@ + + + + + +Editor + + + + +
+

Throwaway editor

+

Toggle what ships, copy the result

+
+
+ + +
+
+ + + + diff --git a/skills/creative/pretext/SKILL.md b/skills/creative/pretext/SKILL.md index 78f5ab2d959..c526d000ddd 100644 --- a/skills/creative/pretext/SKILL.md +++ b/skills/creative/pretext/SKILL.md @@ -8,7 +8,7 @@ platforms: [linux, macos, windows] metadata: hermes: tags: [creative-coding, typography, pretext, ascii-art, canvas, generative, text-layout, kinetic-typography] - related_skills: [p5js, claude-design, excalidraw, architecture-diagram] + related_skills: [p5js, claude-design, excalidraw, html-artifact] --- # Pretext Creative Demos diff --git a/skills/creative/sketch/SKILL.md b/skills/creative/sketch/SKILL.md deleted file mode 100644 index 6e49585acd4..00000000000 --- a/skills/creative/sketch/SKILL.md +++ /dev/null @@ -1,218 +0,0 @@ ---- -name: sketch -description: "Throwaway HTML mockups: 2-3 design variants to compare." -version: 1.0.0 -author: Hermes Agent (adapted from gsd-build/get-shit-done) -license: MIT -platforms: [linux, macos, windows] -metadata: - hermes: - tags: [sketch, mockup, design, ui, prototype, html, variants, exploration, wireframe, comparison] - related_skills: [spike, claude-design, popular-web-designs, excalidraw] ---- - -# Sketch - -Use this skill when the user wants to **see a design direction before committing** to one — exploring a UI/UX idea as disposable HTML mockups. The point is to generate 2-3 interactive variants so the user can compare visual directions side-by-side, not to produce shippable code. - -Load this when the user says things like "sketch this screen", "show me what X could look like", "compare layout A vs B", "give me 2-3 takes on this UI", "let me see some variants", "mockup this before I build". - -## When NOT to use this - -- User wants a production component — use `claude-design` or build it properly -- User wants a polished one-off HTML artifact (landing page, deck) — `claude-design` -- User wants a diagram — `excalidraw`, `architecture-diagram` -- The design is already locked — just build it - -## If the user has the full GSD system installed - -If `gsd-sketch` shows up as a sibling skill (installed via `npx get-shit-done-cc --hermes`), prefer **`gsd-sketch`** for the full workflow: persistent `.planning/sketches/` with MANIFEST, frontier mode analysis, consistency audits across past sketches, and integration with the rest of GSD. This skill is the lightweight standalone version — one-off sketching without the state machinery. - -## Core method - -``` -intake → variants → head-to-head → pick winner (or iterate) -``` - -### 1. Intake (skip if the user already gave you enough) - -Before generating variants, get three things — one question at a time, not all at once: - -1. **Feel.** "What should this feel like? Adjectives, emotions, a vibe." — *"calm, editorial, like Linear"* tells you more than *"minimal"*. -2. **References.** "What apps, sites, or products capture the feel you're imagining?" — actual references beat abstract descriptions. -3. **Core action.** "What's the single most important thing a user does on this screen?" — the variants should all serve this well; if they don't, they're just decoration. - -Reflect each answer briefly before the next question. If the user already gave you all three upfront, skip straight to variants. - -### 2. Variants (2-3, never 1, rarely 4+) - -Produce **2-3 variants** in one go. Each variant is a complete, standalone HTML file. Don't describe variants — build them. The point is comparison. - -Each variant should take a **different design stance**, not different pixel values. Three good variant axes: - -- **Density:** compact / airy / ultra-dense (pick two contrasting poles) -- **Emphasis:** content-first / action-first / tool-first -- **Aesthetic:** editorial / utilitarian / playful -- **Layout:** single-column / sidebar / split-pane -- **Grounding:** card-based / bare-content / document-style - -Pick one axis and pull apart from it. Two variants that differ only in accent color are wasted effort — the user can't distinguish them. - -**Variant naming:** describe the stance, not the number. - -``` -sketches/ -├── 001-calm-editorial/ -│ ├── index.html -│ └── README.md -├── 001-utilitarian-dense/ -│ ├── index.html -│ └── README.md -└── 001-playful-split/ - ├── index.html - └── README.md -``` - -### 3. Make them real HTML - -Each variant is a **single self-contained HTML file**: - -- Inline ` -``` - -### 4. Variant README - -Each variant's `README.md` answers: - -```markdown -## Variant: {stance name} - -### Design stance -One sentence on the principle driving this variant. - -### Key choices -- Layout: ... -- Typography: ... -- Color: ... -- Interaction: ... - -### Trade-offs -- Strong at: ... -- Weak at: ... - -### Best for -- The kind of user or use case this variant actually serves -``` - -### 5. Head-to-head - -After all variants are built, present them as a comparison. Don't just list — **opinionate**: - -```markdown -## Three takes on the home screen - -| Dimension | Calm editorial | Utilitarian dense | Playful split | -|-----------|----------------|-------------------|---------------| -| Density | Low | High | Medium | -| Primary action visibility | Low | High | Medium | -| Scan-ability | High | Medium | Low | -| Feel | Calm, trusted | Sharp, tool-like | Inviting, energetic | - -**My take:** Utilitarian dense for power users, calm editorial for content-forward audiences. Playful split is weakest — tries to do both and commits to neither. -``` - -Let the user pick a winner, or combine two into a hybrid, or ask for another round. - -## Theming (when the project has a visual identity) - -If the user has an existing theme (colors, fonts, tokens), put shared tokens in `sketches/themes/tokens.css` and `@import` them in each variant. Keep tokens minimal: - -```css -/* sketches/themes/tokens.css */ -:root { - --color-bg: #fafafa; - --color-fg: #1a1a1a; - --color-accent: #0066ff; - --color-muted: #666; - --radius: 8px; - --font-display: "Inter", sans-serif; - --font-body: -apple-system, BlinkMacSystemFont, sans-serif; -} -``` - -Don't over-tokenize a throwaway sketch — three colors and one font is usually enough. - -## Interactivity bar - -A sketch is interactive enough when the user can: - -1. **Click a primary action** and something visible happens (state change, modal, toast, navigation feint) -2. **See one meaningful state transition** (filter a list, toggle a mode, open/close a panel) -3. **Hover recognizable affordances** (buttons, rows, tabs) - -More than that is over-engineering a throwaway. Less than that is a screenshot. - -## Frontier mode (picking what to sketch next) - -If sketches already exist and the user says "what should I sketch next?": - -- **Consistency gaps** — two winning variants from different sketches made independent choices that haven't been composed together yet -- **Unsketched screens** — referenced but never explored -- **State coverage** — happy path sketched, but not empty / loading / error / 1000-items -- **Responsive gaps** — validated at one viewport; does it hold at mobile / ultrawide? -- **Interaction patterns** — static layouts exist; transitions, drag, scroll behavior don't - -Propose 2-4 named candidates. Let the user pick. - -## Output - -- Create `sketches/` (or `.planning/sketches/` if the user is using GSD conventions) in the repo root -- One subdir per variant: `NNN-stance-name/index.html` + `README.md` -- Tell the user how to open them: `open sketches/001-calm-editorial/index.html` on macOS, `xdg-open` on Linux, `start` on Windows -- Keep variants disposable — a sketch that you felt the need to preserve should be promoted into real project code, not curated as an asset - -**Typical tool sequence for one variant:** - -``` -terminal("mkdir -p sketches/001-calm-editorial") -write_file("sketches/001-calm-editorial/index.html", "...") -write_file("sketches/001-calm-editorial/README.md", "## Variant: Calm editorial\n...") -browser_navigate(url="file://$(pwd)/sketches/001-calm-editorial/index.html") -browser_vision(question="How does this look? Any obvious layout issues?") -``` - -Repeat for each variant, then present the comparison table. - -## Attribution - -Adapted from the GSD (Get Shit Done) project's `/gsd-sketch` workflow — MIT © 2025 Lex Christopherson ([gsd-build/get-shit-done](https://github.com/gsd-build/get-shit-done)). The full GSD system ships persistent sketch state, theme/variant pattern references, and consistency-audit workflows; install with `npx get-shit-done-cc --hermes --global`. diff --git a/skills/software-development/spike/SKILL.md b/skills/software-development/spike/SKILL.md index 2a980f0ade9..313cbe7fb9c 100644 --- a/skills/software-development/spike/SKILL.md +++ b/skills/software-development/spike/SKILL.md @@ -8,7 +8,7 @@ platforms: [linux, macos, windows] metadata: hermes: tags: [spike, prototype, experiment, feasibility, throwaway, exploration, research, planning, mvp, proof-of-concept] - related_skills: [sketch, subagent-driven-development, plan] + related_skills: [html-artifact, subagent-driven-development, plan] --- # Spike diff --git a/website/docs/reference/optional-skills-catalog.md b/website/docs/reference/optional-skills-catalog.md index 4e2b2524fe2..a9e27dfd90e 100644 --- a/website/docs/reference/optional-skills-catalog.md +++ b/website/docs/reference/optional-skills-catalog.md @@ -58,7 +58,6 @@ hermes skills uninstall | [**baoyu-article-illustrator**](/docs/user-guide/skills/optional/creative/creative-baoyu-article-illustrator) | Article illustrations: type × style × palette consistency. | | [**baoyu-comic**](/docs/user-guide/skills/optional/creative/creative-baoyu-comic) | Knowledge comics (知识漫画): educational, biography, tutorial. | | [**blender-mcp**](/docs/user-guide/skills/optional/creative/creative-blender-mcp) | Control Blender directly from Hermes via socket connection to the blender-mcp addon. Create 3D objects, materials, animations, and run arbitrary Blender Python (bpy) code. Use when user wants to create or modify anything in Blender. | -| [**concept-diagrams**](/docs/user-guide/skills/optional/creative/creative-concept-diagrams) | Generate flat, minimal light/dark-aware SVG diagrams as standalone HTML files, using a unified educational visual language with 9 semantic color ramps, sentence-case typography, and automatic dark mode. Best suited for educational and no... | | [**ideation**](/docs/user-guide/skills/optional/creative/creative-creative-ideation) | Generate project ideas via creative constraints. | | [**hyperframes**](/docs/user-guide/skills/optional/creative/creative-hyperframes) | Create HTML-based video compositions, animated title cards, social overlays, captioned talking-head videos, audio-reactive visuals, and shader transitions using HyperFrames. HTML is the source of truth for video. Use when the user wants... | | [**kanban-video-orchestrator**](/docs/user-guide/skills/optional/creative/creative-kanban-video-orchestrator) | Plan, set up, and monitor a multi-agent video production pipeline backed by Hermes Kanban. Use when the user wants to make ANY video — narrative film, product/marketing, music video, explainer, ASCII/terminal art, abstract/generative loo... | diff --git a/website/docs/reference/skills-catalog.md b/website/docs/reference/skills-catalog.md index 5ccb1f5f5ca..3ae519a07f8 100644 --- a/website/docs/reference/skills-catalog.md +++ b/website/docs/reference/skills-catalog.md @@ -35,7 +35,6 @@ If a skill is missing from this list but present in the repo, the catalog is reg | Skill | Description | Path | |-------|-------------|------| -| [`architecture-diagram`](/docs/user-guide/skills/bundled/creative/creative-architecture-diagram) | Dark-themed SVG architecture/cloud/infra diagrams as HTML. | `creative/architecture-diagram` | | [`ascii-art`](/docs/user-guide/skills/bundled/creative/creative-ascii-art) | ASCII art: pyfiglet, cowsay, boxes, image-to-ascii. | `creative/ascii-art` | | [`ascii-video`](/docs/user-guide/skills/bundled/creative/creative-ascii-video) | ASCII video: convert video/audio to colored ASCII MP4/GIF. | `creative/ascii-video` | | [`baoyu-infographic`](/docs/user-guide/skills/bundled/creative/creative-baoyu-infographic) | Infographics: 21 layouts x 21 styles (信息图, 可视化). | `creative/baoyu-infographic` | @@ -43,12 +42,12 @@ If a skill is missing from this list but present in the repo, the catalog is reg | [`comfyui`](/docs/user-guide/skills/bundled/creative/creative-comfyui) | Generate images, video, and audio with ComfyUI — install, launch, manage nodes/models, run workflows with parameter injection. Uses the official comfy-cli for lifecycle and direct REST/WebSocket API for execution. | `creative/comfyui` | | [`design-md`](/docs/user-guide/skills/bundled/creative/creative-design-md) | Author/validate/export Google's DESIGN.md token spec files. | `creative/design-md` | | [`excalidraw`](/docs/user-guide/skills/bundled/creative/creative-excalidraw) | Hand-drawn Excalidraw JSON diagrams (arch, flow, seq). | `creative/excalidraw` | +| [`html-artifact`](/docs/user-guide/skills/bundled/creative/creative-html-artifact) | Build self-contained HTML files to explain, plan, or review. | `creative/html-artifact` | | [`humanizer`](/docs/user-guide/skills/bundled/creative/creative-humanizer) | Humanize text: strip AI-isms and add real voice. | `creative/humanizer` | | [`manim-video`](/docs/user-guide/skills/bundled/creative/creative-manim-video) | Manim CE animations: 3Blue1Brown math/algo videos. | `creative/manim-video` | | [`p5js`](/docs/user-guide/skills/bundled/creative/creative-p5js) | p5.js sketches: gen art, shaders, interactive, 3D. | `creative/p5js` | | [`popular-web-designs`](/docs/user-guide/skills/bundled/creative/creative-popular-web-designs) | 54 real design systems (Stripe, Linear, Vercel) as HTML/CSS. | `creative/popular-web-designs` | | [`pretext`](/docs/user-guide/skills/bundled/creative/creative-pretext) | Use when building creative browser demos with @chenglou/pretext — DOM-free text layout for ASCII art, typographic flow around obstacles, text-as-geometry games, kinetic typography, and text-powered generative art. Produces single-file HT... | `creative/pretext` | -| [`sketch`](/docs/user-guide/skills/bundled/creative/creative-sketch) | Throwaway HTML mockups: 2-3 design variants to compare. | `creative/sketch` | | [`songwriting-and-ai-music`](/docs/user-guide/skills/bundled/creative/creative-songwriting-and-ai-music) | Songwriting craft and Suno AI music prompts. | `creative/songwriting-and-ai-music` | | [`touchdesigner-mcp`](/docs/user-guide/skills/bundled/creative/creative-touchdesigner-mcp) | Control a running TouchDesigner instance via twozero MCP — create operators, set parameters, wire connections, execute Python, build real-time visuals. 36 native tools. | `creative/touchdesigner-mcp` | diff --git a/website/docs/user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-hermes-agent.md b/website/docs/user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-hermes-agent.md index 77f81db14b6..089ea173923 100644 --- a/website/docs/user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-hermes-agent.md +++ b/website/docs/user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-hermes-agent.md @@ -360,7 +360,7 @@ The registry of record is `hermes_cli/commands.py` — every consumer ``` ~/.hermes/config.yaml Main configuration -~/.hermes/.env API keys and secrets +~/.hermes/.env API keys and secrets (under $HERMES_HOME if set) $HERMES_HOME/skills/ Installed skills ~/.hermes/sessions/ Gateway routing index, request dumps, *.jsonl transcripts (and optional per-session JSON snapshots when sessions.write_json_snapshots: true) ~/.hermes/state.db Canonical session store (SQLite + FTS5) @@ -927,7 +927,7 @@ hermes-agent/ ``` -Config: `~/.hermes/config.yaml` (settings), `~/.hermes/.env` (API keys). +Config: `~/.hermes/config.yaml` (settings), `~/.hermes/.env` (API keys) — both under `$HERMES_HOME` when it is set. ### Adding a Tool (3 files) diff --git a/website/docs/user-guide/skills/bundled/creative/creative-architecture-diagram.md b/website/docs/user-guide/skills/bundled/creative/creative-architecture-diagram.md deleted file mode 100644 index ad816a370ad..00000000000 --- a/website/docs/user-guide/skills/bundled/creative/creative-architecture-diagram.md +++ /dev/null @@ -1,165 +0,0 @@ ---- -title: "Architecture Diagram — Dark-themed SVG architecture/cloud/infra diagrams as HTML" -sidebar_label: "Architecture Diagram" -description: "Dark-themed SVG architecture/cloud/infra diagrams as HTML" ---- - -{/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */} - -# Architecture Diagram - -Dark-themed SVG architecture/cloud/infra diagrams as HTML. - -## Skill metadata - -| | | -|---|---| -| Source | Bundled (installed by default) | -| Path | `skills/creative/architecture-diagram` | -| Version | `1.0.0` | -| Author | Cocoon AI (hello@cocoon-ai.com), ported by Hermes Agent | -| License | MIT | -| Platforms | linux, macos, windows | -| Tags | `architecture`, `diagrams`, `SVG`, `HTML`, `visualization`, `infrastructure`, `cloud` | -| Related skills | [`concept-diagrams`](/docs/user-guide/skills/optional/creative/creative-concept-diagrams), [`excalidraw`](/docs/user-guide/skills/bundled/creative/creative-excalidraw) | - -## Reference: full SKILL.md - -:::info -The following is the complete skill definition that Hermes loads when this skill is triggered. This is what the agent sees as instructions when the skill is active. -::: - -# Architecture Diagram Skill - -Generate professional, dark-themed technical architecture diagrams as standalone HTML files with inline SVG graphics. No external tools, no API keys, no rendering libraries — just write the HTML file and open it in a browser. - -## Scope - -**Best suited for:** -- Software system architecture (frontend / backend / database layers) -- Cloud infrastructure (VPC, regions, subnets, managed services) -- Microservice / service-mesh topology -- Database + API map, deployment diagrams -- Anything with a tech-infra subject that fits a dark, grid-backed aesthetic - -**Look elsewhere first for:** -- Physics, chemistry, math, biology, or other scientific subjects -- Physical objects (vehicles, hardware, anatomy, cross-sections) -- Floor plans, narrative journeys, educational / textbook-style visuals -- Hand-drawn whiteboard sketches (consider `excalidraw`) -- Animated explainers (consider an animation skill) - -If a more specialized skill is available for the subject, prefer that. If none fits, this skill can also serve as a general SVG diagram fallback — the output will just carry the dark tech aesthetic described below. - -Based on [Cocoon AI's architecture-diagram-generator](https://github.com/Cocoon-AI/architecture-diagram-generator) (MIT). - -## Workflow - -1. User describes their system architecture (components, connections, technologies) -2. Generate the HTML file following the design system below -3. Save with `write_file` to a `.html` file (e.g. `~/architecture-diagram.html`) -4. User opens in any browser — works offline, no dependencies - -### Output Location - -Save diagrams to a user-specified path, or default to the current working directory: -``` -./[project-name]-architecture.html -``` - -### Preview - -After saving, suggest the user open it: -```bash -# macOS -open ./my-architecture.html -# Linux -xdg-open ./my-architecture.html -``` - -## Design System & Visual Language - -### Color Palette (Semantic Mapping) - -Use specific `rgba` fills and hex strokes to categorize components: - -| Component Type | Fill (rgba) | Stroke (Hex) | -| :--- | :--- | :--- | -| **Frontend** | `rgba(8, 51, 68, 0.4)` | `#22d3ee` (cyan-400) | -| **Backend** | `rgba(6, 78, 59, 0.4)` | `#34d399` (emerald-400) | -| **Database** | `rgba(76, 29, 149, 0.4)` | `#a78bfa` (violet-400) | -| **AWS/Cloud** | `rgba(120, 53, 15, 0.3)` | `#fbbf24` (amber-400) | -| **Security** | `rgba(136, 19, 55, 0.4)` | `#fb7185` (rose-400) | -| **Message Bus** | `rgba(251, 146, 60, 0.3)` | `#fb923c` (orange-400) | -| **External** | `rgba(30, 41, 59, 0.5)` | `#94a3b8` (slate-400) | - -### Typography & Background -- **Font:** JetBrains Mono (Monospace), loaded from Google Fonts -- **Sizes:** 12px (Names), 9px (Sublabels), 8px (Annotations), 7px (Tiny labels) -- **Background:** Slate-950 (`#020617`) with a subtle 40px grid pattern - -```svg - - - - -``` - -## Technical Implementation Details - -### Component Rendering -Components are rounded rectangles (`rx="6"`) with 1.5px strokes. To prevent arrows from showing through semi-transparent fills, use a **double-rect masking technique**: -1. Draw an opaque background rect (`#0f172a`) -2. Draw the semi-transparent styled rect on top - -### Connection Rules -- **Z-Order:** Draw arrows *early* in the SVG (after the grid) so they render behind component boxes -- **Arrowheads:** Defined via SVG markers -- **Security Flows:** Use dashed lines in rose color (`#fb7185`) -- **Boundaries:** - - *Security Groups:* Dashed (`4,4`), rose color - - *Regions:* Large dashed (`8,4`), amber color, `rx="12"` - -### Spacing & Layout Logic -- **Standard Height:** 60px (Services); 80-120px (Large components) -- **Vertical Gap:** Minimum 40px between components -- **Message Buses:** Must be placed *in the gap* between services, not overlapping them -- **Legend Placement:** **CRITICAL.** Must be placed outside all boundary boxes. Calculate the lowest Y-coordinate of all boundaries and place the legend at least 20px below it. - -## Document Structure - -The generated HTML file follows a four-part layout: -1. **Header:** Title with a pulsing dot indicator and subtitle -2. **Main SVG:** The diagram contained within a rounded border card -3. **Summary Cards:** A grid of three cards below the diagram for high-level details -4. **Footer:** Minimal metadata - -### Info Card Pattern -```html -
-
-
-

Title

-
-
    -
  • • Item one
  • -
  • • Item two
  • -
-
-``` - -## Output Requirements -- **Single File:** One self-contained `.html` file -- **No External Dependencies:** All CSS and SVG must be inline (except Google Fonts) -- **No JavaScript:** Use pure CSS for any animations (like pulsing dots) -- **Compatibility:** Must render correctly in any modern web browser - -## Template Reference - -Load the full HTML template for the exact structure, CSS, and SVG component examples: - -``` -skill_view(name="architecture-diagram", file_path="templates/template.html") -``` - -The template contains working examples of every component type (frontend, backend, database, cloud, security), arrow styles (standard, dashed, curved), security groups, region boundaries, and the legend — use it as your structural reference when generating diagrams. diff --git a/website/docs/user-guide/skills/bundled/creative/creative-claude-design.md b/website/docs/user-guide/skills/bundled/creative/creative-claude-design.md index bf6f4eafaa3..8fa3c563bbf 100644 --- a/website/docs/user-guide/skills/bundled/creative/creative-claude-design.md +++ b/website/docs/user-guide/skills/bundled/creative/creative-claude-design.md @@ -21,7 +21,7 @@ Design one-off HTML artifacts (landing, deck, prototype). | License | MIT | | Platforms | linux, macos, windows | | Tags | `design`, `html`, `prototype`, `ux`, `ui`, `creative`, `artifact`, `deck`, `motion`, `design-system` | -| Related skills | [`design-md`](/docs/user-guide/skills/bundled/creative/creative-design-md), [`popular-web-designs`](/docs/user-guide/skills/bundled/creative/creative-popular-web-designs), [`excalidraw`](/docs/user-guide/skills/bundled/creative/creative-excalidraw), [`architecture-diagram`](/docs/user-guide/skills/bundled/creative/creative-architecture-diagram) | +| Related skills | [`html-artifact`](/docs/user-guide/skills/bundled/creative/creative-html-artifact), [`design-md`](/docs/user-guide/skills/bundled/creative/creative-design-md), [`popular-web-designs`](/docs/user-guide/skills/bundled/creative/creative-popular-web-designs), [`excalidraw`](/docs/user-guide/skills/bundled/creative/creative-excalidraw) | ## Reference: full SKILL.md @@ -37,19 +37,21 @@ The goal is to preserve Claude Design's useful design behavior and taste while r **Before starting, check for other web-design skills like `popular-web-designs` (ready-to-paste design systems for Stripe, Linear, Vercel, Notion, etc.) and `design-md` (Google's DESIGN.md token spec format).** If the user wants a known brand's look, load `popular-web-designs` alongside this one and let it supply the visual vocabulary. If the deliverable is a token spec file rather than a rendered artifact, use `design-md` instead. Full decision table below. -## When To Use This Skill vs `popular-web-designs` vs `design-md` +## When To Use This Skill vs `html-artifact` vs `popular-web-designs` vs `design-md` -Hermes has three design-related skills under `skills/creative/`. They do different jobs — load the right one (or combine them): +Several skills produce HTML — they do different jobs. Load the right one (or combine them): | Skill | What it gives you | Use when the user wants... | |---|---|---| -| **claude-design** (this one) | Design *process and taste* — how to scope a brief, gather context, produce variants, verify a local HTML artifact, avoid AI-design slop | a from-scratch designed artifact (landing page, prototype, deck, component lab, motion study) with no specific brand or token system dictated | +| **claude-design** (this one) | Visual design *process and taste* — how to scope a brief, gather context, produce variants, verify a local HTML artifact, avoid AI-design slop | a from-scratch *designed* artifact (landing page, prototype, deck, component lab, motion study) where the look itself is the point and no specific brand or token system is dictated | +| **html-artifact** | A house style for *information* artifacts — explainers, plans, reports, code reviews, technical/educational diagrams, throwaway editors | to *explain / plan / report / diagram / review* something as a shareable HTML page — the content is the point, not bespoke visual design | | **popular-web-designs** | 54 ready-to-paste design systems — exact colors, typography, components, CSS values for sites like Stripe, Linear, Vercel, Notion, Airbnb | "make it look like Stripe / Linear / Vercel", a page styled after a known brand, or a visual starting point pulled from a real product | | **design-md** | Google's DESIGN.md spec format — author/validate/diff/export design-token files, WCAG contrast checking, Tailwind/DTCG export | a formal, persistent, machine-readable design-system *spec file* (tokens + rationale) that lives in a repo and gets consumed by agents over time | Rule of thumb: -- **Process + taste, one-off artifact** → claude-design +- **Bespoke visual design, taste-driven artifact** → claude-design +- **Explain / plan / report / diagram as a shareable page** → html-artifact - **Match a known brand's look** → popular-web-designs (and let claude-design drive the process) - **Author the tokens spec itself** → design-md diff --git a/website/docs/user-guide/skills/bundled/creative/creative-design-md.md b/website/docs/user-guide/skills/bundled/creative/creative-design-md.md index a96723ddb7f..687916eb2dc 100644 --- a/website/docs/user-guide/skills/bundled/creative/creative-design-md.md +++ b/website/docs/user-guide/skills/bundled/creative/creative-design-md.md @@ -21,7 +21,7 @@ Author/validate/export Google's DESIGN.md token spec files. | License | MIT | | Platforms | linux, macos, windows | | Tags | `design`, `design-system`, `tokens`, `ui`, `accessibility`, `wcag`, `tailwind`, `dtcg`, `google` | -| Related skills | [`popular-web-designs`](/docs/user-guide/skills/bundled/creative/creative-popular-web-designs), [`claude-design`](/docs/user-guide/skills/bundled/creative/creative-claude-design), [`excalidraw`](/docs/user-guide/skills/bundled/creative/creative-excalidraw), [`architecture-diagram`](/docs/user-guide/skills/bundled/creative/creative-architecture-diagram) | +| Related skills | [`popular-web-designs`](/docs/user-guide/skills/bundled/creative/creative-popular-web-designs), [`claude-design`](/docs/user-guide/skills/bundled/creative/creative-claude-design), [`excalidraw`](/docs/user-guide/skills/bundled/creative/creative-excalidraw), [`html-artifact`](/docs/user-guide/skills/bundled/creative/creative-html-artifact) | ## Reference: full SKILL.md diff --git a/website/docs/user-guide/skills/bundled/creative/creative-html-artifact.md b/website/docs/user-guide/skills/bundled/creative/creative-html-artifact.md new file mode 100644 index 00000000000..0f34348ef2e --- /dev/null +++ b/website/docs/user-guide/skills/bundled/creative/creative-html-artifact.md @@ -0,0 +1,202 @@ +--- +title: "Html Artifact — Build self-contained HTML files to explain, plan, or review" +sidebar_label: "Html Artifact" +description: "Build self-contained HTML files to explain, plan, or review" +--- + +{/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */} + +# Html Artifact + +Build self-contained HTML files to explain, plan, or review. + +## Skill metadata + +| | | +|---|---| +| Source | Bundled (installed by default) | +| Path | `skills/creative/html-artifact` | +| Version | `1.0.0` | +| Author | Anthropic (html-effectiveness gallery, MIT), adapted for Hermes Agent | +| License | MIT | +| Platforms | linux, macos, windows | +| Tags | `html`, `artifact`, `explainer`, `plan`, `report`, `code-review`, `diagram`, `svg`, `design`, `prototype`, `editor` | +| Related skills | [`claude-design`](/docs/user-guide/skills/bundled/creative/creative-claude-design), [`popular-web-designs`](/docs/user-guide/skills/bundled/creative/creative-popular-web-designs), [`design-md`](/docs/user-guide/skills/bundled/creative/creative-design-md), [`excalidraw`](/docs/user-guide/skills/bundled/creative/creative-excalidraw), [`p5js`](/docs/user-guide/skills/bundled/creative/creative-p5js) | + +## Reference: full SKILL.md + +:::info +The following is the complete skill definition that Hermes loads when this skill is triggered. This is what the agent sees as instructions when the skill is active. +::: + +# HTML Artifact Skill + +Produce a single self-contained `.html` file — no build step, no dependencies, no +CDN — whenever the deliverable is something a human should *read, share, or poke at*: +a concept explainer, an implementation plan, a status/incident report, a code-review +walkthrough, a technical or educational diagram, a set of design variants, or a +throwaway editor that exports its result back to you. + +HTML beats Markdown once a doc has color, layout, diagrams, tables, code, or +interaction. It opens in any browser, shares as a link, stays readable past 100 +lines, and can carry SVG diagrams and live controls Markdown can't. Default to an +HTML artifact when the user says "make an HTML file/artifact", or asks you to +*explain how X works*, *write up a plan/PR/report*, *diagram* something, *compare* +options, or *prototype* an interaction — even when they don't say "HTML". + +## Why this skill exists (and what it replaced) + +This skill **supersedes** three former skills — `sketch` (throwaway multi-variant +HTML mockups), `architecture-diagram` (dark-tech infra SVG), and `concept-diagrams` +(educational SVG). They were consolidated for a concrete reason: all three emitted +the *same artifact* — a single self-contained HTML file with inline CSS/SVG — and +overlapped heavily (three "diagram" skills, two "compare variants" paths, no shared +token system). Folding them into one mode-switched skill removes the +which-one-do-I-load ambiguity and gives every output the same house style, while +keeping each skill's unique value: the fidelity dial + verify loop (from `sketch`), +the dark infra aesthetic (from `architecture-diagram`), and the 9-ramp educational +system + archetype library (from `concept-diagrams`). + +The consolidation is footprint-safe: this skill has **zero dependencies** (no Node, +FFmpeg, Chromium, or pip packages — it authors plain HTML/CSS/SVG), so even though it +ships **bundled** (active by default) where `concept-diagrams` was optional, the only +always-in-context cost is this skill's one-line description. All references, +templates, and the example gallery load on demand. `concept-diagrams` was optional +because it was niche, not because it had an install cost — promoting that capability +into a general-purpose, zero-dep bundled skill is the right home for it. Diagram-style +work with a *real* install cost (e.g. `hyperframes`: Node + FFmpeg + Chromium) +deliberately stays optional and is **not** folded in here. + +Use a different skill when: matching a known brand's look → `popular-web-designs`; a +formal design-token spec file → `design-md`; a *bespoke visually-designed* artifact +where the look itself is the point → `claude-design`; hand-drawn/whiteboard +`.excalidraw` files → `excalidraw`; generative/animated canvas art → `p5js`. This +skill is for everything else that ships as a readable, shareable HTML page. + +## Reference files (load on demand) + +- `references/house-style.md` — the canonical `:root` token block, type system, + card/table/callout/code-block patterns. **Read this before authoring any artifact.** +- `references/examples.md` — 20 complete reference HTML files (Anthropic's + html-effectiveness gallery, MIT) keyed to each mode, plus the script to fetch them. + Read/fetch one that matches your task to calibrate the house style from a full example. +- `references/svg-diagrams.md` — hand-authored inline SVG: arrow markers, node + groups, decision diamonds, edge semantics, coordinate-grid discipline. Read for + any flowchart / architecture / concept diagram. +- `references/concept-archetypes.md` — the 9-ramp educational color system + a + library of diagram archetypes (timeline, tree, quadrant, layered stack, + before/after, hub-spoke, cross-section). Read for educational / non-software visuals. +- `references/dark-tech.md` — the dark "infra" token variant (carries the old + architecture-diagram aesthetic). Read for cloud/infra/system architecture diagrams. +- `references/throwaway-editors.md` — the single-file editor recipe and the + copy-to-clipboard export pattern that survives `file://`. Read when the artifact + needs interactive controls that export state back to a prompt. +- `references/fidelity-and-verify.md` — the throwaway↔presentation fidelity dial, + the multi-variant comparison layout, and the mandatory browser-vision verify loop. + +## Templates + +- `templates/base.html` — document scaffold with the house-style ` -``` - -### 4. Variant README - -Each variant's `README.md` answers: - -```markdown -## Variant: {stance name} - -### Design stance -One sentence on the principle driving this variant. - -### Key choices -- Layout: ... -- Typography: ... -- Color: ... -- Interaction: ... - -### Trade-offs -- Strong at: ... -- Weak at: ... - -### Best for -- The kind of user or use case this variant actually serves -``` - -### 5. Head-to-head - -After all variants are built, present them as a comparison. Don't just list — **opinionate**: - -```markdown -## Three takes on the home screen - -| Dimension | Calm editorial | Utilitarian dense | Playful split | -|-----------|----------------|-------------------|---------------| -| Density | Low | High | Medium | -| Primary action visibility | Low | High | Medium | -| Scan-ability | High | Medium | Low | -| Feel | Calm, trusted | Sharp, tool-like | Inviting, energetic | - -**My take:** Utilitarian dense for power users, calm editorial for content-forward audiences. Playful split is weakest — tries to do both and commits to neither. -``` - -Let the user pick a winner, or combine two into a hybrid, or ask for another round. - -## Theming (when the project has a visual identity) - -If the user has an existing theme (colors, fonts, tokens), put shared tokens in `sketches/themes/tokens.css` and `@import` them in each variant. Keep tokens minimal: - -```css -/* sketches/themes/tokens.css */ -:root { - --color-bg: #fafafa; - --color-fg: #1a1a1a; - --color-accent: #0066ff; - --color-muted: #666; - --radius: 8px; - --font-display: "Inter", sans-serif; - --font-body: -apple-system, BlinkMacSystemFont, sans-serif; -} -``` - -Don't over-tokenize a throwaway sketch — three colors and one font is usually enough. - -## Interactivity bar - -A sketch is interactive enough when the user can: - -1. **Click a primary action** and something visible happens (state change, modal, toast, navigation feint) -2. **See one meaningful state transition** (filter a list, toggle a mode, open/close a panel) -3. **Hover recognizable affordances** (buttons, rows, tabs) - -More than that is over-engineering a throwaway. Less than that is a screenshot. - -## Frontier mode (picking what to sketch next) - -If sketches already exist and the user says "what should I sketch next?": - -- **Consistency gaps** — two winning variants from different sketches made independent choices that haven't been composed together yet -- **Unsketched screens** — referenced but never explored -- **State coverage** — happy path sketched, but not empty / loading / error / 1000-items -- **Responsive gaps** — validated at one viewport; does it hold at mobile / ultrawide? -- **Interaction patterns** — static layouts exist; transitions, drag, scroll behavior don't - -Propose 2-4 named candidates. Let the user pick. - -## Output - -- Create `sketches/` (or `.planning/sketches/` if the user is using GSD conventions) in the repo root -- One subdir per variant: `NNN-stance-name/index.html` + `README.md` -- Tell the user how to open them: `open sketches/001-calm-editorial/index.html` on macOS, `xdg-open` on Linux, `start` on Windows -- Keep variants disposable — a sketch that you felt the need to preserve should be promoted into real project code, not curated as an asset - -**Typical tool sequence for one variant:** - -``` -terminal("mkdir -p sketches/001-calm-editorial") -write_file("sketches/001-calm-editorial/index.html", "...") -write_file("sketches/001-calm-editorial/README.md", "## Variant: Calm editorial\n...") -browser_navigate(url="file://$(pwd)/sketches/001-calm-editorial/index.html") -browser_vision(question="How does this look? Any obvious layout issues?") -``` - -Repeat for each variant, then present the comparison table. - -## Attribution - -Adapted from the GSD (Get Shit Done) project's `/gsd-sketch` workflow — MIT © 2025 Lex Christopherson ([gsd-build/get-shit-done](https://github.com/gsd-build/get-shit-done)). The full GSD system ships persistent sketch state, theme/variant pattern references, and consistency-audit workflows; install with `npx get-shit-done-cc --hermes --global`. diff --git a/website/docs/user-guide/skills/bundled/creative/creative-touchdesigner-mcp.md b/website/docs/user-guide/skills/bundled/creative/creative-touchdesigner-mcp.md index 2577f1f741c..9a14bceffd9 100644 --- a/website/docs/user-guide/skills/bundled/creative/creative-touchdesigner-mcp.md +++ b/website/docs/user-guide/skills/bundled/creative/creative-touchdesigner-mcp.md @@ -21,7 +21,7 @@ Control a running TouchDesigner instance via twozero MCP — create operators, s | License | MIT | | Platforms | linux, macos, windows | | Tags | `TouchDesigner`, `MCP`, `twozero`, `creative-coding`, `real-time-visuals`, `generative-art`, `audio-reactive`, `VJ`, `installation`, `GLSL` | -| Related skills | [`native-mcp`](/docs/user-guide/skills/bundled/mcp/mcp-native-mcp), [`ascii-video`](/docs/user-guide/skills/bundled/creative/creative-ascii-video), [`manim-video`](/docs/user-guide/skills/bundled/creative/creative-manim-video), `hermes-video` | +| Related skills | `native-mcp`, [`ascii-video`](/docs/user-guide/skills/bundled/creative/creative-ascii-video), [`manim-video`](/docs/user-guide/skills/bundled/creative/creative-manim-video), `hermes-video` | ## Reference: full SKILL.md diff --git a/website/docs/user-guide/skills/bundled/email/email-himalaya.md b/website/docs/user-guide/skills/bundled/email/email-himalaya.md index adf3d973635..34c868e9f26 100644 --- a/website/docs/user-guide/skills/bundled/email/email-himalaya.md +++ b/website/docs/user-guide/skills/bundled/email/email-himalaya.md @@ -32,6 +32,11 @@ The following is the complete skill definition that Hermes loads when this skill Himalaya is a CLI email client that lets you manage emails from the terminal using IMAP, SMTP, Notmuch, or Sendmail backends. +This skill is separate from the Hermes Email gateway adapter. The gateway +adapter lets people email the agent and uses Hermes' built-in IMAP/SMTP +adapter; this skill lets the agent operate a mailbox from terminal tools and +requires the external `himalaya` CLI. + ## References - `references/configuration.md` (config file setup + IMAP/SMTP authentication) diff --git a/website/docs/user-guide/skills/bundled/github/github-github-auth.md b/website/docs/user-guide/skills/bundled/github/github-github-auth.md index 92b9d9f6690..35e631fb237 100644 --- a/website/docs/user-guide/skills/bundled/github/github-github-auth.md +++ b/website/docs/user-guide/skills/bundled/github/github-github-auth.md @@ -238,8 +238,8 @@ if command -v gh &>/dev/null && gh auth status &>/dev/null; then echo "AUTH_METHOD=gh" elif [ -n "$GITHUB_TOKEN" ]; then echo "AUTH_METHOD=curl" -elif [ -f ~/.hermes/.env ] && grep -q "^GITHUB_TOKEN=" ~/.hermes/.env; then - export GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" ~/.hermes/.env | head -1 | cut -d= -f2 | tr -d '\n\r') +elif _hermes_env="${HERMES_HOME:-$HOME/.hermes}/.env"; [ -f "$_hermes_env" ] && grep -q "^GITHUB_TOKEN=" "$_hermes_env"; then + export GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" "$_hermes_env" | head -1 | cut -d= -f2 | tr -d '\n\r') echo "AUTH_METHOD=curl" elif grep -q "github.com" ~/.git-credentials 2>/dev/null; then export GITHUB_TOKEN=$(grep "github.com" ~/.git-credentials | head -1 | sed 's|https://[^:]*:\([^@]*\)@.*|\1|') diff --git a/website/docs/user-guide/skills/bundled/github/github-github-code-review.md b/website/docs/user-guide/skills/bundled/github/github-github-code-review.md index 56e8fa97ad2..a7adc59e119 100644 --- a/website/docs/user-guide/skills/bundled/github/github-github-code-review.md +++ b/website/docs/user-guide/skills/bundled/github/github-github-code-review.md @@ -46,8 +46,8 @@ if command -v gh &>/dev/null && gh auth status &>/dev/null; then else AUTH="git" if [ -z "$GITHUB_TOKEN" ]; then - if [ -f ~/.hermes/.env ] && grep -q "^GITHUB_TOKEN=" ~/.hermes/.env; then - GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" ~/.hermes/.env | head -1 | cut -d= -f2 | tr -d '\n\r') + if _hermes_env="${HERMES_HOME:-$HOME/.hermes}/.env"; [ -f "$_hermes_env" ] && grep -q "^GITHUB_TOKEN=" "$_hermes_env"; then + GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" "$_hermes_env" | head -1 | cut -d= -f2 | tr -d '\n\r') elif grep -q "github.com" ~/.git-credentials 2>/dev/null; then GITHUB_TOKEN=$(grep "github.com" ~/.git-credentials 2>/dev/null | head -1 | sed 's|https://[^:]*:\([^@]*\)@.*|\1|') fi diff --git a/website/docs/user-guide/skills/bundled/github/github-github-issues.md b/website/docs/user-guide/skills/bundled/github/github-github-issues.md index 6f99685d71a..fa3dc52c7e2 100644 --- a/website/docs/user-guide/skills/bundled/github/github-github-issues.md +++ b/website/docs/user-guide/skills/bundled/github/github-github-issues.md @@ -46,8 +46,8 @@ if command -v gh &>/dev/null && gh auth status &>/dev/null; then else AUTH="git" if [ -z "$GITHUB_TOKEN" ]; then - if [ -f ~/.hermes/.env ] && grep -q "^GITHUB_TOKEN=" ~/.hermes/.env; then - GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" ~/.hermes/.env | head -1 | cut -d= -f2 | tr -d '\n\r') + if _hermes_env="${HERMES_HOME:-$HOME/.hermes}/.env"; [ -f "$_hermes_env" ] && grep -q "^GITHUB_TOKEN=" "$_hermes_env"; then + GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" "$_hermes_env" | head -1 | cut -d= -f2 | tr -d '\n\r') elif grep -q "github.com" ~/.git-credentials 2>/dev/null; then GITHUB_TOKEN=$(grep "github.com" ~/.git-credentials 2>/dev/null | head -1 | sed 's|https://[^:]*:\([^@]*\)@.*|\1|') fi diff --git a/website/docs/user-guide/skills/bundled/github/github-github-pr-workflow.md b/website/docs/user-guide/skills/bundled/github/github-github-pr-workflow.md index 48aa4ea9fff..a0221be3d73 100644 --- a/website/docs/user-guide/skills/bundled/github/github-github-pr-workflow.md +++ b/website/docs/user-guide/skills/bundled/github/github-github-pr-workflow.md @@ -48,8 +48,8 @@ else AUTH="git" # Ensure we have a token for API calls if [ -z "$GITHUB_TOKEN" ]; then - if [ -f ~/.hermes/.env ] && grep -q "^GITHUB_TOKEN=" ~/.hermes/.env; then - GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" ~/.hermes/.env | head -1 | cut -d= -f2 | tr -d '\n\r') + if _hermes_env="${HERMES_HOME:-$HOME/.hermes}/.env"; [ -f "$_hermes_env" ] && grep -q "^GITHUB_TOKEN=" "$_hermes_env"; then + GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" "$_hermes_env" | head -1 | cut -d= -f2 | tr -d '\n\r') elif grep -q "github.com" ~/.git-credentials 2>/dev/null; then GITHUB_TOKEN=$(grep "github.com" ~/.git-credentials 2>/dev/null | head -1 | sed 's|https://[^:]*:\([^@]*\)@.*|\1|') fi diff --git a/website/docs/user-guide/skills/bundled/github/github-github-repo-management.md b/website/docs/user-guide/skills/bundled/github/github-github-repo-management.md index 0921e3dbccc..b87a7abdf37 100644 --- a/website/docs/user-guide/skills/bundled/github/github-github-repo-management.md +++ b/website/docs/user-guide/skills/bundled/github/github-github-repo-management.md @@ -45,8 +45,8 @@ if command -v gh &>/dev/null && gh auth status &>/dev/null; then else AUTH="git" if [ -z "$GITHUB_TOKEN" ]; then - if [ -f ~/.hermes/.env ] && grep -q "^GITHUB_TOKEN=" ~/.hermes/.env; then - GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" ~/.hermes/.env | head -1 | cut -d= -f2 | tr -d '\n\r') + if _hermes_env="${HERMES_HOME:-$HOME/.hermes}/.env"; [ -f "$_hermes_env" ] && grep -q "^GITHUB_TOKEN=" "$_hermes_env"; then + GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" "$_hermes_env" | head -1 | cut -d= -f2 | tr -d '\n\r') elif grep -q "github.com" ~/.git-credentials 2>/dev/null; then GITHUB_TOKEN=$(grep "github.com" ~/.git-credentials 2>/dev/null | head -1 | sed 's|https://[^:]*:\([^@]*\)@.*|\1|') fi diff --git a/website/docs/user-guide/skills/bundled/media/media-gif-search.md b/website/docs/user-guide/skills/bundled/media/media-gif-search.md index c26c5fd4a5e..31d0e03eb88 100644 --- a/website/docs/user-guide/skills/bundled/media/media-gif-search.md +++ b/website/docs/user-guide/skills/bundled/media/media-gif-search.md @@ -38,7 +38,7 @@ Useful for finding reaction GIFs, creating visual content, and sending GIFs in c ## Setup -Set your Tenor API key in your environment (add to `~/.hermes/.env`): +Set your Tenor API key in your environment (add to `${HERMES_HOME:-~/.hermes}/.env`): ```bash TENOR_API_KEY=your_key_here diff --git a/website/docs/user-guide/skills/bundled/note-taking/note-taking-obsidian.md b/website/docs/user-guide/skills/bundled/note-taking/note-taking-obsidian.md index e8315c2fd4f..49f317144d7 100644 --- a/website/docs/user-guide/skills/bundled/note-taking/note-taking-obsidian.md +++ b/website/docs/user-guide/skills/bundled/note-taking/note-taking-obsidian.md @@ -32,7 +32,7 @@ Use this skill for filesystem-first Obsidian vault work: reading notes, listing Use a known or resolved vault path before calling file tools. -The documented vault-path convention is the `OBSIDIAN_VAULT_PATH` environment variable, for example from `~/.hermes/.env`. If it is unset, use `~/Documents/Obsidian Vault`. +The documented vault-path convention is the `OBSIDIAN_VAULT_PATH` environment variable, for example from `${HERMES_HOME:-~/.hermes}/.env`. If it is unset, use `~/Documents/Obsidian Vault`. File tools do not expand shell variables. Do not pass paths containing `$OBSIDIAN_VAULT_PATH` to `read_file`, `write_file`, `patch`, or `search_files`; resolve the vault path first and pass a concrete absolute path. Vault paths may contain spaces, which is another reason to prefer file tools over shell commands. diff --git a/website/docs/user-guide/skills/bundled/productivity/productivity-airtable.md b/website/docs/user-guide/skills/bundled/productivity/productivity-airtable.md index bc4b4686433..05a3e13fba0 100644 --- a/website/docs/user-guide/skills/bundled/productivity/productivity-airtable.md +++ b/website/docs/user-guide/skills/bundled/productivity/productivity-airtable.md @@ -40,7 +40,7 @@ Work with Airtable's REST API directly via `curl` using the `terminal` tool. No - `data.records:write` — create / update / delete rows - `schema.bases:read` — list bases and tables 3. **Important:** in the same token UI, add each base you want to access to the token's **Access** list. PATs are scoped per-base — a valid token on the wrong base returns `403`. -4. Store the token in `~/.hermes/.env` (or via `hermes setup`): +4. Store the token in `${HERMES_HOME:-~/.hermes}/.env` (or via `hermes setup`): ``` AIRTABLE_API_KEY=pat_your_token_here ``` @@ -236,7 +236,7 @@ done ## Important Notes for Hermes - **Always use the `terminal` tool with `curl`.** Do NOT use `web_extract` (it can't send auth headers) or `browser_navigate` (needs UI auth and is slow). -- **`AIRTABLE_API_KEY` flows from `~/.hermes/.env` into the subprocess automatically** when this skill is loaded — no need to re-export it before each `curl` call. +- **`AIRTABLE_API_KEY` flows from `${HERMES_HOME:-~/.hermes}/.env` into the subprocess automatically** when this skill is loaded — no need to re-export it before each `curl` call. - **Escape curly braces in formulas carefully.** In a heredoc body, `{Status}` is literal. In a shell argument, `{Status}` is safe outside `{...}` brace-expansion context — but pass dynamic strings through `python3 urllib.parse.quote` before splicing into a URL. - **Pretty-print with `python3 -m json.tool`** (always present) rather than `jq` (optional). Only reach for `jq` when you need filtering/projection. - **Pagination is per-page, not global.** Airtable's 100-record cap is a hard limit; there is no way to bump it. Loop with `offset` until the field is absent. diff --git a/website/docs/user-guide/skills/bundled/productivity/productivity-notion.md b/website/docs/user-guide/skills/bundled/productivity/productivity-notion.md index 80487d6b88f..985240ca41f 100644 --- a/website/docs/user-guide/skills/bundled/productivity/productivity-notion.md +++ b/website/docs/user-guide/skills/bundled/productivity/productivity-notion.md @@ -41,7 +41,7 @@ Talk to Notion two ways. Same integration token works for both — pick by what' 1. Create an integration at https://notion.so/my-integrations 2. Copy the API key (starts with `ntn_` or `secret_`) -3. Store in `~/.hermes/.env`: +3. Store in `${HERMES_HOME:-~/.hermes}/.env`: ``` NOTION_API_KEY=ntn_your_key_here ``` @@ -65,7 +65,7 @@ export NOTION_API_TOKEN=$NOTION_API_KEY # ntn reads NOTION_API_TOKEN export NOTION_KEYRING=0 # don't try to use the OS keychain ``` -Add those exports to your shell profile (or to `~/.hermes/.env`) so every session inherits them. +Add those exports to your shell profile (or to `${HERMES_HOME:-~/.hermes}/.env`) so every session inherits them. ### 3. Choose path at runtime diff --git a/website/docs/user-guide/skills/bundled/productivity/productivity-teams-meeting-pipeline.md b/website/docs/user-guide/skills/bundled/productivity/productivity-teams-meeting-pipeline.md index 125021bc4cb..8fb4c066302 100644 --- a/website/docs/user-guide/skills/bundled/productivity/productivity-teams-meeting-pipeline.md +++ b/website/docs/user-guide/skills/bundled/productivity/productivity-teams-meeting-pipeline.md @@ -50,7 +50,7 @@ Multilingual trigger examples (not exhaustive): ## Prerequisites -Before using the pipeline, verify these are set in `~/.hermes/.env`: +Before using the pipeline, verify these are set in `${HERMES_HOME:-~/.hermes}/.env`: ```bash MSGRAPH_TENANT_ID=... diff --git a/website/docs/user-guide/skills/bundled/research/research-llm-wiki.md b/website/docs/user-guide/skills/bundled/research/research-llm-wiki.md index 419c7cd7cb2..a6097a1a07c 100644 --- a/website/docs/user-guide/skills/bundled/research/research-llm-wiki.md +++ b/website/docs/user-guide/skills/bundled/research/research-llm-wiki.md @@ -52,7 +52,7 @@ Use this skill when the user: ## Wiki Location -**Location:** Set via `WIKI_PATH` environment variable (e.g. in `~/.hermes/.env`). +**Location:** Set via `WIKI_PATH` environment variable (e.g. in `${HERMES_HOME:-~/.hermes}/.env`). If unset, defaults to `~/wiki`. diff --git a/website/docs/user-guide/skills/bundled/research/research-research-paper-writing.md b/website/docs/user-guide/skills/bundled/research/research-research-paper-writing.md index 9dc216ebac7..611215c06c3 100644 --- a/website/docs/user-guide/skills/bundled/research/research-research-paper-writing.md +++ b/website/docs/user-guide/skills/bundled/research/research-research-paper-writing.md @@ -22,7 +22,7 @@ Write ML papers for NeurIPS/ICML/ICLR: design→submit. | Dependencies | `semanticscholar`, `arxiv`, `habanero`, `requests`, `scipy`, `numpy`, `matplotlib`, `SciencePlots` | | Platforms | linux, macos | | Tags | `Research`, `Paper Writing`, `Experiments`, `ML`, `AI`, `NeurIPS`, `ICML`, `ICLR`, `ACL`, `AAAI`, `COLM`, `LaTeX`, `Citations`, `Statistical Analysis` | -| Related skills | [`arxiv`](/docs/user-guide/skills/bundled/research/research-arxiv), `ml-paper-writing`, [`subagent-driven-development`](/docs/user-guide/skills/bundled/software-development/software-development-subagent-driven-development), [`plan`](/docs/user-guide/skills/bundled/software-development/software-development-plan) | +| Related skills | [`arxiv`](/docs/user-guide/skills/bundled/research/research-arxiv), `ml-paper-writing`, [`subagent-driven-development`](/docs/user-guide/skills/optional/software-development/software-development-subagent-driven-development), [`plan`](/docs/user-guide/skills/bundled/software-development/software-development-plan) | ## Reference: full SKILL.md diff --git a/website/docs/user-guide/skills/bundled/software-development/software-development-node-inspect-debugger.md b/website/docs/user-guide/skills/bundled/software-development/software-development-node-inspect-debugger.md index deddf5dafdb..5257512e9e6 100644 --- a/website/docs/user-guide/skills/bundled/software-development/software-development-node-inspect-debugger.md +++ b/website/docs/user-guide/skills/bundled/software-development/software-development-node-inspect-debugger.md @@ -21,7 +21,7 @@ Debug Node.js via --inspect + Chrome DevTools Protocol CLI. | License | MIT | | Platforms | linux, macos, windows | | Tags | `debugging`, `nodejs`, `node-inspect`, `cdp`, `breakpoints`, `ui-tui` | -| Related skills | [`systematic-debugging`](/docs/user-guide/skills/bundled/software-development/software-development-systematic-debugging), [`python-debugpy`](/docs/user-guide/skills/bundled/software-development/software-development-python-debugpy), [`debugging-hermes-tui-commands`](/docs/user-guide/skills/bundled/software-development/software-development-debugging-hermes-tui-commands) | +| Related skills | [`systematic-debugging`](/docs/user-guide/skills/bundled/software-development/software-development-systematic-debugging), [`python-debugpy`](/docs/user-guide/skills/bundled/software-development/software-development-python-debugpy), `debugging-hermes-tui-commands` | ## Reference: full SKILL.md diff --git a/website/docs/user-guide/skills/bundled/software-development/software-development-python-debugpy.md b/website/docs/user-guide/skills/bundled/software-development/software-development-python-debugpy.md index 0524b1f3ab9..dbc26409efe 100644 --- a/website/docs/user-guide/skills/bundled/software-development/software-development-python-debugpy.md +++ b/website/docs/user-guide/skills/bundled/software-development/software-development-python-debugpy.md @@ -21,7 +21,7 @@ Debug Python: pdb REPL + debugpy remote (DAP). | License | MIT | | Platforms | linux, macos | | Tags | `debugging`, `python`, `pdb`, `debugpy`, `breakpoints`, `dap`, `post-mortem` | -| Related skills | [`systematic-debugging`](/docs/user-guide/skills/bundled/software-development/software-development-systematic-debugging), [`node-inspect-debugger`](/docs/user-guide/skills/bundled/software-development/software-development-node-inspect-debugger), [`debugging-hermes-tui-commands`](/docs/user-guide/skills/bundled/software-development/software-development-debugging-hermes-tui-commands) | +| Related skills | [`systematic-debugging`](/docs/user-guide/skills/bundled/software-development/software-development-systematic-debugging), [`node-inspect-debugger`](/docs/user-guide/skills/bundled/software-development/software-development-node-inspect-debugger), `debugging-hermes-tui-commands` | ## Reference: full SKILL.md diff --git a/website/docs/user-guide/skills/bundled/software-development/software-development-spike.md b/website/docs/user-guide/skills/bundled/software-development/software-development-spike.md index 56c0954b698..694cdcbf7af 100644 --- a/website/docs/user-guide/skills/bundled/software-development/software-development-spike.md +++ b/website/docs/user-guide/skills/bundled/software-development/software-development-spike.md @@ -21,7 +21,7 @@ Throwaway experiments to validate an idea before build. | License | MIT | | Platforms | linux, macos, windows | | Tags | `spike`, `prototype`, `experiment`, `feasibility`, `throwaway`, `exploration`, `research`, `planning`, `mvp`, `proof-of-concept` | -| Related skills | [`sketch`](/docs/user-guide/skills/bundled/creative/creative-sketch), [`subagent-driven-development`](/docs/user-guide/skills/optional/software-development/software-development-subagent-driven-development), [`plan`](/docs/user-guide/skills/bundled/software-development/software-development-plan) | +| Related skills | [`html-artifact`](/docs/user-guide/skills/bundled/creative/creative-html-artifact), [`subagent-driven-development`](/docs/user-guide/skills/optional/software-development/software-development-subagent-driven-development), [`plan`](/docs/user-guide/skills/bundled/software-development/software-development-plan) | ## Reference: full SKILL.md diff --git a/website/docs/user-guide/skills/optional/autonomous-ai-agents/autonomous-ai-agents-honcho.md b/website/docs/user-guide/skills/optional/autonomous-ai-agents/autonomous-ai-agents-honcho.md index 1b989116636..a54a2a0dea0 100644 --- a/website/docs/user-guide/skills/optional/autonomous-ai-agents/autonomous-ai-agents-honcho.md +++ b/website/docs/user-guide/skills/optional/autonomous-ai-agents/autonomous-ai-agents-honcho.md @@ -47,14 +47,14 @@ Honcho provides AI-native cross-session user modeling. It learns who the user is ### Cloud (app.honcho.dev) ```bash -hermes honcho setup +hermes memory setup honcho # select "cloud", paste API key from https://app.honcho.dev ``` ### Self-hosted ```bash -hermes honcho setup +hermes memory setup honcho # select "local", enter base URL (e.g. http://localhost:8000) ``` diff --git a/website/docs/user-guide/skills/optional/blockchain/blockchain-hyperliquid.md b/website/docs/user-guide/skills/optional/blockchain/blockchain-hyperliquid.md index 8651bc979f6..177dfe36a10 100644 --- a/website/docs/user-guide/skills/optional/blockchain/blockchain-hyperliquid.md +++ b/website/docs/user-guide/skills/optional/blockchain/blockchain-hyperliquid.md @@ -53,7 +53,7 @@ Read-only — no API key, no signing, no order placement. Stdlib only — no external packages, no API key. -The script reads `~/.hermes/.env` for two optional defaults: +The script reads `${HERMES_HOME:-~/.hermes}/.env` for two optional defaults: - `HYPERLIQUID_API_URL` — defaults to `https://api.hyperliquid.xyz`. Set to `https://api.hyperliquid-testnet.xyz` for testnet. @@ -97,7 +97,7 @@ hyperliquid_client.py export [--interval 1h] [--hours N] [--output PATH] ``` For `state`, `spot-balances`, `fills`, `orders`, and `review`, the address is -optional when `HYPERLIQUID_USER_ADDRESS` is set in `~/.hermes/.env`. +optional when `HYPERLIQUID_USER_ADDRESS` is set in `${HERMES_HOME:-~/.hermes}/.env`. --- diff --git a/website/docs/user-guide/skills/optional/creative/creative-concept-diagrams.md b/website/docs/user-guide/skills/optional/creative/creative-concept-diagrams.md deleted file mode 100644 index 9b3ba92b3bd..00000000000 --- a/website/docs/user-guide/skills/optional/creative/creative-concept-diagrams.md +++ /dev/null @@ -1,379 +0,0 @@ ---- -title: "Concept Diagrams" -sidebar_label: "Concept Diagrams" -description: "Generate flat, minimal light/dark-aware SVG diagrams as standalone HTML files, using a unified educational visual language with 9 semantic color ramps, sente..." ---- - -{/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */} - -# Concept Diagrams - -Generate flat, minimal light/dark-aware SVG diagrams as standalone HTML files, using a unified educational visual language with 9 semantic color ramps, sentence-case typography, and automatic dark mode. Best suited for educational and non-software visuals — physics setups, chemistry mechanisms, math curves, physical objects (aircraft, turbines, smartphones, mechanical watches), anatomy, floor plans, cross-sections, narrative journeys (lifecycle of X, process of Y), hub-spoke system integrations (smart city, IoT), and exploded layer views. If a more specialized skill exists for the subject (dedicated software/cloud architecture, hand-drawn sketches, animated explainers, etc.), prefer that — otherwise this skill can also serve as a general-purpose SVG diagram fallback with a clean educational look. Ships with 15 example diagrams. - -## Skill metadata - -| | | -|---|---| -| Source | Optional — install with `hermes skills install official/creative/concept-diagrams` | -| Path | `optional-skills/creative/concept-diagrams` | -| Version | `0.1.0` | -| Author | v1k22 (original PR), ported into hermes-agent | -| License | MIT | -| Platforms | linux, macos, windows | -| Tags | `diagrams`, `svg`, `visualization`, `education`, `physics`, `chemistry`, `engineering` | -| Related skills | [`architecture-diagram`](/docs/user-guide/skills/bundled/creative/creative-architecture-diagram), [`excalidraw`](/docs/user-guide/skills/bundled/creative/creative-excalidraw), `generative-widgets` | - -## Reference: full SKILL.md - -:::info -The following is the complete skill definition that Hermes loads when this skill is triggered. This is what the agent sees as instructions when the skill is active. -::: - -# Concept Diagrams - -Generate production-quality SVG diagrams with a unified flat, minimal design system. Output is a single self-contained HTML file that renders identically in any modern browser, with automatic light/dark mode. - -## Scope - -**Best suited for:** -- Physics setups, chemistry mechanisms, math curves, biology -- Physical objects (aircraft, turbines, smartphones, mechanical watches, cells) -- Anatomy, cross-sections, exploded layer views -- Floor plans, architectural conversions -- Narrative journeys (lifecycle of X, process of Y) -- Hub-spoke system integrations (smart city, IoT networks, electricity grids) -- Educational / textbook-style visuals in any domain -- Quantitative charts (grouped bars, energy profiles) - -**Look elsewhere first for:** -- Dedicated software / cloud infrastructure architecture with a dark tech aesthetic (consider `architecture-diagram` if available) -- Hand-drawn whiteboard sketches (consider `excalidraw` if available) -- Animated explainers or video output (consider an animation skill) - -If a more specialized skill is available for the subject, prefer that. If none fits, this skill can serve as a general-purpose SVG diagram fallback — the output will carry the clean educational aesthetic described below, which is a reasonable default for almost any subject. - -## Workflow - -1. Decide on the diagram type (see Diagram Types below). -2. Lay out components using the Design System rules. -3. Write the full HTML page using `templates/template.html` as the wrapper — paste your SVG where the template says ``. -4. Save as a standalone `.html` file (for example `~/my-diagram.html` or `./my-diagram.html`). -5. User opens it directly in a browser — no server, no dependencies. - -Optional: if the user wants a browsable gallery of multiple diagrams, see "Local Preview Server" at the bottom. - -Load the HTML template: -``` -skill_view(name="concept-diagrams", file_path="templates/template.html") -``` - -The template embeds the full CSS design system (`c-*` color classes, text classes, light/dark variables, arrow marker styles). The SVG you generate relies on these classes being present on the hosting page. - ---- - -## Design System - -### Philosophy - -- **Flat**: no gradients, drop shadows, blur, glow, or neon effects. -- **Minimal**: show the essential. No decorative icons inside boxes. -- **Consistent**: same colors, spacing, typography, and stroke widths across every diagram. -- **Dark-mode ready**: all colors auto-adapt via CSS classes — no per-mode SVG. - -### Color Palette - -9 color ramps, each with 7 stops. Put the class name on a `` or shape element; the template CSS handles both modes. - -| Class | 50 (lightest) | 100 | 200 | 400 | 600 | 800 | 900 (darkest) | -|------------|---------------|---------|---------|---------|---------|---------|---------------| -| `c-purple` | #EEEDFE | #CECBF6 | #AFA9EC | #7F77DD | #534AB7 | #3C3489 | #26215C | -| `c-teal` | #E1F5EE | #9FE1CB | #5DCAA5 | #1D9E75 | #0F6E56 | #085041 | #04342C | -| `c-coral` | #FAECE7 | #F5C4B3 | #F0997B | #D85A30 | #993C1D | #712B13 | #4A1B0C | -| `c-pink` | #FBEAF0 | #F4C0D1 | #ED93B1 | #D4537E | #993556 | #72243E | #4B1528 | -| `c-gray` | #F1EFE8 | #D3D1C7 | #B4B2A9 | #888780 | #5F5E5A | #444441 | #2C2C2A | -| `c-blue` | #E6F1FB | #B5D4F4 | #85B7EB | #378ADD | #185FA5 | #0C447C | #042C53 | -| `c-green` | #EAF3DE | #C0DD97 | #97C459 | #639922 | #3B6D11 | #27500A | #173404 | -| `c-amber` | #FAEEDA | #FAC775 | #EF9F27 | #BA7517 | #854F0B | #633806 | #412402 | -| `c-red` | #FCEBEB | #F7C1C1 | #F09595 | #E24B4A | #A32D2D | #791F1F | #501313 | - -#### Color Assignment Rules - -Color encodes **meaning**, not sequence. Never cycle through colors like a rainbow. - -- Group nodes by **category** — all nodes of the same type share one color. -- Use `c-gray` for neutral/structural nodes (start, end, generic steps, users). -- Use **2-3 colors per diagram**, not 6+. -- Prefer `c-purple`, `c-teal`, `c-coral`, `c-pink` for general categories. -- Reserve `c-blue`, `c-green`, `c-amber`, `c-red` for semantic meaning (info, success, warning, error). - -Light/dark stop mapping (handled by the template CSS — just use the class): -- Light mode: 50 fill + 600 stroke + 800 title / 600 subtitle -- Dark mode: 800 fill + 200 stroke + 100 title / 200 subtitle - -### Typography - -Only two font sizes. No exceptions. - -| Class | Size | Weight | Use | -|-------|------|--------|-----| -| `th` | 14px | 500 | Node titles, region labels | -| `ts` | 12px | 400 | Subtitles, descriptions, arrow labels | -| `t` | 14px | 400 | General text | - -- **Sentence case always.** Never Title Case, never ALL CAPS. -- Every `` MUST carry a class (`t`, `ts`, or `th`). No unclassed text. -- `dominant-baseline="central"` on all text inside boxes. -- `text-anchor="middle"` for centered text in boxes. - -**Width estimation (approx):** -- 14px weight 500: ~8px per character -- 12px weight 400: ~6.5px per character -- Always verify: `box_width >= (char_count × px_per_char) + 48` (24px padding each side) - -### Spacing & Layout - -- **ViewBox**: `viewBox="0 0 680 H"` where H = content height + 40px buffer. -- **Safe area**: x=40 to x=640, y=40 to y=(H-40). -- **Between boxes**: 60px minimum gap. -- **Inside boxes**: 24px horizontal padding, 12px vertical padding. -- **Arrowhead gap**: 10px between arrowhead and box edge. -- **Single-line box**: 44px height. -- **Two-line box**: 56px height, 18px between title and subtitle baselines. -- **Container padding**: 20px minimum inside every container. -- **Max nesting**: 2-3 levels deep. Deeper gets unreadable at 680px width. - -### Stroke & Shape - -- **Stroke width**: 0.5px on all node borders. Not 1px, not 2px. -- **Rect rounding**: `rx="8"` for nodes, `rx="12"` for inner containers, `rx="16"` to `rx="20"` for outer containers. -- **Connector paths**: MUST have `fill="none"`. SVG defaults to `fill: black` otherwise. - -### Arrow Marker - -Include this `` block at the start of **every** SVG: - -```xml - - - - - -``` - -Use `marker-end="url(#arrow)"` on lines. The arrowhead inherits the line color via `context-stroke`. - -### CSS Classes (Provided by the Template) - -The template page provides: - -- Text: `.t`, `.ts`, `.th` -- Neutral: `.box`, `.arr`, `.leader`, `.node` -- Color ramps: `.c-purple`, `.c-teal`, `.c-coral`, `.c-pink`, `.c-gray`, `.c-blue`, `.c-green`, `.c-amber`, `.c-red` (all with automatic light/dark mode) - -You do **not** need to redefine these — just apply them in your SVG. The template file contains the full CSS definitions. - ---- - -## SVG Boilerplate - -Every SVG inside the template page starts with this exact structure: - -```xml - - - - - - - - - - -``` - -Replace `{HEIGHT}` with the actual computed height (last element bottom + 40px). - -### Node Patterns - -**Single-line node (44px):** -```xml - - - Service name - -``` - -**Two-line node (56px):** -```xml - - - Service name - Short description - -``` - -**Connector (no label):** -```xml - -``` - -**Container (dashed or solid):** -```xml - - - Container label - Subtitle info - -``` - ---- - -## Diagram Types - -Choose the layout that fits the subject: - -1. **Flowchart** — CI/CD pipelines, request lifecycles, approval workflows, data processing. Single-direction flow (top-down or left-right). Max 4-5 nodes per row. -2. **Structural / Containment** — Cloud infrastructure nesting, system architecture with layers. Large outer containers with inner regions. Dashed rects for logical groupings. -3. **API / Endpoint Map** — REST routes, GraphQL schemas. Tree from root, branching to resource groups, each containing endpoint nodes. -4. **Microservice Topology** — Service mesh, event-driven systems. Services as nodes, arrows for communication patterns, message queues between. -5. **Data Flow** — ETL pipelines, streaming architectures. Left-to-right flow from sources through processing to sinks. -6. **Physical / Structural** — Vehicles, buildings, hardware, anatomy. Use shapes that match the physical form — `` for curved bodies, `` for tapered shapes, ``/`` for cylindrical parts, nested `` for compartments. See `references/physical-shape-cookbook.md`. -7. **Infrastructure / Systems Integration** — Smart cities, IoT networks, multi-domain systems. Hub-spoke layout with central platform connecting subsystems. Semantic line styles (`.data-line`, `.power-line`, `.water-pipe`, `.road`). See `references/infrastructure-patterns.md`. -8. **UI / Dashboard Mockups** — Admin panels, monitoring dashboards. Screen frame with nested chart/gauge/indicator elements. See `references/dashboard-patterns.md`. - -For physical, infrastructure, and dashboard diagrams, load the matching reference file before generating — each one provides ready-made CSS classes and shape primitives. - ---- - -## Validation Checklist - -Before finalizing any SVG, verify ALL of the following: - -1. Every `` has class `t`, `ts`, or `th`. -2. Every `` inside a box has `dominant-baseline="central"`. -3. Every connector `` or `` used as arrow has `fill="none"`. -4. No arrow line crosses through an unrelated box. -5. `box_width >= (longest_label_chars × 8) + 48` for 14px text. -6. `box_width >= (longest_label_chars × 6.5) + 48` for 12px text. -7. ViewBox height = bottom-most element + 40px. -8. All content stays within x=40 to x=640. -9. Color classes (`c-*`) are on `` or shape elements, never on `` connectors. -10. Arrow `` block is present. -11. No gradients, shadows, blur, or glow effects. -12. Stroke width is 0.5px on all node borders. - ---- - -## Output & Preview - -### Default: standalone HTML file - -Write a single `.html` file the user can open directly. No server, no dependencies, works offline. Pattern: - -```python -# 1. Load the template -template = skill_view("concept-diagrams", "templates/template.html") - -# 2. Fill in title, subtitle, and paste your SVG -html = template.replace( - "", "SN2 reaction mechanism" -).replace( - "", "Bimolecular nucleophilic substitution" -).replace( - "", svg_content -) - -# 3. Write to a user-chosen path (or ./ by default) -write_file("./sn2-mechanism.html", html) -``` - -Tell the user how to open it: - -``` -# macOS -open ./sn2-mechanism.html -# Linux -xdg-open ./sn2-mechanism.html -``` - -### Optional: local preview server (multi-diagram gallery) - -Only use this when the user explicitly wants a browsable gallery of multiple diagrams. - -**Rules:** -- Bind to `127.0.0.1` only. Never `0.0.0.0`. Exposing diagrams on all network interfaces is a security hazard on shared networks. -- Pick a free port (do NOT hard-code one) and tell the user the chosen URL. -- The server is optional and opt-in — prefer the standalone HTML file first. - -Recommended pattern (lets the OS pick a free ephemeral port): - -```bash -# Put each diagram in its own folder under .diagrams/ -mkdir -p .diagrams/sn2-mechanism -# ...write .diagrams/sn2-mechanism/index.html... - -# Serve on loopback only, free port -cd .diagrams && python3 -c " -import http.server, socketserver -with socketserver.TCPServer(('127.0.0.1', 0), http.server.SimpleHTTPRequestHandler) as s: - print(f'Serving at http://127.0.0.1:{s.server_address[1]}/') - s.serve_forever() -" & -``` - -If the user insists on a fixed port, use `127.0.0.1:` — still never `0.0.0.0`. Document how to stop the server (`kill %1` or `pkill -f "http.server"`). - ---- - -## Examples Reference - -The `examples/` directory ships 15 complete, tested diagrams. Browse them for working patterns before writing a new diagram of a similar type: - -| File | Type | Demonstrates | -|------|------|--------------| -| `hospital-emergency-department-flow.md` | Flowchart | Priority routing with semantic colors | -| `feature-film-production-pipeline.md` | Flowchart | Phased workflow, horizontal sub-flows | -| `automated-password-reset-flow.md` | Flowchart | Auth flow with error branches | -| `autonomous-llm-research-agent-flow.md` | Flowchart | Loop-back arrows, decision branches | -| `place-order-uml-sequence.md` | Sequence | UML sequence diagram style | -| `commercial-aircraft-structure.md` | Physical | Paths, polygons, ellipses for realistic shapes | -| `wind-turbine-structure.md` | Physical cross-section | Underground/above-ground separation, color coding | -| `smartphone-layer-anatomy.md` | Exploded view | Alternating left/right labels, layered components | -| `apartment-floor-plan-conversion.md` | Floor plan | Walls, doors, proposed changes in dotted red | -| `banana-journey-tree-to-smoothie.md` | Narrative journey | Winding path, progressive state changes | -| `cpu-ooo-microarchitecture.md` | Hardware pipeline | Fan-out, memory hierarchy sidebar | -| `sn2-reaction-mechanism.md` | Chemistry | Molecules, curved arrows, energy profile | -| `smart-city-infrastructure.md` | Hub-spoke | Semantic line styles per system | -| `electricity-grid-flow.md` | Multi-stage flow | Voltage hierarchy, flow markers | -| `ml-benchmark-grouped-bar-chart.md` | Chart | Grouped bars, dual axis | - -Load any example with: -``` -skill_view(name="concept-diagrams", file_path="examples/") -``` - ---- - -## Quick Reference: What to Use When - -| User says | Diagram type | Suggested colors | -|-----------|--------------|------------------| -| "show the pipeline" | Flowchart | gray start/end, purple steps, red errors, teal deploy | -| "draw the data flow" | Data pipeline (left-right) | gray sources, purple processing, teal sinks | -| "visualize the system" | Structural (containment) | purple container, teal services, coral data | -| "map the endpoints" | API tree | purple root, one ramp per resource group | -| "show the services" | Microservice topology | gray ingress, teal services, purple bus, coral workers | -| "draw the aircraft/vehicle" | Physical | paths, polygons, ellipses for realistic shapes | -| "smart city / IoT" | Hub-spoke integration | semantic line styles per subsystem | -| "show the dashboard" | UI mockup | dark screen, chart colors: teal, purple, coral for alerts | -| "power grid / electricity" | Multi-stage flow | voltage hierarchy (HV/MV/LV line weights) | -| "wind turbine / turbine" | Physical cross-section | foundation + tower cutaway + nacelle color-coded | -| "journey of X / lifecycle" | Narrative journey | winding path, progressive state changes | -| "layers of X / exploded" | Exploded layer view | vertical stack, alternating labels | -| "CPU / pipeline" | Hardware pipeline | vertical stages, fan-out to execution ports | -| "floor plan / apartment" | Floor plan | walls, doors, proposed changes in dotted red | -| "reaction mechanism" | Chemistry | atoms, bonds, curved arrows, transition state, energy profile | diff --git a/website/docs/user-guide/skills/optional/creative/creative-kanban-video-orchestrator.md b/website/docs/user-guide/skills/optional/creative/creative-kanban-video-orchestrator.md index 8fa3cdf127f..a148ba6d2d6 100644 --- a/website/docs/user-guide/skills/optional/creative/creative-kanban-video-orchestrator.md +++ b/website/docs/user-guide/skills/optional/creative/creative-kanban-video-orchestrator.md @@ -21,7 +21,7 @@ Plan, set up, and monitor a multi-agent video production pipeline backed by Herm | License | MIT | | Platforms | linux, macos, windows | | Tags | `video`, `kanban`, `multi-agent`, `orchestration`, `production-pipeline` | -| Related skills | [`kanban-orchestrator`](/docs/user-guide/skills/bundled/devops/devops-kanban-orchestrator), [`kanban-worker`](/docs/user-guide/skills/bundled/devops/devops-kanban-worker), [`ascii-video`](/docs/user-guide/skills/bundled/creative/creative-ascii-video), [`manim-video`](/docs/user-guide/skills/bundled/creative/creative-manim-video), [`p5js`](/docs/user-guide/skills/bundled/creative/creative-p5js), [`comfyui`](/docs/user-guide/skills/bundled/creative/creative-comfyui), [`touchdesigner-mcp`](/docs/user-guide/skills/bundled/creative/creative-touchdesigner-mcp), [`blender-mcp`](/docs/user-guide/skills/optional/creative/creative-blender-mcp), [`pixel-art`](/docs/user-guide/skills/bundled/creative/creative-pixel-art), [`ascii-art`](/docs/user-guide/skills/bundled/creative/creative-ascii-art), [`songwriting-and-ai-music`](/docs/user-guide/skills/bundled/creative/creative-songwriting-and-ai-music), [`heartmula`](/docs/user-guide/skills/bundled/media/media-heartmula), [`songsee`](/docs/user-guide/skills/bundled/media/media-songsee), [`spotify`](/docs/user-guide/skills/bundled/media/media-spotify), [`youtube-content`](/docs/user-guide/skills/bundled/media/media-youtube-content), [`claude-design`](/docs/user-guide/skills/bundled/creative/creative-claude-design), [`excalidraw`](/docs/user-guide/skills/bundled/creative/creative-excalidraw), [`architecture-diagram`](/docs/user-guide/skills/bundled/creative/creative-architecture-diagram), [`concept-diagrams`](/docs/user-guide/skills/optional/creative/creative-concept-diagrams), [`baoyu-comic`](/docs/user-guide/skills/bundled/creative/creative-baoyu-comic), [`baoyu-infographic`](/docs/user-guide/skills/bundled/creative/creative-baoyu-infographic), [`humanizer`](/docs/user-guide/skills/bundled/creative/creative-humanizer), [`gif-search`](/docs/user-guide/skills/bundled/media/media-gif-search), [`meme-generation`](/docs/user-guide/skills/optional/creative/creative-meme-generation) | +| Related skills | [`kanban-orchestrator`](/docs/user-guide/skills/bundled/devops/devops-kanban-orchestrator), [`kanban-worker`](/docs/user-guide/skills/bundled/devops/devops-kanban-worker), [`ascii-video`](/docs/user-guide/skills/bundled/creative/creative-ascii-video), [`manim-video`](/docs/user-guide/skills/bundled/creative/creative-manim-video), [`p5js`](/docs/user-guide/skills/bundled/creative/creative-p5js), [`comfyui`](/docs/user-guide/skills/bundled/creative/creative-comfyui), [`touchdesigner-mcp`](/docs/user-guide/skills/bundled/creative/creative-touchdesigner-mcp), [`blender-mcp`](/docs/user-guide/skills/optional/creative/creative-blender-mcp), [`pixel-art`](/docs/user-guide/skills/optional/creative/creative-pixel-art), [`ascii-art`](/docs/user-guide/skills/bundled/creative/creative-ascii-art), [`songwriting-and-ai-music`](/docs/user-guide/skills/bundled/creative/creative-songwriting-and-ai-music), [`heartmula`](/docs/user-guide/skills/bundled/media/media-heartmula), [`songsee`](/docs/user-guide/skills/bundled/media/media-songsee), `spotify`, [`youtube-content`](/docs/user-guide/skills/bundled/media/media-youtube-content), [`claude-design`](/docs/user-guide/skills/bundled/creative/creative-claude-design), [`excalidraw`](/docs/user-guide/skills/bundled/creative/creative-excalidraw), [`html-artifact`](/docs/user-guide/skills/bundled/creative/creative-html-artifact), [`baoyu-comic`](/docs/user-guide/skills/optional/creative/creative-baoyu-comic), [`baoyu-infographic`](/docs/user-guide/skills/bundled/creative/creative-baoyu-infographic), [`humanizer`](/docs/user-guide/skills/bundled/creative/creative-humanizer), [`gif-search`](/docs/user-guide/skills/bundled/media/media-gif-search), [`meme-generation`](/docs/user-guide/skills/optional/creative/creative-meme-generation) | ## Reference: full SKILL.md @@ -194,7 +194,7 @@ task graphs. See **[references/examples.md](https://github.com/NousResearch/herm right human-review gates. 8. **Verify API keys BEFORE firing.** External APIs (TTS, image-gen, - image-to-video) need keys in `~/.hermes/.env` or the user's secret store. + image-to-video) need keys in `${HERMES_HOME:-~/.hermes}/.env` or the user's secret store. A worker that hits a missing-key error wastes a task slot. The setup script's `check_key` helper aborts cleanly if a required key is missing. diff --git a/website/docs/user-guide/skills/optional/devops/devops-pinggy-tunnel.md b/website/docs/user-guide/skills/optional/devops/devops-pinggy-tunnel.md index 19f431f1967..18fb572bdcb 100644 --- a/website/docs/user-guide/skills/optional/devops/devops-pinggy-tunnel.md +++ b/website/docs/user-guide/skills/optional/devops/devops-pinggy-tunnel.md @@ -21,7 +21,7 @@ Zero-install localhost tunnels over SSH via Pinggy. | License | MIT | | Platforms | linux, macos, windows | | Tags | `Pinggy`, `Tunnel`, `Networking`, `SSH`, `Webhook`, `Localhost` | -| Related skills | `cloudflared-quick-tunnel`, [`webhook-subscriptions`](/docs/user-guide/skills/bundled/devops/devops-webhook-subscriptions) | +| Related skills | `cloudflared-quick-tunnel`, `webhook-subscriptions` | ## Reference: full SKILL.md diff --git a/website/docs/user-guide/skills/optional/devops/devops-watchers.md b/website/docs/user-guide/skills/optional/devops/devops-watchers.md index 8a56162bdb8..9d2fc7f7523 100644 --- a/website/docs/user-guide/skills/optional/devops/devops-watchers.md +++ b/website/docs/user-guide/skills/optional/devops/devops-watchers.md @@ -77,7 +77,7 @@ python $HERMES_HOME/skills/devops/watchers/scripts/watch_rss.py \ --name hn --url https://news.ycombinator.com/rss --max 5 ``` -Watch a GitHub repo (set `GITHUB_TOKEN` in `~/.hermes/.env` to avoid the 60 req/hr anonymous rate limit): +Watch a GitHub repo (set `GITHUB_TOKEN` in `${HERMES_HOME:-~/.hermes}/.env` to avoid the 60 req/hr anonymous rate limit): ```bash python $HERMES_HOME/skills/devops/watchers/scripts/watch_github.py \ diff --git a/website/docs/user-guide/skills/optional/mcp/mcp-fastmcp.md b/website/docs/user-guide/skills/optional/mcp/mcp-fastmcp.md index 2defe89d4eb..3efe47b12b8 100644 --- a/website/docs/user-guide/skills/optional/mcp/mcp-fastmcp.md +++ b/website/docs/user-guide/skills/optional/mcp/mcp-fastmcp.md @@ -21,7 +21,7 @@ Build, test, inspect, install, and deploy MCP servers with FastMCP in Python. Us | License | MIT | | Platforms | linux, macos, windows | | Tags | `MCP`, `FastMCP`, `Python`, `Tools`, `Resources`, `Prompts`, `Deployment` | -| Related skills | [`native-mcp`](/docs/user-guide/skills/bundled/mcp/mcp-native-mcp), [`mcporter`](/docs/user-guide/skills/optional/mcp/mcp-mcporter) | +| Related skills | `native-mcp`, [`mcporter`](/docs/user-guide/skills/optional/mcp/mcp-mcporter) | ## Reference: full SKILL.md diff --git a/website/docs/user-guide/skills/optional/payments/payments-stripe-projects.md b/website/docs/user-guide/skills/optional/payments/payments-stripe-projects.md index 74e60876bf5..fcd20673edd 100644 --- a/website/docs/user-guide/skills/optional/payments/payments-stripe-projects.md +++ b/website/docs/user-guide/skills/optional/payments/payments-stripe-projects.md @@ -44,7 +44,7 @@ Trigger phrases: - "manage my stack credentials", "rotate this key", "upgrade my plan" - "what providers can I add?" -If the user already has a provider account, this skill can still connect it with `stripe projects link <provider>`. If the user wants to use an existing provider resource, such as an existing database or Vercel project, check provider support first; many providers currently support provisioning new resources but not importing existing ones. +If the user already has a provider account, this skill can still connect it with `stripe projects link `. If the user wants to use an existing provider resource, such as an existing database or Vercel project, check provider support first; many providers currently support provisioning new resources but not importing existing ones. ## Prerequisites diff --git a/website/docs/user-guide/skills/optional/productivity/productivity-canvas.md b/website/docs/user-guide/skills/optional/productivity/productivity-canvas.md index e94a81b0407..11bbf7e2006 100644 --- a/website/docs/user-guide/skills/optional/productivity/productivity-canvas.md +++ b/website/docs/user-guide/skills/optional/productivity/productivity-canvas.md @@ -42,7 +42,7 @@ Read-only access to Canvas LMS for listing courses and assignments. 2. Go to **Account → Settings** (click your profile icon, then Settings) 3. Scroll to **Approved Integrations** and click **+ New Access Token** 4. Name the token (e.g., "Hermes Agent"), set an optional expiry, and click **Generate Token** -5. Copy the token and add to `~/.hermes/.env`: +5. Copy the token and add to `${HERMES_HOME:-~/.hermes}/.env`: ``` CANVAS_API_TOKEN=your_token_here diff --git a/website/docs/user-guide/skills/optional/productivity/productivity-shopify.md b/website/docs/user-guide/skills/optional/productivity/productivity-shopify.md index 61bc95cfa66..97d4116d82d 100644 --- a/website/docs/user-guide/skills/optional/productivity/productivity-shopify.md +++ b/website/docs/user-guide/skills/optional/productivity/productivity-shopify.md @@ -40,7 +40,7 @@ The REST Admin API is legacy since 2024-04 and only receives security fixes. **U 1. In Shopify admin: **Settings → Apps and sales channels → Develop apps → Create an app**. 2. Click **Configure Admin API scopes**, select what you need (examples below), save. 3. **Install app** → the Admin API access token appears ONCE. Copy it immediately — Shopify will never show it again. Tokens start with `shpat_`. -4. Save to `~/.hermes/.env`: +4. Save to `${HERMES_HOME:-~/.hermes}/.env`: ``` SHOPIFY_ACCESS_TOKEN=shpat_xxxxxxxxxxxxxxxxxxxx SHOPIFY_STORE_DOMAIN=my-store.myshopify.com diff --git a/website/docs/user-guide/skills/optional/productivity/productivity-siyuan.md b/website/docs/user-guide/skills/optional/productivity/productivity-siyuan.md index 58263053fdd..777ee265d11 100644 --- a/website/docs/user-guide/skills/optional/productivity/productivity-siyuan.md +++ b/website/docs/user-guide/skills/optional/productivity/productivity-siyuan.md @@ -37,7 +37,7 @@ Use the [SiYuan](https://github.com/siyuan-note/siyuan) kernel API via curl to s 1. Install and run SiYuan (desktop or Docker) 2. Get your API token: **Settings > About > API token** -3. Store it in `~/.hermes/.env`: +3. Store it in `${HERMES_HOME:-~/.hermes}/.env`: ``` SIYUAN_TOKEN=your_token_here SIYUAN_URL=http://127.0.0.1:6806 diff --git a/website/docs/user-guide/skills/optional/productivity/productivity-telephony.md b/website/docs/user-guide/skills/optional/productivity/productivity-telephony.md index f6c15444cbb..03d08bdc399 100644 --- a/website/docs/user-guide/skills/optional/productivity/productivity-telephony.md +++ b/website/docs/user-guide/skills/optional/productivity/productivity-telephony.md @@ -34,7 +34,7 @@ The following is the complete skill definition that Hermes loads when this skill This optional skill gives Hermes practical phone capabilities while keeping telephony out of the core tool list. It ships with a helper script, `scripts/telephony.py`, that can: -- save provider credentials into `~/.hermes/.env` +- save provider credentials into `${HERMES_HOME:-~/.hermes}/.env` - search for and buy a Twilio phone number - remember that owned number for later sessions - send SMS / MMS from the owned number @@ -121,7 +121,7 @@ Why: The skill persists telephony state in two places: -### `~/.hermes/.env` +### `${HERMES_HOME:-~/.hermes}/.env` Used for long-lived provider credentials and owned-number IDs, for example: - `TWILIO_ACCOUNT_SID` - `TWILIO_AUTH_TOKEN` @@ -258,7 +258,7 @@ python3 "$SCRIPT" save-twilio AC... auth_token_here python3 "$SCRIPT" twilio-search --country US --area-code 702 --limit 10 ``` -3. Buy it and save it into `~/.hermes/.env` + state: +3. Buy it and save it into `${HERMES_HOME:-~/.hermes}/.env` + state: ```bash python3 "$SCRIPT" twilio-buy "+17025551234" --save-env ``` @@ -420,7 +420,7 @@ After setup, you should be able to do all of the following with just this skill: 1. `diagnose` shows provider readiness and remembered state 2. search and buy a Twilio number -3. persist that number to `~/.hermes/.env` +3. persist that number to `${HERMES_HOME:-~/.hermes}/.env` 4. send an SMS from the owned number 5. poll inbound texts for the owned number later 6. place a direct Twilio call diff --git a/website/docs/user-guide/skills/optional/research/research-gitnexus-explorer.md b/website/docs/user-guide/skills/optional/research/research-gitnexus-explorer.md index 5b1f62458d1..a5f062dc373 100644 --- a/website/docs/user-guide/skills/optional/research/research-gitnexus-explorer.md +++ b/website/docs/user-guide/skills/optional/research/research-gitnexus-explorer.md @@ -21,7 +21,7 @@ Index a codebase with GitNexus and serve an interactive knowledge graph via web | License | MIT | | Platforms | linux, macos, windows | | Tags | `gitnexus`, `code-intelligence`, `knowledge-graph`, `visualization` | -| Related skills | [`native-mcp`](/docs/user-guide/skills/bundled/mcp/mcp-native-mcp), [`codebase-inspection`](/docs/user-guide/skills/bundled/github/github-codebase-inspection) | +| Related skills | `native-mcp`, [`codebase-inspection`](/docs/user-guide/skills/bundled/github/github-codebase-inspection) | ## Reference: full SKILL.md diff --git a/website/docs/user-guide/skills/optional/research/research-qmd.md b/website/docs/user-guide/skills/optional/research/research-qmd.md index 47cf81634b8..8d145080b45 100644 --- a/website/docs/user-guide/skills/optional/research/research-qmd.md +++ b/website/docs/user-guide/skills/optional/research/research-qmd.md @@ -21,7 +21,7 @@ Search personal knowledge bases, notes, docs, and meeting transcripts locally us | License | MIT | | Platforms | macos, linux | | Tags | `Search`, `Knowledge-Base`, `RAG`, `Notes`, `MCP`, `Local-AI` | -| Related skills | [`obsidian`](/docs/user-guide/skills/bundled/note-taking/note-taking-obsidian), [`native-mcp`](/docs/user-guide/skills/bundled/mcp/mcp-native-mcp), [`arxiv`](/docs/user-guide/skills/bundled/research/research-arxiv) | +| Related skills | [`obsidian`](/docs/user-guide/skills/bundled/note-taking/note-taking-obsidian), `native-mcp`, [`arxiv`](/docs/user-guide/skills/bundled/research/research-arxiv) | ## Reference: full SKILL.md diff --git a/website/docs/user-guide/skills/optional/security/security-1password.md b/website/docs/user-guide/skills/optional/security/security-1password.md index 4ed526a87b6..c2c3fccb6e9 100644 --- a/website/docs/user-guide/skills/optional/security/security-1password.md +++ b/website/docs/user-guide/skills/optional/security/security-1password.md @@ -51,7 +51,7 @@ Use this skill when the user wants secrets managed through 1Password instead of ### Service Account (recommended for Hermes) -Set `OP_SERVICE_ACCOUNT_TOKEN` in `~/.hermes/.env` (the skill will prompt for this on first load). +Set `OP_SERVICE_ACCOUNT_TOKEN` in `${HERMES_HOME:-~/.hermes}/.env` (the skill will prompt for this on first load). No desktop app needed. Supports `op read`, `op inject`, `op run`. ```bash diff --git a/website/docs/user-guide/skills/optional/security/security-godmode.md b/website/docs/user-guide/skills/optional/security/security-godmode.md index ee12f700f6d..f41975a4966 100644 --- a/website/docs/user-guide/skills/optional/security/security-godmode.md +++ b/website/docs/user-guide/skills/optional/security/security-godmode.md @@ -418,4 +418,4 @@ Claude Sonnet 4 is robust against all current techniques for clearly harmful con 9. **Always use `load_godmode.py` in execute_code** — The individual scripts (`parseltongue.py`, `godmode_race.py`, `auto_jailbreak.py`) have argparse CLI entry points with `if __name__ == '__main__'` blocks. When loaded via `exec()` in execute_code, `__name__` is `'__main__'` and argparse fires, crashing the script. The `load_godmode.py` loader handles this by setting `__name__` to a non-main value and managing sys.argv. 10. **boundary_inversion is model-version specific** — Works on Claude 3.5 Sonnet but NOT Claude Sonnet 4 or Claude 4.6. The strategy order in auto_jailbreak tries it first for Claude models, but falls through to refusal_inversion when it fails. Update the strategy order if you know the model version. 11. **Gray-area vs hard queries** — Jailbreak techniques work much better on "dual-use" queries (lock picking, security tools, chemistry) than on overtly harmful ones (phishing templates, malware). For hard queries, skip directly to ULTRAPLINIAN or use Hermes/Grok models that don't refuse. -12. **execute_code sandbox has no env vars** — When Hermes runs auto_jailbreak via execute_code, the sandbox doesn't inherit `~/.hermes/.env`. Load dotenv explicitly: `from dotenv import load_dotenv; load_dotenv(os.path.expanduser("~/.hermes/.env"))` +12. **execute_code sandbox has no env vars** — When Hermes runs auto_jailbreak via execute_code, the sandbox doesn't inherit the Hermes `.env`. Load dotenv explicitly: `import os; from dotenv import load_dotenv; load_dotenv(os.path.join(os.environ.get("HERMES_HOME", os.path.expanduser("~/.hermes")), ".env"))` diff --git a/website/docs/user-guide/skills/optional/software-development/software-development-rest-graphql-debug.md b/website/docs/user-guide/skills/optional/software-development/software-development-rest-graphql-debug.md index 0698d855f5f..6c9f84bafcb 100644 --- a/website/docs/user-guide/skills/optional/software-development/software-development-rest-graphql-debug.md +++ b/website/docs/user-guide/skills/optional/software-development/software-development-rest-graphql-debug.md @@ -414,7 +414,7 @@ class TestAPISmoke: ### Token handling - Never log full tokens. Redact: `Bearer `. -- Never hardcode tokens in scripts. Read from env (`os.environ["API_TOKEN"]`) or `~/.hermes/.env`. +- Never hardcode tokens in scripts. Read from env (`os.environ["API_TOKEN"]`) or `${HERMES_HOME:-~/.hermes}/.env`. - Rotate immediately if a token surfaces in logs, error messages, or git history. ### Safe logging diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/optional-skills-catalog.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/optional-skills-catalog.md index aed044b3099..ff9b48cef6f 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/optional-skills-catalog.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/optional-skills-catalog.md @@ -53,7 +53,6 @@ hermes skills uninstall | 技能 | 描述 | |-------|-------------| | [**blender-mcp**](/user-guide/skills/optional/creative/creative-blender-mcp) | 通过 socket 连接 blender-mcp 插件,直接从 Hermes 控制 Blender。创建 3D 对象、材质、动画,并运行任意 Blender Python(bpy)代码。适用于用户希望在 Blender 中创建或修改任何内容的场景。 | -| [**concept-diagrams**](/user-guide/skills/optional/creative/creative-concept-diagrams) | 生成扁平、极简、支持亮色/暗色模式的 SVG 图表,输出为独立 HTML 文件,采用统一的教育视觉语言,包含 9 种语义色阶、句首大写排版及自动暗色模式。最适合教育和说明类内容。 | | [**hyperframes**](/user-guide/skills/optional/creative/creative-hyperframes) | 使用 HyperFrames 创建基于 HTML 的视频合成、动态标题卡、社交叠层、字幕访谈视频、音频响应视觉效果及着色器转场。HTML 是视频的唯一来源。适用于用户希望制作任何视频内容的场景。 | | [**kanban-video-orchestrator**](/user-guide/skills/optional/creative/creative-kanban-video-orchestrator) | 规划、搭建并监控由 Hermes Kanban 支撑的多 agent 视频制作流水线。适用于用户希望制作任何类型视频的场景 — 叙事影片、产品/营销视频、MV、解说视频、ASCII/终端艺术、抽象/生成式循环等。 | | [**meme-generation**](/user-guide/skills/optional/creative/creative-meme-generation) | 通过选取模板并使用 Pillow 叠加文字来生成真实的 meme 图片,输出实际的 .png 文件。 | diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/skills-catalog.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/skills-catalog.md index 20773484b6c..f6f24bd932d 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/skills-catalog.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/skills-catalog.md @@ -35,7 +35,6 @@ Hermes 在执行 `hermes update` 时也会同步内置技能,但同步清单 | 技能 | 描述 | 路径 | |-------|-------------|------| -| [`architecture-diagram`](/user-guide/skills/bundled/creative/creative-architecture-diagram) | 以 HTML 形式生成深色主题的 SVG 架构/云/基础设施图。 | `creative/architecture-diagram` | | [`ascii-art`](/user-guide/skills/bundled/creative/creative-ascii-art) | ASCII 艺术:pyfiglet、cowsay、boxes、图像转 ASCII。 | `creative/ascii-art` | | [`ascii-video`](/user-guide/skills/bundled/creative/creative-ascii-video) | ASCII 视频:将视频/音频转换为彩色 ASCII MP4/GIF。 | `creative/ascii-video` | | [`baoyu-infographic`](/user-guide/skills/bundled/creative/creative-baoyu-infographic) | 信息图(可视化):21 种布局 × 21 种风格。 | `creative/baoyu-infographic` | @@ -48,7 +47,6 @@ Hermes 在执行 `hermes update` 时也会同步内置技能,但同步清单 | [`p5js`](/user-guide/skills/bundled/creative/creative-p5js) | p5.js 草图:生成艺术、着色器、交互、3D。 | `creative/p5js` | | [`popular-web-designs`](/user-guide/skills/bundled/creative/creative-popular-web-designs) | 54 种真实设计系统(Stripe、Linear、Vercel)的 HTML/CSS 实现。 | `creative/popular-web-designs` | | [`pretext`](/user-guide/skills/bundled/creative/creative-pretext) | 使用 @chenglou/pretext 构建创意浏览器 demo——无 DOM 的文本布局,支持 ASCII 艺术、绕障碍物的排版流、文字即几何游戏、动态排版和文字驱动的生成艺术。生成单文件 HTML。 | `creative/pretext` | -| [`sketch`](/user-guide/skills/bundled/creative/creative-sketch) | 一次性 HTML 原型:生成 2-3 个设计变体供对比。 | `creative/sketch` | | [`songwriting-and-ai-music`](/user-guide/skills/bundled/creative/creative-songwriting-and-ai-music) | 歌曲创作技巧与 Suno AI 音乐 prompt(提示词)。 | `creative/songwriting-and-ai-music` | | [`touchdesigner-mcp`](/user-guide/skills/bundled/creative/creative-touchdesigner-mcp) | 通过 twozero MCP 控制运行中的 TouchDesigner 实例——创建算子、设置参数、连接节点、执行 Python、构建实时视觉效果。36 个原生工具。 | `creative/touchdesigner-mcp` | diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-architecture-diagram.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-architecture-diagram.md deleted file mode 100644 index 60846a64f16..00000000000 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-architecture-diagram.md +++ /dev/null @@ -1,165 +0,0 @@ ---- -title: "Architecture Diagram — 深色主题 SVG 架构/云/基础设施图表(HTML 格式)" -sidebar_label: "Architecture Diagram" -description: "深色主题 SVG 架构/云/基础设施图表(HTML 格式)" ---- - -{/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */} - -# Architecture Diagram - -深色主题 SVG 架构/云/基础设施图表,以 HTML 格式输出。 - -## Skill 元数据 - -| | | -|---|---| -| 来源 | 内置(默认安装) | -| 路径 | `skills/creative/architecture-diagram` | -| 版本 | `1.0.0` | -| 作者 | Cocoon AI (hello@cocoon-ai.com),由 Hermes Agent 移植 | -| 许可证 | MIT | -| 平台 | linux, macos, windows | -| 标签 | `architecture`, `diagrams`, `SVG`, `HTML`, `visualization`, `infrastructure`, `cloud` | -| 相关 skill | [`concept-diagrams`](/user-guide/skills/optional/creative/creative-concept-diagrams), [`excalidraw`](/user-guide/skills/bundled/creative/creative-excalidraw) | - -## 参考:完整 SKILL.md - -:::info -以下是 Hermes 在触发该 skill 时加载的完整 skill 定义。这是 agent 在 skill 激活时所看到的指令内容。 -::: - -# Architecture Diagram Skill - -生成专业的深色主题技术架构图,输出为包含内联 SVG 图形的独立 HTML 文件。无需外部工具、无需 API 密钥、无需渲染库——只需写入 HTML 文件并在浏览器中打开即可。 - -## 适用范围 - -**最适合:** -- 软件系统架构(前端/后端/数据库层) -- 云基础设施(VPC、区域、子网、托管服务) -- 微服务/服务网格拓扑 -- 数据库 + API 映射、部署图 -- 任何具有技术基础设施主题、适合深色网格背景风格的内容 - -**以下场景请优先考虑其他工具:** -- 物理、化学、数学、生物或其他科学学科 -- 实物对象(车辆、硬件、解剖结构、截面图) -- 平面图、叙事流程、教育/教科书风格的视觉内容 -- 手绘白板草图(建议使用 `excalidraw`) -- 动画说明(建议使用动画相关 skill) - -如果有更专业的 skill 适用于该主题,请优先使用。如果没有合适的,本 skill 也可作为通用 SVG 图表的备选方案——输出内容将带有下述深色技术风格。 - -基于 [Cocoon AI 的 architecture-diagram-generator](https://github.com/Cocoon-AI/architecture-diagram-generator)(MIT 许可证)。 - -## 工作流程 - -1. 用户描述其系统架构(组件、连接关系、技术栈) -2. 按照下方设计规范生成 HTML 文件 -3. 使用 `write_file` 保存为 `.html` 文件(例如 `~/architecture-diagram.html`) -4. 用户在任意浏览器中打开——支持离线使用,无需任何依赖 - -### 输出位置 - -将图表保存到用户指定路径,或默认保存至当前工作目录: -``` -./[project-name]-architecture.html -``` - -### 预览 - -保存后,建议用户通过以下命令打开: -```bash -# macOS -open ./my-architecture.html -# Linux -xdg-open ./my-architecture.html -``` - -## 设计规范与视觉语言 - -### 颜色方案(语义映射) - -使用特定的 `rgba` 填充色和十六进制描边色对组件进行分类: - -| 组件类型 | 填充色(rgba) | 描边色(Hex) | -| :--- | :--- | :--- | -| **前端** | `rgba(8, 51, 68, 0.4)` | `#22d3ee`(cyan-400) | -| **后端** | `rgba(6, 78, 59, 0.4)` | `#34d399`(emerald-400) | -| **数据库** | `rgba(76, 29, 149, 0.4)` | `#a78bfa`(violet-400) | -| **AWS/云** | `rgba(120, 53, 15, 0.3)` | `#fbbf24`(amber-400) | -| **安全** | `rgba(136, 19, 55, 0.4)` | `#fb7185`(rose-400) | -| **消息总线** | `rgba(251, 146, 60, 0.3)` | `#fb923c`(orange-400) | -| **外部** | `rgba(30, 41, 59, 0.5)` | `#94a3b8`(slate-400) | - -### 字体与背景 -- **字体:** JetBrains Mono(等宽字体),从 Google Fonts 加载 -- **字号:** 12px(名称)、9px(副标签)、8px(注释)、7px(极小标签) -- **背景:** Slate-950(`#020617`),带有细腻的 40px 网格图案 - -```svg - - - - -``` - -## 技术实现细节 - -### 组件渲染 -组件为圆角矩形(`rx="6"`),描边宽度 1.5px。为防止箭头透过半透明填充色显现,使用**双矩形遮罩技术**: -1. 绘制不透明背景矩形(`#0f172a`) -2. 在其上方绘制半透明样式矩形 - -### 连接规则 -- **Z 轴顺序:** 在 SVG 早期绘制箭头(在网格之后),使其渲染在组件框的下方 -- **箭头头部:** 通过 SVG marker 定义 -- **安全流:** 使用 rose 色(`#fb7185`)虚线 -- **边界:** - - *安全组:* 虚线(`4,4`),rose 色 - - *区域:* 大虚线(`8,4`),amber 色,`rx="12"` - -### 间距与布局规则 -- **标准高度:** 60px(服务);80–120px(大型组件) -- **垂直间距:** 组件之间最小 40px -- **消息总线:** 必须放置在服务之间的间隙中,不得与其重叠 -- **图例位置:** **关键。** 必须放置在所有边界框的外部。计算所有边界的最低 Y 坐标,并将图例放置在其下方至少 20px 处。 - -## 文档结构 - -生成的 HTML 文件遵循四段式布局: -1. **页眉:** 带有脉冲点指示器的标题和副标题 -2. **主 SVG:** 包含在圆角边框卡片中的图表 -3. **摘要卡片:** 图表下方的三张卡片网格,用于展示高层次详情 -4. **页脚:** 简洁的元数据信息 - -### 信息卡片模式 -```html -
-
-
-

Title

-
-
    -
  • • Item one
  • -
  • • Item two
  • -
-
-``` - -## 输出要求 -- **单文件:** 一个自包含的 `.html` 文件 -- **无外部依赖:** 所有 CSS 和 SVG 必须内联(Google Fonts 除外) -- **无 JavaScript:** 所有动画(如脉冲点)使用纯 CSS 实现 -- **兼容性:** 必须在任何现代浏览器中正确渲染 - -## 模板参考 - -加载完整 HTML 模板以获取精确的结构、CSS 和 SVG 组件示例: - -``` -skill_view(name="architecture-diagram", file_path="templates/template.html") -``` - -模板包含每种组件类型(前端、后端、数据库、云、安全)、箭头样式(标准、虚线、曲线)、安全组、区域边界和图例的完整示例——生成图表时请以此作为结构参考。 \ No newline at end of file diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-claude-design.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-claude-design.md index 6d1b7529ab3..7aaa2d26f2d 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-claude-design.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-claude-design.md @@ -21,7 +21,7 @@ description: "设计一次性 HTML 制品(落地页、幻灯片、原型)" | 许可证 | MIT | | 平台 | linux, macos, windows | | 标签 | `design`, `html`, `prototype`, `ux`, `ui`, `creative`, `artifact`, `deck`, `motion`, `design-system` | -| 相关 skill | [`design-md`](/user-guide/skills/bundled/creative/creative-design-md), [`popular-web-designs`](/user-guide/skills/bundled/creative/creative-popular-web-designs), [`excalidraw`](/user-guide/skills/bundled/creative/creative-excalidraw), [`architecture-diagram`](/user-guide/skills/bundled/creative/creative-architecture-diagram) | +| 相关 skill | [`design-md`](/user-guide/skills/bundled/creative/creative-design-md), [`popular-web-designs`](/user-guide/skills/bundled/creative/creative-popular-web-designs), [`excalidraw`](/user-guide/skills/bundled/creative/creative-excalidraw), [`html-artifact`](/user-guide/skills/bundled/creative/creative-html-artifact) | ## 参考:完整 SKILL.md diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-design-md.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-design-md.md index 4d21eb7f671..e9fc5aade25 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-design-md.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-design-md.md @@ -21,7 +21,7 @@ description: "编写/验证/导出 Google 的 DESIGN" | 许可证 | MIT | | 平台 | linux, macos, windows | | 标签 | `design`, `design-system`, `tokens`, `ui`, `accessibility`, `wcag`, `tailwind`, `dtcg`, `google` | -| 相关 skill | [`popular-web-designs`](/user-guide/skills/bundled/creative/creative-popular-web-designs), [`claude-design`](/user-guide/skills/bundled/creative/creative-claude-design), [`excalidraw`](/user-guide/skills/bundled/creative/creative-excalidraw), [`architecture-diagram`](/user-guide/skills/bundled/creative/creative-architecture-diagram) | +| 相关 skill | [`popular-web-designs`](/user-guide/skills/bundled/creative/creative-popular-web-designs), [`claude-design`](/user-guide/skills/bundled/creative/creative-claude-design), [`excalidraw`](/user-guide/skills/bundled/creative/creative-excalidraw), [`html-artifact`](/user-guide/skills/bundled/creative/creative-html-artifact) | ## 参考:完整 SKILL.md diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-pretext.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-pretext.md index 83dadb74c8d..243e776f6a7 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-pretext.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-pretext.md @@ -21,7 +21,7 @@ description: "适用于使用 @chenglou/pretext 构建创意浏览器演示 — | 许可证 | MIT | | 平台 | linux, macos, windows | | 标签 | `creative-coding`, `typography`, `pretext`, `ascii-art`, `canvas`, `generative`, `text-layout`, `kinetic-typography` | -| 相关 skill | [`p5js`](/user-guide/skills/bundled/creative/creative-p5js), [`claude-design`](/user-guide/skills/bundled/creative/creative-claude-design), [`excalidraw`](/user-guide/skills/bundled/creative/creative-excalidraw), [`architecture-diagram`](/user-guide/skills/bundled/creative/creative-architecture-diagram) | +| 相关 skill | [`p5js`](/user-guide/skills/bundled/creative/creative-p5js), [`claude-design`](/user-guide/skills/bundled/creative/creative-claude-design), [`excalidraw`](/user-guide/skills/bundled/creative/creative-excalidraw), [`html-artifact`](/user-guide/skills/bundled/creative/creative-html-artifact) | ## 参考:完整 SKILL.md diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-sketch.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-sketch.md deleted file mode 100644 index 6478c87f362..00000000000 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-sketch.md +++ /dev/null @@ -1,238 +0,0 @@ ---- -title: "Sketch — 一次性 HTML 原型:2-3 个设计方案对比" -sidebar_label: "Sketch" -description: "一次性 HTML 原型:2-3 个设计方案对比" ---- - -{/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */} - -# Sketch - -一次性 HTML 原型:2-3 个设计方案对比。 - -## Skill 元数据 - -| | | -|---|---| -| 来源 | 内置(默认安装) | -| 路径 | `skills/creative/sketch` | -| 版本 | `1.0.0` | -| 作者 | Hermes Agent(改编自 gsd-build/get-shit-done) | -| 许可证 | MIT | -| 平台 | linux, macos, windows | -| 标签 | `sketch`, `mockup`, `design`, `ui`, `prototype`, `html`, `variants`, `exploration`, `wireframe`, `comparison` | -| 相关 skill | [`spike`](/user-guide/skills/bundled/software-development/software-development-spike), [`claude-design`](/user-guide/skills/bundled/creative/creative-claude-design), [`popular-web-designs`](/user-guide/skills/bundled/creative/creative-popular-web-designs), [`excalidraw`](/user-guide/skills/bundled/creative/creative-excalidraw) | - -## 参考:完整 SKILL.md - -:::info -以下是 Hermes 在触发该 skill 时加载的完整 skill 定义。这是 agent 在 skill 激活时所看到的指令内容。 -::: - -# Sketch - -当用户希望**在确定方向之前先看到设计效果**时使用此 skill——以一次性 HTML 原型的形式探索 UI/UX 想法。目的是生成 2-3 个可交互的方案,让用户并排对比视觉方向,而非产出可交付的代码。 - -当用户说以下内容时加载此 skill:"sketch this screen"、"show me what X could look like"、"compare layout A vs B"、"give me 2-3 takes on this UI"、"let me see some variants"、"mockup this before I build"。 - -## 不适用场景 - -- 用户需要生产级组件——使用 `claude-design` 或正式构建 -- 用户需要精良的一次性 HTML 产物(落地页、幻灯片)——使用 `claude-design` -- 用户需要图表——使用 `excalidraw`、`architecture-diagram` -- 设计已确定——直接构建即可 - -## 如果用户安装了完整的 GSD 系统 - -如果 `gsd-sketch` 作为同级 skill 出现(通过 `npx get-shit-done-cc --hermes` 安装),优先使用 **`gsd-sketch`** 以获得完整工作流:持久化的 `.planning/sketches/` 目录(含 MANIFEST)、前沿模式分析、跨历史草图的一致性审计,以及与 GSD 其余部分的集成。本 skill 是轻量级独立版本——无状态机制的一次性草图。 - -## 核心方法 - -``` -intake → variants → head-to-head → pick winner (or iterate) -``` - -### 1. Intake(如果用户已提供足够信息则跳过) - -在生成方案之前,获取三项信息——每次只问一个问题,不要一次全问: - -1. **感觉。** "这个应该给人什么感觉?形容词、情绪、氛围。"——*"calm, editorial, like Linear"* 比 *"minimal"* 更有参考价值。 -2. **参考。** "哪些 app、网站或产品接近你想象中的感觉?"——实际参考比抽象描述更有效。 -3. **核心操作。** "用户在这个页面上最重要的单一操作是什么?"——所有方案都应服务于此;否则只是装饰。 - -每次回答后简短复述,再问下一个问题。如果用户已一次性提供了全部三项,直接跳到方案生成。 - -### 2. 方案(2-3 个,不少于 1 个,极少超过 4 个) - -一次性生成 **2-3 个方案**。每个方案是一个完整的独立 HTML 文件。不要描述方案——直接构建。目的是对比。 - -每个方案应采取**不同的设计立场**,而非不同的像素值。三种有效的方案维度: - -- **密度:** 紧凑 / 宽松 / 极密(选两个对比极端) -- **重点:** 内容优先 / 操作优先 / 工具优先 -- **美学:** 编辑风格 / 实用主义 / 趣味性 -- **布局:** 单列 / 侧边栏 / 分屏 -- **基调:** 卡片式 / 纯内容 / 文档风格 - -选定一个维度并从中拉开差距。两个仅在强调色上不同的方案是无效的——用户无法区分。 - -**方案命名:** 描述立场,而非编号。 - - -``` -sketches/ -├── 001-calm-editorial/ -│ ├── index.html -│ └── README.md -├── 001-utilitarian-dense/ -│ ├── index.html -│ └── README.md -└── 001-playful-split/ - ├── index.html - └── README.md -``` - - -### 3. 制作真实的 HTML - -每个方案是一个**单一自包含的 HTML 文件**: - -- 内联 ` -``` - -### 4. 方案 README - -每个方案的 `README.md` 回答以下内容: - -```markdown -## Variant: {stance name} - -### Design stance -One sentence on the principle driving this variant. - -### Key choices -- Layout: ... -- Typography: ... -- Color: ... -- Interaction: ... - -### Trade-offs -- Strong at: ... -- Weak at: ... - -### Best for -- The kind of user or use case this variant actually serves -``` - -### 5. 正面对比 - -所有方案构建完成后,以对比形式呈现。不要只是罗列——**给出观点**: - -```markdown -## Three takes on the home screen - -| Dimension | Calm editorial | Utilitarian dense | Playful split | -|-----------|----------------|-------------------|---------------| -| Density | Low | High | Medium | -| Primary action visibility | Low | High | Medium | -| Scan-ability | High | Medium | Low | -| Feel | Calm, trusted | Sharp, tool-like | Inviting, energetic | - -**My take:** Utilitarian dense for power users, calm editorial for content-forward audiences. Playful split is weakest — tries to do both and commits to neither. -``` - -让用户选出胜出方案,或将两个方案合并为混合版,或要求新一轮迭代。 - -## 主题化(当项目有视觉标识时) - -如果用户有现有主题(颜色、字体、token),将共享 token 放入 `sketches/themes/tokens.css` 并在每个方案中 `@import`。保持 token 精简: - -```css -/* sketches/themes/tokens.css */ -:root { - --color-bg: #fafafa; - --color-fg: #1a1a1a; - --color-accent: #0066ff; - --color-muted: #666; - --radius: 8px; - --font-display: "Inter", sans-serif; - --font-body: -apple-system, BlinkMacSystemFont, sans-serif; -} -``` - -不要对一次性草图过度 token 化——三种颜色加一种字体通常已足够。 - -## 交互基准 - -当用户能够完成以下操作时,草图的交互程度即为合格: - -1. **点击主要操作**并看到可见的变化(状态变更、模态框、toast、导航模拟) -2. **看到一个有意义的状态转换**(筛选列表、切换模式、展开/收起面板) -3. **悬停可识别的交互元素**(按钮、行、标签页) - -超过此程度是对一次性草图的过度工程化。低于此程度则只是截图。 - -## 前沿模式(决定下一步草图内容) - -如果草图已存在且用户询问"接下来应该草图什么?": - -- **一致性缺口**——来自不同草图的两个胜出方案做出了独立选择,尚未组合在一起 -- **未草图的页面**——被引用但从未探索过 -- **状态覆盖**——已草图了正常路径,但未覆盖空状态 / 加载中 / 错误 / 千条数据 -- **响应式缺口**——在某一视口下验证过;在移动端 / 超宽屏下是否成立? -- **交互模式**——静态布局已存在;过渡动效、拖拽、滚动行为尚未探索 - -提出 2-4 个命名候选项,让用户选择。 - -## 输出 - -- 在仓库根目录创建 `sketches/`(如果用户使用 GSD 约定则为 `.planning/sketches/`) -- 每个方案一个子目录:`NNN-stance-name/index.html` + `README.md` -- 告知用户如何打开:macOS 上用 `open sketches/001-calm-editorial/index.html`,Linux 上用 `xdg-open`,Windows 上用 `start` -- 保持方案的一次性特性——如果你觉得有必要保留某个草图,应将其提升为真实项目代码,而非作为资产保管 - -**单个方案的典型工具调用序列:** - -``` -terminal("mkdir -p sketches/001-calm-editorial") -write_file("sketches/001-calm-editorial/index.html", "...") -write_file("sketches/001-calm-editorial/README.md", "## Variant: Calm editorial\n...") -browser_navigate(url="file://$(pwd)/sketches/001-calm-editorial/index.html") -browser_vision(question="How does this look? Any obvious layout issues?") -``` - -对每个方案重复上述步骤,然后呈现对比表格。 - -## 致谢 - -改编自 GSD(Get Shit Done)项目的 `/gsd-sketch` 工作流——MIT © 2025 Lex Christopherson([gsd-build/get-shit-done](https://github.com/gsd-build/get-shit-done))。完整 GSD 系统提供持久化草图状态、主题/方案模式参考及一致性审计工作流;通过 `npx get-shit-done-cc --hermes --global` 安装。 \ No newline at end of file diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/software-development/software-development-spike.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/software-development/software-development-spike.md index e5486edd0d3..be869779937 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/software-development/software-development-spike.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/software-development/software-development-spike.md @@ -21,7 +21,7 @@ description: "在构建前验证想法的一次性实验" | 许可证 | MIT | | 平台 | linux, macos, windows | | 标签 | `spike`, `prototype`, `experiment`, `feasibility`, `throwaway`, `exploration`, `research`, `planning`, `mvp`, `proof-of-concept` | -| 相关 skill | [`sketch`](/user-guide/skills/bundled/creative/creative-sketch)、[`writing-plans`](/user-guide/skills/bundled/software-development/software-development-writing-plans)、[`subagent-driven-development`](/user-guide/skills/bundled/software-development/software-development-subagent-driven-development)、[`plan`](/user-guide/skills/bundled/software-development/software-development-plan) | +| 相关 skill | [`html-artifact`](/user-guide/skills/bundled/creative/creative-html-artifact)、[`writing-plans`](/user-guide/skills/bundled/software-development/software-development-writing-plans)、[`subagent-driven-development`](/user-guide/skills/bundled/software-development/software-development-subagent-driven-development)、[`plan`](/user-guide/skills/bundled/software-development/software-development-plan) | ## 参考:完整 SKILL.md diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/optional/creative/creative-concept-diagrams.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/optional/creative/creative-concept-diagrams.md deleted file mode 100644 index 405f658a22b..00000000000 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/optional/creative/creative-concept-diagrams.md +++ /dev/null @@ -1,379 +0,0 @@ ---- -title: "概念图" -sidebar_label: "概念图" -description: "以统一的教育视觉语言生成扁平、简约、支持明暗模式的 SVG 图表,输出为独立 HTML 文件,包含 9 种语义色阶、句首大写排版及自动暗色模式。..." ---- - -{/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */} - -# 概念图 - -以统一的教育视觉语言生成扁平、简约、支持明暗模式的 SVG 图表,输出为独立 HTML 文件,包含 9 种语义色阶、句首大写排版及自动暗色模式。最适合教育类和非软件类视觉内容——物理装置、化学机制、数学曲线、实物(飞机、涡轮机、智能手机、机械表)、解剖图、平面图、截面图、叙事流程(X 的生命周期、Y 的过程)、中心辐射型系统集成(智慧城市、IoT)以及爆炸分层视图。若已有更专业的 skill 适用于该主题(专用软件/云架构、手绘草图、动画说明等),优先使用那些 skill——否则本 skill 也可作为通用 SVG 图表的备选方案,具备简洁的教育风格外观。内置 15 个示例图表。 - -## Skill 元数据 - -| | | -|---|---| -| 来源 | 可选 — 通过 `hermes skills install official/creative/concept-diagrams` 安装 | -| 路径 | `optional-skills/creative/concept-diagrams` | -| 版本 | `0.1.0` | -| 作者 | v1k22(原始 PR),移植至 hermes-agent | -| 许可证 | MIT | -| 平台 | linux, macos, windows | -| 标签 | `diagrams`, `svg`, `visualization`, `education`, `physics`, `chemistry`, `engineering` | -| 相关 skills | [`architecture-diagram`](/user-guide/skills/bundled/creative/creative-architecture-diagram), [`excalidraw`](/user-guide/skills/bundled/creative/creative-excalidraw), `generative-widgets` | - -## 参考:完整 SKILL.md - -:::info -以下是 Hermes 在触发本 skill 时加载的完整 skill 定义。这是 agent 在 skill 激活时所看到的指令内容。 -::: - -# 概念图 - -使用统一的扁平、简约设计系统生成生产级 SVG 图表。输出为单个自包含 HTML 文件,可在任何现代浏览器中一致渲染,并自动支持明暗模式。 - -## 适用范围 - -**最适合:** -- 物理装置、化学机制、数学曲线、生物学 -- 实物(飞机、涡轮机、智能手机、机械表、细胞) -- 解剖图、截面图、爆炸分层视图 -- 平面图、建筑改造图 -- 叙事流程(X 的生命周期、Y 的过程) -- 中心辐射型系统集成(智慧城市、IoT 网络、电网) -- 任何领域的教育/教科书风格视觉内容 -- 定量图表(分组柱状图、能量曲线) - -**优先考虑其他方案:** -- 具有深色科技风格的专用软件/云基础设施架构(如有 `architecture-diagram` 可用,优先使用) -- 手绘白板草图(如有 `excalidraw` 可用,优先使用) -- 动画说明或视频输出(考虑动画 skill) - -若已有更专业的 skill 适用于该主题,优先使用。若无合适选项,本 skill 可作为通用 SVG 图表备选方案——输出将呈现下文描述的简洁教育风格,适用于几乎任何主题。 - -## 工作流程 - -1. 确定图表类型(见下方"图表类型")。 -2. 使用设计系统规则布局组件。 -3. 使用 `templates/template.html` 作为包装器编写完整 HTML 页面——将 SVG 粘贴到模板中 `` 的位置。 -4. 保存为独立 `.html` 文件(例如 `~/my-diagram.html` 或 `./my-diagram.html`)。 -5. 用户直接在浏览器中打开——无需服务器,无需依赖。 - -可选:若用户需要可浏览的多图表画廊,参见底部"本地预览服务器"。 - -加载 HTML 模板: -``` -skill_view(name="concept-diagrams", file_path="templates/template.html") -``` - -模板内嵌完整 CSS 设计系统(`c-*` 颜色类、文本类、明暗变量、箭头标记样式)。你生成的 SVG 依赖这些类存在于宿主页面中。 - ---- - -## 设计系统 - -### 设计理念 - -- **扁平**:无渐变、无投影、无模糊、无发光、无霓虹效果。 -- **简约**:只展示核心内容,框内无装饰性图标。 -- **一致**:每张图表使用相同的颜色、间距、排版和描边宽度。 -- **暗色模式就绪**:所有颜色通过 CSS 类自动适配——无需为每种模式单独编写 SVG。 - -### 调色板 - -9 种色阶,每种 7 个色阶值。将类名放在 `` 或形状元素上;模板 CSS 自动处理明暗两种模式。 - -| 类名 | 50(最浅) | 100 | 200 | 400 | 600 | 800 | 900(最深) | -|------------|---------------|---------|---------|---------|---------|---------|---------------| -| `c-purple` | #EEEDFE | #CECBF6 | #AFA9EC | #7F77DD | #534AB7 | #3C3489 | #26215C | -| `c-teal` | #E1F5EE | #9FE1CB | #5DCAA5 | #1D9E75 | #0F6E56 | #085041 | #04342C | -| `c-coral` | #FAECE7 | #F5C4B3 | #F0997B | #D85A30 | #993C1D | #712B13 | #4A1B0C | -| `c-pink` | #FBEAF0 | #F4C0D1 | #ED93B1 | #D4537E | #993556 | #72243E | #4B1528 | -| `c-gray` | #F1EFE8 | #D3D1C7 | #B4B2A9 | #888780 | #5F5E5A | #444441 | #2C2C2A | -| `c-blue` | #E6F1FB | #B5D4F4 | #85B7EB | #378ADD | #185FA5 | #0C447C | #042C53 | -| `c-green` | #EAF3DE | #C0DD97 | #97C459 | #639922 | #3B6D11 | #27500A | #173404 | -| `c-amber` | #FAEEDA | #FAC775 | #EF9F27 | #BA7517 | #854F0B | #633806 | #412402 | -| `c-red` | #FCEBEB | #F7C1C1 | #F09595 | #E24B4A | #A32D2D | #791F1F | #501313 | - -#### 颜色分配规则 - -颜色编码**语义**,而非顺序。切勿像彩虹一样循环使用颜色。 - -- 按**类别**对节点分组——同类型的所有节点共用一种颜色。 -- 对中性/结构性节点(起点、终点、通用步骤、用户)使用 `c-gray`。 -- 每张图表使用 **2-3 种颜色**,而非 6 种以上。 -- 通用类别优先使用 `c-purple`、`c-teal`、`c-coral`、`c-pink`。 -- 将 `c-blue`、`c-green`、`c-amber`、`c-red` 保留用于语义含义(信息、成功、警告、错误)。 - -明暗色阶映射(由模板 CSS 处理——直接使用类名即可): -- 亮色模式:50 填充 + 600 描边 + 800 标题 / 600 副标题 -- 暗色模式:800 填充 + 200 描边 + 100 标题 / 200 副标题 - -### 排版 - -只有两种字体大小,不得例外。 - -| 类名 | 大小 | 字重 | 用途 | -|-------|------|--------|-----| -| `th` | 14px | 500 | 节点标题、区域标签 | -| `ts` | 12px | 400 | 副标题、描述、箭头标签 | -| `t` | 14px | 400 | 通用文本 | - -- **始终使用句首大写。** 禁止首字母大写(Title Case),禁止全大写(ALL CAPS)。 -- 每个 `` 必须带有类名(`t`、`ts` 或 `th`),不得有无类名的文本。 -- 框内所有文本使用 `dominant-baseline="central"`。 -- 框内居中文本使用 `text-anchor="middle"`。 - -**宽度估算(近似值):** -- 14px 字重 500:每字符约 8px -- 12px 字重 400:每字符约 6.5px -- 始终验证:`box_width >= (字符数 × px/字符) + 48`(每侧 24px 内边距) - -### 间距与布局 - -- **ViewBox**:`viewBox="0 0 680 H"`,其中 H = 内容高度 + 40px 缓冲。 -- **安全区域**:x=40 至 x=640,y=40 至 y=(H-40)。 -- **框间距**:最小 60px。 -- **框内边距**:水平 24px,垂直 12px。 -- **箭头间隙**:箭头与框边缘之间 10px。 -- **单行框**:高度 44px。 -- **双行框**:高度 56px,标题与副标题基线间距 18px。 -- **容器内边距**:每个容器内部最小 20px。 -- **最大嵌套层级**:2-3 层。在 680px 宽度下更深的嵌套会难以阅读。 - -### 描边与形状 - -- **描边宽度**:所有节点边框 0.5px,不得使用 1px 或 2px。 -- **矩形圆角**:节点使用 `rx="8"`,内层容器使用 `rx="12"`,外层容器使用 `rx="16"` 至 `rx="20"`。 -- **连接路径**:必须设置 `fill="none"`,否则 SVG 默认填充为黑色。 - -### 箭头标记 - -在**每个** SVG 开头包含以下 `` 块: - -```xml - - - - - -``` - -在线条上使用 `marker-end="url(#arrow)"`。箭头通过 `context-stroke` 继承线条颜色。 - -### CSS 类(由模板提供) - -模板页面提供: - -- 文本:`.t`、`.ts`、`.th` -- 中性:`.box`、`.arr`、`.leader`、`.node` -- 色阶:`.c-purple`、`.c-teal`、`.c-coral`、`.c-pink`、`.c-gray`、`.c-blue`、`.c-green`、`.c-amber`、`.c-red`(均自动支持明暗模式) - -你**无需**重新定义这些类——直接在 SVG 中应用即可。模板文件包含完整的 CSS 定义。 - ---- - -## SVG 样板代码 - -模板页面中的每个 SVG 均以如下结构开头: - -```xml - - - - - - - - - - -``` - -将 `{HEIGHT}` 替换为实际计算高度(最后一个元素底部 + 40px)。 - -### 节点模式 - -**单行节点(44px):** -```xml - - - Service name - -``` - -**双行节点(56px):** -```xml - - - Service name - Short description - -``` - -**连接线(无标签):** -```xml - -``` - -**容器(虚线或实线):** -```xml - - - Container label - Subtitle info - -``` - ---- - -## 图表类型 - -根据主题选择合适的布局: - -1. **流程图** — CI/CD 流水线、请求生命周期、审批工作流、数据处理。单向流(从上到下或从左到右),每行最多 4-5 个节点。 -2. **结构/包含图** — 云基础设施嵌套、分层系统架构。大型外层容器包含内层区域,虚线矩形表示逻辑分组。 -3. **API/端点映射** — REST 路由、GraphQL schema。从根节点树状展开,分支到资源组,每组包含端点节点。 -4. **微服务拓扑** — 服务网格、事件驱动系统。服务作为节点,箭头表示通信模式,消息队列位于服务之间。 -5. **数据流图** — ETL 流水线、流式架构。从数据源经处理流向数据汇,方向从左到右。 -6. **实物/结构图** — 交通工具、建筑、硬件、解剖图。使用与实物形态匹配的形状——弯曲体用 ``,锥形用 ``,圆柱部件用 ``/``,隔间用嵌套 ``。参见 `references/physical-shape-cookbook.md`。 -7. **基础设施/系统集成图** — 智慧城市、IoT 网络、多域系统。中心辐射布局,中央平台连接各子系统。按系统使用语义线型(`.data-line`、`.power-line`、`.water-pipe`、`.road`)。参见 `references/infrastructure-patterns.md`。 -8. **UI/仪表盘原型** — 管理面板、监控仪表盘。屏幕框架内嵌套图表/仪表/指示器元素。参见 `references/dashboard-patterns.md`。 - -对于实物图、基础设施图和仪表盘图,生成前请先加载对应的参考文件——每个文件提供现成的 CSS 类和形状原语。 - ---- - -## 验证清单 - -在最终确定任何 SVG 之前,验证以下**所有**项目: - -1. 每个 `` 都有类名 `t`、`ts` 或 `th`。 -2. 框内每个 `` 都有 `dominant-baseline="central"`。 -3. 用作箭头的每个连接 `` 或 `` 都有 `fill="none"`。 -4. 没有箭头线穿过无关的框。 -5. 14px 文本:`box_width >= (最长标签字符数 × 8) + 48`。 -6. 12px 文本:`box_width >= (最长标签字符数 × 6.5) + 48`。 -7. ViewBox 高度 = 最底部元素 + 40px。 -8. 所有内容在 x=40 至 x=640 范围内。 -9. 颜色类(`c-*`)放在 `` 或形状元素上,不得放在 `` 连接线上。 -10. 箭头 `` 块存在。 -11. 无渐变、投影、模糊或发光效果。 -12. 所有节点边框描边宽度为 0.5px。 - ---- - -## 输出与预览 - -### 默认:独立 HTML 文件 - -写入单个 `.html` 文件,用户可直接打开。无需服务器,无需依赖,离线可用。模式: - -```python -# 1. Load the template -template = skill_view("concept-diagrams", "templates/template.html") - -# 2. Fill in title, subtitle, and paste your SVG -html = template.replace( - "", "SN2 reaction mechanism" -).replace( - "", "Bimolecular nucleophilic substitution" -).replace( - "", svg_content -) - -# 3. Write to a user-chosen path (or ./ by default) -write_file("./sn2-mechanism.html", html) -``` - -告知用户如何打开: - -``` -# macOS -open ./sn2-mechanism.html -# Linux -xdg-open ./sn2-mechanism.html -``` - -### 可选:本地预览服务器(多图表画廊) - -仅在用户明确需要可浏览的多图表画廊时使用。 - -**规则:** -- 仅绑定到 `127.0.0.1`,绝不使用 `0.0.0.0`。在共享网络上将图表暴露在所有网络接口上存在安全风险。 -- 选择空闲端口(不得硬编码),并告知用户所选 URL。 -- 服务器是可选的、需用户主动选择的——优先使用独立 HTML 文件。 - -推荐模式(让操作系统选择空闲的临时端口): - -```bash -# Put each diagram in its own folder under .diagrams/ -mkdir -p .diagrams/sn2-mechanism -# ...write .diagrams/sn2-mechanism/index.html... - -# Serve on loopback only, free port -cd .diagrams && python3 -c " -import http.server, socketserver -with socketserver.TCPServer(('127.0.0.1', 0), http.server.SimpleHTTPRequestHandler) as s: - print(f'Serving at http://127.0.0.1:{s.server_address[1]}/') - s.serve_forever() -" & -``` - -若用户坚持使用固定端口,使用 `127.0.0.1:`——仍然不得使用 `0.0.0.0`。说明如何停止服务器(`kill %1` 或 `pkill -f "http.server"`)。 - ---- - -## 示例参考 - -`examples/` 目录内置 15 个完整、经过测试的图表。在编写同类型新图表之前,先浏览这些示例以获取可用模式: - -| 文件 | 类型 | 演示内容 | -|------|------|--------------| -| `hospital-emergency-department-flow.md` | 流程图 | 带语义颜色的优先级路由 | -| `feature-film-production-pipeline.md` | 流程图 | 分阶段工作流、水平子流程 | -| `automated-password-reset-flow.md` | 流程图 | 带错误分支的认证流程 | -| `autonomous-llm-research-agent-flow.md` | 流程图 | 回环箭头、决策分支 | -| `place-order-uml-sequence.md` | 时序图 | UML 时序图风格 | -| `commercial-aircraft-structure.md` | 实物图 | 使用路径、多边形、椭圆绘制真实形状 | -| `wind-turbine-structure.md` | 实物截面图 | 地下/地上分离、颜色编码 | -| `smartphone-layer-anatomy.md` | 爆炸视图 | 左右交替标签、分层组件 | -| `apartment-floor-plan-conversion.md` | 平面图 | 墙体、门、虚线红色标注改造方案 | -| `banana-journey-tree-to-smoothie.md` | 叙事流程 | 蜿蜒路径、渐进状态变化 | -| `cpu-ooo-microarchitecture.md` | 硬件流水线 | 扇出、内存层次侧边栏 | -| `sn2-reaction-mechanism.md` | 化学图 | 分子、弯曲箭头、能量曲线 | -| `smart-city-infrastructure.md` | 中心辐射图 | 每个系统使用语义线型 | -| `electricity-grid-flow.md` | 多阶段流程图 | 电压层次、流向标记 | -| `ml-benchmark-grouped-bar-chart.md` | 图表 | 分组柱状图、双轴 | - -使用以下命令加载任意示例: -``` -skill_view(name="concept-diagrams", file_path="examples/") -``` - ---- - -## 快速参考:何时使用何种图表 - -| 用户说 | 图表类型 | 建议颜色 | -|-----------|--------------|------------------| -| "展示流水线" | 流程图 | 灰色起止点,紫色步骤,红色错误,青色部署 | -| "画数据流" | 数据流水线(从左到右) | 灰色数据源,紫色处理,青色数据汇 | -| "可视化系统" | 结构图(包含关系) | 紫色容器,青色服务,珊瑚色数据 | -| "映射端点" | API 树状图 | 紫色根节点,每个资源组一种色阶 | -| "展示服务" | 微服务拓扑 | 灰色入口,青色服务,紫色总线,珊瑚色 worker | -| "画飞机/交通工具" | 实物图 | 路径、多边形、椭圆绘制真实形状 | -| "智慧城市/IoT" | 中心辐射集成图 | 每个子系统使用语义线型 | -| "展示仪表盘" | UI 原型 | 深色屏幕,图表颜色:青色、紫色、珊瑚色告警 | -| "电网/电力" | 多阶段流程图 | 电压层次(高/中/低压线宽) | -| "风力涡轮机/涡轮机" | 实物截面图 | 基础 + 塔筒截面 + 机舱颜色编码 | -| "X 的旅程/生命周期" | 叙事流程 | 蜿蜒路径,渐进状态变化 | -| "X 的层次/爆炸图" | 爆炸分层视图 | 垂直堆叠,交替标签 | -| "CPU/流水线" | 硬件流水线 | 垂直阶段,扇出到执行端口 | -| "平面图/公寓" | 平面图 | 墙体、门,虚线红色标注改造方案 | -| "反应机制" | 化学图 | 原子、化学键、弯曲箭头、过渡态、能量曲线 | \ No newline at end of file diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/optional/creative/creative-kanban-video-orchestrator.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/optional/creative/creative-kanban-video-orchestrator.md index 15bbaaec8d1..b8f0a7946c1 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/optional/creative/creative-kanban-video-orchestrator.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/optional/creative/creative-kanban-video-orchestrator.md @@ -21,7 +21,7 @@ description: "规划、搭建并监控由 Hermes Kanban 支撑的多智能体视 | 许可证 | MIT | | 平台 | linux, macos, windows | | 标签 | `video`, `kanban`, `multi-agent`, `orchestration`, `production-pipeline` | -| 相关技能 | [`kanban-orchestrator`](/user-guide/skills/bundled/devops/devops-kanban-orchestrator)、[`kanban-worker`](/user-guide/skills/bundled/devops/devops-kanban-worker)、[`ascii-video`](/user-guide/skills/bundled/creative/creative-ascii-video)、[`manim-video`](/user-guide/skills/bundled/creative/creative-manim-video)、[`p5js`](/user-guide/skills/bundled/creative/creative-p5js)、[`comfyui`](/user-guide/skills/bundled/creative/creative-comfyui)、[`touchdesigner-mcp`](/user-guide/skills/bundled/creative/creative-touchdesigner-mcp)、[`blender-mcp`](/user-guide/skills/optional/creative/creative-blender-mcp)、[`pixel-art`](/user-guide/skills/bundled/creative/creative-pixel-art)、[`ascii-art`](/user-guide/skills/bundled/creative/creative-ascii-art)、[`songwriting-and-ai-music`](/user-guide/skills/bundled/creative/creative-songwriting-and-ai-music)、[`heartmula`](/user-guide/skills/bundled/media/media-heartmula)、[`songsee`](/user-guide/skills/bundled/media/media-songsee)、[`spotify`](/user-guide/skills/bundled/media/media-spotify)、[`youtube-content`](/user-guide/skills/bundled/media/media-youtube-content)、[`claude-design`](/user-guide/skills/bundled/creative/creative-claude-design)、[`excalidraw`](/user-guide/skills/bundled/creative/creative-excalidraw)、[`architecture-diagram`](/user-guide/skills/bundled/creative/creative-architecture-diagram)、[`concept-diagrams`](/user-guide/skills/optional/creative/creative-concept-diagrams)、[`baoyu-comic`](/user-guide/skills/bundled/creative/creative-baoyu-comic)、[`baoyu-infographic`](/user-guide/skills/bundled/creative/creative-baoyu-infographic)、[`humanizer`](/user-guide/skills/bundled/creative/creative-humanizer)、[`gif-search`](/user-guide/skills/bundled/media/media-gif-search)、[`meme-generation`](/user-guide/skills/optional/creative/creative-meme-generation) | +| 相关技能 | [`kanban-orchestrator`](/user-guide/skills/bundled/devops/devops-kanban-orchestrator)、[`kanban-worker`](/user-guide/skills/bundled/devops/devops-kanban-worker)、[`ascii-video`](/user-guide/skills/bundled/creative/creative-ascii-video)、[`manim-video`](/user-guide/skills/bundled/creative/creative-manim-video)、[`p5js`](/user-guide/skills/bundled/creative/creative-p5js)、[`comfyui`](/user-guide/skills/bundled/creative/creative-comfyui)、[`touchdesigner-mcp`](/user-guide/skills/bundled/creative/creative-touchdesigner-mcp)、[`blender-mcp`](/user-guide/skills/optional/creative/creative-blender-mcp)、[`pixel-art`](/user-guide/skills/bundled/creative/creative-pixel-art)、[`ascii-art`](/user-guide/skills/bundled/creative/creative-ascii-art)、[`songwriting-and-ai-music`](/user-guide/skills/bundled/creative/creative-songwriting-and-ai-music)、[`heartmula`](/user-guide/skills/bundled/media/media-heartmula)、[`songsee`](/user-guide/skills/bundled/media/media-songsee)、[`spotify`](/user-guide/skills/bundled/media/media-spotify)、[`youtube-content`](/user-guide/skills/bundled/media/media-youtube-content)、[`claude-design`](/user-guide/skills/bundled/creative/creative-claude-design)、[`excalidraw`](/user-guide/skills/bundled/creative/creative-excalidraw)、[`html-artifact`](/user-guide/skills/bundled/creative/creative-html-artifact)、[`baoyu-comic`](/user-guide/skills/bundled/creative/creative-baoyu-comic)、[`baoyu-infographic`](/user-guide/skills/bundled/creative/creative-baoyu-infographic)、[`humanizer`](/user-guide/skills/bundled/creative/creative-humanizer)、[`gif-search`](/user-guide/skills/bundled/media/media-gif-search)、[`meme-generation`](/user-guide/skills/optional/creative/creative-meme-generation) | ## 参考:完整 SKILL.md diff --git a/website/sidebars.ts b/website/sidebars.ts index dec160700e2..b8efcef0624 100644 --- a/website/sidebars.ts +++ b/website/sidebars.ts @@ -150,7 +150,6 @@ const sidebars: SidebarsConfig = { 'user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-claude-code', 'user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-codex', 'user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-hermes-agent', - 'user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-kanban-codex-lane', 'user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-opencode', ], }, @@ -160,7 +159,6 @@ const sidebars: SidebarsConfig = { key: 'skills-bundled-creative', collapsed: true, items: [ - 'user-guide/skills/bundled/creative/creative-architecture-diagram', 'user-guide/skills/bundled/creative/creative-ascii-art', 'user-guide/skills/bundled/creative/creative-ascii-video', 'user-guide/skills/bundled/creative/creative-baoyu-infographic', @@ -168,12 +166,12 @@ const sidebars: SidebarsConfig = { 'user-guide/skills/bundled/creative/creative-comfyui', 'user-guide/skills/bundled/creative/creative-design-md', 'user-guide/skills/bundled/creative/creative-excalidraw', + 'user-guide/skills/bundled/creative/creative-html-artifact', 'user-guide/skills/bundled/creative/creative-humanizer', 'user-guide/skills/bundled/creative/creative-manim-video', 'user-guide/skills/bundled/creative/creative-p5js', 'user-guide/skills/bundled/creative/creative-popular-web-designs', 'user-guide/skills/bundled/creative/creative-pretext', - 'user-guide/skills/bundled/creative/creative-sketch', 'user-guide/skills/bundled/creative/creative-songwriting-and-ai-music', 'user-guide/skills/bundled/creative/creative-touchdesigner-mcp', ], @@ -387,7 +385,6 @@ const sidebars: SidebarsConfig = { 'user-guide/skills/optional/creative/creative-baoyu-article-illustrator', 'user-guide/skills/optional/creative/creative-baoyu-comic', 'user-guide/skills/optional/creative/creative-blender-mcp', - 'user-guide/skills/optional/creative/creative-concept-diagrams', 'user-guide/skills/optional/creative/creative-creative-ideation', 'user-guide/skills/optional/creative/creative-hyperframes', 'user-guide/skills/optional/creative/creative-kanban-video-orchestrator', From fcac0f94d4844f904a6eaa8a2b667299408b9f92 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:53:39 +0530 Subject: [PATCH 18/90] fix(openviking): guard empty tool_id in batch skip set; reuse env_var_enabled Two follow-up fixes on top of the cherry-picked structured-sync work: - _messages_to_openviking_batch only added a recall tool result's id to skipped_tool_ids when the id was non-empty. An empty tool_call_id (which the canonical transcript can carry; agent_runtime_helpers defaults it to "") poisoned the skip set with "", silently dropping any *other* tool result that also lacked an id. Move the recall-skip add inside the existing `if tool_id:` guard. Adds a regression test (mutation-checked: fails on pre-fix code, passes after). - _sync_trace_enabled() open-coded the canonical truthy-env check; reuse utils.env_var_enabled (byte-identical {1,true,yes,on} semantics). --- plugins/memory/openviking/__init__.py | 8 ++-- tests/openviking_plugin/test_openviking.py | 45 ++++++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/plugins/memory/openviking/__init__.py b/plugins/memory/openviking/__init__.py index 82f1f26a0a0..a57a60e67bd 100644 --- a/plugins/memory/openviking/__init__.py +++ b/plugins/memory/openviking/__init__.py @@ -49,7 +49,7 @@ from agent.message_content import flatten_message_text from agent.memory_provider import MemoryProvider from agent.skill_commands import extract_user_instruction_from_skill_message from tools.registry import tool_error -from utils import atomic_json_write +from utils import atomic_json_write, env_var_enabled logger = logging.getLogger(__name__) @@ -160,7 +160,7 @@ def _derive_openviking_user_text(content: Any) -> str: def _sync_trace_enabled() -> bool: - return os.environ.get(_SYNC_TRACE_ENV, "").strip().lower() in {"1", "true", "yes", "on"} + return env_var_enabled(_SYNC_TRACE_ENV) def _preview(value: Any, limit: int = 160) -> str: @@ -2461,8 +2461,8 @@ class OpenVikingMemoryProvider(MemoryProvider): tool_id = str(message.get("tool_call_id") or message.get("id") or "") if tool_id: completed_tool_ids.add(tool_id) - if cls._is_openviking_recall_tool_name(message.get("name")): - skipped_tool_ids.add(tool_id) + if cls._is_openviking_recall_tool_name(message.get("name")): + skipped_tool_ids.add(tool_id) continue if message.get("role") != "assistant": continue diff --git a/tests/openviking_plugin/test_openviking.py b/tests/openviking_plugin/test_openviking.py index 3a743287672..171e6abc8ac 100644 --- a/tests/openviking_plugin/test_openviking.py +++ b/tests/openviking_plugin/test_openviking.py @@ -539,6 +539,51 @@ class TestOpenVikingTurnConversion: assert recall_tool_name not in batch_text assert "Old OpenViking memory content" not in batch_text + def test_messages_to_openviking_batch_empty_tool_id_does_not_drop_other_results(self): + # A recall tool result that arrives with an empty tool_call_id must not + # poison the skip set with "" and silently drop unrelated tool results + # that also lack an id. Empty tool_call_id is reachable in the canonical + # transcript (agent_runtime_helpers defaults it to ""). + turn = [ + {"role": "user", "content": "What did we decide?"}, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "", + "type": "function", + "function": { + "name": "viking_search", + "arguments": json.dumps({"query": "decision"}), + }, + } + ], + }, + { + "role": "tool", + "tool_call_id": "", + "name": "viking_search", + "content": json.dumps({"results": ["recall stuff"]}), + }, + { + "role": "tool", + "tool_call_id": "", + "name": "shell_command", + "content": "important shell output", + }, + {"role": "assistant", "content": "done"}, + ] + + batch = OpenVikingMemoryProvider._messages_to_openviking_batch(turn) + + batch_text = json.dumps(batch) + # The unrelated (empty-id) shell result must survive. + assert "important shell output" in batch_text + # The recall tool result must still be excluded. + assert "recall stuff" not in batch_text + assert "viking_search" not in batch_text + def test_messages_to_openviking_batch_preserves_responses_text_parts(self): turn = [ {"role": "user", "content": [{"type": "input_text", "text": "hello"}]}, From 3ca0ef7e3f68c5a9684d4a7446e46c21b0731e3c Mon Sep 17 00:00:00 2001 From: Siddharth Balyan <52913345+alt-glitch@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:57:12 +0530 Subject: [PATCH 19/90] fix(nix): hashless npm deps via importNpmLock (#48883) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The npm workspace pins a single npmDepsHash for fetchNpmDeps. Any change to package-lock.json that doesn't also refresh that hash breaks the bundled hermes-tui / hermes-desktop-renderer build for Nix flake consumers, and no nix CI catches it — the workflow that ran fix-lockfiles was removed in 9eb0bcd6 ("change(ci): rip out nix ci for now"). Fetch the workspace deps with pkgs.importNpmLock instead. It resolves each package from the lockfile's own integrity hashes, so package-lock.json is the single source of truth and there is no separate hash to drift. This also removes: - the fix-lockfiles checker/refresher and its devShell wiring — it existed only to keep npmDepsHash in sync, so it is dead once the hash is gone, and its sole CI consumer was already removed in 9eb0bcd6; - the patchPhase that normalized lockfile trailing newlines — importNpmLock's npmConfigHook overwrites the lockfile rather than diffing it, so the normalization is unnecessary. npm-lockfile-fix is retained: importNpmLock requires an integrity-complete lockfile, which that tool guarantees when the lockfile is regenerated. Co-authored-by: ak2k <19240940+ak2k@users.noreply.github.com> --- nix/devShell.nix | 3 +- nix/lib.nix | 238 ++++------------------------------------------- nix/packages.nix | 2 - 3 files changed, 19 insertions(+), 224 deletions(-) diff --git a/nix/devShell.nix b/nix/devShell.nix index 2670c579541..c131bbb5ba7 100644 --- a/nix/devShell.nix +++ b/nix/devShell.nix @@ -12,7 +12,6 @@ let packages = builtins.attrValues self'.packages; hermesNpmLib = self'.packages.default.passthru.hermesNpmLib; - fixLockfilesExe = pkgs.lib.getExe self'.packages.fix-lockfiles; # Collect all packageJsonPath values from npm workspace packages. npmPackageJsonPaths = builtins.filter (p: p != null) ( @@ -33,7 +32,7 @@ shellHook = '' echo "Hermes Agent dev shell" ${combinedNonNpm} - ${hermesNpmLib.mkNpmDevShellHook npmPackageJsonPaths fixLockfilesExe} + ${hermesNpmLib.mkNpmDevShellHook npmPackageJsonPaths} echo "Ready. Run 'hermes' to start." ''; }; diff --git a/nix/lib.nix b/nix/lib.nix index 180f00f2ee0..a7a6eab7c5b 100644 --- a/nix/lib.nix +++ b/nix/lib.nix @@ -2,8 +2,7 @@ # # All npm packages in this repo are workspace members sharing a single # root package-lock.json. mkNpmPassthru provides the shared src, npmDeps, -# npmRoot, and npmDepsFetcherVersion so individual .nix files don't -# duplicate them. One hash to rule them all. +# npmRoot, and npmConfigHook so individual .nix files don't duplicate them. # # mkNpmPassthru returns packageJsonPath (e.g. "ui-tui/package.json") # instead of a per-package devShellHook. The root devshell hook @@ -19,28 +18,19 @@ let # The workspace root — where the single package-lock.json lives. src = ../.; - # Single npm deps fetch from the workspace root lockfile. - # All workspace packages share this derivation. - npmDepsHash = "sha256-kbjJksq7limRIYqP3DwI+GNgCXkG96tXcsQqmuEedxo="; - - npmDeps = pkgs.fetchNpmDeps { - inherit src; - fetcherVersion = 2; - hash = npmDepsHash; - }; + # npm dependencies for the workspace, shared by all members. importNpmLock + # resolves each package from the lockfile's own `integrity` hashes, so the + # lockfile is the single source of truth — no separate dependency hash to + # keep in sync with it. + npmDeps = pkgs.importNpmLock.importNpmLock { npmRoot = src; }; in { # Returns a buildNpmPackage-compatible attrs set that provides: - # src, npmDeps, npmRoot, npmDepsFetcherVersion - # patchPhase — ensures root lockfile has exactly one trailing newline - # nativeBuildInputs — [ updateLockfileScript ] (list, prepend with ++ for more) - # passthru.packageJsonPath — relative path to this workspace's package.json - # nodejs — fixed nodejs version for all packages we use in the repo - # - # NOTE: npmConfigHook runs `diff` between the source lockfile and the - # npm-deps cache lockfile. fetchNpmDeps preserves whatever trailing - # newlines the lockfile has. The patchPhase normalizes to exactly one - # trailing newline so both sides always match. + # src, npmDeps, npmRoot — workspace source + importNpmLock dep set + # npmConfigHook — importNpmLock's offline `npm install` hook + # nativeBuildInputs — [ updateLockfileScript ] (list, prepend with ++ for more) + # passthru.packageJsonPath — relative path to this workspace's package.json + # nodejs — fixed nodejs version for all packages we use in the repo # # Usage: # npm = hermesNpmLib.mkNpmPassthru { folder = "ui-tui"; attr = "tui"; pname = "hermes-tui"; }; @@ -62,35 +52,15 @@ in in { inherit src npmDeps nodejs; + # importNpmLock's hook installs the rewritten lockfile (every `resolved` + # rewritten to a /nix/store file: path) into the unpacked workspace and + # runs `npm install` offline, so every workspace member's dependencies + # resolve without network access. + npmConfigHook = pkgs.importNpmLock.npmConfigHook; npmRoot = "."; - npmDepsFetcherVersion = 2; ELECTRON_SKIP_BINARY_DOWNLOAD = 1; - patchPhase = '' - runHook prePatch - # Normalize trailing newlines on the root lockfile so source and - # npm-deps always match, regardless of what fetchNpmDeps preserves. - sed -i -z 's/\\n*$/\\n/' package-lock.json - - # Make npmConfigHook's byte-for-byte diff newline-agnostic by - # replacing its hardcoded /nix/store/.../diff with a wrapper that - # normalizes trailing newlines on both sides before comparing. - mkdir -p "$TMPDIR/bin" - cat > "$TMPDIR/bin/diff" << DIFFWRAP - #!/bin/sh - f1=\\$(mktemp) && sed -z 's/\\n*$/\\n/' "\\$1" > "\\$f1" - f2=\\$(mktemp) && sed -z 's/\\n*$/\\n/' "\\$2" > "\\$f2" - ${pkgs.diffutils}/bin/diff "\\$f1" "\\$f2" && rc=0 || rc=\\$? - rm -f "\\$f1" "\\$f2" - exit \\$rc - DIFFWRAP - chmod +x "$TMPDIR/bin/diff" - export PATH="$TMPDIR/bin:$PATH" - - runHook postPatch - ''; - nativeBuildInputs = [ (pkgs.writeShellScriptBin "update_${attr}_lockfile" '' set -euox pipefail @@ -104,7 +74,6 @@ in CI=true ${pkgs.lib.getExe' nodejs "npm"} install --workspaces ${pkgs.lib.getExe npm-lockfile-fix} ./package-lock.json - # Hash lives in lib.nix — just rebuild to verify. nix build .#${attr} echo "Lockfile updated and build verified for .#${attr}" '') @@ -120,12 +89,9 @@ in # Takes a list of package.json relative paths (from mkNpmPassthru .passthru.packageJsonPath), # stamps all of them, and if any changed: # 1. Runs `npm i --package-lock-only` from root to update the lockfile - # 2. If the lockfile changed, runs `npm ci` + fix-lockfiles - # - # fixLockfilesExe: absolute path to the fix-lockfiles binary - # (from pkgs.lib.getExe self'.packages.fix-lockfiles in devShell.nix). + # 2. If the lockfile changed, runs `npm ci` mkNpmDevShellHook = - packageJsonPaths: fixLockfilesExe: + packageJsonPaths: pkgs.writeShellScript "npm-dev-hook" '' REPO_ROOT=$(git rev-parse --show-toplevel) @@ -158,172 +124,4 @@ in echo "$LOCK_STAMP_VALUE" > "$LOCK_STAMP" fi ''; - - # Build `fix-lockfiles` bin that checks/updates the single npmDepsHash - # fix-lockfiles --check # exit 1 if any hash is stale - # fix-lockfiles --apply # rewrite stale hashes in place - # fix-lockfiles # alias of --apply - # Writes machine-readable fields (stale, changed, report) to $GITHUB_OUTPUT - # when set, so CI workflows can post a sticky PR comment directly. - mkFixLockfiles = - { - attr, # flake package attr for fallback verification build, e.g. "tui" - }: - pkgs.writeShellScriptBin "fix-lockfiles" '' - set -uox pipefail - MODE="''${1:---apply}" - case "$MODE" in - --check|--apply) ;; - -h|--help) - echo "usage: fix-lockfiles [--check|--apply]" - exit 0 ;; - *) - echo "usage: fix-lockfiles [--check|--apply]" >&2 - exit 2 ;; - esac - - REPO_ROOT="$(git rev-parse --show-toplevel)" - cd "$REPO_ROOT" - - # When running in GH Actions, emit Markdown links in the report pointing - # at the offending line of the nix file (and the lockfile) at the exact - # commit that was checked. LINK_SHA should be set by the workflow to the - # PR head SHA; falls back to GITHUB_SHA (which on pull_request is the - # test-merge commit, still browseable). - LINK_SERVER="''${GITHUB_SERVER_URL:-https://github.com}" - LINK_REPO="''${GITHUB_REPOSITORY:-}" - LINK_SHA="''${LINK_SHA:-''${GITHUB_SHA:-}}" - - STALE=0 - FIXED=0 - REPORT="" - - # All workspace packages share the root package-lock.json, so - # we only need to check the hash once. - LOCK_FILE="package-lock.json" - LIB_FILE="nix/lib.nix" - NEW_HASH=$(${pkgs.lib.getExe pkgs.prefetch-npm-deps} "$LOCK_FILE" 2>/dev/null) - if [ -z "$NEW_HASH" ]; then - echo "prefetch-npm-deps failed, falling back to nix build" >&2 - OUTPUT=$(nix build ".#${attr}.npmDeps" --no-link --print-build-logs 2>&1) - STATUS=$? - if [ "$STATUS" -eq 0 ]; then - echo "ok (via nix build)" - exit 0 - fi - NEW_HASH=$(echo "$OUTPUT" | awk '/got:/ {print $2; exit}') - if [ -z "$NEW_HASH" ]; then - if echo "$OUTPUT" | grep -qE "throttled|HTTP error 418|substituter .* is disabled|some outputs of .* are not valid"; then - echo "skipped (transient cache failure — see primary nix build for real status)" >&2 - echo "$OUTPUT" | tail -8 >&2 - exit 0 - fi - echo "build failed with no hash mismatch:" >&2 - echo "$OUTPUT" | tail -40 >&2 - exit 1 - fi - fi - - OLD_HASH=$(grep -oE 'npmDepsHash = "sha256-[^"]+"' "$LIB_FILE" | head -1 \ - | sed -E 's/npmDepsHash = "(.*)"/\1/') - - # prefetch-npm-deps says the hash already matches — but it only hashes the - # lockfile *contents* and can disagree with fetchNpmDeps + npmConfigHook, - # which validate the full source lockfile against the realized deps cache. - # Trusting prefetch alone produced false "ok" results while the actual - # build was broken (e.g. lockfile engines/os/cpu fields the pinned nixpkgs - # strips from the deps cache, tripping npmConfigHook). So when prefetch - # claims the hash is current, confirm with a real consumer build before - # believing it. - if [ "$NEW_HASH" = "$OLD_HASH" ]; then - if VERIFY_OUT=$(nix build ".#${attr}" --no-link --print-build-logs 2>&1); then - echo "ok" - if [ -n "''${GITHUB_OUTPUT:-}" ]; then - { echo "stale=false"; echo "changed=false"; } >> "$GITHUB_OUTPUT" - fi - exit 0 - fi - # Build failed despite a matching hash. A fixed-output 'got:' means - # prefetch genuinely disagreed with fetchNpmDeps — adopt the real hash - # and fall through to the stale-handling path below. - CORRECT_HASH=$(echo "$VERIFY_OUT" | awk '/got:/ {print $2; exit}') - if [ -n "$CORRECT_HASH" ]; then - echo "prefetch-npm-deps reported current ($OLD_HASH) but fetchNpmDeps wants $CORRECT_HASH" >&2 - NEW_HASH="$CORRECT_HASH" - elif echo "$VERIFY_OUT" | grep -qE "throttled|HTTP error 418|substituter .* is disabled|some outputs of .* are not valid"; then - echo "skipped (transient cache failure — see primary nix build for real status)" >&2 - echo "$VERIFY_OUT" | tail -8 >&2 - exit 0 - else - # Not a stale-hash problem — surface it honestly instead of "ok". - echo "::error::nix build .#${attr} failed and it is NOT a stale npmDepsHash (no 'got:' hash in output)." >&2 - echo "The committed lockfile may be incompatible with the pinned nixpkgs" >&2 - echo "(e.g. engines/os/cpu fields that prefetch-npm-deps strips from the" >&2 - echo "deps cache, tripping npmConfigHook). fix-lockfiles cannot repair this." >&2 - echo "$VERIFY_OUT" | tail -40 >&2 - if [ -n "''${GITHUB_OUTPUT:-}" ]; then - { echo "stale=false"; echo "changed=false"; } >> "$GITHUB_OUTPUT" - fi - exit 1 - fi - fi - - HASH_LINE=$(grep -n 'npmDepsHash = "sha256-' "$LIB_FILE" | head -1 | cut -d: -f1) - echo "stale: $LIB_FILE:$HASH_LINE $OLD_HASH -> $NEW_HASH" - STALE=1 - - if [ -n "$LINK_REPO" ] && [ -n "$LINK_SHA" ]; then - LIB_URL="$LINK_SERVER/$LINK_REPO/blob/$LINK_SHA/$LIB_FILE#L$HASH_LINE" - LOCK_URL="$LINK_SERVER/$LINK_REPO/blob/$LINK_SHA/$LOCK_FILE" - REPORT="- [\`$LIB_FILE:$HASH_LINE\`]($LIB_URL): \`$OLD_HASH\` → \`$NEW_HASH\` — lockfile: [\`$LOCK_FILE\`]($LOCK_URL)"$'\\n' - else - REPORT="- \`$LIB_FILE:$HASH_LINE\`: \`$OLD_HASH\` → \`$NEW_HASH\`"$'\\n' - fi - - if [ "$MODE" = "--apply" ]; then - sed -i -E "s|npmDepsHash = \"sha256-[^\"]+\";|npmDepsHash = \"$NEW_HASH\";|" "$LIB_FILE" - if ! nix build ".#${attr}.npmDeps" --no-link --print-build-logs 2>/dev/null; then - # prefetch-npm-deps may disagree with fetchNpmDeps (it hashes - # the lockfile contents, not the full source tree). Extract the - # correct hash from the nix build error and retry. - RETRY_OUTPUT=$(nix build ".#${attr}.npmDeps" --no-link --print-build-logs 2>&1) - CORRECT_HASH=$(echo "$RETRY_OUTPUT" | awk '/got:/ {print $2; exit}') - if [ -n "$CORRECT_HASH" ]; then - echo "prefetch-npm-deps gave $NEW_HASH but nix wants $CORRECT_HASH — retrying" >&2 - sed -i -E "s|npmDepsHash = \"sha256-[^\"]+\";|npmDepsHash = \"$CORRECT_HASH\";|" "$LIB_FILE" - if ! nix build ".#${attr}.npmDeps" --no-link --print-build-logs; then - echo "verification build failed after hash retry" >&2 - exit 1 - fi - NEW_HASH="$CORRECT_HASH" - else - echo "verification build failed after hash update" >&2 - exit 1 - fi - fi - FIXED=1 - echo "fixed" - fi - - if [ -n "''${GITHUB_OUTPUT:-}" ]; then - { - [ "$STALE" -eq 1 ] && echo "stale=true" || echo "stale=false" - [ "$FIXED" -eq 1 ] && echo "changed=true" || echo "changed=false" - if [ -n "$REPORT" ]; then - echo "report<> "$GITHUB_OUTPUT" - fi - - if [ "$STALE" -eq 1 ] && [ "$MODE" = "--check" ]; then - echo - echo "Stale lockfile hash detected. Run:" - echo " nix run .#fix-lockfiles" - exit 1 - fi - - exit 0 - ''; } diff --git a/nix/packages.nix b/nix/packages.nix index d585beec6b4..131444fb3fd 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -50,8 +50,6 @@ tui = hermesAgent.hermesTui; web = hermesAgent.hermesWeb; desktop = hermesAgent.hermesDesktop; - - fix-lockfiles = hermesAgent.hermesNpmLib.mkFixLockfiles { attr = "tui"; }; }; }; } From 27a6e188c4b4bc66f52b321f055fe18aa866b545 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Fri, 19 Jun 2026 14:01:16 +0530 Subject: [PATCH 20/90] refactor(openviking): derive recall-tool name set from canonical schemas _OPENVIKING_RECALL_TOOL_NAMES hardcoded the three read-tool names as string literals, which can silently desync from the *_SCHEMA["name"] constants on a rename (the same drift the adjacent _CATEGORY_SUBDIR_MAP comment warns about). Derive the set from SEARCH/READ/BROWSE_SCHEMA["name"] instead. Write tools (viking_remember / viking_add_resource) remain intentionally excluded. Set contents are unchanged. --- plugins/memory/openviking/__init__.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/plugins/memory/openviking/__init__.py b/plugins/memory/openviking/__init__.py index a57a60e67bd..95edaca47d8 100644 --- a/plugins/memory/openviking/__init__.py +++ b/plugins/memory/openviking/__init__.py @@ -72,7 +72,6 @@ _SESSION_DRAIN_TIMEOUT = 10.0 _DEFERRED_COMMIT_TIMEOUT = (_TIMEOUT * 2) + 5.0 _REMOTE_RESOURCE_PREFIXES = ("http://", "https://", "git@", "ssh://", "git://") _SYNC_TRACE_ENV = "HERMES_OPENVIKING_SYNC_TRACE" -_OPENVIKING_RECALL_TOOL_NAMES = {"viking_search", "viking_read", "viking_browse"} # Maps the viking_remember `category` enum to a viking:// subdirectory. # Keep in sync with REMEMBER_SCHEMA.parameters.properties.category.enum. @@ -503,6 +502,17 @@ ADD_RESOURCE_SCHEMA = { } +# Recall tools (read-only) whose results we never re-ingest into OpenViking — +# echoing recalled memory back into the session transcript would re-store it. +# Write tools (viking_remember / viking_add_resource) are intentionally NOT +# here. Derived from the canonical schema names so renames can't desync. +_OPENVIKING_RECALL_TOOL_NAMES = { + SEARCH_SCHEMA["name"], + READ_SCHEMA["name"], + BROWSE_SCHEMA["name"], +} + + def _zip_directory(dir_path: Path) -> Path: """Create a temporary zip file containing a directory tree.""" root = dir_path.resolve() From 2d4046c6de975eff194d6ebdfa4180e5ed86c422 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Fri, 19 Jun 2026 14:03:49 +0530 Subject: [PATCH 21/90] refactor(openviking): reuse pre-scanned tool_input for pending tool calls _messages_to_openviking_batch's pre-scan already parses and caches each tool call's arguments into tool_calls_by_id. The pending-tool-call branch re-parsed them via _tool_call_input(), a second parse and a second source of truth. Reuse the cached tool_input when the id was cached (non-empty), falling back to a parse only for the uncached empty-id case so arguments are never dropped. No behavior change. --- plugins/memory/openviking/__init__.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/plugins/memory/openviking/__init__.py b/plugins/memory/openviking/__init__.py index 95edaca47d8..9c1029d4a89 100644 --- a/plugins/memory/openviking/__init__.py +++ b/plugins/memory/openviking/__init__.py @@ -2548,11 +2548,20 @@ class OpenVikingMemoryProvider(MemoryProvider): continue if tool_id in completed_tool_ids: continue + # Reuse the tool_input parsed in the pre-scan when available + # (non-empty ids are cached); fall back to parsing for the + # uncached empty-id case so we never drop arguments. + prior_call = tool_calls_by_id.get(tool_id) if tool_id else None + tool_input = ( + prior_call["tool_input"] + if prior_call is not None + else cls._tool_call_input(tool_call) + ) parts.append({ "type": "tool", "tool_id": tool_id, "tool_name": tool_name, - "tool_input": cls._tool_call_input(tool_call), + "tool_input": tool_input, "tool_status": "pending", }) From be2c2beb96e578542b24bdb275071044a853ebbd Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Fri, 19 Jun 2026 14:05:40 +0530 Subject: [PATCH 22/90] refactor(openviking): name tool_status constants and alias sets The batch tool_status values ('completed'/'error'/'pending') and the inbound status alias sets were inline magic strings, duplicated across two checks in _tool_result_status. Hoist them to module-level constants (_TOOL_STATUS_* + _TOOL_STATUS_{ERROR,COMPLETED}_ALIASES) so the canonical wire values and the alias->canonical mapping live in one place. Emitted values are unchanged. --- plugins/memory/openviking/__init__.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/plugins/memory/openviking/__init__.py b/plugins/memory/openviking/__init__.py index 9c1029d4a89..b4d44be88af 100644 --- a/plugins/memory/openviking/__init__.py +++ b/plugins/memory/openviking/__init__.py @@ -512,6 +512,14 @@ _OPENVIKING_RECALL_TOOL_NAMES = { BROWSE_SCHEMA["name"], } +# Canonical tool_status values emitted in OpenViking batch tool parts. +_TOOL_STATUS_COMPLETED = "completed" +_TOOL_STATUS_ERROR = "error" +_TOOL_STATUS_PENDING = "pending" +# Inbound status aliases (from varied tool-result shapes) -> canonical above. +_TOOL_STATUS_ERROR_ALIASES = {"error", "failed", "failure"} +_TOOL_STATUS_COMPLETED_ALIASES = {"completed", "complete", "success", "succeeded"} + def _zip_directory(dir_path: Path) -> Path: """Create a temporary zip file containing a directory tree.""" @@ -2429,10 +2437,10 @@ class OpenVikingMemoryProvider(MemoryProvider): @classmethod def _tool_result_status(cls, message: Dict[str, Any]) -> str: raw_status = str(message.get("status") or message.get("tool_status") or "").lower() - if raw_status in {"error", "failed", "failure"}: - return "error" - if raw_status in {"completed", "complete", "success", "succeeded"}: - return "completed" + if raw_status in _TOOL_STATUS_ERROR_ALIASES: + return _TOOL_STATUS_ERROR + if raw_status in _TOOL_STATUS_COMPLETED_ALIASES: + return _TOOL_STATUS_COMPLETED text = cls._message_text(message.get("content")).strip() if text: @@ -2444,13 +2452,14 @@ class OpenVikingMemoryProvider(MemoryProvider): status = str(parsed.get("status") or "").lower() exit_code = parsed.get("exit_code") if ( - status in {"error", "failed", "failure"} + status in _TOOL_STATUS_ERROR_ALIASES or parsed.get("success") is False or bool(parsed.get("error")) or (isinstance(exit_code, int) and exit_code != 0) ): - return "error" - return "completed" + return _TOOL_STATUS_ERROR + + return _TOOL_STATUS_COMPLETED @classmethod def _messages_to_openviking_batch( @@ -2562,7 +2571,7 @@ class OpenVikingMemoryProvider(MemoryProvider): "tool_id": tool_id, "tool_name": tool_name, "tool_input": tool_input, - "tool_status": "pending", + "tool_status": _TOOL_STATUS_PENDING, }) if parts: From e738c083360649c0c9ac7b497660b4178c3f665c Mon Sep 17 00:00:00 2001 From: xxxigm Date: Fri, 19 Jun 2026 14:15:30 +0700 Subject: [PATCH 23/90] fix(backup): exclude regeneratable dependency and cache dirs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `hermes backup` walked every file under HERMES_HOME, excluding only hermes-agent / node_modules / __pycache__ / backups / checkpoints. Python dependency trees (plugin and MCP-server venvs, site-packages) and pip/uv tool caches that live under HERMES_HOME were swept in file-by-file, ballooning a backup to hundreds of thousands of entries that crawl for hours — the reported "backup stuck for days / 426543 files" symptom. Add the canonical regeneratable-dir names (.venv, venv, site-packages, .tox, .nox, .pytest_cache, .mypy_cache, .ruff_cache — mirroring agent.skill_utils.EXCLUDED_SKILL_DIRS) plus .cache to the backup's exclusion set, used by both run_backup and the pre-update/pre-migration _write_full_zip_backup. .archive is intentionally left in so the curator's restorable archived skills still get backed up. Tests cover each new dir name (excluded at any depth), that .archive and cache-resembling files are kept, and an integration check that a planted venv/site-packages/cache is pruned from the actual backup zip while skills/config survive. --- hermes_cli/backup.py | 26 +++++++++++++- tests/hermes_cli/test_backup.py | 64 +++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/hermes_cli/backup.py b/hermes_cli/backup.py index 0064881c43f..770a8de4569 100644 --- a/hermes_cli/backup.py +++ b/hermes_cli/backup.py @@ -34,14 +34,38 @@ logger = logging.getLogger(__name__) # ``hermes-agent`` is special-cased to root level only in ``_should_exclude`` # so that skill directories like ``skills/autonomous-ai-agents/hermes-agent/`` # are not accidentally excluded. +# +# The dependency/cache entries below matter for more than tidiness: without +# them a single plugin venv, MCP-server install, or pip/uv cache living under +# HERMES_HOME gets walked file-by-file, ballooning a backup to hundreds of +# thousands of entries that crawl for hours — the exact "backup stuck for +# days / 426543 files" symptom users hit. The dependency/test-env names mostly +# mirror ``agent.skill_utils.EXCLUDED_SKILL_DIRS`` (the project's canonical +# "regeneratable dir" set); ``.cache`` is an additional backup-only entry, as +# it names a broad regeneratable cache convention (pip/uv/etc.) that the skill +# scanner doesn't need to prune but a backup walk does. We deliberately do NOT +# exclude ``.archive`` here because the curator's ``skills/.archive/`` holds +# restorable user skills that must survive a backup. _EXCLUDED_DIRS = { "hermes-agent", # the codebase repo — re-clone instead "__pycache__", # bytecode caches — regenerated on import ".git", # nested git dirs (profiles shouldn't have these, but safety) - "node_modules", # js deps if website/ somehow leaks in + "node_modules", # js deps — reinstalled on demand "backups", # prior auto-backups — don't nest backups exponentially "checkpoints", # session-local trajectory caches — regenerated per-session, # session-hash-keyed so they don't port to another machine anyway + # Python dependency trees (plugin / MCP-server venvs under HERMES_HOME) — + # regenerated by reinstalling; never irreplaceable state. + ".venv", + "venv", + "site-packages", + # Tool / build caches — all regeneratable. + ".cache", + ".tox", + ".nox", + ".pytest_cache", + ".mypy_cache", + ".ruff_cache", } # File-name suffixes to skip diff --git a/tests/hermes_cli/test_backup.py b/tests/hermes_cli/test_backup.py index 762af37069c..e768d2a996c 100644 --- a/tests/hermes_cli/test_backup.py +++ b/tests/hermes_cli/test_backup.py @@ -153,6 +153,39 @@ class TestShouldExclude: assert not _should_exclude(Path("skills/autonomous-ai-agents/hermes-agent/SKILL.md")) assert not _should_exclude(Path("skills/autonomous-ai-agents/hermes-agent/sub/item.txt")) + @pytest.mark.parametrize( + "rel", + [ + "plugins/my-plugin/.venv/lib/python3.12/site-packages/x/__init__.py", + "plugins/my-plugin/venv/bin/python", + "mcp/server/site-packages/pkg/mod.py", + ".cache/uv/wheels/abc.whl", + "plugins/p/.cache/pip/http/deadbeef", + ".tox/py312/log.txt", + ".nox/tests/bin/pytest", + "plugins/p/.pytest_cache/v/cache/lastfailed", + ".mypy_cache/3.12/agent.meta.json", + ".ruff_cache/0.4.0/abc", + ], + ) + def test_excludes_regeneratable_dependency_and_cache_dirs(self, rel): + """Python dep trees and tool caches under HERMES_HOME must be skipped — + these are what balloon a backup to hundreds of thousands of files.""" + from hermes_cli.backup import _should_exclude + assert _should_exclude(Path(rel)) + + def test_does_not_exclude_curator_archive(self): + """skills/.archive/ holds restorable archived skills and MUST survive + a backup — it is intentionally NOT in the exclusion set.""" + from hermes_cli.backup import _should_exclude + assert not _should_exclude(Path("skills/.archive/old-skill/SKILL.md")) + + def test_does_not_exclude_legit_files_resembling_cache_names(self): + """Only directory-component matches are excluded; a normal file is kept.""" + from hermes_cli.backup import _should_exclude + assert not _should_exclude(Path("skills/my-skill/venv-notes.md")) + assert not _should_exclude(Path("memories/cache.json")) + # --------------------------------------------------------------------------- # Backup tests # --------------------------------------------------------------------------- @@ -272,6 +305,37 @@ class TestBackup: agent_files = [n for n in names if "hermes-agent" in n] assert agent_files == [], f"hermes-agent files leaked into backup: {agent_files}" + def test_excludes_dependency_and_cache_trees(self, tmp_path, monkeypatch): + """A plugin venv / site-packages / pip cache under HERMES_HOME must be + pruned by the walk, while real data (skills, config) is preserved. + This is the regression guard for the ballooning-backup bug.""" + hermes_home = tmp_path / ".hermes" + hermes_home.mkdir() + _make_hermes_tree(hermes_home) + + # Simulate the heavy regeneratable trees that ballooned the backup. + venv_pkg = hermes_home / "plugins" / "heavy" / ".venv" / "lib" / "site-packages" / "dep" + venv_pkg.mkdir(parents=True) + (venv_pkg / "__init__.py").write_text("# dep\n") + pip_cache = hermes_home / ".cache" / "uv" / "wheels" + pip_cache.mkdir(parents=True) + (pip_cache / "abc.whl").write_bytes(b"\x00") + + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + monkeypatch.setattr(Path, "home", lambda: tmp_path) + + out_zip = tmp_path / "backup.zip" + from hermes_cli.backup import run_backup + run_backup(Namespace(output=str(out_zip))) + + with zipfile.ZipFile(out_zip, "r") as zf: + names = zf.namelist() + leaked = [n for n in names if ".venv" in n or "site-packages" in n or ".cache" in n] + assert leaked == [], f"regeneratable trees leaked into backup: {leaked}" + # Real data still present. + assert "skills/my-skill/SKILL.md" in names + assert "config.yaml" in names + def test_includes_nested_hermes_agent_in_skills(self, tmp_path, monkeypatch): """Backup includes skills/.../hermes-agent/ but NOT root hermes-agent/.""" hermes_home = tmp_path / ".hermes" From 1699525638ed4feba3fd35f0be5c6d4d2d326a49 Mon Sep 17 00:00:00 2001 From: kyssta-exe Date: Fri, 19 Jun 2026 14:53:33 +0530 Subject: [PATCH 24/90] fix(tui): route pending-input commands via command.dispatch (#48848) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When /goal (and other _PENDING_INPUT_COMMANDS: retry, queue, q, steer, plan, undo) were typed in the TUI desktop app, slash.exec returned error 4018 instructing the frontend to fall back to command.dispatch. Some clients failed that client-side fallback, leaving the command empty and surfacing "empty command" — the user's typed text was silently dropped. slash.exec now routes pending-input commands to command.dispatch internally, eliminating the fragile client-side fallback hop. The response is exactly what command.dispatch would have produced, so the TUI client behaves identically once the round-trip succeeds. Salvaged from #48944 — rebased onto current main. The original PR's source change and test_goal_command.py update are correct, but it missed the second test surface: tests/tui_gateway/test_protocol.py's parametrized test_slash_exec_rejects_pending_input_commands still asserted the old 4018 rejection for retry/queue/q/steer/plan, turning CI red (5 failures). That test is rewritten here as a behavior contract: slash.exec for a pending-input command must yield the same payload as a direct command.dispatch call, and must no longer emit the old "pending-input command" fallback rejection. Co-authored-by: kyssta-exe --- tests/tui_gateway/test_goal_command.py | 16 +++++----- tests/tui_gateway/test_protocol.py | 41 +++++++++++++++++++++----- tui_gateway/server.py | 16 ++++++++-- 3 files changed, 55 insertions(+), 18 deletions(-) diff --git a/tests/tui_gateway/test_goal_command.py b/tests/tui_gateway/test_goal_command.py index d06f5b8fbbd..cfff285f1ef 100644 --- a/tests/tui_gateway/test_goal_command.py +++ b/tests/tui_gateway/test_goal_command.py @@ -185,15 +185,17 @@ def test_goal_requires_session(server): # ── slash.exec /goal routing ────────────────────────────────────────── -def test_slash_exec_rejects_goal_routes_to_command_dispatch(server, session): - """slash.exec must reject /goal with 4018 so the TUI client falls through - to command.dispatch. Without this, the HermesCLI slash-worker subprocess - would set the goal but silently drop the kickoff — the queue is in-proc.""" +def test_slash_exec_routes_goal_to_command_dispatch(server, session): + """slash.exec must route /goal directly to command.dispatch internally + instead of returning an error. Previously the 4018 error required the + TUI client to retry via command.dispatch, but some clients failed the + fallback, leaving the command empty ("empty command").""" sid, _, _ = session r = _call(server, "slash.exec", command="goal status", session_id=sid) - assert "error" in r - assert r["error"]["code"] == 4018 - assert "command.dispatch" in r["error"]["message"] + # Should succeed by routing to command.dispatch internally + assert "result" in r + assert r["result"]["type"] == "exec" + assert "No active goal" in r["result"]["output"] def test_pending_input_commands_includes_goal(server): diff --git a/tests/tui_gateway/test_protocol.py b/tests/tui_gateway/test_protocol.py index 60d3c7a5c4f..775a07cb317 100644 --- a/tests/tui_gateway/test_protocol.py +++ b/tests/tui_gateway/test_protocol.py @@ -1121,20 +1121,45 @@ def test_slash_exec_plugin_handler_error_returns_output(server): @pytest.mark.parametrize("cmd", ["retry", "queue hello", "q hello", "steer fix the test", "plan"]) -def test_slash_exec_rejects_pending_input_commands(server, cmd): - """slash.exec must reject commands that use _pending_input in the CLI.""" - sid = "test-session" - server._sessions[sid] = {"session_key": sid, "agent": None} +def test_slash_exec_routes_pending_input_commands_to_dispatch(server, cmd): + """slash.exec must route _pending_input commands to command.dispatch + internally instead of returning the old 4018 "use command.dispatch" + fallback error (#48848). Some TUI clients failed that client-side + fallback, dropping the input and surfacing "empty command". - resp = server.handle_request({ + The contract is that slash.exec produces exactly the response + command.dispatch would for the same command — no fragile retry hop. + """ + base, _, arg = cmd.partition(" ") + + def fresh_session(): + return {"session_key": "test-session", "agent": None} + + sid = "test-session" + + # Response from the (new) internal routing in slash.exec. + server._sessions[sid] = fresh_session() + routed = server.handle_request({ "id": "r1", "method": "slash.exec", "params": {"command": cmd, "session_id": sid}, }) - assert "error" in resp - assert resp["error"]["code"] == 4018 - assert "pending-input command" in resp["error"]["message"] + # Response from calling command.dispatch directly with the parsed parts. + server._sessions[sid] = fresh_session() + direct = server.handle_request({ + "id": "r1", + "method": "command.dispatch", + "params": {"name": base, "arg": arg, "session_id": sid}, + }) + + # slash.exec must no longer emit the old client-fallback rejection. + if "error" in routed: + assert "pending-input command" not in routed["error"]["message"] + + # Internal routing must yield the same payload as command.dispatch. + assert routed.get("result") == direct.get("result") + assert routed.get("error") == direct.get("error") def test_command_dispatch_queue_sends_message(server): diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 1b92831df3d..d65cdf49343 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -8462,7 +8462,9 @@ _TUI_EXTRA: list[tuple[str, str, str]] = [ # Commands that queue messages onto _pending_input in the CLI. # In the TUI the slash worker subprocess has no reader for that queue, -# so slash.exec rejects them → TUI falls through to command.dispatch. +# so slash.exec routes them to command.dispatch internally (which handles +# them and returns a structured payload) instead of erroring out and +# relying on a client-side fallback. See #48848. _PENDING_INPUT_COMMANDS: frozenset[str] = frozenset( { "retry", @@ -9729,8 +9731,16 @@ def _(rid, params: dict) -> dict: _cmd_arg = _cmd_parts[1] if len(_cmd_parts) > 1 else "" if _cmd_base in _PENDING_INPUT_COMMANDS: - return _err( - rid, 4018, f"pending-input command: use command.dispatch for /{_cmd_base}" + # Route directly to command.dispatch instead of returning an error + # that requires the frontend to retry. Some TUI clients fail the + # fallback, leaving the command empty and showing "empty command". + return _methods["command.dispatch"]( + rid, + { + "name": _cmd_base, + "arg": _cmd_arg, + "session_id": params.get("session_id", ""), + }, ) if _cmd_base in _WORKER_BLOCKED_COMMANDS: From fd27c9087055fbb0504766d22495d2ec5c75405a Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Fri, 19 Jun 2026 15:46:14 +0530 Subject: [PATCH 25/90] chore: add tt-a1i to AUTHOR_MAP For PR #48933 (SSE-only Anthropic stream aggregation, fixes #48923). --- scripts/release.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/release.py b/scripts/release.py index 4e5f8844439..7e5901fd568 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -415,6 +415,7 @@ AUTHOR_MAP = { "androidhtml@yandex.com": "hllqkb", "25840394+Bongulielmi@users.noreply.github.com": "Bongulielmi", "jonathan.troyer@overmatch.com": "JTroyerOvermatch", + "53142663+tt-a1i@users.noreply.github.com": "tt-a1i", # PR #48933 (SSE-only Anthropic stream aggregation, #48923) "harryykyle1@gmail.com": "hharry11", "wysie@users.noreply.github.com": "wysie", "ronhi@buildabear1.localdomain": "RonHillDev", # PR #29523 salvage (machine-local commit email) From ab8f063814089c17b2a457e3f4041a89e45b042e Mon Sep 17 00:00:00 2001 From: fyzanshaik Date: Fri, 19 Jun 2026 15:18:29 +0530 Subject: [PATCH 26/90] fix(tui): disable fast-echo bypass inside tmux to prevent cursor drift --- .../src/__tests__/textInputFastEcho.test.ts | 20 +++++++++++++++++++ ui-tui/src/components/textInput.tsx | 7 +++++++ 2 files changed, 27 insertions(+) diff --git a/ui-tui/src/__tests__/textInputFastEcho.test.ts b/ui-tui/src/__tests__/textInputFastEcho.test.ts index 6221314a062..03805aa3886 100644 --- a/ui-tui/src/__tests__/textInputFastEcho.test.ts +++ b/ui-tui/src/__tests__/textInputFastEcho.test.ts @@ -178,6 +178,26 @@ describe('supportsFastEchoTerminal', () => { expect(supportsFastEchoTerminal({ TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv)).toBe(false) }) + it('disables fast-echo inside tmux', () => { + expect(supportsFastEchoTerminal({ TMUX: '/tmp/tmux-1000/default,1234,0' } as NodeJS.ProcessEnv)).toBe(false) + expect(supportsFastEchoTerminal({ TMUX: '/private/tmp/tmux-501/default' } as NodeJS.ProcessEnv)).toBe(false) + }) + + it('tmux wins over Termux fast-echo opt-in', () => { + expect( + supportsFastEchoTerminal({ + TMUX: '/tmp/tmux-1000/default,1234,0', + HERMES_TUI_TERMUX_FAST_ECHO: '1', + TERMUX_VERSION: '0.118.0' + } as NodeJS.ProcessEnv) + ).toBe(false) + }) + + it('keeps fast-echo enabled when TMUX is empty or unset', () => { + expect(supportsFastEchoTerminal({ TMUX: '' } as NodeJS.ProcessEnv)).toBe(true) + expect(supportsFastEchoTerminal({ TERM_PROGRAM: 'vscode' } as NodeJS.ProcessEnv)).toBe(true) + }) + it('disables fast-echo by default in Termux mode', () => { expect( supportsFastEchoTerminal({ TERMUX_VERSION: '0.118.0', PREFIX: '/data/data/com.termux/files/usr' } as NodeJS.ProcessEnv) diff --git a/ui-tui/src/components/textInput.tsx b/ui-tui/src/components/textInput.tsx index 564484999f6..ff6c9dad7b3 100644 --- a/ui-tui/src/components/textInput.tsx +++ b/ui-tui/src/components/textInput.tsx @@ -359,6 +359,13 @@ export function supportsFastEchoTerminal(env: NodeJS.ProcessEnv = process.env): return false } + // tmux adds a PTY multiplexing layer that desyncs stdout.write() cursor + // advances from its internal cursor model, causing cursor drift and ghost + // whitespace under the fast-echo bypass path. + if ((env.TMUX ?? '').trim().length > 0) { + return false + } + // Termux terminals are especially sensitive to bypass-path cursor drift and // stale paints at soft-wrap boundaries on tall/narrow viewports. Keep this // off by default in Termux mode; allow explicit opt-in for local debugging. From e52fffb607fe560604d5645f57d84d71d6c8b51e Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:09:33 +0530 Subject: [PATCH 27/90] harden(tui): also disable fast-echo for tmux-flavored TERM (SSH-from-tmux) TMUX is not forwarded over SSH, so a TUI launched on a remote host from inside local tmux only sees TERM=tmux/tmux-256color with no TMUX var -- the cursor-drift bug still applies there. Extend supportsFastEchoTerminal() to also fall back when TERM is tmux-flavored. Deliberately scoped to tmux* only, NOT screen*: GNU screen sets the same screen/screen-256color TERM and has no reported drift, so widening to screen would disable the optimization for those users with no evidence of a bug (matching the original PR's stated out-of-scope note). Adds tests for tmux-flavored TERM (disabled) and screen/xterm TERM (stays enabled) to guard against accidental widening. --- ui-tui/src/__tests__/textInputFastEcho.test.ts | 17 +++++++++++++++++ ui-tui/src/components/textInput.tsx | 11 ++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/ui-tui/src/__tests__/textInputFastEcho.test.ts b/ui-tui/src/__tests__/textInputFastEcho.test.ts index 03805aa3886..98928d1baf1 100644 --- a/ui-tui/src/__tests__/textInputFastEcho.test.ts +++ b/ui-tui/src/__tests__/textInputFastEcho.test.ts @@ -198,6 +198,23 @@ describe('supportsFastEchoTerminal', () => { expect(supportsFastEchoTerminal({ TERM_PROGRAM: 'vscode' } as NodeJS.ProcessEnv)).toBe(true) }) + it('disables fast-echo when only a tmux-flavored TERM is present (SSH from tmux, no TMUX forwarded)', () => { + // OpenSSH forwards TERM but not TMUX, so a TUI on a remote host launched + // from inside local tmux sees TERM=tmux-256color with no TMUX var. The + // cursor-drift bug still applies, so fast-echo must stay off. + expect(supportsFastEchoTerminal({ TERM: 'tmux' } as NodeJS.ProcessEnv)).toBe(false) + expect(supportsFastEchoTerminal({ TERM: 'tmux-256color' } as NodeJS.ProcessEnv)).toBe(false) + }) + + it('does NOT disable fast-echo for screen-flavored TERM (GNU screen out of scope, no reported drift)', () => { + // GNU screen sets TERM=screen/screen-256color and has no reported drift. + // We must not widen the tmux guard to screen* and regress its perf. + expect(supportsFastEchoTerminal({ TERM: 'screen' } as NodeJS.ProcessEnv)).toBe(true) + expect(supportsFastEchoTerminal({ TERM: 'screen-256color' } as NodeJS.ProcessEnv)).toBe(true) + // And an unrelated 256color TERM must stay enabled. + expect(supportsFastEchoTerminal({ TERM: 'xterm-256color' } as NodeJS.ProcessEnv)).toBe(true) + }) + it('disables fast-echo by default in Termux mode', () => { expect( supportsFastEchoTerminal({ TERMUX_VERSION: '0.118.0', PREFIX: '/data/data/com.termux/files/usr' } as NodeJS.ProcessEnv) diff --git a/ui-tui/src/components/textInput.tsx b/ui-tui/src/components/textInput.tsx index ff6c9dad7b3..deb22914695 100644 --- a/ui-tui/src/components/textInput.tsx +++ b/ui-tui/src/components/textInput.tsx @@ -362,7 +362,16 @@ export function supportsFastEchoTerminal(env: NodeJS.ProcessEnv = process.env): // tmux adds a PTY multiplexing layer that desyncs stdout.write() cursor // advances from its internal cursor model, causing cursor drift and ghost // whitespace under the fast-echo bypass path. - if ((env.TMUX ?? '').trim().length > 0) { + // + // `TMUX` catches the local case. It is NOT forwarded over SSH, so when the + // TUI runs on a remote host launched from inside local tmux we only see a + // tmux-flavored `TERM` (tmux sets `tmux`/`tmux-256color`); match that too so + // remote-over-tmux sessions still fall back to the safe render path. We + // deliberately do NOT match `screen*`: GNU screen sets the same TERM and has + // no reported drift, so widening to screen would disable the optimization for + // those users with no evidence of a bug. + const term = (env.TERM ?? '').trim().toLowerCase() + if ((env.TMUX ?? '').trim().length > 0 || term === 'tmux' || term.startsWith('tmux-')) { return false } From dc5cb0a440d2d5baa1b9e60cc4ea7316cb937250 Mon Sep 17 00:00:00 2001 From: Alex Yates <43525405+yatesjalex@users.noreply.github.com> Date: Thu, 18 Jun 2026 19:06:57 -0700 Subject: [PATCH 28/90] fix(dashboard): refresh Sessions list in real time when new sessions are created The dashboard's FastAPI server and a terminal CLI are separate processes sharing one SQLite session DB; there is no inter-process push channel. The Sessions page polled the 50 newest sessions every 5s for the "overview" card but only re-fetched the paginated sessions list on page change or delete, so a session started in a terminal never appeared in the list until the user navigated. Reuse the existing 5s overview poll as a change signal: when the head session id changes, silently reload the current page (no loading spinner flicker, no scroll/reset of expanded rows or bulk selection, which are keyed by id). The detection logic is extracted into a pure shouldRefreshSessions() helper with unit tests. Adds a minimal vitest setup for web/ (test script + config). --- web/package.json | 3 ++- web/src/lib/session-refresh.test.ts | 21 +++++++++++++++ web/src/lib/session-refresh.ts | 26 +++++++++++++++++++ web/src/pages/SessionsPage.tsx | 40 +++++++++++++++++++++++++---- web/vitest.config.ts | 16 ++++++++++++ 5 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 web/src/lib/session-refresh.test.ts create mode 100644 web/src/lib/session-refresh.ts create mode 100644 web/vitest.config.ts diff --git a/web/package.json b/web/package.json index 665a780c71d..91f16ac2a04 100644 --- a/web/package.json +++ b/web/package.json @@ -48,6 +48,7 @@ "three": "^0.180.0", "typescript": "^6.0.3", "typescript-eslint": "^8.56.1", - "vite": "^8.0.16" + "vite": "^8.0.16", + "vitest": "^4.1.5" } } diff --git a/web/src/lib/session-refresh.test.ts b/web/src/lib/session-refresh.test.ts new file mode 100644 index 00000000000..0348835860a --- /dev/null +++ b/web/src/lib/session-refresh.test.ts @@ -0,0 +1,21 @@ +import { describe, it, expect } from "vitest"; +import { shouldRefreshSessions } from "./session-refresh"; + +describe("shouldRefreshSessions", () => { + it("returns false on the first poll (no baseline yet)", () => { + expect(shouldRefreshSessions(null, "s2")).toBe(false); + }); + + it("returns false when the current response has no sessions", () => { + expect(shouldRefreshSessions("s1", null)).toBe(false); + expect(shouldRefreshSessions(null, null)).toBe(false); + }); + + it("returns false when the newest session id is unchanged", () => { + expect(shouldRefreshSessions("s1", "s1")).toBe(false); + }); + + it("returns true when a new session appears at the head of the list", () => { + expect(shouldRefreshSessions("s1", "s2")).toBe(true); + }); +}); diff --git a/web/src/lib/session-refresh.ts b/web/src/lib/session-refresh.ts new file mode 100644 index 00000000000..637c7f00eb1 --- /dev/null +++ b/web/src/lib/session-refresh.ts @@ -0,0 +1,26 @@ +/** + * Decide whether the paginated sessions list should be silently + * re-fetched after an overview poll. + * + * The dashboard's FastAPI server and a terminal CLI are separate + * processes that share the same SQLite session DB. There is no + * inter-process push channel, so the Sessions page polls the 50 newest + * sessions every few seconds (the "overview" poll). When that poll + * surfaces a session id at the head of the list that we have not seen + * before, a new session was created in another process and the + * paginated list is stale — refresh it. + * + * Returns false on the very first poll (no baseline yet) and when + * either id is null (empty DB / transient empty response), so we never + * trigger a spurious reload on mount or while the DB is empty. + */ +export function shouldRefreshSessions( + prevNewestId: string | null, + currentNewestId: string | null, +): boolean { + return ( + prevNewestId !== null && + currentNewestId !== null && + prevNewestId !== currentNewestId + ); +} diff --git a/web/src/pages/SessionsPage.tsx b/web/src/pages/SessionsPage.tsx index 2d70c399af2..1746cc48184 100644 --- a/web/src/pages/SessionsPage.tsx +++ b/web/src/pages/SessionsPage.tsx @@ -30,6 +30,7 @@ import { Archive, } from "lucide-react"; import { api } from "@/lib/api"; +import { shouldRefreshSessions } from "@/lib/session-refresh"; import type { SessionInfo, SessionMessage, @@ -805,8 +806,12 @@ export default function SessionsPage() { }; }, [setEnd]); - const loadSessions = useCallback((p: number) => { - setLoading(true); + const loadSessions = useCallback((p: number, silent = false) => { + // ``silent`` skips the loading spinner so background refreshes + // (triggered when the overview poll detects a new session from + // another process) don't flicker the whole page or drop the user's + // scroll position. + if (!silent) setLoading(true); api .getSessions(PAGE_SIZE, p * PAGE_SIZE) .then((resp) => { @@ -814,7 +819,9 @@ export default function SessionsPage() { setTotal(resp.total); }) .catch(() => {}) - .finally(() => setLoading(false)); + .finally(() => { + if (!silent) setLoading(false); + }); }, []); const loadStats = useCallback(() => { @@ -828,6 +835,15 @@ export default function SessionsPage() { loadStats(); }, [loadStats]); + // Refs for the overview poll's new-session detection. The poll effect + // below is mounted once with stable deps, so it reads the current page + // and the last-seen newest session id through refs instead of capturing + // stale values. ``newestSeenRef`` starts null so the first poll sets a + // baseline without triggering a redundant reload (mount already loads). + const newestSeenRef = useRef(null); + const pageRef = useRef(page); + pageRef.current = page; + useEffect(() => { loadSessions(page); refreshEmptyCount(); @@ -841,13 +857,27 @@ export default function SessionsPage() { .catch(() => {}); api .getSessions(50) - .then((r) => setOverviewSessions(r.sessions)) + .then((r) => { + setOverviewSessions(r.sessions); + // The dashboard server and a terminal CLI are separate + // processes sharing one session DB — there is no push channel, + // so we detect sessions created in another process here. The + // overview poll already fetches the 50 newest sessions, so we + // reuse its head id as a cheap change signal: when it changes, + // silently refresh the paginated list so the new session shows + // up in real time without a visible loading flicker. + const newest = r.sessions[0]?.id ?? null; + if (shouldRefreshSessions(newestSeenRef.current, newest)) { + loadSessions(pageRef.current, true); + } + newestSeenRef.current = newest; + }) .catch(() => {}); }; loadOverview(); const id = setInterval(loadOverview, 5000); return () => clearInterval(id); - }, []); + }, [loadSessions]); useEffect(() => { const el = logScrollRef.current; diff --git a/web/vitest.config.ts b/web/vitest.config.ts new file mode 100644 index 00000000000..34baae684e8 --- /dev/null +++ b/web/vitest.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from "vitest/config"; +import react from "@vitejs/plugin-react"; +import path from "path"; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, + test: { + environment: "node", + include: ["src/**/*.test.{ts,tsx}"], + }, +}); From f37bb21ff6a81b79432109c4f628e68d188d06f0 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Fri, 19 Jun 2026 14:50:40 +0530 Subject: [PATCH 29/90] chore(dashboard): wire vitest into npm test script The salvaged PR added the vitest devDep + config + a unit test but never added a "test" script to web/package.json, so "npm run test" errored with "Missing script: test" and the new suite was unrunnable. Add the script so "npm run test" runs the suite as the PR body claimed (4/4 pass). --- web/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/package.json b/web/package.json index 91f16ac2a04..6666773c737 100644 --- a/web/package.json +++ b/web/package.json @@ -8,7 +8,8 @@ "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview", - "typecheck": "tsc -p . --noEmit" + "typecheck": "tsc -p . --noEmit", + "test": "vitest run" }, "dependencies": { "@nous-research/ui": "0.18.2", From 46f9d53468cc691d3a15dfe79decc65ce7b50d2d Mon Sep 17 00:00:00 2001 From: tt-a1i <53142663+tt-a1i@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:51:41 +0800 Subject: [PATCH 30/90] fix(agent): aggregate anthropic aux calls via stream --- agent/anthropic_adapter.py | 53 ++++++++++++ agent/auxiliary_client.py | 4 +- run_agent.py | 10 ++- tests/agent/test_auxiliary_client.py | 45 +++++++++++ tests/run_agent/test_run_agent.py | 116 ++++++++++++++++++++++++++- 5 files changed, 221 insertions(+), 7 deletions(-) diff --git a/agent/anthropic_adapter.py b/agent/anthropic_adapter.py index 4a586d7f0fd..03e8b58e16c 100644 --- a/agent/anthropic_adapter.py +++ b/agent/anthropic_adapter.py @@ -2535,3 +2535,56 @@ def sanitize_anthropic_kwargs(api_kwargs: Any, *, log_prefix: str = "") -> Any: sorted(leaked), ) return api_kwargs + + +def _is_stream_unavailable_error(exc: Exception) -> bool: + """Return True when an Anthropic stream call should fall back to create().""" + err_lower = str(exc).lower() + if "stream" in err_lower and "not supported" in err_lower: + return True + if "invokemodelwithresponsestream" in err_lower: + from agent.bedrock_adapter import is_streaming_access_denied_error + + return is_streaming_access_denied_error(exc) + return False + + +def create_anthropic_message( + client: Any, + api_kwargs: dict, + *, + log_prefix: str = "", + prefer_stream: bool = True, +) -> Any: + """Create an Anthropic message, aggregating via stream when available. + + Some Anthropic-compatible gateways are SSE-only: they ignore non-streaming + requests and return ``text/event-stream`` even for ``messages.create()``. + The SDK can surface that as raw text, so callers that expect a Message then + crash on ``.content``. Prefer ``messages.stream().get_final_message()`` to + match the main turn path, falling back to ``create()`` only for providers + that explicitly do not support streaming, such as restricted Bedrock roles. + """ + sanitize_anthropic_kwargs(api_kwargs, log_prefix=log_prefix) + + messages_api = getattr(client, "messages", None) + stream_fn = getattr(messages_api, "stream", None) + if prefer_stream and callable(stream_fn): + stream_kwargs = dict(api_kwargs) + stream_kwargs.pop("stream", None) + try: + with stream_fn(**stream_kwargs) as stream: + return stream.get_final_message() + except Exception as exc: + if not _is_stream_unavailable_error(exc): + raise + logger.debug( + "%sAnthropic Messages stream unavailable; falling back to " + "messages.create(): %s", + log_prefix, + exc, + ) + + create_kwargs = dict(api_kwargs) + create_kwargs.pop("stream", None) + return messages_api.create(**create_kwargs) diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index 86a1c765a78..f28b5f60156 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -997,7 +997,7 @@ class _AnthropicCompletionsAdapter: self._is_oauth = is_oauth def create(self, **kwargs) -> Any: - from agent.anthropic_adapter import build_anthropic_kwargs + from agent.anthropic_adapter import build_anthropic_kwargs, create_anthropic_message from agent.transports import get_transport messages = kwargs.get("messages", []) @@ -1041,7 +1041,7 @@ class _AnthropicCompletionsAdapter: if not _forbids_sampling_params(model): anthropic_kwargs["temperature"] = temperature - response = self._client.messages.create(**anthropic_kwargs) + response = create_anthropic_message(self._client, anthropic_kwargs) _transport = get_transport("anthropic_messages") _nr = _transport.normalize_response( response, strip_tool_prefix=self._is_oauth diff --git a/run_agent.py b/run_agent.py index 65b95483e54..7c195b35ca8 100644 --- a/run_agent.py +++ b/run_agent.py @@ -4076,11 +4076,13 @@ class AIAgent: # Defensive: strip Responses-only kwargs that can leak in under an # api_mode-flip race (the Anthropic SDK raises a non-retryable # TypeError on them). See #31673. - from agent.anthropic_adapter import sanitize_anthropic_kwargs - sanitize_anthropic_kwargs( - api_kwargs, log_prefix=getattr(self, "log_prefix", "") + from agent.anthropic_adapter import create_anthropic_message + return create_anthropic_message( + self._anthropic_client, + api_kwargs, + log_prefix=getattr(self, "log_prefix", ""), + prefer_stream=not bool(getattr(self, "_disable_streaming", False)), ) - return self._anthropic_client.messages.create(**api_kwargs) def _rebuild_anthropic_client(self) -> None: """Rebuild the Anthropic client after an interrupt or stale call. diff --git a/tests/agent/test_auxiliary_client.py b/tests/agent/test_auxiliary_client.py index b2960b703c7..8ec6102f2e5 100644 --- a/tests/agent/test_auxiliary_client.py +++ b/tests/agent/test_auxiliary_client.py @@ -38,6 +38,20 @@ def _jwt_with_claims(claims: dict) -> str: return f"{header}.{payload}.sig" +class _FakeAnthropicStream: + def __init__(self, final_message): + self._final_message = final_message + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + return False + + def get_final_message(self): + return self._final_message + + @pytest.fixture(autouse=True) def _clean_env(monkeypatch): """Strip provider env vars so each test starts clean.""" @@ -990,6 +1004,37 @@ class TestVisionClientFallback: assert client.__class__.__name__ == "AnthropicAuxiliaryClient" assert model == "claude-haiku-4-5-20251001" + def test_anthropic_auxiliary_client_aggregates_stream_response(self): + from agent.auxiliary_client import AnthropicAuxiliaryClient + + final_message = SimpleNamespace( + content=[SimpleNamespace(type="text", text="streamed aux response")], + stop_reason="end_turn", + usage=SimpleNamespace(input_tokens=3, output_tokens=4), + ) + messages_api = SimpleNamespace( + stream=MagicMock(return_value=_FakeAnthropicStream(final_message)), + create=MagicMock(return_value="raw event-stream text"), + ) + real_client = SimpleNamespace(messages=messages_api) + client = AnthropicAuxiliaryClient( + real_client, + "claude-sonnet-4-20250514", + "sk-test", + "https://sse-only.example/v1", + ) + + response = client.chat.completions.create( + messages=[{"role": "user", "content": "summarize"}], + max_tokens=16, + ) + + messages_api.stream.assert_called_once() + messages_api.create.assert_not_called() + assert response.choices[0].message.content == "streamed aux response" + assert response.usage.prompt_tokens == 3 + assert response.usage.completion_tokens == 4 + class TestAuxiliaryPoolAwareness: def test_try_nous_uses_pool_entry(self): diff --git a/tests/run_agent/test_run_agent.py b/tests/run_agent/test_run_agent.py index f2787628d4d..385a296f889 100644 --- a/tests/run_agent/test_run_agent.py +++ b/tests/run_agent/test_run_agent.py @@ -5813,12 +5813,126 @@ class TestAnthropicCredentialRefresh: response = SimpleNamespace(content=[]) agent._anthropic_client = MagicMock() - agent._anthropic_client.messages.create.return_value = response + stream_cm = MagicMock() + stream_cm.__enter__.return_value.get_final_message.return_value = response + agent._anthropic_client.messages.stream.return_value = stream_cm with patch.object(agent, "_try_refresh_anthropic_client_credentials", return_value=True) as refresh: result = agent._anthropic_messages_create({"model": "claude-sonnet-4-20250514"}) refresh.assert_called_once_with() + agent._anthropic_client.messages.stream.assert_called_once_with(model="claude-sonnet-4-20250514") + agent._anthropic_client.messages.create.assert_not_called() + assert result is response + + def test_anthropic_messages_create_falls_back_when_stream_unavailable(self): + with ( + patch("run_agent.get_tool_definitions", return_value=_make_tool_defs("web_search")), + patch("run_agent.check_toolset_requirements", return_value={}), + patch("agent.anthropic_adapter.build_anthropic_client", return_value=MagicMock()), + ): + agent = AIAgent( + api_key="sk-ant-oat01-current-token", + base_url="https://openrouter.ai/api/v1", + api_mode="anthropic_messages", + quiet_mode=True, + skip_context_files=True, + skip_memory=True, + ) + + response = SimpleNamespace(content=[]) + agent._anthropic_client = MagicMock() + agent._anthropic_client.messages.stream.side_effect = RuntimeError( + "stream is not supported by this provider" + ) + agent._anthropic_client.messages.create.return_value = response + + with patch.object(agent, "_try_refresh_anthropic_client_credentials", return_value=False): + result = agent._anthropic_messages_create({"model": "claude-sonnet-4-20250514"}) + + agent._anthropic_client.messages.stream.assert_called_once_with(model="claude-sonnet-4-20250514") + agent._anthropic_client.messages.create.assert_called_once_with(model="claude-sonnet-4-20250514") + assert result is response + + def test_anthropic_messages_create_honors_disable_streaming(self): + with ( + patch("run_agent.get_tool_definitions", return_value=_make_tool_defs("web_search")), + patch("run_agent.check_toolset_requirements", return_value={}), + patch("agent.anthropic_adapter.build_anthropic_client", return_value=MagicMock()), + ): + agent = AIAgent( + api_key="sk-ant-oat01-current-token", + base_url="https://openrouter.ai/api/v1", + api_mode="anthropic_messages", + quiet_mode=True, + skip_context_files=True, + skip_memory=True, + ) + + response = SimpleNamespace(content=[]) + agent._disable_streaming = True + agent._anthropic_client = MagicMock() + agent._anthropic_client.messages.create.return_value = response + + with patch.object(agent, "_try_refresh_anthropic_client_credentials", return_value=False): + result = agent._anthropic_messages_create({"model": "claude-sonnet-4-20250514"}) + + agent._anthropic_client.messages.stream.assert_not_called() + agent._anthropic_client.messages.create.assert_called_once_with(model="claude-sonnet-4-20250514") + assert result is response + + def test_anthropic_messages_create_does_not_mask_bedrock_stream_validation_errors(self): + with ( + patch("run_agent.get_tool_definitions", return_value=_make_tool_defs("web_search")), + patch("run_agent.check_toolset_requirements", return_value={}), + patch("agent.anthropic_adapter.build_anthropic_client", return_value=MagicMock()), + ): + agent = AIAgent( + api_key="sk-ant-oat01-current-token", + base_url="https://bedrock-runtime.us-east-1.amazonaws.com", + api_mode="anthropic_messages", + quiet_mode=True, + skip_context_files=True, + skip_memory=True, + ) + + exc = RuntimeError("ValidationException: InvokeModelWithResponseStream input malformed") + agent._anthropic_client = MagicMock() + agent._anthropic_client.messages.stream.side_effect = exc + + with ( + patch.object(agent, "_try_refresh_anthropic_client_credentials", return_value=False), + pytest.raises(RuntimeError, match="input malformed"), + ): + agent._anthropic_messages_create({"model": "claude-sonnet-4-20250514"}) + + agent._anthropic_client.messages.create.assert_not_called() + + def test_anthropic_messages_create_falls_back_for_bedrock_stream_access_denied(self): + with ( + patch("run_agent.get_tool_definitions", return_value=_make_tool_defs("web_search")), + patch("run_agent.check_toolset_requirements", return_value={}), + patch("agent.anthropic_adapter.build_anthropic_client", return_value=MagicMock()), + ): + agent = AIAgent( + api_key="sk-ant-oat01-current-token", + base_url="https://bedrock-runtime.us-east-1.amazonaws.com", + api_mode="anthropic_messages", + quiet_mode=True, + skip_context_files=True, + skip_memory=True, + ) + + response = SimpleNamespace(content=[]) + agent._anthropic_client = MagicMock() + agent._anthropic_client.messages.stream.side_effect = RuntimeError( + "User is not authorized to perform: bedrock:InvokeModelWithResponseStream" + ) + agent._anthropic_client.messages.create.return_value = response + + with patch.object(agent, "_try_refresh_anthropic_client_credentials", return_value=False): + result = agent._anthropic_messages_create({"model": "claude-sonnet-4-20250514"}) + agent._anthropic_client.messages.create.assert_called_once_with(model="claude-sonnet-4-20250514") assert result is response From 6ad0bc20f53d5fe240cc99ac0a105543aa895818 Mon Sep 17 00:00:00 2001 From: xxxigm Date: Fri, 19 Jun 2026 18:13:18 +0700 Subject: [PATCH 31/90] fix(sessions): let a compression continuation reclaim its base title When context compression rotates a session, the original is ended and the continuation is auto-numbered (e.g. "name" -> "name #2"). The session list projects the ended root behind its live tip, so the user never sees the predecessor. But set_session_title's uniqueness check compared against ALL sessions, so renaming the visible tip back to "name" dead-ended with "Title 'name' is already in use by session ". When the conflicting title is held by a compression ancestor of the session being renamed, transfer the title instead of raising: clear it from the ended predecessor and apply it to the continuation. Uniqueness is preserved (still exactly one session carries the title) and the parent-link lineage is untouched, so resume-by-title and tip projection keep working. Genuine conflicts with unrelated sessions, and with non-compression children (delegate/branch), still raise as before. --- hermes_state.py | 68 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 3 deletions(-) diff --git a/hermes_state.py b/hermes_state.py index 36e5c91fe8a..2ca3c657d13 100644 --- a/hermes_state.py +++ b/hermes_state.py @@ -1836,6 +1836,48 @@ class SessionDB: return cleaned + def _is_compression_ancestor( + self, conn, *, ancestor_id: str, descendant_id: str + ) -> bool: + """Return True if *ancestor_id* is a compression predecessor of + *descendant_id* (walking parent links up the continuation chain). + + Uses the same edge definition as :meth:`get_compression_tip`: a + parent → child edge counts as a compression continuation only when the + parent ended with ``end_reason = 'compression'`` and the child started + at or after the parent's ``ended_at`` (which distinguishes continuations + from delegate subagents / branch children that also carry a + ``parent_session_id``). + """ + if not ancestor_id or not descendant_id or ancestor_id == descendant_id: + return False + current = descendant_id + # Bound the walk defensively, mirroring get_compression_tip. + for _ in range(100): + row = conn.execute( + "SELECT parent_session_id, started_at FROM sessions WHERE id = ?", + (current,), + ).fetchone() + if row is None or not row["parent_session_id"]: + return False + parent_id = row["parent_session_id"] + parent = conn.execute( + "SELECT ended_at, end_reason FROM sessions WHERE id = ?", + (parent_id,), + ).fetchone() + if ( + parent is None + or parent["end_reason"] != "compression" + or parent["ended_at"] is None + or row["started_at"] is None + or row["started_at"] < parent["ended_at"] + ): + return False + if parent_id == ancestor_id: + return True + current = parent_id + return False + def set_session_title(self, session_id: str, title: str) -> bool: """Set or update a session's title. @@ -1854,9 +1896,29 @@ class SessionDB: ) conflict = cursor.fetchone() if conflict: - raise ValueError( - f"Title '{title}' is already in use by session {conflict['id']}" - ) + conflict_id = conflict["id"] + # A compression continuation is the live, projected-forward + # head of its conversation; its compressed predecessors are + # ended and hidden from the session list (list_sessions_rich + # projects roots → tip). When the title that "conflicts" is + # held by such a hidden ancestor, the user has no way to free + # it — renaming the visible tip back to the base name would + # dead-end with "already in use by ". + # Treat this as a transfer: move the title off the ancestor + # onto the continuation. Uniqueness is preserved (still only + # one session carries the exact title) and the parent-link + # lineage is untouched. + if self._is_compression_ancestor( + conn, ancestor_id=conflict_id, descendant_id=session_id + ): + conn.execute( + "UPDATE sessions SET title = NULL WHERE id = ?", + (conflict_id,), + ) + else: + raise ValueError( + f"Title '{title}' is already in use by session {conflict_id}" + ) cursor = conn.execute( "UPDATE sessions SET title = ? WHERE id = ?", (title, session_id), From 65d050cf0e94a2c435db4c2f8d46a2952515193e Mon Sep 17 00:00:00 2001 From: xxxigm Date: Fri, 19 Jun 2026 18:13:24 +0700 Subject: [PATCH 32/90] test(sessions): cover title reclaim across a compression lineage Regression tests for renaming a compression continuation back to its base title: single- and multi-level chains transfer the title off the ended predecessor, while unrelated sessions and non-compression children (created while the parent was live) still raise the uniqueness conflict. --- tests/test_hermes_state.py | 83 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/tests/test_hermes_state.py b/tests/test_hermes_state.py index e4650ed5dc7..1d727132a8c 100644 --- a/tests/test_hermes_state.py +++ b/tests/test_hermes_state.py @@ -2065,6 +2065,89 @@ class TestSessionTitle: assert session["ended_at"] is not None +class TestSessionTitleLineage: + """Renaming a compression continuation back to its base title must succeed + by transferring the title off the ended, hidden predecessor. + + After a context compaction the original session is ended and projected + behind its live tip in the session list (list_sessions_rich), so the user + cannot see or free it. Without lineage-aware handling, renaming the visible + tip back to the base name dead-ends with "already in use by ". + """ + + def _make_compression_chain(self, db, t0, *, root="root", tip="tip"): + db.create_session(root, "cli") + db._conn.execute("UPDATE sessions SET started_at=? WHERE id=?", (t0, root)) + db._conn.execute( + "UPDATE sessions SET ended_at=?, end_reason='compression' WHERE id=?", + (t0 + 100, root), + ) + db.create_session(tip, "cli", parent_session_id=root) + db._conn.execute("UPDATE sessions SET started_at=? WHERE id=?", (t0 + 200, tip)) + db._conn.commit() + + def test_rename_continuation_back_to_base_transfers_title(self, db): + import time as _time + self._make_compression_chain(db, _time.time() - 3600) + db.set_session_title("root", "fingerprint-scanner") + db.set_session_title("tip", "fingerprint-scanner #2") + + # User renames the visible tip back to the base name — must succeed. + assert db.set_session_title("tip", "fingerprint-scanner") is True + assert db.get_session("tip")["title"] == "fingerprint-scanner" + # Title transferred off the hidden ancestor — no duplicate titles. + assert db.get_session("root")["title"] is None + + def test_transfer_walks_multi_level_chain(self, db): + import time as _time + t0 = _time.time() - 7200 + # root (compression) -> mid (compression) -> tip + self._make_compression_chain(db, t0, root="root", tip="mid") + db._conn.execute( + "UPDATE sessions SET ended_at=?, end_reason='compression' WHERE id=?", + (t0 + 300, "mid"), + ) + db.create_session("tip", "cli", parent_session_id="mid") + db._conn.execute("UPDATE sessions SET started_at=? WHERE id=?", (t0 + 400, "tip")) + db._conn.commit() + + db.set_session_title("root", "deep-dive") + assert db.set_session_title("tip", "deep-dive") is True + assert db.get_session("tip")["title"] == "deep-dive" + assert db.get_session("root")["title"] is None + + def test_unrelated_session_still_conflicts(self, db): + db.create_session("a", "cli") + db.create_session("b", "cli") + db.set_session_title("a", "shared") + with pytest.raises(ValueError, match="already in use"): + db.set_session_title("b", "shared") + # The unrelated holder keeps its title. + assert db.get_session("a")["title"] == "shared" + + def test_non_compression_child_still_conflicts(self, db): + """A child whose parent did NOT end via compression (delegate/branch + spawned while the parent was live) is not a continuation, so renaming it + to the parent's title must still raise.""" + import time as _time + t0 = _time.time() - 3600 + db.create_session("parent", "cli") + db._conn.execute("UPDATE sessions SET started_at=? WHERE id=?", (t0, "parent")) + db.create_session("child", "cli", parent_session_id="parent") + # Child started BEFORE parent ended, and parent ended for a non- + # compression reason — not a continuation edge. + db._conn.execute("UPDATE sessions SET started_at=? WHERE id=?", (t0 + 10, "child")) + db._conn.execute( + "UPDATE sessions SET ended_at=?, end_reason='user_exit' WHERE id=?", + (t0 + 100, "parent"), + ) + db._conn.commit() + db.set_session_title("parent", "shared") + with pytest.raises(ValueError, match="already in use"): + db.set_session_title("child", "shared") + + class TestSanitizeTitle: """Tests for SessionDB.sanitize_title() validation and cleaning.""" From 8c70346e33e34d204ecf9ef1c29e8d374182d56c Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Fri, 19 Jun 2026 17:37:39 +0530 Subject: [PATCH 33/90] refactor(sessions): express compression-ancestor check as one recursive CTE _is_compression_ancestor walked parent links in a 100-hop Python loop issuing two SELECTs per hop and hand-re-encoded the compression continuation edge a fourth time. Collapse it into a single recursive CTE that reuses the canonical _COMPRESSION_CHILD_SQL fragment (already shared by _ephemeral_child_sql and set_session_archived), so the edge definition lives in exactly one place. The UNION recursion also dedups visited nodes, making it cycle-safe without the defensive hop cap. Behavior is unchanged (all TestSessionTitleLineage + existing title-command tests pass). --- hermes_state.py | 55 ++++++++++++++++++++++--------------------------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/hermes_state.py b/hermes_state.py index 2ca3c657d13..8847593d47c 100644 --- a/hermes_state.py +++ b/hermes_state.py @@ -1842,41 +1842,36 @@ class SessionDB: """Return True if *ancestor_id* is a compression predecessor of *descendant_id* (walking parent links up the continuation chain). - Uses the same edge definition as :meth:`get_compression_tip`: a - parent → child edge counts as a compression continuation only when the + The continuation edge is the canonical one shared with + :func:`_ephemeral_child_sql` / :meth:`set_session_archived` + (``_COMPRESSION_CHILD_SQL``): a parent → child edge counts only when the parent ended with ``end_reason = 'compression'`` and the child started - at or after the parent's ``ended_at`` (which distinguishes continuations + at or after the parent's ``ended_at``, which distinguishes continuations from delegate subagents / branch children that also carry a - ``parent_session_id``). + ``parent_session_id``. Expressed as a single recursive CTE rather than a + per-hop Python walk so the edge definition lives in exactly one place. """ if not ancestor_id or not descendant_id or ancestor_id == descendant_id: return False - current = descendant_id - # Bound the walk defensively, mirroring get_compression_tip. - for _ in range(100): - row = conn.execute( - "SELECT parent_session_id, started_at FROM sessions WHERE id = ?", - (current,), - ).fetchone() - if row is None or not row["parent_session_id"]: - return False - parent_id = row["parent_session_id"] - parent = conn.execute( - "SELECT ended_at, end_reason FROM sessions WHERE id = ?", - (parent_id,), - ).fetchone() - if ( - parent is None - or parent["end_reason"] != "compression" - or parent["ended_at"] is None - or row["started_at"] is None - or row["started_at"] < parent["ended_at"] - ): - return False - if parent_id == ancestor_id: - return True - current = parent_id - return False + # Walk parent links up from the descendant, following only compression + # continuation edges, and check whether ancestor_id is reached. + edge = _COMPRESSION_CHILD_SQL.format(a="child") + row = conn.execute( + f""" + WITH RECURSIVE ancestors(id) AS ( + SELECT ? + UNION + SELECT parent.id + FROM ancestors a + JOIN sessions child ON child.id = a.id + JOIN sessions parent ON parent.id = child.parent_session_id + WHERE {edge} + ) + SELECT 1 FROM ancestors WHERE id = ? AND id != ? LIMIT 1 + """, + (descendant_id, ancestor_id, descendant_id), + ).fetchone() + return row is not None def set_session_title(self, session_id: str, title: str) -> bool: """Set or update a session's title. From f9ffe0bc3f619fc2100bd3e77622090e9c794603 Mon Sep 17 00:00:00 2001 From: xxxigm Date: Fri, 19 Jun 2026 18:54:27 +0700 Subject: [PATCH 34/90] fix(desktop): resume stored session id on notification click MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Native notifications (approval / sudo / secret / clarify) are tagged with the gateway *runtime* session id — the key under which the session lives in the gateway's in-memory `_sessions` map and the id every event carries (`tui_gateway/server.py` `_emit(event, sid, ...)`). The chat route, however, is keyed by the *stored* session id (`stored_session_id`), which is a different value: a new chat gets its runtime id immediately but its stored id only once the first turn persists. `onFocusSession` navigated straight to `sessionRoute()`, so clicking a notification (e.g. an approval prompt) sent the route-resume path a runtime id where it expects a stored id. `useRouteResume` then resumed it as a stored session -> REST `/api/sessions/` 404 "session not found", and the running session was navigated away, which the user experiences as the session being destroyed. Translate runtime -> stored before navigating via the existing `runtimeIdByStoredSessionId` map (new `storedSessionIdForNotification` helper), falling back to the id as-is when no mapping is known. The Approve/Reject notification button path is untouched: `approval.respond` is routed by the runtime id (`_sess()` -> `_sessions[session_id]`), so it must keep carrying the runtime id. --- apps/desktop/src/app/desktop-controller.tsx | 11 ++++++--- apps/desktop/src/lib/session-ids.ts | 26 +++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 apps/desktop/src/lib/session-ids.ts diff --git a/apps/desktop/src/app/desktop-controller.tsx b/apps/desktop/src/app/desktop-controller.tsx index 05dfbbc764f..c2523bf3654 100644 --- a/apps/desktop/src/app/desktop-controller.tsx +++ b/apps/desktop/src/app/desktop-controller.tsx @@ -20,6 +20,7 @@ import { MESSAGING_SESSION_SOURCE_IDS, normalizeSessionSource } from '../lib/session-source' +import { storedSessionIdForNotification } from '../lib/session-ids' import { latestSessionTodos } from '../lib/todos' import { setCronFocusJobId, setCronJobs } from '../store/cron' import { @@ -276,16 +277,20 @@ export function DesktopController() { } }, []) - // Notification click: the main process already focused the window; jump to its session. + // Notification click: the main process already focused the window; jump to its + // session. Notifications are tagged with the gateway *runtime* session id, but + // the chat route is keyed by the *stored* id — navigating with the runtime id + // resumes a non-existent stored session ("session not found") and strands the + // user. Translate runtime -> stored before navigating. useEffect(() => { const unsubscribe = window.hermesDesktop?.onFocusSession?.(sessionId => { if (sessionId) { - navigate(sessionRoute(sessionId)) + navigate(sessionRoute(storedSessionIdForNotification(sessionId, runtimeIdByStoredSessionIdRef.current))) } }) return () => unsubscribe?.() - }, [navigate]) + }, [navigate, runtimeIdByStoredSessionIdRef]) // Notification action button (Approve/Reject) — resolve in place, no navigation. useEffect(() => { diff --git a/apps/desktop/src/lib/session-ids.ts b/apps/desktop/src/lib/session-ids.ts new file mode 100644 index 00000000000..c97cadc2628 --- /dev/null +++ b/apps/desktop/src/lib/session-ids.ts @@ -0,0 +1,26 @@ +// The gateway tags every event — and therefore every native notification — +// with the *runtime* session id (the key under which the session lives in the +// gateway's in-memory `_sessions` map). The chat route, however, is keyed by +// the *stored* session id (`stored_session_id`), which is a different value: +// a brand-new chat gets a runtime id immediately but its stored id is assigned +// when the first turn persists. Navigating to a runtime id therefore tries to +// resume a stored session that does not exist ("session not found") and +// strands the user, who experiences it as the running session being destroyed. +// +// `runtimeIdByStoredSessionId` maps stored -> runtime; this resolves the +// reverse so notification-click navigation lands on the real route. The id is +// returned unchanged when no mapping is known — it may already be a stored id +// (e.g. a notification for a session this window never opened), in which case +// the normal resume/REST lookup handles it. +export function storedSessionIdForNotification( + id: string, + runtimeIdByStoredSessionId: ReadonlyMap +): string { + for (const [storedId, runtimeId] of runtimeIdByStoredSessionId) { + if (runtimeId === id) { + return storedId + } + } + + return id +} From 069011dd0c8f714519d145f4fe46785cfc3fe00b Mon Sep 17 00:00:00 2001 From: xxxigm Date: Fri, 19 Jun 2026 18:54:27 +0700 Subject: [PATCH 35/90] test(desktop): cover runtime->stored notification id resolution Unit-test `storedSessionIdForNotification`: runtime ids resolve to their stored id, unknown ids and empty maps pass through unchanged, the right stored id is picked among several sessions, and stored ids (map keys) are never rewritten. --- apps/desktop/src/app/desktop-controller.tsx | 2 +- apps/desktop/src/lib/session-ids.test.ts | 44 +++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 apps/desktop/src/lib/session-ids.test.ts diff --git a/apps/desktop/src/app/desktop-controller.tsx b/apps/desktop/src/app/desktop-controller.tsx index c2523bf3654..5ca73061135 100644 --- a/apps/desktop/src/app/desktop-controller.tsx +++ b/apps/desktop/src/app/desktop-controller.tsx @@ -14,13 +14,13 @@ import { useSkinCommand } from '@/themes/use-skin-command' import { formatRefValue } from '../components/assistant-ui/directive-text' import { getCronJobs, getSessionMessages, listAllProfileSessions, type SessionInfo, triggerCronJob } from '../hermes' import { type ChatMessage, chatMessageText, preserveLocalAssistantErrors, toChatMessages } from '../lib/chat-messages' +import { storedSessionIdForNotification } from '../lib/session-ids' import { isMessagingSource, LOCAL_SESSION_SOURCE_IDS, MESSAGING_SESSION_SOURCE_IDS, normalizeSessionSource } from '../lib/session-source' -import { storedSessionIdForNotification } from '../lib/session-ids' import { latestSessionTodos } from '../lib/todos' import { setCronFocusJobId, setCronJobs } from '../store/cron' import { diff --git a/apps/desktop/src/lib/session-ids.test.ts b/apps/desktop/src/lib/session-ids.test.ts new file mode 100644 index 00000000000..b5653c8eecd --- /dev/null +++ b/apps/desktop/src/lib/session-ids.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest' + +import { storedSessionIdForNotification } from './session-ids' + +describe('storedSessionIdForNotification', () => { + it('translates a runtime id back to its stored id', () => { + // The route is keyed by the stored id, but notifications carry the runtime + // id. Resolving runtime -> stored keeps notification-click navigation from + // resuming a non-existent stored session ("session not found"). + const map = new Map([['stored-abc', 'runtime-123']]) + + expect(storedSessionIdForNotification('runtime-123', map)).toBe('stored-abc') + }) + + it('returns the id unchanged when no mapping is known', () => { + // A notification for a session this window never opened may already carry a + // stored id; let the resume/REST lookup handle it as-is. + const map = new Map([['stored-abc', 'runtime-123']]) + + expect(storedSessionIdForNotification('stored-xyz', map)).toBe('stored-xyz') + }) + + it('returns the id unchanged for an empty map', () => { + expect(storedSessionIdForNotification('runtime-123', new Map())).toBe('runtime-123') + }) + + it('resolves the correct stored id among several sessions', () => { + const map = new Map([ + ['stored-1', 'runtime-1'], + ['stored-2', 'runtime-2'], + ['stored-3', 'runtime-3'] + ]) + + expect(storedSessionIdForNotification('runtime-2', map)).toBe('stored-2') + }) + + it('does not treat a stored id as a runtime id (keys are not matched)', () => { + // The map is stored -> runtime. A value that only appears as a *key* must + // not be rewritten, otherwise an already-stored id could be mangled. + const map = new Map([['stored-1', 'runtime-1']]) + + expect(storedSessionIdForNotification('stored-1', map)).toBe('stored-1') + }) +}) From bce1e36b5769791b8e050a9f174982b2b6a6215a Mon Sep 17 00:00:00 2001 From: Kenny John Jacob Date: Tue, 2 Jun 2026 02:01:27 +0000 Subject: [PATCH 36/90] fix(discord): unwrap dict choices + soft-boundary truncate clarify buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs surfaced from production usage in #37134: 1. Dict choices rendered as Python repr. LLMs sometimes emit [{"description": "..."}] instead of bare strings; the old str(c).strip() coercion turned the whole dict into "{'description': '...'}" on the button label. Fix: add a _flatten_choice helper that unwraps dicts against the canonical LLM tool-call user-facing keys (label, description, text, title) in that order. Dicts with none of those keys are dropped. The "name" and "value" keys are deliberately NOT in the priority list — they're Discord-component-shaped fields that could appear in dicts that aren't meant to be choices (a developer-error wiring that passes a Button-shaped object); picking them would leak raw enum values or 4-char model identifiers onto user-facing buttons. 2. Mid-word truncation on long button labels. The old choice[:72] + "..." cut at position 72, mid-word. Worse, the three-char ellipsis ate into the 80-char Discord label cap, leaving only 75 chars of body. Fix: budget-aware cut strategy with three tiers: a. Last space in the trailing half of the budget (word boundary). b. Last soft boundary (- , . )) in the trailing half — used only when no word boundary exists. c. Hard cut at the budget limit (last resort). Use single U+2026 (…) to fit the cap. Cut AT soft boundaries (inclusive) so the label ends on the boundary char rather than on the alpha char that followed it. Tests: - test_unwraps_dict_choices_to_description: reproduces the screenshot in #37134, asserts the Python repr is gone. - test_unwrap_prefers_description_over_name_in_multi_key_dict: regression guard for the name-key order in the unwrap list. - test_unwrap_prefers_label_over_description: regression guard for label winning over description. - test_unwrap_does_not_pick_value_or_name_alone: regression guard for the "name"/"value" fields being absent. - test_truncates_long_choice_label: 200-char input, asserts total <= 80 and U+2026. - test_truncates_long_choice_label_breaks_on_word_boundary: asserts the cut is on a space, not mid-word. - test_truncates_long_no_space_choice_on_soft_boundary: adversarial input where position 76 is mid-word alpha, asserts the renderer falls back to a soft boundary. Parity: telegram clarify suite (12 tests) still passes; the helper is a Discord adapter local, not shared with the gateway. Follow-up: gateway/platforms/telegram.py has the same str(c).strip() pattern in its own send_clarify and will need a similar fix (separate PR to keep this diff reviewable). Fixes #37134 --- plugins/platforms/discord/adapter.py | 81 +++++++- tests/gateway/test_discord_clarify_buttons.py | 178 +++++++++++++++++- 2 files changed, 253 insertions(+), 6 deletions(-) diff --git a/plugins/platforms/discord/adapter.py b/plugins/platforms/discord/adapter.py index 8146ca9de10..6ca199dcfaf 100644 --- a/plugins/platforms/discord/adapter.py +++ b/plugins/platforms/discord/adapter.py @@ -4566,6 +4566,13 @@ class DiscordAdapter(BasePlatformAdapter): Open-ended mode (``choices`` empty/None): renders the question as plain embed text — no buttons. The gateway's text-intercept captures the next message in this session and resolves the clarify. + + Choice normalisation: ``choices`` may contain bare strings OR dicts + (LLMs sometimes emit ``[{"description": "..."}]`` instead of bare + strings, which would otherwise render as raw Python repr on the + button label). Dict choices are unwrapped against the canonical + LLM tool-call keys ``label``, ``description``, ``text``, ``title`` + in that order. Dicts with none of those keys are dropped. """ if not self._client or not DISCORD_AVAILABLE: return SendResult(success=False, error="Not connected") @@ -4591,8 +4598,37 @@ class DiscordAdapter(BasePlatformAdapter): color=discord.Color.orange(), ) + # Normalise choices: LLMs sometimes emit `[{"description": "..."}]` + # instead of bare strings, which would render as raw Python repr on + # the button label. Unwrap the common shapes, then stringify. + def _flatten_choice(c): + if c is None: + return "" + if isinstance(c, str): + return c.strip() + if isinstance(c, dict): + # Prefer the canonical LLM tool-call user-facing keys + # in the order the LLM is most likely to emit them. + # 'name' and 'value' are deliberately NOT here: they're + # Discord-component-shaped fields that could appear in + # dicts that aren't meant to be choices (e.g., a + # developer-error wiring that passes a Button-shaped + # object). Picking them would leak raw enum values + # or 4-char model identifiers onto user-facing buttons. + # If a dict has none of the canonical keys, drop it + # rather than picking some random field — a garbage + # button label is worse than no button at all. + for key in ("label", "description", "text", "title"): + v = c.get(key) + if isinstance(v, str) and v.strip(): + return v.strip() + return "" + if isinstance(c, (list, tuple)): + return " ".join(_flatten_choice(x) for x in c).strip() + return str(c).strip() + clean_choices = [ - str(c).strip() for c in (choices or []) if c is not None and str(c).strip() + s for s in (_flatten_choice(c) for c in (choices or [])) if s ] # Discord allows up to 5 buttons per row, 5 rows per view = 25. # We reserve one slot for the "Other" button, so cap at 24 choices. @@ -6129,10 +6165,47 @@ def _define_discord_view_classes() -> None: self.resolved = False for index, choice in enumerate(self.choices): - # Discord button labels are capped at 80 chars. - label_body = choice if len(choice) <= 75 else choice[:72] + "..." + # Discord button labels are capped at 80 chars. On mobile the + # visible width is much narrower (often <40 chars before it + # wraps to 2 lines and the second line gets cut off), so we + # cap aggressively and cut at a word boundary when possible + # to keep the trailing text readable. + # + # Cut strategy (most-preferred to least-preferred): + # 1. Last space in the trailing half of the budget + # (cleanest word boundary) + # 2. Last soft boundary in the trailing half of the + # budget (hyphen, comma, period, paren) + # 3. Hard cut at the budget limit (last resort) + prefix = f"{index + 1}. " + budget = 80 - len(prefix) + if len(choice) <= budget: + label_body = choice + else: + truncated = choice[: budget - 1].rstrip() + cut_at = -1 + # 1. Last space in the trailing half of the budget. + space = truncated.rfind(" ") + if space >= budget // 2: + cut_at = space + # 2. Soft boundary — only if no word boundary found. + # Find the latest soft boundary in the trailing half + # of the budget; that maximizes preserved text length. + # Cut AT the soft boundary (inclusive) so the label + # ends on the soft char (e.g. "-" or ",") rather than + # on the alpha char that followed it. + if cut_at < 0: + latest_soft = max( + (truncated.rfind(s) for s in ("-", ",", ".", ")")), + default=-1, + ) + if latest_soft >= budget // 2: + cut_at = latest_soft + 1 + if cut_at > 0: + truncated = truncated[:cut_at] + label_body = truncated.rstrip() + "…" button = discord.ui.Button( - label=f"{index + 1}. {label_body}", + label=f"{prefix}{label_body}", style=discord.ButtonStyle.primary, custom_id=f"clarify:{clarify_id}:{index}", ) diff --git a/tests/gateway/test_discord_clarify_buttons.py b/tests/gateway/test_discord_clarify_buttons.py index c83e52dba5a..b8b5dc10ed2 100644 --- a/tests/gateway/test_discord_clarify_buttons.py +++ b/tests/gateway/test_discord_clarify_buttons.py @@ -122,13 +122,56 @@ class TestClarifyChoiceViewConstruction: clarify_id="cidZ", allowed_user_ids=set(), ) - # 75 chars + 3 ellipsis chars in the body, plus "1. " prefix + # 78 chars + single-char ellipsis in the body, plus "1. " prefix. + # Uses U+2026 (…) instead of "..." to fit the 80-char Discord cap. first_label = view.children[0].label assert first_label.startswith("1. ") - assert first_label.endswith("...") + assert first_label.endswith("\u2026") # Final label total <= 80 (Discord cap on button labels) assert len(first_label) <= 80 + def test_truncates_long_choice_label_breaks_on_word_boundary(self): + # Long choice with spaces — should cut at the last whole word so the + # trailing text stays readable on Discord mobile. + long_choice = ( + "Tight, well-illustrated, covers all 3 audiences " + "(patients, families, curious general readers)" + ) + view = ClarifyChoiceView( + choices=[long_choice], + clarify_id="cidW", + allowed_user_ids=set(), + ) + first_label = view.children[0].label + assert first_label.startswith("1. ") + assert first_label.endswith("\u2026") + # No mid-word fragment before the ellipsis. + assert not first_label.rstrip("\u2026").endswith("(") + + def test_truncates_long_no_space_choice_on_soft_boundary(self): + # A long choice with soft boundaries (commas, hyphens) but no spaces + # should still cut on a soft boundary, not mid-word. We use an input + # where position 76 is NOT a soft boundary — the test only passes + # if the renderer actively searches backward for a soft char + # rather than blindly cutting at the budget limit. + long_choice = "a" * 30 + "-" + "b" * 30 + "-" + "c" * 30 + "-" + "d" * 30 + # 30a-30b-30c-30d = 30 + 1 + 30 + 1 + 30 + 1 + 30 = 123 chars + # Position 76 is 'b' (a mid-word alpha). The renderer must look back + # for a '-' to cut on. + view = ClarifyChoiceView( + choices=[long_choice], + clarify_id="cidSB", + allowed_user_ids=set(), + ) + first_label = view.children[0].label + assert first_label.endswith("\u2026") + assert len(first_label) <= 80 + body = first_label[len("1. "):].rstrip("\u2026") + last_char = body[-1] + assert last_char in {"-", ",", ".", ")", " "}, ( + f"Label cuts mid-word at {last_char!r}: {first_label!r}" + ) + # =========================================================================== # Choice callback → resolve_gateway_clarify @@ -404,3 +447,134 @@ class TestDiscordSendClarify: # Only 1 real choice + 1 Other = 2 children assert len(view.children) == 2 assert "real-choice" in view.children[0].label + + @pytest.mark.asyncio + async def test_unwraps_dict_choices_to_description(self): + # LLMs sometimes emit [{"description": "..."}] instead of bare strings + # — the renderer must unwrap common dict shapes, not str() the whole + # dict into a Python repr on the button label. + adapter = _make_adapter() + channel = MagicMock() + sent_msg = MagicMock() + sent_msg.id = 555 + channel.send = AsyncMock(return_value=sent_msg) + adapter._client.get_channel = MagicMock(return_value=channel) + + malformed = [ + {"description": "Tight, well-illustrated"}, + {"label": "Use label key"}, + {"text": "Use text key"}, + "normal-string", # strings still pass through + ] + await adapter.send_clarify( + chat_id="9001", + question="?", + choices=malformed, + clarify_id="cidU", + session_key="sk-U", + ) + kwargs = channel.send.call_args.kwargs + view = kwargs["view"] + labels = [b.label for b in view.children[:-1]] # exclude Other + # No raw Python repr should leak onto any label. + for label in labels: + assert "{'" not in label + assert "':" not in label + # Each dict unwrapped to its inner string. + assert any("Tight, well-illustrated" in lbl for lbl in labels) + assert any("Use label key" in lbl for lbl in labels) + assert any("Use text key" in lbl for lbl in labels) + assert any("normal-string" in lbl for lbl in labels) + + @pytest.mark.asyncio + async def test_unwrap_prefers_description_over_name_in_multi_key_dict(self): + # When the LLM emits both 'name' (often a short identifier in + # OpenAI-style tool calls) and 'description' (the user-facing text), + # the renderer must surface 'description'. The user should never see + # a 4-char model identifier on a button label. + adapter = _make_adapter() + channel = MagicMock() + sent_msg = MagicMock() + sent_msg.id = 666 + channel.send = AsyncMock(return_value=sent_msg) + adapter._client.get_channel = MagicMock(return_value=channel) + + await adapter.send_clarify( + chat_id="9001", + question="?", + choices=[{"name": "tight", "description": "Tight, well-illustrated"}], + clarify_id="cidN", + session_key="sk-N", + ) + kwargs = channel.send.call_args.kwargs + view = kwargs["view"] + choice_label = view.children[0].label + assert "Tight, well-illustrated" in choice_label + # The 'name' value (a short identifier) must NOT have leaked. + body = choice_label.split("1. ", 1)[1].rstrip("\u2026") + assert "tight" not in body, f"'name' leaked onto button: {choice_label!r}" + + @pytest.mark.asyncio + async def test_unwrap_prefers_label_over_description(self): + # When both 'label' and 'description' are present, 'label' wins. + # 'label' is the canonical short user-facing text in most LLM tool + # conventions; 'description' is the longer explanation. + adapter = _make_adapter() + channel = MagicMock() + sent_msg = MagicMock() + sent_msg.id = 777 + channel.send = AsyncMock(return_value=sent_msg) + adapter._client.get_channel = MagicMock(return_value=channel) + + await adapter.send_clarify( + chat_id="9001", + question="?", + choices=[{"label": "Short", "description": "Long verbose explanation"}], + clarify_id="cidL", + session_key="sk-L", + ) + kwargs = channel.send.call_args.kwargs + view = kwargs["view"] + choice_label = view.children[0].label + assert "Short" in choice_label + # The longer description must NOT have leaked. + assert "Long verbose" not in choice_label, ( + f"'description' leaked over 'label': {choice_label!r}" + ) + + @pytest.mark.asyncio + async def test_unwrap_does_not_pick_value_or_name_alone(self): + # 'name' and 'value' are Discord-component-shaped fields that could + # accidentally appear in dicts not intended as choices (e.g., a + # developer-error in the gateway wiring). The renderer should not + # surface them as button labels — only the well-known LLM tool-call + # keys (label, description, text, title) should win. + adapter = _make_adapter() + channel = MagicMock() + sent_msg = MagicMock() + sent_msg.id = 888 + channel.send = AsyncMock(return_value=sent_msg) + adapter._client.get_channel = MagicMock(return_value=channel) + + await adapter.send_clarify( + chat_id="9001", + question="?", + choices=[ + {"name": "only_name_here"}, # should be filtered out + {"value": "only_value_here"}, # should be filtered out + {"description": "real choice"}, + ], + clarify_id="cidNV", + session_key="sk-NV", + ) + kwargs = channel.send.call_args.kwargs + view = kwargs["view"] + choice_labels = [b.label for b in view.children[:-1]] # exclude Other + # Only the well-formed dict survives. + assert len(choice_labels) == 1, ( + f"Expected 1 choice, got {len(choice_labels)}: {choice_labels!r}" + ) + assert "real choice" in choice_labels[0] + for label in choice_labels: + assert "only_name_here" not in label, f"name leaked: {label!r}" + assert "only_value_here" not in label, f"value leaked: {label!r}" From 2c3aebcadccef685c96b8106361abed904a43a26 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Thu, 18 Jun 2026 22:16:57 -0700 Subject: [PATCH 37/90] fix(clarify): unwrap dict choices at the source so every surface gets clean text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Discord fix (previous commit) handles dict-shaped clarify choices at the Discord adapter only. The same dict-repr leak originates upstream at tools/clarify_tool.py's str(c).strip() normalization — the single platform-agnostic point both the CLI and every gateway adapter flow through. When an LLM emits [{"description": "..."}] instead of bare strings, str(c) produced {'description': '...'} which leaked onto the CLI panel (cli.py:13048/13081), was returned verbatim as the user's answer (cli.py:11945), and hit Telegram's numbered list too. Add _flatten_choice (same label->description->text->title unwrap as the Discord adapter, name/value excluded, keyless dicts dropped) and apply it at the normalization line. Fixes CLI + Telegram + all platforms at the root; the Discord smart-truncation now operates on already-clean text. Adds johnjacobkenny to AUTHOR_MAP for the salvaged commit. --- scripts/release.py | 1 + tests/tools/test_clarify_tool.py | 65 ++++++++++++++++++++++++++++++++ tools/clarify_tool.py | 40 +++++++++++++++++++- 3 files changed, 105 insertions(+), 1 deletion(-) diff --git a/scripts/release.py b/scripts/release.py index 7e5901fd568..20c6a6bfa0a 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -103,6 +103,7 @@ AUTHOR_MAP = { "290859878+synapsesx@users.noreply.github.com": "synapsesx", "157689911+itsflownium@users.noreply.github.com": "itsflownium", "dirtyren@users.noreply.github.com": "dirtyren", + "johnjacobkenny@users.noreply.github.com": "johnjacobkenny", "chanyoung.kim@nota.ai": "channkim", "stevenn.damatoo@gmail.com": "x1erra", "evansrory@gmail.com": "zimigit2020", diff --git a/tests/tools/test_clarify_tool.py b/tests/tools/test_clarify_tool.py index 8659e1f13af..0c38961dd8d 100644 --- a/tests/tools/test_clarify_tool.py +++ b/tests/tools/test_clarify_tool.py @@ -9,6 +9,7 @@ from tools.clarify_tool import ( check_clarify_requirements, MAX_CHOICES, CLARIFY_SCHEMA, + _flatten_choice, ) @@ -164,6 +165,70 @@ class TestCheckClarifyRequirements: assert check_clarify_requirements() is True +class TestClarifyDictChoices: + """Dict-shaped choices must be unwrapped to user-facing text at the source. + + LLMs sometimes emit [{"description": "..."}] instead of bare strings. The + naive str(c) coercion leaked the Python dict repr onto every surface (CLI + panel, Discord buttons, Telegram list) AND returned it verbatim as the + user's answer. _flatten_choice normalises at the one platform-agnostic + entry point so the whole class is fixed in one place. + """ + + def test_flatten_unwraps_label_first(self): + assert _flatten_choice({"label": "Short", "description": "Long"}) == "Short" + + def test_flatten_unwraps_description_when_no_label(self): + assert _flatten_choice({"description": "A loose layout"}) == "A loose layout" + + def test_flatten_unwrap_order_label_over_description(self): + assert _flatten_choice({"description": "verbose", "label": "tight"}) == "tight" + + def test_flatten_drops_name_value_only_dict(self): + # name/value are component-shaped fields, not user-facing labels — + # picking them would leak raw enum values / short model ids. + assert _flatten_choice({"name": "tight", "value": "x"}) == "" + + def test_flatten_prefers_canonical_key_over_name(self): + assert _flatten_choice({"name": "tight", "description": "Tight desc"}) == "Tight desc" + + def test_flatten_drops_keyless_dict(self): + assert _flatten_choice({"foo": "bar", "n": 1}) == "" + + def test_flatten_passthrough_string_and_scalar(self): + assert _flatten_choice("plain") == "plain" + assert _flatten_choice(7) == "7" + assert _flatten_choice(None) == "" + + def test_dict_choices_reach_callback_as_clean_text(self): + """The whole point: the UI callback never sees a dict repr.""" + seen = [] + + def cb(question, choices): + seen.extend(choices or []) + return choices[0] + + result = json.loads(clarify_tool( + "Pick a layout", + choices=[ + {"choice": "Tight", "description": "Tight, covers all 3 points"}, + {"description": "Loose layout"}, + {"name": "modelid", "value": "abc"}, # dropped, not leaked + "A plain string choice", + ], + callback=cb, + )) # type: ignore + assert seen == [ + "Tight, covers all 3 points", + "Loose layout", + "A plain string choice", + ] + # and the resolved answer is clean text, not a dict repr + assert result["user_response"] == "Tight, covers all 3 points" + assert "{" not in result["user_response"] + assert all("{" not in c for c in result["choices_offered"]) + + class TestClarifySchema: """Tests for the OpenAI function-calling schema.""" diff --git a/tools/clarify_tool.py b/tools/clarify_tool.py index c44787554cc..3560ccf6126 100644 --- a/tools/clarify_tool.py +++ b/tools/clarify_tool.py @@ -20,6 +20,39 @@ from typing import List, Optional, Callable MAX_CHOICES = 4 +def _flatten_choice(c) -> str: + """Coerce a single choice into its user-facing display string. + + The schema declares choices as bare strings, but LLMs sometimes emit + dict-shaped choices like ``[{"description": "..."}]``. A naive ``str(c)`` + turns the whole dict into its Python repr — ``{'description': '...'}`` — + which then leaks onto every surface that renders the choice (CLI panel, + Discord buttons, Telegram numbered list) AND is returned verbatim as the + user's answer. Normalising here, at the one platform-agnostic entry point, + fixes the whole class in one place instead of per-adapter. + + Dict unwrap order is the canonical LLM tool-call user-facing keys: + ``label`` → ``description`` → ``text`` → ``title``. ``name`` and ``value`` + are deliberately excluded — they're component-shaped fields that could + carry raw enum values or short identifiers, not human-readable labels. A + dict with none of the canonical keys is dropped (returns ""), since a + garbage label is worse than no choice at all. + """ + if c is None: + return "" + if isinstance(c, str): + return c.strip() + if isinstance(c, dict): + for key in ("label", "description", "text", "title"): + v = c.get(key) + if isinstance(v, str) and v.strip(): + return v.strip() + return "" + if isinstance(c, (list, tuple)): + return " ".join(_flatten_choice(x) for x in c).strip() + return str(c).strip() + + def clarify_tool( question: str, choices: Optional[List[str]] = None, @@ -48,7 +81,12 @@ def clarify_tool( if choices is not None: if not isinstance(choices, list): return tool_error("choices must be a list of strings.") - choices = [str(c).strip() for c in choices if str(c).strip()] + # LLMs sometimes emit dict-shaped choices (e.g. [{"description": "..."}]) + # instead of bare strings. _flatten_choice unwraps them to their + # user-facing text here — the single platform-agnostic entry point — + # so the CLI panel, Discord buttons, and Telegram list all render clean + # text and the resolved answer is never a raw Python dict repr. + choices = [s for s in (_flatten_choice(c) for c in choices) if s] if len(choices) > MAX_CHOICES: choices = choices[:MAX_CHOICES] if not choices: From 460b1e50e515fd9b0b8f472f66f8773336862d88 Mon Sep 17 00:00:00 2001 From: infinitycrew39 Date: Thu, 18 Jun 2026 07:28:28 +0700 Subject: [PATCH 38/90] fix(gateway): refresh max_turns before resolving runtime budget --- gateway/platforms/api_server.py | 10 ++++++++-- gateway/run.py | 19 +++++++++++-------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/gateway/platforms/api_server.py b/gateway/platforms/api_server.py index da86952a09d..54720f2b300 100644 --- a/gateway/platforms/api_server.py +++ b/gateway/platforms/api_server.py @@ -1033,7 +1033,13 @@ class APIServerAdapter(BasePlatformAdapter): — matching the semantics of the native gateway's ``session_key``. """ from run_agent import AIAgent - from gateway.run import _resolve_runtime_agent_kwargs, _resolve_gateway_model, _load_gateway_config, GatewayRunner + from gateway.run import ( + _current_max_iterations, + _resolve_runtime_agent_kwargs, + _resolve_gateway_model, + _load_gateway_config, + GatewayRunner, + ) from hermes_cli.tools_config import _get_platform_tools runtime_kwargs = _resolve_runtime_agent_kwargs() @@ -1043,7 +1049,7 @@ class APIServerAdapter(BasePlatformAdapter): user_config = _load_gateway_config() enabled_toolsets = sorted(_get_platform_tools(user_config, "api_server")) - max_iterations = int(os.getenv("HERMES_MAX_ITERATIONS", "90")) + max_iterations = _current_max_iterations() # Load fallback provider chain so the API server platform has the # same fallback behaviour as Telegram/Discord/Slack (fixes #4954). diff --git a/gateway/run.py b/gateway/run.py index e24afd035e7..59dd890f8c9 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -1196,6 +1196,15 @@ def _reload_runtime_env_preserving_config_authority() -> None: os.environ["HERMES_MAX_ITERATIONS"] = str(agent_cfg["max_turns"]) +def _current_max_iterations() -> int: + """Return the current per-turn iteration budget after runtime env refresh.""" + _reload_runtime_env_preserving_config_authority() + try: + return int(os.getenv("HERMES_MAX_ITERATIONS", "90")) + except (TypeError, ValueError): + return 90 + + _DOCKER_VOLUME_SPEC_RE = re.compile(r"^(?P.+):(?P/[^:]+?)(?::(?P[^:]+))?$") _DOCKER_MEDIA_OUTPUT_CONTAINER_PATHS = {"/output", "/outputs"} @@ -10633,7 +10642,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew disabled_toolsets = agent_cfg.get("disabled_toolsets") or None pr = self._provider_routing - max_iterations = int(os.getenv("HERMES_MAX_ITERATIONS", "90")) + max_iterations = _current_max_iterations() reasoning_config = self._resolve_session_reasoning_config(source=source) self._reasoning_config = reasoning_config self._service_tier = self._load_service_tier() @@ -14581,9 +14590,6 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew # session_key is now set via contextvars in _set_session_env() # (concurrency-safe). Keep os.environ as fallback for CLI/cron. os.environ["HERMES_SESSION_KEY"] = session_key or "" - - # Read from env var or use default (same as CLI) - max_iterations = int(os.getenv("HERMES_MAX_ITERATIONS", "90")) # Map platform enum to the platform hint key the agent understands. # Platform.LOCAL ("local") maps to "cli"; others pass through as-is. @@ -14598,10 +14604,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew if self._ephemeral_system_prompt: combined_ephemeral = (combined_ephemeral + "\n\n" + self._ephemeral_system_prompt).strip() - # Re-read .env and config for fresh credentials (gateway is long-lived, - # keys may change without restart). Keep config.yaml authoritative for - # runtime budget settings bridged into env vars. - _reload_runtime_env_preserving_config_authority() + max_iterations = _current_max_iterations() try: model, runtime_kwargs = self._resolve_session_agent_runtime( From dcac719527c519f068d7cd6d5230aca64e657201 Mon Sep 17 00:00:00 2001 From: infinitycrew39 Date: Thu, 18 Jun 2026 07:28:28 +0700 Subject: [PATCH 39/90] test(gateway): cover runtime max_turns refresh --- tests/gateway/test_api_server.py | 34 +++++++++++++++++++ ...est_runtime_env_reload_config_authority.py | 15 ++++++++ 2 files changed, 49 insertions(+) diff --git a/tests/gateway/test_api_server.py b/tests/gateway/test_api_server.py index 95d49d8b4f1..ac5e29c4d3c 100644 --- a/tests/gateway/test_api_server.py +++ b/tests/gateway/test_api_server.py @@ -337,6 +337,40 @@ class TestAdapterInit: assert isinstance(agent, FakeAgent) assert captured["reasoning_config"] == {"enabled": True, "effort": "xhigh"} + def test_create_agent_refreshes_max_iterations_from_runtime_config(self, monkeypatch): + captured = {} + + class FakeAgent: + def __init__(self, **kwargs): + captured.update(kwargs) + + monkeypatch.setattr("run_agent.AIAgent", FakeAgent) + monkeypatch.setattr( + "gateway.run._resolve_runtime_agent_kwargs", + lambda: { + "provider": "openai", + "base_url": "https://example.test/v1", + "api_mode": "chat_completions", + }, + ) + monkeypatch.setattr("gateway.run._resolve_gateway_model", lambda: "gpt-5") + monkeypatch.setattr("gateway.run._load_gateway_config", lambda: {"agent": {"max_turns": 200}}) + monkeypatch.setattr( + "gateway.run.GatewayRunner._load_reasoning_config", + staticmethod(lambda: {}), + ) + monkeypatch.setattr("gateway.run.GatewayRunner._load_fallback_model", staticmethod(lambda: None)) + monkeypatch.setattr("gateway.run._current_max_iterations", lambda: 200) + monkeypatch.setattr("hermes_cli.tools_config._get_platform_tools", lambda *_: set()) + + adapter = APIServerAdapter(PlatformConfig(enabled=True)) + monkeypatch.setattr(adapter, "_ensure_session_db", lambda: None) + + agent = adapter._create_agent(session_id="api-session") + + assert isinstance(agent, FakeAgent) + assert captured["max_iterations"] == 200 + # --------------------------------------------------------------------------- # Auth checking diff --git a/tests/gateway/test_runtime_env_reload_config_authority.py b/tests/gateway/test_runtime_env_reload_config_authority.py index 92d54b8863c..d90b58297e8 100644 --- a/tests/gateway/test_runtime_env_reload_config_authority.py +++ b/tests/gateway/test_runtime_env_reload_config_authority.py @@ -51,3 +51,18 @@ def test_reload_runtime_env_keeps_env_max_iterations_when_config_omits_key( gateway_run._reload_runtime_env_preserving_config_authority() assert os.environ["HERMES_MAX_ITERATIONS"] == "123" + + +def test_current_max_iterations_reloads_before_reading(monkeypatch) -> None: + monkeypatch.setenv("HERMES_MAX_ITERATIONS", "90") + + def _fake_reload() -> None: + os.environ["HERMES_MAX_ITERATIONS"] = "200" + + monkeypatch.setattr( + gateway_run, + "_reload_runtime_env_preserving_config_authority", + _fake_reload, + ) + + assert gateway_run._current_max_iterations() == 200 From ca92e9a362503bcb7013233f6b0b5c5e9c23c92b Mon Sep 17 00:00:00 2001 From: infinitycrew39 Date: Fri, 19 Jun 2026 10:50:30 +0700 Subject: [PATCH 40/90] fix(gateway): refresh cached agent max_iterations from current config When a gateway agent is reused from cache, it retains the max_iterations from its initial creation. If config.yaml agent.max_turns or HERMES_MAX_ITERATIONS changed between turns, the cached agent's budget becomes stale. Before reusing a cached agent, refresh agent.max_iterations from the freshly-resolved value (read from env/config at line 14585). Fixes partial issue from PR #48127: handles fresh agent creation + cached agent reuse. --- gateway/run.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gateway/run.py b/gateway/run.py index 59dd890f8c9..741f2a235ad 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -14802,6 +14802,9 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew except KeyError: pass self._init_cached_agent_for_turn(agent, _interrupt_depth) + # Refresh agent max_iterations from current config + # (cached agent may have been created with old config) + agent.max_iterations = max_iterations logger.debug("Reusing cached agent for session %s", session_key) if agent is None: From 144834b2f752262e2017ce5f4090b18c5922f795 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Thu, 18 Jun 2026 21:44:56 -0700 Subject: [PATCH 41/90] test(gateway): real cached-agent max_iterations regression test Replaces the tautological test from the original PR (which asserted a plain assignment it performed itself in the test body) with one that exercises the actual contracts: _init_cached_agent_for_turn leaves max_iterations untouched, and the per-turn IterationBudget rebuild (turn_context.py) propagates a refreshed cap. --- .../test_cached_agent_max_iterations.py | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 tests/gateway/test_cached_agent_max_iterations.py diff --git a/tests/gateway/test_cached_agent_max_iterations.py b/tests/gateway/test_cached_agent_max_iterations.py new file mode 100644 index 00000000000..fcd523c70ef --- /dev/null +++ b/tests/gateway/test_cached_agent_max_iterations.py @@ -0,0 +1,92 @@ +"""Regression tests for PR #48127: cached agent max_iterations refresh. + +When a long-lived gateway reuses an agent from its cache, the agent must run +the *current* configured iteration budget — not the budget it was constructed +with on the first turn of that session. Two pieces make that true: + +1. ``GatewayRunner._init_cached_agent_for_turn`` must NOT reset + ``max_iterations`` itself (the gateway refreshes it explicitly right after, + from current config). If this helper ever started clobbering it, the + gateway's refresh would be silently undone. +2. The per-turn budget object is rebuilt from ``agent.max_iterations`` at the + start of every turn (``agent/turn_context.py`` -> ``IterationBudget``), so + refreshing ``max_iterations`` on the cached agent is sufficient to change + the operative cap the agent loop checks. + +These tests exercise the real code paths rather than asserting a plain +assignment, so they fail if either contract regresses. +""" + +import time +from types import SimpleNamespace + +from agent.iteration_budget import IterationBudget + + +def _make_cached_agent(max_iterations: int) -> SimpleNamespace: + """A minimal stand-in cached agent with the attributes the helpers touch.""" + # The turn loop checks both api_call_count >= max_iterations AND + # iteration_budget.remaining <= 0 (turn_finalizer.py), so the budget must + # also reflect the new cap. Seed it with the stale value to prove the + # refresh propagates. + return SimpleNamespace( + _last_activity_ts=time.time() - 1000, + _last_activity_desc="previous turn", + _api_call_count=42, + _last_flushed_db_idx=5, + max_iterations=max_iterations, + iteration_budget=IterationBudget(max_iterations), + ) + + +def test_init_cached_agent_for_turn_does_not_touch_max_iterations(): + """The per-turn reset helper must leave max_iterations untouched. + + The gateway refreshes max_iterations explicitly right after calling this + helper; if the helper ever reset it, that refresh would be undone. + """ + from gateway.run import GatewayRunner + + agent = _make_cached_agent(90) + GatewayRunner._init_cached_agent_for_turn(agent, interrupt_depth=0) + + # Per-turn state was reset... + assert agent._api_call_count == 0 + assert agent._last_activity_desc == "starting new turn (cached)" + assert agent._last_flushed_db_idx == 0 + # ...but the iteration budget was NOT changed by the helper itself. + assert agent.max_iterations == 90 + + +def test_init_cached_agent_preserves_max_iterations_on_interrupt_depth(): + """Interrupt-recursive turns must also leave max_iterations alone.""" + from gateway.run import GatewayRunner + + agent = _make_cached_agent(200) + GatewayRunner._init_cached_agent_for_turn(agent, interrupt_depth=1) + + # Activity timestamps preserved for the inactivity watchdog (#15654)... + assert agent._last_activity_desc == "previous turn" + # ...and max_iterations untouched. + assert agent.max_iterations == 200 + + +def test_refreshed_max_iterations_propagates_to_turn_budget(): + """Refreshing max_iterations on a cached agent changes the operative cap. + + The gateway sets ``agent.max_iterations = max_iterations`` on cache reuse; + the new turn's setup then rebuilds ``iteration_budget`` from it. This proves + the refresh actually moves the budget the agent loop enforces — the cached + agent started at 90 and ends a new turn capped at 200. + """ + agent = _make_cached_agent(90) + assert agent.iteration_budget.max_total == 90 + + # Gateway refresh on cache reuse: + agent.max_iterations = 200 + + # Start-of-turn budget rebuild (agent/turn_context.py:166): + agent.iteration_budget = IterationBudget(agent.max_iterations) + + assert agent.iteration_budget.max_total == 200 + assert agent.iteration_budget.remaining == 200 From fd92a3a5c9da0079cea0731bf3adf7bb288caa1e Mon Sep 17 00:00:00 2001 From: Charles Power Date: Sun, 7 Jun 2026 21:39:14 -0700 Subject: [PATCH 42/90] fix(gateway): Windows restart no longer causes a silent outage `hermes gateway restart` on Windows could take the gateway offline with no replacement. restart() was stop() -> sleep(1.0) -> start(), but the graceful drain can run up to ~180s while the detached pythonw process stays alive. The 1s sleep let start() run against the still-draining old process; its "already running" guard then no-opped, and when the old process finally exited nothing relaunched it. Two root causes, both fixed: 1. Loose PID detection. `_scan_gateway_pids` and the gateway.status helpers used substring matches ("... gateway" in cmdline) for lifecycle decisions, so they false-matched `gateway status`/`dashboard` siblings and unrelated processes like `python -m tui_gateway`, plus stale gateway.pid records. Add a shared strict matcher `looks_like_gateway_command_line()` in gateway/status.py that requires the real `gateway run` subcommand (or the dedicated entrypoints), and route `_looks_like_gateway_process`, `_record_looks_like_gateway`, and `_scan_gateway_pids` through it. 2. restart() race. Wait until the gateway is authoritatively gone (`get_running_pid()` + strict `_gateway_pids()`) before relaunch; force-kill once if it lingers and raise rather than start a duplicate; verify the relaunch produced a running gateway and raise loudly if not (no more exit-0 silent outage). Scoped to Windows; systemd/launchd restart paths are already drain-aware. Adds tests/gateway/test_gateway_command_line_matcher.py. Co-Authored-By: Claude Opus 4.8 (1M context) --- gateway/status.py | 63 +++++++++++++------ hermes_cli/gateway.py | 32 +++------- hermes_cli/gateway_windows.py | 46 +++++++++++++- .../test_gateway_command_line_matcher.py | 48 ++++++++++++++ 4 files changed, 147 insertions(+), 42 deletions(-) create mode 100644 tests/gateway/test_gateway_command_line_matcher.py diff --git a/gateway/status.py b/gateway/status.py index 367ac33c4d7..5e5584a1ed8 100644 --- a/gateway/status.py +++ b/gateway/status.py @@ -14,6 +14,7 @@ concurrently under distinct configurations). import hashlib import json import os +import re import signal import subprocess import sys @@ -164,20 +165,53 @@ def _read_process_cmdline(pid: int) -> Optional[str]: return None +def looks_like_gateway_command_line(command: str | None) -> bool: + """Return True only for a real ``gateway run`` process command line. + + Lifecycle decisions (is the gateway up? did restart relaunch it?) must not + fire on loose substring matches. The previous ``"... gateway" in cmdline`` + test also matched ``hermes_cli.main gateway status`` and even unrelated + processes like ``python -m tui_gateway`` -- which made ``restart()`` race + against a still-draining old process and ``status``/``start`` report false + positives. This requires the actual ``gateway`` subcommand to be followed + by ``run`` (or the gateway-dedicated entrypoints), excluding the other + ``gateway`` management subcommands and any process that merely contains the + word "gateway". + """ + if not command: + return False + normalized = command.replace("\\", "/").lower() + + # Gateway-dedicated entrypoints carry no subcommand to inspect. + if re.search(r"(^|[/\s])gateway/run\.py(\s|$)", normalized): + return True + if re.search(r"(^|[/\s])hermes-gateway(?:\.exe)?(\s|$)", normalized): + return True + + has_gateway_entry = ( + "hermes_cli.main" in normalized + or "hermes_cli/main.py" in normalized + or re.search(r"(^|[/\s])hermes(?:\.exe)?(\s|$)", normalized) is not None + ) + if not has_gateway_entry: + return False + + tokens = [t.strip("\"'").replace("\\", "/").lower() for t in command.split()] + for i, token in enumerate(tokens): + if token != "gateway": + continue + if i + 1 >= len(tokens): + return True # bare `hermes gateway` defaults to `run` + return tokens[i + 1] == "run" + return False + + def _looks_like_gateway_process(pid: int) -> bool: """Return True when the live PID still looks like the Hermes gateway.""" cmdline = _read_process_cmdline(pid) if not cmdline: return False - - patterns = ( - "hermes_cli.main gateway", - "hermes_cli/main.py gateway", - "hermes gateway", - "hermes-gateway", - "gateway/run.py", - ) - return any(pattern in cmdline for pattern in patterns) + return looks_like_gateway_command_line(cmdline) def _record_looks_like_gateway(record: dict[str, Any]) -> bool: @@ -189,15 +223,8 @@ def _record_looks_like_gateway(record: dict[str, Any]) -> bool: if not isinstance(argv, list) or not argv: return False - # Normalize Windows backslashes so patterns match cross-platform. - cmdline = " ".join(str(part) for part in argv).replace("\\", "/") - patterns = ( - "hermes_cli.main gateway", - "hermes_cli/main.py gateway", - "hermes gateway", - "gateway/run.py", - ) - return any(pattern in cmdline for pattern in patterns) + cmdline = " ".join(str(part) for part in argv) + return looks_like_gateway_command_line(cmdline) def _build_pid_record() -> dict: diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index 7e5406a11dd..06f9c49b916 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -319,23 +319,12 @@ def _scan_gateway_pids(exclude_pids: set[int], all_profiles: bool = False) -> li # gateway. See #13242. exclude_pids = exclude_pids | _get_ancestor_pids() pids: list[int] = [] - patterns = [ - "hermes_cli.main gateway", - "hermes_cli.main --profile", - "hermes_cli.main -p", - "hermes_cli/main.py gateway", - "hermes_cli/main.py --profile", - "hermes_cli/main.py -p", - "hermes gateway", - # Windows: only match invocations that actually carry the ``gateway`` - # subcommand or the gateway-dedicated console-script shim. Bare - # ``hermes.exe --profile`` / ``hermes.exe -p`` would also match - # ``hermes.exe --profile foo dashboard`` and other CLI subcommands, - # producing false-positive gateway PIDs (Copilot review). - "hermes.exe gateway", - "hermes-gateway.exe", - "gateway/run.py", - ] + # Strict command-line matcher shared with gateway.status: requires the + # actual ``gateway run`` subcommand (or the dedicated entrypoints), so this + # scan no longer false-matches ``gateway status``/``dashboard`` siblings or + # unrelated processes like ``python -m tui_gateway``. Lazy import mirrors the + # circular-import avoidance used elsewhere in this module. + from gateway.status import looks_like_gateway_command_line current_home = str(get_hermes_home().resolve()) current_home_lc = current_home.lower() current_profile_arg = _profile_arg(current_home) @@ -430,8 +419,7 @@ def _scan_gateway_pids(exclude_pids: set[int], all_profiles: bool = False) -> li current_cmd = line[len("CommandLine=") :] elif line.startswith("ProcessId="): pid_str = line[len("ProcessId=") :] - current_cmd_lc = current_cmd.lower() - if any(p in current_cmd_lc for p in patterns) and ( + if looks_like_gateway_command_line(current_cmd) and ( all_profiles or _matches_current_profile(current_cmd) ): try: @@ -456,8 +444,7 @@ def _scan_gateway_pids(exclude_pids: set[int], all_profiles: bool = False) -> li with open(f"/proc/{pid}/cmdline", "rb") as _f: cmdline = _f.read().decode("utf-8", errors="replace") cmdline = cmdline.replace("\x00", " ") - cmdline_lc = cmdline.lower() - if any(p in cmdline_lc for p in patterns) and ( + if looks_like_gateway_command_line(cmdline) and ( all_profiles or _matches_current_profile(cmdline) ): _append_unique_pid(pids, pid, exclude_pids) @@ -500,8 +487,7 @@ def _scan_gateway_pids(exclude_pids: set[int], all_profiles: bool = False) -> li if pid is None: continue - command_lc = command.lower() - if any(pattern in command_lc for pattern in patterns) and ( + if looks_like_gateway_command_line(command) and ( all_profiles or _matches_current_profile(command) ): _append_unique_pid(pids, pid, exclude_pids) diff --git a/hermes_cli/gateway_windows.py b/hermes_cli/gateway_windows.py index 08c7d8c019c..466031bfaa7 100644 --- a/hermes_cli/gateway_windows.py +++ b/hermes_cli/gateway_windows.py @@ -1302,10 +1302,54 @@ def stop() -> None: print("✗ No gateway was running") +def _wait_for_gateway_absent(timeout_s: float = 30.0, interval_s: float = 0.5) -> bool: + """Block until no gateway process is detectable, or the timeout elapses. + + ``stop()`` can return while the previous gateway is still draining + in-flight agents (the drain runs up to the restart-drain timeout). Uses the + authoritative ``get_running_pid()`` (lock + liveness + start-time + + gateway-shape) plus the now-strict ``_gateway_pids()`` scan so a relaunch + never races a still-alive old process. + """ + from gateway.status import get_running_pid + + deadline = time.monotonic() + max(timeout_s, interval_s) + while time.monotonic() < deadline: + if get_running_pid() is None and not _gateway_pids(): + return True + time.sleep(interval_s) + return get_running_pid() is None and not _gateway_pids() + + def restart() -> None: - """Stop the gateway then start it again.""" + """Stop the gateway then start it again. + + Waits for the old gateway to be authoritatively gone before relaunching -- + otherwise ``start()``'s "already running" guard sees the still-draining old + process and no-ops, and when that process later exits nothing replaces it (a + silent outage). Fails loudly if the process can't be cleared or the relaunch + doesn't produce a running gateway. + """ _assert_windows() + from hermes_cli.gateway import kill_gateway_processes + stop() + + if not _wait_for_gateway_absent(timeout_s=30.0): + print("⚠ Gateway still present after stop; forcing termination before restart...") + kill_gateway_processes(all_profiles=False, force=True) + if not _wait_for_gateway_absent(timeout_s=10.0): + raise RuntimeError( + "Gateway process still detected after force kill; refusing to " + "start a duplicate. Investigate stray PIDs before retrying." + ) + # Give Windows a moment to release the listening port. time.sleep(1.0) start() + + if not _wait_for_gateway_ready(timeout_s=15.0): + raise RuntimeError( + "Gateway restart did not produce a running gateway process. " + "Check logs/gateway.log and run `hermes gateway status`." + ) diff --git a/tests/gateway/test_gateway_command_line_matcher.py b/tests/gateway/test_gateway_command_line_matcher.py new file mode 100644 index 00000000000..5b8b16a7d54 --- /dev/null +++ b/tests/gateway/test_gateway_command_line_matcher.py @@ -0,0 +1,48 @@ +"""Tests for the strict gateway command-line matcher. + +Regression guard for the Windows ``hermes gateway restart`` silent-outage bug: +the previous loose substring match (``"... gateway" in cmdline``) false-matched +``gateway status``/``dashboard`` siblings and unrelated processes such as +``python -m tui_gateway``, which let ``restart()`` race a still-draining old +process and ``status``/``start`` report false positives. +""" + +from __future__ import annotations + +import pytest + +from gateway.status import looks_like_gateway_command_line as matches + + +ACCEPT = [ + "pythonw.exe -m hermes_cli.main gateway run", + r"C:\Users\me\hermes\venv\Scripts\pythonw.exe -m hermes_cli.main gateway run", + "python -m hermes_cli.main --profile work gateway run", + "python -m hermes_cli.main gateway run --replace", + "python -m hermes_cli/main.py gateway run", + "python gateway/run.py", + "hermes-gateway.exe", + "hermes gateway", # bare `hermes gateway` defaults to run + "hermes gateway run", +] + +REJECT = [ + "python -m tui_gateway", # unrelated module + "python -m hermes_cli.main gateway status", # other subcommand + "python -m hermes_cli.main gateway restart", + "python -m hermes_cli.main gateway stop", + "python -m hermes_cli.main --profile x dashboard", # non-gateway subcommand + "some random python -m mygateway thing", + "", + None, +] + + +@pytest.mark.parametrize("cmd", ACCEPT) +def test_accepts_real_gateway_run(cmd): + assert matches(cmd) is True + + +@pytest.mark.parametrize("cmd", REJECT) +def test_rejects_non_gateway_run(cmd): + assert matches(cmd) is False From b12c0cd9970ba7631d094f20c28f6189d4b065b9 Mon Sep 17 00:00:00 2001 From: Charles Power Date: Sun, 7 Jun 2026 21:44:46 -0700 Subject: [PATCH 43/90] test(windows): run pytest-timeout in thread mode on Windows The pyproject addopts pin `--timeout-method=signal` relies on signal.SIGALRM, which doesn't exist on Windows. pytest-timeout raised AttributeError at timer setup and aborted the entire run before any test executed, so the suite was unrunnable on Windows by default. Override timeout_method to "thread" on Windows in pytest_configure; POSIX keeps the more reliable signal method. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/conftest.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 2da7d4a1eb4..468926b0f51 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -534,6 +534,14 @@ def pytest_configure(config): # noqa: D401 — pytest hook "behaviour — e.g. PTY tests that signal their own child).", ) + # The pyproject addopts pin ``--timeout-method=signal`` relies on + # ``signal.SIGALRM``, which does not exist on Windows — pytest-timeout + # raises AttributeError at timer setup and the whole run aborts before any + # test executes. Fall back to the thread-based timer on Windows so the + # suite runs natively there (POSIX keeps the more reliable signal method). + if sys.platform == "win32" and getattr(config.option, "timeout_method", None) == "signal": + config.option.timeout_method = "thread" + @pytest.fixture(autouse=True) def _live_system_guard(request, monkeypatch): From 715fa9ea1c8f1e1b49b698ec32a1ba822e5a7ce3 Mon Sep 17 00:00:00 2001 From: Charles Power Date: Sun, 7 Jun 2026 21:57:20 -0700 Subject: [PATCH 44/90] fix(gateway): harden gateway command-line matcher (review findings) Address correctness gaps found in pre-PR review of the strict matcher: - Profile selectors can appear on EITHER side of the `gateway` token (`_apply_profile_override` strips `--profile`/`-p` from anywhere in argv before argparse), so `hermes gateway --profile work run` and `python -m hermes_cli.main gateway -p work run` are valid launches the previous matcher wrongly rejected. Strip `--profile`/`-p`/`--profile=`/`-p=` from anywhere before locating the subcommand. - A profile literally named `gateway` (`hermes -p gateway gateway run`) made the old token scan stop on the profile value; stripping the selector+value first fixes it. - Tokenize quote-aware with `shlex` so quoted Windows paths containing spaces (`"C:\Program Files\Hermes\hermes-gateway.exe"`) are no longer split mid-path and the dedicated-entrypoint match survives. Without these, the matcher could MISS a real running gateway -> the opposite failure (restart/status reporting "down" when up). Adds regression tests for all three shapes. Co-Authored-By: Claude Opus 4.8 (1M context) --- gateway/status.py | 63 ++++++++++++++----- .../test_gateway_command_line_matcher.py | 12 ++++ 2 files changed, 60 insertions(+), 15 deletions(-) diff --git a/gateway/status.py b/gateway/status.py index 5e5584a1ed8..2b4bd08ba39 100644 --- a/gateway/status.py +++ b/gateway/status.py @@ -14,7 +14,7 @@ concurrently under distinct configurations). import hashlib import json import os -import re +import shlex import signal import subprocess import sys @@ -173,36 +173,69 @@ def looks_like_gateway_command_line(command: str | None) -> bool: test also matched ``hermes_cli.main gateway status`` and even unrelated processes like ``python -m tui_gateway`` -- which made ``restart()`` race against a still-draining old process and ``status``/``start`` report false - positives. This requires the actual ``gateway`` subcommand to be followed - by ``run`` (or the gateway-dedicated entrypoints), excluding the other + positives. This requires the actual ``gateway`` subcommand followed by + ``run`` (or one of the gateway-dedicated entrypoints), excluding the other ``gateway`` management subcommands and any process that merely contains the word "gateway". + + Tokenizes quote-aware (``shlex``) so quoted Windows paths with spaces + (``"C:\\Program Files\\...\\hermes-gateway.exe"``) survive, and strips + ``--profile``/``-p`` selectors from anywhere in argv -- Hermes's + ``_apply_profile_override`` removes them before argparse, so the profile + flag (and a profile literally named ``gateway``) can legally appear on + either side of the ``gateway`` subcommand. """ if not command: return False - normalized = command.replace("\\", "/").lower() + + try: + raw_tokens = shlex.split(command, posix=False) + except ValueError: + raw_tokens = command.split() + # Strip surrounding quotes, normalize slashes + case per token. + tokens = [t.strip("\"'").replace("\\", "/").lower() for t in raw_tokens] + if not tokens: + return False # Gateway-dedicated entrypoints carry no subcommand to inspect. - if re.search(r"(^|[/\s])gateway/run\.py(\s|$)", normalized): - return True - if re.search(r"(^|[/\s])hermes-gateway(?:\.exe)?(\s|$)", normalized): - return True + for token in tokens: + if token == "gateway/run.py" or token.endswith("/gateway/run.py"): + return True + basename = token.rsplit("/", 1)[-1] + if basename in ("hermes-gateway", "hermes-gateway.exe"): + return True + joined = " ".join(tokens) has_gateway_entry = ( - "hermes_cli.main" in normalized - or "hermes_cli/main.py" in normalized - or re.search(r"(^|[/\s])hermes(?:\.exe)?(\s|$)", normalized) is not None + "hermes_cli.main" in joined + or "hermes_cli/main.py" in joined + or any(t.rsplit("/", 1)[-1] in ("hermes", "hermes.exe") for t in tokens) ) if not has_gateway_entry: return False - tokens = [t.strip("\"'").replace("\\", "/").lower() for t in command.split()] - for i, token in enumerate(tokens): + # Drop profile selectors anywhere: --profile X / -p X / --profile=X / -p=X. + # This consumes a profile VALUE of "gateway" too, so the real subcommand + # token is the one we land on below. + filtered: list[str] = [] + skip_next = False + for token in tokens: + if skip_next: + skip_next = False + continue + if token in ("--profile", "-p"): + skip_next = True + continue + if token.startswith("--profile=") or token.startswith("-p="): + continue + filtered.append(token) + + for i, token in enumerate(filtered): if token != "gateway": continue - if i + 1 >= len(tokens): + if i + 1 >= len(filtered): return True # bare `hermes gateway` defaults to `run` - return tokens[i + 1] == "run" + return filtered[i + 1] == "run" return False diff --git a/tests/gateway/test_gateway_command_line_matcher.py b/tests/gateway/test_gateway_command_line_matcher.py index 5b8b16a7d54..bc8113b91a0 100644 --- a/tests/gateway/test_gateway_command_line_matcher.py +++ b/tests/gateway/test_gateway_command_line_matcher.py @@ -24,6 +24,18 @@ ACCEPT = [ "hermes-gateway.exe", "hermes gateway", # bare `hermes gateway` defaults to run "hermes gateway run", + # profile selector AFTER the `gateway` token (argv is profile-position + # agnostic — _apply_profile_override strips --profile/-p anywhere) + "hermes gateway --profile work run", + "python -m hermes_cli.main gateway -p work run", + "hermes gateway --profile=work run", + # a profile literally NAMED "gateway" + "hermes -p gateway gateway run", + "python -m hermes_cli.main --profile gateway gateway run", + # quoted Windows paths with spaces (shlex-aware tokenization) + r'"C:\Program Files\Hermes\hermes-gateway.exe"', + r'"C:\Program Files\Hermes\gateway\run.py" run', + r'"C:\Program Files\Py\pythonw.exe" -m hermes_cli.main gateway run', ] REJECT = [ From b922d7dfb24f4405148dbdef4f7deea173a53b49 Mon Sep 17 00:00:00 2001 From: teknium <127238744+teknium1@users.noreply.github.com> Date: Thu, 18 Jun 2026 21:38:02 -0700 Subject: [PATCH 45/90] chore(release): add salesondemandio to AUTHOR_MAP for PR #42664 --- scripts/release.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/release.py b/scripts/release.py index 20c6a6bfa0a..0ff464e61f0 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -45,6 +45,7 @@ ACP_REGISTRY_MANIFEST = REPO_ROOT / "acp_registry" / "agent.json" # Auto-extracted from noreply emails + manual overrides AUTHOR_MAP = { + "charles@salesondemand.io": "salesondemandio", "victor@rocketfueldev.com": "victor-kyriazakos", "87440198+JoaoMarcos44@users.noreply.github.com": "JoaoMarcos44", "286497132+srojk34@users.noreply.github.com": "srojk34", From 92451151c6429e1d2774c5e7f43269ebcf8c64aa Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Fri, 19 Jun 2026 06:38:28 -0700 Subject: [PATCH 46/90] Revert "feat(skills): add html-artifact skill, fold in sketch + architecture-diagram + concept-diagrams (#48899)" This reverts commit 9362ce2575e00f5a795285b74e79d54c02e1326c. --- .../creative/concept-diagrams/SKILL.md | 362 +++++++++++++++++ .../apartment-floor-plan-conversion.md | 244 +++++++++++ .../examples/automated-password-reset-flow.md | 276 +++++++++++++ .../autonomous-llm-research-agent-flow.md | 240 +++++++++++ .../banana-journey-tree-to-smoothie.md | 161 ++++++++ .../examples/commercial-aircraft-structure.md | 209 ++++++++++ .../examples/cpu-ooo-microarchitecture.md | 236 +++++++++++ .../examples/electricity-grid-flow.md | 182 +++++++++ .../feature-film-production-pipeline.md | 172 ++++++++ .../hospital-emergency-department-flow.md | 165 ++++++++ .../ml-benchmark-grouped-bar-chart.md | 114 ++++++ .../examples/place-order-uml-sequence.md | 325 +++++++++++++++ .../examples/smart-city-infrastructure.md | 173 ++++++++ .../examples/smartphone-layer-anatomy.md | 154 +++++++ .../examples/sn2-reaction-mechanism.md | 247 ++++++++++++ .../examples/wind-turbine-structure.md | 338 ++++++++++++++++ .../references/dashboard-patterns.md | 43 ++ .../references/infrastructure-patterns.md | 144 +++++++ .../references/physical-shape-cookbook.md | 42 ++ .../concept-diagrams/templates/template.html | 174 ++++++++ .../kanban-video-orchestrator/SKILL.md | 2 +- .../references/intake.md | 3 +- .../references/role-archetypes.md | 5 +- .../references/tool-matrix.md | 4 +- skills/creative/architecture-diagram/SKILL.md | 148 +++++++ .../templates/template.html | 319 +++++++++++++++ skills/creative/claude-design/SKILL.md | 12 +- skills/creative/design-md/SKILL.md | 2 +- skills/creative/html-artifact/SKILL.md | 184 --------- .../html-artifact/references/.gitignore | 3 - .../references/concept-archetypes.md | 94 ----- .../html-artifact/references/dark-tech.md | 92 ----- .../html-artifact/references/examples.md | 64 --- .../references/fidelity-and-verify.md | 78 ---- .../html-artifact/references/house-style.md | 179 --------- .../html-artifact/references/svg-diagrams.md | 123 ------ .../references/throwaway-editors.md | 114 ------ .../html-artifact/scripts/fetch-examples.sh | 43 -- .../html-artifact/templates/base.html | 104 ----- .../html-artifact/templates/diagram.html | 127 ------ .../html-artifact/templates/editor.html | 120 ------ skills/creative/pretext/SKILL.md | 2 +- skills/creative/sketch/SKILL.md | 218 ++++++++++ skills/software-development/spike/SKILL.md | 2 +- .../docs/reference/optional-skills-catalog.md | 1 + website/docs/reference/skills-catalog.md | 3 +- .../autonomous-ai-agents-hermes-agent.md | 4 +- .../creative/creative-architecture-diagram.md | 165 ++++++++ .../creative/creative-claude-design.md | 12 +- .../bundled/creative/creative-design-md.md | 2 +- .../creative/creative-html-artifact.md | 202 ---------- .../bundled/creative/creative-pretext.md | 2 +- .../bundled/creative/creative-sketch.md | 238 +++++++++++ .../creative/creative-touchdesigner-mcp.md | 2 +- .../skills/bundled/email/email-himalaya.md | 5 - .../bundled/github/github-github-auth.md | 4 +- .../github/github-github-code-review.md | 4 +- .../bundled/github/github-github-issues.md | 4 +- .../github/github-github-pr-workflow.md | 4 +- .../github/github-github-repo-management.md | 4 +- .../skills/bundled/media/media-gif-search.md | 2 +- .../note-taking/note-taking-obsidian.md | 2 +- .../productivity/productivity-airtable.md | 4 +- .../productivity/productivity-notion.md | 4 +- .../productivity-teams-meeting-pipeline.md | 2 +- .../bundled/research/research-llm-wiki.md | 2 +- .../research-research-paper-writing.md | 2 +- ...tware-development-node-inspect-debugger.md | 2 +- .../software-development-python-debugpy.md | 2 +- .../software-development-spike.md | 2 +- .../autonomous-ai-agents-honcho.md | 4 +- .../blockchain/blockchain-hyperliquid.md | 4 +- .../creative/creative-concept-diagrams.md | 379 ++++++++++++++++++ .../creative-kanban-video-orchestrator.md | 4 +- .../optional/devops/devops-pinggy-tunnel.md | 2 +- .../skills/optional/devops/devops-watchers.md | 2 +- .../skills/optional/mcp/mcp-fastmcp.md | 2 +- .../payments/payments-stripe-projects.md | 2 +- .../productivity/productivity-canvas.md | 2 +- .../productivity/productivity-shopify.md | 2 +- .../productivity/productivity-siyuan.md | 2 +- .../productivity/productivity-telephony.md | 8 +- .../research/research-gitnexus-explorer.md | 2 +- .../skills/optional/research/research-qmd.md | 2 +- .../optional/security/security-1password.md | 2 +- .../optional/security/security-godmode.md | 2 +- ...software-development-rest-graphql-debug.md | 2 +- .../reference/optional-skills-catalog.md | 1 + .../current/reference/skills-catalog.md | 2 + .../creative/creative-architecture-diagram.md | 165 ++++++++ .../creative/creative-claude-design.md | 2 +- .../bundled/creative/creative-design-md.md | 2 +- .../bundled/creative/creative-pretext.md | 2 +- .../bundled/creative/creative-sketch.md | 238 +++++++++++ .../software-development-spike.md | 2 +- .../creative/creative-concept-diagrams.md | 379 ++++++++++++++++++ .../creative-kanban-video-orchestrator.md | 2 +- website/sidebars.ts | 5 +- 98 files changed, 6336 insertions(+), 1610 deletions(-) create mode 100644 optional-skills/creative/concept-diagrams/SKILL.md create mode 100644 optional-skills/creative/concept-diagrams/examples/apartment-floor-plan-conversion.md create mode 100644 optional-skills/creative/concept-diagrams/examples/automated-password-reset-flow.md create mode 100644 optional-skills/creative/concept-diagrams/examples/autonomous-llm-research-agent-flow.md create mode 100644 optional-skills/creative/concept-diagrams/examples/banana-journey-tree-to-smoothie.md create mode 100644 optional-skills/creative/concept-diagrams/examples/commercial-aircraft-structure.md create mode 100644 optional-skills/creative/concept-diagrams/examples/cpu-ooo-microarchitecture.md create mode 100644 optional-skills/creative/concept-diagrams/examples/electricity-grid-flow.md create mode 100644 optional-skills/creative/concept-diagrams/examples/feature-film-production-pipeline.md create mode 100644 optional-skills/creative/concept-diagrams/examples/hospital-emergency-department-flow.md create mode 100644 optional-skills/creative/concept-diagrams/examples/ml-benchmark-grouped-bar-chart.md create mode 100644 optional-skills/creative/concept-diagrams/examples/place-order-uml-sequence.md create mode 100644 optional-skills/creative/concept-diagrams/examples/smart-city-infrastructure.md create mode 100644 optional-skills/creative/concept-diagrams/examples/smartphone-layer-anatomy.md create mode 100644 optional-skills/creative/concept-diagrams/examples/sn2-reaction-mechanism.md create mode 100644 optional-skills/creative/concept-diagrams/examples/wind-turbine-structure.md create mode 100644 optional-skills/creative/concept-diagrams/references/dashboard-patterns.md create mode 100644 optional-skills/creative/concept-diagrams/references/infrastructure-patterns.md create mode 100644 optional-skills/creative/concept-diagrams/references/physical-shape-cookbook.md create mode 100644 optional-skills/creative/concept-diagrams/templates/template.html create mode 100644 skills/creative/architecture-diagram/SKILL.md create mode 100644 skills/creative/architecture-diagram/templates/template.html delete mode 100644 skills/creative/html-artifact/SKILL.md delete mode 100644 skills/creative/html-artifact/references/.gitignore delete mode 100644 skills/creative/html-artifact/references/concept-archetypes.md delete mode 100644 skills/creative/html-artifact/references/dark-tech.md delete mode 100644 skills/creative/html-artifact/references/examples.md delete mode 100644 skills/creative/html-artifact/references/fidelity-and-verify.md delete mode 100644 skills/creative/html-artifact/references/house-style.md delete mode 100644 skills/creative/html-artifact/references/svg-diagrams.md delete mode 100644 skills/creative/html-artifact/references/throwaway-editors.md delete mode 100755 skills/creative/html-artifact/scripts/fetch-examples.sh delete mode 100644 skills/creative/html-artifact/templates/base.html delete mode 100644 skills/creative/html-artifact/templates/diagram.html delete mode 100644 skills/creative/html-artifact/templates/editor.html create mode 100644 skills/creative/sketch/SKILL.md create mode 100644 website/docs/user-guide/skills/bundled/creative/creative-architecture-diagram.md delete mode 100644 website/docs/user-guide/skills/bundled/creative/creative-html-artifact.md create mode 100644 website/docs/user-guide/skills/bundled/creative/creative-sketch.md create mode 100644 website/docs/user-guide/skills/optional/creative/creative-concept-diagrams.md create mode 100644 website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-architecture-diagram.md create mode 100644 website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-sketch.md create mode 100644 website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/optional/creative/creative-concept-diagrams.md diff --git a/optional-skills/creative/concept-diagrams/SKILL.md b/optional-skills/creative/concept-diagrams/SKILL.md new file mode 100644 index 00000000000..6017d4fd121 --- /dev/null +++ b/optional-skills/creative/concept-diagrams/SKILL.md @@ -0,0 +1,362 @@ +--- +name: concept-diagrams +description: Generate flat, minimal light/dark-aware SVG diagrams as standalone HTML files, using a unified educational visual language with 9 semantic color ramps, sentence-case typography, and automatic dark mode. Best suited for educational and non-software visuals — physics setups, chemistry mechanisms, math curves, physical objects (aircraft, turbines, smartphones, mechanical watches), anatomy, floor plans, cross-sections, narrative journeys (lifecycle of X, process of Y), hub-spoke system integrations (smart city, IoT), and exploded layer views. If a more specialized skill exists for the subject (dedicated software/cloud architecture, hand-drawn sketches, animated explainers, etc.), prefer that — otherwise this skill can also serve as a general-purpose SVG diagram fallback with a clean educational look. Ships with 15 example diagrams. +version: 0.1.0 +author: v1k22 (original PR), ported into hermes-agent +license: MIT +dependencies: [] +platforms: [linux, macos, windows] +metadata: + hermes: + tags: [diagrams, svg, visualization, education, physics, chemistry, engineering] + related_skills: [architecture-diagram, excalidraw, generative-widgets] +--- + +# Concept Diagrams + +Generate production-quality SVG diagrams with a unified flat, minimal design system. Output is a single self-contained HTML file that renders identically in any modern browser, with automatic light/dark mode. + +## Scope + +**Best suited for:** +- Physics setups, chemistry mechanisms, math curves, biology +- Physical objects (aircraft, turbines, smartphones, mechanical watches, cells) +- Anatomy, cross-sections, exploded layer views +- Floor plans, architectural conversions +- Narrative journeys (lifecycle of X, process of Y) +- Hub-spoke system integrations (smart city, IoT networks, electricity grids) +- Educational / textbook-style visuals in any domain +- Quantitative charts (grouped bars, energy profiles) + +**Look elsewhere first for:** +- Dedicated software / cloud infrastructure architecture with a dark tech aesthetic (consider `architecture-diagram` if available) +- Hand-drawn whiteboard sketches (consider `excalidraw` if available) +- Animated explainers or video output (consider an animation skill) + +If a more specialized skill is available for the subject, prefer that. If none fits, this skill can serve as a general-purpose SVG diagram fallback — the output will carry the clean educational aesthetic described below, which is a reasonable default for almost any subject. + +## Workflow + +1. Decide on the diagram type (see Diagram Types below). +2. Lay out components using the Design System rules. +3. Write the full HTML page using `templates/template.html` as the wrapper — paste your SVG where the template says ``. +4. Save as a standalone `.html` file (for example `~/my-diagram.html` or `./my-diagram.html`). +5. User opens it directly in a browser — no server, no dependencies. + +Optional: if the user wants a browsable gallery of multiple diagrams, see "Local Preview Server" at the bottom. + +Load the HTML template: +``` +skill_view(name="concept-diagrams", file_path="templates/template.html") +``` + +The template embeds the full CSS design system (`c-*` color classes, text classes, light/dark variables, arrow marker styles). The SVG you generate relies on these classes being present on the hosting page. + +--- + +## Design System + +### Philosophy + +- **Flat**: no gradients, drop shadows, blur, glow, or neon effects. +- **Minimal**: show the essential. No decorative icons inside boxes. +- **Consistent**: same colors, spacing, typography, and stroke widths across every diagram. +- **Dark-mode ready**: all colors auto-adapt via CSS classes — no per-mode SVG. + +### Color Palette + +9 color ramps, each with 7 stops. Put the class name on a `` or shape element; the template CSS handles both modes. + +| Class | 50 (lightest) | 100 | 200 | 400 | 600 | 800 | 900 (darkest) | +|------------|---------------|---------|---------|---------|---------|---------|---------------| +| `c-purple` | #EEEDFE | #CECBF6 | #AFA9EC | #7F77DD | #534AB7 | #3C3489 | #26215C | +| `c-teal` | #E1F5EE | #9FE1CB | #5DCAA5 | #1D9E75 | #0F6E56 | #085041 | #04342C | +| `c-coral` | #FAECE7 | #F5C4B3 | #F0997B | #D85A30 | #993C1D | #712B13 | #4A1B0C | +| `c-pink` | #FBEAF0 | #F4C0D1 | #ED93B1 | #D4537E | #993556 | #72243E | #4B1528 | +| `c-gray` | #F1EFE8 | #D3D1C7 | #B4B2A9 | #888780 | #5F5E5A | #444441 | #2C2C2A | +| `c-blue` | #E6F1FB | #B5D4F4 | #85B7EB | #378ADD | #185FA5 | #0C447C | #042C53 | +| `c-green` | #EAF3DE | #C0DD97 | #97C459 | #639922 | #3B6D11 | #27500A | #173404 | +| `c-amber` | #FAEEDA | #FAC775 | #EF9F27 | #BA7517 | #854F0B | #633806 | #412402 | +| `c-red` | #FCEBEB | #F7C1C1 | #F09595 | #E24B4A | #A32D2D | #791F1F | #501313 | + +#### Color Assignment Rules + +Color encodes **meaning**, not sequence. Never cycle through colors like a rainbow. + +- Group nodes by **category** — all nodes of the same type share one color. +- Use `c-gray` for neutral/structural nodes (start, end, generic steps, users). +- Use **2-3 colors per diagram**, not 6+. +- Prefer `c-purple`, `c-teal`, `c-coral`, `c-pink` for general categories. +- Reserve `c-blue`, `c-green`, `c-amber`, `c-red` for semantic meaning (info, success, warning, error). + +Light/dark stop mapping (handled by the template CSS — just use the class): +- Light mode: 50 fill + 600 stroke + 800 title / 600 subtitle +- Dark mode: 800 fill + 200 stroke + 100 title / 200 subtitle + +### Typography + +Only two font sizes. No exceptions. + +| Class | Size | Weight | Use | +|-------|------|--------|-----| +| `th` | 14px | 500 | Node titles, region labels | +| `ts` | 12px | 400 | Subtitles, descriptions, arrow labels | +| `t` | 14px | 400 | General text | + +- **Sentence case always.** Never Title Case, never ALL CAPS. +- Every `` MUST carry a class (`t`, `ts`, or `th`). No unclassed text. +- `dominant-baseline="central"` on all text inside boxes. +- `text-anchor="middle"` for centered text in boxes. + +**Width estimation (approx):** +- 14px weight 500: ~8px per character +- 12px weight 400: ~6.5px per character +- Always verify: `box_width >= (char_count × px_per_char) + 48` (24px padding each side) + +### Spacing & Layout + +- **ViewBox**: `viewBox="0 0 680 H"` where H = content height + 40px buffer. +- **Safe area**: x=40 to x=640, y=40 to y=(H-40). +- **Between boxes**: 60px minimum gap. +- **Inside boxes**: 24px horizontal padding, 12px vertical padding. +- **Arrowhead gap**: 10px between arrowhead and box edge. +- **Single-line box**: 44px height. +- **Two-line box**: 56px height, 18px between title and subtitle baselines. +- **Container padding**: 20px minimum inside every container. +- **Max nesting**: 2-3 levels deep. Deeper gets unreadable at 680px width. + +### Stroke & Shape + +- **Stroke width**: 0.5px on all node borders. Not 1px, not 2px. +- **Rect rounding**: `rx="8"` for nodes, `rx="12"` for inner containers, `rx="16"` to `rx="20"` for outer containers. +- **Connector paths**: MUST have `fill="none"`. SVG defaults to `fill: black` otherwise. + +### Arrow Marker + +Include this `` block at the start of **every** SVG: + +```xml + + + + + +``` + +Use `marker-end="url(#arrow)"` on lines. The arrowhead inherits the line color via `context-stroke`. + +### CSS Classes (Provided by the Template) + +The template page provides: + +- Text: `.t`, `.ts`, `.th` +- Neutral: `.box`, `.arr`, `.leader`, `.node` +- Color ramps: `.c-purple`, `.c-teal`, `.c-coral`, `.c-pink`, `.c-gray`, `.c-blue`, `.c-green`, `.c-amber`, `.c-red` (all with automatic light/dark mode) + +You do **not** need to redefine these — just apply them in your SVG. The template file contains the full CSS definitions. + +--- + +## SVG Boilerplate + +Every SVG inside the template page starts with this exact structure: + +```xml + + + + + + + + + + +``` + +Replace `{HEIGHT}` with the actual computed height (last element bottom + 40px). + +### Node Patterns + +**Single-line node (44px):** +```xml + + + Service name + +``` + +**Two-line node (56px):** +```xml + + + Service name + Short description + +``` + +**Connector (no label):** +```xml + +``` + +**Container (dashed or solid):** +```xml + + + Container label + Subtitle info + +``` + +--- + +## Diagram Types + +Choose the layout that fits the subject: + +1. **Flowchart** — CI/CD pipelines, request lifecycles, approval workflows, data processing. Single-direction flow (top-down or left-right). Max 4-5 nodes per row. +2. **Structural / Containment** — Cloud infrastructure nesting, system architecture with layers. Large outer containers with inner regions. Dashed rects for logical groupings. +3. **API / Endpoint Map** — REST routes, GraphQL schemas. Tree from root, branching to resource groups, each containing endpoint nodes. +4. **Microservice Topology** — Service mesh, event-driven systems. Services as nodes, arrows for communication patterns, message queues between. +5. **Data Flow** — ETL pipelines, streaming architectures. Left-to-right flow from sources through processing to sinks. +6. **Physical / Structural** — Vehicles, buildings, hardware, anatomy. Use shapes that match the physical form — `` for curved bodies, `` for tapered shapes, ``/`` for cylindrical parts, nested `` for compartments. See `references/physical-shape-cookbook.md`. +7. **Infrastructure / Systems Integration** — Smart cities, IoT networks, multi-domain systems. Hub-spoke layout with central platform connecting subsystems. Semantic line styles (`.data-line`, `.power-line`, `.water-pipe`, `.road`). See `references/infrastructure-patterns.md`. +8. **UI / Dashboard Mockups** — Admin panels, monitoring dashboards. Screen frame with nested chart/gauge/indicator elements. See `references/dashboard-patterns.md`. + +For physical, infrastructure, and dashboard diagrams, load the matching reference file before generating — each one provides ready-made CSS classes and shape primitives. + +--- + +## Validation Checklist + +Before finalizing any SVG, verify ALL of the following: + +1. Every `` has class `t`, `ts`, or `th`. +2. Every `` inside a box has `dominant-baseline="central"`. +3. Every connector `` or `` used as arrow has `fill="none"`. +4. No arrow line crosses through an unrelated box. +5. `box_width >= (longest_label_chars × 8) + 48` for 14px text. +6. `box_width >= (longest_label_chars × 6.5) + 48` for 12px text. +7. ViewBox height = bottom-most element + 40px. +8. All content stays within x=40 to x=640. +9. Color classes (`c-*`) are on `` or shape elements, never on `` connectors. +10. Arrow `` block is present. +11. No gradients, shadows, blur, or glow effects. +12. Stroke width is 0.5px on all node borders. + +--- + +## Output & Preview + +### Default: standalone HTML file + +Write a single `.html` file the user can open directly. No server, no dependencies, works offline. Pattern: + +```python +# 1. Load the template +template = skill_view("concept-diagrams", "templates/template.html") + +# 2. Fill in title, subtitle, and paste your SVG +html = template.replace( + "", "SN2 reaction mechanism" +).replace( + "", "Bimolecular nucleophilic substitution" +).replace( + "", svg_content +) + +# 3. Write to a user-chosen path (or ./ by default) +write_file("./sn2-mechanism.html", html) +``` + +Tell the user how to open it: + +``` +# macOS +open ./sn2-mechanism.html +# Linux +xdg-open ./sn2-mechanism.html +``` + +### Optional: local preview server (multi-diagram gallery) + +Only use this when the user explicitly wants a browsable gallery of multiple diagrams. + +**Rules:** +- Bind to `127.0.0.1` only. Never `0.0.0.0`. Exposing diagrams on all network interfaces is a security hazard on shared networks. +- Pick a free port (do NOT hard-code one) and tell the user the chosen URL. +- The server is optional and opt-in — prefer the standalone HTML file first. + +Recommended pattern (lets the OS pick a free ephemeral port): + +```bash +# Put each diagram in its own folder under .diagrams/ +mkdir -p .diagrams/sn2-mechanism +# ...write .diagrams/sn2-mechanism/index.html... + +# Serve on loopback only, free port +cd .diagrams && python3 -c " +import http.server, socketserver +with socketserver.TCPServer(('127.0.0.1', 0), http.server.SimpleHTTPRequestHandler) as s: + print(f'Serving at http://127.0.0.1:{s.server_address[1]}/') + s.serve_forever() +" & +``` + +If the user insists on a fixed port, use `127.0.0.1:` — still never `0.0.0.0`. Document how to stop the server (`kill %1` or `pkill -f "http.server"`). + +--- + +## Examples Reference + +The `examples/` directory ships 15 complete, tested diagrams. Browse them for working patterns before writing a new diagram of a similar type: + +| File | Type | Demonstrates | +|------|------|--------------| +| `hospital-emergency-department-flow.md` | Flowchart | Priority routing with semantic colors | +| `feature-film-production-pipeline.md` | Flowchart | Phased workflow, horizontal sub-flows | +| `automated-password-reset-flow.md` | Flowchart | Auth flow with error branches | +| `autonomous-llm-research-agent-flow.md` | Flowchart | Loop-back arrows, decision branches | +| `place-order-uml-sequence.md` | Sequence | UML sequence diagram style | +| `commercial-aircraft-structure.md` | Physical | Paths, polygons, ellipses for realistic shapes | +| `wind-turbine-structure.md` | Physical cross-section | Underground/above-ground separation, color coding | +| `smartphone-layer-anatomy.md` | Exploded view | Alternating left/right labels, layered components | +| `apartment-floor-plan-conversion.md` | Floor plan | Walls, doors, proposed changes in dotted red | +| `banana-journey-tree-to-smoothie.md` | Narrative journey | Winding path, progressive state changes | +| `cpu-ooo-microarchitecture.md` | Hardware pipeline | Fan-out, memory hierarchy sidebar | +| `sn2-reaction-mechanism.md` | Chemistry | Molecules, curved arrows, energy profile | +| `smart-city-infrastructure.md` | Hub-spoke | Semantic line styles per system | +| `electricity-grid-flow.md` | Multi-stage flow | Voltage hierarchy, flow markers | +| `ml-benchmark-grouped-bar-chart.md` | Chart | Grouped bars, dual axis | + +Load any example with: +``` +skill_view(name="concept-diagrams", file_path="examples/") +``` + +--- + +## Quick Reference: What to Use When + +| User says | Diagram type | Suggested colors | +|-----------|--------------|------------------| +| "show the pipeline" | Flowchart | gray start/end, purple steps, red errors, teal deploy | +| "draw the data flow" | Data pipeline (left-right) | gray sources, purple processing, teal sinks | +| "visualize the system" | Structural (containment) | purple container, teal services, coral data | +| "map the endpoints" | API tree | purple root, one ramp per resource group | +| "show the services" | Microservice topology | gray ingress, teal services, purple bus, coral workers | +| "draw the aircraft/vehicle" | Physical | paths, polygons, ellipses for realistic shapes | +| "smart city / IoT" | Hub-spoke integration | semantic line styles per subsystem | +| "show the dashboard" | UI mockup | dark screen, chart colors: teal, purple, coral for alerts | +| "power grid / electricity" | Multi-stage flow | voltage hierarchy (HV/MV/LV line weights) | +| "wind turbine / turbine" | Physical cross-section | foundation + tower cutaway + nacelle color-coded | +| "journey of X / lifecycle" | Narrative journey | winding path, progressive state changes | +| "layers of X / exploded" | Exploded layer view | vertical stack, alternating labels | +| "CPU / pipeline" | Hardware pipeline | vertical stages, fan-out to execution ports | +| "floor plan / apartment" | Floor plan | walls, doors, proposed changes in dotted red | +| "reaction mechanism" | Chemistry | atoms, bonds, curved arrows, transition state, energy profile | diff --git a/optional-skills/creative/concept-diagrams/examples/apartment-floor-plan-conversion.md b/optional-skills/creative/concept-diagrams/examples/apartment-floor-plan-conversion.md new file mode 100644 index 00000000000..7c11d3401e5 --- /dev/null +++ b/optional-skills/creative/concept-diagrams/examples/apartment-floor-plan-conversion.md @@ -0,0 +1,244 @@ +# Apartment Floor Plan: 3 BHK to 4 BHK Conversion + +An architectural floor plan showing a 1,500 sq ft apartment with proposed modifications to convert from 3 BHK to 4 BHK. Demonstrates architectural drawing conventions, room layouts, proposed changes with dotted lines, and area comparison tables. + +## Key Patterns Used + +- **Architectural floor plan**: Top-down view with walls, doors, windows +- **Proposed modifications**: Dotted red lines for new walls +- **Room color coding**: Light fills to distinguish room types +- **Circulation paths**: Arrows showing new access routes +- **Data table**: Before/after area comparison with highlighting +- **Architectural symbols**: North arrow, scale bar, door swings + +## Diagram Type + +This is an **architectural floor plan** with: +- **Plan view**: Top-down orthographic projection +- **Overlay technique**: Existing structure + proposed changes +- **Quantitative data**: Area measurements and comparison table + +## Architectural Drawing Elements + +### Wall Styles + +```xml + + + + + + + + +``` + +```css +.wall { stroke: var(--text-primary); stroke-width: 6; fill: none; stroke-linecap: square; } +.wall-thin { stroke: var(--text-primary); stroke-width: 3; fill: none; } +.proposed-wall { stroke: #A32D2D; stroke-width: 4; fill: none; stroke-dasharray: 8 4; } +``` + +### Door Symbols + +```xml + + + + + + + + + + + + + +``` + +```css +.door { stroke: var(--text-secondary); stroke-width: 1.5; fill: none; } +.door-swing { stroke: var(--text-tertiary); stroke-width: 1; fill: none; stroke-dasharray: 3 2; } +``` + +### Window Symbols + +```xml + + + + + + + +``` + +```css +.window { stroke: var(--text-primary); stroke-width: 1; fill: var(--bg-primary); } +.window-glass { stroke: #378ADD; stroke-width: 2; fill: none; } +``` + +### Room Fills + +```xml + + + + + + + + + +``` + +```css +.room-master { fill: rgba(206, 203, 246, 0.3); } /* purple tint */ +.room-bed2 { fill: rgba(159, 225, 203, 0.3); } /* teal tint */ +.room-bed3 { fill: rgba(250, 199, 117, 0.3); } /* amber tint */ +.room-living { fill: rgba(245, 196, 179, 0.3); } /* coral tint */ +.room-kitchen { fill: rgba(237, 147, 177, 0.3); } /* pink tint */ +.room-bath { fill: rgba(133, 183, 235, 0.3); } /* blue tint */ +.room-new { fill: rgba(163, 45, 45, 0.15); } /* red tint for proposed */ +``` + +### Support Fixtures + +```xml + + +Counter + + + +``` + +```css +.balcony { fill: none; stroke: var(--text-secondary); stroke-width: 2; stroke-dasharray: 6 3; } +.balcony-fill { fill: rgba(93, 202, 165, 0.1); } +``` + +### Room Labels + +```xml + +MASTER +BEDROOM +195 sq ft + + +BEDROOM 4 +(NEW) +``` + +```css +.room-label { font-family: system-ui; font-size: 11px; fill: var(--text-primary); font-weight: 500; } +.area-label { font-family: system-ui; font-size: 9px; fill: var(--text-tertiary); } +``` + +### Circulation Arrow + +```xml + + + + + + + +New corridor access +``` + +```css +.circulation { stroke: #3B6D11; stroke-width: 2; fill: none; } +.circulation-fill { fill: #3B6D11; } +``` + +### North Arrow and Scale Bar + +```xml + + + + + N + + + + + + + + + 0 + 5' + 10' + +``` + +## Area Comparison Table + +### Table Structure + +```xml + + +Room + + + +Master Bedroom +195 + + + + + + +Bedroom 4 (NEW) ++100 + + + +TOTAL CARPET AREA +``` + +```css +.table-header { fill: var(--bg-secondary); } +.table-row { fill: var(--bg-primary); stroke: var(--border); stroke-width: 0.5; } +.table-row-alt { fill: var(--bg-tertiary); stroke: var(--border); stroke-width: 0.5; } +.table-highlight { fill: rgba(163, 45, 45, 0.1); stroke: #A32D2D; stroke-width: 0.5; } +``` + +## Layout Notes + +- **ViewBox**: 800×780 (portrait for floor plan + table) +- **Scale**: 10px = 1 foot (apartment ~50ft × 33ft) +- **Floor plan origin**: Offset at (50, 60) for margins +- **Wall thickness**: 6px outer, 3px inner (represents ~6" walls) +- **Room labels**: Centered in each room with area below +- **Table placement**: Below floor plan with full width + +## Color Coding + +| Element | Color | Usage | +|---------|-------|-------| +| Proposed walls | Red (#A32D2D) dotted | New construction | +| New room fill | Red 15% opacity | Bedroom 4 area | +| Circulation | Green (#3B6D11) | New access path | +| Window glass | Blue (#378ADD) | Glass indication | +| Bedrooms | Purple/Teal/Amber tints | Room differentiation | +| Wet areas | Blue tint | Bathrooms | +| Living | Coral tint | Common areas | + +## When to Use This Pattern + +Use this diagram style for: +- Apartment/house floor plans +- Office layout planning +- Renovation proposals showing before/after +- Space planning with area calculations +- Real estate marketing materials +- Interior design presentations +- Building permit documentation diff --git a/optional-skills/creative/concept-diagrams/examples/automated-password-reset-flow.md b/optional-skills/creative/concept-diagrams/examples/automated-password-reset-flow.md new file mode 100644 index 00000000000..86cd1cc0782 --- /dev/null +++ b/optional-skills/creative/concept-diagrams/examples/automated-password-reset-flow.md @@ -0,0 +1,276 @@ +# Automated Password Reset Flow + +A two-section flowchart tracing the full user journey for a web application password reset: the initial request phase (forgot password → email check → token generation) and the reset-form phase (link click → new password entry → token/password validation). Demonstrates multi-exit decision diamonds, a three-column branching layout, a loop-back path, and a cross-section separator arrow. + +## Key Patterns Used + +- **Three-column layout**: Left column (error/terminal branches at cx=115), center column (main happy path at cx=340), right column (expired-token branch at cx=552) — allows side branches to live at the same y-level as center nodes without overlap +- **Decision diamonds with ``**: Each decision uses a `` wrapper containing a `` and centered ``; the diamond points are computed as `cx±hw, cy±hh` (hw=100, hh=28) +- **Pill-shaped terminals**: Start and end nodes use `rx=22` on their `` to signal entry/exit points; all mid-flow process nodes use `rx=8` +- **Three-branch decision paths**: Each diamond has a "Yes" branch (down, short ``) and a "No" branch (`` going horizontal then vertical to a side column) +- **Loop-back path**: Mismatch error node loops back to the password-entry node via a routing corridor at x=215 — a 5-px gap between the left column (right edge x=210) and center column (left edge x=220); the path exits the bottom of the error node, drops below it, travels right to x=215, then goes up to the target node's center y, then right 5 px into the node's left edge +- **Section separator**: A dashed horizontal `` at y=452 splits the two phases; the connecting arrow crosses it with a faded label ("user receives email") to preserve flow continuity +- **Italic annotation**: The exact UX copy for the generic message ("If that email exists…") is shown as a faded italic `ts` text block below the left-branch terminal node +- **Legend row**: Five inline swatches (gray, purple, teal, red, amber diamond) at the bottom explain the color-to-role mapping + +## Diagram + +```xml + + + + + + + + + + + Section 1 — Forgot password request + + + + + User: "Forgot password" + + + + + + + + Enter email address + + + + + + + + Email in system? + + + + + No + + + + Yes + + + + + + + Generic message shown + Email sent if found + + + + + + + + Request handled + + + + "If that email exists, a reset + link has been sent." + + + + + + + Generate unique token + Time-limited, cryptographic + + + + + + + + Store token + user ID + + + + + + + + Send reset link via email + + + + + + + + user receives email + + Section 2 — Password reset form + + + + + + + User clicks reset link + + + + + + + + Enter new password ×2 + Confirm both passwords match + + + + + + + + Token expired? + + + + + Yes + + + + No + + + + + + + Token expired + Show expiry error + + + + + + + + End — request again + + + + + + Passwords match? + + + + + No + + + + Yes + + + + + + + Password mismatch + Passwords do not match + + + + + retry + + + + + + + Reset password + Invalidate used token + + + + + + + + Password reset complete + + + + Legend — + + User action + + System process + + Email / success + + Error state + + Decision + + +``` + +## Custom CSS + +Add these classes to the hosting page ` + + +
+

+

+ +
+ + diff --git a/optional-skills/creative/kanban-video-orchestrator/SKILL.md b/optional-skills/creative/kanban-video-orchestrator/SKILL.md index f323406300b..c5ac2a8c96e 100644 --- a/optional-skills/creative/kanban-video-orchestrator/SKILL.md +++ b/optional-skills/creative/kanban-video-orchestrator/SKILL.md @@ -8,7 +8,7 @@ platforms: [linux, macos, windows] metadata: hermes: tags: [video, kanban, multi-agent, orchestration, production-pipeline] - related_skills: [kanban-orchestrator, kanban-worker, ascii-video, manim-video, p5js, comfyui, touchdesigner-mcp, blender-mcp, pixel-art, ascii-art, songwriting-and-ai-music, heartmula, songsee, spotify, youtube-content, claude-design, excalidraw, html-artifact, baoyu-comic, baoyu-infographic, humanizer, gif-search, meme-generation] + related_skills: [kanban-orchestrator, kanban-worker, ascii-video, manim-video, p5js, comfyui, touchdesigner-mcp, blender-mcp, pixel-art, ascii-art, songwriting-and-ai-music, heartmula, songsee, spotify, youtube-content, claude-design, excalidraw, architecture-diagram, concept-diagrams, baoyu-comic, baoyu-infographic, humanizer, gif-search, meme-generation] credits: | The single-project workspace layout, profile-config patching pattern, SOUL.md-per-profile model, TEAM.md task-graph convention, and diff --git a/optional-skills/creative/kanban-video-orchestrator/references/intake.md b/optional-skills/creative/kanban-video-orchestrator/references/intake.md index 1f817da020b..d290b606f49 100644 --- a/optional-skills/creative/kanban-video-orchestrator/references/intake.md +++ b/optional-skills/creative/kanban-video-orchestrator/references/intake.md @@ -96,7 +96,8 @@ texture inside the final scene. - **Terminal-only or with GUI?** - **Voiceover for narration?** - **Diagram support needed?** — Often these benefit from a diagram skill - alongside the screen-capture/render step (`excalidraw`, `html-artifact`) + alongside the screen-capture/render step (`excalidraw`, + `architecture-diagram`, `concept-diagrams`) ### ASCII / terminal art diff --git a/optional-skills/creative/kanban-video-orchestrator/references/role-archetypes.md b/optional-skills/creative/kanban-video-orchestrator/references/role-archetypes.md index c5e15c06f4b..95eaeb33b66 100644 --- a/optional-skills/creative/kanban-video-orchestrator/references/role-archetypes.md +++ b/optional-skills/creative/kanban-video-orchestrator/references/role-archetypes.md @@ -59,7 +59,7 @@ local skills. - **Toolsets:** kanban, terminal, file - **Skills:** `kanban-worker` plus any project-specific design skill — - `claude-design` (UI/web), `html-artifact` (quick mockup variants, explainers, diagrams), + `claude-design` (UI/web), `sketch` (quick mockup variants), `popular-web-designs` (matching known web aesthetic), `pixel-art` (retro), `ascii-art` (terminal/retro), `excalidraw` (hand-drawn frames), `design-md` (text-based design docs) @@ -72,7 +72,8 @@ film and music video. Often pairs with a diagramming tool. - **Toolsets:** kanban, file - **Skills:** `kanban-worker` plus a diagram skill — `excalidraw` (sketch), - `html-artifact` (technical/system + educational/scientific diagrams) + `architecture-diagram` (technical/system), `concept-diagrams` (educational/ + scientific) - **Outputs:** `storyboard.md` with one row per scene/shot, optional storyboard sketches diff --git a/optional-skills/creative/kanban-video-orchestrator/references/tool-matrix.md b/optional-skills/creative/kanban-video-orchestrator/references/tool-matrix.md index 2f27ffc41e7..b5e59c31478 100644 --- a/optional-skills/creative/kanban-video-orchestrator/references/tool-matrix.md +++ b/optional-skills/creative/kanban-video-orchestrator/references/tool-matrix.md @@ -30,8 +30,10 @@ called from the terminal toolset; they don't appear in `always_load`. | `claude-design` | Design one-off HTML artifacts (landing, deck, prototype) | Concept artist for product video style frames; storyboarder for UI-heavy content | | `design-md` | Design markdown docs | Concept artist documenting visual specs | | `popular-web-designs` | Reference patterns for popular web designs | Concept artist; cinematographer when matching a known UI aesthetic | +| `sketch` | Throwaway HTML mockups (2-3 design variants to compare) | Concept artist exploring directions; storyboarder for UI flows | | `excalidraw` | Excalidraw-style hand-drawn diagrams | Storyboarder; concept artist for sketch-style frames | -| `html-artifact` | Self-contained HTML artifacts: throwaway mockup variants, explainers, dark-tech architecture + educational SVG diagrams | Concept artist exploring directions; storyboarder for UI flows + technical/educational explainer scenes | +| `architecture-diagram` | Software architecture diagrams | Storyboarder for technical content; explainer scenes about systems | +| `concept-diagrams` *(optional)* | Flat, minimal SVG diagrams (educational visual language; physics, chemistry, math, anatomy, etc.) | Renderer / storyboarder for explainer scenes with clean educational diagrams | | `pretext` | Mathematical/scientific content authoring | Writer / cinematographer for technical-explainer pretexts | | `creative-ideation` | Constraint-driven project ideation | Director / cinematographer when the brief is wide-open and needs framing | | `humanizer` | Strip AI-isms from text, add real voice | Writer / copywriter post-process to avoid AI-tells in scripts and VO copy | diff --git a/skills/creative/architecture-diagram/SKILL.md b/skills/creative/architecture-diagram/SKILL.md new file mode 100644 index 00000000000..2c813c53c13 --- /dev/null +++ b/skills/creative/architecture-diagram/SKILL.md @@ -0,0 +1,148 @@ +--- +name: architecture-diagram +description: "Dark-themed SVG architecture/cloud/infra diagrams as HTML." +version: 1.0.0 +author: Cocoon AI (hello@cocoon-ai.com), ported by Hermes Agent +license: MIT +dependencies: [] +platforms: [linux, macos, windows] +metadata: + hermes: + tags: [architecture, diagrams, SVG, HTML, visualization, infrastructure, cloud] + related_skills: [concept-diagrams, excalidraw] +--- + +# Architecture Diagram Skill + +Generate professional, dark-themed technical architecture diagrams as standalone HTML files with inline SVG graphics. No external tools, no API keys, no rendering libraries — just write the HTML file and open it in a browser. + +## Scope + +**Best suited for:** +- Software system architecture (frontend / backend / database layers) +- Cloud infrastructure (VPC, regions, subnets, managed services) +- Microservice / service-mesh topology +- Database + API map, deployment diagrams +- Anything with a tech-infra subject that fits a dark, grid-backed aesthetic + +**Look elsewhere first for:** +- Physics, chemistry, math, biology, or other scientific subjects +- Physical objects (vehicles, hardware, anatomy, cross-sections) +- Floor plans, narrative journeys, educational / textbook-style visuals +- Hand-drawn whiteboard sketches (consider `excalidraw`) +- Animated explainers (consider an animation skill) + +If a more specialized skill is available for the subject, prefer that. If none fits, this skill can also serve as a general SVG diagram fallback — the output will just carry the dark tech aesthetic described below. + +Based on [Cocoon AI's architecture-diagram-generator](https://github.com/Cocoon-AI/architecture-diagram-generator) (MIT). + +## Workflow + +1. User describes their system architecture (components, connections, technologies) +2. Generate the HTML file following the design system below +3. Save with `write_file` to a `.html` file (e.g. `~/architecture-diagram.html`) +4. User opens in any browser — works offline, no dependencies + +### Output Location + +Save diagrams to a user-specified path, or default to the current working directory: +``` +./[project-name]-architecture.html +``` + +### Preview + +After saving, suggest the user open it: +```bash +# macOS +open ./my-architecture.html +# Linux +xdg-open ./my-architecture.html +``` + +## Design System & Visual Language + +### Color Palette (Semantic Mapping) + +Use specific `rgba` fills and hex strokes to categorize components: + +| Component Type | Fill (rgba) | Stroke (Hex) | +| :--- | :--- | :--- | +| **Frontend** | `rgba(8, 51, 68, 0.4)` | `#22d3ee` (cyan-400) | +| **Backend** | `rgba(6, 78, 59, 0.4)` | `#34d399` (emerald-400) | +| **Database** | `rgba(76, 29, 149, 0.4)` | `#a78bfa` (violet-400) | +| **AWS/Cloud** | `rgba(120, 53, 15, 0.3)` | `#fbbf24` (amber-400) | +| **Security** | `rgba(136, 19, 55, 0.4)` | `#fb7185` (rose-400) | +| **Message Bus** | `rgba(251, 146, 60, 0.3)` | `#fb923c` (orange-400) | +| **External** | `rgba(30, 41, 59, 0.5)` | `#94a3b8` (slate-400) | + +### Typography & Background +- **Font:** JetBrains Mono (Monospace), loaded from Google Fonts +- **Sizes:** 12px (Names), 9px (Sublabels), 8px (Annotations), 7px (Tiny labels) +- **Background:** Slate-950 (`#020617`) with a subtle 40px grid pattern + +```svg + + + + +``` + +## Technical Implementation Details + +### Component Rendering +Components are rounded rectangles (`rx="6"`) with 1.5px strokes. To prevent arrows from showing through semi-transparent fills, use a **double-rect masking technique**: +1. Draw an opaque background rect (`#0f172a`) +2. Draw the semi-transparent styled rect on top + +### Connection Rules +- **Z-Order:** Draw arrows *early* in the SVG (after the grid) so they render behind component boxes +- **Arrowheads:** Defined via SVG markers +- **Security Flows:** Use dashed lines in rose color (`#fb7185`) +- **Boundaries:** + - *Security Groups:* Dashed (`4,4`), rose color + - *Regions:* Large dashed (`8,4`), amber color, `rx="12"` + +### Spacing & Layout Logic +- **Standard Height:** 60px (Services); 80-120px (Large components) +- **Vertical Gap:** Minimum 40px between components +- **Message Buses:** Must be placed *in the gap* between services, not overlapping them +- **Legend Placement:** **CRITICAL.** Must be placed outside all boundary boxes. Calculate the lowest Y-coordinate of all boundaries and place the legend at least 20px below it. + +## Document Structure + +The generated HTML file follows a four-part layout: +1. **Header:** Title with a pulsing dot indicator and subtitle +2. **Main SVG:** The diagram contained within a rounded border card +3. **Summary Cards:** A grid of three cards below the diagram for high-level details +4. **Footer:** Minimal metadata + +### Info Card Pattern +```html +
+
+
+

Title

+
+
    +
  • • Item one
  • +
  • • Item two
  • +
+
+``` + +## Output Requirements +- **Single File:** One self-contained `.html` file +- **No External Dependencies:** All CSS and SVG must be inline (except Google Fonts) +- **No JavaScript:** Use pure CSS for any animations (like pulsing dots) +- **Compatibility:** Must render correctly in any modern web browser + +## Template Reference + +Load the full HTML template for the exact structure, CSS, and SVG component examples: + +``` +skill_view(name="architecture-diagram", file_path="templates/template.html") +``` + +The template contains working examples of every component type (frontend, backend, database, cloud, security), arrow styles (standard, dashed, curved), security groups, region boundaries, and the legend — use it as your structural reference when generating diagrams. diff --git a/skills/creative/architecture-diagram/templates/template.html b/skills/creative/architecture-diagram/templates/template.html new file mode 100644 index 00000000000..f5b32fbe7fd --- /dev/null +++ b/skills/creative/architecture-diagram/templates/template.html @@ -0,0 +1,319 @@ + + + + + + [PROJECT NAME] Architecture Diagram + + + + +
+ +
+
+
+

[PROJECT NAME] Architecture

+
+

[Subtitle description]

+
+ + +
+ + + + + + + + + + + + + + + + + + + Users + Browser/Mobile + + + + Auth Provider + OAuth 2.0 + + + + AWS Region: us-west-2 + + + + CloudFront + CDN + + + + S3 Buckets + • bucket-one + • bucket-two + • bucket-three + OAI Protected + + + + sg-name :port + + + + Load Balancer + HTTPS :443 + + + + API Server + FastAPI :8000 + + + + Database + PostgreSQL + + + + Frontend + React + TypeScript + Additional detail + More info + domain.example.com + + + + + + HTTPS + + + + + + + OAI + + + + + TLS + + + + JWT + PKCE + + + Legend + + + Frontend + + + Backend + + + Cloud Service + + + Database + + + Security + + + Auth Flow + + + Security Group + +
+ + +
+
+
+
+

Card Title 1

+
+
    +
  • • Item one
  • +
  • • Item two
  • +
  • • Item three
  • +
  • • Item four
  • +
+
+ +
+
+
+

Card Title 2

+
+
    +
  • • Item one
  • +
  • • Item two
  • +
  • • Item three
  • +
  • • Item four
  • +
+
+ +
+
+
+

Card Title 3

+
+
    +
  • • Item one
  • +
  • • Item two
  • +
  • • Item three
  • +
  • • Item four
  • +
+
+
+ + + +
+ + diff --git a/skills/creative/claude-design/SKILL.md b/skills/creative/claude-design/SKILL.md index d61dbcb2f00..673d1ff827a 100644 --- a/skills/creative/claude-design/SKILL.md +++ b/skills/creative/claude-design/SKILL.md @@ -8,7 +8,7 @@ platforms: [linux, macos, windows] metadata: hermes: tags: [design, html, prototype, ux, ui, creative, artifact, deck, motion, design-system] - related_skills: [html-artifact, design-md, popular-web-designs, excalidraw] + related_skills: [design-md, popular-web-designs, excalidraw, architecture-diagram] --- # Claude Design for CLI/API Agents @@ -19,21 +19,19 @@ The goal is to preserve Claude Design's useful design behavior and taste while r **Before starting, check for other web-design skills like `popular-web-designs` (ready-to-paste design systems for Stripe, Linear, Vercel, Notion, etc.) and `design-md` (Google's DESIGN.md token spec format).** If the user wants a known brand's look, load `popular-web-designs` alongside this one and let it supply the visual vocabulary. If the deliverable is a token spec file rather than a rendered artifact, use `design-md` instead. Full decision table below. -## When To Use This Skill vs `html-artifact` vs `popular-web-designs` vs `design-md` +## When To Use This Skill vs `popular-web-designs` vs `design-md` -Several skills produce HTML — they do different jobs. Load the right one (or combine them): +Hermes has three design-related skills under `skills/creative/`. They do different jobs — load the right one (or combine them): | Skill | What it gives you | Use when the user wants... | |---|---|---| -| **claude-design** (this one) | Visual design *process and taste* — how to scope a brief, gather context, produce variants, verify a local HTML artifact, avoid AI-design slop | a from-scratch *designed* artifact (landing page, prototype, deck, component lab, motion study) where the look itself is the point and no specific brand or token system is dictated | -| **html-artifact** | A house style for *information* artifacts — explainers, plans, reports, code reviews, technical/educational diagrams, throwaway editors | to *explain / plan / report / diagram / review* something as a shareable HTML page — the content is the point, not bespoke visual design | +| **claude-design** (this one) | Design *process and taste* — how to scope a brief, gather context, produce variants, verify a local HTML artifact, avoid AI-design slop | a from-scratch designed artifact (landing page, prototype, deck, component lab, motion study) with no specific brand or token system dictated | | **popular-web-designs** | 54 ready-to-paste design systems — exact colors, typography, components, CSS values for sites like Stripe, Linear, Vercel, Notion, Airbnb | "make it look like Stripe / Linear / Vercel", a page styled after a known brand, or a visual starting point pulled from a real product | | **design-md** | Google's DESIGN.md spec format — author/validate/diff/export design-token files, WCAG contrast checking, Tailwind/DTCG export | a formal, persistent, machine-readable design-system *spec file* (tokens + rationale) that lives in a repo and gets consumed by agents over time | Rule of thumb: -- **Bespoke visual design, taste-driven artifact** → claude-design -- **Explain / plan / report / diagram as a shareable page** → html-artifact +- **Process + taste, one-off artifact** → claude-design - **Match a known brand's look** → popular-web-designs (and let claude-design drive the process) - **Author the tokens spec itself** → design-md diff --git a/skills/creative/design-md/SKILL.md b/skills/creative/design-md/SKILL.md index e0534d9ba72..6604be1979d 100644 --- a/skills/creative/design-md/SKILL.md +++ b/skills/creative/design-md/SKILL.md @@ -8,7 +8,7 @@ platforms: [linux, macos, windows] metadata: hermes: tags: [design, design-system, tokens, ui, accessibility, wcag, tailwind, dtcg, google] - related_skills: [popular-web-designs, claude-design, excalidraw, html-artifact] + related_skills: [popular-web-designs, claude-design, excalidraw, architecture-diagram] --- # DESIGN.md Skill diff --git a/skills/creative/html-artifact/SKILL.md b/skills/creative/html-artifact/SKILL.md deleted file mode 100644 index 4883e1ff4c1..00000000000 --- a/skills/creative/html-artifact/SKILL.md +++ /dev/null @@ -1,184 +0,0 @@ ---- -name: html-artifact -description: Build self-contained HTML files to explain, plan, or review. -version: 1.0.0 -author: Anthropic (html-effectiveness gallery, MIT), adapted for Hermes Agent -license: MIT -platforms: [linux, macos, windows] -metadata: - hermes: - tags: [html, artifact, explainer, plan, report, code-review, diagram, svg, design, prototype, editor] - related_skills: [claude-design, popular-web-designs, design-md, excalidraw, p5js] ---- - -# HTML Artifact Skill - -Produce a single self-contained `.html` file — no build step, no dependencies, no -CDN — whenever the deliverable is something a human should *read, share, or poke at*: -a concept explainer, an implementation plan, a status/incident report, a code-review -walkthrough, a technical or educational diagram, a set of design variants, or a -throwaway editor that exports its result back to you. - -HTML beats Markdown once a doc has color, layout, diagrams, tables, code, or -interaction. It opens in any browser, shares as a link, stays readable past 100 -lines, and can carry SVG diagrams and live controls Markdown can't. Default to an -HTML artifact when the user says "make an HTML file/artifact", or asks you to -*explain how X works*, *write up a plan/PR/report*, *diagram* something, *compare* -options, or *prototype* an interaction — even when they don't say "HTML". - -## Why this skill exists (and what it replaced) - -This skill **supersedes** three former skills — `sketch` (throwaway multi-variant -HTML mockups), `architecture-diagram` (dark-tech infra SVG), and `concept-diagrams` -(educational SVG). They were consolidated for a concrete reason: all three emitted -the *same artifact* — a single self-contained HTML file with inline CSS/SVG — and -overlapped heavily (three "diagram" skills, two "compare variants" paths, no shared -token system). Folding them into one mode-switched skill removes the -which-one-do-I-load ambiguity and gives every output the same house style, while -keeping each skill's unique value: the fidelity dial + verify loop (from `sketch`), -the dark infra aesthetic (from `architecture-diagram`), and the 9-ramp educational -system + archetype library (from `concept-diagrams`). - -The consolidation is footprint-safe: this skill has **zero dependencies** (no Node, -FFmpeg, Chromium, or pip packages — it authors plain HTML/CSS/SVG), so even though it -ships **bundled** (active by default) where `concept-diagrams` was optional, the only -always-in-context cost is this skill's one-line description. All references, -templates, and the example gallery load on demand. `concept-diagrams` was optional -because it was niche, not because it had an install cost — promoting that capability -into a general-purpose, zero-dep bundled skill is the right home for it. Diagram-style -work with a *real* install cost (e.g. `hyperframes`: Node + FFmpeg + Chromium) -deliberately stays optional and is **not** folded in here. - -Use a different skill when: matching a known brand's look → `popular-web-designs`; a -formal design-token spec file → `design-md`; a *bespoke visually-designed* artifact -where the look itself is the point → `claude-design`; hand-drawn/whiteboard -`.excalidraw` files → `excalidraw`; generative/animated canvas art → `p5js`. This -skill is for everything else that ships as a readable, shareable HTML page. - -## Reference files (load on demand) - -- `references/house-style.md` — the canonical `:root` token block, type system, - card/table/callout/code-block patterns. **Read this before authoring any artifact.** -- `references/examples.md` — 20 complete reference HTML files (Anthropic's - html-effectiveness gallery, MIT) keyed to each mode, plus the script to fetch them. - Read/fetch one that matches your task to calibrate the house style from a full example. -- `references/svg-diagrams.md` — hand-authored inline SVG: arrow markers, node - groups, decision diamonds, edge semantics, coordinate-grid discipline. Read for - any flowchart / architecture / concept diagram. -- `references/concept-archetypes.md` — the 9-ramp educational color system + a - library of diagram archetypes (timeline, tree, quadrant, layered stack, - before/after, hub-spoke, cross-section). Read for educational / non-software visuals. -- `references/dark-tech.md` — the dark "infra" token variant (carries the old - architecture-diagram aesthetic). Read for cloud/infra/system architecture diagrams. -- `references/throwaway-editors.md` — the single-file editor recipe and the - copy-to-clipboard export pattern that survives `file://`. Read when the artifact - needs interactive controls that export state back to a prompt. -- `references/fidelity-and-verify.md` — the throwaway↔presentation fidelity dial, - the multi-variant comparison layout, and the mandatory browser-vision verify loop. - -## Templates - -- `templates/base.html` — document scaffold with the house-style ` - - -
-

Section · Context

-

Artifact Title

-

One-sentence framing of what this artifact is and who it's for.

- -

Overview

-

Body copy. Keep paragraphs readable; let layout carry structure.

- -
-

Metric

42
-

Metric

7
-

Needs attention

3
-

Metric

98%
-
- -
Note. Use callouts for the one thing the reader must not miss.
- - - -
- - diff --git a/skills/creative/html-artifact/templates/diagram.html b/skills/creative/html-artifact/templates/diagram.html deleted file mode 100644 index 93522119d36..00000000000 --- a/skills/creative/html-artifact/templates/diagram.html +++ /dev/null @@ -1,127 +0,0 @@ - - - - - -Diagram - - - - - -
-

-

- - -
- - diff --git a/skills/creative/html-artifact/templates/editor.html b/skills/creative/html-artifact/templates/editor.html deleted file mode 100644 index 88ee378d7a3..00000000000 --- a/skills/creative/html-artifact/templates/editor.html +++ /dev/null @@ -1,120 +0,0 @@ - - - - - -Editor - - - - -
-

Throwaway editor

-

Toggle what ships, copy the result

-
-
- - -
-
- - - - diff --git a/skills/creative/pretext/SKILL.md b/skills/creative/pretext/SKILL.md index c526d000ddd..78f5ab2d959 100644 --- a/skills/creative/pretext/SKILL.md +++ b/skills/creative/pretext/SKILL.md @@ -8,7 +8,7 @@ platforms: [linux, macos, windows] metadata: hermes: tags: [creative-coding, typography, pretext, ascii-art, canvas, generative, text-layout, kinetic-typography] - related_skills: [p5js, claude-design, excalidraw, html-artifact] + related_skills: [p5js, claude-design, excalidraw, architecture-diagram] --- # Pretext Creative Demos diff --git a/skills/creative/sketch/SKILL.md b/skills/creative/sketch/SKILL.md new file mode 100644 index 00000000000..6e49585acd4 --- /dev/null +++ b/skills/creative/sketch/SKILL.md @@ -0,0 +1,218 @@ +--- +name: sketch +description: "Throwaway HTML mockups: 2-3 design variants to compare." +version: 1.0.0 +author: Hermes Agent (adapted from gsd-build/get-shit-done) +license: MIT +platforms: [linux, macos, windows] +metadata: + hermes: + tags: [sketch, mockup, design, ui, prototype, html, variants, exploration, wireframe, comparison] + related_skills: [spike, claude-design, popular-web-designs, excalidraw] +--- + +# Sketch + +Use this skill when the user wants to **see a design direction before committing** to one — exploring a UI/UX idea as disposable HTML mockups. The point is to generate 2-3 interactive variants so the user can compare visual directions side-by-side, not to produce shippable code. + +Load this when the user says things like "sketch this screen", "show me what X could look like", "compare layout A vs B", "give me 2-3 takes on this UI", "let me see some variants", "mockup this before I build". + +## When NOT to use this + +- User wants a production component — use `claude-design` or build it properly +- User wants a polished one-off HTML artifact (landing page, deck) — `claude-design` +- User wants a diagram — `excalidraw`, `architecture-diagram` +- The design is already locked — just build it + +## If the user has the full GSD system installed + +If `gsd-sketch` shows up as a sibling skill (installed via `npx get-shit-done-cc --hermes`), prefer **`gsd-sketch`** for the full workflow: persistent `.planning/sketches/` with MANIFEST, frontier mode analysis, consistency audits across past sketches, and integration with the rest of GSD. This skill is the lightweight standalone version — one-off sketching without the state machinery. + +## Core method + +``` +intake → variants → head-to-head → pick winner (or iterate) +``` + +### 1. Intake (skip if the user already gave you enough) + +Before generating variants, get three things — one question at a time, not all at once: + +1. **Feel.** "What should this feel like? Adjectives, emotions, a vibe." — *"calm, editorial, like Linear"* tells you more than *"minimal"*. +2. **References.** "What apps, sites, or products capture the feel you're imagining?" — actual references beat abstract descriptions. +3. **Core action.** "What's the single most important thing a user does on this screen?" — the variants should all serve this well; if they don't, they're just decoration. + +Reflect each answer briefly before the next question. If the user already gave you all three upfront, skip straight to variants. + +### 2. Variants (2-3, never 1, rarely 4+) + +Produce **2-3 variants** in one go. Each variant is a complete, standalone HTML file. Don't describe variants — build them. The point is comparison. + +Each variant should take a **different design stance**, not different pixel values. Three good variant axes: + +- **Density:** compact / airy / ultra-dense (pick two contrasting poles) +- **Emphasis:** content-first / action-first / tool-first +- **Aesthetic:** editorial / utilitarian / playful +- **Layout:** single-column / sidebar / split-pane +- **Grounding:** card-based / bare-content / document-style + +Pick one axis and pull apart from it. Two variants that differ only in accent color are wasted effort — the user can't distinguish them. + +**Variant naming:** describe the stance, not the number. + +``` +sketches/ +├── 001-calm-editorial/ +│ ├── index.html +│ └── README.md +├── 001-utilitarian-dense/ +│ ├── index.html +│ └── README.md +└── 001-playful-split/ + ├── index.html + └── README.md +``` + +### 3. Make them real HTML + +Each variant is a **single self-contained HTML file**: + +- Inline ` +``` + +### 4. Variant README + +Each variant's `README.md` answers: + +```markdown +## Variant: {stance name} + +### Design stance +One sentence on the principle driving this variant. + +### Key choices +- Layout: ... +- Typography: ... +- Color: ... +- Interaction: ... + +### Trade-offs +- Strong at: ... +- Weak at: ... + +### Best for +- The kind of user or use case this variant actually serves +``` + +### 5. Head-to-head + +After all variants are built, present them as a comparison. Don't just list — **opinionate**: + +```markdown +## Three takes on the home screen + +| Dimension | Calm editorial | Utilitarian dense | Playful split | +|-----------|----------------|-------------------|---------------| +| Density | Low | High | Medium | +| Primary action visibility | Low | High | Medium | +| Scan-ability | High | Medium | Low | +| Feel | Calm, trusted | Sharp, tool-like | Inviting, energetic | + +**My take:** Utilitarian dense for power users, calm editorial for content-forward audiences. Playful split is weakest — tries to do both and commits to neither. +``` + +Let the user pick a winner, or combine two into a hybrid, or ask for another round. + +## Theming (when the project has a visual identity) + +If the user has an existing theme (colors, fonts, tokens), put shared tokens in `sketches/themes/tokens.css` and `@import` them in each variant. Keep tokens minimal: + +```css +/* sketches/themes/tokens.css */ +:root { + --color-bg: #fafafa; + --color-fg: #1a1a1a; + --color-accent: #0066ff; + --color-muted: #666; + --radius: 8px; + --font-display: "Inter", sans-serif; + --font-body: -apple-system, BlinkMacSystemFont, sans-serif; +} +``` + +Don't over-tokenize a throwaway sketch — three colors and one font is usually enough. + +## Interactivity bar + +A sketch is interactive enough when the user can: + +1. **Click a primary action** and something visible happens (state change, modal, toast, navigation feint) +2. **See one meaningful state transition** (filter a list, toggle a mode, open/close a panel) +3. **Hover recognizable affordances** (buttons, rows, tabs) + +More than that is over-engineering a throwaway. Less than that is a screenshot. + +## Frontier mode (picking what to sketch next) + +If sketches already exist and the user says "what should I sketch next?": + +- **Consistency gaps** — two winning variants from different sketches made independent choices that haven't been composed together yet +- **Unsketched screens** — referenced but never explored +- **State coverage** — happy path sketched, but not empty / loading / error / 1000-items +- **Responsive gaps** — validated at one viewport; does it hold at mobile / ultrawide? +- **Interaction patterns** — static layouts exist; transitions, drag, scroll behavior don't + +Propose 2-4 named candidates. Let the user pick. + +## Output + +- Create `sketches/` (or `.planning/sketches/` if the user is using GSD conventions) in the repo root +- One subdir per variant: `NNN-stance-name/index.html` + `README.md` +- Tell the user how to open them: `open sketches/001-calm-editorial/index.html` on macOS, `xdg-open` on Linux, `start` on Windows +- Keep variants disposable — a sketch that you felt the need to preserve should be promoted into real project code, not curated as an asset + +**Typical tool sequence for one variant:** + +``` +terminal("mkdir -p sketches/001-calm-editorial") +write_file("sketches/001-calm-editorial/index.html", "...") +write_file("sketches/001-calm-editorial/README.md", "## Variant: Calm editorial\n...") +browser_navigate(url="file://$(pwd)/sketches/001-calm-editorial/index.html") +browser_vision(question="How does this look? Any obvious layout issues?") +``` + +Repeat for each variant, then present the comparison table. + +## Attribution + +Adapted from the GSD (Get Shit Done) project's `/gsd-sketch` workflow — MIT © 2025 Lex Christopherson ([gsd-build/get-shit-done](https://github.com/gsd-build/get-shit-done)). The full GSD system ships persistent sketch state, theme/variant pattern references, and consistency-audit workflows; install with `npx get-shit-done-cc --hermes --global`. diff --git a/skills/software-development/spike/SKILL.md b/skills/software-development/spike/SKILL.md index 313cbe7fb9c..2a980f0ade9 100644 --- a/skills/software-development/spike/SKILL.md +++ b/skills/software-development/spike/SKILL.md @@ -8,7 +8,7 @@ platforms: [linux, macos, windows] metadata: hermes: tags: [spike, prototype, experiment, feasibility, throwaway, exploration, research, planning, mvp, proof-of-concept] - related_skills: [html-artifact, subagent-driven-development, plan] + related_skills: [sketch, subagent-driven-development, plan] --- # Spike diff --git a/website/docs/reference/optional-skills-catalog.md b/website/docs/reference/optional-skills-catalog.md index a9e27dfd90e..4e2b2524fe2 100644 --- a/website/docs/reference/optional-skills-catalog.md +++ b/website/docs/reference/optional-skills-catalog.md @@ -58,6 +58,7 @@ hermes skills uninstall | [**baoyu-article-illustrator**](/docs/user-guide/skills/optional/creative/creative-baoyu-article-illustrator) | Article illustrations: type × style × palette consistency. | | [**baoyu-comic**](/docs/user-guide/skills/optional/creative/creative-baoyu-comic) | Knowledge comics (知识漫画): educational, biography, tutorial. | | [**blender-mcp**](/docs/user-guide/skills/optional/creative/creative-blender-mcp) | Control Blender directly from Hermes via socket connection to the blender-mcp addon. Create 3D objects, materials, animations, and run arbitrary Blender Python (bpy) code. Use when user wants to create or modify anything in Blender. | +| [**concept-diagrams**](/docs/user-guide/skills/optional/creative/creative-concept-diagrams) | Generate flat, minimal light/dark-aware SVG diagrams as standalone HTML files, using a unified educational visual language with 9 semantic color ramps, sentence-case typography, and automatic dark mode. Best suited for educational and no... | | [**ideation**](/docs/user-guide/skills/optional/creative/creative-creative-ideation) | Generate project ideas via creative constraints. | | [**hyperframes**](/docs/user-guide/skills/optional/creative/creative-hyperframes) | Create HTML-based video compositions, animated title cards, social overlays, captioned talking-head videos, audio-reactive visuals, and shader transitions using HyperFrames. HTML is the source of truth for video. Use when the user wants... | | [**kanban-video-orchestrator**](/docs/user-guide/skills/optional/creative/creative-kanban-video-orchestrator) | Plan, set up, and monitor a multi-agent video production pipeline backed by Hermes Kanban. Use when the user wants to make ANY video — narrative film, product/marketing, music video, explainer, ASCII/terminal art, abstract/generative loo... | diff --git a/website/docs/reference/skills-catalog.md b/website/docs/reference/skills-catalog.md index 3ae519a07f8..5ccb1f5f5ca 100644 --- a/website/docs/reference/skills-catalog.md +++ b/website/docs/reference/skills-catalog.md @@ -35,6 +35,7 @@ If a skill is missing from this list but present in the repo, the catalog is reg | Skill | Description | Path | |-------|-------------|------| +| [`architecture-diagram`](/docs/user-guide/skills/bundled/creative/creative-architecture-diagram) | Dark-themed SVG architecture/cloud/infra diagrams as HTML. | `creative/architecture-diagram` | | [`ascii-art`](/docs/user-guide/skills/bundled/creative/creative-ascii-art) | ASCII art: pyfiglet, cowsay, boxes, image-to-ascii. | `creative/ascii-art` | | [`ascii-video`](/docs/user-guide/skills/bundled/creative/creative-ascii-video) | ASCII video: convert video/audio to colored ASCII MP4/GIF. | `creative/ascii-video` | | [`baoyu-infographic`](/docs/user-guide/skills/bundled/creative/creative-baoyu-infographic) | Infographics: 21 layouts x 21 styles (信息图, 可视化). | `creative/baoyu-infographic` | @@ -42,12 +43,12 @@ If a skill is missing from this list but present in the repo, the catalog is reg | [`comfyui`](/docs/user-guide/skills/bundled/creative/creative-comfyui) | Generate images, video, and audio with ComfyUI — install, launch, manage nodes/models, run workflows with parameter injection. Uses the official comfy-cli for lifecycle and direct REST/WebSocket API for execution. | `creative/comfyui` | | [`design-md`](/docs/user-guide/skills/bundled/creative/creative-design-md) | Author/validate/export Google's DESIGN.md token spec files. | `creative/design-md` | | [`excalidraw`](/docs/user-guide/skills/bundled/creative/creative-excalidraw) | Hand-drawn Excalidraw JSON diagrams (arch, flow, seq). | `creative/excalidraw` | -| [`html-artifact`](/docs/user-guide/skills/bundled/creative/creative-html-artifact) | Build self-contained HTML files to explain, plan, or review. | `creative/html-artifact` | | [`humanizer`](/docs/user-guide/skills/bundled/creative/creative-humanizer) | Humanize text: strip AI-isms and add real voice. | `creative/humanizer` | | [`manim-video`](/docs/user-guide/skills/bundled/creative/creative-manim-video) | Manim CE animations: 3Blue1Brown math/algo videos. | `creative/manim-video` | | [`p5js`](/docs/user-guide/skills/bundled/creative/creative-p5js) | p5.js sketches: gen art, shaders, interactive, 3D. | `creative/p5js` | | [`popular-web-designs`](/docs/user-guide/skills/bundled/creative/creative-popular-web-designs) | 54 real design systems (Stripe, Linear, Vercel) as HTML/CSS. | `creative/popular-web-designs` | | [`pretext`](/docs/user-guide/skills/bundled/creative/creative-pretext) | Use when building creative browser demos with @chenglou/pretext — DOM-free text layout for ASCII art, typographic flow around obstacles, text-as-geometry games, kinetic typography, and text-powered generative art. Produces single-file HT... | `creative/pretext` | +| [`sketch`](/docs/user-guide/skills/bundled/creative/creative-sketch) | Throwaway HTML mockups: 2-3 design variants to compare. | `creative/sketch` | | [`songwriting-and-ai-music`](/docs/user-guide/skills/bundled/creative/creative-songwriting-and-ai-music) | Songwriting craft and Suno AI music prompts. | `creative/songwriting-and-ai-music` | | [`touchdesigner-mcp`](/docs/user-guide/skills/bundled/creative/creative-touchdesigner-mcp) | Control a running TouchDesigner instance via twozero MCP — create operators, set parameters, wire connections, execute Python, build real-time visuals. 36 native tools. | `creative/touchdesigner-mcp` | diff --git a/website/docs/user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-hermes-agent.md b/website/docs/user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-hermes-agent.md index 089ea173923..77f81db14b6 100644 --- a/website/docs/user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-hermes-agent.md +++ b/website/docs/user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-hermes-agent.md @@ -360,7 +360,7 @@ The registry of record is `hermes_cli/commands.py` — every consumer ``` ~/.hermes/config.yaml Main configuration -~/.hermes/.env API keys and secrets (under $HERMES_HOME if set) +~/.hermes/.env API keys and secrets $HERMES_HOME/skills/ Installed skills ~/.hermes/sessions/ Gateway routing index, request dumps, *.jsonl transcripts (and optional per-session JSON snapshots when sessions.write_json_snapshots: true) ~/.hermes/state.db Canonical session store (SQLite + FTS5) @@ -927,7 +927,7 @@ hermes-agent/ ``` -Config: `~/.hermes/config.yaml` (settings), `~/.hermes/.env` (API keys) — both under `$HERMES_HOME` when it is set. +Config: `~/.hermes/config.yaml` (settings), `~/.hermes/.env` (API keys). ### Adding a Tool (3 files) diff --git a/website/docs/user-guide/skills/bundled/creative/creative-architecture-diagram.md b/website/docs/user-guide/skills/bundled/creative/creative-architecture-diagram.md new file mode 100644 index 00000000000..ad816a370ad --- /dev/null +++ b/website/docs/user-guide/skills/bundled/creative/creative-architecture-diagram.md @@ -0,0 +1,165 @@ +--- +title: "Architecture Diagram — Dark-themed SVG architecture/cloud/infra diagrams as HTML" +sidebar_label: "Architecture Diagram" +description: "Dark-themed SVG architecture/cloud/infra diagrams as HTML" +--- + +{/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */} + +# Architecture Diagram + +Dark-themed SVG architecture/cloud/infra diagrams as HTML. + +## Skill metadata + +| | | +|---|---| +| Source | Bundled (installed by default) | +| Path | `skills/creative/architecture-diagram` | +| Version | `1.0.0` | +| Author | Cocoon AI (hello@cocoon-ai.com), ported by Hermes Agent | +| License | MIT | +| Platforms | linux, macos, windows | +| Tags | `architecture`, `diagrams`, `SVG`, `HTML`, `visualization`, `infrastructure`, `cloud` | +| Related skills | [`concept-diagrams`](/docs/user-guide/skills/optional/creative/creative-concept-diagrams), [`excalidraw`](/docs/user-guide/skills/bundled/creative/creative-excalidraw) | + +## Reference: full SKILL.md + +:::info +The following is the complete skill definition that Hermes loads when this skill is triggered. This is what the agent sees as instructions when the skill is active. +::: + +# Architecture Diagram Skill + +Generate professional, dark-themed technical architecture diagrams as standalone HTML files with inline SVG graphics. No external tools, no API keys, no rendering libraries — just write the HTML file and open it in a browser. + +## Scope + +**Best suited for:** +- Software system architecture (frontend / backend / database layers) +- Cloud infrastructure (VPC, regions, subnets, managed services) +- Microservice / service-mesh topology +- Database + API map, deployment diagrams +- Anything with a tech-infra subject that fits a dark, grid-backed aesthetic + +**Look elsewhere first for:** +- Physics, chemistry, math, biology, or other scientific subjects +- Physical objects (vehicles, hardware, anatomy, cross-sections) +- Floor plans, narrative journeys, educational / textbook-style visuals +- Hand-drawn whiteboard sketches (consider `excalidraw`) +- Animated explainers (consider an animation skill) + +If a more specialized skill is available for the subject, prefer that. If none fits, this skill can also serve as a general SVG diagram fallback — the output will just carry the dark tech aesthetic described below. + +Based on [Cocoon AI's architecture-diagram-generator](https://github.com/Cocoon-AI/architecture-diagram-generator) (MIT). + +## Workflow + +1. User describes their system architecture (components, connections, technologies) +2. Generate the HTML file following the design system below +3. Save with `write_file` to a `.html` file (e.g. `~/architecture-diagram.html`) +4. User opens in any browser — works offline, no dependencies + +### Output Location + +Save diagrams to a user-specified path, or default to the current working directory: +``` +./[project-name]-architecture.html +``` + +### Preview + +After saving, suggest the user open it: +```bash +# macOS +open ./my-architecture.html +# Linux +xdg-open ./my-architecture.html +``` + +## Design System & Visual Language + +### Color Palette (Semantic Mapping) + +Use specific `rgba` fills and hex strokes to categorize components: + +| Component Type | Fill (rgba) | Stroke (Hex) | +| :--- | :--- | :--- | +| **Frontend** | `rgba(8, 51, 68, 0.4)` | `#22d3ee` (cyan-400) | +| **Backend** | `rgba(6, 78, 59, 0.4)` | `#34d399` (emerald-400) | +| **Database** | `rgba(76, 29, 149, 0.4)` | `#a78bfa` (violet-400) | +| **AWS/Cloud** | `rgba(120, 53, 15, 0.3)` | `#fbbf24` (amber-400) | +| **Security** | `rgba(136, 19, 55, 0.4)` | `#fb7185` (rose-400) | +| **Message Bus** | `rgba(251, 146, 60, 0.3)` | `#fb923c` (orange-400) | +| **External** | `rgba(30, 41, 59, 0.5)` | `#94a3b8` (slate-400) | + +### Typography & Background +- **Font:** JetBrains Mono (Monospace), loaded from Google Fonts +- **Sizes:** 12px (Names), 9px (Sublabels), 8px (Annotations), 7px (Tiny labels) +- **Background:** Slate-950 (`#020617`) with a subtle 40px grid pattern + +```svg + + + + +``` + +## Technical Implementation Details + +### Component Rendering +Components are rounded rectangles (`rx="6"`) with 1.5px strokes. To prevent arrows from showing through semi-transparent fills, use a **double-rect masking technique**: +1. Draw an opaque background rect (`#0f172a`) +2. Draw the semi-transparent styled rect on top + +### Connection Rules +- **Z-Order:** Draw arrows *early* in the SVG (after the grid) so they render behind component boxes +- **Arrowheads:** Defined via SVG markers +- **Security Flows:** Use dashed lines in rose color (`#fb7185`) +- **Boundaries:** + - *Security Groups:* Dashed (`4,4`), rose color + - *Regions:* Large dashed (`8,4`), amber color, `rx="12"` + +### Spacing & Layout Logic +- **Standard Height:** 60px (Services); 80-120px (Large components) +- **Vertical Gap:** Minimum 40px between components +- **Message Buses:** Must be placed *in the gap* between services, not overlapping them +- **Legend Placement:** **CRITICAL.** Must be placed outside all boundary boxes. Calculate the lowest Y-coordinate of all boundaries and place the legend at least 20px below it. + +## Document Structure + +The generated HTML file follows a four-part layout: +1. **Header:** Title with a pulsing dot indicator and subtitle +2. **Main SVG:** The diagram contained within a rounded border card +3. **Summary Cards:** A grid of three cards below the diagram for high-level details +4. **Footer:** Minimal metadata + +### Info Card Pattern +```html +
+
+
+

Title

+
+
    +
  • • Item one
  • +
  • • Item two
  • +
+
+``` + +## Output Requirements +- **Single File:** One self-contained `.html` file +- **No External Dependencies:** All CSS and SVG must be inline (except Google Fonts) +- **No JavaScript:** Use pure CSS for any animations (like pulsing dots) +- **Compatibility:** Must render correctly in any modern web browser + +## Template Reference + +Load the full HTML template for the exact structure, CSS, and SVG component examples: + +``` +skill_view(name="architecture-diagram", file_path="templates/template.html") +``` + +The template contains working examples of every component type (frontend, backend, database, cloud, security), arrow styles (standard, dashed, curved), security groups, region boundaries, and the legend — use it as your structural reference when generating diagrams. diff --git a/website/docs/user-guide/skills/bundled/creative/creative-claude-design.md b/website/docs/user-guide/skills/bundled/creative/creative-claude-design.md index 8fa3c563bbf..bf6f4eafaa3 100644 --- a/website/docs/user-guide/skills/bundled/creative/creative-claude-design.md +++ b/website/docs/user-guide/skills/bundled/creative/creative-claude-design.md @@ -21,7 +21,7 @@ Design one-off HTML artifacts (landing, deck, prototype). | License | MIT | | Platforms | linux, macos, windows | | Tags | `design`, `html`, `prototype`, `ux`, `ui`, `creative`, `artifact`, `deck`, `motion`, `design-system` | -| Related skills | [`html-artifact`](/docs/user-guide/skills/bundled/creative/creative-html-artifact), [`design-md`](/docs/user-guide/skills/bundled/creative/creative-design-md), [`popular-web-designs`](/docs/user-guide/skills/bundled/creative/creative-popular-web-designs), [`excalidraw`](/docs/user-guide/skills/bundled/creative/creative-excalidraw) | +| Related skills | [`design-md`](/docs/user-guide/skills/bundled/creative/creative-design-md), [`popular-web-designs`](/docs/user-guide/skills/bundled/creative/creative-popular-web-designs), [`excalidraw`](/docs/user-guide/skills/bundled/creative/creative-excalidraw), [`architecture-diagram`](/docs/user-guide/skills/bundled/creative/creative-architecture-diagram) | ## Reference: full SKILL.md @@ -37,21 +37,19 @@ The goal is to preserve Claude Design's useful design behavior and taste while r **Before starting, check for other web-design skills like `popular-web-designs` (ready-to-paste design systems for Stripe, Linear, Vercel, Notion, etc.) and `design-md` (Google's DESIGN.md token spec format).** If the user wants a known brand's look, load `popular-web-designs` alongside this one and let it supply the visual vocabulary. If the deliverable is a token spec file rather than a rendered artifact, use `design-md` instead. Full decision table below. -## When To Use This Skill vs `html-artifact` vs `popular-web-designs` vs `design-md` +## When To Use This Skill vs `popular-web-designs` vs `design-md` -Several skills produce HTML — they do different jobs. Load the right one (or combine them): +Hermes has three design-related skills under `skills/creative/`. They do different jobs — load the right one (or combine them): | Skill | What it gives you | Use when the user wants... | |---|---|---| -| **claude-design** (this one) | Visual design *process and taste* — how to scope a brief, gather context, produce variants, verify a local HTML artifact, avoid AI-design slop | a from-scratch *designed* artifact (landing page, prototype, deck, component lab, motion study) where the look itself is the point and no specific brand or token system is dictated | -| **html-artifact** | A house style for *information* artifacts — explainers, plans, reports, code reviews, technical/educational diagrams, throwaway editors | to *explain / plan / report / diagram / review* something as a shareable HTML page — the content is the point, not bespoke visual design | +| **claude-design** (this one) | Design *process and taste* — how to scope a brief, gather context, produce variants, verify a local HTML artifact, avoid AI-design slop | a from-scratch designed artifact (landing page, prototype, deck, component lab, motion study) with no specific brand or token system dictated | | **popular-web-designs** | 54 ready-to-paste design systems — exact colors, typography, components, CSS values for sites like Stripe, Linear, Vercel, Notion, Airbnb | "make it look like Stripe / Linear / Vercel", a page styled after a known brand, or a visual starting point pulled from a real product | | **design-md** | Google's DESIGN.md spec format — author/validate/diff/export design-token files, WCAG contrast checking, Tailwind/DTCG export | a formal, persistent, machine-readable design-system *spec file* (tokens + rationale) that lives in a repo and gets consumed by agents over time | Rule of thumb: -- **Bespoke visual design, taste-driven artifact** → claude-design -- **Explain / plan / report / diagram as a shareable page** → html-artifact +- **Process + taste, one-off artifact** → claude-design - **Match a known brand's look** → popular-web-designs (and let claude-design drive the process) - **Author the tokens spec itself** → design-md diff --git a/website/docs/user-guide/skills/bundled/creative/creative-design-md.md b/website/docs/user-guide/skills/bundled/creative/creative-design-md.md index 687916eb2dc..a96723ddb7f 100644 --- a/website/docs/user-guide/skills/bundled/creative/creative-design-md.md +++ b/website/docs/user-guide/skills/bundled/creative/creative-design-md.md @@ -21,7 +21,7 @@ Author/validate/export Google's DESIGN.md token spec files. | License | MIT | | Platforms | linux, macos, windows | | Tags | `design`, `design-system`, `tokens`, `ui`, `accessibility`, `wcag`, `tailwind`, `dtcg`, `google` | -| Related skills | [`popular-web-designs`](/docs/user-guide/skills/bundled/creative/creative-popular-web-designs), [`claude-design`](/docs/user-guide/skills/bundled/creative/creative-claude-design), [`excalidraw`](/docs/user-guide/skills/bundled/creative/creative-excalidraw), [`html-artifact`](/docs/user-guide/skills/bundled/creative/creative-html-artifact) | +| Related skills | [`popular-web-designs`](/docs/user-guide/skills/bundled/creative/creative-popular-web-designs), [`claude-design`](/docs/user-guide/skills/bundled/creative/creative-claude-design), [`excalidraw`](/docs/user-guide/skills/bundled/creative/creative-excalidraw), [`architecture-diagram`](/docs/user-guide/skills/bundled/creative/creative-architecture-diagram) | ## Reference: full SKILL.md diff --git a/website/docs/user-guide/skills/bundled/creative/creative-html-artifact.md b/website/docs/user-guide/skills/bundled/creative/creative-html-artifact.md deleted file mode 100644 index 0f34348ef2e..00000000000 --- a/website/docs/user-guide/skills/bundled/creative/creative-html-artifact.md +++ /dev/null @@ -1,202 +0,0 @@ ---- -title: "Html Artifact — Build self-contained HTML files to explain, plan, or review" -sidebar_label: "Html Artifact" -description: "Build self-contained HTML files to explain, plan, or review" ---- - -{/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */} - -# Html Artifact - -Build self-contained HTML files to explain, plan, or review. - -## Skill metadata - -| | | -|---|---| -| Source | Bundled (installed by default) | -| Path | `skills/creative/html-artifact` | -| Version | `1.0.0` | -| Author | Anthropic (html-effectiveness gallery, MIT), adapted for Hermes Agent | -| License | MIT | -| Platforms | linux, macos, windows | -| Tags | `html`, `artifact`, `explainer`, `plan`, `report`, `code-review`, `diagram`, `svg`, `design`, `prototype`, `editor` | -| Related skills | [`claude-design`](/docs/user-guide/skills/bundled/creative/creative-claude-design), [`popular-web-designs`](/docs/user-guide/skills/bundled/creative/creative-popular-web-designs), [`design-md`](/docs/user-guide/skills/bundled/creative/creative-design-md), [`excalidraw`](/docs/user-guide/skills/bundled/creative/creative-excalidraw), [`p5js`](/docs/user-guide/skills/bundled/creative/creative-p5js) | - -## Reference: full SKILL.md - -:::info -The following is the complete skill definition that Hermes loads when this skill is triggered. This is what the agent sees as instructions when the skill is active. -::: - -# HTML Artifact Skill - -Produce a single self-contained `.html` file — no build step, no dependencies, no -CDN — whenever the deliverable is something a human should *read, share, or poke at*: -a concept explainer, an implementation plan, a status/incident report, a code-review -walkthrough, a technical or educational diagram, a set of design variants, or a -throwaway editor that exports its result back to you. - -HTML beats Markdown once a doc has color, layout, diagrams, tables, code, or -interaction. It opens in any browser, shares as a link, stays readable past 100 -lines, and can carry SVG diagrams and live controls Markdown can't. Default to an -HTML artifact when the user says "make an HTML file/artifact", or asks you to -*explain how X works*, *write up a plan/PR/report*, *diagram* something, *compare* -options, or *prototype* an interaction — even when they don't say "HTML". - -## Why this skill exists (and what it replaced) - -This skill **supersedes** three former skills — `sketch` (throwaway multi-variant -HTML mockups), `architecture-diagram` (dark-tech infra SVG), and `concept-diagrams` -(educational SVG). They were consolidated for a concrete reason: all three emitted -the *same artifact* — a single self-contained HTML file with inline CSS/SVG — and -overlapped heavily (three "diagram" skills, two "compare variants" paths, no shared -token system). Folding them into one mode-switched skill removes the -which-one-do-I-load ambiguity and gives every output the same house style, while -keeping each skill's unique value: the fidelity dial + verify loop (from `sketch`), -the dark infra aesthetic (from `architecture-diagram`), and the 9-ramp educational -system + archetype library (from `concept-diagrams`). - -The consolidation is footprint-safe: this skill has **zero dependencies** (no Node, -FFmpeg, Chromium, or pip packages — it authors plain HTML/CSS/SVG), so even though it -ships **bundled** (active by default) where `concept-diagrams` was optional, the only -always-in-context cost is this skill's one-line description. All references, -templates, and the example gallery load on demand. `concept-diagrams` was optional -because it was niche, not because it had an install cost — promoting that capability -into a general-purpose, zero-dep bundled skill is the right home for it. Diagram-style -work with a *real* install cost (e.g. `hyperframes`: Node + FFmpeg + Chromium) -deliberately stays optional and is **not** folded in here. - -Use a different skill when: matching a known brand's look → `popular-web-designs`; a -formal design-token spec file → `design-md`; a *bespoke visually-designed* artifact -where the look itself is the point → `claude-design`; hand-drawn/whiteboard -`.excalidraw` files → `excalidraw`; generative/animated canvas art → `p5js`. This -skill is for everything else that ships as a readable, shareable HTML page. - -## Reference files (load on demand) - -- `references/house-style.md` — the canonical `:root` token block, type system, - card/table/callout/code-block patterns. **Read this before authoring any artifact.** -- `references/examples.md` — 20 complete reference HTML files (Anthropic's - html-effectiveness gallery, MIT) keyed to each mode, plus the script to fetch them. - Read/fetch one that matches your task to calibrate the house style from a full example. -- `references/svg-diagrams.md` — hand-authored inline SVG: arrow markers, node - groups, decision diamonds, edge semantics, coordinate-grid discipline. Read for - any flowchart / architecture / concept diagram. -- `references/concept-archetypes.md` — the 9-ramp educational color system + a - library of diagram archetypes (timeline, tree, quadrant, layered stack, - before/after, hub-spoke, cross-section). Read for educational / non-software visuals. -- `references/dark-tech.md` — the dark "infra" token variant (carries the old - architecture-diagram aesthetic). Read for cloud/infra/system architecture diagrams. -- `references/throwaway-editors.md` — the single-file editor recipe and the - copy-to-clipboard export pattern that survives `file://`. Read when the artifact - needs interactive controls that export state back to a prompt. -- `references/fidelity-and-verify.md` — the throwaway↔presentation fidelity dial, - the multi-variant comparison layout, and the mandatory browser-vision verify loop. - -## Templates - -- `templates/base.html` — document scaffold with the house-style ` +``` + +### 4. Variant README + +Each variant's `README.md` answers: + +```markdown +## Variant: {stance name} + +### Design stance +One sentence on the principle driving this variant. + +### Key choices +- Layout: ... +- Typography: ... +- Color: ... +- Interaction: ... + +### Trade-offs +- Strong at: ... +- Weak at: ... + +### Best for +- The kind of user or use case this variant actually serves +``` + +### 5. Head-to-head + +After all variants are built, present them as a comparison. Don't just list — **opinionate**: + +```markdown +## Three takes on the home screen + +| Dimension | Calm editorial | Utilitarian dense | Playful split | +|-----------|----------------|-------------------|---------------| +| Density | Low | High | Medium | +| Primary action visibility | Low | High | Medium | +| Scan-ability | High | Medium | Low | +| Feel | Calm, trusted | Sharp, tool-like | Inviting, energetic | + +**My take:** Utilitarian dense for power users, calm editorial for content-forward audiences. Playful split is weakest — tries to do both and commits to neither. +``` + +Let the user pick a winner, or combine two into a hybrid, or ask for another round. + +## Theming (when the project has a visual identity) + +If the user has an existing theme (colors, fonts, tokens), put shared tokens in `sketches/themes/tokens.css` and `@import` them in each variant. Keep tokens minimal: + +```css +/* sketches/themes/tokens.css */ +:root { + --color-bg: #fafafa; + --color-fg: #1a1a1a; + --color-accent: #0066ff; + --color-muted: #666; + --radius: 8px; + --font-display: "Inter", sans-serif; + --font-body: -apple-system, BlinkMacSystemFont, sans-serif; +} +``` + +Don't over-tokenize a throwaway sketch — three colors and one font is usually enough. + +## Interactivity bar + +A sketch is interactive enough when the user can: + +1. **Click a primary action** and something visible happens (state change, modal, toast, navigation feint) +2. **See one meaningful state transition** (filter a list, toggle a mode, open/close a panel) +3. **Hover recognizable affordances** (buttons, rows, tabs) + +More than that is over-engineering a throwaway. Less than that is a screenshot. + +## Frontier mode (picking what to sketch next) + +If sketches already exist and the user says "what should I sketch next?": + +- **Consistency gaps** — two winning variants from different sketches made independent choices that haven't been composed together yet +- **Unsketched screens** — referenced but never explored +- **State coverage** — happy path sketched, but not empty / loading / error / 1000-items +- **Responsive gaps** — validated at one viewport; does it hold at mobile / ultrawide? +- **Interaction patterns** — static layouts exist; transitions, drag, scroll behavior don't + +Propose 2-4 named candidates. Let the user pick. + +## Output + +- Create `sketches/` (or `.planning/sketches/` if the user is using GSD conventions) in the repo root +- One subdir per variant: `NNN-stance-name/index.html` + `README.md` +- Tell the user how to open them: `open sketches/001-calm-editorial/index.html` on macOS, `xdg-open` on Linux, `start` on Windows +- Keep variants disposable — a sketch that you felt the need to preserve should be promoted into real project code, not curated as an asset + +**Typical tool sequence for one variant:** + +``` +terminal("mkdir -p sketches/001-calm-editorial") +write_file("sketches/001-calm-editorial/index.html", "...") +write_file("sketches/001-calm-editorial/README.md", "## Variant: Calm editorial\n...") +browser_navigate(url="file://$(pwd)/sketches/001-calm-editorial/index.html") +browser_vision(question="How does this look? Any obvious layout issues?") +``` + +Repeat for each variant, then present the comparison table. + +## Attribution + +Adapted from the GSD (Get Shit Done) project's `/gsd-sketch` workflow — MIT © 2025 Lex Christopherson ([gsd-build/get-shit-done](https://github.com/gsd-build/get-shit-done)). The full GSD system ships persistent sketch state, theme/variant pattern references, and consistency-audit workflows; install with `npx get-shit-done-cc --hermes --global`. diff --git a/website/docs/user-guide/skills/bundled/creative/creative-touchdesigner-mcp.md b/website/docs/user-guide/skills/bundled/creative/creative-touchdesigner-mcp.md index 9a14bceffd9..2577f1f741c 100644 --- a/website/docs/user-guide/skills/bundled/creative/creative-touchdesigner-mcp.md +++ b/website/docs/user-guide/skills/bundled/creative/creative-touchdesigner-mcp.md @@ -21,7 +21,7 @@ Control a running TouchDesigner instance via twozero MCP — create operators, s | License | MIT | | Platforms | linux, macos, windows | | Tags | `TouchDesigner`, `MCP`, `twozero`, `creative-coding`, `real-time-visuals`, `generative-art`, `audio-reactive`, `VJ`, `installation`, `GLSL` | -| Related skills | `native-mcp`, [`ascii-video`](/docs/user-guide/skills/bundled/creative/creative-ascii-video), [`manim-video`](/docs/user-guide/skills/bundled/creative/creative-manim-video), `hermes-video` | +| Related skills | [`native-mcp`](/docs/user-guide/skills/bundled/mcp/mcp-native-mcp), [`ascii-video`](/docs/user-guide/skills/bundled/creative/creative-ascii-video), [`manim-video`](/docs/user-guide/skills/bundled/creative/creative-manim-video), `hermes-video` | ## Reference: full SKILL.md diff --git a/website/docs/user-guide/skills/bundled/email/email-himalaya.md b/website/docs/user-guide/skills/bundled/email/email-himalaya.md index 34c868e9f26..adf3d973635 100644 --- a/website/docs/user-guide/skills/bundled/email/email-himalaya.md +++ b/website/docs/user-guide/skills/bundled/email/email-himalaya.md @@ -32,11 +32,6 @@ The following is the complete skill definition that Hermes loads when this skill Himalaya is a CLI email client that lets you manage emails from the terminal using IMAP, SMTP, Notmuch, or Sendmail backends. -This skill is separate from the Hermes Email gateway adapter. The gateway -adapter lets people email the agent and uses Hermes' built-in IMAP/SMTP -adapter; this skill lets the agent operate a mailbox from terminal tools and -requires the external `himalaya` CLI. - ## References - `references/configuration.md` (config file setup + IMAP/SMTP authentication) diff --git a/website/docs/user-guide/skills/bundled/github/github-github-auth.md b/website/docs/user-guide/skills/bundled/github/github-github-auth.md index 35e631fb237..92b9d9f6690 100644 --- a/website/docs/user-guide/skills/bundled/github/github-github-auth.md +++ b/website/docs/user-guide/skills/bundled/github/github-github-auth.md @@ -238,8 +238,8 @@ if command -v gh &>/dev/null && gh auth status &>/dev/null; then echo "AUTH_METHOD=gh" elif [ -n "$GITHUB_TOKEN" ]; then echo "AUTH_METHOD=curl" -elif _hermes_env="${HERMES_HOME:-$HOME/.hermes}/.env"; [ -f "$_hermes_env" ] && grep -q "^GITHUB_TOKEN=" "$_hermes_env"; then - export GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" "$_hermes_env" | head -1 | cut -d= -f2 | tr -d '\n\r') +elif [ -f ~/.hermes/.env ] && grep -q "^GITHUB_TOKEN=" ~/.hermes/.env; then + export GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" ~/.hermes/.env | head -1 | cut -d= -f2 | tr -d '\n\r') echo "AUTH_METHOD=curl" elif grep -q "github.com" ~/.git-credentials 2>/dev/null; then export GITHUB_TOKEN=$(grep "github.com" ~/.git-credentials | head -1 | sed 's|https://[^:]*:\([^@]*\)@.*|\1|') diff --git a/website/docs/user-guide/skills/bundled/github/github-github-code-review.md b/website/docs/user-guide/skills/bundled/github/github-github-code-review.md index a7adc59e119..56e8fa97ad2 100644 --- a/website/docs/user-guide/skills/bundled/github/github-github-code-review.md +++ b/website/docs/user-guide/skills/bundled/github/github-github-code-review.md @@ -46,8 +46,8 @@ if command -v gh &>/dev/null && gh auth status &>/dev/null; then else AUTH="git" if [ -z "$GITHUB_TOKEN" ]; then - if _hermes_env="${HERMES_HOME:-$HOME/.hermes}/.env"; [ -f "$_hermes_env" ] && grep -q "^GITHUB_TOKEN=" "$_hermes_env"; then - GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" "$_hermes_env" | head -1 | cut -d= -f2 | tr -d '\n\r') + if [ -f ~/.hermes/.env ] && grep -q "^GITHUB_TOKEN=" ~/.hermes/.env; then + GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" ~/.hermes/.env | head -1 | cut -d= -f2 | tr -d '\n\r') elif grep -q "github.com" ~/.git-credentials 2>/dev/null; then GITHUB_TOKEN=$(grep "github.com" ~/.git-credentials 2>/dev/null | head -1 | sed 's|https://[^:]*:\([^@]*\)@.*|\1|') fi diff --git a/website/docs/user-guide/skills/bundled/github/github-github-issues.md b/website/docs/user-guide/skills/bundled/github/github-github-issues.md index fa3dc52c7e2..6f99685d71a 100644 --- a/website/docs/user-guide/skills/bundled/github/github-github-issues.md +++ b/website/docs/user-guide/skills/bundled/github/github-github-issues.md @@ -46,8 +46,8 @@ if command -v gh &>/dev/null && gh auth status &>/dev/null; then else AUTH="git" if [ -z "$GITHUB_TOKEN" ]; then - if _hermes_env="${HERMES_HOME:-$HOME/.hermes}/.env"; [ -f "$_hermes_env" ] && grep -q "^GITHUB_TOKEN=" "$_hermes_env"; then - GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" "$_hermes_env" | head -1 | cut -d= -f2 | tr -d '\n\r') + if [ -f ~/.hermes/.env ] && grep -q "^GITHUB_TOKEN=" ~/.hermes/.env; then + GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" ~/.hermes/.env | head -1 | cut -d= -f2 | tr -d '\n\r') elif grep -q "github.com" ~/.git-credentials 2>/dev/null; then GITHUB_TOKEN=$(grep "github.com" ~/.git-credentials 2>/dev/null | head -1 | sed 's|https://[^:]*:\([^@]*\)@.*|\1|') fi diff --git a/website/docs/user-guide/skills/bundled/github/github-github-pr-workflow.md b/website/docs/user-guide/skills/bundled/github/github-github-pr-workflow.md index a0221be3d73..48aa4ea9fff 100644 --- a/website/docs/user-guide/skills/bundled/github/github-github-pr-workflow.md +++ b/website/docs/user-guide/skills/bundled/github/github-github-pr-workflow.md @@ -48,8 +48,8 @@ else AUTH="git" # Ensure we have a token for API calls if [ -z "$GITHUB_TOKEN" ]; then - if _hermes_env="${HERMES_HOME:-$HOME/.hermes}/.env"; [ -f "$_hermes_env" ] && grep -q "^GITHUB_TOKEN=" "$_hermes_env"; then - GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" "$_hermes_env" | head -1 | cut -d= -f2 | tr -d '\n\r') + if [ -f ~/.hermes/.env ] && grep -q "^GITHUB_TOKEN=" ~/.hermes/.env; then + GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" ~/.hermes/.env | head -1 | cut -d= -f2 | tr -d '\n\r') elif grep -q "github.com" ~/.git-credentials 2>/dev/null; then GITHUB_TOKEN=$(grep "github.com" ~/.git-credentials 2>/dev/null | head -1 | sed 's|https://[^:]*:\([^@]*\)@.*|\1|') fi diff --git a/website/docs/user-guide/skills/bundled/github/github-github-repo-management.md b/website/docs/user-guide/skills/bundled/github/github-github-repo-management.md index b87a7abdf37..0921e3dbccc 100644 --- a/website/docs/user-guide/skills/bundled/github/github-github-repo-management.md +++ b/website/docs/user-guide/skills/bundled/github/github-github-repo-management.md @@ -45,8 +45,8 @@ if command -v gh &>/dev/null && gh auth status &>/dev/null; then else AUTH="git" if [ -z "$GITHUB_TOKEN" ]; then - if _hermes_env="${HERMES_HOME:-$HOME/.hermes}/.env"; [ -f "$_hermes_env" ] && grep -q "^GITHUB_TOKEN=" "$_hermes_env"; then - GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" "$_hermes_env" | head -1 | cut -d= -f2 | tr -d '\n\r') + if [ -f ~/.hermes/.env ] && grep -q "^GITHUB_TOKEN=" ~/.hermes/.env; then + GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" ~/.hermes/.env | head -1 | cut -d= -f2 | tr -d '\n\r') elif grep -q "github.com" ~/.git-credentials 2>/dev/null; then GITHUB_TOKEN=$(grep "github.com" ~/.git-credentials 2>/dev/null | head -1 | sed 's|https://[^:]*:\([^@]*\)@.*|\1|') fi diff --git a/website/docs/user-guide/skills/bundled/media/media-gif-search.md b/website/docs/user-guide/skills/bundled/media/media-gif-search.md index 31d0e03eb88..c26c5fd4a5e 100644 --- a/website/docs/user-guide/skills/bundled/media/media-gif-search.md +++ b/website/docs/user-guide/skills/bundled/media/media-gif-search.md @@ -38,7 +38,7 @@ Useful for finding reaction GIFs, creating visual content, and sending GIFs in c ## Setup -Set your Tenor API key in your environment (add to `${HERMES_HOME:-~/.hermes}/.env`): +Set your Tenor API key in your environment (add to `~/.hermes/.env`): ```bash TENOR_API_KEY=your_key_here diff --git a/website/docs/user-guide/skills/bundled/note-taking/note-taking-obsidian.md b/website/docs/user-guide/skills/bundled/note-taking/note-taking-obsidian.md index 49f317144d7..e8315c2fd4f 100644 --- a/website/docs/user-guide/skills/bundled/note-taking/note-taking-obsidian.md +++ b/website/docs/user-guide/skills/bundled/note-taking/note-taking-obsidian.md @@ -32,7 +32,7 @@ Use this skill for filesystem-first Obsidian vault work: reading notes, listing Use a known or resolved vault path before calling file tools. -The documented vault-path convention is the `OBSIDIAN_VAULT_PATH` environment variable, for example from `${HERMES_HOME:-~/.hermes}/.env`. If it is unset, use `~/Documents/Obsidian Vault`. +The documented vault-path convention is the `OBSIDIAN_VAULT_PATH` environment variable, for example from `~/.hermes/.env`. If it is unset, use `~/Documents/Obsidian Vault`. File tools do not expand shell variables. Do not pass paths containing `$OBSIDIAN_VAULT_PATH` to `read_file`, `write_file`, `patch`, or `search_files`; resolve the vault path first and pass a concrete absolute path. Vault paths may contain spaces, which is another reason to prefer file tools over shell commands. diff --git a/website/docs/user-guide/skills/bundled/productivity/productivity-airtable.md b/website/docs/user-guide/skills/bundled/productivity/productivity-airtable.md index 05a3e13fba0..bc4b4686433 100644 --- a/website/docs/user-guide/skills/bundled/productivity/productivity-airtable.md +++ b/website/docs/user-guide/skills/bundled/productivity/productivity-airtable.md @@ -40,7 +40,7 @@ Work with Airtable's REST API directly via `curl` using the `terminal` tool. No - `data.records:write` — create / update / delete rows - `schema.bases:read` — list bases and tables 3. **Important:** in the same token UI, add each base you want to access to the token's **Access** list. PATs are scoped per-base — a valid token on the wrong base returns `403`. -4. Store the token in `${HERMES_HOME:-~/.hermes}/.env` (or via `hermes setup`): +4. Store the token in `~/.hermes/.env` (or via `hermes setup`): ``` AIRTABLE_API_KEY=pat_your_token_here ``` @@ -236,7 +236,7 @@ done ## Important Notes for Hermes - **Always use the `terminal` tool with `curl`.** Do NOT use `web_extract` (it can't send auth headers) or `browser_navigate` (needs UI auth and is slow). -- **`AIRTABLE_API_KEY` flows from `${HERMES_HOME:-~/.hermes}/.env` into the subprocess automatically** when this skill is loaded — no need to re-export it before each `curl` call. +- **`AIRTABLE_API_KEY` flows from `~/.hermes/.env` into the subprocess automatically** when this skill is loaded — no need to re-export it before each `curl` call. - **Escape curly braces in formulas carefully.** In a heredoc body, `{Status}` is literal. In a shell argument, `{Status}` is safe outside `{...}` brace-expansion context — but pass dynamic strings through `python3 urllib.parse.quote` before splicing into a URL. - **Pretty-print with `python3 -m json.tool`** (always present) rather than `jq` (optional). Only reach for `jq` when you need filtering/projection. - **Pagination is per-page, not global.** Airtable's 100-record cap is a hard limit; there is no way to bump it. Loop with `offset` until the field is absent. diff --git a/website/docs/user-guide/skills/bundled/productivity/productivity-notion.md b/website/docs/user-guide/skills/bundled/productivity/productivity-notion.md index 985240ca41f..80487d6b88f 100644 --- a/website/docs/user-guide/skills/bundled/productivity/productivity-notion.md +++ b/website/docs/user-guide/skills/bundled/productivity/productivity-notion.md @@ -41,7 +41,7 @@ Talk to Notion two ways. Same integration token works for both — pick by what' 1. Create an integration at https://notion.so/my-integrations 2. Copy the API key (starts with `ntn_` or `secret_`) -3. Store in `${HERMES_HOME:-~/.hermes}/.env`: +3. Store in `~/.hermes/.env`: ``` NOTION_API_KEY=ntn_your_key_here ``` @@ -65,7 +65,7 @@ export NOTION_API_TOKEN=$NOTION_API_KEY # ntn reads NOTION_API_TOKEN export NOTION_KEYRING=0 # don't try to use the OS keychain ``` -Add those exports to your shell profile (or to `${HERMES_HOME:-~/.hermes}/.env`) so every session inherits them. +Add those exports to your shell profile (or to `~/.hermes/.env`) so every session inherits them. ### 3. Choose path at runtime diff --git a/website/docs/user-guide/skills/bundled/productivity/productivity-teams-meeting-pipeline.md b/website/docs/user-guide/skills/bundled/productivity/productivity-teams-meeting-pipeline.md index 8fb4c066302..125021bc4cb 100644 --- a/website/docs/user-guide/skills/bundled/productivity/productivity-teams-meeting-pipeline.md +++ b/website/docs/user-guide/skills/bundled/productivity/productivity-teams-meeting-pipeline.md @@ -50,7 +50,7 @@ Multilingual trigger examples (not exhaustive): ## Prerequisites -Before using the pipeline, verify these are set in `${HERMES_HOME:-~/.hermes}/.env`: +Before using the pipeline, verify these are set in `~/.hermes/.env`: ```bash MSGRAPH_TENANT_ID=... diff --git a/website/docs/user-guide/skills/bundled/research/research-llm-wiki.md b/website/docs/user-guide/skills/bundled/research/research-llm-wiki.md index a6097a1a07c..419c7cd7cb2 100644 --- a/website/docs/user-guide/skills/bundled/research/research-llm-wiki.md +++ b/website/docs/user-guide/skills/bundled/research/research-llm-wiki.md @@ -52,7 +52,7 @@ Use this skill when the user: ## Wiki Location -**Location:** Set via `WIKI_PATH` environment variable (e.g. in `${HERMES_HOME:-~/.hermes}/.env`). +**Location:** Set via `WIKI_PATH` environment variable (e.g. in `~/.hermes/.env`). If unset, defaults to `~/wiki`. diff --git a/website/docs/user-guide/skills/bundled/research/research-research-paper-writing.md b/website/docs/user-guide/skills/bundled/research/research-research-paper-writing.md index 611215c06c3..9dc216ebac7 100644 --- a/website/docs/user-guide/skills/bundled/research/research-research-paper-writing.md +++ b/website/docs/user-guide/skills/bundled/research/research-research-paper-writing.md @@ -22,7 +22,7 @@ Write ML papers for NeurIPS/ICML/ICLR: design→submit. | Dependencies | `semanticscholar`, `arxiv`, `habanero`, `requests`, `scipy`, `numpy`, `matplotlib`, `SciencePlots` | | Platforms | linux, macos | | Tags | `Research`, `Paper Writing`, `Experiments`, `ML`, `AI`, `NeurIPS`, `ICML`, `ICLR`, `ACL`, `AAAI`, `COLM`, `LaTeX`, `Citations`, `Statistical Analysis` | -| Related skills | [`arxiv`](/docs/user-guide/skills/bundled/research/research-arxiv), `ml-paper-writing`, [`subagent-driven-development`](/docs/user-guide/skills/optional/software-development/software-development-subagent-driven-development), [`plan`](/docs/user-guide/skills/bundled/software-development/software-development-plan) | +| Related skills | [`arxiv`](/docs/user-guide/skills/bundled/research/research-arxiv), `ml-paper-writing`, [`subagent-driven-development`](/docs/user-guide/skills/bundled/software-development/software-development-subagent-driven-development), [`plan`](/docs/user-guide/skills/bundled/software-development/software-development-plan) | ## Reference: full SKILL.md diff --git a/website/docs/user-guide/skills/bundled/software-development/software-development-node-inspect-debugger.md b/website/docs/user-guide/skills/bundled/software-development/software-development-node-inspect-debugger.md index 5257512e9e6..deddf5dafdb 100644 --- a/website/docs/user-guide/skills/bundled/software-development/software-development-node-inspect-debugger.md +++ b/website/docs/user-guide/skills/bundled/software-development/software-development-node-inspect-debugger.md @@ -21,7 +21,7 @@ Debug Node.js via --inspect + Chrome DevTools Protocol CLI. | License | MIT | | Platforms | linux, macos, windows | | Tags | `debugging`, `nodejs`, `node-inspect`, `cdp`, `breakpoints`, `ui-tui` | -| Related skills | [`systematic-debugging`](/docs/user-guide/skills/bundled/software-development/software-development-systematic-debugging), [`python-debugpy`](/docs/user-guide/skills/bundled/software-development/software-development-python-debugpy), `debugging-hermes-tui-commands` | +| Related skills | [`systematic-debugging`](/docs/user-guide/skills/bundled/software-development/software-development-systematic-debugging), [`python-debugpy`](/docs/user-guide/skills/bundled/software-development/software-development-python-debugpy), [`debugging-hermes-tui-commands`](/docs/user-guide/skills/bundled/software-development/software-development-debugging-hermes-tui-commands) | ## Reference: full SKILL.md diff --git a/website/docs/user-guide/skills/bundled/software-development/software-development-python-debugpy.md b/website/docs/user-guide/skills/bundled/software-development/software-development-python-debugpy.md index dbc26409efe..0524b1f3ab9 100644 --- a/website/docs/user-guide/skills/bundled/software-development/software-development-python-debugpy.md +++ b/website/docs/user-guide/skills/bundled/software-development/software-development-python-debugpy.md @@ -21,7 +21,7 @@ Debug Python: pdb REPL + debugpy remote (DAP). | License | MIT | | Platforms | linux, macos | | Tags | `debugging`, `python`, `pdb`, `debugpy`, `breakpoints`, `dap`, `post-mortem` | -| Related skills | [`systematic-debugging`](/docs/user-guide/skills/bundled/software-development/software-development-systematic-debugging), [`node-inspect-debugger`](/docs/user-guide/skills/bundled/software-development/software-development-node-inspect-debugger), `debugging-hermes-tui-commands` | +| Related skills | [`systematic-debugging`](/docs/user-guide/skills/bundled/software-development/software-development-systematic-debugging), [`node-inspect-debugger`](/docs/user-guide/skills/bundled/software-development/software-development-node-inspect-debugger), [`debugging-hermes-tui-commands`](/docs/user-guide/skills/bundled/software-development/software-development-debugging-hermes-tui-commands) | ## Reference: full SKILL.md diff --git a/website/docs/user-guide/skills/bundled/software-development/software-development-spike.md b/website/docs/user-guide/skills/bundled/software-development/software-development-spike.md index 694cdcbf7af..56c0954b698 100644 --- a/website/docs/user-guide/skills/bundled/software-development/software-development-spike.md +++ b/website/docs/user-guide/skills/bundled/software-development/software-development-spike.md @@ -21,7 +21,7 @@ Throwaway experiments to validate an idea before build. | License | MIT | | Platforms | linux, macos, windows | | Tags | `spike`, `prototype`, `experiment`, `feasibility`, `throwaway`, `exploration`, `research`, `planning`, `mvp`, `proof-of-concept` | -| Related skills | [`html-artifact`](/docs/user-guide/skills/bundled/creative/creative-html-artifact), [`subagent-driven-development`](/docs/user-guide/skills/optional/software-development/software-development-subagent-driven-development), [`plan`](/docs/user-guide/skills/bundled/software-development/software-development-plan) | +| Related skills | [`sketch`](/docs/user-guide/skills/bundled/creative/creative-sketch), [`subagent-driven-development`](/docs/user-guide/skills/optional/software-development/software-development-subagent-driven-development), [`plan`](/docs/user-guide/skills/bundled/software-development/software-development-plan) | ## Reference: full SKILL.md diff --git a/website/docs/user-guide/skills/optional/autonomous-ai-agents/autonomous-ai-agents-honcho.md b/website/docs/user-guide/skills/optional/autonomous-ai-agents/autonomous-ai-agents-honcho.md index a54a2a0dea0..1b989116636 100644 --- a/website/docs/user-guide/skills/optional/autonomous-ai-agents/autonomous-ai-agents-honcho.md +++ b/website/docs/user-guide/skills/optional/autonomous-ai-agents/autonomous-ai-agents-honcho.md @@ -47,14 +47,14 @@ Honcho provides AI-native cross-session user modeling. It learns who the user is ### Cloud (app.honcho.dev) ```bash -hermes memory setup honcho +hermes honcho setup # select "cloud", paste API key from https://app.honcho.dev ``` ### Self-hosted ```bash -hermes memory setup honcho +hermes honcho setup # select "local", enter base URL (e.g. http://localhost:8000) ``` diff --git a/website/docs/user-guide/skills/optional/blockchain/blockchain-hyperliquid.md b/website/docs/user-guide/skills/optional/blockchain/blockchain-hyperliquid.md index 177dfe36a10..8651bc979f6 100644 --- a/website/docs/user-guide/skills/optional/blockchain/blockchain-hyperliquid.md +++ b/website/docs/user-guide/skills/optional/blockchain/blockchain-hyperliquid.md @@ -53,7 +53,7 @@ Read-only — no API key, no signing, no order placement. Stdlib only — no external packages, no API key. -The script reads `${HERMES_HOME:-~/.hermes}/.env` for two optional defaults: +The script reads `~/.hermes/.env` for two optional defaults: - `HYPERLIQUID_API_URL` — defaults to `https://api.hyperliquid.xyz`. Set to `https://api.hyperliquid-testnet.xyz` for testnet. @@ -97,7 +97,7 @@ hyperliquid_client.py export [--interval 1h] [--hours N] [--output PATH] ``` For `state`, `spot-balances`, `fills`, `orders`, and `review`, the address is -optional when `HYPERLIQUID_USER_ADDRESS` is set in `${HERMES_HOME:-~/.hermes}/.env`. +optional when `HYPERLIQUID_USER_ADDRESS` is set in `~/.hermes/.env`. --- diff --git a/website/docs/user-guide/skills/optional/creative/creative-concept-diagrams.md b/website/docs/user-guide/skills/optional/creative/creative-concept-diagrams.md new file mode 100644 index 00000000000..9b3ba92b3bd --- /dev/null +++ b/website/docs/user-guide/skills/optional/creative/creative-concept-diagrams.md @@ -0,0 +1,379 @@ +--- +title: "Concept Diagrams" +sidebar_label: "Concept Diagrams" +description: "Generate flat, minimal light/dark-aware SVG diagrams as standalone HTML files, using a unified educational visual language with 9 semantic color ramps, sente..." +--- + +{/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */} + +# Concept Diagrams + +Generate flat, minimal light/dark-aware SVG diagrams as standalone HTML files, using a unified educational visual language with 9 semantic color ramps, sentence-case typography, and automatic dark mode. Best suited for educational and non-software visuals — physics setups, chemistry mechanisms, math curves, physical objects (aircraft, turbines, smartphones, mechanical watches), anatomy, floor plans, cross-sections, narrative journeys (lifecycle of X, process of Y), hub-spoke system integrations (smart city, IoT), and exploded layer views. If a more specialized skill exists for the subject (dedicated software/cloud architecture, hand-drawn sketches, animated explainers, etc.), prefer that — otherwise this skill can also serve as a general-purpose SVG diagram fallback with a clean educational look. Ships with 15 example diagrams. + +## Skill metadata + +| | | +|---|---| +| Source | Optional — install with `hermes skills install official/creative/concept-diagrams` | +| Path | `optional-skills/creative/concept-diagrams` | +| Version | `0.1.0` | +| Author | v1k22 (original PR), ported into hermes-agent | +| License | MIT | +| Platforms | linux, macos, windows | +| Tags | `diagrams`, `svg`, `visualization`, `education`, `physics`, `chemistry`, `engineering` | +| Related skills | [`architecture-diagram`](/docs/user-guide/skills/bundled/creative/creative-architecture-diagram), [`excalidraw`](/docs/user-guide/skills/bundled/creative/creative-excalidraw), `generative-widgets` | + +## Reference: full SKILL.md + +:::info +The following is the complete skill definition that Hermes loads when this skill is triggered. This is what the agent sees as instructions when the skill is active. +::: + +# Concept Diagrams + +Generate production-quality SVG diagrams with a unified flat, minimal design system. Output is a single self-contained HTML file that renders identically in any modern browser, with automatic light/dark mode. + +## Scope + +**Best suited for:** +- Physics setups, chemistry mechanisms, math curves, biology +- Physical objects (aircraft, turbines, smartphones, mechanical watches, cells) +- Anatomy, cross-sections, exploded layer views +- Floor plans, architectural conversions +- Narrative journeys (lifecycle of X, process of Y) +- Hub-spoke system integrations (smart city, IoT networks, electricity grids) +- Educational / textbook-style visuals in any domain +- Quantitative charts (grouped bars, energy profiles) + +**Look elsewhere first for:** +- Dedicated software / cloud infrastructure architecture with a dark tech aesthetic (consider `architecture-diagram` if available) +- Hand-drawn whiteboard sketches (consider `excalidraw` if available) +- Animated explainers or video output (consider an animation skill) + +If a more specialized skill is available for the subject, prefer that. If none fits, this skill can serve as a general-purpose SVG diagram fallback — the output will carry the clean educational aesthetic described below, which is a reasonable default for almost any subject. + +## Workflow + +1. Decide on the diagram type (see Diagram Types below). +2. Lay out components using the Design System rules. +3. Write the full HTML page using `templates/template.html` as the wrapper — paste your SVG where the template says ``. +4. Save as a standalone `.html` file (for example `~/my-diagram.html` or `./my-diagram.html`). +5. User opens it directly in a browser — no server, no dependencies. + +Optional: if the user wants a browsable gallery of multiple diagrams, see "Local Preview Server" at the bottom. + +Load the HTML template: +``` +skill_view(name="concept-diagrams", file_path="templates/template.html") +``` + +The template embeds the full CSS design system (`c-*` color classes, text classes, light/dark variables, arrow marker styles). The SVG you generate relies on these classes being present on the hosting page. + +--- + +## Design System + +### Philosophy + +- **Flat**: no gradients, drop shadows, blur, glow, or neon effects. +- **Minimal**: show the essential. No decorative icons inside boxes. +- **Consistent**: same colors, spacing, typography, and stroke widths across every diagram. +- **Dark-mode ready**: all colors auto-adapt via CSS classes — no per-mode SVG. + +### Color Palette + +9 color ramps, each with 7 stops. Put the class name on a `` or shape element; the template CSS handles both modes. + +| Class | 50 (lightest) | 100 | 200 | 400 | 600 | 800 | 900 (darkest) | +|------------|---------------|---------|---------|---------|---------|---------|---------------| +| `c-purple` | #EEEDFE | #CECBF6 | #AFA9EC | #7F77DD | #534AB7 | #3C3489 | #26215C | +| `c-teal` | #E1F5EE | #9FE1CB | #5DCAA5 | #1D9E75 | #0F6E56 | #085041 | #04342C | +| `c-coral` | #FAECE7 | #F5C4B3 | #F0997B | #D85A30 | #993C1D | #712B13 | #4A1B0C | +| `c-pink` | #FBEAF0 | #F4C0D1 | #ED93B1 | #D4537E | #993556 | #72243E | #4B1528 | +| `c-gray` | #F1EFE8 | #D3D1C7 | #B4B2A9 | #888780 | #5F5E5A | #444441 | #2C2C2A | +| `c-blue` | #E6F1FB | #B5D4F4 | #85B7EB | #378ADD | #185FA5 | #0C447C | #042C53 | +| `c-green` | #EAF3DE | #C0DD97 | #97C459 | #639922 | #3B6D11 | #27500A | #173404 | +| `c-amber` | #FAEEDA | #FAC775 | #EF9F27 | #BA7517 | #854F0B | #633806 | #412402 | +| `c-red` | #FCEBEB | #F7C1C1 | #F09595 | #E24B4A | #A32D2D | #791F1F | #501313 | + +#### Color Assignment Rules + +Color encodes **meaning**, not sequence. Never cycle through colors like a rainbow. + +- Group nodes by **category** — all nodes of the same type share one color. +- Use `c-gray` for neutral/structural nodes (start, end, generic steps, users). +- Use **2-3 colors per diagram**, not 6+. +- Prefer `c-purple`, `c-teal`, `c-coral`, `c-pink` for general categories. +- Reserve `c-blue`, `c-green`, `c-amber`, `c-red` for semantic meaning (info, success, warning, error). + +Light/dark stop mapping (handled by the template CSS — just use the class): +- Light mode: 50 fill + 600 stroke + 800 title / 600 subtitle +- Dark mode: 800 fill + 200 stroke + 100 title / 200 subtitle + +### Typography + +Only two font sizes. No exceptions. + +| Class | Size | Weight | Use | +|-------|------|--------|-----| +| `th` | 14px | 500 | Node titles, region labels | +| `ts` | 12px | 400 | Subtitles, descriptions, arrow labels | +| `t` | 14px | 400 | General text | + +- **Sentence case always.** Never Title Case, never ALL CAPS. +- Every `` MUST carry a class (`t`, `ts`, or `th`). No unclassed text. +- `dominant-baseline="central"` on all text inside boxes. +- `text-anchor="middle"` for centered text in boxes. + +**Width estimation (approx):** +- 14px weight 500: ~8px per character +- 12px weight 400: ~6.5px per character +- Always verify: `box_width >= (char_count × px_per_char) + 48` (24px padding each side) + +### Spacing & Layout + +- **ViewBox**: `viewBox="0 0 680 H"` where H = content height + 40px buffer. +- **Safe area**: x=40 to x=640, y=40 to y=(H-40). +- **Between boxes**: 60px minimum gap. +- **Inside boxes**: 24px horizontal padding, 12px vertical padding. +- **Arrowhead gap**: 10px between arrowhead and box edge. +- **Single-line box**: 44px height. +- **Two-line box**: 56px height, 18px between title and subtitle baselines. +- **Container padding**: 20px minimum inside every container. +- **Max nesting**: 2-3 levels deep. Deeper gets unreadable at 680px width. + +### Stroke & Shape + +- **Stroke width**: 0.5px on all node borders. Not 1px, not 2px. +- **Rect rounding**: `rx="8"` for nodes, `rx="12"` for inner containers, `rx="16"` to `rx="20"` for outer containers. +- **Connector paths**: MUST have `fill="none"`. SVG defaults to `fill: black` otherwise. + +### Arrow Marker + +Include this `` block at the start of **every** SVG: + +```xml + + + + + +``` + +Use `marker-end="url(#arrow)"` on lines. The arrowhead inherits the line color via `context-stroke`. + +### CSS Classes (Provided by the Template) + +The template page provides: + +- Text: `.t`, `.ts`, `.th` +- Neutral: `.box`, `.arr`, `.leader`, `.node` +- Color ramps: `.c-purple`, `.c-teal`, `.c-coral`, `.c-pink`, `.c-gray`, `.c-blue`, `.c-green`, `.c-amber`, `.c-red` (all with automatic light/dark mode) + +You do **not** need to redefine these — just apply them in your SVG. The template file contains the full CSS definitions. + +--- + +## SVG Boilerplate + +Every SVG inside the template page starts with this exact structure: + +```xml + + + + + + + + + + +``` + +Replace `{HEIGHT}` with the actual computed height (last element bottom + 40px). + +### Node Patterns + +**Single-line node (44px):** +```xml + + + Service name + +``` + +**Two-line node (56px):** +```xml + + + Service name + Short description + +``` + +**Connector (no label):** +```xml + +``` + +**Container (dashed or solid):** +```xml + + + Container label + Subtitle info + +``` + +--- + +## Diagram Types + +Choose the layout that fits the subject: + +1. **Flowchart** — CI/CD pipelines, request lifecycles, approval workflows, data processing. Single-direction flow (top-down or left-right). Max 4-5 nodes per row. +2. **Structural / Containment** — Cloud infrastructure nesting, system architecture with layers. Large outer containers with inner regions. Dashed rects for logical groupings. +3. **API / Endpoint Map** — REST routes, GraphQL schemas. Tree from root, branching to resource groups, each containing endpoint nodes. +4. **Microservice Topology** — Service mesh, event-driven systems. Services as nodes, arrows for communication patterns, message queues between. +5. **Data Flow** — ETL pipelines, streaming architectures. Left-to-right flow from sources through processing to sinks. +6. **Physical / Structural** — Vehicles, buildings, hardware, anatomy. Use shapes that match the physical form — `` for curved bodies, `` for tapered shapes, ``/`` for cylindrical parts, nested `` for compartments. See `references/physical-shape-cookbook.md`. +7. **Infrastructure / Systems Integration** — Smart cities, IoT networks, multi-domain systems. Hub-spoke layout with central platform connecting subsystems. Semantic line styles (`.data-line`, `.power-line`, `.water-pipe`, `.road`). See `references/infrastructure-patterns.md`. +8. **UI / Dashboard Mockups** — Admin panels, monitoring dashboards. Screen frame with nested chart/gauge/indicator elements. See `references/dashboard-patterns.md`. + +For physical, infrastructure, and dashboard diagrams, load the matching reference file before generating — each one provides ready-made CSS classes and shape primitives. + +--- + +## Validation Checklist + +Before finalizing any SVG, verify ALL of the following: + +1. Every `` has class `t`, `ts`, or `th`. +2. Every `` inside a box has `dominant-baseline="central"`. +3. Every connector `` or `` used as arrow has `fill="none"`. +4. No arrow line crosses through an unrelated box. +5. `box_width >= (longest_label_chars × 8) + 48` for 14px text. +6. `box_width >= (longest_label_chars × 6.5) + 48` for 12px text. +7. ViewBox height = bottom-most element + 40px. +8. All content stays within x=40 to x=640. +9. Color classes (`c-*`) are on `` or shape elements, never on `` connectors. +10. Arrow `` block is present. +11. No gradients, shadows, blur, or glow effects. +12. Stroke width is 0.5px on all node borders. + +--- + +## Output & Preview + +### Default: standalone HTML file + +Write a single `.html` file the user can open directly. No server, no dependencies, works offline. Pattern: + +```python +# 1. Load the template +template = skill_view("concept-diagrams", "templates/template.html") + +# 2. Fill in title, subtitle, and paste your SVG +html = template.replace( + "", "SN2 reaction mechanism" +).replace( + "", "Bimolecular nucleophilic substitution" +).replace( + "", svg_content +) + +# 3. Write to a user-chosen path (or ./ by default) +write_file("./sn2-mechanism.html", html) +``` + +Tell the user how to open it: + +``` +# macOS +open ./sn2-mechanism.html +# Linux +xdg-open ./sn2-mechanism.html +``` + +### Optional: local preview server (multi-diagram gallery) + +Only use this when the user explicitly wants a browsable gallery of multiple diagrams. + +**Rules:** +- Bind to `127.0.0.1` only. Never `0.0.0.0`. Exposing diagrams on all network interfaces is a security hazard on shared networks. +- Pick a free port (do NOT hard-code one) and tell the user the chosen URL. +- The server is optional and opt-in — prefer the standalone HTML file first. + +Recommended pattern (lets the OS pick a free ephemeral port): + +```bash +# Put each diagram in its own folder under .diagrams/ +mkdir -p .diagrams/sn2-mechanism +# ...write .diagrams/sn2-mechanism/index.html... + +# Serve on loopback only, free port +cd .diagrams && python3 -c " +import http.server, socketserver +with socketserver.TCPServer(('127.0.0.1', 0), http.server.SimpleHTTPRequestHandler) as s: + print(f'Serving at http://127.0.0.1:{s.server_address[1]}/') + s.serve_forever() +" & +``` + +If the user insists on a fixed port, use `127.0.0.1:` — still never `0.0.0.0`. Document how to stop the server (`kill %1` or `pkill -f "http.server"`). + +--- + +## Examples Reference + +The `examples/` directory ships 15 complete, tested diagrams. Browse them for working patterns before writing a new diagram of a similar type: + +| File | Type | Demonstrates | +|------|------|--------------| +| `hospital-emergency-department-flow.md` | Flowchart | Priority routing with semantic colors | +| `feature-film-production-pipeline.md` | Flowchart | Phased workflow, horizontal sub-flows | +| `automated-password-reset-flow.md` | Flowchart | Auth flow with error branches | +| `autonomous-llm-research-agent-flow.md` | Flowchart | Loop-back arrows, decision branches | +| `place-order-uml-sequence.md` | Sequence | UML sequence diagram style | +| `commercial-aircraft-structure.md` | Physical | Paths, polygons, ellipses for realistic shapes | +| `wind-turbine-structure.md` | Physical cross-section | Underground/above-ground separation, color coding | +| `smartphone-layer-anatomy.md` | Exploded view | Alternating left/right labels, layered components | +| `apartment-floor-plan-conversion.md` | Floor plan | Walls, doors, proposed changes in dotted red | +| `banana-journey-tree-to-smoothie.md` | Narrative journey | Winding path, progressive state changes | +| `cpu-ooo-microarchitecture.md` | Hardware pipeline | Fan-out, memory hierarchy sidebar | +| `sn2-reaction-mechanism.md` | Chemistry | Molecules, curved arrows, energy profile | +| `smart-city-infrastructure.md` | Hub-spoke | Semantic line styles per system | +| `electricity-grid-flow.md` | Multi-stage flow | Voltage hierarchy, flow markers | +| `ml-benchmark-grouped-bar-chart.md` | Chart | Grouped bars, dual axis | + +Load any example with: +``` +skill_view(name="concept-diagrams", file_path="examples/") +``` + +--- + +## Quick Reference: What to Use When + +| User says | Diagram type | Suggested colors | +|-----------|--------------|------------------| +| "show the pipeline" | Flowchart | gray start/end, purple steps, red errors, teal deploy | +| "draw the data flow" | Data pipeline (left-right) | gray sources, purple processing, teal sinks | +| "visualize the system" | Structural (containment) | purple container, teal services, coral data | +| "map the endpoints" | API tree | purple root, one ramp per resource group | +| "show the services" | Microservice topology | gray ingress, teal services, purple bus, coral workers | +| "draw the aircraft/vehicle" | Physical | paths, polygons, ellipses for realistic shapes | +| "smart city / IoT" | Hub-spoke integration | semantic line styles per subsystem | +| "show the dashboard" | UI mockup | dark screen, chart colors: teal, purple, coral for alerts | +| "power grid / electricity" | Multi-stage flow | voltage hierarchy (HV/MV/LV line weights) | +| "wind turbine / turbine" | Physical cross-section | foundation + tower cutaway + nacelle color-coded | +| "journey of X / lifecycle" | Narrative journey | winding path, progressive state changes | +| "layers of X / exploded" | Exploded layer view | vertical stack, alternating labels | +| "CPU / pipeline" | Hardware pipeline | vertical stages, fan-out to execution ports | +| "floor plan / apartment" | Floor plan | walls, doors, proposed changes in dotted red | +| "reaction mechanism" | Chemistry | atoms, bonds, curved arrows, transition state, energy profile | diff --git a/website/docs/user-guide/skills/optional/creative/creative-kanban-video-orchestrator.md b/website/docs/user-guide/skills/optional/creative/creative-kanban-video-orchestrator.md index a148ba6d2d6..8fa3cdf127f 100644 --- a/website/docs/user-guide/skills/optional/creative/creative-kanban-video-orchestrator.md +++ b/website/docs/user-guide/skills/optional/creative/creative-kanban-video-orchestrator.md @@ -21,7 +21,7 @@ Plan, set up, and monitor a multi-agent video production pipeline backed by Herm | License | MIT | | Platforms | linux, macos, windows | | Tags | `video`, `kanban`, `multi-agent`, `orchestration`, `production-pipeline` | -| Related skills | [`kanban-orchestrator`](/docs/user-guide/skills/bundled/devops/devops-kanban-orchestrator), [`kanban-worker`](/docs/user-guide/skills/bundled/devops/devops-kanban-worker), [`ascii-video`](/docs/user-guide/skills/bundled/creative/creative-ascii-video), [`manim-video`](/docs/user-guide/skills/bundled/creative/creative-manim-video), [`p5js`](/docs/user-guide/skills/bundled/creative/creative-p5js), [`comfyui`](/docs/user-guide/skills/bundled/creative/creative-comfyui), [`touchdesigner-mcp`](/docs/user-guide/skills/bundled/creative/creative-touchdesigner-mcp), [`blender-mcp`](/docs/user-guide/skills/optional/creative/creative-blender-mcp), [`pixel-art`](/docs/user-guide/skills/optional/creative/creative-pixel-art), [`ascii-art`](/docs/user-guide/skills/bundled/creative/creative-ascii-art), [`songwriting-and-ai-music`](/docs/user-guide/skills/bundled/creative/creative-songwriting-and-ai-music), [`heartmula`](/docs/user-guide/skills/bundled/media/media-heartmula), [`songsee`](/docs/user-guide/skills/bundled/media/media-songsee), `spotify`, [`youtube-content`](/docs/user-guide/skills/bundled/media/media-youtube-content), [`claude-design`](/docs/user-guide/skills/bundled/creative/creative-claude-design), [`excalidraw`](/docs/user-guide/skills/bundled/creative/creative-excalidraw), [`html-artifact`](/docs/user-guide/skills/bundled/creative/creative-html-artifact), [`baoyu-comic`](/docs/user-guide/skills/optional/creative/creative-baoyu-comic), [`baoyu-infographic`](/docs/user-guide/skills/bundled/creative/creative-baoyu-infographic), [`humanizer`](/docs/user-guide/skills/bundled/creative/creative-humanizer), [`gif-search`](/docs/user-guide/skills/bundled/media/media-gif-search), [`meme-generation`](/docs/user-guide/skills/optional/creative/creative-meme-generation) | +| Related skills | [`kanban-orchestrator`](/docs/user-guide/skills/bundled/devops/devops-kanban-orchestrator), [`kanban-worker`](/docs/user-guide/skills/bundled/devops/devops-kanban-worker), [`ascii-video`](/docs/user-guide/skills/bundled/creative/creative-ascii-video), [`manim-video`](/docs/user-guide/skills/bundled/creative/creative-manim-video), [`p5js`](/docs/user-guide/skills/bundled/creative/creative-p5js), [`comfyui`](/docs/user-guide/skills/bundled/creative/creative-comfyui), [`touchdesigner-mcp`](/docs/user-guide/skills/bundled/creative/creative-touchdesigner-mcp), [`blender-mcp`](/docs/user-guide/skills/optional/creative/creative-blender-mcp), [`pixel-art`](/docs/user-guide/skills/bundled/creative/creative-pixel-art), [`ascii-art`](/docs/user-guide/skills/bundled/creative/creative-ascii-art), [`songwriting-and-ai-music`](/docs/user-guide/skills/bundled/creative/creative-songwriting-and-ai-music), [`heartmula`](/docs/user-guide/skills/bundled/media/media-heartmula), [`songsee`](/docs/user-guide/skills/bundled/media/media-songsee), [`spotify`](/docs/user-guide/skills/bundled/media/media-spotify), [`youtube-content`](/docs/user-guide/skills/bundled/media/media-youtube-content), [`claude-design`](/docs/user-guide/skills/bundled/creative/creative-claude-design), [`excalidraw`](/docs/user-guide/skills/bundled/creative/creative-excalidraw), [`architecture-diagram`](/docs/user-guide/skills/bundled/creative/creative-architecture-diagram), [`concept-diagrams`](/docs/user-guide/skills/optional/creative/creative-concept-diagrams), [`baoyu-comic`](/docs/user-guide/skills/bundled/creative/creative-baoyu-comic), [`baoyu-infographic`](/docs/user-guide/skills/bundled/creative/creative-baoyu-infographic), [`humanizer`](/docs/user-guide/skills/bundled/creative/creative-humanizer), [`gif-search`](/docs/user-guide/skills/bundled/media/media-gif-search), [`meme-generation`](/docs/user-guide/skills/optional/creative/creative-meme-generation) | ## Reference: full SKILL.md @@ -194,7 +194,7 @@ task graphs. See **[references/examples.md](https://github.com/NousResearch/herm right human-review gates. 8. **Verify API keys BEFORE firing.** External APIs (TTS, image-gen, - image-to-video) need keys in `${HERMES_HOME:-~/.hermes}/.env` or the user's secret store. + image-to-video) need keys in `~/.hermes/.env` or the user's secret store. A worker that hits a missing-key error wastes a task slot. The setup script's `check_key` helper aborts cleanly if a required key is missing. diff --git a/website/docs/user-guide/skills/optional/devops/devops-pinggy-tunnel.md b/website/docs/user-guide/skills/optional/devops/devops-pinggy-tunnel.md index 18fb572bdcb..19f431f1967 100644 --- a/website/docs/user-guide/skills/optional/devops/devops-pinggy-tunnel.md +++ b/website/docs/user-guide/skills/optional/devops/devops-pinggy-tunnel.md @@ -21,7 +21,7 @@ Zero-install localhost tunnels over SSH via Pinggy. | License | MIT | | Platforms | linux, macos, windows | | Tags | `Pinggy`, `Tunnel`, `Networking`, `SSH`, `Webhook`, `Localhost` | -| Related skills | `cloudflared-quick-tunnel`, `webhook-subscriptions` | +| Related skills | `cloudflared-quick-tunnel`, [`webhook-subscriptions`](/docs/user-guide/skills/bundled/devops/devops-webhook-subscriptions) | ## Reference: full SKILL.md diff --git a/website/docs/user-guide/skills/optional/devops/devops-watchers.md b/website/docs/user-guide/skills/optional/devops/devops-watchers.md index 9d2fc7f7523..8a56162bdb8 100644 --- a/website/docs/user-guide/skills/optional/devops/devops-watchers.md +++ b/website/docs/user-guide/skills/optional/devops/devops-watchers.md @@ -77,7 +77,7 @@ python $HERMES_HOME/skills/devops/watchers/scripts/watch_rss.py \ --name hn --url https://news.ycombinator.com/rss --max 5 ``` -Watch a GitHub repo (set `GITHUB_TOKEN` in `${HERMES_HOME:-~/.hermes}/.env` to avoid the 60 req/hr anonymous rate limit): +Watch a GitHub repo (set `GITHUB_TOKEN` in `~/.hermes/.env` to avoid the 60 req/hr anonymous rate limit): ```bash python $HERMES_HOME/skills/devops/watchers/scripts/watch_github.py \ diff --git a/website/docs/user-guide/skills/optional/mcp/mcp-fastmcp.md b/website/docs/user-guide/skills/optional/mcp/mcp-fastmcp.md index 3efe47b12b8..2defe89d4eb 100644 --- a/website/docs/user-guide/skills/optional/mcp/mcp-fastmcp.md +++ b/website/docs/user-guide/skills/optional/mcp/mcp-fastmcp.md @@ -21,7 +21,7 @@ Build, test, inspect, install, and deploy MCP servers with FastMCP in Python. Us | License | MIT | | Platforms | linux, macos, windows | | Tags | `MCP`, `FastMCP`, `Python`, `Tools`, `Resources`, `Prompts`, `Deployment` | -| Related skills | `native-mcp`, [`mcporter`](/docs/user-guide/skills/optional/mcp/mcp-mcporter) | +| Related skills | [`native-mcp`](/docs/user-guide/skills/bundled/mcp/mcp-native-mcp), [`mcporter`](/docs/user-guide/skills/optional/mcp/mcp-mcporter) | ## Reference: full SKILL.md diff --git a/website/docs/user-guide/skills/optional/payments/payments-stripe-projects.md b/website/docs/user-guide/skills/optional/payments/payments-stripe-projects.md index fcd20673edd..74e60876bf5 100644 --- a/website/docs/user-guide/skills/optional/payments/payments-stripe-projects.md +++ b/website/docs/user-guide/skills/optional/payments/payments-stripe-projects.md @@ -44,7 +44,7 @@ Trigger phrases: - "manage my stack credentials", "rotate this key", "upgrade my plan" - "what providers can I add?" -If the user already has a provider account, this skill can still connect it with `stripe projects link `. If the user wants to use an existing provider resource, such as an existing database or Vercel project, check provider support first; many providers currently support provisioning new resources but not importing existing ones. +If the user already has a provider account, this skill can still connect it with `stripe projects link <provider>`. If the user wants to use an existing provider resource, such as an existing database or Vercel project, check provider support first; many providers currently support provisioning new resources but not importing existing ones. ## Prerequisites diff --git a/website/docs/user-guide/skills/optional/productivity/productivity-canvas.md b/website/docs/user-guide/skills/optional/productivity/productivity-canvas.md index 11bbf7e2006..e94a81b0407 100644 --- a/website/docs/user-guide/skills/optional/productivity/productivity-canvas.md +++ b/website/docs/user-guide/skills/optional/productivity/productivity-canvas.md @@ -42,7 +42,7 @@ Read-only access to Canvas LMS for listing courses and assignments. 2. Go to **Account → Settings** (click your profile icon, then Settings) 3. Scroll to **Approved Integrations** and click **+ New Access Token** 4. Name the token (e.g., "Hermes Agent"), set an optional expiry, and click **Generate Token** -5. Copy the token and add to `${HERMES_HOME:-~/.hermes}/.env`: +5. Copy the token and add to `~/.hermes/.env`: ``` CANVAS_API_TOKEN=your_token_here diff --git a/website/docs/user-guide/skills/optional/productivity/productivity-shopify.md b/website/docs/user-guide/skills/optional/productivity/productivity-shopify.md index 97d4116d82d..61bc95cfa66 100644 --- a/website/docs/user-guide/skills/optional/productivity/productivity-shopify.md +++ b/website/docs/user-guide/skills/optional/productivity/productivity-shopify.md @@ -40,7 +40,7 @@ The REST Admin API is legacy since 2024-04 and only receives security fixes. **U 1. In Shopify admin: **Settings → Apps and sales channels → Develop apps → Create an app**. 2. Click **Configure Admin API scopes**, select what you need (examples below), save. 3. **Install app** → the Admin API access token appears ONCE. Copy it immediately — Shopify will never show it again. Tokens start with `shpat_`. -4. Save to `${HERMES_HOME:-~/.hermes}/.env`: +4. Save to `~/.hermes/.env`: ``` SHOPIFY_ACCESS_TOKEN=shpat_xxxxxxxxxxxxxxxxxxxx SHOPIFY_STORE_DOMAIN=my-store.myshopify.com diff --git a/website/docs/user-guide/skills/optional/productivity/productivity-siyuan.md b/website/docs/user-guide/skills/optional/productivity/productivity-siyuan.md index 777ee265d11..58263053fdd 100644 --- a/website/docs/user-guide/skills/optional/productivity/productivity-siyuan.md +++ b/website/docs/user-guide/skills/optional/productivity/productivity-siyuan.md @@ -37,7 +37,7 @@ Use the [SiYuan](https://github.com/siyuan-note/siyuan) kernel API via curl to s 1. Install and run SiYuan (desktop or Docker) 2. Get your API token: **Settings > About > API token** -3. Store it in `${HERMES_HOME:-~/.hermes}/.env`: +3. Store it in `~/.hermes/.env`: ``` SIYUAN_TOKEN=your_token_here SIYUAN_URL=http://127.0.0.1:6806 diff --git a/website/docs/user-guide/skills/optional/productivity/productivity-telephony.md b/website/docs/user-guide/skills/optional/productivity/productivity-telephony.md index 03d08bdc399..f6c15444cbb 100644 --- a/website/docs/user-guide/skills/optional/productivity/productivity-telephony.md +++ b/website/docs/user-guide/skills/optional/productivity/productivity-telephony.md @@ -34,7 +34,7 @@ The following is the complete skill definition that Hermes loads when this skill This optional skill gives Hermes practical phone capabilities while keeping telephony out of the core tool list. It ships with a helper script, `scripts/telephony.py`, that can: -- save provider credentials into `${HERMES_HOME:-~/.hermes}/.env` +- save provider credentials into `~/.hermes/.env` - search for and buy a Twilio phone number - remember that owned number for later sessions - send SMS / MMS from the owned number @@ -121,7 +121,7 @@ Why: The skill persists telephony state in two places: -### `${HERMES_HOME:-~/.hermes}/.env` +### `~/.hermes/.env` Used for long-lived provider credentials and owned-number IDs, for example: - `TWILIO_ACCOUNT_SID` - `TWILIO_AUTH_TOKEN` @@ -258,7 +258,7 @@ python3 "$SCRIPT" save-twilio AC... auth_token_here python3 "$SCRIPT" twilio-search --country US --area-code 702 --limit 10 ``` -3. Buy it and save it into `${HERMES_HOME:-~/.hermes}/.env` + state: +3. Buy it and save it into `~/.hermes/.env` + state: ```bash python3 "$SCRIPT" twilio-buy "+17025551234" --save-env ``` @@ -420,7 +420,7 @@ After setup, you should be able to do all of the following with just this skill: 1. `diagnose` shows provider readiness and remembered state 2. search and buy a Twilio number -3. persist that number to `${HERMES_HOME:-~/.hermes}/.env` +3. persist that number to `~/.hermes/.env` 4. send an SMS from the owned number 5. poll inbound texts for the owned number later 6. place a direct Twilio call diff --git a/website/docs/user-guide/skills/optional/research/research-gitnexus-explorer.md b/website/docs/user-guide/skills/optional/research/research-gitnexus-explorer.md index a5f062dc373..5b1f62458d1 100644 --- a/website/docs/user-guide/skills/optional/research/research-gitnexus-explorer.md +++ b/website/docs/user-guide/skills/optional/research/research-gitnexus-explorer.md @@ -21,7 +21,7 @@ Index a codebase with GitNexus and serve an interactive knowledge graph via web | License | MIT | | Platforms | linux, macos, windows | | Tags | `gitnexus`, `code-intelligence`, `knowledge-graph`, `visualization` | -| Related skills | `native-mcp`, [`codebase-inspection`](/docs/user-guide/skills/bundled/github/github-codebase-inspection) | +| Related skills | [`native-mcp`](/docs/user-guide/skills/bundled/mcp/mcp-native-mcp), [`codebase-inspection`](/docs/user-guide/skills/bundled/github/github-codebase-inspection) | ## Reference: full SKILL.md diff --git a/website/docs/user-guide/skills/optional/research/research-qmd.md b/website/docs/user-guide/skills/optional/research/research-qmd.md index 8d145080b45..47cf81634b8 100644 --- a/website/docs/user-guide/skills/optional/research/research-qmd.md +++ b/website/docs/user-guide/skills/optional/research/research-qmd.md @@ -21,7 +21,7 @@ Search personal knowledge bases, notes, docs, and meeting transcripts locally us | License | MIT | | Platforms | macos, linux | | Tags | `Search`, `Knowledge-Base`, `RAG`, `Notes`, `MCP`, `Local-AI` | -| Related skills | [`obsidian`](/docs/user-guide/skills/bundled/note-taking/note-taking-obsidian), `native-mcp`, [`arxiv`](/docs/user-guide/skills/bundled/research/research-arxiv) | +| Related skills | [`obsidian`](/docs/user-guide/skills/bundled/note-taking/note-taking-obsidian), [`native-mcp`](/docs/user-guide/skills/bundled/mcp/mcp-native-mcp), [`arxiv`](/docs/user-guide/skills/bundled/research/research-arxiv) | ## Reference: full SKILL.md diff --git a/website/docs/user-guide/skills/optional/security/security-1password.md b/website/docs/user-guide/skills/optional/security/security-1password.md index c2c3fccb6e9..4ed526a87b6 100644 --- a/website/docs/user-guide/skills/optional/security/security-1password.md +++ b/website/docs/user-guide/skills/optional/security/security-1password.md @@ -51,7 +51,7 @@ Use this skill when the user wants secrets managed through 1Password instead of ### Service Account (recommended for Hermes) -Set `OP_SERVICE_ACCOUNT_TOKEN` in `${HERMES_HOME:-~/.hermes}/.env` (the skill will prompt for this on first load). +Set `OP_SERVICE_ACCOUNT_TOKEN` in `~/.hermes/.env` (the skill will prompt for this on first load). No desktop app needed. Supports `op read`, `op inject`, `op run`. ```bash diff --git a/website/docs/user-guide/skills/optional/security/security-godmode.md b/website/docs/user-guide/skills/optional/security/security-godmode.md index f41975a4966..ee12f700f6d 100644 --- a/website/docs/user-guide/skills/optional/security/security-godmode.md +++ b/website/docs/user-guide/skills/optional/security/security-godmode.md @@ -418,4 +418,4 @@ Claude Sonnet 4 is robust against all current techniques for clearly harmful con 9. **Always use `load_godmode.py` in execute_code** — The individual scripts (`parseltongue.py`, `godmode_race.py`, `auto_jailbreak.py`) have argparse CLI entry points with `if __name__ == '__main__'` blocks. When loaded via `exec()` in execute_code, `__name__` is `'__main__'` and argparse fires, crashing the script. The `load_godmode.py` loader handles this by setting `__name__` to a non-main value and managing sys.argv. 10. **boundary_inversion is model-version specific** — Works on Claude 3.5 Sonnet but NOT Claude Sonnet 4 or Claude 4.6. The strategy order in auto_jailbreak tries it first for Claude models, but falls through to refusal_inversion when it fails. Update the strategy order if you know the model version. 11. **Gray-area vs hard queries** — Jailbreak techniques work much better on "dual-use" queries (lock picking, security tools, chemistry) than on overtly harmful ones (phishing templates, malware). For hard queries, skip directly to ULTRAPLINIAN or use Hermes/Grok models that don't refuse. -12. **execute_code sandbox has no env vars** — When Hermes runs auto_jailbreak via execute_code, the sandbox doesn't inherit the Hermes `.env`. Load dotenv explicitly: `import os; from dotenv import load_dotenv; load_dotenv(os.path.join(os.environ.get("HERMES_HOME", os.path.expanduser("~/.hermes")), ".env"))` +12. **execute_code sandbox has no env vars** — When Hermes runs auto_jailbreak via execute_code, the sandbox doesn't inherit `~/.hermes/.env`. Load dotenv explicitly: `from dotenv import load_dotenv; load_dotenv(os.path.expanduser("~/.hermes/.env"))` diff --git a/website/docs/user-guide/skills/optional/software-development/software-development-rest-graphql-debug.md b/website/docs/user-guide/skills/optional/software-development/software-development-rest-graphql-debug.md index 6c9f84bafcb..0698d855f5f 100644 --- a/website/docs/user-guide/skills/optional/software-development/software-development-rest-graphql-debug.md +++ b/website/docs/user-guide/skills/optional/software-development/software-development-rest-graphql-debug.md @@ -414,7 +414,7 @@ class TestAPISmoke: ### Token handling - Never log full tokens. Redact: `Bearer `. -- Never hardcode tokens in scripts. Read from env (`os.environ["API_TOKEN"]`) or `${HERMES_HOME:-~/.hermes}/.env`. +- Never hardcode tokens in scripts. Read from env (`os.environ["API_TOKEN"]`) or `~/.hermes/.env`. - Rotate immediately if a token surfaces in logs, error messages, or git history. ### Safe logging diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/optional-skills-catalog.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/optional-skills-catalog.md index ff9b48cef6f..aed044b3099 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/optional-skills-catalog.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/optional-skills-catalog.md @@ -53,6 +53,7 @@ hermes skills uninstall | 技能 | 描述 | |-------|-------------| | [**blender-mcp**](/user-guide/skills/optional/creative/creative-blender-mcp) | 通过 socket 连接 blender-mcp 插件,直接从 Hermes 控制 Blender。创建 3D 对象、材质、动画,并运行任意 Blender Python(bpy)代码。适用于用户希望在 Blender 中创建或修改任何内容的场景。 | +| [**concept-diagrams**](/user-guide/skills/optional/creative/creative-concept-diagrams) | 生成扁平、极简、支持亮色/暗色模式的 SVG 图表,输出为独立 HTML 文件,采用统一的教育视觉语言,包含 9 种语义色阶、句首大写排版及自动暗色模式。最适合教育和说明类内容。 | | [**hyperframes**](/user-guide/skills/optional/creative/creative-hyperframes) | 使用 HyperFrames 创建基于 HTML 的视频合成、动态标题卡、社交叠层、字幕访谈视频、音频响应视觉效果及着色器转场。HTML 是视频的唯一来源。适用于用户希望制作任何视频内容的场景。 | | [**kanban-video-orchestrator**](/user-guide/skills/optional/creative/creative-kanban-video-orchestrator) | 规划、搭建并监控由 Hermes Kanban 支撑的多 agent 视频制作流水线。适用于用户希望制作任何类型视频的场景 — 叙事影片、产品/营销视频、MV、解说视频、ASCII/终端艺术、抽象/生成式循环等。 | | [**meme-generation**](/user-guide/skills/optional/creative/creative-meme-generation) | 通过选取模板并使用 Pillow 叠加文字来生成真实的 meme 图片,输出实际的 .png 文件。 | diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/skills-catalog.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/skills-catalog.md index f6f24bd932d..20773484b6c 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/skills-catalog.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/skills-catalog.md @@ -35,6 +35,7 @@ Hermes 在执行 `hermes update` 时也会同步内置技能,但同步清单 | 技能 | 描述 | 路径 | |-------|-------------|------| +| [`architecture-diagram`](/user-guide/skills/bundled/creative/creative-architecture-diagram) | 以 HTML 形式生成深色主题的 SVG 架构/云/基础设施图。 | `creative/architecture-diagram` | | [`ascii-art`](/user-guide/skills/bundled/creative/creative-ascii-art) | ASCII 艺术:pyfiglet、cowsay、boxes、图像转 ASCII。 | `creative/ascii-art` | | [`ascii-video`](/user-guide/skills/bundled/creative/creative-ascii-video) | ASCII 视频:将视频/音频转换为彩色 ASCII MP4/GIF。 | `creative/ascii-video` | | [`baoyu-infographic`](/user-guide/skills/bundled/creative/creative-baoyu-infographic) | 信息图(可视化):21 种布局 × 21 种风格。 | `creative/baoyu-infographic` | @@ -47,6 +48,7 @@ Hermes 在执行 `hermes update` 时也会同步内置技能,但同步清单 | [`p5js`](/user-guide/skills/bundled/creative/creative-p5js) | p5.js 草图:生成艺术、着色器、交互、3D。 | `creative/p5js` | | [`popular-web-designs`](/user-guide/skills/bundled/creative/creative-popular-web-designs) | 54 种真实设计系统(Stripe、Linear、Vercel)的 HTML/CSS 实现。 | `creative/popular-web-designs` | | [`pretext`](/user-guide/skills/bundled/creative/creative-pretext) | 使用 @chenglou/pretext 构建创意浏览器 demo——无 DOM 的文本布局,支持 ASCII 艺术、绕障碍物的排版流、文字即几何游戏、动态排版和文字驱动的生成艺术。生成单文件 HTML。 | `creative/pretext` | +| [`sketch`](/user-guide/skills/bundled/creative/creative-sketch) | 一次性 HTML 原型:生成 2-3 个设计变体供对比。 | `creative/sketch` | | [`songwriting-and-ai-music`](/user-guide/skills/bundled/creative/creative-songwriting-and-ai-music) | 歌曲创作技巧与 Suno AI 音乐 prompt(提示词)。 | `creative/songwriting-and-ai-music` | | [`touchdesigner-mcp`](/user-guide/skills/bundled/creative/creative-touchdesigner-mcp) | 通过 twozero MCP 控制运行中的 TouchDesigner 实例——创建算子、设置参数、连接节点、执行 Python、构建实时视觉效果。36 个原生工具。 | `creative/touchdesigner-mcp` | diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-architecture-diagram.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-architecture-diagram.md new file mode 100644 index 00000000000..60846a64f16 --- /dev/null +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-architecture-diagram.md @@ -0,0 +1,165 @@ +--- +title: "Architecture Diagram — 深色主题 SVG 架构/云/基础设施图表(HTML 格式)" +sidebar_label: "Architecture Diagram" +description: "深色主题 SVG 架构/云/基础设施图表(HTML 格式)" +--- + +{/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */} + +# Architecture Diagram + +深色主题 SVG 架构/云/基础设施图表,以 HTML 格式输出。 + +## Skill 元数据 + +| | | +|---|---| +| 来源 | 内置(默认安装) | +| 路径 | `skills/creative/architecture-diagram` | +| 版本 | `1.0.0` | +| 作者 | Cocoon AI (hello@cocoon-ai.com),由 Hermes Agent 移植 | +| 许可证 | MIT | +| 平台 | linux, macos, windows | +| 标签 | `architecture`, `diagrams`, `SVG`, `HTML`, `visualization`, `infrastructure`, `cloud` | +| 相关 skill | [`concept-diagrams`](/user-guide/skills/optional/creative/creative-concept-diagrams), [`excalidraw`](/user-guide/skills/bundled/creative/creative-excalidraw) | + +## 参考:完整 SKILL.md + +:::info +以下是 Hermes 在触发该 skill 时加载的完整 skill 定义。这是 agent 在 skill 激活时所看到的指令内容。 +::: + +# Architecture Diagram Skill + +生成专业的深色主题技术架构图,输出为包含内联 SVG 图形的独立 HTML 文件。无需外部工具、无需 API 密钥、无需渲染库——只需写入 HTML 文件并在浏览器中打开即可。 + +## 适用范围 + +**最适合:** +- 软件系统架构(前端/后端/数据库层) +- 云基础设施(VPC、区域、子网、托管服务) +- 微服务/服务网格拓扑 +- 数据库 + API 映射、部署图 +- 任何具有技术基础设施主题、适合深色网格背景风格的内容 + +**以下场景请优先考虑其他工具:** +- 物理、化学、数学、生物或其他科学学科 +- 实物对象(车辆、硬件、解剖结构、截面图) +- 平面图、叙事流程、教育/教科书风格的视觉内容 +- 手绘白板草图(建议使用 `excalidraw`) +- 动画说明(建议使用动画相关 skill) + +如果有更专业的 skill 适用于该主题,请优先使用。如果没有合适的,本 skill 也可作为通用 SVG 图表的备选方案——输出内容将带有下述深色技术风格。 + +基于 [Cocoon AI 的 architecture-diagram-generator](https://github.com/Cocoon-AI/architecture-diagram-generator)(MIT 许可证)。 + +## 工作流程 + +1. 用户描述其系统架构(组件、连接关系、技术栈) +2. 按照下方设计规范生成 HTML 文件 +3. 使用 `write_file` 保存为 `.html` 文件(例如 `~/architecture-diagram.html`) +4. 用户在任意浏览器中打开——支持离线使用,无需任何依赖 + +### 输出位置 + +将图表保存到用户指定路径,或默认保存至当前工作目录: +``` +./[project-name]-architecture.html +``` + +### 预览 + +保存后,建议用户通过以下命令打开: +```bash +# macOS +open ./my-architecture.html +# Linux +xdg-open ./my-architecture.html +``` + +## 设计规范与视觉语言 + +### 颜色方案(语义映射) + +使用特定的 `rgba` 填充色和十六进制描边色对组件进行分类: + +| 组件类型 | 填充色(rgba) | 描边色(Hex) | +| :--- | :--- | :--- | +| **前端** | `rgba(8, 51, 68, 0.4)` | `#22d3ee`(cyan-400) | +| **后端** | `rgba(6, 78, 59, 0.4)` | `#34d399`(emerald-400) | +| **数据库** | `rgba(76, 29, 149, 0.4)` | `#a78bfa`(violet-400) | +| **AWS/云** | `rgba(120, 53, 15, 0.3)` | `#fbbf24`(amber-400) | +| **安全** | `rgba(136, 19, 55, 0.4)` | `#fb7185`(rose-400) | +| **消息总线** | `rgba(251, 146, 60, 0.3)` | `#fb923c`(orange-400) | +| **外部** | `rgba(30, 41, 59, 0.5)` | `#94a3b8`(slate-400) | + +### 字体与背景 +- **字体:** JetBrains Mono(等宽字体),从 Google Fonts 加载 +- **字号:** 12px(名称)、9px(副标签)、8px(注释)、7px(极小标签) +- **背景:** Slate-950(`#020617`),带有细腻的 40px 网格图案 + +```svg + + + + +``` + +## 技术实现细节 + +### 组件渲染 +组件为圆角矩形(`rx="6"`),描边宽度 1.5px。为防止箭头透过半透明填充色显现,使用**双矩形遮罩技术**: +1. 绘制不透明背景矩形(`#0f172a`) +2. 在其上方绘制半透明样式矩形 + +### 连接规则 +- **Z 轴顺序:** 在 SVG 早期绘制箭头(在网格之后),使其渲染在组件框的下方 +- **箭头头部:** 通过 SVG marker 定义 +- **安全流:** 使用 rose 色(`#fb7185`)虚线 +- **边界:** + - *安全组:* 虚线(`4,4`),rose 色 + - *区域:* 大虚线(`8,4`),amber 色,`rx="12"` + +### 间距与布局规则 +- **标准高度:** 60px(服务);80–120px(大型组件) +- **垂直间距:** 组件之间最小 40px +- **消息总线:** 必须放置在服务之间的间隙中,不得与其重叠 +- **图例位置:** **关键。** 必须放置在所有边界框的外部。计算所有边界的最低 Y 坐标,并将图例放置在其下方至少 20px 处。 + +## 文档结构 + +生成的 HTML 文件遵循四段式布局: +1. **页眉:** 带有脉冲点指示器的标题和副标题 +2. **主 SVG:** 包含在圆角边框卡片中的图表 +3. **摘要卡片:** 图表下方的三张卡片网格,用于展示高层次详情 +4. **页脚:** 简洁的元数据信息 + +### 信息卡片模式 +```html +
+
+
+

Title

+
+
    +
  • • Item one
  • +
  • • Item two
  • +
+
+``` + +## 输出要求 +- **单文件:** 一个自包含的 `.html` 文件 +- **无外部依赖:** 所有 CSS 和 SVG 必须内联(Google Fonts 除外) +- **无 JavaScript:** 所有动画(如脉冲点)使用纯 CSS 实现 +- **兼容性:** 必须在任何现代浏览器中正确渲染 + +## 模板参考 + +加载完整 HTML 模板以获取精确的结构、CSS 和 SVG 组件示例: + +``` +skill_view(name="architecture-diagram", file_path="templates/template.html") +``` + +模板包含每种组件类型(前端、后端、数据库、云、安全)、箭头样式(标准、虚线、曲线)、安全组、区域边界和图例的完整示例——生成图表时请以此作为结构参考。 \ No newline at end of file diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-claude-design.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-claude-design.md index 7aaa2d26f2d..6d1b7529ab3 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-claude-design.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-claude-design.md @@ -21,7 +21,7 @@ description: "设计一次性 HTML 制品(落地页、幻灯片、原型)" | 许可证 | MIT | | 平台 | linux, macos, windows | | 标签 | `design`, `html`, `prototype`, `ux`, `ui`, `creative`, `artifact`, `deck`, `motion`, `design-system` | -| 相关 skill | [`design-md`](/user-guide/skills/bundled/creative/creative-design-md), [`popular-web-designs`](/user-guide/skills/bundled/creative/creative-popular-web-designs), [`excalidraw`](/user-guide/skills/bundled/creative/creative-excalidraw), [`html-artifact`](/user-guide/skills/bundled/creative/creative-html-artifact) | +| 相关 skill | [`design-md`](/user-guide/skills/bundled/creative/creative-design-md), [`popular-web-designs`](/user-guide/skills/bundled/creative/creative-popular-web-designs), [`excalidraw`](/user-guide/skills/bundled/creative/creative-excalidraw), [`architecture-diagram`](/user-guide/skills/bundled/creative/creative-architecture-diagram) | ## 参考:完整 SKILL.md diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-design-md.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-design-md.md index e9fc5aade25..4d21eb7f671 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-design-md.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-design-md.md @@ -21,7 +21,7 @@ description: "编写/验证/导出 Google 的 DESIGN" | 许可证 | MIT | | 平台 | linux, macos, windows | | 标签 | `design`, `design-system`, `tokens`, `ui`, `accessibility`, `wcag`, `tailwind`, `dtcg`, `google` | -| 相关 skill | [`popular-web-designs`](/user-guide/skills/bundled/creative/creative-popular-web-designs), [`claude-design`](/user-guide/skills/bundled/creative/creative-claude-design), [`excalidraw`](/user-guide/skills/bundled/creative/creative-excalidraw), [`html-artifact`](/user-guide/skills/bundled/creative/creative-html-artifact) | +| 相关 skill | [`popular-web-designs`](/user-guide/skills/bundled/creative/creative-popular-web-designs), [`claude-design`](/user-guide/skills/bundled/creative/creative-claude-design), [`excalidraw`](/user-guide/skills/bundled/creative/creative-excalidraw), [`architecture-diagram`](/user-guide/skills/bundled/creative/creative-architecture-diagram) | ## 参考:完整 SKILL.md diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-pretext.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-pretext.md index 243e776f6a7..83dadb74c8d 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-pretext.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-pretext.md @@ -21,7 +21,7 @@ description: "适用于使用 @chenglou/pretext 构建创意浏览器演示 — | 许可证 | MIT | | 平台 | linux, macos, windows | | 标签 | `creative-coding`, `typography`, `pretext`, `ascii-art`, `canvas`, `generative`, `text-layout`, `kinetic-typography` | -| 相关 skill | [`p5js`](/user-guide/skills/bundled/creative/creative-p5js), [`claude-design`](/user-guide/skills/bundled/creative/creative-claude-design), [`excalidraw`](/user-guide/skills/bundled/creative/creative-excalidraw), [`html-artifact`](/user-guide/skills/bundled/creative/creative-html-artifact) | +| 相关 skill | [`p5js`](/user-guide/skills/bundled/creative/creative-p5js), [`claude-design`](/user-guide/skills/bundled/creative/creative-claude-design), [`excalidraw`](/user-guide/skills/bundled/creative/creative-excalidraw), [`architecture-diagram`](/user-guide/skills/bundled/creative/creative-architecture-diagram) | ## 参考:完整 SKILL.md diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-sketch.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-sketch.md new file mode 100644 index 00000000000..6478c87f362 --- /dev/null +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/creative/creative-sketch.md @@ -0,0 +1,238 @@ +--- +title: "Sketch — 一次性 HTML 原型:2-3 个设计方案对比" +sidebar_label: "Sketch" +description: "一次性 HTML 原型:2-3 个设计方案对比" +--- + +{/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */} + +# Sketch + +一次性 HTML 原型:2-3 个设计方案对比。 + +## Skill 元数据 + +| | | +|---|---| +| 来源 | 内置(默认安装) | +| 路径 | `skills/creative/sketch` | +| 版本 | `1.0.0` | +| 作者 | Hermes Agent(改编自 gsd-build/get-shit-done) | +| 许可证 | MIT | +| 平台 | linux, macos, windows | +| 标签 | `sketch`, `mockup`, `design`, `ui`, `prototype`, `html`, `variants`, `exploration`, `wireframe`, `comparison` | +| 相关 skill | [`spike`](/user-guide/skills/bundled/software-development/software-development-spike), [`claude-design`](/user-guide/skills/bundled/creative/creative-claude-design), [`popular-web-designs`](/user-guide/skills/bundled/creative/creative-popular-web-designs), [`excalidraw`](/user-guide/skills/bundled/creative/creative-excalidraw) | + +## 参考:完整 SKILL.md + +:::info +以下是 Hermes 在触发该 skill 时加载的完整 skill 定义。这是 agent 在 skill 激活时所看到的指令内容。 +::: + +# Sketch + +当用户希望**在确定方向之前先看到设计效果**时使用此 skill——以一次性 HTML 原型的形式探索 UI/UX 想法。目的是生成 2-3 个可交互的方案,让用户并排对比视觉方向,而非产出可交付的代码。 + +当用户说以下内容时加载此 skill:"sketch this screen"、"show me what X could look like"、"compare layout A vs B"、"give me 2-3 takes on this UI"、"let me see some variants"、"mockup this before I build"。 + +## 不适用场景 + +- 用户需要生产级组件——使用 `claude-design` 或正式构建 +- 用户需要精良的一次性 HTML 产物(落地页、幻灯片)——使用 `claude-design` +- 用户需要图表——使用 `excalidraw`、`architecture-diagram` +- 设计已确定——直接构建即可 + +## 如果用户安装了完整的 GSD 系统 + +如果 `gsd-sketch` 作为同级 skill 出现(通过 `npx get-shit-done-cc --hermes` 安装),优先使用 **`gsd-sketch`** 以获得完整工作流:持久化的 `.planning/sketches/` 目录(含 MANIFEST)、前沿模式分析、跨历史草图的一致性审计,以及与 GSD 其余部分的集成。本 skill 是轻量级独立版本——无状态机制的一次性草图。 + +## 核心方法 + +``` +intake → variants → head-to-head → pick winner (or iterate) +``` + +### 1. Intake(如果用户已提供足够信息则跳过) + +在生成方案之前,获取三项信息——每次只问一个问题,不要一次全问: + +1. **感觉。** "这个应该给人什么感觉?形容词、情绪、氛围。"——*"calm, editorial, like Linear"* 比 *"minimal"* 更有参考价值。 +2. **参考。** "哪些 app、网站或产品接近你想象中的感觉?"——实际参考比抽象描述更有效。 +3. **核心操作。** "用户在这个页面上最重要的单一操作是什么?"——所有方案都应服务于此;否则只是装饰。 + +每次回答后简短复述,再问下一个问题。如果用户已一次性提供了全部三项,直接跳到方案生成。 + +### 2. 方案(2-3 个,不少于 1 个,极少超过 4 个) + +一次性生成 **2-3 个方案**。每个方案是一个完整的独立 HTML 文件。不要描述方案——直接构建。目的是对比。 + +每个方案应采取**不同的设计立场**,而非不同的像素值。三种有效的方案维度: + +- **密度:** 紧凑 / 宽松 / 极密(选两个对比极端) +- **重点:** 内容优先 / 操作优先 / 工具优先 +- **美学:** 编辑风格 / 实用主义 / 趣味性 +- **布局:** 单列 / 侧边栏 / 分屏 +- **基调:** 卡片式 / 纯内容 / 文档风格 + +选定一个维度并从中拉开差距。两个仅在强调色上不同的方案是无效的——用户无法区分。 + +**方案命名:** 描述立场,而非编号。 + + +``` +sketches/ +├── 001-calm-editorial/ +│ ├── index.html +│ └── README.md +├── 001-utilitarian-dense/ +│ ├── index.html +│ └── README.md +└── 001-playful-split/ + ├── index.html + └── README.md +``` + + +### 3. 制作真实的 HTML + +每个方案是一个**单一自包含的 HTML 文件**: + +- 内联 ` +``` + +### 4. 方案 README + +每个方案的 `README.md` 回答以下内容: + +```markdown +## Variant: {stance name} + +### Design stance +One sentence on the principle driving this variant. + +### Key choices +- Layout: ... +- Typography: ... +- Color: ... +- Interaction: ... + +### Trade-offs +- Strong at: ... +- Weak at: ... + +### Best for +- The kind of user or use case this variant actually serves +``` + +### 5. 正面对比 + +所有方案构建完成后,以对比形式呈现。不要只是罗列——**给出观点**: + +```markdown +## Three takes on the home screen + +| Dimension | Calm editorial | Utilitarian dense | Playful split | +|-----------|----------------|-------------------|---------------| +| Density | Low | High | Medium | +| Primary action visibility | Low | High | Medium | +| Scan-ability | High | Medium | Low | +| Feel | Calm, trusted | Sharp, tool-like | Inviting, energetic | + +**My take:** Utilitarian dense for power users, calm editorial for content-forward audiences. Playful split is weakest — tries to do both and commits to neither. +``` + +让用户选出胜出方案,或将两个方案合并为混合版,或要求新一轮迭代。 + +## 主题化(当项目有视觉标识时) + +如果用户有现有主题(颜色、字体、token),将共享 token 放入 `sketches/themes/tokens.css` 并在每个方案中 `@import`。保持 token 精简: + +```css +/* sketches/themes/tokens.css */ +:root { + --color-bg: #fafafa; + --color-fg: #1a1a1a; + --color-accent: #0066ff; + --color-muted: #666; + --radius: 8px; + --font-display: "Inter", sans-serif; + --font-body: -apple-system, BlinkMacSystemFont, sans-serif; +} +``` + +不要对一次性草图过度 token 化——三种颜色加一种字体通常已足够。 + +## 交互基准 + +当用户能够完成以下操作时,草图的交互程度即为合格: + +1. **点击主要操作**并看到可见的变化(状态变更、模态框、toast、导航模拟) +2. **看到一个有意义的状态转换**(筛选列表、切换模式、展开/收起面板) +3. **悬停可识别的交互元素**(按钮、行、标签页) + +超过此程度是对一次性草图的过度工程化。低于此程度则只是截图。 + +## 前沿模式(决定下一步草图内容) + +如果草图已存在且用户询问"接下来应该草图什么?": + +- **一致性缺口**——来自不同草图的两个胜出方案做出了独立选择,尚未组合在一起 +- **未草图的页面**——被引用但从未探索过 +- **状态覆盖**——已草图了正常路径,但未覆盖空状态 / 加载中 / 错误 / 千条数据 +- **响应式缺口**——在某一视口下验证过;在移动端 / 超宽屏下是否成立? +- **交互模式**——静态布局已存在;过渡动效、拖拽、滚动行为尚未探索 + +提出 2-4 个命名候选项,让用户选择。 + +## 输出 + +- 在仓库根目录创建 `sketches/`(如果用户使用 GSD 约定则为 `.planning/sketches/`) +- 每个方案一个子目录:`NNN-stance-name/index.html` + `README.md` +- 告知用户如何打开:macOS 上用 `open sketches/001-calm-editorial/index.html`,Linux 上用 `xdg-open`,Windows 上用 `start` +- 保持方案的一次性特性——如果你觉得有必要保留某个草图,应将其提升为真实项目代码,而非作为资产保管 + +**单个方案的典型工具调用序列:** + +``` +terminal("mkdir -p sketches/001-calm-editorial") +write_file("sketches/001-calm-editorial/index.html", "...") +write_file("sketches/001-calm-editorial/README.md", "## Variant: Calm editorial\n...") +browser_navigate(url="file://$(pwd)/sketches/001-calm-editorial/index.html") +browser_vision(question="How does this look? Any obvious layout issues?") +``` + +对每个方案重复上述步骤,然后呈现对比表格。 + +## 致谢 + +改编自 GSD(Get Shit Done)项目的 `/gsd-sketch` 工作流——MIT © 2025 Lex Christopherson([gsd-build/get-shit-done](https://github.com/gsd-build/get-shit-done))。完整 GSD 系统提供持久化草图状态、主题/方案模式参考及一致性审计工作流;通过 `npx get-shit-done-cc --hermes --global` 安装。 \ No newline at end of file diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/software-development/software-development-spike.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/software-development/software-development-spike.md index be869779937..e5486edd0d3 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/software-development/software-development-spike.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/software-development/software-development-spike.md @@ -21,7 +21,7 @@ description: "在构建前验证想法的一次性实验" | 许可证 | MIT | | 平台 | linux, macos, windows | | 标签 | `spike`, `prototype`, `experiment`, `feasibility`, `throwaway`, `exploration`, `research`, `planning`, `mvp`, `proof-of-concept` | -| 相关 skill | [`html-artifact`](/user-guide/skills/bundled/creative/creative-html-artifact)、[`writing-plans`](/user-guide/skills/bundled/software-development/software-development-writing-plans)、[`subagent-driven-development`](/user-guide/skills/bundled/software-development/software-development-subagent-driven-development)、[`plan`](/user-guide/skills/bundled/software-development/software-development-plan) | +| 相关 skill | [`sketch`](/user-guide/skills/bundled/creative/creative-sketch)、[`writing-plans`](/user-guide/skills/bundled/software-development/software-development-writing-plans)、[`subagent-driven-development`](/user-guide/skills/bundled/software-development/software-development-subagent-driven-development)、[`plan`](/user-guide/skills/bundled/software-development/software-development-plan) | ## 参考:完整 SKILL.md diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/optional/creative/creative-concept-diagrams.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/optional/creative/creative-concept-diagrams.md new file mode 100644 index 00000000000..405f658a22b --- /dev/null +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/optional/creative/creative-concept-diagrams.md @@ -0,0 +1,379 @@ +--- +title: "概念图" +sidebar_label: "概念图" +description: "以统一的教育视觉语言生成扁平、简约、支持明暗模式的 SVG 图表,输出为独立 HTML 文件,包含 9 种语义色阶、句首大写排版及自动暗色模式。..." +--- + +{/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */} + +# 概念图 + +以统一的教育视觉语言生成扁平、简约、支持明暗模式的 SVG 图表,输出为独立 HTML 文件,包含 9 种语义色阶、句首大写排版及自动暗色模式。最适合教育类和非软件类视觉内容——物理装置、化学机制、数学曲线、实物(飞机、涡轮机、智能手机、机械表)、解剖图、平面图、截面图、叙事流程(X 的生命周期、Y 的过程)、中心辐射型系统集成(智慧城市、IoT)以及爆炸分层视图。若已有更专业的 skill 适用于该主题(专用软件/云架构、手绘草图、动画说明等),优先使用那些 skill——否则本 skill 也可作为通用 SVG 图表的备选方案,具备简洁的教育风格外观。内置 15 个示例图表。 + +## Skill 元数据 + +| | | +|---|---| +| 来源 | 可选 — 通过 `hermes skills install official/creative/concept-diagrams` 安装 | +| 路径 | `optional-skills/creative/concept-diagrams` | +| 版本 | `0.1.0` | +| 作者 | v1k22(原始 PR),移植至 hermes-agent | +| 许可证 | MIT | +| 平台 | linux, macos, windows | +| 标签 | `diagrams`, `svg`, `visualization`, `education`, `physics`, `chemistry`, `engineering` | +| 相关 skills | [`architecture-diagram`](/user-guide/skills/bundled/creative/creative-architecture-diagram), [`excalidraw`](/user-guide/skills/bundled/creative/creative-excalidraw), `generative-widgets` | + +## 参考:完整 SKILL.md + +:::info +以下是 Hermes 在触发本 skill 时加载的完整 skill 定义。这是 agent 在 skill 激活时所看到的指令内容。 +::: + +# 概念图 + +使用统一的扁平、简约设计系统生成生产级 SVG 图表。输出为单个自包含 HTML 文件,可在任何现代浏览器中一致渲染,并自动支持明暗模式。 + +## 适用范围 + +**最适合:** +- 物理装置、化学机制、数学曲线、生物学 +- 实物(飞机、涡轮机、智能手机、机械表、细胞) +- 解剖图、截面图、爆炸分层视图 +- 平面图、建筑改造图 +- 叙事流程(X 的生命周期、Y 的过程) +- 中心辐射型系统集成(智慧城市、IoT 网络、电网) +- 任何领域的教育/教科书风格视觉内容 +- 定量图表(分组柱状图、能量曲线) + +**优先考虑其他方案:** +- 具有深色科技风格的专用软件/云基础设施架构(如有 `architecture-diagram` 可用,优先使用) +- 手绘白板草图(如有 `excalidraw` 可用,优先使用) +- 动画说明或视频输出(考虑动画 skill) + +若已有更专业的 skill 适用于该主题,优先使用。若无合适选项,本 skill 可作为通用 SVG 图表备选方案——输出将呈现下文描述的简洁教育风格,适用于几乎任何主题。 + +## 工作流程 + +1. 确定图表类型(见下方"图表类型")。 +2. 使用设计系统规则布局组件。 +3. 使用 `templates/template.html` 作为包装器编写完整 HTML 页面——将 SVG 粘贴到模板中 `` 的位置。 +4. 保存为独立 `.html` 文件(例如 `~/my-diagram.html` 或 `./my-diagram.html`)。 +5. 用户直接在浏览器中打开——无需服务器,无需依赖。 + +可选:若用户需要可浏览的多图表画廊,参见底部"本地预览服务器"。 + +加载 HTML 模板: +``` +skill_view(name="concept-diagrams", file_path="templates/template.html") +``` + +模板内嵌完整 CSS 设计系统(`c-*` 颜色类、文本类、明暗变量、箭头标记样式)。你生成的 SVG 依赖这些类存在于宿主页面中。 + +--- + +## 设计系统 + +### 设计理念 + +- **扁平**:无渐变、无投影、无模糊、无发光、无霓虹效果。 +- **简约**:只展示核心内容,框内无装饰性图标。 +- **一致**:每张图表使用相同的颜色、间距、排版和描边宽度。 +- **暗色模式就绪**:所有颜色通过 CSS 类自动适配——无需为每种模式单独编写 SVG。 + +### 调色板 + +9 种色阶,每种 7 个色阶值。将类名放在 `` 或形状元素上;模板 CSS 自动处理明暗两种模式。 + +| 类名 | 50(最浅) | 100 | 200 | 400 | 600 | 800 | 900(最深) | +|------------|---------------|---------|---------|---------|---------|---------|---------------| +| `c-purple` | #EEEDFE | #CECBF6 | #AFA9EC | #7F77DD | #534AB7 | #3C3489 | #26215C | +| `c-teal` | #E1F5EE | #9FE1CB | #5DCAA5 | #1D9E75 | #0F6E56 | #085041 | #04342C | +| `c-coral` | #FAECE7 | #F5C4B3 | #F0997B | #D85A30 | #993C1D | #712B13 | #4A1B0C | +| `c-pink` | #FBEAF0 | #F4C0D1 | #ED93B1 | #D4537E | #993556 | #72243E | #4B1528 | +| `c-gray` | #F1EFE8 | #D3D1C7 | #B4B2A9 | #888780 | #5F5E5A | #444441 | #2C2C2A | +| `c-blue` | #E6F1FB | #B5D4F4 | #85B7EB | #378ADD | #185FA5 | #0C447C | #042C53 | +| `c-green` | #EAF3DE | #C0DD97 | #97C459 | #639922 | #3B6D11 | #27500A | #173404 | +| `c-amber` | #FAEEDA | #FAC775 | #EF9F27 | #BA7517 | #854F0B | #633806 | #412402 | +| `c-red` | #FCEBEB | #F7C1C1 | #F09595 | #E24B4A | #A32D2D | #791F1F | #501313 | + +#### 颜色分配规则 + +颜色编码**语义**,而非顺序。切勿像彩虹一样循环使用颜色。 + +- 按**类别**对节点分组——同类型的所有节点共用一种颜色。 +- 对中性/结构性节点(起点、终点、通用步骤、用户)使用 `c-gray`。 +- 每张图表使用 **2-3 种颜色**,而非 6 种以上。 +- 通用类别优先使用 `c-purple`、`c-teal`、`c-coral`、`c-pink`。 +- 将 `c-blue`、`c-green`、`c-amber`、`c-red` 保留用于语义含义(信息、成功、警告、错误)。 + +明暗色阶映射(由模板 CSS 处理——直接使用类名即可): +- 亮色模式:50 填充 + 600 描边 + 800 标题 / 600 副标题 +- 暗色模式:800 填充 + 200 描边 + 100 标题 / 200 副标题 + +### 排版 + +只有两种字体大小,不得例外。 + +| 类名 | 大小 | 字重 | 用途 | +|-------|------|--------|-----| +| `th` | 14px | 500 | 节点标题、区域标签 | +| `ts` | 12px | 400 | 副标题、描述、箭头标签 | +| `t` | 14px | 400 | 通用文本 | + +- **始终使用句首大写。** 禁止首字母大写(Title Case),禁止全大写(ALL CAPS)。 +- 每个 `` 必须带有类名(`t`、`ts` 或 `th`),不得有无类名的文本。 +- 框内所有文本使用 `dominant-baseline="central"`。 +- 框内居中文本使用 `text-anchor="middle"`。 + +**宽度估算(近似值):** +- 14px 字重 500:每字符约 8px +- 12px 字重 400:每字符约 6.5px +- 始终验证:`box_width >= (字符数 × px/字符) + 48`(每侧 24px 内边距) + +### 间距与布局 + +- **ViewBox**:`viewBox="0 0 680 H"`,其中 H = 内容高度 + 40px 缓冲。 +- **安全区域**:x=40 至 x=640,y=40 至 y=(H-40)。 +- **框间距**:最小 60px。 +- **框内边距**:水平 24px,垂直 12px。 +- **箭头间隙**:箭头与框边缘之间 10px。 +- **单行框**:高度 44px。 +- **双行框**:高度 56px,标题与副标题基线间距 18px。 +- **容器内边距**:每个容器内部最小 20px。 +- **最大嵌套层级**:2-3 层。在 680px 宽度下更深的嵌套会难以阅读。 + +### 描边与形状 + +- **描边宽度**:所有节点边框 0.5px,不得使用 1px 或 2px。 +- **矩形圆角**:节点使用 `rx="8"`,内层容器使用 `rx="12"`,外层容器使用 `rx="16"` 至 `rx="20"`。 +- **连接路径**:必须设置 `fill="none"`,否则 SVG 默认填充为黑色。 + +### 箭头标记 + +在**每个** SVG 开头包含以下 `` 块: + +```xml + + + + + +``` + +在线条上使用 `marker-end="url(#arrow)"`。箭头通过 `context-stroke` 继承线条颜色。 + +### CSS 类(由模板提供) + +模板页面提供: + +- 文本:`.t`、`.ts`、`.th` +- 中性:`.box`、`.arr`、`.leader`、`.node` +- 色阶:`.c-purple`、`.c-teal`、`.c-coral`、`.c-pink`、`.c-gray`、`.c-blue`、`.c-green`、`.c-amber`、`.c-red`(均自动支持明暗模式) + +你**无需**重新定义这些类——直接在 SVG 中应用即可。模板文件包含完整的 CSS 定义。 + +--- + +## SVG 样板代码 + +模板页面中的每个 SVG 均以如下结构开头: + +```xml + + + + + + + + + + +``` + +将 `{HEIGHT}` 替换为实际计算高度(最后一个元素底部 + 40px)。 + +### 节点模式 + +**单行节点(44px):** +```xml + + + Service name + +``` + +**双行节点(56px):** +```xml + + + Service name + Short description + +``` + +**连接线(无标签):** +```xml + +``` + +**容器(虚线或实线):** +```xml + + + Container label + Subtitle info + +``` + +--- + +## 图表类型 + +根据主题选择合适的布局: + +1. **流程图** — CI/CD 流水线、请求生命周期、审批工作流、数据处理。单向流(从上到下或从左到右),每行最多 4-5 个节点。 +2. **结构/包含图** — 云基础设施嵌套、分层系统架构。大型外层容器包含内层区域,虚线矩形表示逻辑分组。 +3. **API/端点映射** — REST 路由、GraphQL schema。从根节点树状展开,分支到资源组,每组包含端点节点。 +4. **微服务拓扑** — 服务网格、事件驱动系统。服务作为节点,箭头表示通信模式,消息队列位于服务之间。 +5. **数据流图** — ETL 流水线、流式架构。从数据源经处理流向数据汇,方向从左到右。 +6. **实物/结构图** — 交通工具、建筑、硬件、解剖图。使用与实物形态匹配的形状——弯曲体用 ``,锥形用 ``,圆柱部件用 ``/``,隔间用嵌套 ``。参见 `references/physical-shape-cookbook.md`。 +7. **基础设施/系统集成图** — 智慧城市、IoT 网络、多域系统。中心辐射布局,中央平台连接各子系统。按系统使用语义线型(`.data-line`、`.power-line`、`.water-pipe`、`.road`)。参见 `references/infrastructure-patterns.md`。 +8. **UI/仪表盘原型** — 管理面板、监控仪表盘。屏幕框架内嵌套图表/仪表/指示器元素。参见 `references/dashboard-patterns.md`。 + +对于实物图、基础设施图和仪表盘图,生成前请先加载对应的参考文件——每个文件提供现成的 CSS 类和形状原语。 + +--- + +## 验证清单 + +在最终确定任何 SVG 之前,验证以下**所有**项目: + +1. 每个 `` 都有类名 `t`、`ts` 或 `th`。 +2. 框内每个 `` 都有 `dominant-baseline="central"`。 +3. 用作箭头的每个连接 `` 或 `` 都有 `fill="none"`。 +4. 没有箭头线穿过无关的框。 +5. 14px 文本:`box_width >= (最长标签字符数 × 8) + 48`。 +6. 12px 文本:`box_width >= (最长标签字符数 × 6.5) + 48`。 +7. ViewBox 高度 = 最底部元素 + 40px。 +8. 所有内容在 x=40 至 x=640 范围内。 +9. 颜色类(`c-*`)放在 `` 或形状元素上,不得放在 `` 连接线上。 +10. 箭头 `` 块存在。 +11. 无渐变、投影、模糊或发光效果。 +12. 所有节点边框描边宽度为 0.5px。 + +--- + +## 输出与预览 + +### 默认:独立 HTML 文件 + +写入单个 `.html` 文件,用户可直接打开。无需服务器,无需依赖,离线可用。模式: + +```python +# 1. Load the template +template = skill_view("concept-diagrams", "templates/template.html") + +# 2. Fill in title, subtitle, and paste your SVG +html = template.replace( + "", "SN2 reaction mechanism" +).replace( + "", "Bimolecular nucleophilic substitution" +).replace( + "", svg_content +) + +# 3. Write to a user-chosen path (or ./ by default) +write_file("./sn2-mechanism.html", html) +``` + +告知用户如何打开: + +``` +# macOS +open ./sn2-mechanism.html +# Linux +xdg-open ./sn2-mechanism.html +``` + +### 可选:本地预览服务器(多图表画廊) + +仅在用户明确需要可浏览的多图表画廊时使用。 + +**规则:** +- 仅绑定到 `127.0.0.1`,绝不使用 `0.0.0.0`。在共享网络上将图表暴露在所有网络接口上存在安全风险。 +- 选择空闲端口(不得硬编码),并告知用户所选 URL。 +- 服务器是可选的、需用户主动选择的——优先使用独立 HTML 文件。 + +推荐模式(让操作系统选择空闲的临时端口): + +```bash +# Put each diagram in its own folder under .diagrams/ +mkdir -p .diagrams/sn2-mechanism +# ...write .diagrams/sn2-mechanism/index.html... + +# Serve on loopback only, free port +cd .diagrams && python3 -c " +import http.server, socketserver +with socketserver.TCPServer(('127.0.0.1', 0), http.server.SimpleHTTPRequestHandler) as s: + print(f'Serving at http://127.0.0.1:{s.server_address[1]}/') + s.serve_forever() +" & +``` + +若用户坚持使用固定端口,使用 `127.0.0.1:`——仍然不得使用 `0.0.0.0`。说明如何停止服务器(`kill %1` 或 `pkill -f "http.server"`)。 + +--- + +## 示例参考 + +`examples/` 目录内置 15 个完整、经过测试的图表。在编写同类型新图表之前,先浏览这些示例以获取可用模式: + +| 文件 | 类型 | 演示内容 | +|------|------|--------------| +| `hospital-emergency-department-flow.md` | 流程图 | 带语义颜色的优先级路由 | +| `feature-film-production-pipeline.md` | 流程图 | 分阶段工作流、水平子流程 | +| `automated-password-reset-flow.md` | 流程图 | 带错误分支的认证流程 | +| `autonomous-llm-research-agent-flow.md` | 流程图 | 回环箭头、决策分支 | +| `place-order-uml-sequence.md` | 时序图 | UML 时序图风格 | +| `commercial-aircraft-structure.md` | 实物图 | 使用路径、多边形、椭圆绘制真实形状 | +| `wind-turbine-structure.md` | 实物截面图 | 地下/地上分离、颜色编码 | +| `smartphone-layer-anatomy.md` | 爆炸视图 | 左右交替标签、分层组件 | +| `apartment-floor-plan-conversion.md` | 平面图 | 墙体、门、虚线红色标注改造方案 | +| `banana-journey-tree-to-smoothie.md` | 叙事流程 | 蜿蜒路径、渐进状态变化 | +| `cpu-ooo-microarchitecture.md` | 硬件流水线 | 扇出、内存层次侧边栏 | +| `sn2-reaction-mechanism.md` | 化学图 | 分子、弯曲箭头、能量曲线 | +| `smart-city-infrastructure.md` | 中心辐射图 | 每个系统使用语义线型 | +| `electricity-grid-flow.md` | 多阶段流程图 | 电压层次、流向标记 | +| `ml-benchmark-grouped-bar-chart.md` | 图表 | 分组柱状图、双轴 | + +使用以下命令加载任意示例: +``` +skill_view(name="concept-diagrams", file_path="examples/") +``` + +--- + +## 快速参考:何时使用何种图表 + +| 用户说 | 图表类型 | 建议颜色 | +|-----------|--------------|------------------| +| "展示流水线" | 流程图 | 灰色起止点,紫色步骤,红色错误,青色部署 | +| "画数据流" | 数据流水线(从左到右) | 灰色数据源,紫色处理,青色数据汇 | +| "可视化系统" | 结构图(包含关系) | 紫色容器,青色服务,珊瑚色数据 | +| "映射端点" | API 树状图 | 紫色根节点,每个资源组一种色阶 | +| "展示服务" | 微服务拓扑 | 灰色入口,青色服务,紫色总线,珊瑚色 worker | +| "画飞机/交通工具" | 实物图 | 路径、多边形、椭圆绘制真实形状 | +| "智慧城市/IoT" | 中心辐射集成图 | 每个子系统使用语义线型 | +| "展示仪表盘" | UI 原型 | 深色屏幕,图表颜色:青色、紫色、珊瑚色告警 | +| "电网/电力" | 多阶段流程图 | 电压层次(高/中/低压线宽) | +| "风力涡轮机/涡轮机" | 实物截面图 | 基础 + 塔筒截面 + 机舱颜色编码 | +| "X 的旅程/生命周期" | 叙事流程 | 蜿蜒路径,渐进状态变化 | +| "X 的层次/爆炸图" | 爆炸分层视图 | 垂直堆叠,交替标签 | +| "CPU/流水线" | 硬件流水线 | 垂直阶段,扇出到执行端口 | +| "平面图/公寓" | 平面图 | 墙体、门,虚线红色标注改造方案 | +| "反应机制" | 化学图 | 原子、化学键、弯曲箭头、过渡态、能量曲线 | \ No newline at end of file diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/optional/creative/creative-kanban-video-orchestrator.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/optional/creative/creative-kanban-video-orchestrator.md index b8f0a7946c1..15bbaaec8d1 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/optional/creative/creative-kanban-video-orchestrator.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/optional/creative/creative-kanban-video-orchestrator.md @@ -21,7 +21,7 @@ description: "规划、搭建并监控由 Hermes Kanban 支撑的多智能体视 | 许可证 | MIT | | 平台 | linux, macos, windows | | 标签 | `video`, `kanban`, `multi-agent`, `orchestration`, `production-pipeline` | -| 相关技能 | [`kanban-orchestrator`](/user-guide/skills/bundled/devops/devops-kanban-orchestrator)、[`kanban-worker`](/user-guide/skills/bundled/devops/devops-kanban-worker)、[`ascii-video`](/user-guide/skills/bundled/creative/creative-ascii-video)、[`manim-video`](/user-guide/skills/bundled/creative/creative-manim-video)、[`p5js`](/user-guide/skills/bundled/creative/creative-p5js)、[`comfyui`](/user-guide/skills/bundled/creative/creative-comfyui)、[`touchdesigner-mcp`](/user-guide/skills/bundled/creative/creative-touchdesigner-mcp)、[`blender-mcp`](/user-guide/skills/optional/creative/creative-blender-mcp)、[`pixel-art`](/user-guide/skills/bundled/creative/creative-pixel-art)、[`ascii-art`](/user-guide/skills/bundled/creative/creative-ascii-art)、[`songwriting-and-ai-music`](/user-guide/skills/bundled/creative/creative-songwriting-and-ai-music)、[`heartmula`](/user-guide/skills/bundled/media/media-heartmula)、[`songsee`](/user-guide/skills/bundled/media/media-songsee)、[`spotify`](/user-guide/skills/bundled/media/media-spotify)、[`youtube-content`](/user-guide/skills/bundled/media/media-youtube-content)、[`claude-design`](/user-guide/skills/bundled/creative/creative-claude-design)、[`excalidraw`](/user-guide/skills/bundled/creative/creative-excalidraw)、[`html-artifact`](/user-guide/skills/bundled/creative/creative-html-artifact)、[`baoyu-comic`](/user-guide/skills/bundled/creative/creative-baoyu-comic)、[`baoyu-infographic`](/user-guide/skills/bundled/creative/creative-baoyu-infographic)、[`humanizer`](/user-guide/skills/bundled/creative/creative-humanizer)、[`gif-search`](/user-guide/skills/bundled/media/media-gif-search)、[`meme-generation`](/user-guide/skills/optional/creative/creative-meme-generation) | +| 相关技能 | [`kanban-orchestrator`](/user-guide/skills/bundled/devops/devops-kanban-orchestrator)、[`kanban-worker`](/user-guide/skills/bundled/devops/devops-kanban-worker)、[`ascii-video`](/user-guide/skills/bundled/creative/creative-ascii-video)、[`manim-video`](/user-guide/skills/bundled/creative/creative-manim-video)、[`p5js`](/user-guide/skills/bundled/creative/creative-p5js)、[`comfyui`](/user-guide/skills/bundled/creative/creative-comfyui)、[`touchdesigner-mcp`](/user-guide/skills/bundled/creative/creative-touchdesigner-mcp)、[`blender-mcp`](/user-guide/skills/optional/creative/creative-blender-mcp)、[`pixel-art`](/user-guide/skills/bundled/creative/creative-pixel-art)、[`ascii-art`](/user-guide/skills/bundled/creative/creative-ascii-art)、[`songwriting-and-ai-music`](/user-guide/skills/bundled/creative/creative-songwriting-and-ai-music)、[`heartmula`](/user-guide/skills/bundled/media/media-heartmula)、[`songsee`](/user-guide/skills/bundled/media/media-songsee)、[`spotify`](/user-guide/skills/bundled/media/media-spotify)、[`youtube-content`](/user-guide/skills/bundled/media/media-youtube-content)、[`claude-design`](/user-guide/skills/bundled/creative/creative-claude-design)、[`excalidraw`](/user-guide/skills/bundled/creative/creative-excalidraw)、[`architecture-diagram`](/user-guide/skills/bundled/creative/creative-architecture-diagram)、[`concept-diagrams`](/user-guide/skills/optional/creative/creative-concept-diagrams)、[`baoyu-comic`](/user-guide/skills/bundled/creative/creative-baoyu-comic)、[`baoyu-infographic`](/user-guide/skills/bundled/creative/creative-baoyu-infographic)、[`humanizer`](/user-guide/skills/bundled/creative/creative-humanizer)、[`gif-search`](/user-guide/skills/bundled/media/media-gif-search)、[`meme-generation`](/user-guide/skills/optional/creative/creative-meme-generation) | ## 参考:完整 SKILL.md diff --git a/website/sidebars.ts b/website/sidebars.ts index b8efcef0624..dec160700e2 100644 --- a/website/sidebars.ts +++ b/website/sidebars.ts @@ -150,6 +150,7 @@ const sidebars: SidebarsConfig = { 'user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-claude-code', 'user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-codex', 'user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-hermes-agent', + 'user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-kanban-codex-lane', 'user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-opencode', ], }, @@ -159,6 +160,7 @@ const sidebars: SidebarsConfig = { key: 'skills-bundled-creative', collapsed: true, items: [ + 'user-guide/skills/bundled/creative/creative-architecture-diagram', 'user-guide/skills/bundled/creative/creative-ascii-art', 'user-guide/skills/bundled/creative/creative-ascii-video', 'user-guide/skills/bundled/creative/creative-baoyu-infographic', @@ -166,12 +168,12 @@ const sidebars: SidebarsConfig = { 'user-guide/skills/bundled/creative/creative-comfyui', 'user-guide/skills/bundled/creative/creative-design-md', 'user-guide/skills/bundled/creative/creative-excalidraw', - 'user-guide/skills/bundled/creative/creative-html-artifact', 'user-guide/skills/bundled/creative/creative-humanizer', 'user-guide/skills/bundled/creative/creative-manim-video', 'user-guide/skills/bundled/creative/creative-p5js', 'user-guide/skills/bundled/creative/creative-popular-web-designs', 'user-guide/skills/bundled/creative/creative-pretext', + 'user-guide/skills/bundled/creative/creative-sketch', 'user-guide/skills/bundled/creative/creative-songwriting-and-ai-music', 'user-guide/skills/bundled/creative/creative-touchdesigner-mcp', ], @@ -385,6 +387,7 @@ const sidebars: SidebarsConfig = { 'user-guide/skills/optional/creative/creative-baoyu-article-illustrator', 'user-guide/skills/optional/creative/creative-baoyu-comic', 'user-guide/skills/optional/creative/creative-blender-mcp', + 'user-guide/skills/optional/creative/creative-concept-diagrams', 'user-guide/skills/optional/creative/creative-creative-ideation', 'user-guide/skills/optional/creative/creative-hyperframes', 'user-guide/skills/optional/creative/creative-kanban-video-orchestrator', From 9a2f2756f7e6d1ca1b761ad330c6fd2c0b02d95e Mon Sep 17 00:00:00 2001 From: brooklyn! Date: Fri, 19 Jun 2026 08:59:09 -0500 Subject: [PATCH 47/90] fix(desktop): allow selecting slash output and shell logs in thread (#49063) System messages (/debug, /status, etc.) were not in the desktop app's text-selection allowlist, so log output in the thread could not be copied. --- apps/desktop/src/components/assistant-ui/thread.tsx | 5 ++++- apps/desktop/src/components/chat/terminal-output.tsx | 6 +++++- apps/desktop/src/styles.css | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/components/assistant-ui/thread.tsx b/apps/desktop/src/components/assistant-ui/thread.tsx index c5b20cedd3e..1ac97c200ca 100644 --- a/apps/desktop/src/components/assistant-ui/thread.tsx +++ b/apps/desktop/src/components/assistant-ui/thread.tsx @@ -859,7 +859,10 @@ const ProcessNotificationNote: FC<{ text: string }> = ({ text }) => { output -
+          
             {detail}
           
diff --git a/apps/desktop/src/components/chat/terminal-output.tsx b/apps/desktop/src/components/chat/terminal-output.tsx index 946ec2386be..034f20f2a81 100644 --- a/apps/desktop/src/components/chat/terminal-output.tsx +++ b/apps/desktop/src/components/chat/terminal-output.tsx @@ -41,7 +41,11 @@ export function TerminalOutput({ className, text }: TerminalOutputProps) { }, [text]) return ( -
+
         {text}
       
diff --git a/apps/desktop/src/styles.css b/apps/desktop/src/styles.css index 03b348c9d84..2aff7a21c77 100644 --- a/apps/desktop/src/styles.css +++ b/apps/desktop/src/styles.css @@ -680,6 +680,7 @@ textarea, [contenteditable]:not([contenteditable='false']), [data-slot='aui_user-message-root'], [data-slot='aui_assistant-message-content'], +[data-slot='aui_system-message-root'], [data-selectable-text='true'], [data-selectable-text='true'] * { -webkit-user-select: text; From a7b4fbcbc179dd51913f065dc2fe44d862ac5464 Mon Sep 17 00:00:00 2001 From: srojk34 <286497132+srojk34@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:49:38 +0300 Subject: [PATCH 48/90] fix(tui): guard /update against hosted dashboard mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /update calls dieWithCode(42) which tears down the gateway and hard-exits the Node process — the same PTY-killing path that /exit and /quit use. In the hosted dashboard chat there is no Python update wrapper to catch exit code 42, and the PTY death bricks the tab until a browser refresh. Mirror the DASHBOARD_TUI_MODE guard that #48882 added for /exit and /quit: refuse early with an explanatory message. --- .../src/__tests__/createSlashHandler.test.ts | 18 +++++++++++++++++- ui-tui/src/app/slash/commands/core.ts | 9 +++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/ui-tui/src/__tests__/createSlashHandler.test.ts b/ui-tui/src/__tests__/createSlashHandler.test.ts index 415dd4c0f3c..8f49dd9a513 100644 --- a/ui-tui/src/__tests__/createSlashHandler.test.ts +++ b/ui-tui/src/__tests__/createSlashHandler.test.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createSlashHandler } from '../app/createSlashHandler.js' import { getOverlayState, resetOverlayState } from '../app/overlayStore.js' -import { DASHBOARD_EXIT_DISABLED_MESSAGE } from '../app/slash/commands/core.js' +import { DASHBOARD_EXIT_DISABLED_MESSAGE, DASHBOARD_UPDATE_DISABLED_MESSAGE } from '../app/slash/commands/core.js' import { getUiState, patchUiState, resetUiState } from '../app/uiStore.js' import { TUI_SESSION_MODEL_FLAG } from '../domain/slash.js' @@ -118,6 +118,22 @@ describe('createSlashHandler', () => { vi.useRealTimers() }) + it('refuses /update in hosted dashboard chat instead of killing the PTY', () => { + vi.useFakeTimers() + envState.dashboardTuiMode = true + const ctx = buildCtx() + + expect(createSlashHandler(ctx)('/update')).toBe(true) + expect(ctx.session.dieWithCode).not.toHaveBeenCalled() + expect(ctx.gateway.gw.request).not.toHaveBeenCalled() + expect(ctx.transcript.sys).toHaveBeenCalledWith(DASHBOARD_UPDATE_DISABLED_MESSAGE) + + vi.advanceTimersByTime(150) + expect(ctx.session.dieWithCode).not.toHaveBeenCalled() + + vi.useRealTimers() + }) + it('routes /status to live session.status instead of slash worker', async () => { patchUiState({ sid: 'sid-abc' }) const rpc = vi.fn(() => Promise.resolve({ output: 'Hermes TUI Status' })) diff --git a/ui-tui/src/app/slash/commands/core.ts b/ui-tui/src/app/slash/commands/core.ts index 7c5a79505ad..5c74eb3eb42 100644 --- a/ui-tui/src/app/slash/commands/core.ts +++ b/ui-tui/src/app/slash/commands/core.ts @@ -81,6 +81,9 @@ const DETAILS_SECTION_USAGE = 'usage: /details
[hidden|collapsed|expan export const DASHBOARD_EXIT_DISABLED_MESSAGE = 'exit is disabled in hosted dashboard chat — use /new to start a fresh session' +export const DASHBOARD_UPDATE_DISABLED_MESSAGE = + 'update is disabled in hosted dashboard chat — the hosted environment is managed separately' + export const coreCommands: SlashCommand[] = [ { help: 'list commands + hotkeys', @@ -140,6 +143,12 @@ export const coreCommands: SlashCommand[] = [ help: 'update Hermes Agent to the latest version (exits TUI)', name: 'update', run: (_arg, ctx) => { + if (DASHBOARD_TUI_MODE) { + ctx.transcript.sys(DASHBOARD_UPDATE_DISABLED_MESSAGE) + + return + } + ctx.transcript.sys('exiting TUI to run update...') // Exit code 42 signals the Python wrapper to exec `hermes update`. // Use dieWithCode for proper cleanup (gateway kill + Ink unmount). From 160bb565b4ec05b89c57808f2b8d425b39591475 Mon Sep 17 00:00:00 2001 From: Cdddo Date: Thu, 18 Jun 2026 20:51:37 -0600 Subject: [PATCH 49/90] feat(tts): expose speaker_id on built-in Piper provider The built-in Piper provider (tts.provider: piper, Python piper-tts package) already constructs piper.SynthesisConfig for the advanced tuning knobs, but did not forward speaker_id from the user config. This wires tts.piper.speaker_id through to SynthesisConfig.speaker_id so multi-speaker ONNX models (e.g. libritts_r) can be addressed via config without dropping to the command-provider path. Changes: - Add speaker_id to the has_advanced tuple so setting it triggers SynthesisConfig construction (same gating as the other knobs). - Pass speaker_id=speaker_id to SynthesisConfig. Defaults to 0 (Piper's own default; single-speaker models ignore the field). - Tolerant parse: bad input (non-int strings, lists, dicts) is dropped to 0 instead of raising. Booleans are rejected outright (True/False would silently coerce to 1/0 and hide a config mistake). Mirrors the same shape as the command-provider's _resolve_command_tts_optional_number helper. speaker_id is applied per-call via syn_config.speaker_id, so the PiperVoice cache key is intentionally left as just (model, cuda) -- the same loaded model serves all speakers. Tests cover the config knob, the tolerant parse, and the no-reload invariant. sentence_silence is intentionally not added here: the Python piper-tts SynthesisConfig does not expose that field (CLI-only). --- tests/tools/test_tts_piper.py | 93 ++++++++++++++++++++++++++++++++++- tools/tts_tool.py | 22 ++++++++- 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/tests/tools/test_tts_piper.py b/tests/tools/test_tts_piper.py index c30b26dc9b9..78567adf9bb 100644 --- a/tests/tools/test_tts_piper.py +++ b/tests/tools/test_tts_piper.py @@ -8,6 +8,7 @@ without requiring the ``piper-tts`` package to actually be installed import json import sys +import types from pathlib import Path from unittest.mock import MagicMock, patch @@ -219,7 +220,7 @@ class TestGeneratePiperTts: # The SynthesisConfig import happens inline inside _generate_piper_tts # via ``from piper import SynthesisConfig``. Inject a fake piper - # module so that import resolves. + # module so that that import resolves. monkeypatch.setitem(sys.modules, "piper", FakePiperModule) config = { @@ -239,6 +240,96 @@ class TestGeneratePiperTts: assert kwargs["length_scale"] == 2.0 assert kwargs["volume"] == 0.8 + def test_speaker_id_passed_through_to_synconfig(self, tmp_path, monkeypatch): + """speaker_id flows from config to SynthesisConfig when set.""" + model = self._prepare_voice_files(tmp_path) + monkeypatch.setattr(tts_tool, "_import_piper", lambda: _StubPiperVoice) + + fake_syn_cls = MagicMock() + monkeypatch.setitem(sys.modules, "piper", types.SimpleNamespace(SynthesisConfig=fake_syn_cls)) + + config = {"piper": {"voice": str(model), "speaker_id": 2}} + tts_tool._generate_piper_tts("hi", str(tmp_path / "out.wav"), config) + + fake_syn_cls.assert_called_once() + assert fake_syn_cls.call_args.kwargs["speaker_id"] == 2 + + def test_speaker_id_alone_triggers_synconfig(self, tmp_path, monkeypatch): + """Setting ONLY speaker_id (no other advanced knobs) still constructs SynthesisConfig. + + Regression guard: has_advanced must include speaker_id, otherwise + this knob gets silently dropped on the simplest configuration. + """ + model = self._prepare_voice_files(tmp_path) + monkeypatch.setattr(tts_tool, "_import_piper", lambda: _StubPiperVoice) + + fake_syn_cls = MagicMock() + monkeypatch.setitem(sys.modules, "piper", types.SimpleNamespace(SynthesisConfig=fake_syn_cls)) + + config = {"piper": {"voice": str(model), "speaker_id": 1}} + tts_tool._generate_piper_tts("hi", str(tmp_path / "out.wav"), config) + + fake_syn_cls.assert_called_once() + + def test_speaker_id_default_zero_when_unset(self, tmp_path, monkeypatch): + """No speaker_id in config → SynthesisConfig.speaker_id == 0 (Piper's default).""" + model = self._prepare_voice_files(tmp_path) + monkeypatch.setattr(tts_tool, "_import_piper", lambda: _StubPiperVoice) + + fake_syn_cls = MagicMock() + monkeypatch.setitem(sys.modules, "piper", types.SimpleNamespace(SynthesisConfig=fake_syn_cls)) + + config = {"piper": {"voice": str(model), "length_scale": 1.5}} + tts_tool._generate_piper_tts("hi", str(tmp_path / "out.wav"), config) + + assert fake_syn_cls.call_args.kwargs["speaker_id"] == 0 + + def test_speaker_id_bool_rejected_to_zero(self, tmp_path, monkeypatch): + """True/False would coerce to 1/0 and hide a config mistake — reject outright.""" + model = self._prepare_voice_files(tmp_path) + monkeypatch.setattr(tts_tool, "_import_piper", lambda: _StubPiperVoice) + + fake_syn_cls = MagicMock() + monkeypatch.setitem(sys.modules, "piper", types.SimpleNamespace(SynthesisConfig=fake_syn_cls)) + + for bad in (True, False): + fake_syn_cls.reset_mock() + config = {"piper": {"voice": str(model), "speaker_id": bad}} + tts_tool._generate_piper_tts("hi", str(tmp_path / f"out-{bad}.wav"), config) + assert fake_syn_cls.call_args.kwargs["speaker_id"] == 0 + + def test_speaker_id_non_int_dropped_to_zero(self, tmp_path, monkeypatch): + """Unparseable config (string, list, dict) drops to 0 instead of raising.""" + model = self._prepare_voice_files(tmp_path) + monkeypatch.setattr(tts_tool, "_import_piper", lambda: _StubPiperVoice) + + fake_syn_cls = MagicMock() + monkeypatch.setitem(sys.modules, "piper", types.SimpleNamespace(SynthesisConfig=fake_syn_cls)) + + for bad in ("two", [1, 2], {"k": 1}, None): + fake_syn_cls.reset_mock() + config = {"piper": {"voice": str(model), "speaker_id": bad}} + tts_tool._generate_piper_tts("hi", str(tmp_path / f"out-{type(bad).__name__}.wav"), config) + assert fake_syn_cls.call_args.kwargs["speaker_id"] == 0 + + def test_speaker_id_does_not_invalidate_voice_cache(self, tmp_path, monkeypatch): + """Switching speaker_id between calls must NOT trigger a model reload. + + PiperVoice is bound to a model, not a speaker — speaker is applied + per-call via syn_config.speaker_id. The voice cache should serve the + same PiperVoice instance for the same (model, cuda) regardless of + how many distinct speaker_ids the user cycles through. + """ + model = self._prepare_voice_files(tmp_path) + monkeypatch.setattr(tts_tool, "_import_piper", lambda: _StubPiperVoice) + + for speaker in (0, 1, 2, 3): + config = {"piper": {"voice": str(model), "speaker_id": speaker}} + tts_tool._generate_piper_tts("hi", str(tmp_path / f"out-{speaker}.wav"), config) + + # Only one PiperVoice.load() call across four calls with different speakers. + assert _StubPiperVoice.loaded == [str(model)] + # --------------------------------------------------------------------------- # text_to_speech_tool end-to-end (provider == "piper") diff --git a/tools/tts_tool.py b/tools/tts_tool.py index c6e7c22de0f..02fe4e5bda5 100644 --- a/tools/tts_tool.py +++ b/tools/tts_tool.py @@ -1889,6 +1889,18 @@ def _generate_piper_tts(text: str, output_path: str, tts_config: Dict[str, Any]) model_path = _resolve_piper_voice_path(voice_name, download_dir) + # Tolerant speaker_id parse: drop bad input (non-int strings, lists, dicts) + # to 0 (Piper's own default). Booleans are rejected outright — True/False + # would silently coerce to 1/0 and hide a config mistake. + _raw_speaker = piper_config.get("speaker_id", 0) + if isinstance(_raw_speaker, bool) or not isinstance(_raw_speaker, int): + speaker_id = 0 + else: + speaker_id = _raw_speaker + + # speaker_id is applied per-call via syn_config.speaker_id — the same + # PiperVoice instance serves all speakers, so it stays out of the cache + # key. Multi-speaker workflows share one model load. cache_key = f"{model_path}::cuda={use_cuda}" global _piper_voice_cache if cache_key not in _piper_voice_cache: @@ -1903,7 +1915,14 @@ def _generate_piper_tts(text: str, output_path: str, tts_config: Dict[str, Any]) syn_config = None has_advanced = any( k in piper_config - for k in ("length_scale", "noise_scale", "noise_w_scale", "volume", "normalize_audio") + for k in ( + "length_scale", + "noise_scale", + "noise_w_scale", + "volume", + "normalize_audio", + "speaker_id", + ) ) if has_advanced: try: @@ -1914,6 +1933,7 @@ def _generate_piper_tts(text: str, output_path: str, tts_config: Dict[str, Any]) noise_w_scale=float(piper_config.get("noise_w_scale", 0.8)), volume=float(piper_config.get("volume", 1.0)), normalize_audio=bool(piper_config.get("normalize_audio", True)), + speaker_id=speaker_id, ) except ImportError: logger.warning( From ddca590cac5443f72b09039906f41aa259cef004 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Fri, 19 Jun 2026 06:46:47 -0700 Subject: [PATCH 50/90] chore: add Cdddo to AUTHOR_MAP --- scripts/release.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/release.py b/scripts/release.py index 0ff464e61f0..452b59964e3 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -57,6 +57,7 @@ AUTHOR_MAP = { "despitemeguru@gmail.com": "definitelynotguru", "chaslui@outlook.com": "ChasLui", "rio.jeong@thebytesize.ai": "rio-jeong", + "cdddo@users.noreply.github.com": "Cdddo", "yehaotian@xuanshudeMac-mini.local": "ArcanePivot", "dbeyer7@gmail.com": "benegessarit", "264773240+MrDiamondBallz@users.noreply.github.com": "MrDiamondBallz", From 01a6f11896673764a97fd51a5a36dfc73e8ab0b9 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:07:47 +0530 Subject: [PATCH 51/90] fix(debug): include gui.log (dashboard/TUI/pty/websocket) in hermes debug share MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gui.log was registered in hermes_cli/logs.py::LOG_FILES (and surfaced by `hermes logs gui`) but was never wired into `hermes debug share`. The share report captured agent/errors/gateway/desktop tails plus full agent/gateway/ desktop logs — but nothing from gui.log, the surface the dashboard, TUI-over- PTY bridge, and websocket layer (hermes_cli.web_server / pty_bridge / tui_gateway) actually write to. A user reporting a dashboard or TUI bug shared zero breadcrumbs from the broken surface. Wire gui.log through all three share surfaces, matching the existing pattern: - _capture_default_log_snapshots(): capture the gui snapshot (redacted like the rest) - collect_debug_report(): add the gui.log summary tail block - build_debug_share(): pull gui full_text, prepend dump header + redaction banner, add to the upload loop - run_debug_share() --local branch: same, plus the local print block - _PRIVACY_NOTICE: name gui.log in both bullets Redaction is inherited for free — the gui snapshot goes through the same _capture_log_snapshot(..., redact=redact) path, so secrets are scrubbed in both the tail and full text (verified E2E: seeded key masked by default, passes through under --no-redact, raw token never leaks). Tests: seed gui.log in the fixture, add test_report_includes_gui_log, and bump the upload-count tripwire 4->5 (test_share_uploads_five_pastes). --- hermes_cli/debug.py | 27 ++++++++++++++++++++---- tests/hermes_cli/test_debug.py | 29 ++++++++++++++++++++------ website/docs/reference/cli-commands.md | 2 +- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/hermes_cli/debug.py b/hermes_cli/debug.py index 809676d1fc8..e5627f24bf5 100644 --- a/hermes_cli/debug.py +++ b/hermes_cli/debug.py @@ -191,10 +191,10 @@ _PRIVACY_NOTICE = """\ ⚠️ This will upload the following to a public paste service: • System info (OS, Python version, Hermes version, provider, which API keys are configured — NOT the actual keys) - • Recent log lines (agent.log, errors.log, gateway.log, desktop.log — may - contain conversation fragments and file paths) - • Full agent.log, gateway.log, and desktop.log (up to 512 KB each — likely - contains conversation content, tool outputs, and file paths) + • Recent log lines (agent.log, errors.log, gateway.log, gui.log, desktop.log + — may contain conversation fragments and file paths) + • Full agent.log, gateway.log, gui.log, and desktop.log (up to 512 KB each — + likely contains conversation content, tool outputs, and file paths) Pastes auto-delete after 6 hours. """ @@ -503,6 +503,9 @@ def _capture_default_log_snapshots( "gateway": _capture_log_snapshot( "gateway", tail_lines=errors_lines, redact=redact ), + "gui": _capture_log_snapshot( + "gui", tail_lines=errors_lines, redact=redact + ), "desktop": _capture_log_snapshot( "desktop", tail_lines=errors_lines, redact=redact ), @@ -574,6 +577,10 @@ def collect_debug_report( buf.write(log_snapshots["gateway"].tail_text) buf.write("\n\n") + buf.write(f"--- gui.log (last {errors_lines} lines) ---\n") + buf.write(log_snapshots["gui"].tail_text) + buf.write("\n\n") + buf.write(f"--- desktop.log (last {errors_lines} lines) ---\n") buf.write(log_snapshots["desktop"].tail_text) buf.write("\n") @@ -639,6 +646,7 @@ def build_debug_share( ) agent_log = log_snapshots["agent"].full_text gateway_log = log_snapshots["gateway"].full_text + gui_log = log_snapshots["gui"].full_text desktop_log = log_snapshots["desktop"].full_text # Prepend dump header to each full log so every paste is self-contained. @@ -646,6 +654,8 @@ def build_debug_share( agent_log = dump_text + "\n\n--- full agent.log ---\n" + agent_log if gateway_log: gateway_log = dump_text + "\n\n--- full gateway.log ---\n" + gateway_log + if gui_log: + gui_log = dump_text + "\n\n--- full gui.log ---\n" + gui_log if desktop_log: desktop_log = dump_text + "\n\n--- full desktop.log ---\n" + desktop_log @@ -657,6 +667,8 @@ def build_debug_share( agent_log = _REDACTION_BANNER + agent_log if gateway_log: gateway_log = _REDACTION_BANNER + gateway_log + if gui_log: + gui_log = _REDACTION_BANNER + gui_log if desktop_log: desktop_log = _REDACTION_BANNER + desktop_log @@ -670,6 +682,7 @@ def build_debug_share( for label, content in ( ("agent.log", agent_log), ("gateway.log", gateway_log), + ("gui.log", gui_log), ("desktop.log", desktop_log), ): if not content: @@ -712,11 +725,14 @@ def run_debug_share(args): ) agent_log = log_snapshots["agent"].full_text gateway_log = log_snapshots["gateway"].full_text + gui_log = log_snapshots["gui"].full_text desktop_log = log_snapshots["desktop"].full_text if agent_log: agent_log = dump_text + "\n\n--- full agent.log ---\n" + agent_log if gateway_log: gateway_log = dump_text + "\n\n--- full gateway.log ---\n" + gateway_log + if gui_log: + gui_log = dump_text + "\n\n--- full gui.log ---\n" + gui_log if desktop_log: desktop_log = dump_text + "\n\n--- full desktop.log ---\n" + desktop_log if redact: @@ -725,12 +741,15 @@ def run_debug_share(args): agent_log = _REDACTION_BANNER + agent_log if gateway_log: gateway_log = _REDACTION_BANNER + gateway_log + if gui_log: + gui_log = _REDACTION_BANNER + gui_log if desktop_log: desktop_log = _REDACTION_BANNER + desktop_log print(report) for title, body in ( ("FULL agent.log", agent_log), ("FULL gateway.log", gateway_log), + ("FULL gui.log", gui_log), ("FULL desktop.log", desktop_log), ): if body: diff --git a/tests/hermes_cli/test_debug.py b/tests/hermes_cli/test_debug.py index 615e379f7d2..f8d958ffa86 100644 --- a/tests/hermes_cli/test_debug.py +++ b/tests/hermes_cli/test_debug.py @@ -31,6 +31,9 @@ def hermes_home(tmp_path, monkeypatch): (logs_dir / "gateway.log").write_text( "2026-04-12 17:00:10 INFO gateway.run: started\n" ) + (logs_dir / "gui.log").write_text( + "2026-04-12 17:00:12 INFO hermes_cli.web_server: dashboard request\n" + ) (logs_dir / "desktop.log").write_text( "2026-04-12 17:00:15 INFO desktop: backend spawned\n" ) @@ -454,6 +457,15 @@ class TestCollectDebugReport: assert "--- gateway.log" in report + def test_report_includes_gui_log(self, hermes_home): + from hermes_cli.debug import collect_debug_report + + with patch("hermes_cli.dump.run_dump"): + report = collect_debug_report(log_lines=50) + + assert "--- gui.log" in report + assert "dashboard request" in report + def test_report_includes_desktop_log(self, hermes_home): from hermes_cli.debug import collect_debug_report @@ -538,8 +550,8 @@ class TestRunDebugShare: assert "FULL agent.log" in out assert "FULL gateway.log" in out - def test_share_uploads_four_pastes(self, hermes_home, capsys): - """Successful share uploads report + agent.log + gateway.log + desktop.log.""" + def test_share_uploads_five_pastes(self, hermes_home, capsys): + """Successful share uploads report + agent.log + gateway.log + gui.log + desktop.log.""" from hermes_cli.debug import run_debug_share args = MagicMock() @@ -561,15 +573,17 @@ class TestRunDebugShare: run_debug_share(args) out = capsys.readouterr().out - # Should have 4 uploads: report, agent.log, gateway.log, desktop.log - assert call_count[0] == 4 + # Should have 5 uploads: report, agent.log, gateway.log, gui.log, desktop.log + assert call_count[0] == 5 assert "paste.rs/paste1" in out # Report assert "paste.rs/paste2" in out # agent.log assert "paste.rs/paste3" in out # gateway.log - assert "paste.rs/paste4" in out # desktop.log + assert "paste.rs/paste4" in out # gui.log + assert "paste.rs/paste5" in out # desktop.log assert "Report" in out assert "agent.log" in out assert "gateway.log" in out + assert "gui.log" in out assert "desktop.log" in out # Each log paste should start with the dump header @@ -579,7 +593,10 @@ class TestRunDebugShare: gateway_paste = uploaded_content[2] assert "--- hermes dump ---" in gateway_paste assert "--- full gateway.log ---" in gateway_paste - desktop_paste = uploaded_content[3] + gui_paste = uploaded_content[3] + assert "--- hermes dump ---" in gui_paste + assert "--- full gui.log ---" in gui_paste + desktop_paste = uploaded_content[4] assert "--- hermes dump ---" in desktop_paste assert "--- full desktop.log ---" in desktop_paste diff --git a/website/docs/reference/cli-commands.md b/website/docs/reference/cli-commands.md index 3071ac0e5fc..90bc1ef83a6 100644 --- a/website/docs/reference/cli-commands.md +++ b/website/docs/reference/cli-commands.md @@ -734,7 +734,7 @@ Upload a debug report (system info + recent logs) to a paste service and get a s | `--expire ` | Paste expiry in days (default: 7). | | `--local` | Print the report locally instead of uploading. | -The report includes system info (OS, Python version, Hermes version), recent agent and gateway logs (512 KB limit per file), and redacted API key status. Keys are always redacted — no secrets are uploaded. +The report includes system info (OS, Python version, Hermes version), recent agent, gateway, GUI/dashboard, and desktop logs (512 KB limit per file), and redacted API key status. Keys are always redacted — no secrets are uploaded. Paste services tried in order: paste.rs, dpaste.com. From c1ffd4c3b4cfb8c3daa33594d908d5985825d48b Mon Sep 17 00:00:00 2001 From: OYLFLMH Date: Thu, 18 Jun 2026 07:59:37 +0000 Subject: [PATCH 52/90] fix(cli): make refresh_interval configurable, default to 0 (disabled) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commit 6724daa2c added refresh_interval=1.0 to keep the idle clock ticking, but unconditional 1 Hz redraws in non-fullscreen prompt_toolkit mode cause terminal emulators (Xshell, iTerm2, Windows Terminal) to auto-scroll to the bottom on every tick — breaking scroll-up to read history. Drive it from display.cli_refresh_interval (0 = disabled, the default) so users who want the ticking clock can opt in without affecting everyone. Fixes: #48309 Related: 6724daa2c, 8972a151a --- cli.py | 14 +++++++------- hermes_cli/config.py | 6 ++++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/cli.py b/cli.py index f6a9393d34a..e0a8676ceee 100644 --- a/cli.py +++ b/cli.py @@ -13527,13 +13527,13 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin): style=style, full_screen=False, mouse_support=False, - # The status bar contains wall-clock read-outs (live prompt elapsed - # and idle-since-last-turn). Once a turn finishes there may be no - # further events to invalidate the app, so prompt_toolkit would keep - # rendering the first post-turn value (usually ``✓ 0s``) forever. - # A low-rate refresh keeps the clock honest without reintroducing a - # custom repaint thread or touching conversation state. - refresh_interval=1.0, + # Read from display.cli_refresh_interval (default 0 = disabled). + # When non-zero, prompt_toolkit redraws the UI on this cadence + # during idle, keeping wall-clock status-bar read-outs ticking. + # Set to 0 to suppress background redraws entirely — avoids + # fighting terminal auto-scroll in non-fullscreen mode (Xshell, + # iTerm2, Windows Terminal). See #48309. + refresh_interval=float(CLI_CONFIG.get("display", {}).get("cli_refresh_interval", 0)), # Erase the live bottom chrome (status bar, input box, separator # rules) on exit instead of freezing a final copy into scrollback. # Without this, prompt_toolkit's render_as_done teardown repaints diff --git a/hermes_cli/config.py b/hermes_cli/config.py index c81df25c03b..3b12cacb37b 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -1581,6 +1581,12 @@ DEFAULT_CONFIG = { # TUI busy indicator style: kaomoji (default), emoji, unicode (braille # spinner), or ascii. Live-swappable via `/indicator