diff --git a/hermes_cli/config.py b/hermes_cli/config.py index c578ded96..282327c84 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -486,7 +486,27 @@ DEFAULT_CONFIG = { # exceed this are rejected with guidance to use offset+limit. # 100K chars ≈ 25–35K tokens across typical tokenisers. "file_read_max_chars": 100_000, - + + # Tool-output truncation thresholds. When terminal output or a + # single read_file page exceeds these limits, Hermes truncates the + # payload sent to the model (keeping head + tail for terminal, + # enforcing pagination for read_file). Tuning these trades context + # footprint against how much raw output the model can see in one + # shot. Ported from anomalyco/opencode PR #23770. + # + # - max_bytes: terminal_tool output cap, in chars + # (default 50_000 ≈ 12-15K tokens). + # - max_lines: read_file pagination cap — the maximum `limit` + # a single read_file call can request before + # being clamped (default 2000). + # - max_line_length: per-line cap applied when read_file emits a + # line-numbered view (default 2000 chars). + "tool_output": { + "max_bytes": 50_000, + "max_lines": 2000, + "max_line_length": 2000, + }, + "compression": { "enabled": True, "threshold": 0.50, # compress when context usage exceeds this ratio diff --git a/skills/creative/design-md/SKILL.md b/skills/creative/design-md/SKILL.md new file mode 100644 index 000000000..36c4138db --- /dev/null +++ b/skills/creative/design-md/SKILL.md @@ -0,0 +1,196 @@ +--- +name: design-md +description: Author, validate, diff, and export DESIGN.md files — Google's open-source format spec that gives coding agents a persistent, structured understanding of a design system (tokens + rationale in one file). Use when building a design system, porting style rules between projects, generating UI with consistent brand, or auditing accessibility/contrast. +version: 1.0.0 +author: Hermes Agent +license: MIT +metadata: + hermes: + tags: [design, design-system, tokens, ui, accessibility, wcag, tailwind, dtcg, google] + related_skills: [popular-web-designs, excalidraw, architecture-diagram] +--- + +# DESIGN.md Skill + +DESIGN.md is Google's open spec (Apache-2.0, `google-labs-code/design.md`) for +describing a visual identity to coding agents. One file combines: + +- **YAML front matter** — machine-readable design tokens (normative values) +- **Markdown body** — human-readable rationale, organized into canonical sections + +Tokens give exact values. Prose tells agents *why* those values exist and how to +apply them. The CLI (`npx @google/design.md`) lints structure + WCAG contrast, +diffs versions for regressions, and exports to Tailwind or W3C DTCG JSON. + +## When to use this skill + +- User asks for a DESIGN.md file, design tokens, or a design system spec +- User wants consistent UI/brand across multiple projects or tools +- User pastes an existing DESIGN.md and asks to lint, diff, export, or extend it +- User asks to port a style guide into a format agents can consume +- User wants contrast / WCAG accessibility validation on their color palette + +For purely visual inspiration or layout examples, use `popular-web-designs` +instead. This skill is for the *formal spec file* itself. + +## File anatomy + +```md +--- +version: alpha +name: Heritage +description: Architectural minimalism meets journalistic gravitas. +colors: + primary: "#1A1C1E" + secondary: "#6C7278" + tertiary: "#B8422E" + neutral: "#F7F5F2" +typography: + h1: + fontFamily: Public Sans + fontSize: 3rem + fontWeight: 700 + lineHeight: 1.1 + letterSpacing: "-0.02em" + body-md: + fontFamily: Public Sans + fontSize: 1rem +rounded: + sm: 4px + md: 8px + lg: 16px +spacing: + sm: 8px + md: 16px + lg: 24px +components: + button-primary: + backgroundColor: "{colors.tertiary}" + textColor: "#FFFFFF" + rounded: "{rounded.sm}" + padding: 12px + button-primary-hover: + backgroundColor: "{colors.primary}" +--- + +## Overview + +Architectural Minimalism meets Journalistic Gravitas... + +## Colors + +- **Primary (#1A1C1E):** Deep ink for headlines and core text. +- **Tertiary (#B8422E):** "Boston Clay" — the sole driver for interaction. + +## Typography + +Public Sans for everything except small all-caps labels... + +## Components + +`button-primary` is the only high-emphasis action on a page... +``` + +## Token types + +| Type | Format | Example | +|------|--------|---------| +| Color | `#` + hex (sRGB) | `"#1A1C1E"` | +| Dimension | number + unit (`px`, `em`, `rem`) | `48px`, `-0.02em` | +| Token reference | `{path.to.token}` | `{colors.primary}` | +| Typography | object with `fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation` | see above | + +Component property whitelist: `backgroundColor`, `textColor`, `typography`, +`rounded`, `padding`, `size`, `height`, `width`. Variants (hover, active, +pressed) are **separate component entries** with related key names +(`button-primary-hover`), not nested. + +## Canonical section order + +Sections are optional, but present ones MUST appear in this order. Duplicate +headings reject the file. + +1. Overview (alias: Brand & Style) +2. Colors +3. Typography +4. Layout (alias: Layout & Spacing) +5. Elevation & Depth (alias: Elevation) +6. Shapes +7. Components +8. Do's and Don'ts + +Unknown sections are preserved, not errored. Unknown token names are accepted +if the value type is valid. Unknown component properties produce a warning. + +## Workflow: authoring a new DESIGN.md + +1. **Ask the user** (or infer) the brand tone, accent color, and typography + direction. If they provided a site, image, or vibe, translate it to the + token shape above. +2. **Write `DESIGN.md`** in their project root using `write_file`. Always + include `name:` and `colors:`; other sections optional but encouraged. +3. **Use token references** (`{colors.primary}`) in the `components:` section + instead of re-typing hex values. Keeps the palette single-source. +4. **Lint it** (see below). Fix any broken references or WCAG failures + before returning. +5. **If the user has an existing project**, also write Tailwind or DTCG + exports next to the file (`tailwind.theme.json`, `tokens.json`). + +## Workflow: lint / diff / export + +The CLI is `@google/design.md` (Node). Use `npx` — no global install needed. + +```bash +# Validate structure + token references + WCAG contrast +npx -y @google/design.md lint DESIGN.md + +# Compare two versions, fail on regression (exit 1 = regression) +npx -y @google/design.md diff DESIGN.md DESIGN-v2.md + +# Export to Tailwind theme JSON +npx -y @google/design.md export --format tailwind DESIGN.md > tailwind.theme.json + +# Export to W3C DTCG (Design Tokens Format Module) JSON +npx -y @google/design.md export --format dtcg DESIGN.md > tokens.json + +# Print the spec itself — useful when injecting into an agent prompt +npx -y @google/design.md spec --rules-only --format json +``` + +All commands accept `-` for stdin. `lint` returns exit 1 on errors. Use the +`--format json` flag and parse the output if you need to report findings +structurally. + +### Lint rule reference (what the 7 rules catch) + +- `broken-ref` (error) — `{colors.missing}` points at a non-existent token +- `duplicate-section` (error) — same `## Heading` appears twice +- `invalid-color`, `invalid-dimension`, `invalid-typography` (error) +- `wcag-contrast` (warning/info) — component `textColor` vs `backgroundColor` + ratio against WCAG AA (4.5:1) and AAA (7:1) +- `unknown-component-property` (warning) — outside the whitelist above + +When the user cares about accessibility, call this out explicitly in your +summary — WCAG findings are the most load-bearing reason to use the CLI. + +## Pitfalls + +- **Don't nest component variants.** `button-primary.hover` is wrong; + `button-primary-hover` as a sibling key is right. +- **Hex colors must be quoted strings.** YAML will otherwise choke on `#` or + truncate values like `#1A1C1E` oddly. +- **Negative dimensions need quotes too.** `letterSpacing: -0.02em` parses as + a YAML flow — write `letterSpacing: "-0.02em"`. +- **Section order is enforced.** If the user gives you prose in a random order, + reorder it to match the canonical list before saving. +- **`version: alpha` is the current spec version** (as of Apr 2026). The spec + is marked alpha — watch for breaking changes. +- **Token references resolve by dotted path.** `{colors.primary}` works; + `{primary}` does not. + +## Spec source of truth + +- Repo: https://github.com/google-labs-code/design.md (Apache-2.0) +- CLI: `@google/design.md` on npm +- License of generated DESIGN.md files: whatever the user's project uses; + the spec itself is Apache-2.0. diff --git a/skills/creative/design-md/templates/starter.md b/skills/creative/design-md/templates/starter.md new file mode 100644 index 000000000..03d54785f --- /dev/null +++ b/skills/creative/design-md/templates/starter.md @@ -0,0 +1,99 @@ +--- +version: alpha +name: MyBrand +description: One-sentence description of the visual identity. +colors: + primary: "#0F172A" + secondary: "#64748B" + tertiary: "#2563EB" + neutral: "#F8FAFC" + on-primary: "#FFFFFF" + on-tertiary: "#FFFFFF" +typography: + h1: + fontFamily: Inter + fontSize: 3rem + fontWeight: 700 + lineHeight: 1.1 + letterSpacing: "-0.02em" + h2: + fontFamily: Inter + fontSize: 2rem + fontWeight: 600 + lineHeight: 1.2 + body-md: + fontFamily: Inter + fontSize: 1rem + lineHeight: 1.5 + label-caps: + fontFamily: Inter + fontSize: 0.75rem + fontWeight: 600 + letterSpacing: "0.08em" +rounded: + sm: 4px + md: 8px + lg: 16px + full: 9999px +spacing: + xs: 4px + sm: 8px + md: 16px + lg: 24px + xl: 48px +components: + button-primary: + backgroundColor: "{colors.tertiary}" + textColor: "{colors.on-tertiary}" + rounded: "{rounded.sm}" + padding: 12px + button-primary-hover: + backgroundColor: "{colors.primary}" + textColor: "{colors.on-primary}" + card: + backgroundColor: "{colors.neutral}" + textColor: "{colors.primary}" + rounded: "{rounded.md}" + padding: 24px +--- + +## Overview + +Describe the voice and feel of the brand in one or two paragraphs. What mood +does it evoke? What emotional response should a user have on first impression? + +## Colors + +- **Primary ({colors.primary}):** Core text, headlines, high-emphasis surfaces. +- **Secondary ({colors.secondary}):** Supporting text, borders, metadata. +- **Tertiary ({colors.tertiary}):** Interaction driver — buttons, links, + selected states. Use sparingly to preserve its signal. +- **Neutral ({colors.neutral}):** Page background and surface fills. + +## Typography + +Inter for everything. Weight and size carry hierarchy, not font family. Tight +letter-spacing on display sizes; default tracking on body. + +## Layout + +Spacing scale is a 4px baseline. Use `md` (16px) for intra-component gaps, +`lg` (24px) for inter-component gaps, `xl` (48px) for section breaks. + +## Shapes + +Rounded corners are modest — `sm` on interactive elements, `md` on cards. +`full` is reserved for avatars and pill badges. + +## Components + +- `button-primary` is the only high-emphasis action per screen. +- `card` is the default surface for grouped content. No shadow by default. + +## Do's and Don'ts + +- **Do** use token references (`{colors.primary}`) instead of literal hex in + component definitions. +- **Don't** introduce colors outside the palette — extend the palette first. +- **Don't** nest component variants. `button-primary-hover` is a sibling, + not a child. diff --git a/tests/tools/test_tool_output_limits.py b/tests/tools/test_tool_output_limits.py new file mode 100644 index 000000000..19fa3fc05 --- /dev/null +++ b/tests/tools/test_tool_output_limits.py @@ -0,0 +1,152 @@ +"""Tests for tools.tool_output_limits. + +Covers: +1. Default values when no config is provided. +2. Config override picks up user-supplied max_bytes / max_lines / + max_line_length. +3. Malformed values (None, negative, wrong type) fall back to defaults + rather than raising. +4. Integration: the helpers return what the terminal_tool and + file_operations call paths will actually consume. + +Port-tracking: anomalyco/opencode PR #23770 +(feat(truncate): allow configuring tool output truncation limits). +""" + +from __future__ import annotations + +from unittest.mock import patch + +import pytest + +from tools import tool_output_limits as tol + + +class TestDefaults: + def test_defaults_match_previous_hardcoded_values(self): + assert tol.DEFAULT_MAX_BYTES == 50_000 + assert tol.DEFAULT_MAX_LINES == 2000 + assert tol.DEFAULT_MAX_LINE_LENGTH == 2000 + + def test_get_limits_returns_defaults_when_config_missing(self): + with patch("hermes_cli.config.load_config", return_value={}): + limits = tol.get_tool_output_limits() + assert limits == { + "max_bytes": tol.DEFAULT_MAX_BYTES, + "max_lines": tol.DEFAULT_MAX_LINES, + "max_line_length": tol.DEFAULT_MAX_LINE_LENGTH, + } + + def test_get_limits_returns_defaults_when_config_not_a_dict(self): + # load_config should always return a dict but be defensive anyway. + with patch("hermes_cli.config.load_config", return_value="not a dict"): + limits = tol.get_tool_output_limits() + assert limits["max_bytes"] == tol.DEFAULT_MAX_BYTES + + def test_get_limits_returns_defaults_when_load_config_raises(self): + def _boom(): + raise RuntimeError("boom") + + with patch("hermes_cli.config.load_config", side_effect=_boom): + limits = tol.get_tool_output_limits() + assert limits["max_lines"] == tol.DEFAULT_MAX_LINES + + +class TestOverrides: + def test_user_config_overrides_all_three(self): + cfg = { + "tool_output": { + "max_bytes": 100_000, + "max_lines": 5000, + "max_line_length": 4096, + } + } + with patch("hermes_cli.config.load_config", return_value=cfg): + limits = tol.get_tool_output_limits() + assert limits == { + "max_bytes": 100_000, + "max_lines": 5000, + "max_line_length": 4096, + } + + def test_partial_override_preserves_other_defaults(self): + cfg = {"tool_output": {"max_bytes": 200_000}} + with patch("hermes_cli.config.load_config", return_value=cfg): + limits = tol.get_tool_output_limits() + assert limits["max_bytes"] == 200_000 + assert limits["max_lines"] == tol.DEFAULT_MAX_LINES + assert limits["max_line_length"] == tol.DEFAULT_MAX_LINE_LENGTH + + def test_section_not_a_dict_falls_back(self): + cfg = {"tool_output": "nonsense"} + with patch("hermes_cli.config.load_config", return_value=cfg): + limits = tol.get_tool_output_limits() + assert limits["max_bytes"] == tol.DEFAULT_MAX_BYTES + + +class TestCoercion: + @pytest.mark.parametrize("bad", [None, "not a number", -1, 0, [], {}]) + def test_invalid_values_fall_back_to_defaults(self, bad): + cfg = {"tool_output": {"max_bytes": bad, "max_lines": bad, "max_line_length": bad}} + with patch("hermes_cli.config.load_config", return_value=cfg): + limits = tol.get_tool_output_limits() + assert limits["max_bytes"] == tol.DEFAULT_MAX_BYTES + assert limits["max_lines"] == tol.DEFAULT_MAX_LINES + assert limits["max_line_length"] == tol.DEFAULT_MAX_LINE_LENGTH + + def test_string_integer_is_coerced(self): + cfg = {"tool_output": {"max_bytes": "75000"}} + with patch("hermes_cli.config.load_config", return_value=cfg): + limits = tol.get_tool_output_limits() + assert limits["max_bytes"] == 75_000 + + +class TestShortcuts: + def test_individual_accessors_delegate_to_get_tool_output_limits(self): + cfg = { + "tool_output": { + "max_bytes": 111, + "max_lines": 222, + "max_line_length": 333, + } + } + with patch("hermes_cli.config.load_config", return_value=cfg): + assert tol.get_max_bytes() == 111 + assert tol.get_max_lines() == 222 + assert tol.get_max_line_length() == 333 + + +class TestDefaultConfigHasSection: + """The DEFAULT_CONFIG in hermes_cli.config must expose tool_output so + that ``hermes setup`` and default installs stay in sync with the + helpers here.""" + + def test_default_config_contains_tool_output_section(self): + from hermes_cli.config import DEFAULT_CONFIG + assert "tool_output" in DEFAULT_CONFIG + section = DEFAULT_CONFIG["tool_output"] + assert isinstance(section, dict) + assert section["max_bytes"] == tol.DEFAULT_MAX_BYTES + assert section["max_lines"] == tol.DEFAULT_MAX_LINES + assert section["max_line_length"] == tol.DEFAULT_MAX_LINE_LENGTH + + +class TestIntegrationReadPagination: + """normalize_read_pagination uses get_max_lines() — verify the plumbing.""" + + def test_pagination_limit_clamped_by_config_value(self): + from tools.file_operations import normalize_read_pagination + cfg = {"tool_output": {"max_lines": 50}} + with patch("hermes_cli.config.load_config", return_value=cfg): + offset, limit = normalize_read_pagination(offset=1, limit=1000) + # limit should have been clamped to 50 (the configured max_lines) + assert limit == 50 + assert offset == 1 + + def test_pagination_default_when_config_missing(self): + from tools.file_operations import normalize_read_pagination + with patch("hermes_cli.config.load_config", return_value={}): + offset, limit = normalize_read_pagination(offset=10, limit=100000) + # Clamped to default MAX_LINES (2000). + assert limit == tol.DEFAULT_MAX_LINES + assert offset == 10 diff --git a/tools/file_operations.py b/tools/file_operations.py index 7e75578b2..9e0b44c14 100644 --- a/tools/file_operations.py +++ b/tools/file_operations.py @@ -292,10 +292,15 @@ def normalize_read_pagination(offset: Any = DEFAULT_READ_OFFSET, Tool schemas declare minimum/maximum values, but not every caller or provider enforces schemas before dispatch. Clamp here so invalid values cannot leak into sed ranges like ``0,-1p``. + + The upper bound on ``limit`` comes from ``tool_output.max_lines`` in + config.yaml (defaults to the module-level ``MAX_LINES`` constant). """ + from tools.tool_output_limits import get_max_lines + max_lines = get_max_lines() normalized_offset = max(1, _coerce_int(offset, DEFAULT_READ_OFFSET)) normalized_limit = _coerce_int(limit, DEFAULT_READ_LIMIT) - normalized_limit = max(1, min(normalized_limit, MAX_LINES)) + normalized_limit = max(1, min(normalized_limit, max_lines)) return normalized_offset, normalized_limit @@ -414,12 +419,14 @@ class ShellFileOperations(FileOperations): def _add_line_numbers(self, content: str, start_line: int = 1) -> str: """Add line numbers to content in LINE_NUM|CONTENT format.""" + from tools.tool_output_limits import get_max_line_length + max_line_length = get_max_line_length() lines = content.split('\n') numbered = [] for i, line in enumerate(lines, start=start_line): # Truncate long lines - if len(line) > MAX_LINE_LENGTH: - line = line[:MAX_LINE_LENGTH] + "... [truncated]" + if len(line) > max_line_length: + line = line[:max_line_length] + "... [truncated]" numbered.append(f"{i:6d}|{line}") return '\n'.join(numbered) diff --git a/tools/terminal_tool.py b/tools/terminal_tool.py index 22c8dcbc6..b288d4ad9 100644 --- a/tools/terminal_tool.py +++ b/tools/terminal_tool.py @@ -1805,7 +1805,8 @@ def terminal_tool( pass # Truncate output if too long, keeping both head and tail - MAX_OUTPUT_CHARS = 50000 + from tools.tool_output_limits import get_max_bytes + MAX_OUTPUT_CHARS = get_max_bytes() if len(output) > MAX_OUTPUT_CHARS: head_chars = int(MAX_OUTPUT_CHARS * 0.4) # 40% head (error messages often appear early) tail_chars = MAX_OUTPUT_CHARS - head_chars # 60% tail (most recent/relevant output) diff --git a/tools/tool_output_limits.py b/tools/tool_output_limits.py new file mode 100644 index 000000000..fd24a2da3 --- /dev/null +++ b/tools/tool_output_limits.py @@ -0,0 +1,92 @@ +"""Configurable tool-output truncation limits. + +Ported from anomalyco/opencode PR #23770 (``feat(truncate): allow +configuring tool output truncation limits``). + +OpenCode hardcoded ``MAX_LINES = 2000`` and ``MAX_BYTES = 50 * 1024`` +as tool-output truncation thresholds. Hermes-agent had the same +hardcoded constants in two places: + +* ``tools/terminal_tool.py`` — ``MAX_OUTPUT_CHARS = 50000`` (terminal + stdout/stderr cap) +* ``tools/file_operations.py`` — ``MAX_LINES = 2000`` / + ``MAX_LINE_LENGTH = 2000`` (read_file pagination cap + per-line cap) + +This module centralises those values behind a single config section +(``tool_output`` in ``config.yaml``) so power users can tune them +without patching the source. The existing hardcoded numbers remain as +defaults, so behaviour is unchanged when the config key is absent. + +Example ``config.yaml``:: + + tool_output: + max_bytes: 100000 # terminal output cap (chars) + max_lines: 5000 # read_file pagination + truncation cap + max_line_length: 2000 # per-line length cap before '... [truncated]' + +The limits reader is defensive: any error (missing config file, invalid +value type, etc.) falls back to the built-in defaults so tools never +fail because of a malformed config. +""" + +from __future__ import annotations + +from typing import Any, Dict + +# Hardcoded defaults — these match the pre-existing values, so adding +# this module is behaviour-preserving for users who don't set +# ``tool_output`` in config.yaml. +DEFAULT_MAX_BYTES = 50_000 # terminal_tool.MAX_OUTPUT_CHARS +DEFAULT_MAX_LINES = 2000 # file_operations.MAX_LINES +DEFAULT_MAX_LINE_LENGTH = 2000 # file_operations.MAX_LINE_LENGTH + + +def _coerce_positive_int(value: Any, default: int) -> int: + """Return ``value`` as a positive int, or ``default`` on any issue.""" + try: + iv = int(value) + except (TypeError, ValueError): + return default + if iv <= 0: + return default + return iv + + +def get_tool_output_limits() -> Dict[str, int]: + """Return resolved tool-output limits, reading ``tool_output`` from config. + + Keys: ``max_bytes``, ``max_lines``, ``max_line_length``. Missing or + invalid entries fall through to the ``DEFAULT_*`` constants. This + function NEVER raises. + """ + try: + from hermes_cli.config import load_config + cfg = load_config() or {} + section = cfg.get("tool_output") if isinstance(cfg, dict) else None + if not isinstance(section, dict): + section = {} + except Exception: + section = {} + + return { + "max_bytes": _coerce_positive_int(section.get("max_bytes"), DEFAULT_MAX_BYTES), + "max_lines": _coerce_positive_int(section.get("max_lines"), DEFAULT_MAX_LINES), + "max_line_length": _coerce_positive_int( + section.get("max_line_length"), DEFAULT_MAX_LINE_LENGTH + ), + } + + +def get_max_bytes() -> int: + """Shortcut for terminal-tool callers that only need the byte cap.""" + return get_tool_output_limits()["max_bytes"] + + +def get_max_lines() -> int: + """Shortcut for file-ops callers that only need the line cap.""" + return get_tool_output_limits()["max_lines"] + + +def get_max_line_length() -> int: + """Shortcut for file-ops callers that only need the per-line cap.""" + return get_tool_output_limits()["max_line_length"] diff --git a/website/docs/user-guide/configuration.md b/website/docs/user-guide/configuration.md index 055fdb3d3..420ca1468 100644 --- a/website/docs/user-guide/configuration.md +++ b/website/docs/user-guide/configuration.md @@ -431,6 +431,35 @@ file_read_max_chars: 30000 The agent also deduplicates file reads automatically — if the same file region is read twice and the file hasn't changed, a lightweight stub is returned instead of re-sending the content. This resets on context compression so the agent can re-read files after their content is summarized away. +## Tool Output Truncation Limits + +Three related caps control how much raw output a tool can return before Hermes truncates it: + +```yaml +tool_output: + max_bytes: 50000 # terminal output cap (chars) + max_lines: 2000 # read_file pagination cap + max_line_length: 2000 # per-line cap in read_file's line-numbered view +``` + +- **`max_bytes`** — When a `terminal` command produces more than this many characters of combined stdout/stderr, Hermes keeps the first 40% and last 60% and inserts a `[OUTPUT TRUNCATED]` notice between them. Default `50000` (≈12-15K tokens across typical tokenisers). +- **`max_lines`** — Upper bound on the `limit` parameter of a single `read_file` call. Requests above this are clamped so a single read can't flood the context window. Default `2000`. +- **`max_line_length`** — Per-line cap applied when `read_file` emits the line-numbered view. Lines longer than this are truncated to this many chars followed by `... [truncated]`. Default `2000`. + +Raise the limits on models with large context windows that can afford more raw output per call. Lower them for small-context models to keep tool results compact: + +```yaml +# Large context model (200K+) +tool_output: + max_bytes: 150000 + max_lines: 5000 + +# Small local model (16K context) +tool_output: + max_bytes: 20000 + max_lines: 500 +``` + ## Git Worktree Isolation Enable isolated git worktrees for running multiple agents in parallel on the same repo: