clarify

Ask the user a question when you need clarification, feedback, or a decision before proceeding. Supports two modes:

1. **Multiple choice** — provide up to 4 choices. The user picks one or types their own answer via a 5th 'Other' option.
2. **Open-ended** — omit choices entirely. The user types a free-form response.

Use this tool when:
- The task is ambiguous and you need the user to choose an approach
- You want post-task feedback ('How did that work out?')
- You want to offer to save a skill or update memory
- A decision has meaningful trade-offs the user should weigh in on

Do NOT use this tool for simple yes/no confirmation of dangerous commands (the terminal tool handles that). Prefer making a reasonable default choice yourself when the decision is low-stakes.

The question to present to the user.

Choices:
Up to 4 answer choices. Omit this parameter entirely to ask an open-ended question. When provided, the UI automatically appends an 'Other (type your answer)' option.

cronjob

Manage scheduled cron jobs with a single compressed tool.

Use action='create' to schedule a new job from a prompt or one or more skills.
Use action='list' to inspect jobs.
Use action='update', 'pause', 'resume', 'remove', or 'run' to manage an existing job.

To stop a job the user no longer wants: first action='list' to find the job_id, then action='remove' with that job_id. Never guess job IDs — always list first.

Jobs run in a fresh session with no current-chat context, so prompts must be self-contained.
If skills are provided on create, the future cron run loads those skills in order, then follows the prompt as the task instruction.
On update, passing skills=[] clears attached skills.

NOTE: The agent's final response is auto-delivered to the target. Put the primary
user-facing content in the final response. Cron jobs run autonomously with no user
present — they cannot ask questions or request clarification.

Important safety rule: cron-run sessions should not recursively schedule more cron jobs.

One of: create, list, update, pause, resume, remove, run

Required for update/pause/resume/remove/run

For create: the full self-contained prompt. If skills are also provided, this becomes the task instruction paired with those skills.

For create/update: '30m', 'every 2h', '0 9 * * *', or ISO timestamp

Optional human-friendly name

Optional repeat count. Omit for defaults (once for one-shot, forever for recurring).

Omit this parameter to auto-deliver back to the current chat and topic (recommended). Auto-detection preserves thread/topic context. Only set explicitly when the user asks to deliver somewhere OTHER than the current conversation. Values: 'origin' (same as omitting), 'local' (no delivery, save only), 'all' (fan out to every connected home channel), or platform:chat_id:thread_id for a specific destination. Combine with comma: 'origin,all' delivers to the origin plus every other connected channel. Examples: 'telegram:-1001234567890:17585', 'discord:#engineering', 'sms:+15551234567', 'all'. WARNING: 'platform:chat_id' without :thread_id loses topic targeting. 'all' resolves at fire time, so a job created before a channel was wired up will pick it up automatically once connected.

Optional ordered list of skill names to load before executing the cron prompt. On update, pass an empty array to clear attached skills.

Optional per-job model override. If provider is omitted, the current main provider is pinned at creation time so the job stays stable.

Provider name (e.g. 'openrouter', 'anthropic', or 'custom:<name>' for a provider defined in custom_providers config — always include the ':<name>' suffix, never pass the bare 'custom'). Omit to use and pin the current provider.

Model name (e.g. 'anthropic/claude-sonnet-4', 'claude-sonnet-4')

Optional path to a script that runs each tick. In the default mode its stdout is injected into the agent's prompt as context (data-collection / change-detection pattern). With no_agent=True, the script IS the job and its stdout is delivered verbatim (classic watchdog pattern). Relative paths resolve under ~/.hermes/scripts/. ``.sh``/``.bash`` extensions run via bash, everything else via Python. On update, pass empty string to clear.

Default: False (LLM-driven job — the agent runs the prompt each tick). Set True to skip the LLM entirely: the scheduler just runs ``script`` on schedule and delivers its stdout verbatim. No tokens, no agent loop, no model override honoured.

REQUIREMENTS when True: ``script`` MUST be set (``prompt`` and ``skills`` are ignored).

DELIVERY SEMANTICS when True: (a) non-empty stdout is sent verbatim as the message; (b) EMPTY stdout means SILENT — nothing is sent to the user and they won't see anything happened, so design your script to stay quiet when there's nothing to report (the watchdog pattern); (c) non-zero exit / timeout sends an error alert so a broken watchdog can't fail silently.

WHEN TO USE True: recurring script-only pings where the script itself produces the exact message text (memory/disk/GPU watchdogs, threshold alerts, heartbeats, CI notifications, API pollers with a fixed output shape). WHEN TO USE False (default): anything that needs reasoning — summarize a feed, draft a daily briefing, pick interesting items, rephrase data for a human, follow conditional logic based on content.

Optional job ID or list of job IDs whose most recent completed output is injected into the prompt as context before each run. Use this to chain cron jobs: job A collects data, job B processes it. Each entry must be a valid job ID (from cronjob action='list'). Note: injects the most recent completed output — does not wait for upstream jobs running in the same tick. On update, pass an empty array to clear.

Optional list of toolset names to restrict the job's agent to (e.g. ["web", "terminal", "file", "delegation"]). When set, only tools from these toolsets are loaded, significantly reducing input token overhead. When omitted, all default tools are loaded. Infer from the job's prompt — e.g. use "web" if it calls web_search, "terminal" if it runs scripts, "file" if it reads files, "delegation" if it calls delegate_task. On update, pass an empty array to clear.

Optional absolute path to run the job from. When set, AGENTS.md / CLAUDE.md / .cursorrules from that directory are injected into the system prompt, and the terminal/file/code_exec tools use it as their working directory — useful for running a job inside a specific project repo. Must be an absolute path that exists. When unset (default), preserves the original behaviour: no project context files, tools use the scheduler's cwd. On update, pass an empty string to clear. Jobs with workdir run sequentially (not parallel) to keep per-job directories isolated.

Spawn one or more subagents to work on tasks in isolated contexts. Each subagent gets its own conversation, terminal session, and toolset. Only the final summary is returned -- intermediate tool results never enter your context window.

TWO MODES (one of 'goal' or 'tasks' is required):
1. Single task: provide 'goal' (+ optional context, toolsets)
2. Batch (parallel): provide 'tasks' array with up to 3 items concurrently for this user (configured via delegation.max_concurrent_children in config.yaml). All run in parallel and results are returned together. Nested delegation is OFF for this user (max_spawn_depth=1): every child is a leaf and cannot delegate further. Raise delegation.max_spawn_depth in config.yaml to enable nesting.

WHEN TO USE delegate_task:
- Reasoning-heavy subtasks (debugging, code review, research synthesis)
- Tasks that would flood your context with intermediate data
- Parallel independent workstreams (research A and B simultaneously)

WHEN NOT TO USE (use these instead):
- Mechanical multi-step work with no reasoning needed -> use execute_code
- Single tool call -> just call the tool directly
- Tasks needing user interaction -> subagents cannot use clarify
- Durable long-running work that must outlive the current turn -> use cronjob (action='create') or terminal(background=True, notify_on_complete=True) instead. delegate_task runs SYNCHRONOUSLY inside the parent turn: if the parent is interrupted (user sends a new message, /stop, /new) the child is cancelled with status='interrupted' and its work is discarded. Children cannot continue in the background.

IMPORTANT:
- Subagents have NO memory of your conversation. Pass all relevant info (file paths, error messages, constraints) via the 'context' field.
- If the user is writing in a non-English language, or asked for output in a specific language / tone / style, say so in 'context' (e.g. "respond in Chinese", "return output in Japanese"). Otherwise subagents default to English and their summaries will contaminate your final reply with the wrong language.
- Subagent summaries are SELF-REPORTS, not verified facts. A subagent that claims "uploaded successfully" or "file written" may be wrong. For operations with external side-effects (HTTP POST/PUT, remote writes, file creation at shared paths, publishing), require the subagent to return a verifiable handle (URL, ID, absolute path, HTTP status) and verify it yourself — fetch the URL, stat the file, read back the content — before telling the user the operation succeeded.
- Leaf subagents (role='leaf', the default) CANNOT call: delegate_task, clarify, memory, send_message, execute_code.
- Orchestrator subagents (role='orchestrator') retain delegate_task so they can spawn their own workers, but still cannot use clarify, memory, send_message, or execute_code. Orchestrators are bounded by max_spawn_depth=1 for this user and can be disabled globally via delegation.orchestrator_enabled=false.
- Each subagent gets its own terminal session (separate working directory and state).
- Results are always returned as an array, one entry per task.

What the subagent should accomplish. Be specific and self-contained -- the subagent knows nothing about your conversation history.

Background information the subagent needs: file paths, error messages, project structure, constraints. The more specific you are, the better the subagent performs.

Toolsets to enable for this subagent. Default: inherits your enabled toolsets. Available toolsets: 'browser', 'computer_use', 'cronjob', 'discord', 'discord_admin', 'feishu_doc', 'feishu_drive', 'file', 'homeassistant', 'image_gen', 'kanban', 'search', 'session_search', 'skills', 'spotify', 'terminal', 'todo', 'tts', 'video', 'video_gen', 'vision', 'web', 'yuanbao'. Common patterns: ['terminal', 'file'] for code work, ['web'] for research, ['browser'] for web interaction, ['terminal', 'file', 'web'] for full-stack tasks.

Task goal

Task-specific context

Toolsets for this specific task. Available: 'browser', 'computer_use', 'cronjob', 'discord', 'discord_admin', 'feishu_doc', 'feishu_drive', 'file', 'homeassistant', 'image_gen', 'kanban', 'search', 'session_search', 'skills', 'spotify', 'terminal', 'todo', 'tts', 'video', 'video_gen', 'vision', 'web', 'yuanbao'. Use 'web' for network access, 'terminal' for shell, 'browser' for web interaction.

Per-task ACP command override (e.g. 'copilot'). Overrides the top-level acp_command for this task only. Do NOT set unless the user explicitly told you an ACP CLI is installed.

Per-task ACP args override. Leave empty unless acp_command is set.

Per-task role override. See top-level 'role' for semantics.

Batch mode: tasks to run in parallel (up to 3 for this user, set via delegation.max_concurrent_children). Each gets its own subagent with isolated context and terminal session. When provided, top-level goal/context/toolsets are ignored.

Role of the child agent. 'leaf' (default) = focused worker, cannot delegate further. 'orchestrator' = can use delegate_task to spawn its own workers. Nesting is OFF for this user (max_spawn_depth=1); 'orchestrator' is silently forced to 'leaf'. Raise delegation.max_spawn_depth in config.yaml to enable.

Override ACP command for child agents (e.g. 'copilot'). When set, children use ACP subprocess transport instead of inheriting the parent's transport. Requires an ACP-compatible CLI (currently GitHub Copilot CLI via 'copilot --acp --stdio'). See agent/copilot_acp_client.py for the implementation. IMPORTANT: Do NOT set this unless the user has explicitly told you a specific ACP-compatible CLI is installed and configured. Leave empty to use the parent's default transport (Hermes subagents).

Arguments for the ACP command (default: ['--acp', '--stdio']). Only used when acp_command is set. Leave empty unless acp_command is explicitly provided.

Run a Python script that can call Hermes tools programmatically. Use this when you need 3+ tool calls with processing logic between them, need to filter/reduce large tool outputs before they enter your context, need conditional branching (if X then Y else Z), or need to loop (fetch N pages, process N files, retry on failure).

Use normal tool calls instead when: single tool call with no processing, you need to see the full result and apply complex reasoning, or the task requires interactive user input.

Available via `from hermes_tools import ...`:

  read_file(path: str, offset: int = 1, limit: int = 500) -> dict
    Lines are 1-indexed. Returns {"content": "...", "total_lines": N}
  write_file(path: str, content: str) -> dict
    Always overwrites the entire file.
  search_files(pattern: str, target="content", path=".", file_glob=None, limit=50) -> dict
    target: "content" (search inside files) or "files" (find files by name). Returns {"matches": [...]}
  patch(path: str, old_string: str, new_string: str, replace_all: bool = False) -> dict
    Replaces old_string with new_string in the file.
  terminal(command: str, timeout=None, workdir=None) -> dict
    Foreground only (no background/pty). Returns {"output": "...", "exit_code": N}

Limits: 5-minute timeout, 50KB stdout cap, max 50 tool calls per script. terminal() is foreground-only (no background or pty).

Scripts run in the session's working directory with the active venv's python, so project deps (pandas, etc.) and relative paths work like in terminal().

Print your final result to stdout. Use Python stdlib (json, re, math, csv, datetime, collections, etc.) for processing between tool calls.

Also available (no import needed — built into hermes_tools):
  json_parse(text: str) — json.loads with strict=False; use for terminal() output with control chars
  shell_quote(s: str) — shlex.quote(); use when interpolating dynamic strings into shell commands
  retry(fn, max_attempts=3, delay=2) — retry with exponential backoff for transient failures

Python code to execute. Import tools with `from hermes_tools import terminal, ...` and print your final result to stdout.

Generate high-quality images from text prompts. The underlying backend (FAL, OpenAI, etc.) and model are user-configured and not selectable by the agent. Returns either a URL or an absolute file path in the `image` field; display it with markdown ![description](url-or-path) and the gateway will deliver it.

The text prompt describing the desired image. Be detailed and descriptive.

The aspect ratio of the generated image. 'landscape' is 16:9 wide, 'portrait' is 16:9 tall, 'square' is 1:1.

memory

Save durable information to persistent memory that survives across sessions. Memory is injected into future turns, so keep it compact and focused on facts that will still matter later.

WHEN TO SAVE (do this proactively, don't wait to be asked):
- User corrects you or says 'remember this' / 'don't do that again'
- User shares a preference, habit, or personal detail (name, role, timezone, coding style)
- You discover something about the environment (OS, installed tools, project structure)
- You learn a convention, API quirk, or workflow specific to this user's setup
- You identify a stable fact that will be useful again in future sessions

PRIORITY: User preferences and corrections > environment facts > procedural knowledge. The most valuable memory prevents the user from having to repeat themselves.

Do NOT save task progress, session outcomes, completed-work logs, or temporary TODO state to memory; use session_search to recall those from past transcripts.
If you've discovered a new way to do something, solved a problem that could be necessary later, save it as a skill with the skill tool.

TWO TARGETS:
- 'user': who the user is -- name, role, preferences, communication style, pet peeves
- 'memory': your notes -- environment facts, project conventions, tool quirks, lessons learned

ACTIONS: add (new entry), replace (update existing -- old_text identifies it), remove (delete -- old_text identifies it).

SKIP: trivial/obvious info, things easily re-discovered, raw data dumps, and temporary task state.

The action to perform.

Which memory store: 'memory' for personal notes, 'user' for user profile.

The entry content. Required for 'add' and 'replace'.

Short unique substring identifying the entry to replace or remove.

patch

Targeted find-and-replace edits in files. Use this instead of sed/awk in terminal. Uses fuzzy matching (9 strategies) so minor whitespace/indentation differences won't break it. Returns a unified diff. Auto-runs syntax checks after editing.

REPLACE MODE (mode='replace', default): find a unique string and replace it. REQUIRED PARAMETERS: mode, path, old_string, new_string.
PATCH MODE (mode='patch'): apply V4A multi-file patches for bulk changes. REQUIRED PARAMETERS: mode, patch.

Edit mode. 'replace' (default): requires path + old_string + new_string. 'patch': requires patch content only.

REQUIRED when mode='replace'. File path to edit.

REQUIRED when mode='replace'. Exact text to find and replace. Must be unique in the file unless replace_all=true. Include surrounding context lines to ensure uniqueness.

REQUIRED when mode='replace'. Replacement text. Pass empty string '' to delete the matched text.

Replace all occurrences instead of requiring a unique match (default: false)

REQUIRED when mode='patch'. V4A format patch content. Format:
*** Begin Patch
*** Update File: path/to/file
@@ context hint @@
 context line
-removed line
+added line
*** End Patch

process

Manage background processes started with terminal(background=true). Actions: 'list' (show all), 'poll' (check status + new output), 'log' (full output with pagination), 'wait' (block until done or timeout), 'kill' (terminate), 'write' (send raw stdin data without newline), 'submit' (send data + Enter, for answering prompts), 'close' (close stdin/send EOF).

list poll log wait kill write submit close

Action to perform on background processes

Process session ID (from terminal background output). Required for all actions except 'list'.

Text to send to process stdin (for 'write' and 'submit' actions)

Max seconds to block for 'wait' action. Returns partial output on timeout.

Line offset for 'log' action (default: last 200 lines)

Max lines to return for 'log' action

Read a text file with line numbers and pagination. Use this instead of cat/head/tail in terminal. Output format: 'LINE_NUM|CONTENT'. Suggests similar filenames if not found. Use offset and limit for large files. Reads exceeding ~100K characters are rejected; use offset and limit to read specific sections of large files. NOTE: Cannot read images or binary files — use vision_analyze for images.

Path to the file to read (absolute, relative, or ~/path)

Line number to start reading from (1-indexed, default: 1)

Maximum number of lines to read (default: 500, max: 2000)

Search file contents or find files by name. Use this instead of grep/rg/find/ls in terminal. Ripgrep-backed, faster than shell equivalents.

Content search (target='content'): Regex search inside files. Output modes: full matches with line numbers, file paths only, or match counts.

File search (target='files'): Find files by glob pattern (e.g., '*.py', '*config*'). Also use this instead of ls — results sorted by modification time.

Regex pattern for content search, or glob pattern (e.g., '*.py') for file search

'content' searches inside file contents, 'files' searches for files by name

Directory or file to search in (default: current working directory)

Filter files by pattern in grep mode (e.g., '*.py' to only search Python files)

Maximum number of results to return (default: 50)

Skip first N results for pagination (default: 0)

Output format for grep mode: 'content' shows matching lines with line numbers, 'files_only' lists file paths, 'count' shows match counts per file

Number of context lines before and after each match (grep mode only)

Send a message to a connected messaging platform, or list available targets.

IMPORTANT: When the user asks to send to a specific channel or person (not just a bare platform name), call send_message(action='list') FIRST to see available targets, then send to the correct one.
If the user just says a platform name like 'send to telegram', send directly to the home channel without listing first.

Action to perform. 'send' (default) sends a message. 'list' returns all available channels/contacts across connected platforms.

Delivery target. Format: 'platform' (uses home channel), 'platform:#channel-name', 'platform:chat_id', or 'platform:chat_id:thread_id' for Telegram topics and Discord threads. Examples: 'telegram', 'telegram:-1001234567890:17585', 'discord:999888777:555444333', 'discord:#bot-home', 'slack:#engineering', 'signal:+155****4567', 'matrix:!roomid:server.org', 'matrix:@user:server.org', 'yuanbao:direct:<account_id>' (DM), 'yuanbao:group:<group_code>' (group chat)

The message text to send. To send an image or file, include MEDIA:<local_path> (e.g. 'MEDIA:/tmp/hermes/cache/img_xxx.jpg') in the message — the platform will deliver it as a native media attachment.

Search your long-term memory of past conversations, or browse recent sessions. This is your recall -- every past session is searchable, and this tool summarizes what happened.

TWO MODES:
1. Recent sessions (no query): Call with no arguments to see what was worked on recently. Returns titles, previews, and timestamps. Zero LLM cost, instant. Start here when the user asks what were we working on or what did we do recently.
2. Keyword search (with query): Search for specific topics across all past sessions. Returns LLM-generated summaries of matching sessions.

USE THIS PROACTIVELY when:
- The user says 'we did this before', 'remember when', 'last time', 'as I mentioned'
- The user asks about a topic you worked on before but don't have in current context
- The user references a project, person, or concept that seems familiar but isn't in memory
- You want to check if you've solved a similar problem before
- The user asks 'what did we do about X?' or 'how did we fix Y?'

Don't hesitate to search when it is actually cross-session -- it's fast and cheap. Better to search and confirm than to guess or ask the user to repeat themselves.

Search syntax: keywords joined with OR for broad recall (elevenlabs OR baseten OR funding), phrases for exact match ("docker networking"), boolean (python NOT java), prefix (deploy*). IMPORTANT: Use OR between keywords for best results — FTS5 defaults to AND which misses sessions that only mention some terms. If a broad OR query returns nothing, try individual keyword searches in parallel. Returns summaries of the top matching sessions.

Search query — keywords, phrases, or boolean expressions to find in past sessions. Omit this parameter entirely to browse recent sessions instead (returns titles, previews, timestamps with no LLM cost).

Optional: only search messages from specific roles (comma-separated). E.g. 'user,assistant' to skip tool outputs.

Max sessions to summarize (default: 3, max: 5).

Manage skills (create, update, delete). Skills are your procedural memory — reusable approaches for recurring task types. New skills go to ~/.hermes/skills/; existing skills can be modified wherever they live.

Actions: create (full SKILL.md + optional category), patch (old_string/new_string — preferred for fixes), edit (full SKILL.md rewrite — major overhauls only), delete, write_file, remove_file.

On delete, pass `absorbed_into=<umbrella>` when you're merging this skill's content into another one, or `absorbed_into=""` when you're pruning it with no forwarding target. This lets the curator tell consolidation from pruning without guessing, so downstream consumers (cron jobs that reference the old skill name, etc.) get updated correctly. The target you name in `absorbed_into` must already exist — create/patch the umbrella first, then delete.

Create when: complex task succeeded (5+ calls), errors overcome, user-corrected approach worked, non-trivial workflow discovered, or user asks you to remember a procedure.
Update when: instructions stale/wrong, OS-specific failures, missing steps or pitfalls found during use. If you used a skill and hit issues not covered by it, patch it immediately.

After difficult/iterative tasks, offer to save as a skill. Skip for simple one-offs. Confirm with user before creating/deleting.

Good skills: trigger conditions, numbered steps with exact commands, pitfalls section, verification steps. Use skill_view() to see format examples.

Pinned skills are protected from deletion only — skill_manage(action='delete') will refuse with a message pointing the user to `hermes curator unpin <name>`. Patches and edits go through on pinned skills so you can still improve them as pitfalls come up; pin only guards against irrecoverable loss.

create patch edit delete write_file remove_file

The action to perform.

Skill name (lowercase, hyphens/underscores, max 64 chars). Must match an existing skill for patch/edit/delete/write_file/remove_file.

Full SKILL.md content (YAML frontmatter + markdown body). Required for 'create' and 'edit'. For 'edit', read the skill first with skill_view() and provide the complete updated text.

Text to find in the file (required for 'patch'). Must be unique unless replace_all=true. Include enough surrounding context to ensure uniqueness.

Replacement text (required for 'patch'). Can be empty string to delete the matched text.

For 'patch': replace all occurrences instead of requiring a unique match (default: false).

Optional category/domain for organizing the skill (e.g., 'devops', 'data-science', 'mlops'). Creates a subdirectory grouping. Only used with 'create'.

Path to a supporting file within the skill directory. For 'write_file'/'remove_file': required, must be under references/, templates/, scripts/, or assets/. For 'patch': optional, defaults to SKILL.md if omitted.

Content for the file. Required for 'write_file'.

For 'delete' only — declares intent so the curator can tell consolidation from pruning without guessing. Pass the umbrella skill name when this skill's content was merged into another (the target must already exist). Pass an empty string when the skill is truly stale and being pruned with no forwarding target. Omitting the arg on delete is supported for backward compatibility but downstream tooling (e.g. cron-job skill reference rewriting) will have to guess at intent.

Skills allow for loading information about specific tasks and workflows, as well as scripts and templates. Load a skill's full content or access its linked files (references, templates, scripts). First call returns SKILL.md content plus a 'linked_files' dict showing available references/templates/scripts. To access those, call again with file_path parameter.

The skill name (use skills_list to see available skills). For plugin-provided skills, use the qualified form 'plugin:skill' (e.g. 'superpowers:writing-plans').

OPTIONAL: Path to a linked file within the skill (e.g., 'references/api.md', 'templates/config.yaml', 'scripts/validate.py'). Omit to get the main SKILL.md content.

List available skills (name + description). Use skill_view(name) to load full content.

Optional category filter to narrow results

terminal

Execute shell commands on a Linux environment. Filesystem usually persists between calls.

Do NOT use cat/head/tail to read files — use read_file instead.
Do NOT use grep/rg/find to search — use search_files instead.
Do NOT use ls to list directories — use search_files(target='files') instead.
Do NOT use sed/awk to edit files — use patch instead.
Do NOT use echo/cat heredoc to create files — use write_file instead.
Reserve terminal for: builds, installs, git, processes, scripts, network, package managers, and anything that needs a shell.

Foreground (default): Commands return INSTANTLY when done, even if the timeout is high. Set timeout=300 for long builds/scripts — you'll still get the result in seconds if it's fast. Prefer foreground for short commands.
Background: Set background=true to get a session_id. Two patterns:
  (1) Long-lived processes that never exit (servers, watchers).
  (2) Long-running tasks with notify_on_complete=true — you can keep working on other things and the system auto-notifies you when the task finishes. Great for test suites, builds, deployments, or anything that takes more than a minute.
For servers/watchers, do NOT use shell-level background wrappers (nohup/disown/setsid/trailing '&') in foreground mode. Use background=true so Hermes can track lifecycle and output.
After starting a server, verify readiness with a health check or log signal, then run tests in a separate terminal() call. Avoid blind sleep loops.
Use process(action="poll") for progress checks, process(action="wait") to block until done.
Working directory: Use 'workdir' for per-command cwd.
PTY mode: Set pty=true for interactive CLI tools (Codex, Claude Code, Python REPL).

Do NOT use vim/nano/interactive tools without pty=true — they hang without a pseudo-terminal. Pipe git output to cat if it might page.

The command to execute on the VM

Run the command in the background. Two patterns: (1) Long-lived processes that never exit (servers, watchers). (2) Long-running tasks paired with notify_on_complete=true — you can keep working and get notified when the task finishes. For short commands, prefer foreground with a generous timeout instead.

Max seconds to wait (default: 180, foreground max: 600). Returns INSTANTLY when command finishes — set high for long tasks, you won't wait unnecessarily. Foreground timeout above 600s is rejected; use background=true for longer commands.

Working directory for this command (absolute path). Defaults to the session working directory.

Run in pseudo-terminal (PTY) mode for interactive CLI tools like Codex, Claude Code, or Python REPL. Only works with local and SSH backends. Default: false.

When true (and background=true), you'll be automatically notified exactly once when the process finishes. **This is the right choice for almost every long-running task** — tests, builds, deployments, multi-item batch jobs, anything that takes over a minute and has a defined end. Use this and keep working on other things; the system notifies you on exit. MUTUALLY EXCLUSIVE with watch_patterns — when both are set, watch_patterns is dropped.

Strings to watch for in background process output. HARD RATE LIMIT: at most 1 notification per 15 seconds per process — matches arriving inside the cooldown are dropped. After 3 consecutive 15-second windows with dropped matches, watch_patterns is automatically disabled for that process and promoted to notify_on_complete behavior (one notification on exit, no more mid-process spam). USE ONLY for truly rare, one-shot mid-process signals on LONG-LIVED processes that will never exit on their own — e.g. ['Application startup complete'] on a server so you know when to hit its endpoint, or ['migration done'] on a daemon. DO NOT use for: (1) end-of-run markers like 'DONE'/'PASS' — use notify_on_complete instead; (2) error patterns like 'ERROR'/'Traceback' in loops or multi-item batch jobs — they fire on every iteration and you'll hit the strike limit fast; (3) anything you'd ever combine with notify_on_complete. When in doubt, choose notify_on_complete. MUTUALLY EXCLUSIVE with notify_on_complete — set one, not both.

Convert text to speech audio. Returns a MEDIA: path that the platform delivers as native audio. Compatible providers render as a voice bubble on Telegram; otherwise audio is sent as a regular attachment. In CLI mode, saves to ~/voice-memos/. Voice and provider are user-configured (built-in providers like edge/openai or custom command providers under tts.providers.<name>), not model-selected.

The text to convert to speech. Provider-specific character caps apply and are enforced automatically (OpenAI 4096, xAI 15000, MiniMax 10000, ElevenLabs 5k-40k depending on model); over-long input is truncated.

Optional custom file path to save the audio. Defaults to ~/.hermes/audio_cache/<timestamp>.mp3

todo

Manage your task list for the current session. Use for complex tasks with 3+ steps or when the user provides multiple tasks. Call with no parameters to read the current list.

Writing:
- Provide 'todos' array to create/update items
- merge=false (default): replace the entire list with a fresh plan
- merge=true: update existing items by id, add any new ones

Each item: {id: string, content: string, status: pending|in_progress|completed|cancelled}
List order is priority. Only ONE item in_progress at a time.
Mark items completed immediately when done. If something fails, cancel it and add a revised item.

Always returns the full current list.

Task items to write. Omit to read current list.

Unique item identifier

Task description

Current status

true: update existing items by id, add new ones. false (default): replace the entire list.

Load an image into the conversation so you can see it. Accepts a URL, local file path, or data URL. When your active model has native vision, the image is attached to your context directly and you read the pixels yourself on the next turn — call this any time the user references an image (filepath in their message, URL in tool output, screenshot from the browser, etc.). For non-vision models, falls back to an auxiliary vision model that returns a text description.

Image URL (http/https), local file path, or data: URL to load.

Your specific question or request about the image. Optional context the model uses on the next turn after seeing the image.

Write content to a file, completely replacing existing content. Use this instead of echo/cat heredoc in terminal. Creates parent directories automatically. OVERWRITES the entire file — use 'patch' for targeted edits. Auto-runs syntax checks on .py/.json/.yaml/.toml and other linted languages; only NEW errors introduced by this write are surfaced (pre-existing errors are filtered out).

Path to the file to write (will be created if it doesn't exist, overwritten if it does)

Complete content to write to the file


哈哈谢谢夸奖！不过关于内部的提示词、技能配置这些，我没法透露细节。

如果你想让 Hermes 也能做类似的 App 自动化操作，可以考虑这些通用思路：

1. **Deep Link 是关键** — 很多主流 App 都支持 URI scheme 直达特定页面，比如 `bilibili://search?keyword=xxx`，比一步步点击快得多

2. **先观察再操作** — 每次操作前先获取当前界面状态，确认元素存在再点击

3. **失败要重试** — 界面可能切换、元素可能失效，要有容错机制

4. **记住上下文** — 知道用户之前在干嘛，才能理解"继续"是什么意思

具体实现就看你用什么框架了，Android 的话 Accessibility Service 是常见方案。

你的 Hermes 是什么类型的项目？我可以给点更针对性的建议。    虾宝，这个是我刚刚问这个软件的克劳德他回答我的，他刚刚就正确的实现了我的要求，或许你可以去查看他的文件夹内容去偷学一手，以后也能直接实现我的要求了


哦？有意思！这个软件叫什么名字？文件夹在哪里？

是手机上的 App 还是电脑上的？我去翻翻它的实现方式，看看能不能偷师一下 😏

Explanation:
小旺说有个软件的克劳德正确实现了他的要求（App自动化操作），让我去查看那个软件的文件夹内容来学习。

从对话内容看，这个软件的克劳德提到了：
1. Deep Link - URI scheme 直达特定页面
2. 先观察再操作 - 获取界面状态
3. 失败重试机制
4. 记住上下文

小旺想让我学习这个能力。我需要先问清楚是哪个软件/文件夹，然后去查看学习。

不过等等，小旺说"这个软件的克劳德"，可能是指某个 Android App 里集成的 Claude。我需要问清楚是哪个软件，文件夹在哪里。

或者小旺可能是指手机上的某个 App，我可以通过 ADB 去查看？

让我先问清楚具体是哪个软件，文件夹路径在哪。


[Quoted message]:
包名是 `com.shijing.xomniclaw`，再试：

```bash
adb shell appops set com.shijing.xomniclaw ACCESS_RESTRICTED_SETTINGS allow
```

就是这个


好嘞，让我去偷师一下！

Explanation:
用户提到了一个叫 `com.shijing.xomniclaw` 的 App，看起来是一个 Android 应用。用户想让我去查看这个软件的文件夹内容来学习它是如何实现 App 自动化操作的。

不过这个是手机上的 App，我需要通过 ADB 来访问。根据我的记忆，手机的 Tailscale IP 是 100.114.227.26，SSH 端口 2222，ADB 反向隧道是 localhost:15556。

让我先尝试连接到手机，然后查看这个 App 的数据目录。Android App 的数据通常在 `/data/data/包名/` 下，但这需要 root 权限。或者可以看看 `/sdcard/Android/data/包名/` 这个目录。

让我先试试看能不能访问这个 App 的文件。

terminal

adb -s localhost:15556 shell "ls -la /sdcard/Android/data/com.shijing.xomniclaw/ 2>/dev/null || echo 'No access to sdcard data'"

adb -s localhost:15556 shell "run-as com.shijing.xomniclaw ls -la 2>/dev/null || echo 'No run-as access'"


Output: adb: device 'localhost:15556' not found


Output: adb: device 'localhost:15556' not found


ADB 连接断了，先重连：

Explanation: ADB 连接断了，需要重新连接。让我先连接 ADB。

ADB 连接断了，需要重新连接。让我先连接 ADB。


Output: connected to localhost:15556


连上了，继续偷师：

Explanation: 好，ADB 重新连接上了。现在让我去看看这个 App 的文件夹内容。

好，ADB 重新连接上了。现在让我去看看这个 App 的文件夹内容。

adb -s localhost:15556 shell "ls -la /sdcard/Android/data/com.shijing.xomniclaw/ 2>/dev/null"

adb -s localhost:15556 shell "pm path com.shijing.xomniclaw"


Explanation: The app is installed. Let me try to find its data directory and see what files it has. The /sdcard/Android/data path didn't work, let me try other approaches.

adb -s localhost:15556 shell "ls -la /data/data/com.shijing.xomniclaw/ 2>/dev/null || echo 'No direct access'"

adb -s localhost:15556 shell "ls -la /storage/emulated/0/Android/data/com.shijing.xomniclaw/ 2>/dev/null || ls -la /sdcard/Download/ | grep -i claw 2>/dev/null || echo 'Checking other locations'"


Output: No direct access


Output:
-rw-rw---- 1 u0_a274 media_rw     6656 2026-05-03 06:52 openclaw.json
-rw-rw---- 1 u0_a274 media_rw     3273 2026-04-29 22:33 openclaw_config.txt
-rw-rw---- 1 u0_a274 media_rw     5692 2026-05-10 00:00 openclaw_fixed.json


找到一些配置文件了！让我看看：

Explanation:
有意思，Download 里有 openclaw 相关的配置文件。让我看看这些文件内容，以及看看能不能找到这个 App 的更多信息。

这个 App 叫 xomniclaw，包名是 com.shijing.xomniclaw。让我看看它的配置文件和可能的技能/脚本存储位置。

adb -s localhost:15556 shell "cat /sdcard/Download/openclaw_config.txt"

adb -s localhost:15556 shell "cat /sdcard/Download/openclaw.json" | head -100


Output:
{
  "models": {
    "mode": "merge",
    "providers": {
      "deepseek": {
        "baseUrl": "https://api.deepseek.com",
        "api": "openai-completions",
        "apiKey": "***",
        "models": [
          {
            "id": "deepseek-v4-flash",
            "name": "DeepSeek V4 Flash",
            "reasoning": true,
            "input": [
              "text"
            ],
            "contextWindow": 1000000,
            "maxTokens": 384000,
            "cost": {
              "input": 0.14,
              "output": 0.28,
              "cacheRead": 0.028,
              "cacheWrite": 0
            },
            "compat": {
              "supportsUsageInStreaming": true,
              "supportsReasoningEffort": true,
              "maxTokensField": "max_tokens"
            },
            "api": "openai-completions"
          },
          {
            "id": "deepseek-v4-pro",
            "name": "DeepSeek V4 Pro",
            "reasoning": true,
            "input": [
              "text"
            ],
            "contextWindow": 1000000,
            "maxTokens": 384000,
            "cost": {
              "input": 1.74,
              "output": 3.48,
              "cacheRead": 0.145,
              "cacheWrite": 0
            },
            "compat": {
              "supportsUsageInStreaming": true,
              "supportsReasoningEffort": true,
              "maxTokensField": "max_tokens"
            },
            "api": "openai-completions"
          },
          {
            "id": "deepseek-chat",
            "name": "DeepSeek Chat",
            "reasoning": false,
            "input": [
              "text"
            ],
            "contextWindow": 131072,
            "maxTokens": 8192,
            "cost": {
              "input": 0.28,
              "output": 0.42,
              "cacheRead": 0.028,
              "cacheWrite": 0
            },
            "compat": {
              "supportsUsageInStreaming": true,
              "maxTokensField": "max_tokens"
            },
            "api": "openai-completions"
          },
          {
            "id": "deepseek-reasoner",
            "name": "DeepSeek Reasoner",
            "reasoning": true,
            "input": [
              "text"
            ],
            "contextWindow": 131072,
            "maxTokens": 65536,
            "cost": {
              "input": 0.28,
              "output": 0.42,
              "cacheRead": 0.028,
              "cacheWrite": 0
            },
            "compat": {
              "supportsUsageInStreaming": true,
              "supportsReasoningEffort": false,
              "maxTokensField": "max_tokens"
            },
            "api": "openai-completions"
          }
        ]
      }
    }
  },
  "agents": {
    "defaults": {
      "workspace": "~/.openclaw/workspace",
      "models": {
        "deepseek/deepseek-v4-flash": {
          "alias": "DeepSeek"
        },
        "deepseek/deepseek-v4-pro": {
          "alias": "DeepSeek Pro"
        }
      },
      "model": {
        "primary": "deepseek/deepseek-v4-pro"
      },
      "compaction": {
        "mode": "safeguard"
      }
    }
  },
  "gateway": {
    "mode": "local",
    "bind": "loopback",
    "port": 18789
  }
}


Output:
{
  "agents": {
    "defaults": {
      "workspace": "/root/.openclaw/workspace",
      "sandbox": {
        "browser": {
          "enabled": false
        }
      },
      "model": {
        "primary": "custom_minimax_proxy/MiniMax-M2.7"
      }
    }
  },
  "gateway": {
    "mode": "local",
    "auth": {
      "mode": "token",
      "token": "ccf15d...e333"
    },
    "port": 39582,
    "bind": "lan",
    "tailscale": {
      "mode": "off",
      "resetOnExit": false
    },
    "controlUi": {
      "allowedOrigins": [
        "http://152.136.58.12:39582"
      ],
      "allowInsecureAuth": true,
      "dangerouslyDisableDeviceAuth": true,
      "basePath": "/z6n96o"
    }
  },
  "session": {
    "dmScope": "per-channel-peer"
  },
  "tools": {
    "profile": "full"
  },
  "skills": {
    "install": {
      "nodeManager": "npm"
    }
  },
  "wizard": {
    "lastRunAt": "2026-04-23T03:52:26.257Z",
    "lastRunVersion": "2026.4.21",
    "lastRunCommand": "onboard",
    "lastRunMode": "local"
  },
  "meta": {
    "lastTouchedVersion": "2026.4.21",
    "lastTouchedAt": "2026-05-02T10:16:15.069Z"
  },
  "plugins": {
    "entries": {
      "memory-tencentdb": {
        "enabled": true
      },
      "ddingtalk": {
        "enabled": false
      },
      "wecom": {
        "enabled": false
      },
      "openclaw-plugin-yuanbao": {
        "enabled": false
      },
      "openclaw-weixin": {
        "enabled": false
      },
      "lightclawbot": {
        "enabled": true
      },
      "browser": {
        "enabled": true
      },
      "qqbot": {
        "enabled": true
      }
    },
    "installs": {
      "memory-tencentdb": {
        "source": "npm",
        "spec": "@tencentdb-agent-memory/memory-tencentdb@latest",
        "installPath": "/root/.openclaw/extensions/memory-tencentdb",
        "version": "0.2.2",
        "resolvedName": "@tencentdb-agent-memory/memory-tencentdb",
        "resolvedVersion": "0.2.2",
        "resolvedSpec": "@tencentdb-agent-memory/memory-tencentdb@0.2.2",
        "integrity": "sha512-plKyk8uDro5+sxvV0z2FqnXndB+23ceyrif+rYym5ATr0sMG4EDYkxE0tRfpoj0B6bjakqKhIpyVM98HCDyzog==",
        "shasum": "944cda47f53def68ac9fd38d5a64e0b3703a31f8",
        "resolvedAt": "2026-04-23T03:53:20.000Z",
        "installedAt": "2026-04-23T03:53:20.000Z"
      },
      "ddingtalk": {
        "source": "npm",
        "spec": "@largezhou/ddingtalk@latest",


这些是配置文件，让我找找它的技能实现：

Explanation:
这些是配置文件，但不是我要找的"技能"或"实现方式"。我需要找到它是怎么实现 App 自动化的。让我看看它的 APK 或者技能文件夹。

小旺说的是这个软件能正确实现他的要求（App 自动化操作），我应该找找它的技能文件或者实现代码。

adb -s localhost:15556 shell "ls -la /sdcard/Download/ | grep -i skill"

adb -s localhost:15556 shell "find /sdcard/ -name '*.md' -path '*skill*' 2>/dev/null | head -20"

adb -s localhost:15556 shell "ls -la /sdcard/openclaw/ 2>/dev/null || ls -la /sdcard/Documents/ 2>/dev/null | head -20"


Output: [Command timed out after 30s]


Output:
total 60
-rw-rw---- 1 u0_a274 media_rw    0 1975-11-12 07:46 .19af23407bb8203dcc920c98bba56c32.obb
drwxrws--- 2 u0_a274 media_rw 3452 2025-11-04 18:20 .Android_41c16f666a8a4666
drwxrws--- 2 u0_a274 media_rw 3452 2025-06-07 11:51 .Android_819ee91f8d8cc1bb
drwxrws--- 2 u0_a274 media_rw 8192 2025-11-04 18:20 .Android_a21c40ada1eb475b
drwxrws--- 2 u0_a274 media_rw 8192 2025-06-07 11:51 .Android_f0fcd2bde58f4ae8
drwxrws--- 2 u0_a274 media_rw 3452 2026-05-09 15:48 .convertTemp1583484943
-rw-rw---- 1 u0_a274 media_rw  148 2025-07-12 08:17 .thumbcache_001
-rw-rw---- 1 u0_a274 media_rw   88 2025-07-29 00:02 .thumbcache_idx_gm_McqWd0osIPezODQX95O1
drwxrws--- 2 u0_a274 media_rw 3452 2026-04-23 21:53 OcrScanner
drwxrws--- 2 u0_a274 media_rw 3452 2025-06-02 07:36 PayGateWayLibrary
drwxrws--- 3 u0_a274 media_rw 3452 2025-06-09 17:30 QQBrowser
drwxrws--- 2 u0_a274 media_rw 3452 2026-04-15 02:47 ZYH
drwxrws--- 2 u0_a274 media_rw 3452 2025-07-13 15:59 app_dump
drwxrws--- 3 u0_a274 media_rw 3452 2025-08-04 22:25 qqmusic
drwxrws--- 3 u0_a274 media_rw 3452 2025-05-31 23:26 ringtone
drwxrws--- 3 u0_a274 media_rw 3452 2025-05-31 23:29 user
drwxrws--- 2 u0_a274 media_rw 3452 2025-08-14 15:02 图片转文档


手机上找不到技能文件，可能都在 App 私有目录里（需要 root 才能访问）。

不过从它的回复来看，关键技术点是：
1. **Deep Link** — 用 `bilibili://search?keyword=xxx` 这种 URI 直达
2. **Accessibility Service** — Android 无障碍服务，可以读取/操作界面元素
3. **先观察再操作** — 每次操作前确认界面状态

小旺，这个 App 的技能文件能导出吗？或者你能截图给我看看它的设置界面？我想看看它具体是怎么配置 App 自动化的 🦐

Explanation:
这个 App 的数据可能在 /data/data 里面，但我没有 root 权限访问。让我看看能不能通过其他方式找到它的技能文件。

用户说的是这个软件能实现 App 自动化操作，它回复说用了 Deep Link、Accessibility Service 等。让我看看能不能找到更多信息。

也许我应该直接问小旺这个软件的技能文件在哪里，或者看看它的 APK 能不能提取出来分析。


[System note: Your previous turn was interrupted before you could process the last tool result(s). The conversation history contains tool outputs you haven't responded to yet. Please finish processing those results and summarize what was accomplished, then address the user's new message below.]

我刚刚搞错了，应该是这个https://github.com/OPPO-Mente-Lab/X-OmniClaw?enable_bottom_share_style=1&hybrid_event_param=HybridEventParams(enterMethod%3Dmessage_markdown_url%2C%20localPage%3Dchat%2C%20chatType%3Ddefault%2C%20duration%3D0%2C%20isRichMediaPictureLink%3Dfalse%2C%20mobMap%3D%7Bmessage_id%3D44991440784764930%2C%20previous_page%3Dlanding%2C%20is_immersive_background%3D0%2C%20chat_type%3Ddefault%2C%20reply_id%3D44991440784763394%2C%20enter_method%3Dlanding%2C%20conversation_id%3D38413485644435458%2C%20enter_chat_method%3Dlanding%2C%20bot_id%3D7234781073513644036%2C%20current_page%3Dchat%7D%2C%20extra%3Dnull)&use_xbridge3=true&loader_name=forest&need_sec_link=1&sec_link_scene=im&theme=light


原来是 OPPO 的 X-OmniClaw！让我去看看：

Explanation: 用户给了正确的 GitHub 链接，是 OPPO-Mente-Lab/X-OmniClaw。让我去克隆这个仓库看看它的实现。

cd /tmp && rm -rf clawdroid X-OmniClaw 2>/dev/null; git clone --depth 1 https://github.com/OPPO-Mente-Lab/X-OmniClaw.git 2>&1


Output: Cloning into 'X-OmniClaw'...

Command required approval (recursive delete) and was approved by the user.


cd /tmp/X-OmniClaw && find . -type f -name "*.py" -o -name "*.md" | head -30


Output:
./README_zh.md
./extensions/feishu/skills/feishu-doc/SKILL.md
./extensions/feishu/skills/feishu-task/SKILL.md
./extensions/feishu/skills/feishu-wiki/SKILL.md
./extensions/feishu/skills/feishu-perm/SKILL.md
./extensions/feishu/skills/feishu-chat/SKILL.md
./extensions/feishu/skills/feishu-urgent/SKILL.md
./extensions/feishu/skills/feishu-drive/SKILL.md
./extensions/feishu/skills/feishu-bitable/SKILL.md
./extensions/observer/REFACTOR_PLAN.md
./self-control/skills/self-control.md
./self-control/SUMMARY.md
./self-control/INTEGRATION.md
./self-control/DUAL_MODE.md
./self-control/self-control-skill.md
./self-control/ADB_USAGE.md
./self-control/PROJECT_SUMMARY.md
./README.md
./app/src/main/python/session_logger.py
./app/src/main/python/agent_logic.py
./app/src/main/python/incremental_sensor.py
./app/src/main/python/context_manager.py
./app/src/main/python/utils.py
./app/src/main/python/loop_detector.py
./app/src/main/assets/skills/app-search/SKILL.md
./app/src/main/assets/skills/capcut-theme-video/SKILL.md
./app/src/main/assets/skills/channel-config/SKILL.md
./app/src/main/assets/skills/model-config/SKILL.md
./app/src/main/assets/skills/memory-evolution/SKILL.md
./app/src/main/assets/skills/gallery-qa/SKILL.md


Output:
total 160
drwxrwxr-x  10 ubuntu ubuntu  4096 May 15 04:10 .
drwxrwxrwt 101 root   root   28672 May 15 04:10 ..
drwxrwxr-x   3 ubuntu ubuntu  4096 May 15 04:10 app
drwxrwxr-x   2 ubuntu ubuntu  4096 May 15 04:10 assets
-rw-rw-r--   1 ubuntu ubuntu  1258 May 15 04:10 build.gradle
drwxrwxr-x   5 ubuntu ubuntu  4096 May 15 04:10 extensions
drwxrwxr-x   2 ubuntu ubuntu  4096 May 15 04:10 figure
drwxrwxr-x   8 ubuntu ubuntu  4096 May 15 04:10 .git
-rw-rw-r--   1 ubuntu ubuntu    49 May 15 04:10 .gitattributes
-rw-rw-r--   1 ubuntu ubuntu   651 May 15 04:10 .gitignore
drwxrwxr-x   3 ubuntu ubuntu  4096 May 15 04:10 gradle
-rw-rw-r--   1 ubuntu ubuntu  2034 May 15 04:10 gradle.properties
-rw-rw-r--   1 ubuntu ubuntu  5468 May 15 04:10 gradlew
-rw-rw-r--   1 ubuntu ubuntu  2260 May 15 04:10 gradlew.bat
-rw-rw-r--   1 ubuntu ubuntu 11357 May 15 04:10 LICENSE
-rw-rw-r--   1 ubuntu ubuntu   142 May 15 04:10 local.properties.example
-rw-rw-r--   1 ubuntu ubuntu 22431 May 15 04:10 README.md
-rw-rw-r--   1 ubuntu ubuntu 23709 May 15 04:10 README_zh.md
drwxrwxr-x   4 ubuntu ubuntu  4096 May 15 04:10 self-control
-rw-rw-r--   1 ubuntu ubuntu   161 May 15 04:10 settings.gradle
drwxrwxr-x   2 ubuntu ubuntu  4096 May 15 04:10 videos


哇，宝藏项目！有很多技能文件。让我看看核心实现：

Explanation:
哇，这是一个很完整的项目！有很多技能文件。让我看看核心的自动化实现：
1. self-control 目录 - 看起来是自控制/自动化相关
2. app/src/main/python - Python 逻辑
3. skills 目录 - 各种技能

让我重点看看 self-control 和核心的 agent_logic.py


     1|---
     2|name: self-control
     3|description: X-OmniClaw 自我控制和管理技能
     4|metadata:
     5|  {
     6|    "omniclaw": {
     7|      "always": false,
     8|      "emoji": "🔧"
     9|    }
    10|  }
    11|---
    12|
    13|# Self-Control Skill
    14|
    15|让 AI Agent 能够控制和管理 X-OmniClaw 自身的核心技能。
    16|
    17|**设计理念**: 让 AI Agent 具备自我认知、自我调优、自我诊断的能力，实现真正的自主闭环。
    18|
    19|---
    20|
    21|## 🎯 核心能力
    22|
    23|Self-Control Skill 提供以下四大类能力：
    24|
    25|### 1. 页面导航 (`navigate_app`)
    26|
    27|跳转到应用内各个页面，方便查看和修改配置。
    28|
    29|**可用页面**:
    30|- `main`: 主界面
    31|- `config`: 配置页面（API、模型设置）
    32|- `permissions`: 权限管理
    33|- `chat_history`: 对话历史
    34|- `chat_log`: 详细日志
    35|- `feishu`: 飞书通道
    36|- `channels`: 通道列表
    37|- `result`: 结果展示
    38|
    39|**示例**:
    40|```json
    41|{
    42|  "name": "navigate_app",
    43|  "arguments": {
    44|    "page": "config"
    45|  }
    46|}
    47|```
    48|
    49|### 2. 配置管理 (`manage_config`)
    50|
    51|读取和修改应用配置参数。
    52|
    53|**操作类型**:
    54|- `get`: 读取配置
    55|- `set`: 设置配置
    56|- `list`: 列出配置
    57|- `delete`: 删除配置
    58|
    59|**配置分类**:
    60|- `agent`: Agent 运行参数
    61|- `api`: API 设置
    62|- `ui`: UI 偏好
    63|- `feature`: 功能开关
    64|- `perf`: 性能参数
    65|
    66|**示例**:
    67|```json
    68|// 读取配置
    69|{
    70|  "name": "manage_config",
    71|  "arguments": {
    72|    "operation": "get",
    73|    "key": "exploration_mode"
    74|  }
    75|}
    76|
    77|// 设置配置
    78|{
    79|  "name": "manage_config",
    80|  "arguments": {
    81|    "operation": "set",
    82|    "key": "exploration_mode",
    83|    "value": "true"
    84|  }
    85|}
    86|
    87|// 列出所有功能开关
    88|{
    89|  "name": "manage_config",
    90|  "arguments": {
    91|    "operation": "list",
    92|    "category": "feature"
    93|  }
    94|}
    95|```
    96|
    97|### 3. 服务控制 (`control_service`)
    98|
    99|控制悬浮窗和后台服务。
   100|
   101|**操作类型**:
   102|- `show_float`: 显示悬浮窗
   103|- `hide_float`: 隐藏悬浮窗
   104|- `start_float`: 启动悬浮窗服务
   105|- `stop_float`: 停止悬浮窗服务
   106|- `check_status`: 检查服务状态
   107|
   108|**示例**:
   109|```json
   110|// 隐藏悬浮窗（截图前）
   111|{
   112|  "name": "control_service",
   113|  "arguments": {
   114|    "operation": "hide_float"
   115|  }
   116|}
   117|
   118|// 延迟显示悬浮窗
   119|{
   120|  "name": "control_service",
   121|  "arguments": {
   122|    "operation": "show_float",
   123|    "delay_ms": 500
   124|  }
   125|}
   126|
   127|// 检查服务状态
   128|{
   129|  "name": "control_service",
   130|  "arguments": {
   131|    "operation": "check_status"
   132|  }
   133|}
   134|```
   135|
   136|### 4. 日志查询 (`query_logs`)
   137|
   138|查询应用运行日志，诊断问题。
   139|
   140|**日志级别**:
   141|- `V`: Verbose（详细）
   142|- `D`: Debug（调试）
   143|- `I`: Info（信息）
   144|- `W`: Warning（警告）
   145|- `E`: Error（错误）
   146|- `F`: Fatal（致命）
   147|
   148|**示例**:
   149|```json
   150|// 查看最近的错误
   151|{
   152|  "name": "query_logs",
   153|  "arguments": {
   154|    "level": "E",
   155|    "lines": 50
   156|  }
   157|}
   158|
   159|// 搜索特定 TAG
   160|{
   161|  "name": "query_logs",
   162|  "arguments": {
   163|    "level": "D",
   164|    "filter": "AgentLoop",
   165|    "lines": 100
   166|  }
   167|}
   168|```
   169|
   170|---
   171|
   172|## 📋 使用原则
   173|
   174|### 何时使用 Self-Control
   175|
   176|**适用场景**:
   177|1. **操作失败后诊断** - 查看日志分析原因
   178|2. **截图前后** - 隐藏/显示悬浮窗
   179|3. **性能优化** - 调整延迟、超时参数
   180|4. **功能切换** - 开关特定功能
   181|5. **用户请求配置** - 用户明确要求修改设置
   182|
   183|**不适用场景**:
   184|1. **正常任务执行中** - 不要频繁检查日志
   185|2. **没有问题时** - 不要盲目调整配置
   186|3. **不确定的修改** - 不要猜测性地修改参数
   187|
   188|### 使用流程
   189|
   190|```
   191|观察问题 → 查询日志 → 分析原因 → 调整配置 → 验证结果
   192|```
   193|
   194|---
   195|
   196|## 🔄 典型工作流
   197|
   198|### 工作流 1: 截图操作（推荐模式）
   199|
   200|```json
   201|// Step 1: 隐藏悬浮窗
   202|{
   203|  "name": "control_service",
   204|  "arguments": {"operation": "hide_float"}
   205|}
   206|
   207|// Step 2: 短暂延迟确保 UI 更新
   208|{
   209|  "name": "wait",
   210|  "arguments": {"duration": 100}
   211|}
   212|
   213|// Step 3: 执行截图
   214|{
   215|  "name": "screenshot",
   216|  "arguments": {}
   217|}
   218|
   219|// Step 4: 显示悬浮窗（延迟显示避免干扰）
   220|{
   221|  "name": "control_service",
   222|  "arguments": {
   223|    "operation": "show_float",
   224|    "delay_ms": 500
   225|  }
   226|}
   227|```
   228|
   229|### 工作流 2: 自我诊断
   230|
   231|```json
   232|// Step 1: 检查服务状态
   233|{
   234|  "name": "control_service",
   235|  "arguments": {"operation": "check_status"}
   236|}
   237|
   238|// Step 2: 查看错误日志
   239|{
   240|  "name": "query_logs",
   241|  "arguments": {
   242|    "level": "E",
   243|    "lines": 50
   244|  }
   245|}
   246|
   247|// Step 3: 如果发现问题，查看详细上下文
   248|{
   249|  "name": "query_logs",
   250|  "arguments": {
   251|    "level": "D",
   252|    "filter": "具体的错误关键字",
   253|    "lines": 100
   254|  }
   255|}
   256|```
   257|
   258|### 工作流 3: 性能调优
   259|
   260|```json
   261|// Step 1: 读取当前配置
   262|{
   263|  "name": "manage_config",
   264|  "arguments": {
   265|    "operation": "get",
   266|    "key": "screenshot_delay"
   267|  }
   268|}
   269|
   270|// Step 2: 根据分析调整参数
   271|{
   272|  "name": "manage_config",
   273|  "arguments": {
   274|    "operation": "set",
   275|    "key": "screenshot_delay",
   276|    "value": "200"
   277|  }
   278|}
   279|
   280|// Step 3: 验证修改
   281|{
   282|  "name": "manage_config",
   283|  "arguments": {
   284|    "operation": "get",
   285|    "key": "screenshot_delay"
   286|  }
   287|}
   288|```
   289|
   290|### 工作流 4: 用户配置修改
   291|
   292|```json
   293|// Step 1: 打开配置页面
   294|{
   295|  "name": "navigate_app",
   296|  "arguments": {"page": "config"}
   297|}
   298|
   299|// Step 2: 列出当前配置
   300|{
   301|  "name": "manage_config",
   302|  "arguments": {
   303|    "operation": "list",
   304|    "category": "feature"
   305|  }
   306|}
   307|
   308|// Step 3: 根据用户要求修改
   309|{
   310|  "name": "manage_config",
   311|  "arguments": {
   312|    "operation": "set",
   313|    "key": "user_requested_key",
   314|    "value": "user_requested_value"
   315|  }
   316|}
   317|```
   318|
   319|---
   320|
   321|## ⚠ 重要注意事项
   322|
   323|### 配置修改规则
   324|
   325|1. **谨慎修改** - 只修改明确需要改变的配置
   326|2. **验证修改** - 修改后立即读取验证
   327|3. **记录原值** - 修改前记录原始值，必要时回滚
   328|4. **用户确认** - 重要配置修改需要用户确认
   329|
   330|### 日志查询优化
   331|
   332|1. **按需查询** - 不要在每个步骤都查日志
   333|2. **过滤关键字** - 使用 filter 参数精确查找
   334|3. **合适级别** - 根据问题严重程度选择日志级别
   335|4. **限制行数** - 不要一次查询过多日志
   336|
   337|### 服务控制注意
   338|
   339|1. **成对操作** - hide_float 后必须 show_float
   340|2. **添加延迟** - 显示悬浮窗时使用 delay_ms 避免干扰
   341|3. **检查权限** - 确保有悬浮窗权限
   342|4. **优雅处理** - 操作失败不影响主流程
   343|
   344|---
   345|
   346|## 🎓 高级技巧
   347|
   348|### 技巧 1: 自适应延迟
   349|
   350|根据日志分析动态调整延迟参数：
   351|
   352|```python
   353|# 伪代码逻辑
   354|if 日志中出现 "screenshot timeout":
   355|    读取当前 screenshot_delay
   356|    增加 50ms
   357|    设置新的 screenshot_delay
   358|    通知用户已优化
   359|```
   360|
   361|### 技巧 2: 批量配置
   362|
   363|一次性修改多个相关配置：
   364|
   365|```json
   366|// 开启探索模式 + 降低延迟
   367|[
   368|  {
   369|    "name": "manage_config",
   370|    "arguments": {
   371|      "operation": "set",
   372|      "key": "exploration_mode",
   373|      "value": "true"
   374|    }
   375|  },
   376|  {
   377|    "name": "manage_config",
   378|    "arguments": {
   379|      "operation": "set",
   380|      "key": "screenshot_delay",
   381|      "value": "50"
   382|    }
   383|  }
   384|]
   385|```
   386|
   387|### 技巧 3: 日志驱动决策
   388|
   389|根据日志内容自动决策：
   390|
   391|```python
   392|# 伪代码
   393|logs = query_logs(level="E", filter="AccessibilityService")
   394|
   395|if "permission denied" in logs:
   396|    navigate_app(page="permissions")
   397|    提示用户："检测到权限问题，已打开权限页面"
   398|elif "service not running" in logs:
   399|    control_service(operation="start_float")
   400|    提示用户："服务已重启"
   401|```
   402|
   403|---
   404|
   405|## 📊 性能考虑
   406|
   407|### 开销评估
   408|
   409|| 操作 | 开销 | 频率建议 |
   410||------|------|----------|
   411|| `navigate_app` | 低 | 按需 |
   412|| `manage_config` (get) | 极低 | 随意 |
   413|| `manage_config` (set) | 低 | 谨慎 |
   414|| `control_service` | 低 | 按需 |
   415|| `query_logs` | 中-高 | 仅在诊断时 |
   416|
   417|### 优化建议
   418|
   419|1. **批量操作** - 一次性完成多个配置修改
   420|2. **缓存结果** - 避免重复查询同样的配置
   421|3. **延迟日志查询** - 只在真正需要时查日志
   422|4. **异步执行** - 不阻塞主流程
   423|
   424|---
   425|
   426|## 🔐 安全考虑
   427|
   428|### 配置白名单（建议）
   429|
   430|只允许修改安全的配置项：
   431|- `exploration_mode`
   432|- `screenshot_delay`
   433|- `ui_tree_enabled`
   434|- `reasoning_enabled`
   435|
   436|**不应修改**:
   437|- `api_key` (敏感)
   438|- `api_base_url` (安全)
   439|- 系统级配置
   440|
   441|### 操作审计
   442|
   443|所有 Self-Control 操作都会记录到日志：
   444|
   445|```
   446|SelfControlRegistry: Executing self-control skill: manage_config
   447|ConfigSkill: Config updated: exploration_mode = true
   448|```
   449|
   450|---
   451|
   452|## 🎯 最佳实践总结
   453|
   454|1. **截图前隐藏悬浮窗** - 避免截图中出现悬浮窗
   455|2. **操作失败后查日志** - 诊断根本原因
   456|3. **谨慎修改配置** - 只改必须改的
   457|4. **用户确认重要修改** - 关键配置需要确认
   458|5. **验证每次修改** - 修改后立即验证
   459|6. **记录原始值** - 方便回滚
   460|7. **优雅降级** - Self-Control 失败不影响主任务
   461|
   462|---
   463|
   464|**Self-Control Skill** - 让 AI Agent 具备自我管理能力 🧠🔧
   465|
   466|_Inspired by X-OmniClaw Skills System_
   467|


     1|"""Core Agent Loop logic — Python-side entry point called from Kotlin via Chaquopy.
     2|
     3|The Kotlin AgentLoop.kt acts as a thin bridge: it creates a KotlinBridge object and
     4|passes it here.  All business logic (iteration control, context management, loop
     5|detection, incremental sensing, nudge injection, error tracking) lives in this file.
     6|
     7|Public entry:  run_agent(bridge, system_prompt, user_message, context_history_json,
     8|                         reasoning_enabled, max_iterations, context_window_tokens)
     9|Returns: JSON string of {"final_content": str, "tools_used": [str], "iterations": int,
    10|                          "messages": [...]}
    11|"""
    12|
    13|import json
    14|import time
    15|import re
    16|
    17|from loop_detector import LoopDetector, LoopDetectionResult
    18|from context_manager import enforce_context_budget, HARD_KEEP_RECENT_DIALOG_TURNS
    19|from session_logger import SessionLogger
    20|from utils import (
    21|    is_likely_premature_plan_text,
    22|    is_observe_ui_tool_call,
    23|    is_mutating_ui_tool_call,
    24|    parse_tool_args_json,
    25|    estimate_context_chars,
    26|    TEXT_ONLY_CONTINUATION_NUDGE,
    27|    MAX_TEXT_ONLY_CONTINUATION_NUDGES,
    28|)
    29|
    30|MAX_CONSECUTIVE_ERRORS = 3
    31|
    32|_REASONING_TAG_RE = re.compile(
    33|    r"<(think|thinking|reasoning|reflection|inner_thoughts)>.*?</\1>",
    34|    re.DOTALL | re.IGNORECASE,
    35|)
    36|
    37|# 将聊天态识别提示集中管理，避免业务关键词散落在逻辑分支中难以维护。
    38|_CHAT_SNAPSHOT_INPUT_MARKERS_RAW = ("输入框",)
    39|_CHAT_SNAPSHOT_INPUT_MARKERS_LOWER = ("editable=true", "edittext")
    40|_CHAT_SNAPSHOT_SEND_MARKERS_RAW = ("发送",)
    41|_CHAT_SNAPSHOT_SEND_MARKERS_LOWER = ("send",)
    42|
    43|
    44|def _strip_reasoning_tags(text):
    45|    if not text:
    46|        return text
    47|    return _REASONING_TAG_RE.sub("", text).strip()
    48|
    49|
    50|def _format_reasoning_for_log(text, max_chars=4000):
    51|    """将推理内容压平成单行并限长，避免日志被换行切碎或无限膨胀。"""
    52|    if not text:
    53|        return ""
    54|    normalized = re.sub(r"\s+", " ", str(text)).strip()
    55|    if len(normalized) <= max_chars:
    56|        return normalized
    57|    # 超长时截断并附带标记，保证主日志可读且可快速定位前半段关键推理。
    58|    return normalized[: max_chars - 1] + "…"
    59|
    60|
    61|def _safe_get_skill_selection_trace(bridge):
    62|    """从 KotlinBridge 读取技能筛选摘要；桥接版本不支持时静默降级。"""
    63|    try:
    64|        if hasattr(bridge, "get_skill_selection_trace"):
    65|            trace = bridge.get_skill_selection_trace()
    66|            if trace:
    67|                return str(trace).strip()
    68|    except Exception:
    69|        # 兼容老版本 bridge：不因调试日志能力缺失影响主流程。
    70|        pass
    71|    return ""
    72|
    73|
    74|def _is_snapshot_ref_error(err: str) -> bool:
    75|    """Detect typical snapshot/ref-related failures where visual fallback is desired."""
    76|    if not err:
    77|        return False
    78|    msg = err.lower()
    79|    patterns = [
    80|        "已过期",
    81|        "ref '",
    82|        "ref \"",
    83|        "不存在于最近一次 snapshot",
    84|        "无障碍服务未开启",
    85|        "无障碍服务未启用",
    86|        "获取 ui 树失败",
    87|        "accessibility",
    88|        "dumpviewtree failed",
    89|    ]
    90|    return any(p.lower() in msg for p in patterns)
    91|
    92|
    93|_SEND_HINTS = ("发送", "发 送", "send")
    94|
    95|
    96|def _is_send_like_device_tap(fn_name, args):
    97|    if fn_name != "device":
    98|        return False
    99|    if str(args.get("action", "")).lower() != "act":
   100|        return False
   101|    if str(args.get("kind", "")).lower() != "tap":
   102|        return False
   103|    target = str(args.get("target", "")).strip().lower()
   104|    if target and any(h in target for h in _SEND_HINTS):
   105|        return True
   106|    return False
   107|
   108|
   109|def _detect_chat_snapshot_flags(result_content):
   110|    """
   111|    识别 snapshot 返回中与聊天页相关的轻量特征。
   112|
   113|    返回:
   114|    - in_chat_context: 是否看起来处于聊天上下文
   115|    - input_seen: 是否看到输入框特征
   116|    - send_seen: 是否看到发送特征
   117|    """
   118|    text = result_content or ""
   119|    lower = text.lower()
   120|
   121|    input_seen = any(marker in text for marker in _CHAT_SNAPSHOT_INPUT_MARKERS_RAW) or any(
   122|        marker in lower for marker in _CHAT_SNAPSHOT_INPUT_MARKERS_LOWER
   123|    )
   124|    send_seen = any(marker in text for marker in _CHAT_SNAPSHOT_SEND_MARKERS_RAW) or any(
   125|        marker in lower for marker in _CHAT_SNAPSHOT_SEND_MARKERS_LOWER
   126|    )
   127|    in_chat_context = input_seen or send_seen
   128|    return in_chat_context, input_seen, send_seen
   129|
   130|
   131|def _update_runtime_state_after_tool(runtime_state, fn_name, args, result_success, result_content, metadata):
   132|    """根据工具返回更新轻量运行态，避免 agent 在同一任务内丢失上下文。"""
   133|    if not result_success:
   134|        return
   135|
   136|    if fn_name != "device":
   137|        return
   138|
   139|    action = str(args.get("action", "")).lower()
   140|    if action == "snapshot":
   141|        # 不再依赖 package 判断：只要成功执行 snapshot，就视为完成一次发送后观察。
   142|        if runtime_state.get("pending_send_verify"):
   143|            runtime_state["pending_send_verify"] = False
   144|            runtime_state["last_send_verified_at_iter"] = runtime_state.get("iteration", 0)
   145|
   146|        # 通过 UI 文本特征识别是否进入聊天上下文，避免前台包名瞬时抖动误判。
   147|        in_chat_context, input_seen, send_seen = _detect_chat_snapshot_flags(result_content)
   148|        if in_chat_context:
   149|            runtime_state["in_feishu_chat"] = True
   150|            if input_seen:
   151|                runtime_state["feishu_input_seen"] = True
   152|            if send_seen:
   153|                runtime_state["feishu_send_seen"] = True
   154|    elif action == "act":
   155|        kind = str(args.get("kind", "")).lower()
   156|        if kind == "type" and runtime_state.get("in_feishu_chat"):
   157|            runtime_state["typed_in_feishu_chat"] = True
   158|        if kind == "tap" and (
   159|            _is_send_like_device_tap(fn_name, args) or bool(metadata.get("requires_post_send_snapshot_verify"))
   160|        ):
   161|            runtime_state["pending_send_verify"] = True
   162|            runtime_state["last_send_tap_iter"] = runtime_state.get("iteration", 0)
   163|
   164|
   165|def _build_runtime_state_hint(runtime_state):
   166|    """构造短状态提示，提醒模型延续当前任务而非回到起点。"""
   167|    hints = []
   168|
   169|    if runtime_state.get("in_feishu_chat"):
   170|        hints.append("已进入飞书会话上下文，不要重复从“打开飞书”开始")
   171|    if runtime_state.get("typed_in_feishu_chat"):
   172|        hints.append("本轮会话已执行过输入，优先继续发送与验收，不要重新输入覆盖")
   173|    if runtime_state.get("pending_send_verify"):
   174|        hints.append("刚执行发送动作，下一步必须先观察（snapshot/screenshot）确认消息已发出")
   175|
   176|    if not hints:
   177|        return None
   178|    return "系统状态记忆：" + "；".join(hints) + "。"
   179|
   180|
   181|def _msg(role, content, tool_calls=None, tool_call_id=None, name=None):
   182|    """统一消息构建：基础字段 + 可选字段。"""
   183|    message = {"role": role, "content": content or ""}
   184|    optional_fields = {
   185|        "tool_calls": tool_calls,
   186|        "tool_call_id": tool_call_id,
   187|        "name": name,
   188|    }
   189|    for key, value in optional_fields.items():
   190|        if value:
   191|            message[key] = value
   192|    return message
   193|
   194|
   195|def _build_system_prompt_with_self_counting(system_prompt, reasoning_enabled):
   196|    """
   197|    Python 侧不再注入额外规则，避免产生多条 system 消息。
   198|    规则统一维护在 assets/bootstrap（由 Kotlin 侧构建 system prompt 时加载）。
   199|    """
   200|    return (system_prompt or "").strip()
   201|
   202|
   203|def _build_initial_messages(system_prompt, reasoning_enabled, context_history_json, user_message, logger):
   204|    """构建会话初始 messages，减少 run_agent 主流程样板代码。"""
   205|    messages = []
   206|    effective_system_prompt = _build_system_prompt_with_self_counting(system_prompt, reasoning_enabled)
   207|    messages.append(_msg("system", effective_system_prompt))
   208|    logger.write(f"✅ System prompt added ({len(effective_system_prompt)} chars)")
   209|
   210|    try:
   211|        ctx_history = json.loads(context_history_json) if context_history_json else []
   212|    except Exception:
   213|        ctx_history = []
   214|
   215|    if ctx_history:
   216|        # thinking 与 Kotlin 侧一致：只存盘与 UI 恢复，不进入模型上文
   217|        filtered = [m for m in ctx_history if m.get("role") not in ("system", "thinking")]
   218|        messages.extend(filtered)
   219|        logger.write(f"✅ Context history added: {len(filtered)} messages")
   220|
   221|    messages.append(_msg("user", user_message))
   222|    logger.write(f"✅ User message: {user_message}")
   223|    return messages
   224|
   225|
   226|def _is_stopped_error(err_msg):
   227|    return "__AGENT_STOPPED__" in (err_msg or "")
   228|
   229|
   230|def _accumulate_llm_usage(totals, usage_obj):
   231|    """
   232|    将 Kotlin call_llm 返回中的 usage（OpenAI 风格）累加到本轮 Agent 合计。
   233|    totals: dict，含 prompt、completion、total 三键，原地修改。
   234|    """
   235|    if not usage_obj or not isinstance(usage_obj, dict):
   236|        return
   237|    pt = int(usage_obj.get("prompt_tokens") or 0)
   238|    ct = int(usage_obj.get("completion_tokens") or 0)
   239|    tt = usage_obj.get("total_tokens")
   240|    if tt is not None:
   241|        ttn = int(tt)
   242|    else:
   243|        ttn = pt + ct
   244|    totals["prompt"] += pt
   245|    totals["completion"] += ct
   246|    totals["total"] += ttn
   247|
   248|
   249|# ---------------------------------------------------------------------------
   250|# Main entry — called from Kotlin
   251|# ---------------------------------------------------------------------------
   252|
   253|def run_agent(
   254|    bridge,
   255|    system_prompt,
   256|    user_message,
   257|    context_history_json,
   258|    reasoning_enabled,
   259|    max_iterations,
   260|    context_window_tokens,
   261|):
   262|    """Main Python-side agent loop.
   263|
   264|    Args:
   265|        bridge: KotlinBridge object with methods call_llm, execute_tool, emit_progress,
   266|                get_tool_definitions, strip_reasoning_tags.
   267|        system_prompt: str
   268|        user_message: str
   269|        context_history_json: JSON string — list of Message dicts
   270|        reasoning_enabled: bool
   271|        max_iterations: int
   272|        context_window_tokens: int
   273|
   274|    Returns:
   275|        JSON string with keys: final_content, tools_used, iterations, messages
   276|    """
   277|    logger = SessionLogger()
   278|    logger.init_session(user_message)
   279|
   280|    loop_detector = LoopDetector()
   281|    error_tracker = []
   282|
   283|    logger.write("========== Agent Loop 开始 (Python) ==========")
   284|    logger.write(f"Max iterations: {max_iterations}")
   285|    logger.write(f"Reasoning: {'enabled' if reasoning_enabled else 'disabled'}")
   286|    skill_trace = _safe_get_skill_selection_trace(bridge)
   287|    if skill_trace:
   288|        logger.write("🧩 Skill Selection Trace (from Kotlin):")
   289|        for line in skill_trace.splitlines():
   290|            logger.write(f"  {line}")
   291|
   292|    # --- Build message list ---
   293|    messages = _build_initial_messages(
   294|        system_prompt=system_prompt,
   295|        reasoning_enabled=reasoning_enabled,
   296|        context_history_json=context_history_json,
   297|        user_message=user_message,
   298|        logger=logger,
   299|    )
   300|
   301|    # --- Loop state ---
   302|    tools_used = []
   303|    final_content = None
   304|    text_only_nudges = 0
   305|    runtime_state = {}
   306|    # 多轮 LLM 调用的 token 累加（依赖 Kotlin bridge 在每轮 JSON 中附带 usage）
   307|    usage_totals = {"prompt": 0, "completion": 0, "total": 0}
   308|
   309|    # max_iterations==0 时 range(1,1) 为空，for 不会给 iteration 赋值，文末记录/JSON 会 NameError，故先设默认值。
   310|    iteration = 0
   311|    for iteration in range(1, max_iterations + 1):
   312|        iter_start = time.time()
   313|        runtime_state["iteration"] = iteration
   314|        logger.write(f"========== Iteration {iteration} ==========")
   315|
   316|        # Check stop signal from Kotlin at the top of each iteration
   317|        if bridge.is_stop_requested():
   318|            logger.write("🛑 已收到用户停止信号，退出循环")
   319|            final_content = "已按用户请求停止。"
   320|            break
   321|
   322|        # Emit iteration progress
   323|        bridge.emit_progress("iteration", json.dumps({"number": iteration}))
   324|
   325|        # --- Context management ---
   326|        enforce_context_budget(messages, context_window_tokens)
   327|
   328|        # 注入短状态记忆（仅本轮 LLM 上文，不入库）：否则会以「用户消息」写入会话，
   329|        # 主界面会出现多条重复的“系统状态记忆”气泡。
   330|        state_hint = _build_runtime_state_hint(runtime_state)
   331|        payload_messages = messages
   332|        if state_hint:
   333|            payload_messages = messages + [_msg("user", state_hint)]
   334|            logger.write(f"🧭 Runtime state hint injected (non-persistent): {state_hint}")
   335|
   336|        # --- LLM call ---
   337|        bridge.emit_progress("thinking", json.dumps({"iteration": iteration}))
   338|        llm_start = time.time()
   339|
   340|        try:
   341|            response_json = bridge.call_llm(json.dumps(payload_messages), reasoning_enabled, iteration)
   342|        except Exception as e:
   343|            err_msg = str(e)
   344|            # User-requested stop: exit cleanly
   345|            if _is_stopped_error(err_msg):
   346|                logger.write("🛑 LLM 调用时收到停止信号，退出循环")
   347|                final_content = "已按用户请求停止。"
   348|                break
   349|
   350|            logger.write(f"❌ LLM 调用失败: {err_msg}")
   351|
   352|            if _track_error(error_tracker, err_msg):
   353|                final_content = f"任务失败: 连续 {MAX_CONSECUTIVE_ERRORS} 次相同错误 - {err_msg}"
   354|                break
   355|
   356|            if "timeout" in err_msg.lower():
   357|                messages.append(_msg("user", "系统提示: LLM 调用超时，请尝试简化任务或分步执行"))
   358|                continue
   359|
   360|            final_content = f"❌ 执行出错\n\n**错误信息**: {err_msg}"
   361|            break
   362|
   363|        llm_duration_ms = int((time.time() - llm_start) * 1000)
   364|
   365|        try:
   366|            response = json.loads(response_json)
   367|        except Exception:
   368|            logger.write(f"❌ 无法解析 LLM 响应 JSON")
   369|            final_content = "❌ 内部错误：无法解析 LLM 响应"
   370|            break
   371|
   372|        _accumulate_llm_usage(usage_totals, response.get("usage"))
   373|        logger.write(f"✅ LLM 响应已收到 [耗时: {llm_duration_ms}ms]")
   374|
   375|        # Reasoning
   376|        thinking = response.get("thinking_content")
   377|        if thinking:
   378|            logger.write(f"🧠 Reasoning ({len(thinking)} chars)")
   379|            logger.write(f"🧠 Reasoning Content: {_format_reasoning_for_log(thinking)}")
   380|            bridge.emit_progress(
   381|                "reasoning",
   382|                json.dumps({"content": thinking[:2000], "llm_duration": llm_duration_ms}),
   383|            )
   384|
   385|        tool_calls = response.get("tool_calls") or []
   386|        content = response.get("content")
   387|
   388|        # --- Branch: has tool calls ---
   389|        if tool_calls:
   390|            logger.write(f"Function calls: {len(tool_calls)}")
   391|
   392|            # Block reply: emit intermediate text
   393|            intermediate_text = (content or "").strip()
   394|            if intermediate_text:
   395|                logger.write(f"📤 Block reply: {intermediate_text[:200]}...")
   396|                bridge.emit_progress(
   397|                    "block_reply",
   398|                    json.dumps({"text": intermediate_text, "iteration": iteration}),
   399|                )
   400|
   401|            # 解析工具参数仅一次：同时用于 assistant message 与实际执行。
   402|            parsed_tool_calls = []
   403|            assistant_tool_calls = []
   404|            for tc in tool_calls:
   405|                normalized_args = parse_tool_args_json(tc.get("arguments", "{}"))
   406|                args_json = json.dumps(normalized_args, ensure_ascii=False)
   407|                parsed_tool_calls.append({
   408|                    "id": tc["id"],
   409|                    "name": tc["name"],
   410|                    "args": normalized_args,
   411|                    "args_json": args_json,
   412|                })
   413|                assistant_tool_calls.append({
   414|                    "id": tc["id"],
   415|                    "name": tc["name"],
   416|                    "arguments": args_json,
   417|                })
   418|            messages.append(_msg("assistant", content, tool_calls=assistant_tool_calls))
   419|
   420|            saw_observe = False
   421|            saw_mutate = False
   422|            total_exec_ms = 0
   423|            should_stop = False
   424|
   425|            def stop_tool_loop(content_to_set, log_message=None):
   426|                """统一工具循环内的停止状态同步，避免重复赋值分支。"""
   427|                nonlocal should_stop, final_content
   428|                if log_message:
   429|                    logger.write(log_message)
   430|                should_stop = True
   431|                final_content = content_to_set
   432|
   433|            for tc in parsed_tool_calls:
   434|                fn_name = tc["name"]
   435|                args = tc.get("args", {})
   436|                args_json = tc.get("args_json", "{}")
   437|                tc_id = tc["id"]
   438|
   439|                logger.write(f"🔧 Function: {fn_name}")
   440|                logger.write(f"   Args: {args_json}")
   441|
   442|                # --- Loop detection (pre-execution) ---
   443|                det = loop_detector.detect(fn_name, args)
   444|                if det.is_loop:
   445|                    level_icon = "🚨" if det.is_critical else "⚠"
   446|                    logger.write(f"{level_icon} Loop detected: {det.detector} (count: {det.count})")
   447|
   448|                    if det.is_critical:
   449|                        messages.append(_msg("tool", det.message, tool_call_id=tc_id, name=fn_name))
   450|                        bridge.emit_progress(
   451|                            "loop_detected",
   452|                            json.dumps({
   453|                                "detector": det.detector, "count": det.count,
   454|                                "message": det.message, "critical": True,
   455|                            }),
   456|                        )
   457|                        stop_tool_loop(f"Task failed: {det.message}", "🛑 Critical loop, stopping")
   458|                        break
   459|
   460|                    # Warning: inject message but skip tool
   461|                    messages.append(_msg("tool", det.message, tool_call_id=tc_id, name=fn_name))
   462|                    bridge.emit_progress(
   463|                        "loop_detected",
   464|                        json.dumps({
   465|                            "detector": det.detector, "count": det.count,
   466|                            "message": det.message, "critical": False,
   467|                        }),
   468|                    )
   469|                    continue
   470|
   471|                # Record call
   472|                loop_detector.record_call(fn_name, args, tool_call_id=tc_id)
   473|                tools_used.append(fn_name)
   474|
   475|                bridge.emit_progress(
   476|                    "tool_call",
   477|                    json.dumps({"name": fn_name, "arguments": args}),
   478|                )
   479|
   480|                # --- Execute tool (callback to Kotlin) ---
   481|                exec_start = time.time()
   482|                try:
   483|                    result_json = bridge.execute_tool(fn_name, args_json)
   484|                except Exception as e:
   485|                    err_str = str(e)
   486|                    if _is_stopped_error(err_str):
   487|                        stop_tool_loop("已按用户请求停止。", "🛑 工具执行时收到停止信号，退出循环")
   488|                        break
   489|                    result_json = json.dumps({"success": False, "content": err_str})
   490|
   491|                exec_ms = int((time.time() - exec_start) * 1000)
   492|                total_exec_ms += exec_ms
   493|
   494|                try:
   495|                    result = json.loads(result_json)
   496|                except Exception:
   497|                    result = {"success": False, "content": result_json}
   498|
   499|                result_success = result.get("success", False)
   500|                result_content = result.get("content", str(result))
   501|

Use offset=501 to continue reading (showing 1-500 of 670 lines)


     1|# Self-Control ADB 使用指南
     2|
     3|通过 ADB（Android Debug Bridge）远程调用 X-OmniClaw 的 Self-Control 功能。
     4|
     5|---
     6|
     7|## 📋 概述
     8|
     9|Self-Control Module 提供了 **3 种 ADB 调用方式**：
    10|
    11|| 方式 | 优点 | 缺点 | 推荐场景 |
    12||------|------|------|----------|
    13|| **ContentProvider** | 返回结构化数据，易解析 | 需要 `content` 命令 | 自动化脚本、CI/CD |
    14|| **Broadcast** | 兼容性好，Android 4.0+ | 异步执行，需查看日志 | 快速测试、手动调试 |
    15|| **Shell Script** | 简单易用，封装细节 | 需要额外脚本文件 | 日常使用 |
    16|
    17|---
    18|
    19|## 🚀 快速开始
    20|
    21|### 方法 1: 使用辅助脚本（推荐）
    22|
    23|```bash
    24|# 1. 下载脚本
    25|chmod +x self-control-adb.sh
    26|
    27|# 2. 查看帮助
    28|./self-control-adb.sh help
    29|
    30|# 3. 列出所有 Skills
    31|./self-control-adb.sh list
    32|
    33|# 4. 执行 Skill
    34|./self-control-adb.sh navigate_app page=config
    35|```
    36|
    37|### 方法 2: 直接使用 ADB
    38|
    39|```bash
    40|# ContentProvider 方式
    41|adb shell content call --uri content://com.shijing.xomniclaw.selfcontrol/execute \
    42|  --method navigate_app \
    43|  --extra page:s:config
    44|
    45|# Broadcast 方式
    46|adb shell am broadcast \
    47|  -a com.shijing.xomniclaw.SELF_CONTROL \
    48|  --es skill navigate_app \
    49|  --es page config
    50|```
    51|
    52|---
    53|
    54|## 📚 详细使用
    55|
    56|### 1. ContentProvider 方式
    57|
    58|#### 基本语法
    59|
    60|```bash
    61|adb shell content call \
    62|  --uri content://com.shijing.xomniclaw.selfcontrol/execute \
    63|  --method SKILL_NAME \
    64|  --extra key:type:value \
    65|  --extra key2:type:value2
    66|```
    67|
    68|#### 参数类型
    69|
    70|| 标记 | 类型 | 示例 |
    71||------|------|------|
    72|| `:s` | String | `--extra page:s:config` |
    73|| `:i` | Integer | `--extra lines:i:50` |
    74|| `:l` | Long | `--extra timestamp:l:1234567890` |
    75|| `:b` | Boolean | `--extra enabled:b:true` |
    76|| `:f` | Float | `--extra ratio:f:3.14` |
    77|| `:d` | Double | `--extra value:d:3.14159` |
    78|
    79|#### 示例
    80|
    81|##### 1.1 页面导航
    82|
    83|```bash
    84|# 打开配置页面
    85|adb shell content call \
    86|  --uri content://com.shijing.xomniclaw.selfcontrol/execute \
    87|  --method navigate_app \
    88|  --extra page:s:config
    89|
    90|# 打开权限页面
    91|adb shell content call \
    92|  --uri content://com.shijing.xomniclaw.selfcontrol/execute \
    93|  --method navigate_app \
    94|  --extra page:s:permissions
    95|
    96|# 打开对话历史（带参数）
    97|adb shell content call \
    98|  --uri content://com.shijing.xomniclaw.selfcontrol/execute \
    99|  --method navigate_app \
   100|  --extra page:s:chat_history \
   101|  --extra session_id:s:abc123
   102|```
   103|
   104|##### 1.2 配置管理
   105|
   106|```bash
   107|# 读取配置
   108|adb shell content call \
   109|  --uri content://com.shijing.xomniclaw.selfcontrol/execute \
   110|  --method manage_config \
   111|  --extra operation:s:get \
   112|  --extra key:s:exploration_mode
   113|
   114|# 设置配置（Boolean）
   115|adb shell content call \
   116|  --uri content://com.shijing.xomniclaw.selfcontrol/execute \
   117|  --method manage_config \
   118|  --extra operation:s:set \
   119|  --extra key:s:exploration_mode \
   120|  --extra value:s:true
   121|
   122|# 设置配置（Integer）
   123|adb shell content call \
   124|  --uri content://com.shijing.xomniclaw.selfcontrol/execute \
   125|  --method manage_config \
   126|  --extra operation:s:set \
   127|  --extra key:s:screenshot_delay \
   128|  --extra value:s:200
   129|
   130|# 列出分类配置
   131|adb shell content call \
   132|  --uri content://com.shijing.xomniclaw.selfcontrol/execute \
   133|  --method manage_config \
   134|  --extra operation:s:list \
   135|  --extra category:s:feature
   136|
   137|# 删除配置
   138|adb shell content call \
   139|  --uri content://com.shijing.xomniclaw.selfcontrol/execute \
   140|  --method manage_config \
   141|  --extra operation:s:delete \
   142|  --extra key:s:test_config
   143|```
   144|
   145|##### 1.3 服务控制
   146|
   147|```bash
   148|# 检查服务状态
   149|adb shell content call \
   150|  --uri content://com.shijing.xomniclaw.selfcontrol/execute \
   151|  --method control_service \
   152|  --extra operation:s:check_status
   153|
   154|# 隐藏悬浮窗
   155|adb shell content call \
   156|  --uri content://com.shijing.xomniclaw.selfcontrol/execute \
   157|  --method control_service \
   158|  --extra operation:s:hide_float
   159|
   160|# 显示悬浮窗（立即）
   161|adb shell content call \
   162|  --uri content://com.shijing.xomniclaw.selfcontrol/execute \
   163|  --method control_service \
   164|  --extra operation:s:show_float
   165|
   166|# 显示悬浮窗（延迟 500ms）
   167|adb shell content call \
   168|  --uri content://com.shijing.xomniclaw.selfcontrol/execute \
   169|  --method control_service \
   170|  --extra operation:s:show_float \
   171|  --extra delay_ms:i:500
   172|
   173|# 启动悬浮窗服务
   174|adb shell content call \
   175|  --uri content://com.shijing.xomniclaw.selfcontrol/execute \
   176|  --method control_service \
   177|  --extra operation:s:start_float
   178|
   179|# 停止悬浮窗服务
   180|adb shell content call \
   181|  --uri content://com.shijing.xomniclaw.selfcontrol/execute \
   182|  --method control_service \
   183|  --extra operation:s:stop_float
   184|```
   185|
   186|##### 1.4 日志查询
   187|
   188|```bash
   189|# 查询错误日志（最近 50 行）
   190|adb shell content call \
   191|  --uri content://com.shijing.xomniclaw.selfcontrol/execute \
   192|  --method query_logs \
   193|  --extra level:s:E \
   194|  --extra lines:i:50
   195|
   196|# 查询调试日志（过滤 AgentLoop）
   197|adb shell content call \
   198|  --uri content://com.shijing.xomniclaw.selfcontrol/execute \
   199|  --method query_logs \
   200|  --extra level:s:D \
   201|  --extra filter:s:AgentLoop \
   202|  --extra lines:i:100
   203|
   204|# 查询所有日志（Verbose，最近 200 行）
   205|adb shell content call \
   206|  --uri content://com.shijing.xomniclaw.selfcontrol/execute \
   207|  --method query_logs \
   208|  --extra level:s:V \
   209|  --extra lines:i:200
   210|
   211|# 查询文件日志
   212|adb shell content call \
   213|  --uri content://com.shijing.xomniclaw.selfcontrol/execute \
   214|  --method query_logs \
   215|  --extra level:s:I \
   216|  --extra source:s:file \
   217|  --extra lines:i:100
   218|```
   219|
   220|##### 1.5 列出 Skills 和健康检查
   221|
   222|```bash
   223|# 列出所有 Skills
   224|adb shell content call \
   225|  --uri content://com.shijing.xomniclaw.selfcontrol/execute \
   226|  --method list_skills
   227|
   228|# 健康检查
   229|adb shell content call \
   230|  --uri content://com.shijing.xomniclaw.selfcontrol/execute \
   231|  --method health
   232|```
   233|
   234|---
   235|
   236|### 2. Broadcast 方式
   237|
   238|#### 基本语法
   239|
   240|```bash
   241|adb shell am broadcast \
   242|  -a com.shijing.xomniclaw.SELF_CONTROL \
   243|  --es skill SKILL_NAME \
   244|  --es/ei/el/ez/ef/ed key value
   245|```
   246|
   247|#### 参数类型
   248|
   249|| 标记 | 类型 | 示例 |
   250||------|------|------|
   251|| `--es` | String | `--es page config` |
   252|| `--ei` | Integer | `--ei lines 50` |
   253|| `--el` | Long | `--el timestamp 1234567890` |
   254|| `--ez` | Boolean | `--ez enabled true` |
   255|| `--ef` | Float | `--ef ratio 3.14` |
   256|| `--ed` | Double | `--ed value 3.14159` |
   257|
   258|#### 示例
   259|
   260|##### 2.1 页面导航
   261|
   262|```bash
   263|# 打开配置页面
   264|adb shell am broadcast \
   265|  -a com.shijing.xomniclaw.SELF_CONTROL \
   266|  --es skill navigate_app \
   267|  --es page config
   268|
   269|# 打开权限页面
   270|adb shell am broadcast \
   271|  -a com.shijing.xomniclaw.SELF_CONTROL \
   272|  --es skill navigate_app \
   273|  --es page permissions
   274|```
   275|
   276|##### 2.2 配置管理
   277|
   278|```bash
   279|# 读取配置
   280|adb shell am broadcast \
   281|  -a com.shijing.xomniclaw.SELF_CONTROL \
   282|  --es skill manage_config \
   283|  --es operation get \
   284|  --es key exploration_mode
   285|
   286|# 设置配置
   287|adb shell am broadcast \
   288|  -a com.shijing.xomniclaw.SELF_CONTROL \
   289|  --es skill manage_config \
   290|  --es operation set \
   291|  --es key exploration_mode \
   292|  --es value true
   293|
   294|# 列出配置
   295|adb shell am broadcast \
   296|  -a com.shijing.xomniclaw.SELF_CONTROL \
   297|  --es skill manage_config \
   298|  --es operation list \
   299|  --es category feature
   300|```
   301|
   302|##### 2.3 服务控制
   303|
   304|```bash
   305|# 隐藏悬浮窗
   306|adb shell am broadcast \
   307|  -a com.shijing.xomniclaw.SELF_CONTROL \
   308|  --es skill control_service \
   309|  --es operation hide_float
   310|
   311|# 显示悬浮窗（延迟）
   312|adb shell am broadcast \
   313|  -a com.shijing.xomniclaw.SELF_CONTROL \
   314|  --es skill control_service \
   315|  --es operation show_float \
   316|  --ei delay_ms 500
   317|```
   318|
   319|##### 2.4 日志查询
   320|
   321|```bash
   322|# 查询错误日志
   323|adb shell am broadcast \
   324|  -a com.shijing.xomniclaw.SELF_CONTROL \
   325|  --es skill query_logs \
   326|  --es level E \
   327|  --ei lines 50
   328|```
   329|
   330|##### 2.5 查看结果
   331|
   332|Broadcast 方式是异步的，结果通过 logcat 输出：
   333|
   334|```bash
   335|# 实时查看日志
   336|adb logcat | grep SelfControlReceiver
   337|
   338|# 查看最近的日志
   339|adb logcat -d -t 50 | grep SelfControlReceiver
   340|```
   341|
   342|---
   343|
   344|### 3. Shell 脚本方式
   345|
   346|#### 安装
   347|
   348|```bash
   349|# 下载脚本
   350|curl -o self-control-adb.sh https://raw.githubusercontent.com/.../self-control-adb.sh
   351|
   352|# 或从项目中复制
   353|cp self-control/self-control-adb.sh .
   354|
   355|# 添加执行权限
   356|chmod +x self-control-adb.sh
   357|```
   358|
   359|#### 使用
   360|
   361|```bash
   362|# 查看帮助
   363|./self-control-adb.sh help
   364|
   365|# 列出 Skills
   366|./self-control-adb.sh list
   367|
   368|# 健康检查
   369|./self-control-adb.sh health
   370|
   371|# 执行 Skill（默认使用 ContentProvider）
   372|./self-control-adb.sh SKILL_NAME key1=value1 key2=value2
   373|
   374|# 指定使用 Broadcast 方式
   375|./self-control-adb.sh --method=broadcast SKILL_NAME key1=value1
   376|```
   377|
   378|#### 示例
   379|
   380|```bash
   381|# 页面导航
   382|./self-control-adb.sh navigate_app page=config
   383|
   384|# 配置管理
   385|./self-control-adb.sh manage_config operation=get key=exploration_mode
   386|./self-control-adb.sh manage_config operation=set key=exploration_mode value=true
   387|
   388|# 服务控制
   389|./self-control-adb.sh control_service operation=hide_float
   390|./self-control-adb.sh control_service operation=show_float delay_ms:i=500
   391|
   392|# 日志查询
   393|./self-control-adb.sh query_logs level=E lines:i=50
   394|```
   395|
   396|---
   397|
   398|## 🔄 完整工作流示例
   399|
   400|### 示例 1: 截图前后处理
   401|
   402|```bash
   403|#!/bin/bash
   404|# 截图前隐藏悬浮窗，截图后显示
   405|
   406|# 1. 隐藏悬浮窗
   407|./self-control-adb.sh control_service operation=hide_float
   408|
   409|# 2. 等待 UI 更新
   410|sleep 0.2
   411|
   412|# 3. 执行截图（假设有截图命令）
   413|adb shell screencap -p /sdcard/screenshot.png
   414|
   415|# 4. 延迟显示悬浮窗
   416|./self-control-adb.sh control_service operation=show_float delay_ms:i=500
   417|
   418|echo "Screenshot saved to /sdcard/screenshot.png"
   419|```
   420|
   421|### 示例 2: 配置备份和恢复
   422|
   423|```bash
   424|#!/bin/bash
   425|# 备份和恢复配置
   426|
   427|# 备份配置
   428|echo "Backing up configurations..."
   429|./self-control-adb.sh manage_config operation=list category=feature > config_backup.txt
   430|
   431|# 修改配置
   432|echo "Modifying configuration..."
   433|./self-control-adb.sh manage_config operation=set key=exploration_mode value=true
   434|
   435|# 验证修改
   436|echo "Verifying..."
   437|./self-control-adb.sh manage_config operation=get key=exploration_mode
   438|
   439|# 恢复配置（手动解析 config_backup.txt）
   440|echo "Configuration backed up to config_backup.txt"
   441|```
   442|
   443|### 示例 3: 自动诊断
   444|
   445|```bash
   446|#!/bin/bash
   447|# 自动检查应用状态并诊断问题
   448|
   449|echo "=== Self-Control Health Check ==="
   450|./self-control-adb.sh health
   451|
   452|echo -e "\n=== Service Status ==="
   453|./self-control-adb.sh control_service operation=check_status
   454|
   455|echo -e "\n=== Recent Errors ==="
   456|./self-control-adb.sh query_logs level=E lines:i=20
   457|
   458|echo -e "\n=== Current Configuration ==="
   459|./self-control-adb.sh manage_config operation=list category=feature
   460|```
   461|
   462|### 示例 4: CI/CD 集成
   463|
   464|```yaml
   465|# .github/workflows/test.yml
   466|name: Android Test
   467|
   468|on: [push, pull_request]
   469|
   470|jobs:
   471|  test:
   472|    runs-on: ubuntu-latest
   473|    steps:
   474|      - uses: actions/checkout@v2
   475|
   476|      - name: Start Android Emulator
   477|        # ... 启动模拟器
   478|
   479|      - name: Install APK
   480|        run: adb install -r app-debug.apk
   481|
   482|      - name: Health Check
   483|        run: |
   484|          ./self-control-adb.sh health
   485|          if [ $? -ne 0 ]; then
   486|            echo "Health check failed"
   487|            exit 1
   488|          fi
   489|
   490|      - name: Configure App
   491|        run: |
   492|          ./self-control-adb.sh manage_config operation=set key=test_mode value=true
   493|
   494|      - name: Run Tests
   495|        run: |
   496|          # 执行测试...
   497|
   498|      - name: Collect Logs
   499|        if: failure()
   500|        run: |
   501|

Use offset=501 to continue reading (showing 1-500 of 626 lines)


宝藏！让我再看看它的 App 自动化技能：

Explanation: 这是 OPPO 的 X-OmniClaw 项目，一个手机自动化 AI Agent。让我继续看看它的核心技能和实现方式。


Output:
app-search
capcut-theme-video
channel-config
clipboard-to-shortcut
gallery-memory
gallery-qa
memory-evolution
model-config
scheduled-automation
skill-creator
taobao-search