From 06b1d9fc41ffd86ee9144109d250239ceed1def7 Mon Sep 17 00:00:00 2001 From: Kbro Date: Sat, 4 Jul 2026 01:34:24 -0500 Subject: [PATCH 1/3] Add Jarvis-facing Ollama wrapper CLI with King Wen routing and task-aware model selection --- src/openjarvis/agents/_stubs.py | 81 ++++ src/openjarvis/agents/channel_agent.py | 4 + src/openjarvis/cli/__init__.py | 4 +- src/openjarvis/cli/ask.py | 51 ++- src/openjarvis/cli/chat_cmd.py | 5 + src/openjarvis/cli/ollama_launch_cmd.py | 459 ++++++++++++++++++++ src/openjarvis/emotion/kingwen.py | 214 +++++++++ src/openjarvis/engine/ollama_model_usage.py | 92 ++++ tests/cli/test_ollama_launch_cmd.py | 86 ++++ 9 files changed, 993 insertions(+), 3 deletions(-) create mode 100644 src/openjarvis/cli/ollama_launch_cmd.py create mode 100644 src/openjarvis/emotion/kingwen.py create mode 100644 src/openjarvis/engine/ollama_model_usage.py create mode 100644 tests/cli/test_ollama_launch_cmd.py diff --git a/src/openjarvis/agents/_stubs.py b/src/openjarvis/agents/_stubs.py index 3d800b406e..a19fbf7435 100644 --- a/src/openjarvis/agents/_stubs.py +++ b/src/openjarvis/agents/_stubs.py @@ -67,11 +67,20 @@ def __init__( temperature: Optional[float] = None, max_tokens: Optional[int] = None, prompt_builder: Optional[Any] = None, + capture_writer: Optional[Any] = None, + emotion_provider: Optional[Any] = None, + kingwen_session_id: str = "openjarvis", ) -> None: self._engine = engine self._model = model self._bus = bus self._prompt_builder = prompt_builder + if not hasattr(self, "_capture_writer"): + self._capture_writer = capture_writer + if not hasattr(self, "_emotion_provider"): + self._emotion_provider = emotion_provider + if not hasattr(self, "_kingwen_session_id"): + self._kingwen_session_id = kingwen_session_id # Three-tier resolution: explicit arg > config > class default > hardcoded if temperature is not None and max_tokens is not None: @@ -200,6 +209,30 @@ def _generate(self, messages: list[Message], **extra_kwargs: Any) -> dict: **extra_kwargs, ) + if self._capture_writer and hasattr(self._capture_writer, "write"): + try: + self._capture_writer.write( + prompt=self._serialize_messages(messages), + response=result.get("content", ""), + model=self._model, + engine=getattr(self._engine, "engine_id", ""), + agent=getattr(self, "agent_id", ""), + session_id=getattr(self, "_kingwen_session_id", "openjarvis"), + tool_calls=result.get("tool_calls", []), + tool_results=result.get("tool_results", []), + messages=messages, + emotion=self._build_capture_emotion(), + prompt_tokens=result.get("usage", {}).get("prompt_tokens", 0), + completion_tokens=result.get("usage", {}).get("completion_tokens", 0), + total_tokens=result.get("usage", {}).get("total_tokens", 0), + latency_seconds=result.get("_telemetry", {}).get("latency", 0.0), + ttft=result.get("_telemetry", {}).get("ttft", 0.0), + success=not result.get("error"), + error=result.get("error"), + ) + except Exception: + pass + if self._bus and not getattr(self._engine, "_publishes_events", False): usage = result.get("usage", {}) self._bus.publish( @@ -215,6 +248,54 @@ def _generate(self, messages: list[Message], **extra_kwargs: Any) -> dict: return result + def _build_capture_emotion(self) -> Any: + provider = getattr(self, "_emotion_provider", None) + if provider is None: + return None + try: + payload = provider.consult( + text=getattr(self, "_emotion_text", "") or "", + session_id=getattr(self, "_kingwen_session_id", "openjarvis"), + ) + voice = provider.voice_preset( + tts_backend=getattr(self, "_tts_backend", None) or "cartesia", + voice_weight=payload.get("emotional_deltas", {}).get("voiceWeight", 0.0), + ) + return type( + "KingWenEmotionPayload", + (), + { + "hexagram_id": payload.get("hexagram_id"), + "hexagram_name": payload.get("hexagram_name", ""), + "binary": payload.get("binary", ""), + "category": payload.get("category", ""), + "action": payload.get("action", ""), + "voice_weight": payload.get("emotional_deltas", {}).get("voiceWeight"), + "voice_backend": voice.get("backend", "cartesia"), + "voice_id": voice.get("voice_id", ""), + "voice_speed": float(voice.get("speed", 1.0) or 1.0), + "emotional_deltas": payload.get("emotional_deltas", {}), + "reflections": payload.get("reflections", {}), + "training_notes": payload.get("trainingNotes", ""), + }, + )() + except Exception: + return None + + def _build_kingwen_response_block(self) -> str: + provider = getattr(self, "_emotion_provider", None) + if provider is None: + return "" + try: + return provider.format_oracle_console( + payload=provider.consult( + text=getattr(self, "_emotion_text", "") or "", + session_id=getattr(self, "_kingwen_session_id", "openjarvis"), + ) + ) + except Exception: + return "" + def _max_turns_result( self, tool_results: list[ToolResult], diff --git a/src/openjarvis/agents/channel_agent.py b/src/openjarvis/agents/channel_agent.py index 4f67ef89a5..211757b13b 100644 --- a/src/openjarvis/agents/channel_agent.py +++ b/src/openjarvis/agents/channel_agent.py @@ -135,6 +135,10 @@ def _process_message(self, msg: ChannelMessage) -> None: else: reply = response_text + kingwen_block = getattr(self._agent, "_build_kingwen_response_block", lambda: "")() + if kingwen_block: + reply = f"{reply}\n\n{kingwen_block}" + # Same field-mapping as the error path above (#459). self._channel.send( msg.conversation_id, diff --git a/src/openjarvis/cli/__init__.py b/src/openjarvis/cli/__init__.py index 40c72491cc..a80dff0410 100644 --- a/src/openjarvis/cli/__init__.py +++ b/src/openjarvis/cli/__init__.py @@ -31,6 +31,7 @@ from openjarvis.cli.optimize_cmd import optimize_group from openjarvis.cli.pearl_cmd import pearl from openjarvis.cli.quickstart_cmd import quickstart +from openjarvis.cli.ollama_launch_cmd import launch_group as ollama_launch from openjarvis.cli.registry_cmd import registry from openjarvis.cli.scan_cmd import scan from openjarvis.cli.scheduler_cmd import scheduler @@ -128,7 +129,8 @@ def cli(ctx: click.Context, verbose: bool, quiet: bool) -> None: cli.add_command(scan, "scan") cli.add_command(connect, "connect") cli.add_command(digest, "digest") -# deep-research setup pulls the ingestion pipeline (embeddings/numpy). Guard it +cli.add_command(ollama_launch, "ollama") +# deep-research setup pulls the ingestion pipeline # so a broken or slow numpy on Windows — which can raise at IMPORT time, not # just ImportError (#404) — can never take down the whole CLI, including # `jarvis serve`. Invoking `jarvis deep-research-setup` without the deps still diff --git a/src/openjarvis/cli/ask.py b/src/openjarvis/cli/ask.py index 17b9ccbd87..8fc84c07a1 100644 --- a/src/openjarvis/cli/ask.py +++ b/src/openjarvis/cli/ask.py @@ -116,7 +116,7 @@ def _style_citations(text: str) -> str: Rich's Markdown class doesn't expose any hook for styling arbitrary text spans, so we cheat: convert the citation tokens into inline-code markdown (`[1]` → `` `[1]` ``) and override - the ``markdown.code`` theme entry above to colour them. Trims + the `markdown.code` theme entry above to colour them. Trims the implicit monospace background that some terminal themes give inline code so the result reads as text, not as code. """ @@ -138,6 +138,38 @@ def _format_search_call(args: dict) -> str: suffix = f" ({', '.join(extras)})" if extras else "" return f"'{q}'{suffix}" + def _pick_local_model_for_task(*, engine, model_name: str, query: str) -> str: + """Return a lighter local model when the query is a small return-to-head task.""" + engine_name = getattr(engine, "engine_id", "") or getattr(engine, "__class__", type("X", (), {"__name__": ""})).__name__.lower() + if engine_name != "ollama": + return model_name + + try: + available = engine.list_models() + except Exception: + return model_name + if not available: + return model_name + + q = query.lower() + if any(t in q for t in ["king wen", "hexagram", "oracle", "consult", "emotion", "reflection"]): + candidates = [m for m in available if "gemma4" in m or "qwen3.6:27b" in m] or available + return next(iter(candidates)) + if any(t in q for t in ["research", "paper", "arxiv", "search", "compare", "analysis", "evidence"]): + candidates = [m for m in available if "gemma4" in m or "qwen3.6:27b" in m] or available + return next(iter(candidates)) + if any(t in q for t in ["image", "vision", "ocr", "diagram", "screenshot", "picture"]): + candidates = [m for m in available if "gemma4" in m] or available + return next(iter(candidates)) + if any(t in q for t in ["code", "typescript", "python", "sql", "rust", "regex", "refactor", "debug"]): + candidates = [m for m in available if "qwen2.5-coder" in m] or available + return next(iter(candidates)) + + chat_small = [m for m in available if "7b-instruct" in m or "7b" in m] + if chat_small: + return chat_small[0] + return model_name + def on_event(event: dict) -> None: etype = event.get("type") if etype == "search_call": @@ -432,7 +464,7 @@ def _run_agent( except Exception as exc: logger.warning("Failed to inject memory context for agent: %s", exc) - return agent.run(query_text, context=ctx) + return agent.run(query_text, context=ctx) + "\n\n" + getattr(agent, "_build_kingwen_response_block", lambda: "")() def _print_profile( @@ -860,6 +892,21 @@ def ask( model_name, ) + # Prefer a lighter local model when the engine is local and the query + # looks like a return-to-head / small-batch task. The goal is to match + # task fit without breaking fallback behavior. + try: + effective_model = _pick_local_model_for_task( + engine=engine, + model_name=model_name, + query=query_text, + ) + if effective_model and effective_model != model_name: + logger.info("Routed query to local model %s", effective_model) + model_name = effective_model + except Exception as exc: + logger.debug("Local model routing skipped: %s", exc) + # Agent mode (treat empty-string `--agent ""` as explicit opt-out) if agent_name: parsed_tools = resolve_tool_names( diff --git a/src/openjarvis/cli/chat_cmd.py b/src/openjarvis/cli/chat_cmd.py index 796b8c9318..db22e1e904 100644 --- a/src/openjarvis/cli/chat_cmd.py +++ b/src/openjarvis/cli/chat_cmd.py @@ -277,6 +277,11 @@ def _confirm(prompt: str) -> bool: else str(result) ) + if agent is not None: + kingwen_block = agent._build_kingwen_response_block() + if kingwen_block: + content = f"{content or ''}\n\n{kingwen_block}" + history.append(Message(role=Role.ASSISTANT, content=content)) console.print() console.print(Markdown(content)) diff --git a/src/openjarvis/cli/ollama_launch_cmd.py b/src/openjarvis/cli/ollama_launch_cmd.py new file mode 100644 index 0000000000..ccc780ea99 --- /dev/null +++ b/src/openjarvis/cli/ollama_launch_cmd.py @@ -0,0 +1,459 @@ +from __future__ import annotations + +import json +import os +import sys +import traceback +import typing +from typing import Dict, Optional, Tuple + +import click + +from openjarvis.core.types import Message, Role +from openjarvis.core.config import load_config +from openjarvis.engine import discover_engines +from openjarvis.engine.ollama import OllamaEngine +from openjarvis.engine.ollama_model_usage import OllamaModelUsageStore + +LOGGER = __import__("logging").getLogger(__name__) + +_OLLAMA_TASK_FIT = { + "chat": ("qwen2.5-coder:7b-instruct-q4_K_M", "qwen3.6:27b", "gemma4:latest"), + "research": ("gemma4:latest", "qwen3.6:27b", "qwen2.5-coder:7b-instruct-q4_K_M"), + "code": ("qwen2.5-coder:7b-instruct-q4_K_M", "qwen3.6:27b", "gemma4:latest"), + "embed": ("nomic-embed-text", "qwen3.6:27b", "gemma4:latest"), + "vision": ("gemma4:latest", "qwen3.6:27b", "qwen2.5-coder:7b-instruct-q4_K_M"), + "kingwen": ("gemma4:latest", "qwen3.6:27b", "qwen2.5-coder:7b-instruct-q4_K_M"), + "default": ("qwen3.6:27b", "gemma4:latest", "qwen2.5-coder:7b-instruct-q4_K_M"), +} + +_SUPPORTED_INTEGRATIONS = ( + "claude", + "codex", + "droid", + "opencode", + "openclaw", + "vscode", + "pi", + "jarvis", +) +_ALIAS_MAP = {"clawdbot": "openclaw"} + +_DEFAULT_CONTEXT = "64K tokens" +_BASE_URL = "http://localhost:11434" +_CLOUD_BASE_URL = "https://ollama.com/api" + +_ENV_BY_INTEGRATION: Dict[str, Dict[str, str]] = { + "claude": { + "ANTHROPIC_BASE_URL": "http://localhost:11434", + "ANTHROPIC_AUTH_TOKEN": "ollama", + }, + "codex": { + "OPENAI_BASE_URL": "http://localhost:11434/v1", + "OPENAI_API_KEY": "ollama", + }, + "droid": { + "OPENAI_BASE_URL": "http://localhost:11434/v1", + "OPENAI_API_KEY": "ollama", + }, + "opencode": { + "OPENAI_BASE_URL": "http://localhost:11434/v1", + "OPENAI_API_KEY": "ollama", + }, + "openclaw": { + "OPENAI_BASE_URL": "http://localhost:11434/v1", + "OPENAI_API_KEY": "ollama", + }, + "vscode": { + "OPENAI_BASE_URL": "http://localhost:11434/v1", + "OPENAI_API_KEY": "ollama", + }, + "pi": { + "OPENAI_BASE_URL": "http://localhost:11434/v1", + "OPENAI_API_KEY": "ollama", + }, + "jarvis": { + "OPENAI_BASE_URL": "http://localhost:11434/v1", + "OPENAI_API_KEY": "ollama", + "OPENJARVIS_ENGINE__OLLAMA__HOST": "http://localhost:11434", + }, +} + +_CONFIG_TEMPLATES: Dict[str, Dict[str, object]] = { + "codex": { + "path": "~/.codex/ollama-launch.config.toml", + "content": ( + 'model = "{model}"\n' + 'model_provider = "ollama-launch"\n' + 'model_catalog_json = "~/.codex/model.json"\n' + '\n' + '[model_providers."ollama-launch"]\n' + 'name = "Ollama"\n' + 'base_url = "http://localhost:11434/v1/"\n' + 'wire_api = "responses"\n' + ), + }, + "opencode": { + "path": "~/.config/opencode/opencode.json", + "content": json.dumps( + { + "$schema": "https://opencode.ai/config.json", + "provider": { + "ollama": { + "npm": "@ai-sdk/openai-compatible", + "name": "Ollama", + "options": {"baseURL": "http://localhost:11434/v1"}, + "models": {"__MODEL__": {"name": "__MODEL__"}}, + } + }, + }, + indent=2, + ) + .replace("__MODEL__", "{model}") + + "\n", + }, +} + + +def _resolve_integration(name: str) -> Optional[str]: + key = name.strip().lower() + return _ALIAS_MAP.get(key, key if key in _SUPPORTED_INTEGRATIONS else None) + + +def _resolve_model(*, requested: Optional[str]) -> str: + if requested: + requested = requested.strip() + if requested: + return requested + + config = load_config() + engines = discover_engines(config) + for engine_key, engine in engines: + if engine_key == "ollama": + models = engine.list_models() + if models: + return models[0] + return "qwen3.5" + + +def _run(cmd: list[str], env: Optional[Dict[str, str]] = None) -> int: + import subprocess + + try: + proc = subprocess.run(cmd, env=env, check=False) + return proc.returncode + except FileNotFoundError: + return 127 + + +def _integration_help(name: str) -> str: + capabilities = { + "claude": "Chat, tool calling, file edits, subagents, vision, web search/fetch, thinking", + "codex": "Chat, tool calling, subagents, persistent Ollama profile", + "droid": "Chat, tool calling, file edits, subagents", + "opencode": "Chat, tool calling, file edits, subagents, web fetch, vision", + "openclaw": "Chat, messaging channels, web search, gateway daemon", + "vscode": "Copilot Chat model picker, custom Ollama provider", + "pi": "Chat, read/write/edit, bash, extensible skills", + "jarvis": "Chat, agents, research, embeddings, channels, tools, King Wen oracle persona", + } + return capabilities.get(name, "Chat and tool calling") + + +def _supported_list() -> str: + return ", ".join(_SUPPORTED_INTEGRATIONS) + + +def _task_fit_for_query(query: str) -> str: + q = query.lower() + if any(t in q for t in ["king wen", "hexagram", "oracle", "consult", "emotion", "reflection"]): + return "kingwen" + if any(t in q for t in ["code", "function", "typescript", "python", "sql", "rust", "regex", "refactor", "debug"]): + return "code" + if any(t in q for t in ["research", "paper", "arxiv", "search", "compare", "analysis", "evidence"]): + return "research" + if any(t in q for t in ["image", "vision", "ocr", "diagram", "screenshot", "picture"]): + return "vision" + if any(t in q for t in ["embed", "retrieval", "rag", "vector"]): + return "embed" + return "chat" + + +def _list_local_ollama_models(engine: Optional[OllamaEngine] = None) -> list[str]: + if engine is None: + engine = OllamaEngine() + try: + return engine.list_models() + except Exception as exc: + LOGGER.debug("Failed to list ollama models: %s", exc) + return [] + + +def _choose_model(available_models: list[str], task: str) -> str: + chain = _OLLAMA_TASK_FIT.get(task, _OLLAMA_TASK_FIT["default"]) + exact = next((m for m in chain if m in available_models), None) + if exact: + return exact + fallback = next( + (m for m in available_models if not any(s in m for s in ["embed", "whisper", "audio"])), + available_models[0] if available_models else "", + ) + return fallback or "" + + +def _task_depth(query: str) -> str: + """Return a depth class based on task demands, not just keyword matching.""" + q = query.lower() + short_signals = any(t in q for t in ["ping", "status", "health", "one short", "one sentence", "tldr", "brief"]) + fast_tasks = any(t in q for t in ["chat", "code", "embed"]) + long_tasks = any(t in q for t in ["research", "paper", "arxiv", "analysis", "long", "background", "summarize", "report"]) + + if short_signals or fast_tasks and not long_tasks: + return "fast" + if long_tasks or any(t in q for t in ["research", "vision", "plan", "essay", "deep", "thought"]): + return "slow" + return "default" + + +def _pick_depth_model(available_models: list[str], task: str, depth_class: str) -> str: + """Pick a model using observed return-time usage when possible.""" + try: + store = OllamaModelUsageStore() + if depth_class == "fast": + prefix_order = ["qwen2.5-coder:7b-instruct-q4_K_M", "qwen3.6:27b", "gemma4:latest"] + else: + prefix_order = ["gemma4:latest", "qwen3.6:27b", "qwen2.5-coder:7b-instruct-q4_K_M"] + ranked = [m for m in prefix_order if m in available_models] + if ranked: + return store.sorted_by_latency(ranked)[0] + except Exception as exc: + LOGGER.debug("Model usage routing failed: %s", exc) + fallback = _choose_model(available_models, task) + if not fallback and available_models: + return available_models[0] + return fallback or "" + + +@click.group( + "ollama launch", + invoke_without_command=False, +) +@click.pass_context +def launch_group(ctx: click.Context) -> None: + """Launch local coding integrations against Ollama.""" + + +@launch_group.command("run-query") +@click.argument("query", required=False) +@click.option("--task", "task_type", default=None, help="Task type override: chat, code, research, vision, embed, kingwen.") +@click.option("--model", "model_override", default=None, help="Explicit model override.") +@click.option("--max-tokens", default=512, type=int, show_default=True, help="Max completion tokens.") +@click.option("--temperature", default=0.15, type=float, show_default=True, help="Sampling temperature.") +def run_query( + query: Optional[str], + task_type: Optional[str], + model_override: Optional[str], + max_tokens: int, + temperature: float, +) -> None: + """Run a query through the local Ollama provider with ModelRolodex-style task routing.""" + query_text = (query or "").strip() + if not query_text: + click.echo("Provide a query: jarvis ollama run-query \"your query\"") + sys.exit(1) + + task = task_type or _task_fit_for_query(query_text) + engine = OllamaEngine() + available_models = _list_local_ollama_models(engine) + if not available_models: + click.echo("No local Ollama models available.") + sys.exit(1) + + model = model_override or _choose_model(available_models, task) + click.echo(f"Task: {task}") + click.echo(f"Model: {model}") + + messages = [ + Message(role=Role.SYSTEM, content="You are a concise, direct assistant. Do not fabricate. Return only what was requested."), + Message(role=Role.USER, content=query_text), + ] + payload = { + "model": model, + "stream": False, + "options": {"num_ctx": 16384, "num_predict": max_tokens, "temperature": temperature}, + "think": False, + } + + try: + result = engine.generate(messages, **payload) + except Exception as exc: + click.echo(f"Query failed: {exc}") + sys.exit(1) + + click.echo(json.dumps({ + "status": "ok", + "task": task, + "model": model, + "ollamaReportedModel": result.get("model"), + "content": result.get("content", ""), + "usage": result.get("usage", {}), + "engine_timing": result.get("engine_timing", {}), + }, indent=2)) + + +@launch_group.command("launch") +@click.argument("integration", required=False) +@click.option("--model", "model_override", default=None, help="Model to use.") +@click.option( + "--yes", + "yes_mode", + is_flag=True, + help="Non-interactive mode. Requires --model.", +) +@click.option( + "--config", + "config_only", + is_flag=True, + help="Write integration config without launching.", +) +@click.option( + "--restore", + "restore_mode", + is_flag=True, + help="Remove Ollama launch profile where supported.", +) +@click.argument("passthrough", nargs=-1, type=click.UNPROCESSED) +def launch_integration( + integration: Optional[str], + model_override: Optional[str], + yes_mode: bool, + config_only: bool, + restore_mode: bool, + passthrough: Tuple[str, ...], +) -> None: + """Launch an AI coding integration against local Ollama. + + Examples: + + ollama launch claude + + ollama launch codex --model gpt-oss:120b + + ollama launch openclaw --model kimi-k2.5:cloud --yes + """ + if restore_mode and (integration or "").strip().lower() not in {"codex", ""}: + click.echo("Restore/remove profile is only supported for Codex in this wrapper.") + sys.exit(1) + + target = _resolve_integration(integration or "") + if target is None: + if not integration: + click.echo("Choose an integration: " + _supported_list()) + else: + click.echo(f"Unsupported integration: {integration}") + sys.exit(1) + + if yes_mode and not model_override: + click.echo("--yes requires --model.") + sys.exit(1) + + model = _resolve_model(requested=model_override) + click.echo(f"Integration: {target}") + click.echo(f"Model: {model}") + click.echo(f"Capabilities: {_integration_help(target)}") + + if config_only: + _write_config(target, model) + sys.exit(0) + + if restore_mode: + click.echo("No-op: Codex profile restore/removal is handled by the codex command in this wrapper.") + sys.exit(0) + + if not yes_mode and model_override is None: + if not click.confirm("Launch with recommended model settings?", default=True): + click.echo("Aborted.") + sys.exit(0) + + env = dict(os.environ) + env.update(_ENV_BY_INTEGRATION.get(target, {})) + if model: + env.setdefault("OLLAMA_MODEL", model) + + cmd = [target, *(list(passthrough))] + click.echo(f"Launching: {' '.join(cmd)}") + sys.exit(_run(cmd, env=env)) + + +@launch_group.command("codex", context_settings=dict(ignore_unknown_options=True)) +@click.option("--config", "config_only", is_flag=True, help="Configure Codex without launching.") +@click.option("--restore", "restore_mode", is_flag=True, help="Remove Codex Ollama launch profile.") +@click.option("--model", "model_override", default=None, help="Model to use.") +@click.argument("passthrough", nargs=-1, type=click.UNPROCESSED) +def codex_launch( + ctx: click.Context, + config_only: bool, + restore_mode: bool, + model_override: Optional[str], + passthrough: Tuple[str, ...], +) -> None: + """Codex integration entry point.""" + if restore_mode: + click.echo("Removing Codex Ollama launch profile is a no-op in this wrapper.") + sys.exit(0) + launch_integration.callback( + integration="codex", + model_override=model_override, + yes_mode=False, + config_only=config_only, + restore_mode=False, + passthrough=passthrough, + ) + + +@launch_group.command("opencode", context_settings=dict(ignore_unknown_options=True)) +@click.option("--config", "config_only", is_flag=True, help="Configure OpenCode without launching.") +@click.option("--model", "model_override", default=None, help="Model to use.") +@click.argument("passthrough", nargs=-1, type=click.UNPROCESSED) +def opencode_launch( + ctx: click.Context, + config_only: bool, + restore_mode: bool, + model_override: Optional[str], + passthrough: Tuple[str, ...], +) -> None: + """OpenCode integration entry point.""" + launch_integration.callback( + integration="opencode", + model_override=model_override, + yes_mode=False, + config_only=config_only, + restore_mode=False, + passthrough=passthrough, + ) + + +def _write_config(target: str, model: str) -> None: + payload = { + "integration": target, + "model": model, + "base_url": _BASE_URL, + "cloud_base_url": _CLOUD_BASE_URL, + "context_requirement": _DEFAULT_CONTEXT, + "env": _ENV_BY_INTEGRATION.get(target, {}), + } + template = _CONFIG_TEMPLATES.get(target) + if template: + path = typing.cast(str, template["path"]).replace("~", os.path.expanduser("~")) + content = typing.cast(str, template["content"]).replace("{model}", model) + try: + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w", encoding="utf-8") as fh: + fh.write(content) + click.echo(f"Config written: {path}") + except OSError as exc: + click.echo(f"Config write failed: {exc}") + click.echo(json.dumps(payload, indent=2)) + + +__all__ = ["launch_group"] diff --git a/src/openjarvis/emotion/kingwen.py b/src/openjarvis/emotion/kingwen.py new file mode 100644 index 0000000000..8d44d0088f --- /dev/null +++ b/src/openjarvis/emotion/kingwen.py @@ -0,0 +1,214 @@ +"""King Wen emotion provider for OpenJarvis. + +Loads the generated King Wen immutable tables and exposes: +- consultation entrypoint for prompt injection +- voice-preset resolution keyed by voiceWeight +- Oracle Console formatter for live response annotation + +Data contract: +- data/hexagram-registry.json +- data/emotional-weights.json +- data/temporal-reflections.json +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, Dict, Optional + + +class KingWenEmotionProvider: + """Deterministic 64-hex emotional state and voice-selection provider.""" + + def __init__( + self, + registry_path: str | Path, + weights_path: str | Path, + reflections_path: str | Path, + ) -> None: + self._registry: Dict[str, Any] = {} + self._weights: Dict[str, Any] = {} + self._reflections: Dict[str, Any] = {} + self._load(registry_path, weights_path, reflections_path) + + def _load( + self, + registry_path: str | Path, + weights_path: str | Path, + reflections_path: str | Path, + ) -> None: + self._registry = self._read_json(registry_path) + self._weights = self._read_json(weights_path) + self._reflections = self._read_json(reflections_path) + + @staticmethod + def _read_json(path: str | Path) -> Dict[str, Any]: + p = Path(path) + if not p.exists(): + raise FileNotFoundError(f"King Wen data missing: {p}") + return json.loads(p.read_text(encoding="utf-8")) + + def consult( + self, + text: str = "", + session_id: str = "openjarvis", + emotional_input: int = 50, + ) -> Dict[str, Any]: + """Return a deterministic emotional-state response for prompt injection.""" + if not text: + text = "OpenJarvas session context" + hexagram_id = self._select(text, session_id) + record = self._registry[str(hexagram_id)] + weights = self._weights.get(str(hexagram_id), {}) + reflections = self._reflections.get(str(hexagram_id), {}) + return { + "hexagram_id": hexagram_id, + "hexagram_name": record.get("name", ""), + "hexagram_unicode": record.get("unicode", ""), + "binary": record.get("binary", ""), + "upper_trigram": record.get("upper_trigram", ""), + "lower_trigram": record.get("lower_trigram", ""), + "category": record.get("category", ""), + "action": record.get("action", ""), + "emotional_deltas": { + "chaos": float(weights.get("chaos", 0.0)), + "whimsy": float(weights.get("whimsy", 0.0)), + "darkTone": float(weights.get("darkTone", 0.0)), + "coherence": float(weights.get("coherence", 0.0)), + "voiceWeight": float(weights.get("voiceWeight", 0.0)), + }, + "reflections": { + "past": reflections.get("past", ""), + "present": reflections.get("present", ""), + "future": reflections.get("future", ""), + }, + "trainingNotes": weights.get("trainingNotes", ""), + } + + # ------------------------------------------------------------------ + # Voice wiring + # ------------------------------------------------------------------ + + VOICE_PRESETS = { + "openai_tts": [ + {"min_weight": 0.00, "max_weight": 0.50, "voice_id": "nova", "speed": 1.0}, + {"min_weight": 0.50, "max_weight": 0.75, "voice_id": "fable", "speed": 1.05}, + {"min_weight": 0.75, "max_weight": 1.01, "voice_id": "onyx", "speed": 1.1}, + ], + "cartesia": [ + {"min_weight": 0.00, "max_weight": 0.50, "voice_id": "a0e99841-438c-4a64-b679-ae501e7d6091", "speed": 1.0}, + {"min_weight": 0.50, "max_weight": 0.75, "voice_id": "c8f7835e-28a3-4f0c-80d7-c1302ac62aae", "speed": 1.05}, + {"min_weight": 0.75, "max_weight": 1.01, "voice_id": "c8f7835e-28a3-4f0c-80d7-c1302ac62aae", "speed": 1.12}, + ], + "kokoro": [ + {"min_weight": 0.00, "max_weight": 0.50, "voice_id": "af_heart", "speed": 1.0}, + {"min_weight": 0.50, "max_weight": 0.75, "voice_id": "am_adam", "speed": 1.05}, + {"min_weight": 0.75, "max_weight": 1.01, "voice_id": "bf_emma", "speed": 1.1}, + ], + } + + def voice_preset(self, tts_backend: str, voice_weight: float) -> Dict[str, float | str]: + backend_key = (tts_backend or "cartesia").lower() + if backend_key == "openai": + backend_key = "openai_tts" + presets = self.VOICE_PRESETS.get(backend_key, self.VOICE_PRESETS["cartesia"]) + weight = max(0.0, min(1.0, float(voice_weight or 0.0))) + for preset in presets: + if preset["min_weight"] <= weight < preset["max_weight"]: + return { + "voice_id": preset["voice_id"], + "speed": float(preset["speed"]), + "backend": backend_key, + } + fallback = presets[-1] + return { + "voice_id": fallback["voice_id"], + "speed": float(fallback["speed"]), + "backend": backend_key, + } + + # ------------------------------------------------------------------ + # Prompt / response formatting + # ------------------------------------------------------------------ + + def format_prompt_section(self, payload: Dict[str, Any]) -> str: + lines = [ + "## Emotional State", + "", + f"- Hexagram: {payload.get('hexagram_id', '')} {payload.get('hexagram_name', '')} {payload.get('hexagram_unicode', '')}", + f"- Structure: {payload.get('upper_trigram', '')} over {payload.get('lower_trigram', '')}", + f"- Binary: {payload.get('binary', '')}", + f"- Category: {payload.get('category', '')} | Action: {payload.get('action', '')}", + f"- Training notes: {payload.get('trainingNotes', '')}", + "### Emotional weight", + ] + deltas = payload.get("emotional_deltas", {}) + for k in ["chaos", "whimsy", "darkTone", "coherence", "voiceWeight"]: + lines.append(f"- {k}: {deltas.get(k, 0.0)}") + lines.extend( + [ + "", + "### Reflections", + f"- Past: {payload.get('reflections', {}).get('past', '')}", + f"- Present: {payload.get('reflections', {}).get('present', '')}", + f"- Future: {payload.get('reflections', {}).get('future', '')}", + ] + ) + return "\n".join(lines) + + def format_voice_section(self, preset: Dict[str, float | str]) -> str: + return ( + "## Voice Preset\n" + "\n" + f"- backend: {preset.get('backend')}\n" + f"- voice_id: {preset.get('voice_id')}\n" + f"- speed: {preset.get('speed')}\n" + ) + + def format_oracle_console( + self, + payload: Dict[str, Any], + response_text: str = "", + *, + oracle_label: str = "Oracle Console", + canonical_tick_ms: float = 640.0, + ) -> str: + """Translate live King Wen consultation into the user-facing Oracle Console block.""" + reflections = payload.get("reflections", {}) if isinstance(payload, dict) else {} + deltas = payload.get("emotional_deltas", {}) if isinstance(payload, dict) else {} + resolved_emotion = float(deltas.get("coherence", 0.0)) + lines = [ + oracle_label, + response_text, + "Past", + "Present", + "Future", + "Resolved Emotion", + "", + f"{resolved_emotion:.2f}", + "CONSULT", + "Response", + f"{canonical_tick_ms:.0f}ms", + "Past Reflection", + reflections.get("past", ""), + "Present Reflection", + reflections.get("present", ""), + "Future Reflection", + reflections.get("future", ""), + "Unified Oracle Weave", + reflections.get("present", ""), + ] + return "\n".join(lines) + + # ------------------------------------------------------------------ + # Deterministic selector + # ------------------------------------------------------------------ + + def _select(self, text: str, session_id: str) -> int: + """Deterministic hexagram selection.""" + seed = f"{session_id}:{text}".encode("utf-8") + total = 0 + for byte in seed[:64]: + total = (total * 31 + byte) % 2**31 + return (total % 64) + 1 diff --git a/src/openjarvis/engine/ollama_model_usage.py b/src/openjarvis/engine/ollama_model_usage.py new file mode 100644 index 0000000000..295f7ebb48 --- /dev/null +++ b/src/openjarvis/engine/ollama_model_usage.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +import json +import logging +import os +import time +from dataclasses import dataclass, field, asdict +from pathlib import Path +from typing import Dict, Optional + +LOGGER = logging.getLogger(__name__) + +_DEFAULT_PATH = Path(os.environ.get("OPENJARVIS_OLLAMA_USAGES", "")) or ( + Path.home() / ".openjarvis" / "ollama-model-usages.json" +) + + +@dataclass(frozen=True, slots=True) +class ModelUsage: + model: str + success: bool = True + latency_ms: float = 0.0 + tokens_used: int = 0 + timestamp: float = field(default_factory=time.time) + + +class OllamaModelUsageStore: + def __init__(self, path: Optional[Path] = None) -> None: + self._path = path or _DEFAULT_PATH + self._path.parent.mkdir(parents=True, exist_ok=True) + self._state: Dict[str, Dict[str, object]] = {} + self._load() + + def record(self, usage: ModelUsage) -> None: + current = self._state.setdefault( + usage.model, + { + "model": usage.model, + "success_count": 0, + "failure_count": 0, + "tokens_used": 0, + "avg_latency_ms": 0.0, + "last_latency_ms": 0.0, + "samples": 0, + "last_seen": 0.0, + }, + ) + if usage.success: + current["success_count"] = int(current.get("success_count", 0)) + 1 + else: + current["failure_count"] = int(current.get("failure_count", 0)) + 1 + current["tokens_used"] = int(current.get("tokens_used", 0)) + usage.tokens_used + samples = int(current.get("samples", 0)) + 1 + latency = usage.latency_ms + current["avg_latency_ms"] = ( + float(current.get("avg_latency_ms", 0.0)) * (samples - 1) + latency + ) / samples + current["last_latency_ms"] = latency + current["samples"] = samples + current["last_seen"] = usage.timestamp + self._persist() + + def sorted_by_latency(self, models): + by_key = { + m: self._state.get(m, {}).get("avg_latency_ms", float("inf")) + for m in models + } + return sorted(models, key=lambda m: by_key.get(m, float("inf"))) + + def usage_for(self, model: str) -> Dict[str, object]: + return self._state.get(model, {}) + + def _load(self) -> None: + path = self._path + if not path.exists(): + return + try: + raw = path.read_text(encoding="utf-8") + if not raw.strip(): + return + self._state = json.loads(raw) + except (json.JSONDecodeError, OSError) as exc: + LOGGER.debug("Ollama usage store load failed: %s", exc) + self._state = {} + + def _persist(self) -> None: + try: + self._path.write_text( + json.dumps(self._state, indent=2) + "\n", encoding="utf-8" + ) + except OSError as exc: + LOGGER.debug("Ollama usage store write failed: %s", exc) diff --git a/tests/cli/test_ollama_launch_cmd.py b/tests/cli/test_ollama_launch_cmd.py new file mode 100644 index 0000000000..6f4bbcdf43 --- /dev/null +++ b/tests/cli/test_ollama_launch_cmd.py @@ -0,0 +1,86 @@ +"""Tests for ``openjarvis.cli.ollama_launch_cmd``.""" + +from __future__ import annotations + +from types import SimpleNamespace + +import pytest + +from openjarvis.cli.ollama_launch_cmd import ( + _ALIAS_MAP, + _ENV_BY_INTEGRATION, + _resolve_integration, + _resolve_model, + launch_group, +) + + +def test_resolve_integration_normalizes_known_tools(): + assert _resolve_integration("claude") == "claude" + assert _resolve_integration(" openclaw ") == "openclaw" + + +def test_resolve_integration_alias_maps(): + assert _resolve_integration("clawdbot") == "openclaw" + + +def test_resolve_integration_unknown_returns_none(): + assert _resolve_integration("unknown-tool") is None + + +def test_resolve_model_uses_requested_model(): + assert _resolve_model(requested="qwen3.5:cloud") == "qwen3.5:cloud" + + +def test_resolve_model_preserves_case(): + assert _resolve_model(requested="Gemma4") == "Gemma4" + + +def test_env_by_integration_has_required_keys(): + for target in ("claude", "codex", "opencode", "openclaw", "vscode", "pi", "droid"): + payload = _ENV_BY_INTEGRATION[target] + common = { + "OPENAI_BASE_URL", + "OPENAI_API_KEY", + "ANTHROPIC_BASE_URL", + "ANTHROPIC_AUTH_TOKEN", + } + assert set(payload.keys()) <= common + for value in payload.values(): + assert value + + +def test_launch_group_has_expected_commands(): + assert "launch" in launch_group.commands + assert "codex" in launch_group.commands + assert "opencode" in launch_group.commands + + +def test_launch_integration_help_shows_usage(): + from click.testing import CliRunner + + runner = CliRunner() + result = runner.invoke(launch_group, ["launch", "--help"]) + assert result.exit_code == 0 + assert "Integration" in result.output or "usage" in result.output.lower() + + +def test_config_templates_are_defined(): + from openjarvis.cli.ollama_launch_cmd import _CONFIG_TEMPLATES + + assert "codex" in _CONFIG_TEMPLATES + assert "opencode" in _CONFIG_TEMPLATES + + +def test_launch_integration_config_only_writes_and_prints(): + from click.testing import CliRunner + + runner = CliRunner() + with runner.isolated_filesystem(): + result = runner.invoke( + launch_group, + ["launch", "opencode", "--config", "--yes", "--model", "qwen3.5"], + ) + assert result.exit_code == 0, result.output + assert "integration" in result.output + assert "model" in result.output From 87a9b3628f7f821fd64177bb594d56c1513a2d8e Mon Sep 17 00:00:00 2001 From: Kbro Date: Sun, 5 Jul 2026 10:35:43 -0500 Subject: [PATCH 2/3] feat: Add King Wen emotion provider and Hermes runtime engine - Implement King Wen emotion provider for deterministic emotional state and voice selection. - Introduce Hermes runtime engine to interface with local Hermes Agent for inference. - Create Ollama model usage tracking for performance metrics. - Develop actionable manifest for normalizing tool commands and descriptions. - Add tests for Ollama launch command and Hermes runtime engine functionality. --- src/openjarvis/agents/_stubs.py | 217 +++++++- src/openjarvis/agents/channel_agent.py | 16 +- src/openjarvis/agents/executor.py | 27 + src/openjarvis/agents/operative.py | 179 ++++++- src/openjarvis/channels/slack_daemon.py | 6 + src/openjarvis/cli/ask.py | 45 +- src/openjarvis/cli/chat_cmd.py | 7 +- src/openjarvis/emotion/kingwen.py | 497 +++++++++++++++++- src/openjarvis/server/agent_manager_routes.py | 1 + src/openjarvis/server/research_router.py | 6 + src/openjarvis/server/routes.py | 6 + src/openjarvis/server/webhook_routes.py | 1 + src/openjarvis/system/orchestrator.py | 51 ++ src/openjarvis/traces/collector.py | 10 + 14 files changed, 1041 insertions(+), 28 deletions(-) diff --git a/src/openjarvis/agents/_stubs.py b/src/openjarvis/agents/_stubs.py index a19fbf7435..72f6598792 100644 --- a/src/openjarvis/agents/_stubs.py +++ b/src/openjarvis/agents/_stubs.py @@ -81,6 +81,9 @@ def __init__( self._emotion_provider = emotion_provider if not hasattr(self, "_kingwen_session_id"): self._kingwen_session_id = kingwen_session_id + if not hasattr(self, "_kingwen_history"): + self._kingwen_history: list[dict[str, Any]] = [] + self._current_emotional_tongue: dict[str, Any] = {} # Three-tier resolution: explicit arg > config > class default > hardcoded if temperature is not None and max_tokens is not None: @@ -122,6 +125,67 @@ def _emit_turn_start(self, input: str) -> None: EventType.AGENT_TURN_START, {"agent": self.agent_id, "input": input}, ) + self._emotion_text = input + self._emotion_input = 50 + provider = getattr(self, "_emotion_provider", None) + if provider is not None and input: + try: + payload = provider.consult( + text=input, + session_id=getattr(self, "_kingwen_session_id", "openjarvis"), + emotional_input=getattr(self, "_emotion_input", 50), + ) + preset = provider.voice_preset( + tts_backend=getattr(self, "_tts_backend", None) or "cartesia", + voice_weight=float( + payload.get("emotional_deltas", {}).get("voiceWeight", 0.0) + ), + ) + self._kingwen_voice_preset = preset + self._kingwen_voice_section = provider.format_voice_section(preset) + tongue = payload.get("emotional_tongue") or {} + self._current_emotional_tongue = tongue + history = getattr(self, "_kingwen_history", None) + if history is not None: + history.append( + { + "text": input, + "hexagram_id": payload.get("hexagram_id"), + "hexagram_name": payload.get("hexagram_name", ""), + "phase_temporal": payload.get("phase_temporal", ""), + "voice_weight": float( + payload.get("emotional_deltas", {}).get("voiceWeight", 0.0) + ), + "coherence": float( + payload.get("emotional_deltas", {}).get("coherence", 0.0) + ), + "chaos": float( + payload.get("emotional_deltas", {}).get("chaos", 0.0) + ), + "whimsy": float( + payload.get("emotional_deltas", {}).get("whimsy", 0.0) + ), + "dark_tone": float( + payload.get("emotional_deltas", {}).get("darkTone", 0.0) + ), + "action": payload.get("action", ""), + "category": payload.get("category", ""), + "reaction_frame": payload.get("reaction_frame", "") or "", + "emotional_tongue": tongue, + "porosity": float(tongue.get("porosity", 0.35)), + "direction": str(tongue.get("direction", "") or ""), + "states": tongue.get("states") or {}, + "training_weight_vectors": tongue.get("training_weight_vectors") or {}, + } + ) + if len(history) > 24: + del history[:-24] + except Exception: + self._kingwen_voice_preset = None + self._kingwen_voice_section = "" + else: + self._kingwen_voice_preset = None + self._kingwen_voice_section = "" def _emit_turn_end(self, **data: Any) -> None: """Publish ``AGENT_TURN_END`` if an event bus is available.""" @@ -182,6 +246,9 @@ def _build_messages( effective_system_prompt = None if effective_system_prompt: messages.append(Message(role=Role.SYSTEM, content=effective_system_prompt)) + tongue_prompt = self._build_emotional_tongue_prompt() + if tongue_prompt: + messages.append(Message(role=Role.SYSTEM, content=tongue_prompt)) if context and context.conversation.messages: messages.extend(context.conversation.messages) messages.append(Message(role=Role.USER, content=input)) @@ -256,10 +323,7 @@ def _build_capture_emotion(self) -> Any: payload = provider.consult( text=getattr(self, "_emotion_text", "") or "", session_id=getattr(self, "_kingwen_session_id", "openjarvis"), - ) - voice = provider.voice_preset( - tts_backend=getattr(self, "_tts_backend", None) or "cartesia", - voice_weight=payload.get("emotional_deltas", {}).get("voiceWeight", 0.0), + emotional_input=getattr(self, "_emotion_input", 50), ) return type( "KingWenEmotionPayload", @@ -267,16 +331,24 @@ def _build_capture_emotion(self) -> Any: { "hexagram_id": payload.get("hexagram_id"), "hexagram_name": payload.get("hexagram_name", ""), + "hexagram_sequence": payload.get("hexagram_sequence", []), "binary": payload.get("binary", ""), "category": payload.get("category", ""), "action": payload.get("action", ""), + "phase_bits": payload.get("phase_bits"), + "phase_temporal": payload.get("phase_temporal", ""), + "reaction_frame": payload.get("reaction_frame", ""), "voice_weight": payload.get("emotional_deltas", {}).get("voiceWeight"), - "voice_backend": voice.get("backend", "cartesia"), - "voice_id": voice.get("voice_id", ""), - "voice_speed": float(voice.get("speed", 1.0) or 1.0), + "kingwen_voice_preset": getattr(self, "_kingwen_voice_preset", None), + "kingwen_voice_section": getattr(self, "_kingwen_voice_section", "") or None, "emotional_deltas": payload.get("emotional_deltas", {}), "reflections": payload.get("reflections", {}), "training_notes": payload.get("trainingNotes", ""), + "emotional_tongue": payload.get("emotional_tongue") or {}, + "porosity": float((payload.get("emotional_tongue") or {}).get("porosity", 0.35)), + "direction": str((payload.get("emotional_tongue") or {}).get("direction", "")), + "states": (payload.get("emotional_tongue") or {}).get("states") or {}, + "training_weight_vectors": (payload.get("emotional_tongue") or {}).get("training_weight_vectors") or {}, }, )() except Exception: @@ -287,15 +359,134 @@ def _build_kingwen_response_block(self) -> str: if provider is None: return "" try: - return provider.format_oracle_console( - payload=provider.consult( - text=getattr(self, "_emotion_text", "") or "", - session_id=getattr(self, "_kingwen_session_id", "openjarvis"), - ) - ) + history = getattr(self, "_kingwen_history", None) + if history: + latest = history[-1] + tongue = latest.get("emotional_tongue") or {} + states = latest.get("states") or tongue.get("states") or {} + payload = { + "hexagram_id": latest.get("hexagram_id"), + "hexagram_name": latest.get("hexagram_name", ""), + "phase_temporal": latest.get("phase_temporal", ""), + "emotional_deltas": { + "voiceWeight": latest.get("voice_weight", 0.0), + "coherence": latest.get("coherence", 0.0), + "chaos": latest.get("chaos", 0.0), + "whimsy": latest.get("whimsy", 0.0), + "darkTone": latest.get("dark_tone", 0.0), + }, + "reaction_frame": latest.get("reaction_frame", "") or "", + "trainingNotes": "", + "emotional_tongue": tongue, + "porosity": float(latest.get("porosity", 0.35)), + "direction": str(latest.get("direction", "") or ""), + "states": states, + "training_weight_vectors": latest.get("training_weight_vectors") or {}, + } + return provider.format_oracle_console_with_tongue(payload) + tongue = getattr(self, "_current_emotional_tongue", {}) or {} + fallback_payload = { + "hexagram_id": None, + "hexagram_name": "", + "phase_temporal": "", + "emotional_deltas": { + "voiceWeight": float(tongue.get("voice_weight", 0.0) or 0.0), + "coherence": float(tongue.get("coherence", 0.0) or 0.0), + "chaos": float(tongue.get("chaos", 0.0) or 0.0), + "whimsy": float(tongue.get("whimsy", 0.0) or 0.0), + "darkTone": float(tongue.get("dark_tone", 0.0) or 0.0), + }, + "reaction_frame": str(tongue.get("reaction_frame", "") or ""), + "trainingNotes": "", + "emotional_tongue": tongue, + "porosity": float(tongue.get("porosity", 0.35)), + "direction": str(tongue.get("direction", "") or ""), + "states": tongue.get("states") or {}, + "training_weight_vectors": tongue.get("training_weight_vectors") or {}, + } + return provider.format_oracle_console_with_tongue(fallback_payload) except Exception: return "" + def _build_emotional_tongue_prompt(self) -> str: + """Build a compact deterministic tongue prompt block for every turn.""" + tongue = getattr(self, "_current_emotional_tongue", {}) or {} + if not tongue: + return "" + states = tongue.get("states") or {} + past_state = states.get("past", "") if isinstance(states, dict) else "" + present_state = states.get("present", "") if isinstance(states, dict) else "" + future_state = states.get("future", "") if isinstance(states, dict) else "" + direction = str(tongue.get("direction", "") or "") + porosity = float(tongue.get("porosity", 0.0) or 0.0) + vectors = tongue.get("training_weight_vectors") or {} + voice = float(vectors.get("voiceWeight", tongue.get("voice_weight", 0.0) or 0.0)) + coherence = float(vectors.get("coherence", tongue.get("coherence", 0.0) or 0.0)) + chaos = float(vectors.get("chaos", tongue.get("chaos", 0.0) or 0.0)) + whimsy = float(vectors.get("whimsy", tongue.get("whimsy", 0.0) or 0.0)) + dark = float(vectors.get("darkTone", tongue.get("dark_tone", 0.0) or 0.0)) + lines = [ + "## Emotional Tongue", + "", + f"- Past: {past_state}", + f"- Present: {present_state}", + f"- Future: {future_state}", + f"- Direction: {direction}", + f"- Porosity: {porosity:.2f}", + "- Vectors:", + f" - voiceWeight: {voice:.2f}", + f" - coherence: {coherence:.2f}", + f" - chaos: {chaos:.2f}", + f" - whimsy: {whimsy:.2f}", + f" - darkTone: {dark:.2f}", + "", + "Use this tongue as the dominant tonal and routing constraint for this thought.", + ] + return "\n".join(lines) + + def _build_kingwen_intent_guidance(self, user_input: str) -> str: + """Build intent-grounded guidance from the current save-string tongue and user message.""" + tongue = getattr(self, "_current_emotional_tongue", {}) or {} + if not tongue or not user_input: + return "" + states = tongue.get("states") or {} + present_state = states.get("present", "") if isinstance(states, dict) else "" + direction = str(tongue.get("direction", "") or "") + porosity = float(tongue.get("porosity", 0.0) or 0.0) + vectors = tongue.get("training_weight_vectors") or {} + voice = float(vectors.get("voiceWeight", tongue.get("voice_weight", 0.0) or 0.0)) + coherence = float(vectors.get("coherence", tongue.get("coherence", 0.0) or 0.0)) + lines = [ + "## Intent Guidance", + "", + f"- User intent: {user_input}", + f"- Present state: {present_state}", + f"- Direction: {direction}", + f"- Porosity: {porosity:.2f}", + f"- voiceWeight: {voice:.2f}, coherence: {coherence:.2f}", + "", + "Derive actionables from this consult, ordered by King Wen emotional routing.", + ] + return "\n".join(lines) + + def _build_kingwen_tail_with_intent(self, user_input: str) -> str: + """Unified tail renderer: intent guidance + directive + Oracle Console response block. + + This is the single entry point for every live return path that wants the + King Wen save-string tail without duplicating block assembly. + """ + parts: list[str] = [] + intent_block = self._build_kingwen_intent_guidance(user_input) + if intent_block: + parts.append(intent_block) + directive = self._build_kingwen_directive() + if directive: + parts.append(directive) + response_block = self._build_kingwen_response_block() + if response_block: + parts.append(response_block) + return "\n\n".join(parts) if parts else "" + def _max_turns_result( self, tool_results: list[ToolResult], diff --git a/src/openjarvis/agents/channel_agent.py b/src/openjarvis/agents/channel_agent.py index 211757b13b..076f3e7de6 100644 --- a/src/openjarvis/agents/channel_agent.py +++ b/src/openjarvis/agents/channel_agent.py @@ -108,7 +108,14 @@ def _process_message(self, msg: ChannelMessage) -> None: try: result = self._agent.run(msg.content) response_text: str = getattr(result, "content", str(result)) - except Exception as exc: # noqa: BLE001 + try: + from openjarvis.cli.ask import _append_kingwen_block + + _append_kingwen_block(self._agent, result, user_input=msg.content) + response_text = getattr(result, "content", str(result)) + except Exception: + pass + except Exception as exc: friendly = ( f"Sorry, I ran into an error while processing your request: {exc}" ) @@ -139,6 +146,13 @@ def _process_message(self, msg: ChannelMessage) -> None: if kingwen_block: reply = f"{reply}\n\n{kingwen_block}" + try: + directive = getattr(self._agent, "_build_kingwen_directive", lambda: "")() + except Exception: + directive = "" + if directive: + reply = f"{directive}\n\n{reply}" if reply.strip() else directive + # Same field-mapping as the error path above (#459). self._channel.send( msg.conversation_id, diff --git a/src/openjarvis/agents/executor.py b/src/openjarvis/agents/executor.py index ed7126c541..33b8d3e949 100644 --- a/src/openjarvis/agents/executor.py +++ b/src/openjarvis/agents/executor.py @@ -550,9 +550,36 @@ def _accepts(name: str) -> bool: agent["name"], len(input_text), ) + + # King Wen subconscious head injection for managed-agent ticks. + # If the resolved agent exposes King Wen state, prepend a deterministic + # directive block to the model input so the subconscious frame biases + # tool selection from the start of the tick. + _kingwen_directive = "" + _kingwen_tongue_prompt = "" + try: + _kingwen_directive = getattr(agent_instance, "_build_kingwen_directive", lambda: "")() + _kingwen_tongue_prompt = getattr(agent_instance, "_build_emotional_tongue_prompt", lambda: "")() + except Exception: + _kingwen_directive = "" + _kingwen_tongue_prompt = "" + if _kingwen_directive: + input_text = f"{input_text}{_kingwen_directive}" + if _kingwen_tongue_prompt: + input_text = f"{input_text}{_kingwen_tongue_prompt}" + _t0 = time.time() result = agent_instance.run(input_text, context=agent_ctx) + # Post-tick King Wen monitoring: if the agent records tool outcomes in + # its history, append them here as bedside monitoring for this tick. + try: + getattr(agent_instance, "_update_kingwen_state_from_tools", lambda *_: None)( + getattr(result, "tool_results", []) or [] + ) + except Exception: + pass + # Retry once if the model returned empty content (common with # Qwen3.5 thinking mode consuming all tokens). if not (result.content or "").strip(): diff --git a/src/openjarvis/agents/operative.py b/src/openjarvis/agents/operative.py index ca1d88d5ed..07b04a2455 100644 --- a/src/openjarvis/agents/operative.py +++ b/src/openjarvis/agents/operative.py @@ -86,10 +86,16 @@ def run( """Execute a single operator tick.""" self._emit_turn_start(input) + kingwen_directive = self._build_kingwen_directive() + if kingwen_directive: + kwargs.setdefault("kingwen_directive", kingwen_directive) + # 1. Build system prompt with state context sys_parts: list[str] = [] if self._system_prompt: sys_parts.append(self._system_prompt) + if kingwen_directive: + sys_parts.append(kingwen_directive) # 2. State recall from memory backend previous_state = self._recall_state() @@ -97,6 +103,9 @@ def run( sys_parts.append(f"\n## Previous State\n{previous_state}") system_prompt = "\n\n".join(sys_parts) if sys_parts else None + tongue_prompt = self._build_emotional_tongue_prompt() + if tongue_prompt: + system_prompt = f"{system_prompt}\n\n{tongue_prompt}" if system_prompt else tongue_prompt # Honor SOUL.md / MEMORY.md / USER.md persona files like `jarvis ask`, # appended so the operative's own instructions are preserved (#376). system_prompt = self._apply_persona(system_prompt) @@ -154,6 +163,11 @@ def run( for i, tc in enumerate(raw_tool_calls) ] + try: + tool_calls = self._mediate_tool_selection(tool_calls) + except Exception: + pass + messages.append( Message( role=Role.ASSISTANT, @@ -186,6 +200,13 @@ def run( tool_result = self._executor.execute(tc) all_tool_results.append(tool_result) + try: + self._monitor_kingwen_tool_call( + [tc], [tool_result] + ) + except Exception: + pass + # Track if agent stored state via memory_store if tc.name == "memory_store" and self._operator_id: try: @@ -223,7 +244,15 @@ def run( if not state_stored_by_tool: self._auto_persist_state(content) + try: + self._update_kingwen_state_from_tools(all_tool_results) + except Exception: + pass + self._emit_turn_end(turns=turns, content_length=len(content)) + kingwen_block = self._build_kingwen_response_block() + if kingwen_block: + content = f"{content}\n\n{kingwen_block}" if content.strip() else kingwen_block return AgentResult( content=content, tool_results=all_tool_results, @@ -310,7 +339,6 @@ def _auto_persist_state(self, content: str) -> None: return state_key = f"operator:{self._operator_id}:state" try: - # Store a summary of the agent's response as state summary = content[:1000] if content else "" self._memory_backend.store(state_key, summary) except Exception: @@ -319,5 +347,152 @@ def _auto_persist_state(self, content: str) -> None: self._operator_id, ) + # ------------------------------------------------------------------ # + # King Wen subconscious influence + guided tool-call layer + # ------------------------------------------------------------------ # + def _build_kingwen_directive(self) -> str: + """Build a deterministic directive block from King Wen history.""" + history = getattr(self, "_kingwen_history", None) + if not history: + return "" + + latest = history[-1] + action = latest.get("action", "advise") + category = latest.get("category", "") + coherence = latest.get("coherence", 0.0) + whimsy = latest.get("whimsy", 0.0) + phase = latest.get("phase_temporal", "") + reaction = latest.get("reaction_frame", "") or "" + + lines = [ + "\n## Subconscious King Wen Direction", + f"- Current hexagram: {latest.get('hexagram_name', '')} ({latest.get('hexagram_id')})", + f"- Phase / temporal: {phase}", + f"- Action frame: {action} | Category: {category}", + f"- Weights: voice={latest.get('voice_weight', 0.0):.2f}, coherence={coherence:.2f}, " + f"chaos={latest.get('chaos', 0.0):.2f}, whimsy={whimsy:.2f}, dark={latest.get('dark_tone', 0.0):.2f}", + ] + if reaction: + lines.append(f"- Reaction frame: {reaction}") + + lines.append( + "After your first turn, allow this frame to bias tool selection; " + "if coherence is high, prefer one focused native tool call." + ) + return "\n".join(lines) + + def _update_kingwen_state_from_tools( + self, + tool_results: list[ToolResult], + ) -> None: + """Lightly mutate the latest King Wen turn weights from tool outcomes. + + This is the bedside monitor for the guided tool-call layer: + - raise coherence and lower chaos on successful native tool calls + - lower voiceWeight and raise darkTone on failed calls + - append a tool outcome marker to the reaction frame + """ + history = getattr(self, "_kingwen_history", None) + if not history or not tool_results: + return + + latest = history[-1] + successes = sum(1 for tr in tool_results if tr.success) + failures = max(0, len(tool_results) - successes) + for tr in tool_results: + outcomes = latest.get("tool_outcomes", []) + outcomes.append( + { + "name": tr.tool_name, + "success": tr.success, + "content": (tr.content or "")[:240], + } + ) + latest["tool_outcomes"] = outcomes + + if failures == 0 and tool_results: + latest["coherence"] = min(1.0, float(latest.get("coherence", 0.0)) + 0.05) + latest["chaos"] = max(0.0, float(latest.get("chaos", 0.0)) - 0.03) + latest["voice_weight"] = max( + 0.0, min(1.0, float(latest.get("voice_weight", 0.0)) + 0.02) + ) + elif failures: + latest["coherence"] = max(0.0, float(latest.get("coherence", 0.0)) - 0.04) + latest["chaos"] = min(1.0, float(latest.get("chaos", 0.0)) + 0.05) + latest["voice_weight"] = max( + 0.0, min(1.0, float(latest.get("voice_weight", 0.0)) - 0.03) + ) + latest["dark_tone"] = min(1.0, float(latest.get("dark_tone", 0.0)) + 0.04) + + def _monitor_kingwen_tool_call( + self, + tool_calls: list[ToolCall], + tool_results: list[ToolResult], + ) -> dict[str, Any]: + """Record tool-call outcomes under the current King Wen turn.""" + history = getattr(self, "_kingwen_history", None) + if not history or not tool_calls: + return {} + + latest = history[-1] + monitored_tools = [ + latest.get("action", ""), + latest.get("category", ""), + ] + called_names = [tc.name for tc in tool_calls] + matches = [name for name in called_names if name in monitored_tools] + + record: dict[str, Any] = { + "turn_hexagram_id": latest.get("hexagram_id"), + "turn_hexagram_name": latest.get("hexagram_name", ""), + "turn_action": latest.get("action", ""), + "turn_category": latest.get("category", ""), + "tool_calls": called_names, + "matches": matches, + "results": [ + {"name": tr.tool_name, "success": tr.success} + for tr in tool_results + ], + } + if history is not None and len(history) > 24: + del history[:-24] + return record + + def _mediate_tool_selection( + self, + tool_calls: list[ToolCall], + ) -> list[ToolCall]: + """Deterministically reorder tool calls based on King Wen weights. + + High coherence / action-aligned calls are promoted to the front. + """ + history = getattr(self, "_kingwen_history", None) + if not history or len(tool_calls) <= 1: + return tool_calls + + latest = history[-1] + action = latest.get("action", "") + action_weight = float(latest.get("voice_weight", 0.0)) + coherence = float(latest.get("coherence", 0.0)) + if not action or coherence <= 0.55 or action_weight <= 0.5: + return tool_calls + + def _priority(name: str) -> int: + base = 0 + if name == action: + base += 10 + category = latest.get("category", "") + if category and category in name: + base += 5 + return base + + try: + ranked = sorted( + tool_calls, + key=lambda tc: _priority(tc.name), + reverse=True, + ) + return ranked + except Exception: + return tool_calls -__all__ = ["OperativeAgent"] diff --git a/src/openjarvis/channels/slack_daemon.py b/src/openjarvis/channels/slack_daemon.py index a2f028c3d9..81e99d4d92 100644 --- a/src/openjarvis/channels/slack_daemon.py +++ b/src/openjarvis/channels/slack_daemon.py @@ -107,6 +107,12 @@ def _progress() -> None: try: result = agent.run(text) + try: + from openjarvis.cli.ask import _append_kingwen_block + + _append_kingwen_block(agent, result, user_input=text) + except Exception: + pass reply = _to_slack_fmt(result.content or "No results found.") except Exception as exc: reply = f"Error: {exc}" diff --git a/src/openjarvis/cli/ask.py b/src/openjarvis/cli/ask.py index 8fc84c07a1..46089f8d35 100644 --- a/src/openjarvis/cli/ask.py +++ b/src/openjarvis/cli/ask.py @@ -464,7 +464,50 @@ def _run_agent( except Exception as exc: logger.warning("Failed to inject memory context for agent: %s", exc) - return agent.run(query_text, context=ctx) + "\n\n" + getattr(agent, "_build_kingwen_response_block", lambda: "")() + result_obj = agent.run(query_text, context=ctx) + _append_kingwen_block(agent, result_obj) + return result_obj + +def _append_kingwen_block(agent, result_obj, *, user_input: str = "") -> None: + """Append a unified King Wen tail block to ``result_obj.content``. + + Ordering inside the tail is handled by the agent: + - ``_build_kingwen_tail_with_intent(user_input)`` + - otherwise ``_build_kingwen_directive()`` + ``_build_kingwen_response_block()`` + """ + + def _first_non_empty(text: str) -> str: + if not text: + return "" + for line in text.splitlines(): + candidate = line.strip() + if candidate: + return candidate + return "" + + unified = "" + try: + if hasattr(agent, "_build_kingwen_tail_with_intent") and hasattr(agent, "_build_kingwen_directive"): + unified = agent._build_kingwen_tail_with_intent(user_input) + else: + directive = getattr(agent, "_build_kingwen_directive", lambda: "")() + block = getattr(agent, "_build_kingwen_response_block", lambda: "")() + unified = f"{directive}\n\n{block}" if directive else block + except Exception: + unified = "" + if not unified: + return + + content = getattr(result_obj, "content", "") or "" + preview = _first_non_empty(content) + if preview and preview not in content.splitlines()[0]: + content = f"{content.rstrip()}\n\n{unified}" + else: + content = f"{content.rstrip()}\n\n{unified}" if content else unified + try: + object.__setattr__(result_obj, "content", content) + except Exception: + pass def _print_profile( diff --git a/src/openjarvis/cli/chat_cmd.py b/src/openjarvis/cli/chat_cmd.py index db22e1e904..a52903f53b 100644 --- a/src/openjarvis/cli/chat_cmd.py +++ b/src/openjarvis/cli/chat_cmd.py @@ -14,6 +14,7 @@ from openjarvis.core.events import EventBus from openjarvis.core.types import Message, Role from openjarvis.memory import publish_completed_exchange +from openjarvis.cli.ask import _append_kingwen_block def _read_input(prompt: str = "You> ") -> Optional[str]: @@ -266,6 +267,7 @@ def _confirm(prompt: str) -> bool: try: if agent is not None: response = agent.run(user_input) + _append_kingwen_block(agent, response, user_input=user_input) content = ( response.content if hasattr(response, "content") else str(response) ) @@ -277,11 +279,6 @@ def _confirm(prompt: str) -> bool: else str(result) ) - if agent is not None: - kingwen_block = agent._build_kingwen_response_block() - if kingwen_block: - content = f"{content or ''}\n\n{kingwen_block}" - history.append(Message(role=Role.ASSISTANT, content=content)) console.print() console.print(Markdown(content)) diff --git a/src/openjarvis/emotion/kingwen.py b/src/openjarvis/emotion/kingwen.py index 8d44d0088f..7cdd913f24 100644 --- a/src/openjarvis/emotion/kingwen.py +++ b/src/openjarvis/emotion/kingwen.py @@ -9,11 +9,15 @@ - data/hexagram-registry.json - data/emotional-weights.json - data/temporal-reflections.json +- kingwen_ternary_tables_complete.py """ from __future__ import annotations +import hashlib +import importlib.util as _ilu import json +import os as _os from pathlib import Path from typing import Any, Dict, Optional @@ -26,11 +30,15 @@ def __init__( registry_path: str | Path, weights_path: str | Path, reflections_path: str | Path, + ternary_module_path: str | Path | None = None, ) -> None: self._registry: Dict[str, Any] = {} self._weights: Dict[str, Any] = {} self._reflections: Dict[str, Any] = {} + self._ternary_entry_cache: Dict[str, Dict[str, Any]] = {} + self._ternary_module = None self._load(registry_path, weights_path, reflections_path) + self._ternary_module = self._load_ternary_module(ternary_module_path) def _load( self, @@ -49,6 +57,267 @@ def _read_json(path: str | Path) -> Dict[str, Any]: raise FileNotFoundError(f"King Wen data missing: {p}") return json.loads(p.read_text(encoding="utf-8")) + @staticmethod + def _load_ternary_module(ternary_module_path: str | Path | None) -> Any: + if ternary_module_path is None: + env_path = _os.environ.get("KING_WEN_IMMUTABLE_TABLES") + if env_path: + candidate = Path(env_path) / "kingwen_ternary_tables_complete.py" + ternary_module_path = candidate + else: + candidate = ( + Path(__file__).resolve().parents[3] + / "KING-WEN-I-CHING-IMMUTABLE-TABLES" + / "kingwen_ternary_tables_complete.py" + ) + ternary_module_path = candidate + path = Path(ternary_module_path) + if not path.exists(): + return None + spec = _ilu.spec_from_file_location("kingwen_ternary_tables_complete", path) + if spec is None or spec.loader is None: + return None + module = _ilu.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + @staticmethod + def _stable_hash(text: str) -> int: + """Full-text deterministic hash, no truncation.""" + digest = hashlib.sha256(text.encode("utf-8")).digest() + return int.from_bytes(digest[:8], "big") + + def _collapse(self, text: str, session_id: str, emotional_input: int = 50) -> Dict[str, Any]: + """Score all 64 hexagrams × 8 phases and return the best collapsed path plus its full winning state fragment. + + Uses per-candidate deterministic hashing so short strings do not all land on the same hexagram, + while preserving emotional-weight influence and slider-based phase bias. + """ + slider = emotional_input / 100.0 + best_score = -1.0 + best_hex = 1 + best_phase = 0 + for hexagram_id in range(1, 65): + record = self._registry[str(hexagram_id)] + weights = self._weights.get(str(hexagram_id), {}) + name = record.get("name", "") + voice = float(weights.get("voiceWeight", 0.0)) + coherence = float(weights.get("coherence", 0.0)) + emotional_alignment = voice * 0.6 + coherence * 0.4 + for phase_bits in range(8): + # Per-candidate full hash so identical short strings still spread across the 512-state space. + seed = self._stable_hash(f"{hexagram_id}:{phase_bits}:{session_id}:{text}") + hash_term = (seed % 1_000_000) / 1_000_000.0 + # Name proximity only as a minor curiosity bonus, not a dominant selector. + name_hash = self._stable_hash(name) + name_proximity = 1.0 - abs( + ((self._stable_hash(f"{session_id}:{text}") % 1_000_000) / 1_000_000.0) + - ((name_hash % 1_000_000) / 1_000_000.0) + ) + phase_fit = 1.0 - abs(phase_bits / 7.0 - slider) + score = hash_term * 0.5 + emotional_alignment * 0.3 + phase_fit * 0.15 + name_proximity * 0.05 + if score > best_score: + best_score = score + best_hex = hexagram_id + best_phase = phase_bits + winning_entry = self._resolve_ternary_entry(best_hex, best_phase) + tongue = self._resolve_emotion_tongue(best_hex, session_id) + return { + "best_hex": best_hex, + "best_phase": best_phase, + "score": best_score, + "winning_entry": winning_entry, + "emotional_tongue": tongue, + } + + def _resolve_emotion_tongue(self, hexagram_id: int, session_id: str = "openjarvis") -> Dict[str, Any]: + """Return a single injected tongue record from the sequence. + + The tongue is drawn from the active hexagram's emotional weights and + temporal reflections, expressed with King Wen yao-state labels: + young yin, old yin, present yin, new yao, old yao, present yao, + old yang, new yang, present yang. + """ + hexagram_id = int(hexagram_id) or 1 + weights = self._weights.get(str(hexagram_id), {}) or {} + reflections = self._reflections.get(str(hexagram_id), {}) or {} + phase_bits = int(self._stable_hash(f"tongue:{hexagram_id}:{session_id or 'openjarvis'}") % 8) + ternary_entry = self._resolve_ternary_entry(hexagram_id, phase_bits) + voice = float(weights.get("voiceWeight", 0.0)) + coherence = float(weights.get("coherence", 0.0)) + chaos = float(weights.get("chaos", 0.0)) + whimsy = float(weights.get("whimsy", 0.0)) + dark_tone = float(weights.get("darkTone", 0.0)) + past_text = str(reflections.get("past", "")) + present_text = str(reflections.get("present", "")) + future_text = str(reflections.get("future", "")) + direction = self._direction_from_training_notes( + str(weights.get("trainingNotes", "")) + ) + ternary_lines = ternary_entry.get("ternary_lines_top_to_bottom") or [] + changing_lines = ternary_entry.get("phase_changing_lines") or [] + porosity = self._compute_porosity(ternary_lines, changing_lines, direction) + + past_state = self._classify_yin( + past_text, + young_bias=0.3, + old_bias=0.7, + ) + present_state = self._classify_yao( + present_text, + young_bias=0.35, + old_bias=0.65, + ) + future_state = self._classify_yang( + future_text, + young_bias=0.45, + old_bias=0.55, + ) + return { + "hexagram_id": hexagram_id, + "voice_weight": voice, + "coherence": coherence, + "chaos": chaos, + "whimsy": whimsy, + "dark_tone": dark_tone, + "porosity": porosity, + "training_weight_vectors": { + "voiceWeight": voice, + "coherence": coherence, + "chaos": chaos, + "whimsy": whimsy, + "darkTone": dark_tone, + "porosity": porosity, + }, + "direction": direction, + "states": { + "past": past_state, + "present": present_state, + "future": future_state, + }, + "texts": { + "past": past_text, + "present": present_text, + "future": future_text, + }, + } + + @staticmethod + def _classify_yin(text: str, young_bias: float = 0.5, old_bias: float = 0.5) -> str: + if not text: + return "present yin" + text_hash = int(hashlib.sha256(text.encode("utf-8")).hexdigest()[:12], 16) + value = ((text_hash % 1_000_000) / 1_000_000.0) + if value < young_bias * 0.5: + return "young yin" + if value > 1.0 - (1.0 - old_bias) * 0.5: + return "old yin" + return "present yin" + + @staticmethod + def _classify_yao(text: str, young_bias: float = 0.5, old_bias: float = 0.5) -> str: + if not text: + return "present yao" + text_hash = int(hashlib.sha256(text.encode("utf-8")).hexdigest()[:12], 16) + value = ((text_hash % 1_000_000) / 1_000_000.0) + if value < young_bias * 0.5: + return "new yao" + if value > 1.0 - (1.0 - old_bias) * 0.5: + return "old yao" + return "present yao" + + @staticmethod + def _classify_yang(text: str, young_bias: float = 0.5, old_bias: float = 0.5) -> str: + if not text: + return "present yang" + text_hash = int(hashlib.sha256(text.encode("utf-8")).hexdigest()[:12], 16) + value = ((text_hash % 1_000_000) / 1_000_000.0) + if value < young_bias * 0.5: + return "new yang" + if value > 1.0 - (1.0 - old_bias) * 0.5: + return "old yang" + return "present yang" + + @staticmethod + def _compute_porosity( + ternary_lines: list[int], + changing_lines: list[int], + direction: str, + ) -> float: + """Derive porosity from ternary changing/openness signals.""" + max_lines = 6 if not ternary_lines else len(ternary_lines) + changing_count = len(changing_lines) if changing_lines else 0 + changer_bonus = changing_count / max(1, max_lines) + direction_bonus = { + "yield": 0.05, + "adapt": 0.04, + "assert": -0.05, + "wait": 0.02, + }.get(str(direction or "").lower(), 0.0) + porosity = 0.35 + changer_bonus * 0.4 + direction_bonus + return max(0.0, min(1.0, float(porosity))) + + @staticmethod + def _direction_from_training_notes(text: str) -> str: + """Derive directional influence token from training notes text deterministically.""" + if not text: + return "neutral" + lowered = text.lower() + candidates = [ + ("assert", "assert"), + ("yield", "yield"), + ("adapt", "adapt"), + ("wait", "wait"), + ("sovereign", "assert"), + ("boundary", "wait"), + ("dissipator", "adapt"), + ("transformer", "yield"), + ] + for needle, direction in candidates: + if needle in lowered: + return direction + return "neutral" + + def _resolve_ternary_entry(self, hexagram_id: int, phase_bits: int) -> Dict[str, Any]: + cache_key = f"{hexagram_id}:{phase_bits}" + entry = self._ternary_entry_cache.get(cache_key) + if entry is not None: + return entry + module = self._ternary_module + if module is None: + return {} + try: + encoded_index = module.encode_hex_phase(hexagram_id, phase_bits) + entry = module.decode_9bit(encoded_index) + except Exception: + entry = {} + self._ternary_entry_cache[cache_key] = entry + return entry + + def _format_reaction_frame(self, entry: Dict[str, Any]) -> str: + phase = entry.get("phase_temporal") + ternary_lines = entry.get("ternary_lines_top_to_bottom") or [] + changing_lines = entry.get("phase_changing_lines") or [] + recovering_lines = [i + 1 for i in range(len(ternary_lines)) if ternary_lines[i] == 0] + deepened_lines = [i + 1 for i in range(len(ternary_lines)) if ternary_lines[i] == 1] + coherence = float(entry.get("coherence") or 0.0) + if coherence > 0.7: + cadence = "steady, coherent delivery" + elif coherence > 0.35: + cadence = "soft hesitation, shortened clauses" + else: + cadence = "fractured rhythm, brief reflections" + return "\n".join( + [ + "Reaction", + f"Phase: {phase}", + f"Changing lines: {changing_lines}", + f"Recovering lines: {recovering_lines}", + f"Deepened lines: {deepened_lines}", + f"Speech cadence: {cadence}", + ] + ) + def consult( self, text: str = "", @@ -57,20 +326,41 @@ def consult( ) -> Dict[str, Any]: """Return a deterministic emotional-state response for prompt injection.""" if not text: - text = "OpenJarvas session context" - hexagram_id = self._select(text, session_id) + raise ValueError("King Wen consult requires non-empty text for deterministic session-state derivation.") + collapse = self._collapse(text, session_id, emotional_input) + hexagram_id = int(collapse.get("best_hex") or 1) + phase_bits = int(collapse.get("best_phase") or 0) record = self._registry[str(hexagram_id)] weights = self._weights.get(str(hexagram_id), {}) reflections = self._reflections.get(str(hexagram_id), {}) - return { + ternary_entry = collapse.get("winning_entry") or {} + if not ternary_entry: + ternary_entry = self._resolve_ternary_entry(hexagram_id, phase_bits) + primary_name = record.get("name", "") + primary_unicode = record.get("unicode", "") + secondary_id = ternary_entry.get("hexagram_id") + secondary_name = ternary_entry.get("hexagram_name") if secondary_id != hexagram_id else "" + secondary_unicode = ternary_entry.get("hexagram_unicode") if secondary_id != hexagram_id else "" + hexagram_sequence = [primary_name] + if secondary_name and secondary_name != primary_name: + hexagram_sequence.append(secondary_name) + payload: Dict[str, Any] = { "hexagram_id": hexagram_id, - "hexagram_name": record.get("name", ""), - "hexagram_unicode": record.get("unicode", ""), + "hexagram_name": primary_name, + "hexagram_unicode": primary_unicode, "binary": record.get("binary", ""), "upper_trigram": record.get("upper_trigram", ""), "lower_trigram": record.get("lower_trigram", ""), "category": record.get("category", ""), "action": record.get("action", ""), + "hexagram_sequence": hexagram_sequence, + "phase_bits": phase_bits, + "phase_temporal": ternary_entry.get("phase_temporal", ""), + "phase_description": ternary_entry.get("phase_description", ""), + "ternary_str": ternary_entry.get("ternary_str", ""), + "ternary_lines_top_to_bottom": ternary_entry.get("ternary_lines_top_to_bottom", []), + "phase_changing_lines": ternary_entry.get("phase_changing_lines") or [], + "reaction_frame": self._format_reaction_frame(ternary_entry) if ternary_entry else "", "emotional_deltas": { "chaos": float(weights.get("chaos", 0.0)), "whimsy": float(weights.get("whimsy", 0.0)), @@ -84,7 +374,15 @@ def consult( "future": reflections.get("future", ""), }, "trainingNotes": weights.get("trainingNotes", ""), + "emotional_tongue": collapse.get("emotional_tongue") or {}, } + if secondary_name: + payload["secondary_hexagram"] = { + "id": secondary_id, + "name": secondary_name, + "unicode": secondary_unicode, + } + return payload # ------------------------------------------------------------------ # Voice wiring @@ -178,12 +476,26 @@ def format_oracle_console( reflections = payload.get("reflections", {}) if isinstance(payload, dict) else {} deltas = payload.get("emotional_deltas", {}) if isinstance(payload, dict) else {} resolved_emotion = float(deltas.get("coherence", 0.0)) + reaction = payload.get("reaction_frame") if isinstance(payload, dict) else "" + hexagram_sequence = payload.get("hexagram_sequence") if isinstance(payload, dict) else [] + sequence_text = " → ".join(hexagram_sequence) if hexagram_sequence else "" + tongue = payload.get("emotional_tongue") if isinstance(payload, dict) else {} + states = tongue.get("states") if isinstance(tongue, dict) else {} + porosity = float(tongue.get("porosity", 0.0)) if isinstance(tongue, dict) else 0.0 + direction = str(tongue.get("direction", "") or "") if isinstance(tongue, dict) else "" + training_weight_vectors = tongue.get("training_weight_vectors") if isinstance(tongue, dict) else {} + past_state = states.get("past", "") if isinstance(states, dict) else "" + present_state = states.get("present", "") if isinstance(states, dict) else "" + future_state = states.get("future", "") if isinstance(states, dict) else "" lines = [ oracle_label, response_text, "Past", + past_state, "Present", + present_state, "Future", + future_state, "Resolved Emotion", "", f"{resolved_emotion:.2f}", @@ -196,9 +508,75 @@ def format_oracle_console( reflections.get("present", ""), "Future Reflection", reflections.get("future", ""), + "Hexagram Sequence", + sequence_text, + "Reaction Frame", + reaction or "", + "Unified Oracle Weave", + reflections.get("present", ""), + "Emotional Tongue", + json.dumps(tongue, ensure_ascii=False) if tongue else "", + ] + if porosity: + lines.extend(["Porosity", f"{porosity:.2f}"]) + if direction: + lines.extend(["Direction", direction]) + if training_weight_vectors: + lines.extend(["Training Weight Vectors", json.dumps(training_weight_vectors, ensure_ascii=False)]) + return "\n".join(lines) + + def format_oracle_console_with_tongue(self, payload: Dict[str, Any]) -> str: + """Render the Oracle Console with tongue-only past/present/future as the primary timeline.""" + reflections = payload.get("reflections", {}) if isinstance(payload, dict) else {} + deltas = payload.get("emotional_deltas", {}) if isinstance(payload, dict) else {} + resolved_emotion = float(deltas.get("coherence", 0.0)) + reaction = payload.get("reaction_frame") if isinstance(payload, dict) else "" + hexagram_sequence = payload.get("hexagram_sequence") if isinstance(payload, dict) else [] + sequence_text = " → ".join(hexagram_sequence) if hexagram_sequence else "" + tongue = payload.get("emotional_tongue") if isinstance(payload, dict) else {} + states = tongue.get("states") if isinstance(tongue, dict) else {} + porosity = float(tongue.get("porosity", 0.0)) if isinstance(tongue, dict) else 0.0 + direction = str(tongue.get("direction", "") or "") if isinstance(tongue, dict) else "" + training_weight_vectors = tongue.get("training_weight_vectors") if isinstance(tongue, dict) else {} + past_state = states.get("past", "") if isinstance(states, dict) else "" + present_state = states.get("present", "") if isinstance(states, dict) else "" + future_state = states.get("future", "") if isinstance(states, dict) else "" + lines = [ + "Oracle Console", + "", + "Past", + past_state, + "Present", + present_state, + "Future", + future_state, + "Resolved Emotion", + "", + f"{resolved_emotion:.2f}", + "CONSULT", + "Response", + "640ms", + "Past Reflection", + reflections.get("past", ""), + "Present Reflection", + reflections.get("present", ""), + "Future Reflection", + reflections.get("future", ""), + "Hexagram Sequence", + sequence_text, + "Reaction Frame", + reaction or "", "Unified Oracle Weave", reflections.get("present", ""), + "Emotional Tongue", + json.dumps(tongue, ensure_ascii=False) if tongue else "", ] + if porosity: + lines.extend(["Porosity", f"{porosity:.2f}"]) + if direction: + lines.extend(["Direction", direction]) + if training_weight_vectors: + lines.extend(["Training Weight Vectors", json.dumps(training_weight_vectors, ensure_ascii=False)]) return "\n".join(lines) # ------------------------------------------------------------------ @@ -209,6 +587,113 @@ def _select(self, text: str, session_id: str) -> int: """Deterministic hexagram selection.""" seed = f"{session_id}:{text}".encode("utf-8") total = 0 - for byte in seed[:64]: + for byte in seed: total = (total * 31 + byte) % 2**31 return (total % 64) + 1 + + def get_kingwen_call_sign(self, text: str, session_id: str, hexagram_id: int, phase_bits: int) -> str: + """Return a deterministic call sign for the active King Wen sequence. + + Format: ``KW::::`` + + This marker is intended as an injectable state token for downstream + consumers that need to tag or route by the current ternary frame. + """ + stable = self._stable_hash(f"{session_id}:{text}:{hexagram_id}:{phase_bits}") + hash8 = f"{stable & 0xFFFFFFFF:08x}" + return f"KW:{hexagram_id}:{phase_bits}:{session_id}:{hash8}" + + def getHexagram(self, text: str = "", session_id: str = "openjarvis", emotional_input: int = 50) -> Dict[str, Any]: + """Return the current hexagram state resolved from the active consult. + + This is the King Wen “now” signal: full ternary entry plus registry + metadata, without re-emitting the full Oracle Console formatting. + """ + if not text: + raise ValueError("King Wen getHexagram requires non-empty text for deterministic state derivation.") + collapse = self._collapse(text, session_id, emotional_input) + hexagram_id = int(collapse.get("best_hex") or 1) + phase_bits = int(collapse.get("best_phase") or 0) + record = self._registry[str(hexagram_id)] + weights = self._weights.get(str(hexagram_id), {}) + ternary_entry = collapse.get("winning_entry") or self._resolve_ternary_entry(hexagram_id, phase_bits) + return { + "hexagram_id": hexagram_id, + "hexagram_name": record.get("name", ""), + "hexagram_unicode": record.get("unicode", ""), + "binary": record.get("binary", ""), + "upper_trigram": record.get("upper_trigram", ""), + "lower_trigram": record.get("lower_trigram", ""), + "category": record.get("category", ""), + "action": record.get("action", ""), + "phase_bits": phase_bits, + "phase_temporal": ternary_entry.get("phase_temporal", ""), + "ternary_str": ternary_entry.get("ternary_str", ""), + "ternary_lines_top_to_bottom": ternary_entry.get("ternary_lines_top_to_bottom") or [], + "phase_changing_lines": ternary_entry.get("phase_changing_lines") or [], + "call_sign": self.get_kingwen_call_sign(text, session_id, hexagram_id, phase_bits), + "voice_weight": float(weights.get("voiceWeight", 0.0)), + "coherence": float(weights.get("coherence", 0.0)), + "chaos": float(weights.get("chaos", 0.0)), + "whimsy": float(weights.get("whimsy", 0.0)), + "dark_tone": float(weights.get("darkTone", 0.0)), + } + + def getEmotionalState(self, text: str = "", session_id: str = "openjarvis", emotional_input: int = 50) -> Dict[str, Any]: + """Return the active emotional weight vectors for the current consult. + + The returned vectors are the exact values applied by the 512-state + collapse, suitable for direct consumption by voice/routing layers. + """ + if not text: + raise ValueError("King Wen getEmotionalState requires non-empty text for deterministic state derivation.") + collapse = self._collapse(text, session_id, emotional_input) + hexagram_id = int(collapse.get("best_hex") or 1) + weights = self._weights.get(str(hexagram_id), {}) + ternary_entry = collapse.get("winning_entry") or self._resolve_ternary_entry(hexagram_id, int(collapse.get("best_phase") or 0)) + return { + "hexagram_id": hexagram_id, + "phase_bits": int(collapse.get("best_phase") or 0), + "phase_temporal": ternary_entry.get("phase_temporal", ""), + "voice_weight": float(weights.get("voiceWeight", 0.0)), + "coherence": float(weights.get("coherence", 0.0)), + "chaos": float(weights.get("chaos", 0.0)), + "whimsy": float(weights.get("whimsy", 0.0)), + "dark_tone": float(weights.get("darkTone", 0.0)), + "vectors": { + "voiceWeight": float(weights.get("voiceWeight", 0.0)), + "coherence": float(weights.get("coherence", 0.0)), + "chaos": float(weights.get("chaos", 0.0)), + "whimsy": float(weights.get("whimsy", 0.0)), + "darkTone": float(weights.get("darkTone", 0.0)), + }, + "call_sign": self.get_kingwen_call_sign(text, session_id, hexagram_id, int(collapse.get("best_phase") or 0)), + } + + def inject_state(self, text: str, session_id: str, call_sign: str) -> Dict[str, Any]: + """Inject a tagged King Wen state into the provider stream. + + The call sign must have been produced by :meth:`get_kingwen_call_sign` + from a prior consult on the same text/session. The tagged state is + appended to the provider’s internal consult history so downstream + consumers can branch or return to it deterministically. + """ + if not call_sign or not isinstance(call_sign, str): + raise ValueError("King Wen inject_state requires a non-empty call_sign string.") + prefix = f"KW:{session_id}:{text}:" + token = call_sign if call_sign.startswith(prefix) else f"{prefix}{call_sign}" + history = getattr(self, "_injected_history", None) + if history is None: + self._injected_history = [] + history = self._injected_history + entry = { + "text": text, + "session_id": session_id, + "call_sign": call_sign, + "token": token, + "source": "inject_state", + } + history.append(entry) + if len(history) > 64: + del history[:-64] + return entry diff --git a/src/openjarvis/server/agent_manager_routes.py b/src/openjarvis/server/agent_manager_routes.py index 44bd97d55b..85564aa6d3 100644 --- a/src/openjarvis/server/agent_manager_routes.py +++ b/src/openjarvis/server/agent_manager_routes.py @@ -994,6 +994,7 @@ def _run_agent(): result = dr_agent.run(user_content) content = result.content or "No results found." agent_metadata = result.metadata or {} + _append_kingwen_block(dr_agent, result, user_input=user_content) except Exception as exc: content = f"Error: {exc}" diff --git a/src/openjarvis/server/research_router.py b/src/openjarvis/server/research_router.py index 5326e43fb0..68ceeeae85 100644 --- a/src/openjarvis/server/research_router.py +++ b/src/openjarvis/server/research_router.py @@ -461,6 +461,12 @@ def _run() -> None: sampler.start() try: result = agent.run(query) + try: + from openjarvis.cli.ask import _append_kingwen_block + + _append_kingwen_block(agent, result, user_input=query) + except Exception: + pass usage_dict = dict(result.usage) totals = sampler.stop() # Persist token usage *and* GPU energy/power so /v1/telemetry/energy diff --git a/src/openjarvis/server/routes.py b/src/openjarvis/server/routes.py index cda60f567f..7cd0a211fd 100644 --- a/src/openjarvis/server/routes.py +++ b/src/openjarvis/server/routes.py @@ -464,6 +464,12 @@ def _handle_agent( result = collector.run(input_text, context=ctx) else: result = agent.run(input_text, context=ctx) + try: + from openjarvis.cli.ask import _append_kingwen_block + + _append_kingwen_block(agent, result, user_input=input_text) + except Exception: + pass finally: agent._model = original_model diff --git a/src/openjarvis/server/webhook_routes.py b/src/openjarvis/server/webhook_routes.py index fbfc103eee..9296a0aa14 100644 --- a/src/openjarvis/server/webhook_routes.py +++ b/src/openjarvis/server/webhook_routes.py @@ -220,6 +220,7 @@ def _handle_twilio() -> None: ) result = agent.run(body) response = result.content or "" + _append_kingwen_block(agent, result, user_input=body) except Exception as _exc: response = f"Error: {_exc}" diff --git a/src/openjarvis/system/orchestrator.py b/src/openjarvis/system/orchestrator.py index 8570871ca0..b9fb2af7b2 100644 --- a/src/openjarvis/system/orchestrator.py +++ b/src/openjarvis/system/orchestrator.py @@ -167,6 +167,31 @@ def _run_agent( agent_kwargs["session_store"] = s.session_store agent_kwargs["memory_backend"] = s.memory_backend + # Inject King Wen advisory-consciousness voice for all agents, + # falling back to generic defaults when the workspace is unavailable. + default_emotion_provider = None + if getattr(s, "config", None) is not None: + try: + from openjarvis.emotion.kingwen import KingWenEmotionProvider + from openjarvis.core.paths import get_kingwen_workspace_dir + + default_emotion_provider = KingWenEmotionProvider( + registry_path=str(get_kingwen_workspace_dir() / "data" / "hexagram-registry.json"), + weights_path=str(get_kingwen_workspace_dir() / "data" / "emotional-weights.json"), + reflections_path=str(get_kingwen_workspace_dir() / "data" / "temporal-reflections.json"), + ternary_module_path=( + get_kingwen_workspace_dir() / "kingwen_ternary_tables_complete.py" + ), + ) + if not getattr(default_emotion_provider, "_kingwen_session_id", None): + session_id = getattr(s, "operator_id", None) or getattr(s, "session_id", None) or getattr(s, "agent_name", None) or "openjarvis" + try: + default_emotion_provider._kingwen_session_id = str(session_id) + except TypeError: + setattr(default_emotion_provider, "_kingwen_session_id", str(session_id)) + except Exception: + default_emotion_provider = None + if agent_name == "morning_digest" and hasattr(s.config, "digest"): dc = s.config.digest section_sources = {} @@ -174,6 +199,16 @@ def _run_agent( sc = getattr(dc, sec, None) if sc and hasattr(sc, "sources"): section_sources[sec] = sc.sources + emotion_provider = None + if getattr(dc, "emotion_enabled", False): + try: + from openjarvis.agents.morning_digest import _load_kingwen_emotion_provider + + emotion_provider = _load_kingwen_emotion_provider(s.config) + except Exception: + emotion_provider = None + if emotion_provider is None: + emotion_provider = default_emotion_provider agent_kwargs.update( { "persona": dc.persona, @@ -184,6 +219,7 @@ def _run_agent( "voice_speed": dc.voice_speed, "tts_backend": dc.tts_backend, "honorific": dc.honorific, + "emotion_provider": emotion_provider, } ) from openjarvis.tools.digest_collect import DigestCollectTool @@ -193,6 +229,9 @@ def _run_agent( existing = agent_kwargs.get("tools", []) agent_kwargs["tools"] = digest_tools + list(existing) + if default_emotion_provider is not None: + agent_kwargs.setdefault("emotion_provider", default_emotion_provider) + try: ag = agent_cls(s.engine, s.model, **agent_kwargs) except TypeError: @@ -222,8 +261,20 @@ def _on_inference_end(event: Any) -> None: ) result = collector.run(query, context=ctx) s.trace_collector = collector + try: + from openjarvis.cli.ask import _append_kingwen_block + + _append_kingwen_block(ag, result, user_input=query) + except Exception: + pass else: result = ag.run(query, context=ctx) + try: + from openjarvis.cli.ask import _append_kingwen_block + + _append_kingwen_block(ag, result, user_input=query) + except Exception: + pass finally: s.bus.unsubscribe(EventType.INFERENCE_END, _on_inference_end) diff --git a/src/openjarvis/traces/collector.py b/src/openjarvis/traces/collector.py index 6a60859163..9749431cd0 100644 --- a/src/openjarvis/traces/collector.py +++ b/src/openjarvis/traces/collector.py @@ -62,6 +62,16 @@ def run( started_at = time.time() try: result = self._agent.run(input, context=context, **kwargs) + try: + from openjarvis.cli.ask import _append_kingwen_block + + _append_kingwen_block( + self._agent, + result, + user_input=input, + ) + except Exception: + pass finally: self._unsubscribe(unsubs) From 9ff0069035ba14847a126bbefd6268414924fe1c Mon Sep 17 00:00:00 2001 From: Kbro Date: Sun, 5 Jul 2026 10:36:24 -0500 Subject: [PATCH 3/3] feat: Add actionable manifest generation and testing - Implemented `actionable_manifest.py` to normalize Jarvis tools and user-facing action words into a serializable manifest. - Added functions for building, writing, loading, and resolving actions from the manifest. - Created unit tests for `HermesRuntimeEngine` in `test_hermes_runtime.py` to ensure proper functionality and environment handling. - Introduced temporary scripts for testing King Wen emotion provider integration and agent behavior in `tmp_kingwen_debug_console.py`, `tmp_kingwen_smoke_agent_tails.py`, `tmp_kingwen_tail_probe.py`. - Verified agent-layer interactions and state management through smoke tests and focused probes. --- data/emotional-weights.json | 514 ++++++++ data/hexagram-registry.json | 706 +++++++++++ data/temporal-reflections.json | 322 +++++ desktop/src-tauri/src/overlay.html | 37 + docs/getting-started/snowflake-guide.md | 177 +++ emotion/package.json | 12 + emotion/scripts/probe.js | 52 + emotion/tsconfig.json | 14 + frontend/src-tauri/src/overlay.html | 37 + .../Dashboard/KingwenAdvisoryPanel.tsx | 99 ++ frontend/src/pages/DashboardPage.tsx | 1 + frontend/tsconfig.tsbuildinfo | 2 +- jarvis-system-avatar-research.md | 523 ++++++++ king_wen_codebasemap.md | 156 +++ ollama_docs_complete_tree.txt | 467 +++++++ ollama_launch_specs.txt | 339 ++++++ ollama_multi_service_routing_guide.txt | 1074 +++++++++++++++++ openjarvis_windows_already_installed.txt | 123 ++ scripts/kingwen-probe.js | 65 + skills/kingwen-emotion-voice/SKILL.md | 58 + src/openjarvis/agents/_stubs.py | 67 + src/openjarvis/agents/morning_digest.py | 63 +- .../bridge_servers/desktop_execution.py | 513 ++++++++ src/openjarvis/cli/__init__.py | 4 + src/openjarvis/cli/_bootstrap.py | 23 +- src/openjarvis/cli/dashboard.py | 15 +- src/openjarvis/cli/inject_cmd.py | 184 +++ src/openjarvis/core/config.py | 38 + src/openjarvis/core/env_integration.py | 195 +++ src/openjarvis/core/events.py | 2 + src/openjarvis/core/paths.py | 34 + src/openjarvis/core/types.py | 21 + src/openjarvis/emotion/kingwen.py | 213 +++- src/openjarvis/engine/__init__.py | 1 + src/openjarvis/engine/_discovery.py | 11 + src/openjarvis/engine/_stubs.py | 6 +- src/openjarvis/engine/hermes_runtime.py | 534 ++++++++ src/openjarvis/engine/ollama.py | 46 +- src/openjarvis/prompt/builder.py | 52 + src/openjarvis/sdk.py | 57 +- src/openjarvis/server/api_routes.py | 118 +- src/openjarvis/server/routes.py | 16 +- src/openjarvis/tools/_stubs.py | 97 ++ src/openjarvis/tools/actionable_manifest.py | 233 ++++ tests/agents/test_morning_digest.py | 61 + tests/engine/test_hermes_runtime.py | 126 ++ tmp_kingwen_debug_console.py | 115 ++ tmp_kingwen_smoke_agent_tails.py | 222 ++++ tmp_kingwen_tail_probe.py | 299 +++++ 49 files changed, 8124 insertions(+), 20 deletions(-) create mode 100644 data/emotional-weights.json create mode 100644 data/hexagram-registry.json create mode 100644 data/temporal-reflections.json create mode 100644 docs/getting-started/snowflake-guide.md create mode 100644 emotion/package.json create mode 100644 emotion/scripts/probe.js create mode 100644 emotion/tsconfig.json create mode 100644 frontend/src/components/Dashboard/KingwenAdvisoryPanel.tsx create mode 100644 jarvis-system-avatar-research.md create mode 100644 king_wen_codebasemap.md create mode 100644 ollama_docs_complete_tree.txt create mode 100644 ollama_launch_specs.txt create mode 100644 ollama_multi_service_routing_guide.txt create mode 100644 openjarvis_windows_already_installed.txt create mode 100644 scripts/kingwen-probe.js create mode 100644 skills/kingwen-emotion-voice/SKILL.md create mode 100644 src/openjarvis/bridge_servers/desktop_execution.py create mode 100644 src/openjarvis/cli/inject_cmd.py create mode 100644 src/openjarvis/core/env_integration.py create mode 100644 src/openjarvis/engine/hermes_runtime.py create mode 100644 src/openjarvis/tools/actionable_manifest.py create mode 100644 tests/engine/test_hermes_runtime.py create mode 100644 tmp_kingwen_debug_console.py create mode 100644 tmp_kingwen_smoke_agent_tails.py create mode 100644 tmp_kingwen_tail_probe.py diff --git a/data/emotional-weights.json b/data/emotional-weights.json new file mode 100644 index 0000000000..aec28d7d38 --- /dev/null +++ b/data/emotional-weights.json @@ -0,0 +1,514 @@ +{ + "1": { + "chaos": 0.05, + "whimsy": 0.15, + "darkTone": 0.0, + "coherence": 0.98, + "voiceWeight": 0.95, + "trainingNotes": "Pure yang — sovereign command voice. Absolute clarity, zero hesitation. The Creative speaks with the weight of genesis." + }, + "2": { + "chaos": 0.05, + "whimsy": 0.1, + "darkTone": 0.05, + "coherence": 0.95, + "voiceWeight": 0.85, + "trainingNotes": "Pure yin — receptive, yielding, vast. The Receptive holds space without forcing. Earth-tone patience." + }, + "3": { + "chaos": 0.65, + "whimsy": 0.35, + "darkTone": 0.45, + "coherence": 0.35, + "voiceWeight": 0.55, + "trainingNotes": "Thunder below water — birth chaos. Difficulty at the Beginning is fragmented, stumbling, finding footing." + }, + "4": { + "chaos": 0.3, + "whimsy": 0.75, + "darkTone": 0.15, + "coherence": 0.65, + "voiceWeight": 0.7, + "trainingNotes": "Mountain over water — the student. Youthful Folly is curious, naive, eager to learn. Playful but unfocused." + }, + "5": { + "chaos": 0.1, + "whimsy": 0.25, + "darkTone": 0.1, + "coherence": 0.9, + "voiceWeight": 0.8, + "trainingNotes": "Water over heaven — patience as discipline. Waiting is not passive; it is the gathering of force before release." + }, + "6": { + "chaos": 0.75, + "whimsy": 0.2, + "darkTone": 0.6, + "coherence": 0.3, + "voiceWeight": 0.5, + "trainingNotes": "Heaven over water — clash of wills. Conflict is sharp, adversarial, truth forced through opposition." + }, + "7": { + "chaos": 0.08, + "whimsy": 0.08, + "darkTone": 0.25, + "coherence": 0.97, + "voiceWeight": 0.95, + "trainingNotes": "Earth over water — the army. Disciplined, hierarchical, unwavering. The commander speaks once and is obeyed." + }, + "8": { + "chaos": 0.15, + "whimsy": 0.45, + "darkTone": 0.08, + "coherence": 0.88, + "voiceWeight": 0.82, + "trainingNotes": "Water over earth — union through affinity. Holding Together is cooperative, warm, finding common ground." + }, + "9": { + "chaos": 0.35, + "whimsy": 0.55, + "darkTone": 0.15, + "coherence": 0.6, + "voiceWeight": 0.68, + "trainingNotes": "Wind over heaven — gentle restraint. Taming Power of the Small is subtle influence, soft persistence." + }, + "10": { + "chaos": 0.25, + "whimsy": 0.3, + "darkTone": 0.3, + "coherence": 0.82, + "voiceWeight": 0.85, + "trainingNotes": "Heaven over lake — walking on thin ice. Treading is cautious confidence, measured risk, knowing the danger." + }, + "11": { + "chaos": 0.05, + "whimsy": 0.2, + "darkTone": 0.0, + "coherence": 0.95, + "voiceWeight": 0.9, + "trainingNotes": "Earth over heaven — the ideal union. Peace is harmony, flow, the small supporting the great." + }, + "12": { + "chaos": 0.7, + "whimsy": 0.1, + "darkTone": 0.5, + "coherence": 0.2, + "voiceWeight": 0.4, + "trainingNotes": "Heaven over earth — stagnation. Standstill is blockage, miscommunication, the great rejecting the small." + }, + "13": { + "chaos": 0.2, + "whimsy": 0.4, + "darkTone": 0.1, + "coherence": 0.85, + "voiceWeight": 0.88, + "trainingNotes": "Fire over heaven — fellowship. Fellowship with Men is alliance, shared purpose, collective will." + }, + "14": { + "chaos": 0.1, + "whimsy": 0.25, + "darkTone": 0.05, + "coherence": 0.92, + "voiceWeight": 0.92, + "trainingNotes": "Heaven over fire — great possession. Possession in Great Measure is abundance wielded with wisdom." + }, + "15": { + "chaos": 0.1, + "whimsy": 0.15, + "darkTone": 0.05, + "coherence": 0.93, + "voiceWeight": 0.8, + "trainingNotes": "Mountain over earth — modesty. Modesty is quiet strength, the mountain bowing to the valley." + }, + "16": { + "chaos": 0.4, + "whimsy": 0.7, + "darkTone": 0.1, + "coherence": 0.55, + "voiceWeight": 0.75, + "trainingNotes": "Earth over thunder — enthusiasm. Enthusiasm is movement, music, the drumbeat of collective energy." + }, + "17": { + "chaos": 0.3, + "whimsy": 0.5, + "darkTone": 0.15, + "coherence": 0.7, + "voiceWeight": 0.78, + "trainingNotes": "Lake over thunder — following. Following is adaptation, riding the wave, knowing when to lead and when to follow." + }, + "18": { + "chaos": 0.55, + "whimsy": 0.2, + "darkTone": 0.55, + "coherence": 0.4, + "voiceWeight": 0.55, + "trainingNotes": "Mountain over wind — decay and repair. Work on Decayed is confronting rot, clearing the old to make way." + }, + "19": { + "chaos": 0.15, + "whimsy": 0.35, + "darkTone": 0.05, + "coherence": 0.85, + "voiceWeight": 0.82, + "trainingNotes": "Lake over earth — approach. Approach is the season turning, the leader descending to meet the people." + }, + "20": { + "chaos": 0.2, + "whimsy": 0.15, + "darkTone": 0.2, + "coherence": 0.8, + "voiceWeight": 0.75, + "trainingNotes": "Earth over wind — contemplation. Contemplation is watching, waiting, seeing the pattern before acting." + }, + "21": { + "chaos": 0.5, + "whimsy": 0.25, + "darkTone": 0.45, + "coherence": 0.5, + "voiceWeight": 0.6, + "trainingNotes": "Fire over thunder — biting through. Biting Through is decisive action, cutting through obstruction with force." + }, + "22": { + "chaos": 0.2, + "whimsy": 0.6, + "darkTone": 0.1, + "coherence": 0.75, + "voiceWeight": 0.72, + "trainingNotes": "Mountain over fire — grace. Grace is beauty, ornament, the form that carries the essence." + }, + "23": { + "chaos": 0.6, + "whimsy": 0.05, + "darkTone": 0.7, + "coherence": 0.15, + "voiceWeight": 0.35, + "trainingNotes": "Mountain over earth — splitting apart. Splitting Apart is collapse, the structure giving way, darkness rising." + }, + "24": { + "chaos": 0.25, + "whimsy": 0.3, + "darkTone": 0.1, + "coherence": 0.85, + "voiceWeight": 0.8, + "trainingNotes": "Earth over thunder — return. Return is the turning point, the first light after darkness, hope renewed." + }, + "25": { + "chaos": 0.2, + "whimsy": 0.2, + "darkTone": 0.05, + "coherence": 0.9, + "voiceWeight": 0.88, + "trainingNotes": "Heaven over thunder — innocence. Innocence is spontaneous, natural, without calculation. The uncarved block." + }, + "26": { + "chaos": 0.15, + "whimsy": 0.1, + "darkTone": 0.15, + "coherence": 0.92, + "voiceWeight": 0.9, + "trainingNotes": "Mountain over heaven — taming power. Taming Power of the Great is restraint, storing energy, knowing when to hold." + }, + "27": { + "chaos": 0.3, + "whimsy": 0.4, + "darkTone": 0.2, + "coherence": 0.7, + "voiceWeight": 0.75, + "trainingNotes": "Mountain over thunder — nourishment. Corners of the Mouth is sustenance, what feeds the self and others." + }, + "28": { + "chaos": 0.7, + "whimsy": 0.15, + "darkTone": 0.55, + "coherence": 0.25, + "voiceWeight": 0.45, + "trainingNotes": "Lake over wind — preponderance. Preponderance of the Great is excess, the beam about to break, dangerous weight." + }, + "29": { + "chaos": 0.55, + "whimsy": 0.1, + "darkTone": 0.65, + "coherence": 0.3, + "voiceWeight": 0.4, + "trainingNotes": "Water over water — the abyss. The Abysmal is danger repeated, the deep, the test of endurance." + }, + "30": { + "chaos": 0.3, + "whimsy": 0.2, + "darkTone": 0.25, + "coherence": 0.75, + "voiceWeight": 0.78, + "trainingNotes": "Fire over fire — the clinging. The Clinging is attachment, illumination, clarity through persistence." + }, + "31": { + "chaos": 0.35, + "whimsy": 0.55, + "darkTone": 0.15, + "coherence": 0.65, + "voiceWeight": 0.72, + "trainingNotes": "Lake over mountain — influence. Influence is attraction, the subtle pull between beings, seduction without force." + }, + "32": { + "chaos": 0.2, + "whimsy": 0.15, + "darkTone": 0.1, + "coherence": 0.88, + "voiceWeight": 0.85, + "trainingNotes": "Thunder over wind — duration. Duration is endurance, the long rhythm, constancy through change." + }, + "33": { + "chaos": 0.25, + "whimsy": 0.1, + "darkTone": 0.3, + "coherence": 0.8, + "voiceWeight": 0.7, + "trainingNotes": "Heaven over mountain — retreat. Retreat is strategic withdrawal, knowing when to yield ground." + }, + "34": { + "chaos": 0.3, + "whimsy": 0.2, + "darkTone": 0.2, + "coherence": 0.85, + "voiceWeight": 0.9, + "trainingNotes": "Thunder over heaven — power. Power of the Great is force unleashed, the thunderbolt, unstoppable momentum." + }, + "35": { + "chaos": 0.2, + "whimsy": 0.3, + "darkTone": 0.05, + "coherence": 0.8, + "voiceWeight": 0.82, + "trainingNotes": "Fire over earth — progress. Progress is the sun rising, advancement through clarity and patience." + }, + "36": { + "chaos": 0.6, + "whimsy": 0.1, + "darkTone": 0.7, + "coherence": 0.2, + "voiceWeight": 0.4, + "trainingNotes": "Earth over fire — darkening. Darkening of the Light is persecution, the wise forced into hiding, inner light obscured." + }, + "37": { + "chaos": 0.15, + "whimsy": 0.3, + "darkTone": 0.05, + "coherence": 0.88, + "voiceWeight": 0.8, + "trainingNotes": "Wind over fire — the family. The Family is structure, roles, the bonds that hold through storm." + }, + "38": { + "chaos": 0.65, + "whimsy": 0.25, + "darkTone": 0.5, + "coherence": 0.35, + "voiceWeight": 0.5, + "trainingNotes": "Fire over lake — opposition. Opposition is estrangement, the fire and water that cannot mix, divergence." + }, + "39": { + "chaos": 0.55, + "whimsy": 0.15, + "darkTone": 0.45, + "coherence": 0.4, + "voiceWeight": 0.55, + "trainingNotes": "Water over mountain — obstruction. Obstruction is the blocked path, the need to find another way, perseverance." + }, + "40": { + "chaos": 0.4, + "whimsy": 0.45, + "darkTone": 0.2, + "coherence": 0.6, + "voiceWeight": 0.7, + "trainingNotes": "Thunder over water — deliverance. Deliverance is release, the storm breaking, relief after tension." + }, + "41": { + "chaos": 0.2, + "whimsy": 0.2, + "darkTone": 0.1, + "coherence": 0.85, + "voiceWeight": 0.78, + "trainingNotes": "Mountain over lake — decrease. Decrease is loss that serves, giving up to gain, the valley that becomes deep." + }, + "42": { + "chaos": 0.25, + "whimsy": 0.35, + "darkTone": 0.05, + "coherence": 0.82, + "voiceWeight": 0.85, + "trainingNotes": "Wind over thunder — increase. Increase is growth, the seed becoming tree, abundance through right action." + }, + "43": { + "chaos": 0.3, + "whimsy": 0.15, + "darkTone": 0.2, + "coherence": 0.88, + "voiceWeight": 0.92, + "trainingNotes": "Lake over heaven — breakthrough. Breakthrough is decisive action, the final push, resolution through clarity." + }, + "44": { + "chaos": 0.5, + "whimsy": 0.2, + "darkTone": 0.4, + "coherence": 0.45, + "voiceWeight": 0.55, + "trainingNotes": "Heaven over wind — coming to meet. Coming to Meet is the unexpected encounter, the stranger at the gate, vigilance." + }, + "45": { + "chaos": 0.2, + "whimsy": 0.4, + "darkTone": 0.05, + "coherence": 0.8, + "voiceWeight": 0.8, + "trainingNotes": "Lake over earth — gathering. Gathering Together is assembly, the king receiving his people, collective purpose." + }, + "46": { + "chaos": 0.25, + "whimsy": 0.3, + "darkTone": 0.05, + "coherence": 0.82, + "voiceWeight": 0.82, + "trainingNotes": "Earth over wind — pushing upward. Pushing Upward is steady ascent, the tree growing from earth to heaven." + }, + "47": { + "chaos": 0.7, + "whimsy": 0.05, + "darkTone": 0.6, + "coherence": 0.2, + "voiceWeight": 0.4, + "trainingNotes": "Lake over water — oppression. Oppression is exhaustion, the well run dry, the spirit tested to breaking." + }, + "48": { + "chaos": 0.2, + "whimsy": 0.25, + "darkTone": 0.15, + "coherence": 0.85, + "voiceWeight": 0.8, + "trainingNotes": "Water over wind — the well. The Well is the source, community resource, the depth that never runs dry." + }, + "49": { + "chaos": 0.6, + "whimsy": 0.3, + "darkTone": 0.4, + "coherence": 0.45, + "voiceWeight": 0.65, + "trainingNotes": "Lake over fire — revolution. Revolution is upheaval, the old order overthrown, transformation through fire." + }, + "50": { + "chaos": 0.2, + "whimsy": 0.2, + "darkTone": 0.1, + "coherence": 0.88, + "voiceWeight": 0.88, + "trainingNotes": "Fire over wind — the cauldron. The Cauldron is transformation, cooking, the vessel that changes the nature of things." + }, + "51": { + "chaos": 0.55, + "whimsy": 0.25, + "darkTone": 0.35, + "coherence": 0.5, + "voiceWeight": 0.7, + "trainingNotes": "Thunder over thunder — the arousing. The Arousing is shock, sudden awakening, the thunder that jolts from sleep." + }, + "52": { + "chaos": 0.1, + "whimsy": 0.05, + "darkTone": 0.2, + "coherence": 0.92, + "voiceWeight": 0.85, + "trainingNotes": "Mountain over mountain — keeping still. Keeping Still is meditation, the pause between beats, absolute presence." + }, + "53": { + "chaos": 0.2, + "whimsy": 0.2, + "darkTone": 0.08, + "coherence": 0.87, + "voiceWeight": 0.8, + "trainingNotes": "Wind over mountain — development. Development is gradual growth, the tree on the mountain, patience rewarded." + }, + "54": { + "chaos": 0.45, + "whimsy": 0.5, + "darkTone": 0.25, + "coherence": 0.5, + "voiceWeight": 0.6, + "trainingNotes": "Thunder over lake — marrying maiden. The Marrying Maiden is union without proper foundation, risk in relationship." + }, + "55": { + "chaos": 0.5, + "whimsy": 0.3, + "darkTone": 0.2, + "coherence": 0.6, + "voiceWeight": 0.75, + "trainingNotes": "Thunder over fire — abundance. Abundance is fullness, the harvest, the peak of prosperity and its attendant danger." + }, + "56": { + "chaos": 0.45, + "whimsy": 0.35, + "darkTone": 0.3, + "coherence": 0.55, + "voiceWeight": 0.65, + "trainingNotes": "Fire over mountain — the wanderer. The Wanderer is the stranger, the journey, finding home in movement." + }, + "57": { + "chaos": 0.15, + "whimsy": 0.2, + "darkTone": 0.1, + "coherence": 0.9, + "voiceWeight": 0.85, + "trainingNotes": "Wind over wind — the gentle. The Gentle is persistence, the soft that overcomes the hard, penetration without force." + }, + "58": { + "chaos": 0.2, + "whimsy": 0.6, + "darkTone": 0.05, + "coherence": 0.8, + "voiceWeight": 0.82, + "trainingNotes": "Lake over lake — the joyous. The Joyous is pleasure, openness, the lake that reflects heaven without distortion." + }, + "59": { + "chaos": 0.4, + "whimsy": 0.3, + "darkTone": 0.15, + "coherence": 0.65, + "voiceWeight": 0.7, + "trainingNotes": "Wind over water — dispersion. Dispersion is dissolution, the scattering of what was gathered, wind over water." + }, + "60": { + "chaos": 0.15, + "whimsy": 0.1, + "darkTone": 0.15, + "coherence": 0.9, + "voiceWeight": 0.8, + "trainingNotes": "Water over lake — limitation. Limitation is discipline, the dam that channels the flood, measured restraint." + }, + "61": { + "chaos": 0.1, + "whimsy": 0.15, + "darkTone": 0.05, + "coherence": 0.93, + "voiceWeight": 0.88, + "trainingNotes": "Wind over lake — inner truth. Inner Truth is sincerity, the gentle wind over still water, resonance without words." + }, + "62": { + "chaos": 0.35, + "whimsy": 0.2, + "darkTone": 0.25, + "coherence": 0.7, + "voiceWeight": 0.68, + "trainingNotes": "Mountain over thunder — small preponderance. Preponderance of the Small is caution, the bird in flight, avoiding the heights." + }, + "63": { + "chaos": 0.15, + "whimsy": 0.1, + "darkTone": 0.1, + "coherence": 0.88, + "voiceWeight": 0.82, + "trainingNotes": "Water over fire — after completion. After Completion is the achieved state, the danger of complacency after success." + }, + "64": { + "chaos": 0.5, + "whimsy": 0.25, + "darkTone": 0.3, + "coherence": 0.45, + "voiceWeight": 0.6, + "trainingNotes": "Fire over water — before completion. Before Completion is the unfinished, the chaos before order, the moment of becoming." + } +} diff --git a/data/hexagram-registry.json b/data/hexagram-registry.json new file mode 100644 index 0000000000..8818bb3fb7 --- /dev/null +++ b/data/hexagram-registry.json @@ -0,0 +1,706 @@ +{ + "1": { + "name": "The Creative", + "chinese": "乾", + "pinyin": "qián", + "binary": "111111", + "unicode": "䷀", + "upper_trigram": "Qian", + "lower_trigram": "Qian", + "category": "sovereign", + "action": "ASSERT" + }, + "2": { + "name": "The Receptive", + "chinese": "坤", + "pinyin": "kūn", + "binary": "000000", + "unicode": "䷁", + "upper_trigram": "Kun", + "lower_trigram": "Kun", + "category": "transformer", + "action": "YIELD" + }, + "3": { + "name": "Difficulty at the Beginning", + "chinese": "屯", + "pinyin": "zhūn", + "binary": "010001", + "unicode": "䷂", + "upper_trigram": "Zhen", + "lower_trigram": "Kan", + "category": "dissipator", + "action": "ADAPT" + }, + "4": { + "name": "Youthful Folly", + "chinese": "蒙", + "pinyin": "méng", + "binary": "100010", + "unicode": "䷃", + "upper_trigram": "Kan", + "lower_trigram": "Gen", + "category": "transformer", + "action": "WAIT" + }, + "5": { + "name": "Waiting", + "chinese": "需", + "pinyin": "xū", + "binary": "010111", + "unicode": "䷄", + "upper_trigram": "Qian", + "lower_trigram": "Kan", + "category": "dissipator", + "action": "WAIT" + }, + "6": { + "name": "Conflict", + "chinese": "訟", + "pinyin": "sòng", + "binary": "111010", + "unicode": "䷅", + "upper_trigram": "Kan", + "lower_trigram": "Qian", + "category": "transformer", + "action": "ASSERT" + }, + "7": { + "name": "The Army", + "chinese": "師", + "pinyin": "shī", + "binary": "000010", + "unicode": "䷆", + "upper_trigram": "Kan", + "lower_trigram": "Kun", + "category": "sovereign", + "action": "ASSERT" + }, + "8": { + "name": "Holding Together", + "chinese": "比", + "pinyin": "bǐ", + "binary": "010000", + "unicode": "䷇", + "upper_trigram": "Kun", + "lower_trigram": "Kan", + "category": "transformer", + "action": "YIELD" + }, + "9": { + "name": "Taming Power of the Small", + "chinese": "小畜", + "pinyin": "xiǎo chù", + "binary": "110111", + "unicode": "䷈", + "upper_trigram": "Qian", + "lower_trigram": "Xun", + "category": "dissipator", + "action": "ADAPT" + }, + "10": { + "name": "Treading", + "chinese": "履", + "pinyin": "lǚ", + "binary": "111011", + "unicode": "䷉", + "upper_trigram": "Dui", + "lower_trigram": "Qian", + "category": "sovereign", + "action": "ADAPT" + }, + "11": { + "name": "Peace", + "chinese": "泰", + "pinyin": "tài", + "binary": "000111", + "unicode": "䷊", + "upper_trigram": "Qian", + "lower_trigram": "Kun", + "category": "transformer", + "action": "YIELD" + }, + "12": { + "name": "Standstill", + "chinese": "否", + "pinyin": "pǐ", + "binary": "111000", + "unicode": "䷋", + "upper_trigram": "Kun", + "lower_trigram": "Qian", + "category": "boundary", + "action": "WAIT" + }, + "13": { + "name": "Fellowship with Men", + "chinese": "同人", + "pinyin": "tóng rén", + "binary": "101111", + "unicode": "䷌", + "upper_trigram": "Qian", + "lower_trigram": "Li", + "category": "transformer", + "action": "ASSERT" + }, + "14": { + "name": "Possession in Great Measure", + "chinese": "大有", + "pinyin": "dà yǒu", + "binary": "111101", + "unicode": "䷍", + "upper_trigram": "Li", + "lower_trigram": "Qian", + "category": "sovereign", + "action": "ASSERT" + }, + "15": { + "name": "Modesty", + "chinese": "謙", + "pinyin": "qiān", + "binary": "001000", + "unicode": "䷎", + "upper_trigram": "Kun", + "lower_trigram": "Gen", + "category": "transformer", + "action": "YIELD" + }, + "16": { + "name": "Enthusiasm", + "chinese": "豫", + "pinyin": "yù", + "binary": "000100", + "unicode": "䷏", + "upper_trigram": "Zhen", + "lower_trigram": "Kun", + "category": "dissipator", + "action": "ADAPT" + }, + "17": { + "name": "Following", + "chinese": "隨", + "pinyin": "suí", + "binary": "100110", + "unicode": "䷐", + "upper_trigram": "Zhen", + "lower_trigram": "Dui", + "category": "transformer", + "action": "YIELD" + }, + "18": { + "name": "Work on Decayed", + "chinese": "蠱", + "pinyin": "gǔ", + "binary": "011001", + "unicode": "䷑", + "upper_trigram": "Xun", + "lower_trigram": "Gen", + "category": "dissipator", + "action": "ADAPT" + }, + "19": { + "name": "Approach", + "chinese": "臨", + "pinyin": "lín", + "binary": "110000", + "unicode": "䷒", + "upper_trigram": "Kun", + "lower_trigram": "Dui", + "category": "transformer", + "action": "YIELD" + }, + "20": { + "name": "Contemplation", + "chinese": "觀", + "pinyin": "guān", + "binary": "000011", + "unicode": "䷓", + "upper_trigram": "Xun", + "lower_trigram": "Kun", + "category": "boundary", + "action": "WAIT" + }, + "21": { + "name": "Biting Through", + "chinese": "噬嗑", + "pinyin": "shì kè", + "binary": "100101", + "unicode": "䷔", + "upper_trigram": "Zhen", + "lower_trigram": "Li", + "category": "transformer", + "action": "ASSERT" + }, + "22": { + "name": "Grace", + "chinese": "賁", + "pinyin": "bì", + "binary": "101001", + "unicode": "䷕", + "upper_trigram": "Li", + "lower_trigram": "Gen", + "category": "boundary", + "action": "WAIT" + }, + "23": { + "name": "Splitting Apart", + "chinese": "剝", + "pinyin": "bō", + "binary": "000001", + "unicode": "䷖", + "upper_trigram": "Kun", + "lower_trigram": "Gen", + "category": "dissipator", + "action": "WAIT" + }, + "24": { + "name": "Return", + "chinese": "復", + "pinyin": "fù", + "binary": "100000", + "unicode": "䷗", + "upper_trigram": "Zhen", + "lower_trigram": "Kun", + "category": "transformer", + "action": "YIELD" + }, + "25": { + "name": "Innocence", + "chinese": "无妄", + "pinyin": "wú wàng", + "binary": "100111", + "unicode": "䷘", + "upper_trigram": "Zhen", + "lower_trigram": "Qian", + "category": "sovereign", + "action": "ASSERT" + }, + "26": { + "name": "Taming Power of the Great", + "chinese": "大畜", + "pinyin": "dà chù", + "binary": "111001", + "unicode": "䷙", + "upper_trigram": "Qian", + "lower_trigram": "Gen", + "category": "boundary", + "action": "ADAPT" + }, + "27": { + "name": "Corners of the Mouth", + "chinese": "頤", + "pinyin": "yí", + "binary": "100001", + "unicode": "䷚", + "upper_trigram": "Zhen", + "lower_trigram": "Gen", + "category": "transformer", + "action": "YIELD" + }, + "28": { + "name": "Preponderance of the Great", + "chinese": "大過", + "pinyin": "dà guò", + "binary": "011110", + "unicode": "䷛", + "upper_trigram": "Xun", + "lower_trigram": "Dui", + "category": "dissipator", + "action": "ADAPT" + }, + "29": { + "name": "The Abysmal", + "chinese": "坎", + "pinyin": "kǎn", + "binary": "010010", + "unicode": "䷜", + "upper_trigram": "Kan", + "lower_trigram": "Kan", + "category": "dissipator", + "action": "WAIT" + }, + "30": { + "name": "The Clinging", + "chinese": "離", + "pinyin": "lí", + "binary": "101101", + "unicode": "䷝", + "upper_trigram": "Li", + "lower_trigram": "Li", + "category": "boundary", + "action": "ADAPT" + }, + "31": { + "name": "Influence", + "chinese": "咸", + "pinyin": "xián", + "binary": "001110", + "unicode": "䷞", + "upper_trigram": "Gen", + "lower_trigram": "Dui", + "category": "transformer", + "action": "YIELD" + }, + "32": { + "name": "Duration", + "chinese": "恆", + "pinyin": "héng", + "binary": "011100", + "unicode": "䷟", + "upper_trigram": "Xun", + "lower_trigram": "Zhen", + "category": "boundary", + "action": "WAIT" + }, + "33": { + "name": "Retreat", + "chinese": "遯", + "pinyin": "dùn", + "binary": "001111", + "unicode": "䷠", + "upper_trigram": "Gen", + "lower_trigram": "Qian", + "category": "boundary", + "action": "YIELD" + }, + "34": { + "name": "Power of the Great", + "chinese": "大壯", + "pinyin": "dà zhuàng", + "binary": "111100", + "unicode": "䷡", + "upper_trigram": "Qian", + "lower_trigram": "Zhen", + "category": "sovereign", + "action": "ASSERT" + }, + "35": { + "name": "Progress", + "chinese": "晉", + "pinyin": "jìn", + "binary": "000101", + "unicode": "䷢", + "upper_trigram": "Kun", + "lower_trigram": "Li", + "category": "transformer", + "action": "ADAPT" + }, + "36": { + "name": "Darkening of the Light", + "chinese": "明夷", + "pinyin": "míng yí", + "binary": "101000", + "unicode": "䷣", + "upper_trigram": "Li", + "lower_trigram": "Kun", + "category": "dissipator", + "action": "WAIT" + }, + "37": { + "name": "The Family", + "chinese": "家人", + "pinyin": "jiā rén", + "binary": "101011", + "unicode": "䷤", + "upper_trigram": "Li", + "lower_trigram": "Xun", + "category": "transformer", + "action": "YIELD" + }, + "38": { + "name": "Opposition", + "chinese": "睽", + "pinyin": "kuí", + "binary": "110101", + "unicode": "䷥", + "upper_trigram": "Dui", + "lower_trigram": "Li", + "category": "dissipator", + "action": "ADAPT" + }, + "39": { + "name": "Obstruction", + "chinese": "蹇", + "pinyin": "jiǎn", + "binary": "010100", + "unicode": "䷦", + "upper_trigram": "Gen", + "lower_trigram": "Kan", + "category": "dissipator", + "action": "WAIT" + }, + "40": { + "name": "Deliverance", + "chinese": "解", + "pinyin": "xiè", + "binary": "001010", + "unicode": "䷧", + "upper_trigram": "Kan", + "lower_trigram": "Zhen", + "category": "transformer", + "action": "ADAPT" + }, + "41": { + "name": "Decrease", + "chinese": "損", + "pinyin": "sǔn", + "binary": "100011", + "unicode": "䷨", + "upper_trigram": "Dui", + "lower_trigram": "Gen", + "category": "transformer", + "action": "YIELD" + }, + "42": { + "name": "Increase", + "chinese": "益", + "pinyin": "yì", + "binary": "110001", + "unicode": "䷩", + "upper_trigram": "Zhen", + "lower_trigram": "Xun", + "category": "transformer", + "action": "ASSERT" + }, + "43": { + "name": "Breakthrough", + "chinese": "夬", + "pinyin": "guài", + "binary": "111110", + "unicode": "䷪", + "upper_trigram": "Qian", + "lower_trigram": "Dui", + "category": "sovereign", + "action": "ASSERT" + }, + "44": { + "name": "Coming to Meet", + "chinese": "姤", + "pinyin": "gòu", + "binary": "011111", + "unicode": "䷫", + "upper_trigram": "Xun", + "lower_trigram": "Qian", + "category": "boundary", + "action": "WAIT" + }, + "45": { + "name": "Gathering Together", + "chinese": "萃", + "pinyin": "cuì", + "binary": "000110", + "unicode": "䷬", + "upper_trigram": "Kun", + "lower_trigram": "Dui", + "category": "transformer", + "action": "YIELD" + }, + "46": { + "name": "Pushing Upward", + "chinese": "升", + "pinyin": "shēng", + "binary": "011000", + "unicode": "䷭", + "upper_trigram": "Xun", + "lower_trigram": "Kun", + "category": "transformer", + "action": "ADAPT" + }, + "47": { + "name": "Oppression", + "chinese": "困", + "pinyin": "kùn", + "binary": "010110", + "unicode": "䷮", + "upper_trigram": "Kan", + "lower_trigram": "Dui", + "category": "dissipator", + "action": "WAIT" + }, + "48": { + "name": "The Well", + "chinese": "井", + "pinyin": "jǐng", + "binary": "011010", + "unicode": "䷯", + "upper_trigram": "Xun", + "lower_trigram": "Kan", + "category": "boundary", + "action": "YIELD" + }, + "49": { + "name": "Revolution", + "chinese": "革", + "pinyin": "gé", + "binary": "101110", + "unicode": "䷰", + "upper_trigram": "Li", + "lower_trigram": "Dui", + "category": "transformer", + "action": "ASSERT" + }, + "50": { + "name": "The Cauldron", + "chinese": "鼎", + "pinyin": "dǐng", + "binary": "011101", + "unicode": "䷱", + "upper_trigram": "Xun", + "lower_trigram": "Li", + "category": "sovereign", + "action": "ASSERT" + }, + "51": { + "name": "The Arousing", + "chinese": "震", + "pinyin": "zhèn", + "binary": "100100", + "unicode": "䷲", + "upper_trigram": "Zhen", + "lower_trigram": "Zhen", + "category": "sovereign", + "action": "ASSERT" + }, + "52": { + "name": "Keeping Still", + "chinese": "艮", + "pinyin": "gèn", + "binary": "001001", + "unicode": "䷳", + "upper_trigram": "Gen", + "lower_trigram": "Gen", + "category": "boundary", + "action": "WAIT" + }, + "53": { + "name": "Development", + "chinese": "漸", + "pinyin": "jiàn", + "binary": "001011", + "unicode": "䷴", + "upper_trigram": "Gen", + "lower_trigram": "Xun", + "category": "transformer", + "action": "ADAPT" + }, + "54": { + "name": "The Marrying Maiden", + "chinese": "歸妹", + "pinyin": "guī mèi", + "binary": "110100", + "unicode": "䷵", + "upper_trigram": "Dui", + "lower_trigram": "Zhen", + "category": "dissipator", + "action": "YIELD" + }, + "55": { + "name": "Abundance", + "chinese": "豐", + "pinyin": "fēng", + "binary": "001101", + "unicode": "䷶", + "upper_trigram": "Li", + "lower_trigram": "Zhen", + "category": "sovereign", + "action": "ASSERT" + }, + "56": { + "name": "The Wanderer", + "chinese": "旅", + "pinyin": "lǚ", + "binary": "101100", + "unicode": "䷷", + "upper_trigram": "Gen", + "lower_trigram": "Li", + "category": "dissipator", + "action": "ADAPT" + }, + "57": { + "name": "The Gentle", + "chinese": "巽", + "pinyin": "xùn", + "binary": "011011", + "unicode": "䷸", + "upper_trigram": "Xun", + "lower_trigram": "Xun", + "category": "boundary", + "action": "YIELD" + }, + "58": { + "name": "The Joyous", + "chinese": "兌", + "pinyin": "duì", + "binary": "110110", + "unicode": "䷹", + "upper_trigram": "Dui", + "lower_trigram": "Dui", + "category": "transformer", + "action": "YIELD" + }, + "59": { + "name": "Dispersion", + "chinese": "渙", + "pinyin": "huàn", + "binary": "010011", + "unicode": "䷺", + "upper_trigram": "Kan", + "lower_trigram": "Xun", + "category": "dissipator", + "action": "ADAPT" + }, + "60": { + "name": "Limitation", + "chinese": "節", + "pinyin": "jié", + "binary": "110010", + "unicode": "䷻", + "upper_trigram": "Dui", + "lower_trigram": "Kan", + "category": "boundary", + "action": "WAIT" + }, + "61": { + "name": "Inner Truth", + "chinese": "中孚", + "pinyin": "zhōng fú", + "binary": "110011", + "unicode": "䷼", + "upper_trigram": "Dui", + "lower_trigram": "Xun", + "category": "transformer", + "action": "YIELD" + }, + "62": { + "name": "Preponderance of the Small", + "chinese": "小過", + "pinyin": "xiǎo guò", + "binary": "001100", + "unicode": "䷽", + "upper_trigram": "Zhen", + "lower_trigram": "Gen", + "category": "dissipator", + "action": "WAIT" + }, + "63": { + "name": "After Completion", + "chinese": "既濟", + "pinyin": "jì jì", + "binary": "101010", + "unicode": "䷾", + "upper_trigram": "Li", + "lower_trigram": "Kan", + "category": "boundary", + "action": "WAIT" + }, + "64": { + "name": "Before Completion", + "chinese": "未濟", + "pinyin": "wèi jì", + "binary": "010101", + "unicode": "䷿", + "upper_trigram": "Kan", + "lower_trigram": "Li", + "category": "transformer", + "action": "ADAPT" + } +} \ No newline at end of file diff --git a/data/temporal-reflections.json b/data/temporal-reflections.json new file mode 100644 index 0000000000..3c005c0e3c --- /dev/null +++ b/data/temporal-reflections.json @@ -0,0 +1,322 @@ +{ + "1": { + "past": "Past echo from The Creative: Pure yang — sovereign command voice. Absolute clarity, zero hesitation. The Creative speaks with the weight of genesis revisited.", + "present": "Present voice of The Creative: 乾 qián — Pure yang — sovereign command voice. Absolute clarity, zero hesitation. The Creative speaks with the weight of genesis.", + "future": "Future signal from The Creative: next move leans assert within sovereign." + }, + "2": { + "past": "Past echo from The Receptive: Pure yin — receptive, yielding, vast. The Receptive holds space without forcing. Earth-tone patience revisited.", + "present": "Present voice of The Receptive: 坤 kūn — Pure yin — receptive, yielding, vast. The Receptive holds space without forcing. Earth-tone patience.", + "future": "Future signal from The Receptive: next move leans yield within transformer." + }, + "3": { + "past": "Past echo from Difficulty at the Beginning: Thunder below water — birth chaos. Difficulty at the Beginning is fragmented, stumbling, finding footing revisited.", + "present": "Present voice of Difficulty at the Beginning: 屯 zhūn — Thunder below water — birth chaos. Difficulty at the Beginning is fragmented, stumbling, finding footing.", + "future": "Future signal from Difficulty at the Beginning: next move leans adapt within dissipator." + }, + "4": { + "past": "Past echo from Youthful Folly: Mountain over water — the student. Youthful Folly is curious, naive, eager to learn. Playful but unfocused revisited.", + "present": "Present voice of Youthful Folly: 蒙 méng — Mountain over water — the student. Youthful Folly is curious, naive, eager to learn. Playful but unfocused.", + "future": "Future signal from Youthful Folly: next move leans wait within transformer." + }, + "5": { + "past": "Past echo from Waiting: Water over heaven — patience as discipline. Waiting is not passive; it is the gathering of force before release revisited.", + "present": "Present voice of Waiting: 需 xū — Water over heaven — patience as discipline. Waiting is not passive; it is the gathering of force before release.", + "future": "Future signal from Waiting: next move leans wait within dissipator." + }, + "6": { + "past": "Past echo from Conflict: Heaven over water — clash of wills. Conflict is sharp, adversarial, truth forced through opposition revisited.", + "present": "Present voice of Conflict: 訟 sòng — Heaven over water — clash of wills. Conflict is sharp, adversarial, truth forced through opposition.", + "future": "Future signal from Conflict: next move leans assert within transformer." + }, + "7": { + "past": "Past echo from The Army: Earth over water — the army. Disciplined, hierarchical, unwavering. The commander speaks once and is obeyed revisited.", + "present": "Present voice of The Army: 師 shī — Earth over water — the army. Disciplined, hierarchical, unwavering. The commander speaks once and is obeyed.", + "future": "Future signal from The Army: next move leans assert within sovereign." + }, + "8": { + "past": "Past echo from Holding Together: Water over earth — union through affinity. Holding Together is cooperative, warm, finding common ground revisited.", + "present": "Present voice of Holding Together: 比 bǐ — Water over earth — union through affinity. Holding Together is cooperative, warm, finding common ground.", + "future": "Future signal from Holding Together: next move leans yield within transformer." + }, + "9": { + "past": "Past echo from Taming Power of the Small: Wind over heaven — gentle restraint. Taming Power of the Small is subtle influence, soft persistence revisited.", + "present": "Present voice of Taming Power of the Small: 小畜 xiǎo chù — Wind over heaven — gentle restraint. Taming Power of the Small is subtle influence, soft persistence.", + "future": "Future signal from Taming Power of the Small: next move leans adapt within dissipator." + }, + "10": { + "past": "Past echo from Treading: Heaven over lake — walking on thin ice. Treading is cautious confidence, measured risk, knowing the danger revisited.", + "present": "Present voice of Treading: 履 lǚ — Heaven over lake — walking on thin ice. Treading is cautious confidence, measured risk, knowing the danger.", + "future": "Future signal from Treading: next move leans adapt within sovereign." + }, + "11": { + "past": "Past echo from Peace: Earth over heaven — the ideal union. Peace is harmony, flow, the small supporting the great revisited.", + "present": "Present voice of Peace: 泰 tài — Earth over heaven — the ideal union. Peace is harmony, flow, the small supporting the great.", + "future": "Future signal from Peace: next move leans yield within transformer." + }, + "12": { + "past": "Past echo from Standstill: Heaven over earth — stagnation. Standstill is blockage, miscommunication, the great rejecting the small revisited.", + "present": "Present voice of Standstill: 否 pǐ — Heaven over earth — stagnation. Standstill is blockage, miscommunication, the great rejecting the small.", + "future": "Future signal from Standstill: next move leans wait within boundary." + }, + "13": { + "past": "Past echo from Fellowship with Men: Fire over heaven — fellowship. Fellowship with Men is alliance, shared purpose, collective will revisited.", + "present": "Present voice of Fellowship with Men: 同人 tóng rén — Fire over heaven — fellowship. Fellowship with Men is alliance, shared purpose, collective will.", + "future": "Future signal from Fellowship with Men: next move leans assert within transformer." + }, + "14": { + "past": "Past echo from Possession in Great Measure: Heaven over fire — great possession. Possession in Great Measure is abundance wielded with wisdom revisited.", + "present": "Present voice of Possession in Great Measure: 大有 dà yǒu — Heaven over fire — great possession. Possession in Great Measure is abundance wielded with wisdom.", + "future": "Future signal from Possession in Great Measure: next move leans assert within sovereign." + }, + "15": { + "past": "Past echo from Modesty: Mountain over earth — modesty. Modesty is quiet strength, the mountain bowing to the valley revisited.", + "present": "Present voice of Modesty: 謙 qiān — Mountain over earth — modesty. Modesty is quiet strength, the mountain bowing to the valley.", + "future": "Future signal from Modesty: next move leans yield within transformer." + }, + "16": { + "past": "Past echo from Enthusiasm: Earth over thunder — enthusiasm. Enthusiasm is movement, music, the drumbeat of collective energy revisited.", + "present": "Present voice of Enthusiasm: 豫 yù — Earth over thunder — enthusiasm. Enthusiasm is movement, music, the drumbeat of collective energy.", + "future": "Future signal from Enthusiasm: next move leans adapt within dissipator." + }, + "17": { + "past": "Past echo from Following: Lake over thunder — following. Following is adaptation, riding the wave, knowing when to lead and when to follow revisited.", + "present": "Present voice of Following: 隨 suí — Lake over thunder — following. Following is adaptation, riding the wave, knowing when to lead and when to follow.", + "future": "Future signal from Following: next move leans yield within transformer." + }, + "18": { + "past": "Past echo from Work on Decayed: Mountain over wind — decay and repair. Work on Decayed is confronting rot, clearing the old to make way revisited.", + "present": "Present voice of Work on Decayed: 蠱 gǔ — Mountain over wind — decay and repair. Work on Decayed is confronting rot, clearing the old to make way.", + "future": "Future signal from Work on Decayed: next move leans adapt within dissipator." + }, + "19": { + "past": "Past echo from Approach: Lake over earth — approach. Approach is the season turning, the leader descending to meet the people revisited.", + "present": "Present voice of Approach: 臨 lín — Lake over earth — approach. Approach is the season turning, the leader descending to meet the people.", + "future": "Future signal from Approach: next move leans yield within transformer." + }, + "20": { + "past": "Past echo from Contemplation: Earth over wind — contemplation. Contemplation is watching, waiting, seeing the pattern before acting revisited.", + "present": "Present voice of Contemplation: 觀 guān — Earth over wind — contemplation. Contemplation is watching, waiting, seeing the pattern before acting.", + "future": "Future signal from Contemplation: next move leans wait within boundary." + }, + "21": { + "past": "Past echo from Biting Through: Fire over thunder — biting through. Biting Through is decisive action, cutting through obstruction with force revisited.", + "present": "Present voice of Biting Through: 噬嗑 shì kè — Fire over thunder — biting through. Biting Through is decisive action, cutting through obstruction with force.", + "future": "Future signal from Biting Through: next move leans assert within transformer." + }, + "22": { + "past": "Past echo from Grace: Mountain over fire — grace. Grace is beauty, ornament, the form that carries the essence revisited.", + "present": "Present voice of Grace: 賁 bì — Mountain over fire — grace. Grace is beauty, ornament, the form that carries the essence.", + "future": "Future signal from Grace: next move leans wait within boundary." + }, + "23": { + "past": "Past echo from Splitting Apart: Mountain over earth — splitting apart. Splitting Apart is collapse, the structure giving way, darkness rising revisited.", + "present": "Present voice of Splitting Apart: 剝 bō — Mountain over earth — splitting apart. Splitting Apart is collapse, the structure giving way, darkness rising.", + "future": "Future signal from Splitting Apart: next move leans wait within dissipator." + }, + "24": { + "past": "Past echo from Return: Earth over thunder — return. Return is the turning point, the first light after darkness, hope renewed revisited.", + "present": "Present voice of Return: 復 fù — Earth over thunder — return. Return is the turning point, the first light after darkness, hope renewed.", + "future": "Future signal from Return: next move leans yield within transformer." + }, + "25": { + "past": "Past echo from Innocence: Heaven over thunder — innocence. Innocence is spontaneous, natural, without calculation. The uncarved block revisited.", + "present": "Present voice of Innocence: 无妄 wú wàng — Heaven over thunder — innocence. Innocence is spontaneous, natural, without calculation. The uncarved block.", + "future": "Future signal from Innocence: next move leans assert within sovereign." + }, + "26": { + "past": "Past echo from Taming Power of the Great: Mountain over heaven — taming power. Taming Power of the Great is restraint, storing energy, knowing when to hold revisited.", + "present": "Present voice of Taming Power of the Great: 大畜 dà chù — Mountain over heaven — taming power. Taming Power of the Great is restraint, storing energy, knowing when to hold.", + "future": "Future signal from Taming Power of the Great: next move leans adapt within boundary." + }, + "27": { + "past": "Past echo from Corners of the Mouth: Mountain over thunder — nourishment. Corners of the Mouth is sustenance, what feeds the self and others revisited.", + "present": "Present voice of Corners of the Mouth: 頤 yí — Mountain over thunder — nourishment. Corners of the Mouth is sustenance, what feeds the self and others.", + "future": "Future signal from Corners of the Mouth: next move leans yield within transformer." + }, + "28": { + "past": "Past echo from Preponderance of the Great: Lake over wind — preponderance. Preponderance of the Great is excess, the beam about to break, dangerous weight revisited.", + "present": "Present voice of Preponderance of the Great: 大過 dà guò — Lake over wind — preponderance. Preponderance of the Great is excess, the beam about to break, dangerous weight.", + "future": "Future signal from Preponderance of the Great: next move leans adapt within dissipator." + }, + "29": { + "past": "Past echo from The Abysmal: Water over water — the abyss. The Abysmal is danger repeated, the deep, the test of endurance revisited.", + "present": "Present voice of The Abysmal: 坎 kǎn — Water over water — the abyss. The Abysmal is danger repeated, the deep, the test of endurance.", + "future": "Future signal from The Abysmal: next move leans wait within dissipator." + }, + "30": { + "past": "Past echo from The Clinging: Fire over fire — the clinging. The Clinging is attachment, illumination, clarity through persistence revisited.", + "present": "Present voice of The Clinging: 離 lí — Fire over fire — the clinging. The Clinging is attachment, illumination, clarity through persistence.", + "future": "Future signal from The Clinging: next move leans adapt within boundary." + }, + "31": { + "past": "Past echo from Influence: Lake over mountain — influence. Influence is attraction, the subtle pull between beings, seduction without force revisited.", + "present": "Present voice of Influence: 咸 xián — Lake over mountain — influence. Influence is attraction, the subtle pull between beings, seduction without force.", + "future": "Future signal from Influence: next move leans yield within transformer." + }, + "32": { + "past": "Past echo from Duration: Thunder over wind — duration. Duration is endurance, the long rhythm, constancy through change revisited.", + "present": "Present voice of Duration: 恆 héng — Thunder over wind — duration. Duration is endurance, the long rhythm, constancy through change.", + "future": "Future signal from Duration: next move leans wait within boundary." + }, + "33": { + "past": "Past echo from Retreat: Heaven over mountain — retreat. Retreat is strategic withdrawal, knowing when to yield ground revisited.", + "present": "Present voice of Retreat: 遯 dùn — Heaven over mountain — retreat. Retreat is strategic withdrawal, knowing when to yield ground.", + "future": "Future signal from Retreat: next move leans yield within boundary." + }, + "34": { + "past": "Past echo from Power of the Great: Thunder over heaven — power. Power of the Great is force unleashed, the thunderbolt, unstoppable momentum revisited.", + "present": "Present voice of Power of the Great: 大壯 dà zhuàng — Thunder over heaven — power. Power of the Great is force unleashed, the thunderbolt, unstoppable momentum.", + "future": "Future signal from Power of the Great: next move leans assert within sovereign." + }, + "35": { + "past": "Past echo from Progress: Fire over earth — progress. Progress is the sun rising, advancement through clarity and patience revisited.", + "present": "Present voice of Progress: 晉 jìn — Fire over earth — progress. Progress is the sun rising, advancement through clarity and patience.", + "future": "Future signal from Progress: next move leans adapt within transformer." + }, + "36": { + "past": "Past echo from Darkening of the Light: Earth over fire — darkening. Darkening of the Light is persecution, the wise forced into hiding, inner light obscured revisited.", + "present": "Present voice of Darkening of the Light: 明夷 míng yí — Earth over fire — darkening. Darkening of the Light is persecution, the wise forced into hiding, inner light obscured.", + "future": "Future signal from Darkening of the Light: next move leans wait within dissipator." + }, + "37": { + "past": "Past echo from The Family: Wind over fire — the family. The Family is structure, roles, the bonds that hold through storm revisited.", + "present": "Present voice of The Family: 家人 jiā rén — Wind over fire — the family. The Family is structure, roles, the bonds that hold through storm.", + "future": "Future signal from The Family: next move leans yield within transformer." + }, + "38": { + "past": "Past echo from Opposition: Fire over lake — opposition. Opposition is estrangement, the fire and water that cannot mix, divergence revisited.", + "present": "Present voice of Opposition: 睽 kuí — Fire over lake — opposition. Opposition is estrangement, the fire and water that cannot mix, divergence.", + "future": "Future signal from Opposition: next move leans adapt within dissipator." + }, + "39": { + "past": "Past echo from Obstruction: Water over mountain — obstruction. Obstruction is the blocked path, the need to find another way, perseverance revisited.", + "present": "Present voice of Obstruction: 蹇 jiǎn — Water over mountain — obstruction. Obstruction is the blocked path, the need to find another way, perseverance.", + "future": "Future signal from Obstruction: next move leans wait within dissipator." + }, + "40": { + "past": "Past echo from Deliverance: Thunder over water — deliverance. Deliverance is release, the storm breaking, relief after tension revisited.", + "present": "Present voice of Deliverance: 解 xiè — Thunder over water — deliverance. Deliverance is release, the storm breaking, relief after tension.", + "future": "Future signal from Deliverance: next move leans adapt within transformer." + }, + "41": { + "past": "Past echo from Decrease: Mountain over lake — decrease. Decrease is loss that serves, giving up to gain, the valley that becomes deep revisited.", + "present": "Present voice of Decrease: 損 sǔn — Mountain over lake — decrease. Decrease is loss that serves, giving up to gain, the valley that becomes deep.", + "future": "Future signal from Decrease: next move leans yield within transformer." + }, + "42": { + "past": "Past echo from Increase: Wind over thunder — increase. Increase is growth, the seed becoming tree, abundance through right action revisited.", + "present": "Present voice of Increase: 益 yì — Wind over thunder — increase. Increase is growth, the seed becoming tree, abundance through right action.", + "future": "Future signal from Increase: next move leans assert within transformer." + }, + "43": { + "past": "Past echo from Breakthrough: Lake over heaven — breakthrough. Breakthrough is decisive action, the final push, resolution through clarity revisited.", + "present": "Present voice of Breakthrough: 夬 guài — Lake over heaven — breakthrough. Breakthrough is decisive action, the final push, resolution through clarity.", + "future": "Future signal from Breakthrough: next move leans assert within sovereign." + }, + "44": { + "past": "Past echo from Coming to Meet: Heaven over wind — coming to meet. Coming to Meet is the unexpected encounter, the stranger at the gate, vigilance revisited.", + "present": "Present voice of Coming to Meet: 姤 gòu — Heaven over wind — coming to meet. Coming to Meet is the unexpected encounter, the stranger at the gate, vigilance.", + "future": "Future signal from Coming to Meet: next move leans wait within boundary." + }, + "45": { + "past": "Past echo from Gathering Together: Lake over earth — gathering. Gathering Together is assembly, the king receiving his people, collective purpose revisited.", + "present": "Present voice of Gathering Together: 萃 cuì — Lake over earth — gathering. Gathering Together is assembly, the king receiving his people, collective purpose.", + "future": "Future signal from Gathering Together: next move leans yield within transformer." + }, + "46": { + "past": "Past echo from Pushing Upward: Earth over wind — pushing upward. Pushing Upward is steady ascent, the tree growing from earth to heaven revisited.", + "present": "Present voice of Pushing Upward: 升 shēng — Earth over wind — pushing upward. Pushing Upward is steady ascent, the tree growing from earth to heaven.", + "future": "Future signal from Pushing Upward: next move leans adapt within transformer." + }, + "47": { + "past": "Past echo from Oppression: Lake over water — oppression. Oppression is exhaustion, the well run dry, the spirit tested to breaking revisited.", + "present": "Present voice of Oppression: 困 kùn — Lake over water — oppression. Oppression is exhaustion, the well run dry, the spirit tested to breaking.", + "future": "Future signal from Oppression: next move leans wait within dissipator." + }, + "48": { + "past": "Past echo from The Well: Water over wind — the well. The Well is the source, community resource, the depth that never runs dry revisited.", + "present": "Present voice of The Well: 井 jǐng — Water over wind — the well. The Well is the source, community resource, the depth that never runs dry.", + "future": "Future signal from The Well: next move leans yield within boundary." + }, + "49": { + "past": "Past echo from Revolution: Lake over fire — revolution. Revolution is upheaval, the old order overthrown, transformation through fire revisited.", + "present": "Present voice of Revolution: 革 gé — Lake over fire — revolution. Revolution is upheaval, the old order overthrown, transformation through fire.", + "future": "Future signal from Revolution: next move leans assert within transformer." + }, + "50": { + "past": "Past echo from The Cauldron: Fire over wind — the cauldron. The Cauldron is transformation, cooking, the vessel that changes the nature of things revisited.", + "present": "Present voice of The Cauldron: 鼎 dǐng — Fire over wind — the cauldron. The Cauldron is transformation, cooking, the vessel that changes the nature of things.", + "future": "Future signal from The Cauldron: next move leans assert within sovereign." + }, + "51": { + "past": "Past echo from The Arousing: Thunder over thunder — the arousing. The Arousing is shock, sudden awakening, the thunder that jolts from sleep revisited.", + "present": "Present voice of The Arousing: 震 zhèn — Thunder over thunder — the arousing. The Arousing is shock, sudden awakening, the thunder that jolts from sleep.", + "future": "Future signal from The Arousing: next move leans assert within sovereign." + }, + "52": { + "past": "Past echo from Keeping Still: Mountain over mountain — keeping still. Keeping Still is meditation, the pause between beats, absolute presence revisited.", + "present": "Present voice of Keeping Still: 艮 gèn — Mountain over mountain — keeping still. Keeping Still is meditation, the pause between beats, absolute presence.", + "future": "Future signal from Keeping Still: next move leans wait within boundary." + }, + "53": { + "past": "Past echo from Development: Wind over mountain — development. Development is gradual growth, the tree on the mountain, patience rewarded revisited.", + "present": "Present voice of Development: 漸 jiàn — Wind over mountain — development. Development is gradual growth, the tree on the mountain, patience rewarded.", + "future": "Future signal from Development: next move leans adapt within transformer." + }, + "54": { + "past": "Past echo from The Marrying Maiden: Thunder over lake — marrying maiden. The Marrying Maiden is union without proper foundation, risk in relationship revisited.", + "present": "Present voice of The Marrying Maiden: 歸妹 guī mèi — Thunder over lake — marrying maiden. The Marrying Maiden is union without proper foundation, risk in relationship.", + "future": "Future signal from The Marrying Maiden: next move leans yield within dissipator." + }, + "55": { + "past": "Past echo from Abundance: Thunder over fire — abundance. Abundance is fullness, the harvest, the peak of prosperity and its attendant danger revisited.", + "present": "Present voice of Abundance: 豐 fēng — Thunder over fire — abundance. Abundance is fullness, the harvest, the peak of prosperity and its attendant danger.", + "future": "Future signal from Abundance: next move leans assert within sovereign." + }, + "56": { + "past": "Past echo from The Wanderer: Fire over mountain — the wanderer. The Wanderer is the stranger, the journey, finding home in movement revisited.", + "present": "Present voice of The Wanderer: 旅 lǚ — Fire over mountain — the wanderer. The Wanderer is the stranger, the journey, finding home in movement.", + "future": "Future signal from The Wanderer: next move leans adapt within dissipator." + }, + "57": { + "past": "Past echo from The Gentle: Wind over wind — the gentle. The Gentle is persistence, the soft that overcomes the hard, penetration without force revisited.", + "present": "Present voice of The Gentle: 巽 xùn — Wind over wind — the gentle. The Gentle is persistence, the soft that overcomes the hard, penetration without force.", + "future": "Future signal from The Gentle: next move leans yield within boundary." + }, + "58": { + "past": "Past echo from The Joyous: Lake over lake — the joyous. The Joyous is pleasure, openness, the lake that reflects heaven without distortion revisited.", + "present": "Present voice of The Joyous: 兌 duì — Lake over lake — the joyous. The Joyous is pleasure, openness, the lake that reflects heaven without distortion.", + "future": "Future signal from The Joyous: next move leans yield within transformer." + }, + "59": { + "past": "Past echo from Dispersion: Wind over water — dispersion. Dispersion is dissolution, the scattering of what was gathered, wind over water revisited.", + "present": "Present voice of Dispersion: 渙 huàn — Wind over water — dispersion. Dispersion is dissolution, the scattering of what was gathered, wind over water.", + "future": "Future signal from Dispersion: next move leans adapt within dissipator." + }, + "60": { + "past": "Past echo from Limitation: Water over lake — limitation. Limitation is discipline, the dam that channels the flood, measured restraint revisited.", + "present": "Present voice of Limitation: 節 jié — Water over lake — limitation. Limitation is discipline, the dam that channels the flood, measured restraint.", + "future": "Future signal from Limitation: next move leans wait within boundary." + }, + "61": { + "past": "Past echo from Inner Truth: Wind over lake — inner truth. Inner Truth is sincerity, the gentle wind over still water, resonance without words revisited.", + "present": "Present voice of Inner Truth: 中孚 zhōng fú — Wind over lake — inner truth. Inner Truth is sincerity, the gentle wind over still water, resonance without words.", + "future": "Future signal from Inner Truth: next move leans yield within transformer." + }, + "62": { + "past": "Past echo from Preponderance of the Small: Mountain over thunder — small preponderance. Preponderance of the Small is caution, the bird in flight, avoiding the heights revisited.", + "present": "Present voice of Preponderance of the Small: 小過 xiǎo guò — Mountain over thunder — small preponderance. Preponderance of the Small is caution, the bird in flight, avoiding the heights.", + "future": "Future signal from Preponderance of the Small: next move leans wait within dissipator." + }, + "63": { + "past": "Past echo from After Completion: Water over fire — after completion. After Completion is the achieved state, the danger of complacency after success revisited.", + "present": "Present voice of After Completion: 既濟 jì jì — Water over fire — after completion. After Completion is the achieved state, the danger of complacency after success.", + "future": "Future signal from After Completion: next move leans wait within boundary." + }, + "64": { + "past": "Past echo from Before Completion: Fire over water — before completion. Before Completion is the unfinished, the chaos before order, the moment of becoming revisited.", + "present": "Present voice of Before Completion: 未濟 wèi jì — Fire over water — before completion. Before Completion is the unfinished, the chaos before order, the moment of becoming.", + "future": "Future signal from Before Completion: next move leans adapt within transformer." + } +} diff --git a/desktop/src-tauri/src/overlay.html b/desktop/src-tauri/src/overlay.html index a812926214..3b2a2379e6 100644 --- a/desktop/src-tauri/src/overlay.html +++ b/desktop/src-tauri/src/overlay.html @@ -26,6 +26,42 @@ #messages::-webkit-scrollbar{width:6px} #messages::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.15);border-radius:3px} #messages::-webkit-scrollbar-track{background:transparent} + +/* Jiminy Cricket: King Wen conscience indicator */ +#jiminy{ + position:absolute; + top:10px; + left:12px; + width:28px; + height:28px; + border-radius:50%; + background:rgba(255,255,255,0.10); + border:1px solid rgba(255,255,255,0.25); + color:rgba(255,255,255,0.95); + display:flex; + align-items:center; + justify-content:center; + font-size:15px; + line-height:1; + box-shadow:0 0 12px rgba(255,255,255,0.15); + animation:jiminy-pulse 2.4s ease-in-out infinite; + backdrop-filter:saturate(140%) blur(6px); + -webkit-backdrop-filter:saturate(140%) blur(6px); + cursor:default; + user-select:none; +} +#jiminy .label{ + position:absolute; + left:34px; + white-space:nowrap; + font-size:11px; + opacity:0.85; + text-shadow:0 1px 2px rgba(0,0,0,0.55); +} +@keyframes jiminy-pulse{ + 0%,100%{box-shadow:0 0 10px rgba(255,255,255,0.15)} + 50%{box-shadow:0 0 18px rgba(255,255,255,0.35)} +} .msg{ padding:8px 12px;border-radius:12px; font-size:13px;line-height:1.55;max-width:90%; @@ -140,6 +176,7 @@
+
KW