Merge remote-tracking branch 'origin/feature/sync-20260430' into feat/file-ops
This commit is contained in:
@@ -219,8 +219,20 @@ async def _captioning_chain(
|
||||
logger.warning("vision_fallback failed; retrying configured model")
|
||||
if result := await caption_tool_image(intent, image_content):
|
||||
return result
|
||||
logger.warning("vision_fallback retry failed; trying gemini-3-flash-preview")
|
||||
return await caption_tool_image(intent, image_content, model_override="gemini/gemini-3-flash-preview")
|
||||
# Match the configured model's proxy prefix so the override is routed
|
||||
# through the same endpoint with the same auth shape. Without this,
|
||||
# a Hive subscriber's `hive/...` config would override to
|
||||
# `gemini/...` — which sends Google's Gemini protocol to the
|
||||
# Anthropic-compatible Hive proxy (404), not what we want.
|
||||
configured = (get_vision_fallback_model() or "").lower()
|
||||
if configured.startswith("hive/"):
|
||||
override = "hive/gemini-3-flash-preview"
|
||||
elif configured.startswith("kimi/"):
|
||||
override = "kimi/gemini-3-flash-preview"
|
||||
else:
|
||||
override = "gemini/gemini-3-flash-preview"
|
||||
logger.warning("vision_fallback retry failed; trying %s", override)
|
||||
return await caption_tool_image(intent, image_content, model_override=override)
|
||||
|
||||
|
||||
# Pattern for detecting context-window-exceeded errors across LLM providers.
|
||||
|
||||
@@ -211,10 +211,12 @@ async def caption_tool_image(
|
||||
"max_tokens": 8192,
|
||||
"timeout": timeout_s,
|
||||
}
|
||||
# Pass api_key directly only when there are no proxy-rewritten
|
||||
# extra_headers carrying the auth (e.g. the gemini-3-flash override
|
||||
# path goes direct to Gemini, not through the Hive proxy).
|
||||
if api_key and not extra_headers:
|
||||
# Always pass api_key when we have one, even alongside proxy-rewritten
|
||||
# extra_headers. litellm's anthropic handler refuses to dispatch
|
||||
# without an api_key (it sends it as x-api-key); the proxy itself
|
||||
# authenticates via the Authorization: Bearer header in
|
||||
# extra_headers. Both are needed — matches LiteLLMProvider's path.
|
||||
if api_key:
|
||||
kwargs["api_key"] = api_key
|
||||
if rewritten_base:
|
||||
kwargs["api_base"] = rewritten_base
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"include": ["gcu-tools", "hive_tools"]
|
||||
"include": ["gcu-tools", "hive_tools", "terminal-tools"]
|
||||
}
|
||||
|
||||
@@ -51,10 +51,14 @@ _TOOL_CATEGORIES: dict[str, list[str]] = {
|
||||
"hashline_edit",
|
||||
],
|
||||
# Shell + process control — engineering personas only.
|
||||
# Includes the legacy coder-tools commands (run_command, bash_*) and
|
||||
# the full terminal-tools MCP server (foreground exec with auto-promotion,
|
||||
# background jobs, persistent PTY sessions, ripgrep/find).
|
||||
"shell": [
|
||||
"execute_command_tool",
|
||||
"bash_kill",
|
||||
"bash_output",
|
||||
"@server:terminal-tools",
|
||||
],
|
||||
# Tabular data. CSV/Excel read/write + DuckDB SQL.
|
||||
"data": [
|
||||
|
||||
@@ -51,6 +51,10 @@ _DEFAULT_LOCAL_SERVERS: dict[str, dict[str, Any]] = {
|
||||
"description": "File I/O: read, write, edit, search, list, run commands",
|
||||
"args": ["run", "python", "files_server.py", "--stdio"],
|
||||
},
|
||||
"terminal-tools": {
|
||||
"description": "Terminal capabilities: process exec, background jobs, PTY sessions, fs search. Bash-only on POSIX.",
|
||||
"args": ["run", "python", "terminal_tools_server.py", "--stdio"],
|
||||
},
|
||||
}
|
||||
|
||||
# Aliases that earlier versions of ensure_defaults wrote under the wrong name.
|
||||
@@ -58,6 +62,10 @@ _DEFAULT_LOCAL_SERVERS: dict[str, dict[str, Any]] = {
|
||||
# name so the active agents (queen, credential_tester) can find their tools.
|
||||
_STALE_DEFAULT_ALIASES: dict[str, str] = {
|
||||
"hive_tools": "hive-tools",
|
||||
# 2026-04-30: shell-tools renamed to terminal-tools. Drop the stale name
|
||||
# on next ensure_defaults() so the queen's allowlist (which now includes
|
||||
# @server:terminal-tools) actually finds a server with the new name.
|
||||
"terminal-tools": "shell-tools",
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +85,30 @@ class MCPRegistry:
|
||||
# ── Initialization ──────────────────────────────────────────────
|
||||
|
||||
def initialize(self) -> None:
|
||||
"""Create directory structure and default files if missing."""
|
||||
"""Create directory structure, default files, and seed bundled servers.
|
||||
|
||||
Every read path (queen orchestrator, pipeline stage, CLI, routes)
|
||||
calls this — keeping the seeding here means a fresh ``HIVE_HOME``
|
||||
(e.g. the desktop's per-user dir under ``~/.config/Hive/users/<hash>/``
|
||||
or ``~/Library/Application Support/Hive/users/<hash>/``) is always
|
||||
populated with ``hive_tools`` / ``gcu-tools`` / ``files-tools`` /
|
||||
``shell-tools`` before any agent code reads ``installed.json``.
|
||||
Without this, ``load_agent_selection()`` resolves an empty registry
|
||||
and emits "Server X requested but not installed" warnings even
|
||||
though the server is bundled.
|
||||
|
||||
Idempotent — already-installed entries are left untouched.
|
||||
"""
|
||||
self._bootstrap_io()
|
||||
self._seed_defaults()
|
||||
|
||||
def _bootstrap_io(self) -> None:
|
||||
"""Create the registry directory + empty config/installed files.
|
||||
|
||||
Split out from ``initialize()`` so ``_seed_defaults()`` can call it
|
||||
without re-entering the seeding logic (which would recurse via
|
||||
``_read_installed()`` → ``initialize()``).
|
||||
"""
|
||||
self._base.mkdir(parents=True, exist_ok=True)
|
||||
self._cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -88,21 +119,26 @@ class MCPRegistry:
|
||||
self._write_json(self._installed_path, {"servers": {}})
|
||||
|
||||
def ensure_defaults(self) -> list[str]:
|
||||
"""Seed the built-in local MCP servers (hive-tools, gcu-tools, files-tools).
|
||||
"""Public alias kept for the ``hive mcp init`` CLI command.
|
||||
|
||||
Idempotent — servers already present are left untouched. Skips seeding
|
||||
entirely when the source-tree ``tools/`` directory cannot be located
|
||||
(e.g. when Hive is installed from a wheel rather than a checkout).
|
||||
|
||||
Returns the list of names that were newly registered.
|
||||
Returns the list of newly-registered server names so the CLI can
|
||||
print them. Same idempotent seeding logic as ``initialize()``.
|
||||
"""
|
||||
self.initialize()
|
||||
self._bootstrap_io()
|
||||
return self._seed_defaults()
|
||||
|
||||
def _seed_defaults(self) -> list[str]:
|
||||
"""Idempotently register the bundled default local servers.
|
||||
|
||||
Skips entirely when the source-tree ``tools/`` directory cannot
|
||||
be located (e.g. wheel installs). Returns the list of names that
|
||||
were newly registered.
|
||||
"""
|
||||
# parents: [0]=loader, [1]=framework, [2]=core, [3]=repo root
|
||||
tools_dir = Path(__file__).resolve().parents[3] / "tools"
|
||||
if not tools_dir.is_dir():
|
||||
logger.debug(
|
||||
"MCPRegistry.ensure_defaults: tools dir %s missing; skipping default seed",
|
||||
"MCPRegistry._seed_defaults: tools dir %s missing; skipping default seed",
|
||||
tools_dir,
|
||||
)
|
||||
return []
|
||||
@@ -119,7 +155,7 @@ class MCPRegistry:
|
||||
for canonical, stale in _STALE_DEFAULT_ALIASES.items():
|
||||
if stale in existing and canonical not in existing:
|
||||
logger.info(
|
||||
"MCPRegistry.ensure_defaults: removing stale alias '%s' (canonical: '%s')",
|
||||
"MCPRegistry._seed_defaults: removing stale alias '%s' (canonical: '%s')",
|
||||
stale,
|
||||
canonical,
|
||||
)
|
||||
@@ -142,7 +178,7 @@ class MCPRegistry:
|
||||
)
|
||||
added.append(name)
|
||||
except MCPError as exc:
|
||||
logger.warning("MCPRegistry.ensure_defaults: failed to seed '%s': %s", name, exc)
|
||||
logger.warning("MCPRegistry._seed_defaults: failed to seed '%s': %s", name, exc)
|
||||
|
||||
if added:
|
||||
logger.info("MCPRegistry: seeded default local servers: %s", added)
|
||||
|
||||
@@ -44,6 +44,9 @@ class McpRegistryStage(PipelineStage):
|
||||
from framework.loader.mcp_registry import MCPRegistry
|
||||
from framework.orchestrator.files import FILES_MCP_SERVER_NAME
|
||||
|
||||
# Bundled defaults (hive_tools / gcu-tools / files-tools / shell-tools)
|
||||
# are seeded inside MCPRegistry.initialize(); resolve_for_agent below
|
||||
# will find them even on a fresh HIVE_HOME.
|
||||
registry = MCPRegistry()
|
||||
mcp_loaded = False
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
---
|
||||
name: hive.terminal-tools-foundations
|
||||
description: Required reading whenever any shell_* tool is available. Teaches the foreground/background dichotomy (terminal_exec auto-promotes past 30s, returns a job_id you poll with terminal_job_logs), the standard envelope shape (exit_code, stdout, stdout_truncated_bytes, output_handle, semantic_status, warning, auto_backgrounded, job_id), output handle pagination via terminal_output_get, when to read semantic_status instead of raw exit_code (grep/rg/find/diff/test exit 1 is NOT an error), the destructive-warning surface (rm -rf, git push --force, DROP TABLE), tool preference (use files-tools / gcu-tools / hive_tools before raw shell), and the bash-only-on-macOS policy. Skipping this leads to "tool returned no output" surprises, orphaned jobs, and panic over benign grep exit codes.
|
||||
metadata:
|
||||
author: hive
|
||||
type: preset-skill
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# terminal-tools — foundations
|
||||
|
||||
These tools give you a real terminal: foreground exec with smart envelopes, background jobs with offset-based log streaming, persistent PTY shells, and filesystem search. Bash-only on POSIX.
|
||||
|
||||
## Tool preference (read first)
|
||||
|
||||
Before reaching for terminal-tools, check whether a higher-level tool already covers the task. Shell is for system operations the other servers don't reach.
|
||||
|
||||
- **Reading files** → `files-tools.read_file` (handles size, paging, line-numbered output) — NOT `terminal_exec("cat ...")`
|
||||
- **Editing files** → `files-tools.edit_file` (atomic patch with diff verification) — NOT `terminal_exec("sed -i ...")`
|
||||
- **Writing files** → `files-tools.write_file` — NOT `terminal_exec("echo > ...")`
|
||||
- **In-project search** → `files-tools.search_files` (project-scoped, code-aware) — use `terminal_rg` only for raw paths outside the project (`/var/log`, `/etc`)
|
||||
- **Browser / web pages** → `gcu-tools.browser_*` for rendered pages — NOT `terminal_exec("curl ...")`
|
||||
- **Web search** → `hive_tools.web_search` — NOT scraping
|
||||
- **System operations** (process exec, jobs, PTYs, raw fs search) → terminal-tools. This is its territory.
|
||||
|
||||
## The standard envelope
|
||||
|
||||
Every spawn-style call (`terminal_exec`, the auto-promoted job state) returns this shape:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"exit_code": 0, // null when auto-backgrounded or pre-spawn error
|
||||
"stdout": "...", // decoded, truncated to max_output_kb (default 256 KB)
|
||||
"stderr": "...",
|
||||
"stdout_truncated_bytes": 0, // > 0 means more is in output_handle
|
||||
"stderr_truncated_bytes": 0,
|
||||
"runtime_ms": 42,
|
||||
"pid": 12345,
|
||||
"output_handle": null, // "out_<hex>" when truncated — paginate with terminal_output_get
|
||||
"timed_out": false,
|
||||
"semantic_status": "ok", // "ok" | "signal" | "error" — read THIS, not just exit_code
|
||||
"semantic_message": null, // e.g. "No matches found" for grep exit 1
|
||||
"warning": null, // e.g. "may force-remove files" for rm -rf
|
||||
"auto_backgrounded": false,
|
||||
"job_id": null // set when auto_backgrounded=true
|
||||
}
|
||||
```
|
||||
|
||||
## Auto-promotion (the core mental model)
|
||||
|
||||
`terminal_exec` runs commands in the foreground until the **auto-background budget** (default 30s) elapses. Past that point, the process is silently transferred to a background job and the call returns immediately with:
|
||||
|
||||
```jsonc
|
||||
{ "auto_backgrounded": true, "exit_code": null, "job_id": "job_<hex>", ... }
|
||||
```
|
||||
|
||||
When you see `auto_backgrounded: true`, **pivot to polling**. The job is still running:
|
||||
|
||||
```
|
||||
terminal_job_logs(job_id, since_offset=0, wait_until_exit=true, wait_timeout_sec=60)
|
||||
→ blocks server-side until the job exits or the timeout, returns logs + status
|
||||
```
|
||||
|
||||
You're not failing — you're freed up to do other work while the long task runs.
|
||||
|
||||
To force pure-foreground (kill on `timeout_sec`), pass `auto_background_after_sec=0`. Use this when you genuinely don't want a background job (small commands where promotion would surprise you).
|
||||
|
||||
## Semantic exit codes — read `semantic_status`, not raw `exit_code`
|
||||
|
||||
Several common commands use exit 1 for legitimate non-error states:
|
||||
|
||||
| Command | exit 0 | exit 1 |
|
||||
|---|---|---|
|
||||
| `grep` / `rg` | matches found | **no matches** (not an error) |
|
||||
| `find` | success | **some dirs unreadable** (informational) |
|
||||
| `diff` | identical | **files differ** (informational) |
|
||||
| `test` / `[` | true | **false** (informational) |
|
||||
|
||||
For these, `semantic_status` will be `"ok"` even when `exit_code == 1`, with `semantic_message` describing why ("No matches found"). For everything else, `semantic_status` defaults to `"ok"` on 0 and `"error"` on nonzero.
|
||||
|
||||
**Rule**: always check `semantic_status` first. Only fall back to `exit_code` when you need the exact number (e.g. distinguishing `make` errors).
|
||||
|
||||
## Destructive warnings — re-read your command
|
||||
|
||||
The envelope's `warning` field is set when the command matches a known destructive pattern (`rm -rf`, `git push --force`, `git reset --hard`, `DROP TABLE`, `kubectl delete`, `terraform destroy`, etc.). The command **still ran** — the warning is informational. Use it as a "did I mean to do that?" prompt before trusting subsequent steps that depend on the side effect.
|
||||
|
||||
If a `warning` appears unexpectedly, stop and verify: was the destructive action intended, or did a path/glob slip in?
|
||||
|
||||
## Output handles — never lose output
|
||||
|
||||
When `stdout_truncated_bytes > 0` or `stderr_truncated_bytes > 0`, the inline output was capped at `max_output_kb` (default 256 KB). The full bytes are stashed under `output_handle` for **5 minutes**. Paginate with:
|
||||
|
||||
```
|
||||
terminal_output_get(output_handle, since_offset=0, max_kb=64)
|
||||
→ { data, offset, next_offset, eof, expired }
|
||||
```
|
||||
|
||||
Track `next_offset` across calls. If `expired: true`, re-run the command (the handle's TTL has lapsed).
|
||||
|
||||
The store has a 64 MB cap with LRU eviction. For huge outputs, prefer `terminal_job_start` + `terminal_job_logs` polling (4 MB ring buffer per stream, infinite total throughput).
|
||||
|
||||
## Bash, not zsh — even on macOS
|
||||
|
||||
`terminal_exec` and `terminal_pty_open` always invoke `/bin/bash`. The user's `$SHELL` is ignored. Explicit `shell="/bin/zsh"` is **rejected** with a clear error. This is a deliberate security stance, not aesthetic — zsh has command/builtin classes (`zmodload`, `=cmd` expansion, `zpty`, `ztcp`, `zf_*`) that bypass bash-shaped checks. The `terminal-tools-pty-sessions` skill explains the implications for PTY sessions specifically.
|
||||
|
||||
`ZDOTDIR` and `ZSH_*` env vars are stripped before exec to prevent zsh dotfiles leaking in. Bash dotfiles still apply when invoked interactively (e.g. PTY sessions use `bash --norc --noprofile` to keep things predictable).
|
||||
|
||||
## Pipelines and complex commands
|
||||
|
||||
Pipes (`|`), redirects (`>`, `<`, `>>`), conditionals (`&&`, `||`, `;`), and globs (`*`, `?`, `[`) are detected automatically. You can pass them with the default `shell=False` and the runtime will transparently route through `/bin/bash -c` and surface `auto_shell: true` in the envelope:
|
||||
|
||||
```
|
||||
terminal_exec("ps aux | sort -k3 -rn | head -40")
|
||||
→ { exit_code: 0, stdout: "...", auto_shell: true, ... }
|
||||
```
|
||||
|
||||
For simple argv commands (no metacharacters) `shell=False` is faster and direct-execs the binary. For commands with shell features but no metacharacters that the detector catches (rare — exotic bash builtins, here-strings), pass `shell=True` explicitly:
|
||||
|
||||
```
|
||||
terminal_exec("set -e; complicated bash logic", shell=True)
|
||||
```
|
||||
|
||||
Quoted strings work either way — the detector uses `shlex.split` which handles `"quoted args with spaces"` correctly.
|
||||
|
||||
## When to use what (cheat sheet)
|
||||
|
||||
| Need | Tool |
|
||||
|---|---|
|
||||
| One-shot command, ≤30s | `terminal_exec` |
|
||||
| One-shot command, might be longer | `terminal_exec` (auto-promotes) |
|
||||
| Long-running job from the start | `terminal_job_start` |
|
||||
| State across calls (cd, env, REPL) | `terminal_pty_open` + `terminal_pty_run` |
|
||||
| Search file contents (raw paths) | `terminal_rg` |
|
||||
| Find files by predicate | `terminal_find` |
|
||||
| Retrieve truncated output | `terminal_output_get` |
|
||||
| Tree / stat / du | `terminal_exec("ls -la"/"stat foo"/"du -sh path")` |
|
||||
| HTTP / DNS / ping / archives | `terminal_exec("curl ..."/"dig ..."/"tar xzf ...")` |
|
||||
|
||||
See `references/exit_codes.md` for the full POSIX + signal-induced + semantic catalog.
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
# Exit code reference
|
||||
|
||||
## POSIX conventions
|
||||
|
||||
| Code | Meaning |
|
||||
|---|---|
|
||||
| 0 | Success |
|
||||
| 1 | General error / catchall |
|
||||
| 2 | Misuse of shell builtins, syntax error |
|
||||
| 126 | Command found but not executable |
|
||||
| 127 | Command not found |
|
||||
| 128 | Invalid argument to `exit` |
|
||||
| 128 + N | Killed by signal N |
|
||||
| 130 | Killed by SIGINT (Ctrl-C) |
|
||||
| 137 | Killed by SIGKILL |
|
||||
| 143 | Killed by SIGTERM |
|
||||
| 255 | Exit status out of range |
|
||||
|
||||
When `exit_code < 0` in the envelope, the process was killed by a signal: `abs(exit_code)` is the signal number (subprocess uses negative codes for signaled exits, separate from the `128 + N` shell convention).
|
||||
|
||||
## Semantic exits — when exit 1 is NOT an error
|
||||
|
||||
terminal-tools encodes these in `semantic_status`. The agent should read `semantic_status` first.
|
||||
|
||||
| Command | Code 0 | Code 1 | Code ≥2 |
|
||||
|---|---|---|---|
|
||||
| `grep` / `rg` / `ripgrep` | matches found | **no matches** (ok) | error |
|
||||
| `find` | success | **some dirs unreadable** (ok) | error |
|
||||
| `diff` | files identical | **files differ** (ok) | error |
|
||||
| `test` / `[` | condition true | **condition false** (ok) | error |
|
||||
|
||||
For any command not in this table, the default convention applies (0 = ok, nonzero = error).
|
||||
|
||||
## When `exit_code` is `null`
|
||||
|
||||
- `auto_backgrounded: true` — the process is still running under a `job_id`. Poll with `terminal_job_logs`.
|
||||
- Pre-spawn error (command not found, exec failed) — see `error` field in the envelope.
|
||||
- `timed_out: true` and the process refused to die — extremely rare; the kernel has the answer.
|
||||
|
||||
## Common signal-induced exits
|
||||
|
||||
| Signal | Number | Subprocess exit | Shell exit | Meaning |
|
||||
|---|---|---|---|---|
|
||||
| SIGHUP | 1 | -1 | 129 | Terminal hangup |
|
||||
| SIGINT | 2 | -2 | 130 | Interrupt (Ctrl-C) |
|
||||
| SIGQUIT | 3 | -3 | 131 | Quit (Ctrl-\\) |
|
||||
| SIGKILL | 9 | -9 | 137 | Forced kill (uncatchable) |
|
||||
| SIGTERM | 15 | -15 | 143 | Polite termination |
|
||||
| SIGSEGV | 11 | -11 | 139 | Segmentation fault |
|
||||
| SIGABRT | 6 | -6 | 134 | Abort (assertion failed, etc.) |
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
name: hive.terminal-tools-fs-search
|
||||
description: Use terminal_rg / terminal_find when you need raw filesystem search outside the project tree — system configs, /var/log, /etc, archive contents — or when files-tools.search_files is too project-scoped. Teaches the rg vs find vs terminal_exec("ls/du/tree") split, common rg flag combos for code/logs/configs, find predicates for mtime/size/type queries, and the rule that for tree views or single-file stat info you should just use terminal_exec instead of inventing a tool. Read before reaching for raw shell to grep or find anything.
|
||||
metadata:
|
||||
author: hive
|
||||
type: preset-skill
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# Filesystem search
|
||||
|
||||
terminal-tools provides two structured search tools: `terminal_rg` (ripgrep for content) and `terminal_find` (find for predicates). Everything else (tree, stat, du) is just `terminal_exec`.
|
||||
|
||||
## When to use what
|
||||
|
||||
| Task | Tool |
|
||||
|---|---|
|
||||
| Find code/text matching a pattern in your **project** | `files-tools.search_files` (project-aware, ranks by relevance) |
|
||||
| Find code/text matching a pattern in `/var/log`, `/etc`, archives, system dirs | `terminal_rg` |
|
||||
| Find files matching name/glob/predicate | `terminal_find` |
|
||||
| List a directory | `terminal_exec("ls -la /path")` |
|
||||
| Tree view | `terminal_exec("tree -L 2 /path")` |
|
||||
| Single-path stat | `terminal_exec("stat /path")` |
|
||||
| Disk usage | `terminal_exec("du -sh /path")` or `terminal_exec("du -h --max-depth=2 /")` |
|
||||
| Count matches across files | `terminal_rg(pattern, count=True via extra_args=["-c"])` |
|
||||
|
||||
## `terminal_rg` — content search
|
||||
|
||||
ripgrep is fast, gitignore-aware, and has a deep flag surface. The structured wrapper exposes the most useful flags directly; `extra_args` covers the rest.
|
||||
|
||||
### Common patterns
|
||||
|
||||
```
|
||||
# All Python files containing "TODO"
|
||||
terminal_rg(pattern="TODO", path=".", type_filter="py")
|
||||
|
||||
# Case-insensitive, with context
|
||||
terminal_rg(pattern="error", path="/var/log", ignore_case=True, context=2)
|
||||
|
||||
# Search hidden files (rg ignores them by default)
|
||||
terminal_rg(pattern="api_key", path="~", hidden=True)
|
||||
|
||||
# Don't respect .gitignore (find files git would ignore)
|
||||
terminal_rg(pattern="generated", path=".", no_ignore=True)
|
||||
|
||||
# Multi-line pattern (e.g., function definitions spanning lines)
|
||||
terminal_rg(pattern=r"def\s+\w+\(.*\n.*\n", path="src", extra_args=["--multiline"])
|
||||
|
||||
# Specific filename glob
|
||||
terminal_rg(pattern="version", path=".", glob="*.toml")
|
||||
```
|
||||
|
||||
### rg flag idioms
|
||||
|
||||
| Flag | Effect |
|
||||
|---|---|
|
||||
| `-tpy` (`type_filter="py"`) | Only Python files |
|
||||
| `-uu` | Don't respect any ignores (incl. `.git/`) |
|
||||
| `--multiline` (`extra_args`) | Allow regex spanning lines |
|
||||
| `--max-count` (`max_count`) | Stop after N matches per file |
|
||||
| `--max-depth` (`max_depth`) | Limit recursion |
|
||||
| `-w` (`extra_args`) | Whole word match |
|
||||
| `-F` (`extra_args`) | Fixed string (no regex) |
|
||||
|
||||
See `references/ripgrep_cheatsheet.md` for the long form.
|
||||
|
||||
## `terminal_find` — predicate search
|
||||
|
||||
`find` excels at "files matching N criteria". The wrapper surfaces the most common predicates; combine via the structured arguments.
|
||||
|
||||
```
|
||||
# All .log files modified in the last 7 days, larger than 1MB
|
||||
terminal_find(path="/var/log", iname="*.log", mtime_days=7, size_kb_min=1024)
|
||||
|
||||
# All directories named ".git" (find Git repos under a tree)
|
||||
terminal_find(path="~/projects", name=".git", type_filter="d")
|
||||
|
||||
# Only the top three levels
|
||||
terminal_find(path="/etc", max_depth=3, type_filter="f")
|
||||
|
||||
# Symlinks
|
||||
terminal_find(path=".", type_filter="l")
|
||||
```
|
||||
|
||||
See `references/find_predicates.md` for combinations not directly exposed.
|
||||
|
||||
## Output truncation
|
||||
|
||||
Both tools return `truncated: true` when their output exceeded the inline cap. For `terminal_rg`, this means matches were dropped (refine the pattern or narrow the path); for `terminal_find`, results past `max_results` (default 1000) are dropped. Tighten predicates rather than raising the cap.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- **Don't `terminal_rg` your project tree** — `files-tools.search_files` is project-aware and ranks results.
|
||||
- **Don't reach for `terminal_find` to list one directory** — `terminal_exec("ls -la /path")` is shorter.
|
||||
- **Don't use `terminal_exec("grep ...")`** when `terminal_rg` exists — rg is faster, gitignore-aware, and returns structured matches.
|
||||
- **Don't use `terminal_exec("find ...")`** to invent your own predicate combinations — use `terminal_find` and report missing capabilities.
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
# find predicate reference
|
||||
|
||||
The `terminal_find` wrapper exposes name/iname, type, mtime_days, size bounds, max_depth, max_results. For combinations beyond that, drop to `terminal_exec("find ...")`.
|
||||
|
||||
## Time predicates
|
||||
|
||||
| Need | find predicate |
|
||||
|---|---|
|
||||
| Modified within N days | `-mtime -N` (wrapper: `mtime_days=N`) |
|
||||
| Modified more than N days ago | `-mtime +N` |
|
||||
| Modified exactly N days ago | `-mtime N` |
|
||||
| Accessed within N days | `-atime -N` |
|
||||
| Inode changed within N days | `-ctime -N` |
|
||||
| Modified in last N minutes | `-mmin -N` |
|
||||
| Newer than reference file | `-newer ref` |
|
||||
|
||||
## Size predicates
|
||||
|
||||
| Need | find predicate |
|
||||
|---|---|
|
||||
| Bigger than N kilobytes | `-size +Nk` (wrapper: `size_kb_min`) |
|
||||
| Smaller than N kilobytes | `-size -Nk` (wrapper: `size_kb_max`) |
|
||||
| Exactly N kilobytes | `-size Nk` |
|
||||
| Bigger than N megabytes | `-size +NM` |
|
||||
| Empty files | `-empty` |
|
||||
|
||||
## Type predicates
|
||||
|
||||
| Need | find predicate |
|
||||
|---|---|
|
||||
| Regular file | `-type f` (wrapper: `type_filter="f"`) |
|
||||
| Directory | `-type d` (wrapper: `type_filter="d"`) |
|
||||
| Symlink | `-type l` (wrapper: `type_filter="l"`) |
|
||||
| Block device | `-type b` |
|
||||
| Character device | `-type c` |
|
||||
| FIFO | `-type p` |
|
||||
| Socket | `-type s` |
|
||||
|
||||
## Permission predicates
|
||||
|
||||
| Need | find predicate |
|
||||
|---|---|
|
||||
| Owned by user | `-user alice` |
|
||||
| Owned by group | `-group dev` |
|
||||
| Permission bits exact | `-perm 644` |
|
||||
| Has any of these bits | `-perm /u+x` |
|
||||
| Has all of these bits | `-perm -u+x` |
|
||||
| Readable by current user | `-readable` |
|
||||
| Writable | `-writable` |
|
||||
| Executable | `-executable` |
|
||||
|
||||
## Composing
|
||||
|
||||
`find` evaluates predicates left-to-right with implicit AND. For OR, use `\(`...\` or .
|
||||
|
||||
```
|
||||
# .log OR .txt (drop to terminal_exec for OR)
|
||||
terminal_exec(r"find /path \( -name '*.log' -o -name '*.txt' \) -type f", shell=True)
|
||||
|
||||
# NOT in a directory called node_modules
|
||||
terminal_exec("find . -path '*/node_modules' -prune -o -name '*.js' -print", shell=True)
|
||||
```
|
||||
|
||||
## Actions
|
||||
|
||||
| Need | predicate |
|
||||
|---|---|
|
||||
| Print path (default) | (implicit `-print`) |
|
||||
| Print null-separated | `-print0` (for piping to xargs -0) |
|
||||
| Delete | `-delete` (DANGEROUS — use terminal_exec with explicit confirmation) |
|
||||
| Run command per match | `-exec cmd {} \;` (drop to terminal_exec) |
|
||||
| Run command, batched | `-exec cmd {} +` |
|
||||
|
||||
## When NOT to use find
|
||||
|
||||
- **One directory listing**: `terminal_exec("ls -la /path")`
|
||||
- **Recursive grep**: `terminal_rg`
|
||||
- **Count files**: `terminal_exec("find /path -type f | wc -l")`
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
# ripgrep cheatsheet
|
||||
|
||||
For when the structured `terminal_rg` flags don't cover the case. Pass via `extra_args=[...]`.
|
||||
|
||||
## Filtering
|
||||
|
||||
| Need | Flag |
|
||||
|---|---|
|
||||
| Whole word | `-w` |
|
||||
| Fixed string (no regex) | `-F` |
|
||||
| Match files only (paths, not lines) | `-l` |
|
||||
| Count matches per file | `-c` |
|
||||
| Print only filenames with no matches | `--files-without-match` |
|
||||
| Exclude binary files | (default) |
|
||||
| Include binaries | `--binary` |
|
||||
| Search archives transparently | (rg doesn't — extract first) |
|
||||
|
||||
## Output shape
|
||||
|
||||
| Need | Flag |
|
||||
|---|---|
|
||||
| Show only matched part | `-o` |
|
||||
| Show byte offset of match | `-b` |
|
||||
| No filename prefix | `-N` (or pipe through awk) |
|
||||
| Color always (for piping into a colorizer) | `--color=always` |
|
||||
| JSON output | (the wrapper already uses `--json` internally) |
|
||||
|
||||
## Boundaries
|
||||
|
||||
| Need | Flag |
|
||||
|---|---|
|
||||
| Line-by-line (default) | (default) |
|
||||
| Multi-line regex | `--multiline` (or `-U`) |
|
||||
| Multi-line dotall (`.` matches `\n`) | `--multiline-dotall` |
|
||||
| Crlf line endings | `--crlf` |
|
||||
|
||||
## Path control
|
||||
|
||||
| Need | Flag |
|
||||
|---|---|
|
||||
| Follow symlinks | `-L` |
|
||||
| Don't follow | (default) |
|
||||
| Search hidden | `-.` (also expressed as `hidden=True`) |
|
||||
| Don't respect any ignores | `-uuu` |
|
||||
| Glob include | `-g 'pattern'` (also `glob="..."`) |
|
||||
| Glob exclude | `-g '!pattern'` |
|
||||
|
||||
## Performance
|
||||
|
||||
| Need | Flag |
|
||||
|---|---|
|
||||
| One thread | `-j 1` |
|
||||
| Smaller mmap chunks | `--mmap` (default behavior usually fine) |
|
||||
| Per-file match cap | `-m N` (also `max_count=N`) |
|
||||
|
||||
## Common composed queries
|
||||
|
||||
```
|
||||
# Find unused imports in Python
|
||||
terminal_rg(pattern=r"^import\s+\w+$", path="src", type_filter="py")
|
||||
|
||||
# All TODO/FIXME/XXX with file:line
|
||||
terminal_rg(pattern=r"\b(TODO|FIXME|XXX)\b", path=".", extra_args=["-n"])
|
||||
|
||||
# Functions defined at module top-level
|
||||
terminal_rg(pattern=r"^def\s+\w+", path=".", type_filter="py")
|
||||
|
||||
# Lines that DON'T match a pattern (filtered through awk)
|
||||
# rg can't invert at line level; use terminal_exec with grep -v
|
||||
```
|
||||
@@ -0,0 +1,110 @@
|
||||
---
|
||||
name: hive.terminal-tools-job-control
|
||||
description: Use when launching anything that runs longer than a minute, anything that streams logs, anything you want to keep running while doing other work — or when terminal_exec auto-backgrounded on you and returned a job_id. Teaches the start→poll→wait pattern with terminal_job_logs offset bookkeeping, the `wait_until_exit=True` blocking-poll idiom, the truncated_bytes_dropped resumption signal, the merge_stderr decision, the SIGINT→SIGTERM→SIGKILL escalation ladder via terminal_job_manage, and the hard rule that jobs die when the terminal-tools server restarts. Read before calling terminal_job_start, or right after terminal_exec auto-backgrounded.
|
||||
metadata:
|
||||
author: hive
|
||||
type: preset-skill
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# Background job control
|
||||
|
||||
Background jobs are how you do things that take time without blocking your conversation. Three tools cover the surface: `terminal_job_start`, `terminal_job_logs`, `terminal_job_manage`.
|
||||
|
||||
## When to use a job
|
||||
|
||||
- Builds, deploys, long tests
|
||||
- Processes you want to monitor (streaming a log file, a dev server)
|
||||
- Anything that auto-backgrounded from `terminal_exec` (you have a `job_id`; pivot to this skill's idioms)
|
||||
|
||||
For one-shot work expected to finish quickly, `terminal_exec` is simpler. The auto-promotion mechanic in `terminal_exec` is your safety net — start with `terminal_exec`, take over with this skill if needed.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
```
|
||||
terminal_job_start(command, ...)
|
||||
→ { job_id, pid, started_at }
|
||||
|
||||
terminal_job_logs(job_id, since_offset=0, max_bytes=64000)
|
||||
→ { data, offset, next_offset, status: "running"|"exited", exit_code, ... }
|
||||
|
||||
# Repeat with since_offset = previous next_offset until status == "exited"
|
||||
# Or block once with wait_until_exit=True:
|
||||
terminal_job_logs(job_id, since_offset=N, wait_until_exit=True, wait_timeout_sec=60)
|
||||
→ blocks server-side until exit or timeout
|
||||
```
|
||||
|
||||
After exit, the job is retained for inspection (`terminal_job_manage(action="list")`) until evicted by FIFO (50 most recent exits kept).
|
||||
|
||||
## Offset bookkeeping — the only rule that matters
|
||||
|
||||
The job's output lives in a 4 MB ring buffer per stream. Each call to `terminal_job_logs` returns:
|
||||
|
||||
- `data` — bytes between `since_offset` and `next_offset`
|
||||
- `next_offset` — pass this as `since_offset` on your next call
|
||||
- `truncated_bytes_dropped` — non-zero when your `since_offset` was older than the ring's floor (you fell behind)
|
||||
|
||||
**Always carry `next_offset` forward.** Don't replay from 0 — that's an offset reset, you'll see the same data twice and miss the part that fell off.
|
||||
|
||||
When `truncated_bytes_dropped > 0`, the buffer evicted N bytes between your last call and now. Treat it as a signal that the job is producing output faster than you're consuming. Either poll more often or accept the gap and read from `next_offset` going forward.
|
||||
|
||||
## merge_stderr — interleaved or separate
|
||||
|
||||
```
|
||||
merge_stderr=False → two streams, request "stdout" or "stderr" by name
|
||||
merge_stderr=True → one stream ("merged"), order preserved
|
||||
```
|
||||
|
||||
Pick `merge_stderr=True` when:
|
||||
- The job's logs are designed to be read together (most servers, build tools)
|
||||
- You don't need to distinguish "this was stderr"
|
||||
|
||||
Pick `merge_stderr=False` when:
|
||||
- stderr is genuinely error-only and stdout is data
|
||||
- You'll process them differently
|
||||
|
||||
## Signal escalation
|
||||
|
||||
```
|
||||
terminal_job_manage(action="signal_int", job_id=...) # graceful (Ctrl-C-equivalent)
|
||||
terminal_job_manage(action="signal_term", job_id=...) # polite kill (SIGTERM)
|
||||
terminal_job_manage(action="signal_kill", job_id=...) # forced kill (SIGKILL, uncatchable)
|
||||
```
|
||||
|
||||
The idiom: `signal_int` → wait 2-5s → `signal_term` → wait 2-5s → `signal_kill`. Most well-behaved processes handle SIGINT (graceful) and SIGTERM (cleanup, then exit). SIGKILL bypasses cleanup — use only when the process is truly unresponsive.
|
||||
|
||||
After signaling, check exit with `terminal_job_logs(job_id, wait_until_exit=True, wait_timeout_sec=2)`.
|
||||
|
||||
## Stdin
|
||||
|
||||
```
|
||||
terminal_job_manage(action="stdin", job_id=..., data="some input\n")
|
||||
terminal_job_manage(action="close_stdin", job_id=...)
|
||||
```
|
||||
|
||||
For tools that read stdin to EOF, `close_stdin` after writing flushes them. For interactive tools that read line-by-line, just write each line.
|
||||
|
||||
## Take-over: when terminal_exec auto-backgrounds
|
||||
|
||||
When `terminal_exec` returned `auto_backgrounded: true, job_id: <X>`, the process is **already** in the JobManager with its output flowing into the ring buffer. Your transition is seamless:
|
||||
|
||||
```
|
||||
# Already saw the start of output in terminal_exec's stdout/stderr.
|
||||
# Pick up reading where the env left off — use the byte count of the
|
||||
# initial stdout as your since_offset, OR just request tail output:
|
||||
terminal_job_logs(job_id="job_xxx", tail=True, max_bytes=64000)
|
||||
```
|
||||
|
||||
Or block until exit and grab everything:
|
||||
|
||||
```
|
||||
terminal_job_logs(job_id="job_xxx", since_offset=0, wait_until_exit=True, wait_timeout_sec=120)
|
||||
```
|
||||
|
||||
## Hard rules
|
||||
|
||||
- **Jobs die when the server restarts.** The desktop runtime restarts terminal-tools when Hive restarts. There's no re-attach. If you need durability, use `nohup` + `terminal_exec` to detach into the system's process tree and track the PID yourself.
|
||||
- **Server-wide hard cap on concurrent jobs** (`TERMINAL_TOOLS_MAX_JOBS`, default 32). Past the cap, `terminal_job_start` returns an error. Wait for jobs to exit or kill old ones.
|
||||
- **No cross-restart output.** Output handles and ring buffers are in-memory only.
|
||||
|
||||
See `references/signals.md` for the full signal catalog.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Signal reference
|
||||
|
||||
terminal_job_manage exposes six signals via the action name.
|
||||
|
||||
| Action | Signal | Number | Purpose | Catchable? |
|
||||
|---|---|---|---|---|
|
||||
| `signal_int` | SIGINT | 2 | Interrupt — Ctrl-C equivalent. Most CLIs treat as "stop gracefully". | Yes |
|
||||
| `signal_term` | SIGTERM | 15 | Polite termination request. Default for `kill`. | Yes |
|
||||
| `signal_kill` | SIGKILL | 9 | Forced kill. Process can't catch, clean up, or finalize. Use sparingly. | **No** |
|
||||
| `signal_hup` | SIGHUP | 1 | Hangup. Many daemons reload config on this. | Yes |
|
||||
| `signal_usr1` | SIGUSR1 | 10 | User-defined #1. Common: dump state, rotate logs (nginx, etc). | Yes |
|
||||
| `signal_usr2` | SIGUSR2 | 12 | User-defined #2. Common: graceful binary upgrade (unicorn, etc). | Yes |
|
||||
|
||||
## Escalation idiom
|
||||
|
||||
```
|
||||
1. signal_int (Ctrl-C — graceful)
|
||||
2. wait 2-5s, check status with terminal_job_logs(wait_until_exit=True, wait_timeout_sec=3)
|
||||
3. if still running: signal_term (cleanup-then-exit)
|
||||
4. wait 2-5s
|
||||
5. if still running: signal_kill (forced)
|
||||
```
|
||||
|
||||
The waits matter: SIGTERM handlers do real work (flush logs, close DBs, release locks) and need time. Skipping straight to SIGKILL leaks resources.
|
||||
|
||||
## When to use SIGUSR1 / SIGUSR2
|
||||
|
||||
These are application-defined. Read the target's docs first. Common:
|
||||
- **nginx**: SIGUSR1 → reopen log files (for log rotation)
|
||||
- **unicorn / puma**: SIGUSR2 → fork a new master with the latest binary (graceful restart)
|
||||
- **rsync**: SIGUSR1 → print stats so far
|
||||
|
||||
## Reading exit codes after a signal
|
||||
|
||||
When a job exits via signal, `terminal_job_logs` returns `exit_code: -N` (subprocess convention) where `abs(N)` is the signal number. The shell convention `128 + N` doesn't apply to the JobManager — that's for shell-spawned children.
|
||||
|
||||
| exit_code | Means |
|
||||
|---|---|
|
||||
| -2 | Killed by SIGINT |
|
||||
| -9 | Killed by SIGKILL |
|
||||
| -15 | Killed by SIGTERM |
|
||||
@@ -0,0 +1,127 @@
|
||||
---
|
||||
name: hive.terminal-tools-pty-sessions
|
||||
description: Use when you need state across calls — building env vars, navigating with cd, driving REPLs (python -i, mysql, psql, node), or responding to interactive prompts (sudo password, ssh host-key confirmation, mysql connection). Teaches the prompt-sentinel exec pattern (default mode), raw I/O for REPLs (raw_send=True then read_only=True), the one-in-flight-per-session rule, and the close-or-leak-against-the-cap discipline. Bash on macOS — never zsh; explicit shell=/bin/zsh is rejected. Read before calling terminal_pty_open.
|
||||
metadata:
|
||||
author: hive
|
||||
type: preset-skill
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# Persistent PTY sessions
|
||||
|
||||
PTY sessions are how you talk to interactive programs — programs that detect a terminal (`isatty()`) and behave differently when they don't see one. Use a session when:
|
||||
|
||||
- You need state to persist across calls (`cd`, env vars, sourced scripts)
|
||||
- You're driving a REPL (`python -i`, `mysql`, `psql`, `node`, `irb`)
|
||||
- A program demands an interactive prompt (`sudo`, `ssh`, `npm login`, `gh auth login`)
|
||||
|
||||
For everything else, `terminal_exec` is simpler. Sessions cost more (per-session bash process, ring buffer, idle-reaping bookkeeping) and have a hard cap (`TERMINAL_TOOLS_MAX_PTY`, default 8).
|
||||
|
||||
## Why PTY (and not subprocess pipes)
|
||||
|
||||
Subprocess pipes break on every interactive program. The moment a program calls `isatty()` and sees False, it disables prompts, color, line-editing, password masking, progress bars — sometimes refuses to start. PTY makes us look like a real terminal so these programs work the same as in your shell.
|
||||
|
||||
The cost: PTY output includes terminal escape codes (cursor moves, color codes). The session captures them as-is; if you need clean text, strip ANSI escapes in your processing layer.
|
||||
|
||||
## Bash on macOS — by deliberate policy
|
||||
|
||||
`terminal_pty_open` always invokes `/bin/bash`, regardless of the user's `$SHELL`. macOS users: yes, even when zsh is your interactive default. This is the **terminal-tools-foundations** policy applied to PTYs.
|
||||
|
||||
Reasons:
|
||||
- zsh has command/builtin classes (`zmodload`, `=cmd` expansion, `zpty`, `ztcp`) that bypass bash-shaped security checks
|
||||
- One shell behavior across platforms eliminates "works on Linux, breaks on macOS" surprises
|
||||
- Bash is universal: any shell you've used will accept the bash subset
|
||||
|
||||
The bash invocation uses `--norc --noprofile` so user dotfiles don't leak in. PS1 is set to a unique sentinel for prompt detection. PS2 is empty. PROMPT_COMMAND is empty.
|
||||
|
||||
## Three modes of `terminal_pty_run`
|
||||
|
||||
### 1. Default: send command, wait for prompt sentinel
|
||||
|
||||
```
|
||||
terminal_pty_run(session_id, command="ls -la")
|
||||
→ { output, prompt_after: True, ... }
|
||||
```
|
||||
|
||||
The session writes `ls -la\n`, waits for the sentinel that its custom PS1 emits, returns the slice between submission and prompt. **One in-flight call per session** — a concurrent call returns a `"session busy"` error.
|
||||
|
||||
### 2. raw_send: send raw input, no waiting
|
||||
|
||||
```
|
||||
terminal_pty_run(session_id, command="print('hi')\n", raw_send=True)
|
||||
→ { bytes_sent: 12 }
|
||||
```
|
||||
|
||||
For REPLs, vim keystrokes, password prompts. The session writes the bytes and returns immediately — it doesn't wait for a prompt (REPLs don't print bash's prompt; they print their own).
|
||||
|
||||
After a `raw_send`, you typically follow with:
|
||||
|
||||
### 3. read_only: drain currently-buffered output
|
||||
|
||||
```
|
||||
terminal_pty_run(session_id, read_only=True, timeout_sec=2)
|
||||
→ { output: "hi\n", more: False, ... }
|
||||
```
|
||||
|
||||
Reads whatever the session has accumulated since the last drain, with a brief settle window. Use after raw_send to capture the REPL's response.
|
||||
|
||||
## Custom prompt detection (`expect`)
|
||||
|
||||
When the command launches a program with its own prompt (Python REPL's `>>> `, mysql's `mysql> `, sudo's password prompt), the bash sentinel won't appear until the program exits. Override:
|
||||
|
||||
```
|
||||
terminal_pty_run(session_id, command="python3", expect=r">>>\s*$", timeout_sec=10)
|
||||
→ output up to and including ">>>", then control returns
|
||||
```
|
||||
|
||||
For sudo:
|
||||
|
||||
```
|
||||
terminal_pty_run(session_id, command="sudo -k && sudo whoami", expect=r"[Pp]assword:")
|
||||
terminal_pty_run(session_id, command="<password>", raw_send=True, command="<password>\n")
|
||||
terminal_pty_run(session_id, read_only=True, timeout_sec=5)
|
||||
```
|
||||
|
||||
(Treat passwords carefully — they end up in the ring buffer.)
|
||||
|
||||
## Always close
|
||||
|
||||
```
|
||||
terminal_pty_close(session_id)
|
||||
```
|
||||
|
||||
Leaked sessions count against `TERMINAL_TOOLS_MAX_PTY` (default 8). Idle reaping happens lazily on every `_open` call (sessions inactive longer than `idle_timeout_sec`, default 1800s, are dropped) — but don't rely on it. Close when you're done.
|
||||
|
||||
For unresponsive sessions, `force=True` skips the graceful "exit" attempt and goes straight to SIGTERM/SIGKILL.
|
||||
|
||||
## Common patterns
|
||||
|
||||
### Stateful navigation
|
||||
|
||||
```
|
||||
sid = terminal_pty_open(cwd="/")
|
||||
terminal_pty_run(sid, command="cd /var/log")
|
||||
terminal_pty_run(sid, command="ls -la *.log | head")
|
||||
terminal_pty_close(sid)
|
||||
```
|
||||
|
||||
### Python REPL
|
||||
|
||||
```
|
||||
sid = terminal_pty_open()
|
||||
terminal_pty_run(sid, command="python3", expect=r">>>\s*$")
|
||||
terminal_pty_run(sid, command="x = 42", raw_send=True)
|
||||
terminal_pty_run(sid, command="print(x*x)\n", raw_send=True)
|
||||
result = terminal_pty_run(sid, read_only=True) # → "1764\n>>> "
|
||||
terminal_pty_run(sid, command="exit()", raw_send=True)
|
||||
terminal_pty_close(sid)
|
||||
```
|
||||
|
||||
### ssh with host-key prompt
|
||||
|
||||
```
|
||||
sid = terminal_pty_open()
|
||||
terminal_pty_run(sid, command="ssh user@new-host", expect=r"\(yes/no.*\)\?")
|
||||
terminal_pty_run(sid, command="yes\n", raw_send=True)
|
||||
terminal_pty_run(sid, read_only=True, timeout_sec=10) # password prompt or login
|
||||
```
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
name: hive.terminal-tools-troubleshooting
|
||||
description: Read when a terminal-tools call returned something surprising — empty stdout despite no error, exit_code is null, output_handle came back expired, "too many jobs" / "session busy" / "too many PTYs", warning was set unexpectedly, semantic_status disagrees with exit_code. Diagnostic recipes only — load on demand. Don't preload; the foundational skill covers the happy path.
|
||||
metadata:
|
||||
author: hive
|
||||
type: preset-skill
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# Troubleshooting terminal-tools
|
||||
|
||||
Recipes for surprising results. Match the symptom to the section.
|
||||
|
||||
## Empty `stdout` despite the command "should have" produced output
|
||||
|
||||
Possible causes:
|
||||
1. Output went to **stderr** instead. Check `stderr` in the envelope (or use `merge_stderr=True` for jobs).
|
||||
2. Output was **fully truncated** because `max_output_kb` is too small. Check `stdout_truncated_bytes > 0`. Bump `max_output_kb` or paginate via `output_handle`.
|
||||
3. Command produced no output (correct, just unexpected — `silent` flags, no matches).
|
||||
4. Pipeline issue: the last stage of a pipe ran but stdout went elsewhere (`> /dev/null`, redirected via `2>&1`).
|
||||
5. Process is buffering its output and didn't flush before exit. Add `stdbuf -oL` (line-buffered) or `unbuffer` to the command.
|
||||
|
||||
## `exit_code: null`
|
||||
|
||||
| Cause | Other field |
|
||||
|---|---|
|
||||
| Auto-backgrounded | `auto_backgrounded: true, job_id: <X>` |
|
||||
| Hard timeout, process killed | `timed_out: true` |
|
||||
| Pre-spawn failure (command not found) | `error: ...` set, `pid: null` |
|
||||
| Still running (in `terminal_job_logs`) | `status: "running"` |
|
||||
|
||||
## `output_handle` returned `expired: true`
|
||||
|
||||
5-minute TTL. Either (a) you waited too long, or (b) the store evicted it under memory pressure (64 MB total cap, LRU eviction). Re-run the command.
|
||||
|
||||
To reduce risk: paginate the handle as soon as you receive it, or use `terminal_job_*` for huge outputs (4 MB ring buffer with offsets — no expiry).
|
||||
|
||||
## "too many jobs" / `JobLimitExceeded`
|
||||
|
||||
`TERMINAL_TOOLS_MAX_JOBS` (default 32) hit. Either:
|
||||
- Wait for jobs to exit (poll with `terminal_job_logs(wait_until_exit=True)`)
|
||||
- Kill old jobs: `terminal_job_manage(action="list")` to see what's running, then `signal_term` the abandoned ones
|
||||
- Raise the cap via env (rare)
|
||||
|
||||
## "session busy"
|
||||
|
||||
A `terminal_pty_run` was issued while another `_run` is in flight on the same session. PTY sessions are single-threaded conversations. Wait for the prior call to return, or open a second session.
|
||||
|
||||
## "PTY cap reached"
|
||||
|
||||
`TERMINAL_TOOLS_MAX_PTY` (default 8) hit. Close idle sessions (`terminal_pty_close`). Idle reaping is lazy; force it by opening — no, actually, opening throws when the cap is hit. Just close manually.
|
||||
|
||||
## `warning` is set, the command worked
|
||||
|
||||
Informational only. The pattern matched (e.g. `rm -rf` literally appears, or `git push --force` was used). The command ran. The warning is your "did I mean to do that?" prompt — verify the side effect was intended before continuing.
|
||||
|
||||
## `semantic_status: "ok"` but `exit_code: 1`
|
||||
|
||||
Working as designed. Some commands use exit 1 for legitimate non-error states:
|
||||
- `grep` / `rg` exit 1 when **no matches** found
|
||||
- `find` exit 1 when **some directories were unreadable** (typical on `/proc`, etc.)
|
||||
- `diff` exit 1 when **files differ**
|
||||
- `test` / `[` exit 1 when **condition is false**
|
||||
|
||||
The `semantic_message` field explains. Trust `semantic_status`, not raw `exit_code`.
|
||||
|
||||
## `semantic_status: "error"` but `exit_code: 0`
|
||||
|
||||
Shouldn't happen. If it does, file a bug.
|
||||
|
||||
## `truncated_bytes_dropped > 0` in `terminal_job_logs`
|
||||
|
||||
Your `since_offset` was older than the ring buffer's floor — bytes evicted before you could read them. Either:
|
||||
- Poll faster (lower latency between calls)
|
||||
- Use `merge_stderr=True` (single 4 MB ring instead of 4 MB × 2)
|
||||
- Accept the gap and move forward from `next_offset`
|
||||
|
||||
## `terminal_pty_open` succeeds but the first `_run` times out
|
||||
|
||||
The session may not have produced its first prompt sentinel within the 2-second startup window. Try:
|
||||
- A `terminal_pty_run(sid, read_only=True, timeout_sec=2)` to drain whatever's accumulated
|
||||
- A noop command (`terminal_pty_run(sid, command="true")`) to force a prompt cycle
|
||||
|
||||
Could also indicate the bash process died at startup — `terminal_pty_run(sid, ...)` would then return `"session has exited"`.
|
||||
|
||||
## `shell="/bin/zsh"` returned an error
|
||||
|
||||
By design. terminal-tools is bash-only on POSIX. Use `shell=True` (default `/bin/bash`) or omit `shell=` to exec directly.
|
||||
|
||||
## A command in `shell=True` is interpreted differently than expected
|
||||
|
||||
Bash, not zsh, semantics. `**/*` doesn't recurse without `shopt -s globstar`; `=cmd` expansion doesn't work; arrays use `arr[idx]` not `${arr[idx]}` differently than zsh. When in doubt, the foundational skill's "bash, not zsh" section is the canonical statement.
|
||||
@@ -33,6 +33,7 @@ _BUNDLED_DIRS: tuple[Path, ...] = (
|
||||
# (tool-name prefix, skill directory name, display name)
|
||||
_TOOL_GATED_SKILLS: list[tuple[str, str, str]] = [
|
||||
("browser_", "browser-automation", "hive.browser-automation"),
|
||||
("terminal_", "terminal-tools-foundations", "hive.terminal-tools-foundations"),
|
||||
]
|
||||
|
||||
_BODY_CACHE: dict[str, str] = {}
|
||||
|
||||
Reference in New Issue
Block a user