diff --git a/.cursor/rules/no-co-author-trailers.mdc b/.cursor/rules/no-co-author-trailers.mdc new file mode 100644 index 00000000..1f23da31 --- /dev/null +++ b/.cursor/rules/no-co-author-trailers.mdc @@ -0,0 +1,31 @@ +--- +description: Never add Co-authored-by or other AI/tool trailers to git commits or PRs +alwaysApply: true +--- + +# No co-author or AI trailers on commits + +**Never** add `Co-authored-by`, `Signed-off-by` for an AI tool, or any Cursor/Claude/Copilot +footer to commit messages or PR descriptions. + +This matches `AGENTS.md`: commits use Conventional Commits subject (+ optional body) only. + +## When creating commits + +- Pass the message via HEREDoc or `-m` with **only** the intended subject/body. +- Do **not** append `Co-authored-by: Cursor ` or similar. +- If a hook or tool adds a co-author trailer, **amend it out** before pushing (only when amend rules allow). + +## Examples + +```text +feat(soul): emit TodoListUpdated wire event +``` + +Not: + +```text +feat(soul): emit TodoListUpdated wire event + +Co-authored-by: Cursor +``` diff --git a/.github/workflows/ci-pythinker-host.yml b/.github/workflows/ci-pythinker-host.yml index 1d83abde..38747ef4 100644 --- a/.github/workflows/ci-pythinker-host.yml +++ b/.github/workflows/ci-pythinker-host.yml @@ -17,6 +17,9 @@ on: - "uv.lock" +permissions: + contents: read + env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" diff --git a/CHANGELOG.md b/CHANGELOG.md index d4f12d12..129c6b85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,28 @@ GitHub Releases page; `0.8.0` is the new starting line. ## Unreleased +- **Stop-time memory extraction can now be enabled explicitly.** Added an opt-in + `memory.harvest_on_stop` setting that stages safe assistant decisions, blockers, evidence, and + next steps into the existing scratchpad recall flow at turn end without writing directly to + durable `MEMORY.md`. +- **Agents can now discover visible tools and temporarily work from a session worktree.** Added + `ToolSearch` plus root-session `EnterWorktree` and `ExitWorktree` tools so agents can find + currently available capabilities by keyword and isolate a session's operational working directory + in a git worktree without deleting user work on exit. +- **Agent-loop observability now emits explicit Wire events for key runtime state.** Added + `TodoListUpdated`, `SubagentToolFallback`, `AgentListDelta`, `ToolUseSkipped`, and + `ContextOverflowRecovered` events, with todo updates, subagent launch fallbacks, same-step tool + reuse/policy skips for tools that explicitly opt in, agent-list injections, and + context-overflow recovery now surfaced best-effort over Wire without changing existing tool + results. +- **Spend ceilings now warn before they stop a session.** When `max_session_cost_usd` is configured, + the loop appends a bounded system reminder after a turn crosses `budget_nudge_ratio` of the + ceiling, nudging the agent to conserve budget without auto-continuing or hiding a later + `budget_exhausted` stop. +- **The Agent tool description now gives clearer prompt-briefing guidance.** Fresh subagents should + receive the goal, scope, expected output contract, and verification criteria; the Haiku-style + tool-use summary from the blackbox reference was deliberately not ported. + ## 0.46.0 (2026-06-14) - **Startup auto-update now picks up new releases within half an hour instead of up to a day.** The background update check was throttled to once every 24h, so a freshly published release could go unnoticed for a full day after the last check; the interval is now 30 minutes. The silent installer also no longer marks the throttle *before* the network call — a transient startup network error returns `FAILED` and is retried on the next launch instead of suppressing updates for the whole window. diff --git a/docs/en/customization/architecture.md b/docs/en/customization/architecture.md index 46cdde2d..063726e7 100644 --- a/docs/en/customization/architecture.md +++ b/docs/en/customization/architecture.md @@ -87,6 +87,12 @@ The end-to-end flow when a session starts and processes a turn: The soul is the heart of the runtime. Beyond the loop itself it owns approvals, context and compaction, slash commands, dynamic prompt injection, and a checkpoint-rewind mechanism. +The agent loop emits per-turn and per-step wire events for orchestration observability. The +canonical list lives in `src/pythinker_code/wire/types.py` (`Event` union): `StepBegin`, +`StepRetry`, `StepInterrupted`, `ToolExecutionStarted`, `StatusUpdate`, plus +`TodoListUpdated`, `SubagentToolFallback`, `AgentListDelta`, `ToolUseSkipped`, and +`ContextOverflowRecovered`. + | Path | Purpose | Key entry points and interfaces | | --- | --- | --- | | `src/pythinker_code/soul/pythinkersoul.py` | Core loop: user input, slash commands, LLM calls, tool runs, compaction, telemetry spans. | `PythinkerSoul`, `PythinkerSoul.run`, `FLOW_COMMAND_PREFIX` | @@ -168,6 +174,12 @@ model; `/usage` defaults to the active provider, with `/usage all` as the explic | `src/pythinker_code/ui/print/` | Non-interactive output (text / stream-json). | `Print` | | `src/pythinker_code/ui/acp/` | Deprecated single-session ACP shim (raises on use); the live server is `src/pythinker_code/acp/`. | `ACP` | +The agent loop emits per-turn and per-step events for UI, replay, and dashboard consumers. The +canonical list lives in the `Event` union in `src/pythinker_code/wire/types.py`; commonly consumed +events include `StepBegin`, `StepRetry`, `StepInterrupted`, `ToolExecutionStarted`, `StatusUpdate`, +`TodoListUpdated`, `SubagentToolFallback`, `AgentListDelta`, `ToolUseSkipped`, and +`ContextOverflowRecovered`. + The shell can run with a working directory inside its subtree, so `src/pythinker_code/ui/` is a candidate for a focused nested guide on prompt, visualization, and component layout. diff --git a/docs/en/release-notes/changelog.md b/docs/en/release-notes/changelog.md index 3f00e154..51d21c66 100644 --- a/docs/en/release-notes/changelog.md +++ b/docs/en/release-notes/changelog.md @@ -17,6 +17,28 @@ GitHub Releases page; `0.8.0` is the new starting line. ## Unreleased +- **Stop-time memory extraction can now be enabled explicitly.** Added an opt-in + `memory.harvest_on_stop` setting that stages safe assistant decisions, blockers, evidence, and + next steps into the existing scratchpad recall flow at turn end without writing directly to + durable `MEMORY.md`. +- **Agents can now discover visible tools and temporarily work from a session worktree.** Added + `ToolSearch` plus root-session `EnterWorktree` and `ExitWorktree` tools so agents can find + currently available capabilities by keyword and isolate a session's operational working directory + in a git worktree without deleting user work on exit. +- **Agent-loop observability now emits explicit Wire events for key runtime state.** Added + `TodoListUpdated`, `SubagentToolFallback`, `AgentListDelta`, `ToolUseSkipped`, and + `ContextOverflowRecovered` events, with todo updates, subagent launch fallbacks, same-step tool + reuse/policy skips for tools that explicitly opt in, agent-list injections, and + context-overflow recovery now surfaced best-effort over Wire without changing existing tool + results. +- **Spend ceilings now warn before they stop a session.** When `max_session_cost_usd` is configured, + the loop appends a bounded system reminder after a turn crosses `budget_nudge_ratio` of the + ceiling, nudging the agent to conserve budget without auto-continuing or hiding a later + `budget_exhausted` stop. +- **The Agent tool description now gives clearer prompt-briefing guidance.** Fresh subagents should + receive the goal, scope, expected output contract, and verification criteria; the Haiku-style + tool-use summary from the blackbox reference was deliberately not ported. + ## 0.46.0 (2026-06-14) - **Startup auto-update now picks up new releases within half an hour instead of up to a day.** The background update check was throttled to once every 24h, so a freshly published release could go unnoticed for a full day after the last check; the interval is now 30 minutes. The silent installer also no longer marks the throttle *before* the network call — a transient startup network error returns `FAILED` and is retried on the next launch instead of suppressing updates for the whole window. diff --git a/docs/superpowers/plans/2026-06-03-scoped-config.md b/docs/superpowers/plans/2026-06-03-scoped-config.md deleted file mode 100644 index a67a62ce..00000000 --- a/docs/superpowers/plans/2026-06-03-scoped-config.md +++ /dev/null @@ -1,1216 +0,0 @@ -# Scoped Configuration Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Replace pythinker's single-file config with a three-scope system (User → Project → Local) using type-based merging, hard security locks on sensitive fields, and env-var overrides. - -**Architecture:** Load raw TOML dicts from up to three files, check scope-locked fields before merging, then type-merge (scalars override deepest-wins, lists concatenate, dicts deep-merge) into a single dict, overlay `PYTHINKER_*` env vars, and validate once through Pydantic. A parallel provenance map tracks which scope each value came from so validation errors name the source file. - -**Tech Stack:** Python 3.12+, `tomlkit` (already in deps), `pydantic` v2 (already in deps), `pytest` + `monkeypatch` for tests. - -**Spec:** `docs/superpowers/specs/2026-06-03-pythinker-scope-config-design.md` - ---- - -## File Map - -| Action | Path | Responsibility | -|--------|------|----------------| -| Modify | `src/pythinker_code/config.py` | All new constants, helpers, pipeline functions, `Config` field, `load_config` wiring | -| Create | `src/pythinker_code/utils/gitignore.py` | `ensure_gitignored` utility | -| Modify | `tests/core/test_config.py` | Unit + integration tests for pipeline functions | -| Create | `tests/utils/test_gitignore.py` | Tests for `ensure_gitignored` | - -No other files need changes — all existing `load_config()` call sites automatically gain scope resolution. - ---- - -## Task 1: Sync `_find_project_root` in `config.py` - -**Files:** -- Modify: `src/pythinker_code/config.py` -- Test: `tests/core/test_config.py` - -> **Context:** `utils/path.py` already has an async `find_project_root` that returns `work_dir` when no `.git` is found. We need a sync version that returns `None` — different enough to warrant a new private function in `config.py` rather than changing the shared one. - -- [ ] **Step 1: Write the failing test** - -Add to `tests/core/test_config.py`: - -```python -from pythinker_code.config import _find_project_root - - -def test_find_project_root_finds_git_root(tmp_path): - git_dir = tmp_path / ".git" - git_dir.mkdir() - subdir = tmp_path / "src" / "pkg" - subdir.mkdir(parents=True) - assert _find_project_root(subdir) == tmp_path - - -def test_find_project_root_returns_none_outside_git(tmp_path): - # tmp_path itself has no .git ancestor in practice - assert _find_project_root(tmp_path) is None - - -def test_find_project_root_finds_root_in_cwd(tmp_path): - (tmp_path / ".git").mkdir() - assert _find_project_root(tmp_path) == tmp_path -``` - -- [ ] **Step 2: Run tests to verify they fail** - -```bash -cd /home/ai/Projects/pythinker-code-main -.venv/bin/pytest tests/core/test_config.py::test_find_project_root_finds_git_root tests/core/test_config.py::test_find_project_root_returns_none_outside_git tests/core/test_config.py::test_find_project_root_finds_root_in_cwd -v -``` - -Expected: `ImportError` or `AttributeError` — `_find_project_root` does not exist yet. - -- [ ] **Step 3: Implement `_find_project_root`** - -Add after the `get_share_dir` import block in `src/pythinker_code/config.py`, before the `AgentExecutionProfile` definition: - -```python -def _find_project_root(cwd: Path) -> Path | None: - """Walk up from cwd to find the nearest directory containing .git/. - - Returns None when no .git marker is found before reaching the filesystem - root, so callers can skip project/local scopes without a fallback. - """ - current = cwd.resolve() - while True: - if (current / ".git").exists(): - return current - parent = current.parent - if parent == current: - return None - current = parent -``` - -- [ ] **Step 4: Run tests to verify they pass** - -```bash -.venv/bin/pytest tests/core/test_config.py::test_find_project_root_finds_git_root tests/core/test_config.py::test_find_project_root_returns_none_outside_git tests/core/test_config.py::test_find_project_root_finds_root_in_cwd -v -``` - -Expected: all 3 PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/pythinker_code/config.py tests/core/test_config.py -git commit -m "feat(config): add sync _find_project_root helper" -``` - ---- - -## Task 2: `utils/gitignore.py` — `ensure_gitignored` - -**Files:** -- Create: `src/pythinker_code/utils/gitignore.py` -- Create: `tests/utils/test_gitignore.py` - -- [ ] **Step 1: Write failing tests** - -Create `tests/utils/test_gitignore.py`: - -```python -from __future__ import annotations - -from pathlib import Path - -import pytest - -from pythinker_code.utils.gitignore import ensure_gitignored - - -def test_creates_gitignore_when_absent(tmp_path): - ensure_gitignored(tmp_path, ".pythinker/config.local.toml", comment="Added by pythinker") - gi = tmp_path / ".gitignore" - assert gi.exists() - content = gi.read_text() - assert ".pythinker/config.local.toml" in content - assert "Added by pythinker" in content - - -def test_appends_to_existing_gitignore(tmp_path): - gi = tmp_path / ".gitignore" - gi.write_text("*.pyc\n", encoding="utf-8") - ensure_gitignored(tmp_path, ".pythinker/config.local.toml") - content = gi.read_text() - assert "*.pyc" in content - assert ".pythinker/config.local.toml" in content - - -def test_no_op_when_pattern_already_present(tmp_path): - gi = tmp_path / ".gitignore" - gi.write_text(".pythinker/config.local.toml\n", encoding="utf-8") - ensure_gitignored(tmp_path, ".pythinker/config.local.toml") - # No duplicate - lines = [l for l in gi.read_text().splitlines() if l == ".pythinker/config.local.toml"] - assert len(lines) == 1 - - -def test_fixes_missing_trailing_newline(tmp_path): - gi = tmp_path / ".gitignore" - gi.write_text("*.pyc", encoding="utf-8") # no trailing newline - ensure_gitignored(tmp_path, ".pythinker/config.local.toml") - content = gi.read_text() - # Pattern must start on its own line, not appended to "*.pyc" - assert "\n.pythinker/config.local.toml" in content - - -def test_omits_comment_when_empty(tmp_path): - ensure_gitignored(tmp_path, ".pythinker/config.local.toml", comment="") - content = (tmp_path / ".gitignore").read_text() - assert ".pythinker/config.local.toml" in content - assert "#" not in content -``` - -- [ ] **Step 2: Run tests to verify they fail** - -```bash -.venv/bin/pytest tests/utils/test_gitignore.py -v -``` - -Expected: `ModuleNotFoundError` — `utils/gitignore.py` does not exist yet. - -- [ ] **Step 3: Implement `ensure_gitignored`** - -Create `src/pythinker_code/utils/gitignore.py`: - -```python -from __future__ import annotations - -from pathlib import Path - - -def ensure_gitignored(git_root: Path, pattern: str, comment: str = "") -> None: - """Append *pattern* to /.gitignore if not already present. - - Creates .gitignore if the file does not exist. Handles missing trailing - newline before appending. Prepends a comment line when *comment* is given. - """ - gi_path = git_root / ".gitignore" - - if gi_path.exists(): - content = gi_path.read_text(encoding="utf-8") - # Check if pattern is already present as a standalone line - if any(line.strip() == pattern for line in content.splitlines()): - return - else: - content = "" - - lines_to_append: list[str] = [] - if content and not content.endswith("\n"): - lines_to_append.append("\n") - if comment: - lines_to_append.append(f"# {comment}\n") - lines_to_append.append(f"{pattern}\n") - - with gi_path.open("a", encoding="utf-8") as f: - f.write("".join(lines_to_append)) -``` - -- [ ] **Step 4: Run tests to verify they pass** - -```bash -.venv/bin/pytest tests/utils/test_gitignore.py -v -``` - -Expected: all 5 PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/pythinker_code/utils/gitignore.py tests/utils/test_gitignore.py -git commit -m "feat(utils): add ensure_gitignored utility" -``` - ---- - -## Task 3: Constants and helper functions in `config.py` - -**Files:** -- Modify: `src/pythinker_code/config.py` -- Test: `tests/core/test_config.py` - -> **Context:** Add the constants (`SCOPE_LOCKED_PATHS`, `DEDUP_LIST_FIELDS`, `ENV_FIELD_MAP`) and the two small helper functions (`_set_nested`, `_lookup_provenance`). These are pure functions with no side effects and can be tested in isolation. - -- [ ] **Step 1: Write failing tests** - -Add to `tests/core/test_config.py`: - -```python -from pythinker_code.config import _lookup_provenance, _set_nested - - -def test_set_nested_flat(): - d: dict = {} - _set_nested(d, ("theme",), "light") - assert d == {"theme": "light"} - - -def test_set_nested_deep(): - d: dict = {} - _set_nested(d, ("tui", "style"), "card") - assert d == {"tui": {"style": "card"}} - - -def test_set_nested_overwrites_existing(): - d = {"tui": {"style": "pythinker", "smooth_streaming": True}} - _set_nested(d, ("tui", "style"), "card") - assert d["tui"]["style"] == "card" - assert d["tui"]["smooth_streaming"] is True # sibling preserved - - -def test_lookup_provenance_scalar(): - prov = {"theme": ".pythinker/config.local.toml"} - assert _lookup_provenance(prov, ("theme",)) == ".pythinker/config.local.toml" - - -def test_lookup_provenance_nested(): - prov = {"tui": {"style": ".pythinker/config.toml"}} - assert _lookup_provenance(prov, ("tui", "style")) == ".pythinker/config.toml" - - -def test_lookup_provenance_list_index(): - # Pydantic gives loc=("hooks", 0, "command") for a bad list element. - # Should return the collection scope, not crash. - prov = {"hooks": "~/.pythinker/config.toml+.pythinker/config.toml"} - assert _lookup_provenance(prov, ("hooks", 0, "command")) == "~/.pythinker/config.toml+.pythinker/config.toml" - - -def test_lookup_provenance_partial_path(): - prov = {"tui": {"style": ".pythinker/config.toml"}} - assert _lookup_provenance(prov, ("tui", "nonexistent")) == "unknown scope" - - -def test_lookup_provenance_empty_loc(): - prov = "~/.pythinker/config.toml" - assert _lookup_provenance(prov, ()) == "~/.pythinker/config.toml" - - -def test_lookup_provenance_unknown(): - prov: dict = {} - assert _lookup_provenance(prov, ("missing_key",)) == "unknown scope" -``` - -- [ ] **Step 2: Run tests to verify they fail** - -```bash -.venv/bin/pytest tests/core/test_config.py::test_set_nested_flat tests/core/test_config.py::test_lookup_provenance_scalar -v -``` - -Expected: `ImportError` — `_set_nested`, `_lookup_provenance` not defined yet. - -- [ ] **Step 3: Add constants and helpers to `config.py`** - -Add after the `_find_project_root` function: - -```python -# --------------------------------------------------------------------------- -# Scope system constants -# --------------------------------------------------------------------------- - -SCOPE_LOCKED_PATHS: frozenset[tuple[str, ...]] = frozenset( - { - ("providers",), # contains api_key per provider — must stay in user scope - ("services",), # contains api_key fields — must stay in user scope - ("feedback", "api_key"), # only the key, not the whole feedback section - } -) - -DEDUP_LIST_FIELDS: frozenset[str] = frozenset({"allowed_domains", "extra_skill_dirs"}) - -ENV_FIELD_MAP: dict[str, tuple[str, ...]] = { - "PYTHINKER_DEFAULT_MODEL": ("default_model",), - "PYTHINKER_DEFAULT_THINKING": ("default_thinking",), - "PYTHINKER_DEFAULT_THINKING_EFFORT": ("default_thinking_effort",), - "PYTHINKER_AGENT_EXECUTION_PROFILE": ("agent_execution_profile",), - "PYTHINKER_DEFAULT_YOLO": ("default_yolo",), - "PYTHINKER_ASK_USER_QUESTION_POLICY": ("ask_user_question_policy",), - "PYTHINKER_AUTO_DELIBERATE_DESTRUCTIVE_ACTIONS": ("auto_deliberate_destructive_actions",), - "PYTHINKER_SKIP_AUTO_PROMPT_INJECTION": ("skip_auto_prompt_injection",), - "PYTHINKER_DEFAULT_PLAN_MODE": ("default_plan_mode",), - "PYTHINKER_DEFAULT_EDITOR": ("default_editor",), - "PYTHINKER_THEME": ("theme",), - "PYTHINKER_SHOW_THINKING_STREAM": ("show_thinking_stream",), - "PYTHINKER_PREVENT_IDLE_SLEEP": ("prevent_idle_sleep",), - "PYTHINKER_TELEMETRY": ("telemetry",), - "PYTHINKER_SESSION_RETENTION_DAYS": ("session_retention_days",), - "PYTHINKER_MERGE_ALL_AVAILABLE_SKILLS": ("merge_all_available_skills",), -} - - -# --------------------------------------------------------------------------- -# Pipeline helpers -# --------------------------------------------------------------------------- - - -def _set_nested(d: dict, path: tuple[str, ...], value: object) -> None: - """Walk *path* into *d*, creating intermediate dicts, then set the leaf.""" - node = d - for part in path[:-1]: - if part not in node or not isinstance(node[part], dict): - node[part] = {} - node = node[part] - node[path[-1]] = value - - -def _lookup_provenance(prov: "dict | str", loc: tuple) -> str: - """Recursively follow *loc* through the provenance map. - - Integer elements (Pydantic list indices) are skipped — we map them back - to the parent collection's scope string so error messages stay useful. - Returns "unknown scope" when the path cannot be fully resolved. - """ - if not loc or isinstance(prov, str): - return prov if isinstance(prov, str) else "unknown scope" - head, *tail = loc - if isinstance(head, int): - return prov if isinstance(prov, str) else _lookup_provenance(prov, tuple(tail)) - if isinstance(prov, dict) and head in prov: - return _lookup_provenance(prov[head], tuple(tail)) - return "unknown scope" -``` - -- [ ] **Step 4: Run tests to verify they pass** - -```bash -.venv/bin/pytest tests/core/test_config.py::test_set_nested_flat tests/core/test_config.py::test_set_nested_deep tests/core/test_config.py::test_set_nested_overwrites_existing tests/core/test_config.py::test_lookup_provenance_scalar tests/core/test_config.py::test_lookup_provenance_nested tests/core/test_config.py::test_lookup_provenance_list_index tests/core/test_config.py::test_lookup_provenance_partial_path tests/core/test_config.py::test_lookup_provenance_empty_loc tests/core/test_config.py::test_lookup_provenance_unknown -v -``` - -Expected: all 9 PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/pythinker_code/config.py tests/core/test_config.py -git commit -m "feat(config): add scope constants and provenance helpers" -``` - ---- - -## Task 4: `_check_scope_locks` - -**Files:** -- Modify: `src/pythinker_code/config.py` -- Test: `tests/core/test_config.py` - -- [ ] **Step 1: Write failing tests** - -Add to `tests/core/test_config.py`: - -```python -from pythinker_code.config import _check_scope_locks - - -def test_scope_lock_providers_in_project(): - with pytest.raises(ConfigError, match="'providers'.*project scope"): - _check_scope_locks({"providers": {"openai": {}}}, ".pythinker/config.toml") - - -def test_scope_lock_services_in_local(): - with pytest.raises(ConfigError, match="'services'.*local scope"): - _check_scope_locks({"services": {"pythinker_ai_search": {}}}, ".pythinker/config.local.toml") - - -def test_scope_lock_feedback_api_key(): - with pytest.raises(ConfigError, match="'feedback.api_key'"): - _check_scope_locks( - {"feedback": {"api_key": "secret"}}, ".pythinker/config.toml" - ) - - -def test_scope_lock_feedback_url_allowed(): - # feedback.endpoint_url is NOT locked — should not raise - _check_scope_locks( - {"feedback": {"endpoint_url": "https://internal.example.com"}}, - ".pythinker/config.toml", - ) - - -def test_scope_lock_clean_dict(): - _check_scope_locks({"theme": "light", "default_model": "gpt-4"}, ".pythinker/config.toml") - - -def test_scope_lock_error_mentions_env_var(): - with pytest.raises(ConfigError, match="PYTHINKER_PROVIDER"): - _check_scope_locks({"providers": {}}, ".pythinker/config.toml") -``` - -- [ ] **Step 2: Run tests to verify they fail** - -```bash -.venv/bin/pytest tests/core/test_config.py::test_scope_lock_providers_in_project tests/core/test_config.py::test_scope_lock_clean_dict -v -``` - -Expected: `ImportError` — `_check_scope_locks` not defined yet. - -- [ ] **Step 3: Implement `_check_scope_locks`** - -Add after `_lookup_provenance` in `src/pythinker_code/config.py`: - -```python -def _check_scope_locks(scope_dict: dict, scope_name: str) -> None: - """Raise ConfigError if *scope_dict* contains any scope-locked field paths. - - Checks every path in SCOPE_LOCKED_PATHS by walking the raw dict before - Pydantic validation, so secrets are blocked before they can be merged. - """ - for path in SCOPE_LOCKED_PATHS: - node: object = scope_dict - for part in path: - if not isinstance(node, dict) or part not in node: - break - else: - field_path = ".".join(path) - # Derive a short scope label for the error message - if "local" in scope_name: - scope_label = "local scope" - elif "project" in scope_name or scope_name.startswith(".pythinker"): - scope_label = "project scope" - else: - scope_label = scope_name - raise ConfigError( - f"'{field_path}' cannot be set in {scope_name} ({scope_label}).\n" - f" Move it to ~/.pythinker/config.toml or set the corresponding " - f"PYTHINKER_* environment variable." - ) -``` - -- [ ] **Step 4: Run tests to verify they pass** - -```bash -.venv/bin/pytest tests/core/test_config.py::test_scope_lock_providers_in_project tests/core/test_config.py::test_scope_lock_services_in_local tests/core/test_config.py::test_scope_lock_feedback_api_key tests/core/test_config.py::test_scope_lock_feedback_url_allowed tests/core/test_config.py::test_scope_lock_clean_dict tests/core/test_config.py::test_scope_lock_error_mentions_env_var -v -``` - -Expected: all 6 PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/pythinker_code/config.py tests/core/test_config.py -git commit -m "feat(config): add _check_scope_locks with path-level secret detection" -``` - ---- - -## Task 5: `_type_based_merge` - -**Files:** -- Modify: `src/pythinker_code/config.py` -- Test: `tests/core/test_config.py` - -- [ ] **Step 1: Write failing tests** - -Add to `tests/core/test_config.py`: - -```python -from pythinker_code.config import _type_based_merge - - -def test_merge_scalar_override(): - prov: dict = {} - result = _type_based_merge({"theme": "dark"}, {"theme": "light"}, prov, ".pythinker/config.local.toml") - assert result["theme"] == "light" - assert prov["theme"] == ".pythinker/config.local.toml" - - -def test_merge_scalar_three_scopes(): - prov: dict = {} - base = _type_based_merge({}, {"theme": "dark"}, prov, "~/.pythinker/config.toml") - base = _type_based_merge(base, {"theme": "solarized"}, prov, ".pythinker/config.toml") - base = _type_based_merge(base, {"theme": "light"}, prov, ".pythinker/config.local.toml") - assert base["theme"] == "light" - assert prov["theme"] == ".pythinker/config.local.toml" - - -def test_merge_list_concat(): - prov: dict = {} - base = _type_based_merge({}, {"hooks": [{"event": "Stop", "command": "a"}]}, prov, "~/.pythinker/config.toml") - base = _type_based_merge(base, {"hooks": [{"event": "Stop", "command": "b"}]}, prov, ".pythinker/config.toml") - assert len(base["hooks"]) == 2 - assert base["hooks"][0]["command"] == "a" - assert base["hooks"][1]["command"] == "b" - - -def test_merge_list_concat_provenance(): - prov: dict = {} - base = _type_based_merge({}, {"hooks": []}, prov, "~/.pythinker/config.toml") - base = _type_based_merge(base, {"hooks": []}, prov, ".pythinker/config.toml") - assert prov["hooks"] == "~/.pythinker/config.toml+.pythinker/config.toml" - - -def test_merge_list_base_case_provenance(): - prov: dict = {} - _type_based_merge({}, {"hooks": []}, prov, "~/.pythinker/config.toml") - assert prov["hooks"] == "~/.pythinker/config.toml" - - -def test_merge_list_dedup_extra_skill_dirs(): - prov: dict = {} - base = _type_based_merge({}, {"extra_skill_dirs": ["/a", "/b"]}, prov, "~/.pythinker/config.toml") - base = _type_based_merge(base, {"extra_skill_dirs": ["/b", "/c"]}, prov, ".pythinker/config.toml") - # /b appears in both — should appear only once (first occurrence kept) - assert base["extra_skill_dirs"] == ["/a", "/b", "/c"] - - -def test_merge_dict_deep(): - prov: dict = {} - base = _type_based_merge( - {}, {"tui": {"style": "pythinker", "smooth_streaming": True}}, prov, "~/.pythinker/config.toml" - ) - base = _type_based_merge( - base, {"tui": {"style": "card"}}, prov, ".pythinker/config.toml" - ) - assert base["tui"]["style"] == "card" - assert base["tui"]["smooth_streaming"] is True # sibling preserved - assert prov["tui"]["style"] == ".pythinker/config.toml" - assert prov["tui"]["smooth_streaming"] == "~/.pythinker/config.toml" - - -def test_merge_key_only_in_overlay(): - prov: dict = {} - result = _type_based_merge({}, {"theme": "dark"}, prov, "~/.pythinker/config.toml") - assert result["theme"] == "dark" - assert prov["theme"] == "~/.pythinker/config.toml" -``` - -- [ ] **Step 2: Run tests to verify they fail** - -```bash -.venv/bin/pytest tests/core/test_config.py::test_merge_scalar_override tests/core/test_config.py::test_merge_list_concat -v -``` - -Expected: `ImportError` — `_type_based_merge` not defined yet. - -- [ ] **Step 3: Implement `_type_based_merge`** - -Add after `_check_scope_locks` in `src/pythinker_code/config.py`: - -```python -def _type_based_merge(base: dict, overlay: dict, provenance: dict, scope: str) -> dict: - """Merge *overlay* into *base* using type-based rules, tracking provenance. - - Rules: - - Scalar (str/bool/int/float/None): overlay wins, provenance records scope. - - List: base + overlay concatenated; DEDUP_LIST_FIELDS deduplicated - (order-preserving, first occurrence wins). - - Dict: recurse so nested keys can be independently overridden. - - Mutates *base* and *provenance* in place; also returns *base* for chaining. - """ - for key, value in overlay.items(): - if key not in base: - base[key] = value - provenance[key] = scope - elif isinstance(value, list) and isinstance(base[key], list): - combined = base[key] + value - if key in DEDUP_LIST_FIELDS: - combined = list(dict.fromkeys(combined)) - base[key] = combined - existing = provenance.get(key) - provenance[key] = f"{existing}+{scope}" if existing else scope - elif isinstance(value, dict) and isinstance(base[key], dict): - _type_based_merge( - base[key], - value, - provenance.setdefault(key, {}), - scope, - ) - else: - base[key] = value - provenance[key] = scope - return base -``` - -- [ ] **Step 4: Run tests to verify they pass** - -```bash -.venv/bin/pytest tests/core/test_config.py::test_merge_scalar_override tests/core/test_config.py::test_merge_scalar_three_scopes tests/core/test_config.py::test_merge_list_concat tests/core/test_config.py::test_merge_list_concat_provenance tests/core/test_config.py::test_merge_list_base_case_provenance tests/core/test_config.py::test_merge_list_dedup_extra_skill_dirs tests/core/test_config.py::test_merge_dict_deep tests/core/test_config.py::test_merge_key_only_in_overlay -v -``` - -Expected: all 8 PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/pythinker_code/config.py tests/core/test_config.py -git commit -m "feat(config): add _type_based_merge with dedup and provenance tracking" -``` - ---- - -## Task 6: `_apply_env_vars` - -**Files:** -- Modify: `src/pythinker_code/config.py` -- Test: `tests/core/test_config.py` - -- [ ] **Step 1: Write failing tests** - -Add to `tests/core/test_config.py`: - -```python -from pythinker_code.config import _apply_env_vars - - -def test_apply_env_vars_known_key(monkeypatch): - monkeypatch.setenv("PYTHINKER_THEME", "light") - merged: dict = {} - prov: dict = {} - _apply_env_vars(merged, prov) - assert merged["theme"] == "light" - assert prov["theme"] == "env PYTHINKER_THEME" - - -def test_apply_env_vars_unknown_key_ignored(monkeypatch): - monkeypatch.setenv("PYTHINKER_XYZZY_UNKNOWN", "whatever") - merged: dict = {} - prov: dict = {} - _apply_env_vars(merged, prov) - assert "xyzzy_unknown" not in merged - - -def test_apply_env_vars_bool_coercion(monkeypatch): - monkeypatch.setenv("PYTHINKER_DEFAULT_YOLO", "true") - merged: dict = {} - prov: dict = {} - _apply_env_vars(merged, prov) - # Stored as string; Pydantic coerces during model_validate - assert merged["default_yolo"] == "true" - assert prov["default_yolo"] == "env PYTHINKER_DEFAULT_YOLO" - - -def test_apply_env_vars_overrides_existing(monkeypatch): - monkeypatch.setenv("PYTHINKER_THEME", "light") - merged = {"theme": "dark"} - prov = {"theme": "~/.pythinker/config.toml"} - _apply_env_vars(merged, prov) - assert merged["theme"] == "light" - assert prov["theme"] == "env PYTHINKER_THEME" -``` - -- [ ] **Step 2: Run tests to verify they fail** - -```bash -.venv/bin/pytest tests/core/test_config.py::test_apply_env_vars_known_key tests/core/test_config.py::test_apply_env_vars_unknown_key_ignored -v -``` - -Expected: `ImportError` — `_apply_env_vars` not defined yet. - -- [ ] **Step 3: Implement `_apply_env_vars`** - -Add after `_type_based_merge` in `src/pythinker_code/config.py`: - -```python -def _apply_env_vars(merged: dict, provenance: dict) -> None: - """Overlay PYTHINKER_* env vars onto *merged*, updating *provenance*. - - Values are stored as raw strings; Pydantic coerces them during - model_validate(). Only keys in ENV_FIELD_MAP are recognised; all others - are silently ignored. - """ - for env_key, path in ENV_FIELD_MAP.items(): - value = os.environ.get(env_key) - if value is not None: - _set_nested(merged, path, value) - _set_nested(provenance, path, f"env {env_key}") -``` - -- [ ] **Step 4: Run tests to verify they pass** - -```bash -.venv/bin/pytest tests/core/test_config.py::test_apply_env_vars_known_key tests/core/test_config.py::test_apply_env_vars_unknown_key_ignored tests/core/test_config.py::test_apply_env_vars_bool_coercion tests/core/test_config.py::test_apply_env_vars_overrides_existing -v -``` - -Expected: all 4 PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/pythinker_code/config.py tests/core/test_config.py -git commit -m "feat(config): add _apply_env_vars with ENV_FIELD_MAP" -``` - ---- - -## Task 7: `source_scopes` field on `Config` - -**Files:** -- Modify: `src/pythinker_code/config.py` -- Test: `tests/core/test_config.py` - -> **Context:** Add `source_scopes: dict[str, Path]` as an `exclude=True` metadata field alongside the existing `source_file` and `is_from_default_location` fields. It is never serialised. - -- [ ] **Step 1: Write a failing test** - -Add to `tests/core/test_config.py`: - -```python -def test_config_source_scopes_default_empty(): - config = get_default_config() - assert config.source_scopes == {} - - -def test_config_source_scopes_not_in_dump(): - config = get_default_config() - dumped = config.model_dump() - assert "source_scopes" not in dumped -``` - -- [ ] **Step 2: Run tests to verify they fail** - -```bash -.venv/bin/pytest tests/core/test_config.py::test_config_source_scopes_default_empty tests/core/test_config.py::test_config_source_scopes_not_in_dump -v -``` - -Expected: `AttributeError` — `source_scopes` does not exist yet. - -- [ ] **Step 3: Add `source_scopes` to `Config`** - -In `src/pythinker_code/config.py`, inside the `Config` class, add alongside the existing `source_file` field: - -```python -source_scopes: dict[str, Path] = Field( - default_factory=dict, - description=( - "Paths of config files that contributed to this resolved config, keyed by scope name. " - "e.g. {'user': Path('~/.pythinker/config.toml'), 'project': Path('.pythinker/config.toml')}." - ), - exclude=True, -) -``` - -- [ ] **Step 4: Run tests to verify they pass** - -```bash -.venv/bin/pytest tests/core/test_config.py::test_config_source_scopes_default_empty tests/core/test_config.py::test_config_source_scopes_not_in_dump -v -``` - -Expected: both PASS. - -- [ ] **Step 5: Run existing config tests to confirm no regression** - -```bash -.venv/bin/pytest tests/core/test_config.py -v -``` - -Expected: all existing tests PASS. - -- [ ] **Step 6: Commit** - -```bash -git add src/pythinker_code/config.py tests/core/test_config.py -git commit -m "feat(config): add source_scopes metadata field to Config" -``` - ---- - -## Task 8: `_load_scoped` pipeline function - -**Files:** -- Modify: `src/pythinker_code/config.py` -- Test: `tests/core/test_config.py` - -> **Context:** This is the heart of the feature. It wires all previous functions into the five-step pipeline: Ingest → Guard → Merge → Env → Validate. - -- [ ] **Step 1: Write integration tests** - -Add to `tests/core/test_config.py`: - -```python -import tomlkit -from pythinker_code.config import _load_scoped - - -def _write_toml(path: Path, data: dict) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(tomlkit.dumps(data), encoding="utf-8") # type: ignore[arg-type] - - -def test_load_scoped_user_only(tmp_path, monkeypatch): - monkeypatch.setenv("PYTHINKER_SHARE_DIR", str(tmp_path)) - _write_toml(tmp_path / "config.toml", {"theme": "light"}) - config = _load_scoped(project_root=None) - assert config.theme == "light" - assert config.source_scopes["user"] == (tmp_path / "config.toml").resolve() - - -def test_load_scoped_project_overrides_user(tmp_path, monkeypatch): - monkeypatch.setenv("PYTHINKER_SHARE_DIR", str(tmp_path)) - _write_toml(tmp_path / "config.toml", {"theme": "dark"}) - project_root = tmp_path / "myproject" - _write_toml(project_root / ".pythinker" / "config.toml", {"theme": "solarized"}) - config = _load_scoped(project_root=project_root) - assert config.theme == "solarized" - - -def test_load_scoped_local_overrides_project(tmp_path, monkeypatch): - monkeypatch.setenv("PYTHINKER_SHARE_DIR", str(tmp_path)) - _write_toml(tmp_path / "config.toml", {"theme": "dark"}) - project_root = tmp_path / "myproject" - _write_toml(project_root / ".pythinker" / "config.toml", {"theme": "solarized"}) - _write_toml(project_root / ".pythinker" / "config.local.toml", {"theme": "light"}) - config = _load_scoped(project_root=project_root) - assert config.theme == "light" - - -def test_load_scoped_hooks_concatenate(tmp_path, monkeypatch): - monkeypatch.setenv("PYTHINKER_SHARE_DIR", str(tmp_path)) - _write_toml(tmp_path / "config.toml", {"hooks": [{"event": "Stop", "command": "user-hook"}]}) - project_root = tmp_path / "myproject" - _write_toml( - project_root / ".pythinker" / "config.toml", - {"hooks": [{"event": "Stop", "command": "project-hook"}]}, - ) - config = _load_scoped(project_root=project_root) - commands = [h.command for h in config.hooks] - assert "user-hook" in commands - assert "project-hook" in commands - - -def test_load_scoped_scope_lock_violation(tmp_path, monkeypatch): - monkeypatch.setenv("PYTHINKER_SHARE_DIR", str(tmp_path)) - _write_toml(tmp_path / "config.toml", {}) - project_root = tmp_path / "myproject" - _write_toml( - project_root / ".pythinker" / "config.toml", - {"providers": {"bad": {"type": "openai", "base_url": "x", "api_key": "sk-x"}}}, - ) - with pytest.raises(ConfigError, match="'providers'"): - _load_scoped(project_root=project_root) - - -def test_load_scoped_validation_error_attributes_scope(tmp_path, monkeypatch): - monkeypatch.setenv("PYTHINKER_SHARE_DIR", str(tmp_path)) - _write_toml(tmp_path / "config.toml", {}) - project_root = tmp_path / "myproject" - _write_toml( - project_root / ".pythinker" / "config.local.toml", - {"theme": "neon"}, # invalid value - ) - with pytest.raises(ConfigError, match="config.local.toml"): - _load_scoped(project_root=project_root) - - -def test_load_scoped_env_override(tmp_path, monkeypatch): - monkeypatch.setenv("PYTHINKER_SHARE_DIR", str(tmp_path)) - monkeypatch.setenv("PYTHINKER_THEME", "light") - _write_toml(tmp_path / "config.toml", {"theme": "dark"}) - project_root = tmp_path / "myproject" - _write_toml(project_root / ".pythinker" / "config.toml", {"theme": "solarized"}) - config = _load_scoped(project_root=project_root) - assert config.theme == "light" # env beats all file scopes - - -def test_load_scoped_source_scopes_populated(tmp_path, monkeypatch): - monkeypatch.setenv("PYTHINKER_SHARE_DIR", str(tmp_path)) - _write_toml(tmp_path / "config.toml", {}) - project_root = tmp_path / "myproject" - _write_toml(project_root / ".pythinker" / "config.toml", {}) - config = _load_scoped(project_root=project_root) - assert "user" in config.source_scopes - assert "project" in config.source_scopes - assert "local" not in config.source_scopes # local file absent -``` - -- [ ] **Step 2: Run tests to verify they fail** - -```bash -.venv/bin/pytest tests/core/test_config.py::test_load_scoped_user_only tests/core/test_config.py::test_load_scoped_scope_lock_violation -v -``` - -Expected: `ImportError` — `_load_scoped` not defined yet. - -- [ ] **Step 3: Implement `_load_scoped`** - -Add after `_apply_env_vars` in `src/pythinker_code/config.py`. Also add `import copy` at the top of the file if not already present: - -```python -def _load_scoped(project_root: Path | None) -> Config: - """Run the five-step scoped config resolution pipeline. - - Steps: Ingest → Guard → Merge → Env → Validate. - Returns a fully-validated Config with source_scopes populated. - """ - from pythinker_code.utils.gitignore import ensure_gitignored - - # ── INGEST ──────────────────────────────────────────────────────────── - default_user_file = get_config_file().expanduser().resolve(strict=False) - # Trigger JSON→TOML migration if needed (existing logic) - if not default_user_file.exists(): - migration_error = _migrate_json_config_to_toml() - if migration_error is not None: - raise ConfigError( - f"Legacy config file has incompatible settings; please fix or " - f"rename/delete {migration_error.config_file} to continue. " - f"Errors: {migration_error.errors}" - ) from None - - def _read_toml(path: Path) -> dict: - if not path.exists(): - return {} - try: - return dict(tomlkit.loads(path.read_text(encoding="utf-8"))) - except TOMLKitError as exc: - raise ConfigError(f"Invalid TOML in {path}: {exc}") from exc - - user_file = default_user_file - user_dict = _read_toml(user_file) - - project_file: Path | None = None - local_file: Path | None = None - project_dict: dict = {} - local_dict: dict = {} - - if project_root is not None: - project_file = project_root / ".pythinker" / "config.toml" - local_file = project_root / ".pythinker" / "config.local.toml" - project_dict = _read_toml(project_file) - local_dict = _read_toml(local_file) - - # ── GUARD ───────────────────────────────────────────────────────────── - if project_file is not None: - _check_scope_locks(project_dict, str(project_file)) - if local_file is not None: - _check_scope_locks(local_dict, str(local_file)) - - # ── MERGE ───────────────────────────────────────────────────────────── - provenance: dict = {} - merged = _type_based_merge({}, user_dict, provenance, str(user_file)) - if project_dict: - merged = _type_based_merge(merged, project_dict, provenance, str(project_file)) - if local_dict: - merged = _type_based_merge(merged, local_dict, provenance, str(local_file)) - - # ── ENV OVERLAY ─────────────────────────────────────────────────────── - _apply_env_vars(merged, provenance) - - # ── VALIDATE ────────────────────────────────────────────────────────── - try: - config = Config.model_validate(merged) - except ValidationError as exc: - enriched: list[str] = [] - for err in exc.errors(): - scope = _lookup_provenance(provenance, tuple(err["loc"])) - field = ".".join(str(p) for p in err["loc"]) - enriched.append(f" {field}: {err['msg']} [from {scope}]") - raise ConfigError("Invalid configuration:\n" + "\n".join(enriched)) from exc - - # ── METADATA ────────────────────────────────────────────────────────── - config.is_from_default_location = True - config.source_file = user_file - if user_file.exists(): - config.source_scopes["user"] = user_file - if project_file is not None and project_file.exists(): - config.source_scopes["project"] = project_file - if local_file is not None and local_file.exists(): - config.source_scopes["local"] = local_file - # Auto-gitignore local config so it is never accidentally committed - ensure_gitignored( - project_root, # type: ignore[arg-type] - ".pythinker/config.local.toml", - comment="Added by pythinker", - ) - - return config -``` - -- [ ] **Step 4: Verify `import os` is present** - -Check that `config.py` imports `os` (required by `_apply_env_vars`). Note: `_type_based_merge` mutates in-place, so `import copy` is not needed. - -```bash -grep "^import os" /home/ai/Projects/pythinker-code-main/src/pythinker_code/config.py -``` - -Expected: `import os` found. If not, add `import os` to the imports. - -- [ ] **Step 5: Run integration tests** - -```bash -.venv/bin/pytest tests/core/test_config.py::test_load_scoped_user_only tests/core/test_config.py::test_load_scoped_project_overrides_user tests/core/test_config.py::test_load_scoped_local_overrides_project tests/core/test_config.py::test_load_scoped_hooks_concatenate tests/core/test_config.py::test_load_scoped_scope_lock_violation tests/core/test_config.py::test_load_scoped_validation_error_attributes_scope tests/core/test_config.py::test_load_scoped_env_override tests/core/test_config.py::test_load_scoped_source_scopes_populated -v -``` - -Expected: all 8 PASS. - -- [ ] **Step 6: Commit** - -```bash -git add src/pythinker_code/config.py tests/core/test_config.py -git commit -m "feat(config): add _load_scoped five-step pipeline" -``` - ---- - -## Task 9: Wire `load_config` + full regression sweep - -**Files:** -- Modify: `src/pythinker_code/config.py` -- Test: `tests/core/test_config.py` - -> **Context:** Update `load_config` to route through `_load_scoped` when called with no explicit file path. When an explicit path is given, use the original code path unchanged. Run the full test suite to verify no regression. - -- [ ] **Step 1: Write a backward-compatibility test** - -Add to `tests/core/test_config.py`: - -```python -def test_load_config_explicit_path_bypasses_scoping(tmp_path): - """--config flag must bypass scope resolution entirely.""" - config_file = tmp_path / "explicit.toml" - config_file.write_text('theme = "light"\n', encoding="utf-8") - config = load_config(config_file) - assert config.theme == "light" - assert config.source_file == config_file.resolve() - # source_scopes is empty because no scope pipeline was run - assert config.source_scopes == {} - - -def test_load_config_no_args_uses_scope_resolution(tmp_path, monkeypatch): - """load_config() with no args routes through scoped pipeline.""" - monkeypatch.setenv("PYTHINKER_SHARE_DIR", str(tmp_path)) - (tmp_path / "config.toml").write_text('theme = "light"\n', encoding="utf-8") - # No git root in tmp_path — falls back to user-only - config = load_config() - assert config.theme == "light" - assert "user" in config.source_scopes -``` - -- [ ] **Step 2: Run these tests to verify they fail** - -```bash -.venv/bin/pytest tests/core/test_config.py::test_load_config_explicit_path_bypasses_scoping tests/core/test_config.py::test_load_config_no_args_uses_scope_resolution -v -``` - -Expected: `test_load_config_no_args_uses_scope_resolution` FAIL (source_scopes empty because `load_config` hasn't been updated yet). - -- [ ] **Step 3: Update `load_config` in `config.py`** - -Replace the start of `load_config` so it routes through `_load_scoped` when no explicit file is given: - -```python -def load_config(config_file: Path | None = None) -> Config: - """Load configuration, resolving up to three scopes when no explicit file is given. - - When *config_file* is None (the default), the scoped pipeline runs: - User (~/.pythinker/config.toml) → Project (.pythinker/config.toml) → - Local (.pythinker/config.local.toml), merged with type-based rules. - - When *config_file* is given explicitly (e.g. via --config), that single - file is loaded directly with no scope resolution — preserving the legacy - behaviour used by tests and the CLI --config flag. - """ - if config_file is None: - project_root = _find_project_root(Path.cwd()) - return _load_scoped(project_root) - - # ── Explicit path: legacy single-file load (unchanged) ──────────────── - default_config_file = get_config_file().expanduser().resolve(strict=False) - config_file = config_file.expanduser().resolve(strict=False) - is_default_config_file = config_file == default_config_file - logger.debug("Loading config from file: {file}", file=config_file) - - if is_default_config_file and not config_file.exists(): - migration_error = _migrate_json_config_to_toml() - if migration_error is not None: - raise ConfigError( - f"Legacy config file has incompatible settings; please fix or " - f"rename/delete {migration_error.config_file} to continue. " - f"Errors: {migration_error.errors}" - ) from None - - if not config_file.exists(): - config = get_default_config() - logger.debug("No config file found, creating default config: {config}", config=config) - save_config(config, config_file) - config.is_from_default_location = is_default_config_file - config.source_file = config_file - return config - - try: - config_text = config_file.read_text(encoding="utf-8") - if config_file.suffix.lower() == ".json": - data = json.loads(config_text) - else: - data = tomlkit.loads(config_text) - config = Config.model_validate(data) - except json.JSONDecodeError as e: - raise ConfigError(f"Invalid JSON in configuration file {config_file}: {e}") from e - except TOMLKitError as e: - raise ConfigError(f"Invalid TOML in configuration file {config_file}: {e}") from e - except ValidationError as e: - raise ConfigError(f"Invalid configuration file {config_file}: {e}") from e - config.is_from_default_location = is_default_config_file - config.source_file = config_file - return config -``` - -- [ ] **Step 4: Run the two new tests** - -```bash -.venv/bin/pytest tests/core/test_config.py::test_load_config_explicit_path_bypasses_scoping tests/core/test_config.py::test_load_config_no_args_uses_scope_resolution -v -``` - -Expected: both PASS. - -- [ ] **Step 5: Run the full config test suite** - -```bash -.venv/bin/pytest tests/core/test_config.py -v -``` - -Expected: all tests PASS. If `test_load_config_sets_source_file` fails because `source_scopes` is now non-empty, update its assertion to only check `source_file` and `is_from_default_location`. - -- [ ] **Step 6: Run the broader test suite** - -```bash -.venv/bin/pytest tests/ -x -q --ignore=tests/e2e 2>&1 | tail -30 -``` - -Expected: no new failures. Fix any failures before committing. - -- [ ] **Step 7: Run the linter/formatter** - -```bash -cd /home/ai/Projects/pythinker-code-main && make check-pythinker-code -``` - -Expected: all checks pass. Fix any ruff errors before committing. - -- [ ] **Step 8: Commit** - -```bash -git add src/pythinker_code/config.py tests/core/test_config.py -git commit -m "feat(config): wire load_config to scope resolution pipeline - -When called with no explicit file path, load_config now discovers -User → Project → Local scopes relative to the nearest .git root, -merges them with type-based rules, overlays PYTHINKER_* env vars, -and validates once through Pydantic with provenance-enriched errors. -Explicit --config path continues to bypass scope resolution." -``` - ---- - -## Self-Review Checklist - -- [x] **`_find_project_root`** — Task 1 ✓ -- [x] **`ensure_gitignored`** — Task 2 ✓ (including FileNotFoundError / create-if-absent, trailing newline, comment hygiene) -- [x] **`SCOPE_LOCKED_PATHS`, `DEDUP_LIST_FIELDS`, `ENV_FIELD_MAP`** — Task 3 ✓ -- [x] **`_set_nested`, `_lookup_provenance`** — Task 3 ✓ (integer index bypass in lookup) -- [x] **`_check_scope_locks`** with path-level check — Task 4 ✓ (`feedback.api_key` locked, `feedback.endpoint_url` allowed) -- [x] **`_type_based_merge`** with all three dispatch branches + dedup — Task 5 ✓ -- [x] **`_apply_env_vars`** with full `ENV_FIELD_MAP` — Task 6 ✓ -- [x] **`source_scopes` field** on `Config` — Task 7 ✓ (exclude=True, not serialised) -- [x] **`_load_scoped`** five-step pipeline — Task 8 ✓ (auto-gitignore in metadata step) -- [x] **`load_config` wiring + regression sweep** — Task 9 ✓ -- [x] **Backward compatibility** — explicit `--config` path still bypasses scoping (Task 9 Step 3) -- [x] **All test names** reference functions defined in earlier tasks — no forward references -- [x] **Human-readable scope strings** passed as `scope` param (file paths, not tags like `"local"`) -- [x] **Provenance base-case** for list: `scope` alone when no prior entry (Task 5 impl) diff --git a/examples/feedback-worker/src/index.ts b/examples/feedback-worker/src/index.ts index 889f575a..382d2a0c 100644 --- a/examples/feedback-worker/src/index.ts +++ b/examples/feedback-worker/src/index.ts @@ -297,9 +297,14 @@ async function sendSupportEmail(env: Env, subject: string, body: string): Promis } function parseMailbox(value: string): { email: string; name?: string } { - const match = value.match(/^(.+?)\s*<([^>]+)>$/); - if (!match) return { email: value.trim() }; - return { name: match[1].trim().replace(/^"|"$/g, ""), email: match[2].trim() }; + // Linear parse (no backtracking regex) for the "Name " form to avoid + // polynomial ReDoS on uncontrolled header input. + const trimmed = value.trim(); + const lt = trimmed.indexOf("<"); + if (lt === -1 || !trimmed.endsWith(">")) return { email: trimmed }; + const email = trimmed.slice(lt + 1, -1).trim(); + const name = trimmed.slice(0, lt).trim().replace(/^"|"$/g, ""); + return { name, email }; } function splitCsv(value: string): string[] { diff --git a/packages/pythinker-review/src/pythinker_review/security_scan/dependencies.py b/packages/pythinker-review/src/pythinker_review/security_scan/dependencies.py index 90b25e3a..dd73bfd2 100644 --- a/packages/pythinker-review/src/pythinker_review/security_scan/dependencies.py +++ b/packages/pythinker-review/src/pythinker_review/security_scan/dependencies.py @@ -14,7 +14,7 @@ from pythinker_review.security_intel.service import scan_packages from pythinker_review.security_scan.paths import data_dir -_VERSION_PREFIX_RE = re.compile(r"^[\^~>=<\s=]+") +_VERSION_PREFIX_RE = re.compile(r"^[\^~>=<\s]+") _REQUIREMENT_RE = re.compile( r"^([A-Za-z0-9_.\-]+(?:\[[A-Za-z0-9_,]+\])?)\s*(==|>=|<=|~=|!=|>|<)\s*([A-Za-z0-9_.\-+*,<>=]+)" ) diff --git a/src/pythinker_code/agents/default/agent.yaml b/src/pythinker_code/agents/default/agent.yaml index 4b8bcfaa..ed9e089d 100644 --- a/src/pythinker_code/agents/default/agent.yaml +++ b/src/pythinker_code/agents/default/agent.yaml @@ -12,6 +12,9 @@ agent: # - "pythinker_code.tools.think:Think" - "pythinker_code.tools.ask_user:AskUserQuestion" - "pythinker_code.tools.todo:SetTodoList" + - "pythinker_code.tools.tool_search:ToolSearch" + - "pythinker_code.tools.worktree:EnterWorktree" + - "pythinker_code.tools.worktree:ExitWorktree" - "pythinker_code.tools.goal:UpdateGoal" - "pythinker_code.tools.progress:Progress" - "pythinker_code.tools.suggest:Suggest" diff --git a/src/pythinker_code/config.py b/src/pythinker_code/config.py index af56fa0e..dd3d0149 100644 --- a/src/pythinker_code/config.py +++ b/src/pythinker_code/config.py @@ -592,6 +592,9 @@ class LoopControl(BaseModel): this value, instead of continuing to ``max_steps_per_turn``. Off by default (``None``). Best-effort: cost is estimated from token usage and is ``0`` for models with unknown pricing, so the ceiling never blocks when spend cannot be estimated.""" + budget_nudge_ratio: float = Field(default=0.75, ge=0.0, le=0.99) + """Append a one-shot per-turn reminder after a turn crosses this fraction of + ``max_session_cost_usd``. The nudge is advisory and never auto-continues.""" max_retries_per_step: int = Field(default=3, ge=1) """Maximum number of retries in one step""" max_ralph_iterations: int = Field(default=0, ge=-1) @@ -616,6 +619,9 @@ class LoopControl(BaseModel): prune_min_chars: int = Field(default=2000, ge=0) """Only tool outputs whose text exceeds this many characters are pruned, so small results are left intact. Default: 2000.""" + prune_tool_result_max_chars: int = Field(default=0, ge=0) + """Optional per-tool-result character budget for old tool messages before + full pruning. ``0`` keeps current behavior. Default: 0.""" class BackgroundConfig(BaseModel): @@ -677,6 +683,10 @@ class MemoryConfig(BaseModel): "Persist safe decisions/blockers/next steps before compaction discards history." ), ) + harvest_on_stop: bool = Field( + default=False, + description="Stage safe decisions/blockers/next steps after a turn reaches Stop.", + ) journal_recaps: bool = Field( default=False, description="Write stable-schema JOURNAL.md session recap blocks.", diff --git a/src/pythinker_code/llm.py b/src/pythinker_code/llm.py index 9d1c6b3f..f6c76e56 100644 --- a/src/pythinker_code/llm.py +++ b/src/pythinker_code/llm.py @@ -495,11 +495,17 @@ def _is_glm_model(model_name: str) -> bool: def _is_dashscope_endpoint(base_url: str) -> bool: """True for any Alibaba DashScope endpoint (standard, intl, workspace).""" - return "aliyuncs.com" in base_url + from urllib.parse import urlparse + + host = urlparse(base_url).hostname or "" + return host == "aliyuncs.com" or host.endswith(".aliyuncs.com") def _is_alibaba_workspace_endpoint(base_url: str) -> bool: - return "://ws-" in base_url and ".maas.aliyuncs.com" in base_url + from urllib.parse import urlparse + + host = urlparse(base_url).hostname or "" + return host.startswith("ws-") and host.endswith(".maas.aliyuncs.com") def _load_scripted_echo_scripts() -> list[str]: diff --git a/src/pythinker_code/prompts/__init__.py b/src/pythinker_code/prompts/__init__.py index eb66c35a..b2266f90 100644 --- a/src/pythinker_code/prompts/__init__.py +++ b/src/pythinker_code/prompts/__init__.py @@ -9,6 +9,11 @@ GOAL_SET = (Path(__file__).parent / "goal_set.md").read_text(encoding="utf-8") GOAL_CONTINUATION = (Path(__file__).parent / "goal_continuation.md").read_text(encoding="utf-8") GOAL_WRAP_UP = (Path(__file__).parent / "goal_wrap_up.md").read_text(encoding="utf-8") +BUDGET_CONTINUATION_NUDGE = ( + "The session has crossed {ratio:.0%} of its configured spend ceiling " + "(estimated ${spent:.2f} of ${ceiling:.2f}). Continue only if the remaining work is " + "worth the budget; prefer summarizing progress or asking the user before expensive next steps." +) def apply_always_on_best_practices(system_prompt: str, *, enabled: bool) -> str: diff --git a/src/pythinker_code/soul/compaction.py b/src/pythinker_code/soul/compaction.py index 8933f2f5..5b4b393e 100644 --- a/src/pythinker_code/soul/compaction.py +++ b/src/pythinker_code/soul/compaction.py @@ -85,6 +85,7 @@ def should_prune(token_count: int, max_context_size: int, *, ratio: float) -> bo PRUNE_PLACEHOLDER = "[tool output elided to save context: {n} chars]" +CAP_PLACEHOLDER = "\n[tool output capped: removed {n} chars]" def prune_stale_tool_outputs( @@ -116,6 +117,44 @@ def prune_stale_tool_outputs( return pruned, freed +def _cap_text_to_budget(body: str, max_chars: int) -> str: + removed = len(body) - max_chars + while True: + suffix = CAP_PLACEHOLDER.format(n=removed) + if len(suffix) >= max_chars: + return suffix[:max_chars] + prefix_chars = max_chars - len(suffix) + capped = body[:prefix_chars] + suffix + new_removed = len(body) - len(capped) + if new_removed == removed: + return capped + removed = new_removed + + +def cap_stale_tool_result_bodies( + messages: Sequence[Message], *, protect_last: int, max_chars: int +) -> tuple[list[Message], int]: + """Cap old tool-result text bodies without dropping messages or tool IDs.""" + if max_chars <= 0: + return list(messages), 0 + + cutoff = max(0, len(messages) - protect_last) + capped_messages: list[Message] = [] + freed = 0 + for index, msg in enumerate(messages): + if index >= cutoff or msg.role != "tool": + capped_messages.append(msg) + continue + body = msg.extract_text("") + if len(body) <= max_chars: + capped_messages.append(msg) + continue + capped_body = _cap_text_to_budget(body, max_chars) + freed += len(body) - len(capped_body) + capped_messages.append(msg.model_copy(update={"content": [TextPart(text=capped_body)]})) + return capped_messages, freed + + @runtime_checkable class Compaction(Protocol): async def compact( diff --git a/src/pythinker_code/soul/dynamic_injections/agent_list.py b/src/pythinker_code/soul/dynamic_injections/agent_list.py new file mode 100644 index 00000000..7605bbc2 --- /dev/null +++ b/src/pythinker_code/soul/dynamic_injections/agent_list.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +from collections.abc import Sequence +from typing import TYPE_CHECKING + +from pythinker_core.message import Message + +from pythinker_code.soul import wire_send +from pythinker_code.soul.dynamic_injection import DynamicInjection, DynamicInjectionProvider +from pythinker_code.subagents.models import AgentTypeDefinition +from pythinker_code.utils.logging import logger +from pythinker_code.wire.types import AgentListDelta + +if TYPE_CHECKING: + from pythinker_code.soul.pythinkersoul import PythinkerSoul + + +def format_agent_line(agent: AgentTypeDefinition) -> str: + if agent.tool_policy.mode == "allowlist": + tools = ", ".join(_unique_tool_names(agent.tool_policy.tools)) or "(none)" + else: + tools = "*" + when = f" When to use: {' '.join(agent.when_to_use.split())}" if agent.when_to_use else "" + return f"- `{agent.name}`: {agent.description} (Tools: {tools}).{when}" + + +def _unique_tool_names(tool_paths: tuple[str, ...]) -> list[str]: + names: list[str] = [] + for path in tool_paths: + name = path.split(":")[-1] + if name not in names: + names.append(name) + return names + + +class AgentListInjectionProvider(DynamicInjectionProvider): + """Inject the live built-in agent list for the root session only.""" + + def __init__(self) -> None: + self._last_fingerprint: tuple[str, ...] | None = None + + async def get_injections( + self, + history: Sequence[Message], + soul: PythinkerSoul, + ) -> list[DynamicInjection]: + if soul.is_subagent: + return [] + del history + agents = sorted( + soul.runtime.labor_market.builtin_types.values(), + key=lambda item: item.name, + ) + lines = tuple(format_agent_line(agent) for agent in agents) + if not lines: + return [] + if lines == self._last_fingerprint: + return [] + self._last_fingerprint = lines + _emit_agent_list_delta(lines) + return [ + DynamicInjection( + type="agent_list", + content=( + "Available agent types (regenerated when subagent specs change):\n" + + "\n".join(lines) + ), + ) + ] + + async def on_context_compacted(self) -> None: + self._last_fingerprint = None + + def rearm(self, key: str) -> bool: + if key != "agent_list": + return False + self._last_fingerprint = None + return True + + +def _emit_agent_list_delta(lines: tuple[str, ...]) -> None: + try: + wire_send(AgentListDelta(items=lines, complete=True)) + except Exception as exc: # noqa: BLE001 - prompt injection must not fail on UI telemetry + logger.debug("Failed to emit AgentListDelta wire event: {error}", error=exc) diff --git a/src/pythinker_code/soul/pythinkersoul.py b/src/pythinker_code/soul/pythinkersoul.py index 46a4154a..604f1b55 100644 --- a/src/pythinker_code/soul/pythinkersoul.py +++ b/src/pythinker_code/soul/pythinkersoul.py @@ -43,6 +43,7 @@ extract_notification_ids, ) from pythinker_code.prompt_templates import PromptTemplate, expand_prompt_template +from pythinker_code.prompts import BUDGET_CONTINUATION_NUDGE from pythinker_code.skill import Skill, read_skill_text_with_local_specialization from pythinker_code.soul import ( LLMNotSet, @@ -66,6 +67,7 @@ from pythinker_code.soul.compaction import ( CompactionResult, SimpleCompaction, + cap_stale_tool_result_bodies, estimate_text_tokens, prune_stale_tool_outputs, should_auto_compact, @@ -85,6 +87,7 @@ injection_budget_from_runtime, normalize_history, ) +from pythinker_code.soul.dynamic_injections.agent_list import AgentListInjectionProvider from pythinker_code.soul.dynamic_injections.auto_mode import AutoModeInjectionProvider from pythinker_code.soul.dynamic_injections.goal_mode import GoalModeInjectionProvider from pythinker_code.soul.dynamic_injections.inline_commands import InlineCommandReminderProvider @@ -125,6 +128,7 @@ CompactionBegin, CompactionEnd, ContentPart, + ContextOverflowRecovered, MCPLoadingBegin, MCPLoadingEnd, QuestionItem, @@ -283,6 +287,31 @@ def _budget_exhausted_message(session_cost_usd: float, ceiling: float) -> Messag return Message(role="assistant", content=[TextPart(text=text)]) +def _crossed_budget_nudge_threshold( + *, + before_usd: float, + after_usd: float, + ceiling: float | None, + ratio: float, + stop_reason: StepStopReason, +) -> bool: + if ceiling is None or ceiling <= 0 or after_usd <= 0: + return False + if stop_reason == "budget_exhausted": + return False + threshold = ratio * ceiling + return before_usd < threshold <= after_usd + + +def _budget_nudge_message(*, session_cost_usd: float, ceiling: float, ratio: float) -> Message: + text = BUDGET_CONTINUATION_NUDGE.format( + ratio=ratio, + spent=session_cost_usd, + ceiling=ceiling, + ) + return Message(role="user", content=[system_reminder(text[:500])]) + + def _user_message_with_hook_context( user_input: str | list[ContentPart], results: Sequence[HookResult] ) -> Message: @@ -521,6 +550,9 @@ def __init__( # Self-filtering: root-only; posture-fingerprinted so it re-emits # exactly when yolo/auto/safe-mode/profile/session-approvals change. PermissionsInjectionProvider(), + # Self-filtering: root-only; keeps the model's subagent list current + # without tying it to the static tool description cache. + AgentListInjectionProvider(), *( [] if self._runtime.config.skip_auto_prompt_injection @@ -1090,6 +1122,7 @@ async def run( user_message = _user_message_with_hook_context(user_input, hook_results) # Slash-command parsing must see only the user's text, never appended hook context. text_input = Message(role="user", content=user_input).extract_text(" ").strip() + stop_harvest_start_index = len(self._context.history) primary_outcome: TurnOutcome | None = None if command_call := parse_slash_command_call(text_input): @@ -1132,6 +1165,9 @@ async def run( if primary_outcome is not None: await self._run_goal_continuations(primary_outcome) + if getattr(self._runtime.config.memory, "harvest_on_stop", False): + await self._harvest_on_stop(stop_harvest_start_index) + wire_send(TurnEnd()) turn_finished = True @@ -1289,7 +1325,25 @@ async def _turn(self, user_message: Message) -> TurnOutcome: await self._checkpoint() # this creates the checkpoint 0 on first run await self._context.append_message(user_message) logger.debug("Appended user message to context") + cost_before_turn = self._session_cost_usd outcome = await self._agent_loop() + ceiling = self._loop_control.max_session_cost_usd + goal_config = self._runtime.config.goal + if not goal_config.auto_continue and _crossed_budget_nudge_threshold( + before_usd=cost_before_turn, + after_usd=self._session_cost_usd, + ceiling=ceiling, + ratio=self._loop_control.budget_nudge_ratio, + stop_reason=outcome.stop_reason, + ): + assert ceiling is not None + await self._context.append_message( + _budget_nudge_message( + session_cost_usd=self._session_cost_usd, + ceiling=ceiling, + ratio=self._loop_control.budget_nudge_ratio, + ) + ) span.set_attribute("turn.stop_reason", outcome.stop_reason) span.set_attribute("turn.step_count", outcome.step_count) # Observable signal that a turn ended without a substantive answer (a @@ -1502,6 +1556,8 @@ async def _agent_loop(self) -> TurnOutcome: self._truncation_recoveries = 0 # One-shot per turn: reactive compact-and-retry after a provider # context-length rejection (proactive thresholds can undercount). + # A second overflow in the same turn must propagate instead of looping + # through repeated compactions. overflow_recovery_used = False while True: # Spend ceiling: stop before starting another (paid) step once the session's @@ -1622,7 +1678,14 @@ async def _agent_loop(self) -> TurnOutcome: track("api_error", **api_error_props) if error_type == "context_overflow" and not overflow_recovery_used: overflow_recovery_used = True - if await self._recover_from_context_overflow(step_no): + recovered = await self._recover_from_context_overflow(step_no) + wire_send( + ContextOverflowRecovered( + outcome="recovered" if recovered else "failed", + trigger_step=step_no, + ) + ) + if recovered: continue # --- StopFailure hook --- from pythinker_code.hooks import events as _hook_events @@ -2183,11 +2246,17 @@ async def prune_context(self) -> bool: when there is nothing worth pruning. Runs silently — no compaction wire events — since it may fire often and is not a user-visible summary. """ - pruned, freed = prune_stale_tool_outputs( + capped, cap_freed = cap_stale_tool_result_bodies( self._context.history, protect_last=self._loop_control.prune_protect_last, + max_chars=self._loop_control.prune_tool_result_max_chars, + ) + pruned, prune_freed = prune_stale_tool_outputs( + capped, + protect_last=self._loop_control.prune_protect_last, min_chars=self._loop_control.prune_min_chars, ) + freed = cap_freed + prune_freed if freed <= 0: return False @@ -2459,6 +2528,48 @@ async def _harvest_before_compaction( except Exception as exc: logger.warning("rearm_injection(project_memory) failed: {!r}", exc) + async def _harvest_on_stop(self, history_start_index: int) -> None: + recent_history = list(self._context.history[history_start_index:]) + if not recent_history: + return + try: + from pythinker_code.memory.harvest import CompactionHarvester + from pythinker_code.scratchpad import append_scratch_note + + notes = CompactionHarvester().harvest(recent_history) + except Exception as exc: + logger.warning("stop-time memory harvester crashed: {!r}", exc) + return + persisted = 0 + for note in notes: + try: + append_result = await append_scratch_note( + self._runtime.work_dir, + kind=note.kind, + content=note.content, + session_id=self._runtime.session.id, + session_title=self._runtime.session.title, + labels=["source:stop"], + ) + if append_result.appended: + persisted += 1 + else: + logger.debug( + "stop-time memory note was not appended: reason={reason}", + reason=append_result.reason, + ) + except Exception as exc: + logger.warning( + "append_scratch_note failed during stop harvest for kind={!r}: {!r}", + note.kind, + exc, + ) + if persisted: + try: + self.rearm_injection("project_memory") + except Exception as exc: + logger.warning("rearm_injection(project_memory) failed: {!r}", exc) + @staticmethod def _is_retryable_error(exception: BaseException) -> bool: if isinstance(exception, (APIConnectionError, APITimeoutError)): diff --git a/src/pythinker_code/soul/toolset.py b/src/pythinker_code/soul/toolset.py index b70b7dbe..9411524a 100644 --- a/src/pythinker_code/soul/toolset.py +++ b/src/pythinker_code/soul/toolset.py @@ -12,6 +12,7 @@ from collections.abc import AsyncGenerator, Awaitable, Callable, Iterable from contextvars import ContextVar from dataclasses import dataclass +from dataclasses import replace as dataclass_replace from datetime import timedelta from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, Literal, cast, overload @@ -32,6 +33,7 @@ ) from pythinker_core.tooling.mcp import convert_mcp_content from pythinker_core.utils.typing import JsonType +from pythinker_host.path import HostPath from pythinker_code.exception import InvalidToolError, MCPRuntimeError from pythinker_code.hooks.engine import HookEngine @@ -49,6 +51,7 @@ ToolExecutionStarted, ToolResult, ToolReturnValue, + ToolUseSkipped, VideoURLPart, ) @@ -122,6 +125,34 @@ def emit_current_tool_execution_started() -> None: ) +def _emit_tool_use_skipped( + *, + tool_call_id: str, + tool_name: str, + reason: Literal["dedup", "policy", "interrupt", "concurrent_inflight"], + resumed: bool = False, +) -> None: + try: + from pythinker_code.soul import get_wire_or_none + + if wire := get_wire_or_none(): + wire.soul_side.send( + ToolUseSkipped( + tool_call_id=tool_call_id, + tool_name=tool_name, + reason=reason, + resumed=resumed, + ) + ) + except Exception as exc: # noqa: BLE001 - observability must not break tool execution + logger.debug( + "Failed to emit tool skipped event: {tool_name} (call_id={call_id}): {error}", + tool_name=tool_name, + call_id=tool_call_id, + error=exc, + ) + + def _tool_defers_execution_started(tool: ToolType) -> bool: return bool(getattr(tool, "emits_tool_execution_started_after_approval", False)) @@ -280,6 +311,8 @@ def type_check(pythinker_toolset: PythinkerToolset): "\n" ) +TOOL_USE_SKIPPED_REASONS = frozenset({"dedup", "policy", "interrupt", "concurrent_inflight"}) + def _make_reminder_text_2(tool_name: str, repeat_count: int, canonical_args: str) -> str: # Echo only a bounded preview of the arguments: large-payload tools @@ -357,6 +390,24 @@ def _append_reminder_to_return_value(return_value: Any, reminder_text: str) -> A return return_value.model_copy(update={"output": new_output}) +def _emit_tool_use_skipped_if_opted_in( + tool: ToolType, + *, + tool_call_id: str, + tool_name: str, + reason: Literal["dedup", "policy", "interrupt", "concurrent_inflight"], + resumed: bool = False, +) -> None: + if not getattr(tool, "emits_tool_use_skipped", False): + return + _emit_tool_use_skipped( + tool_call_id=tool_call_id, + tool_name=tool_name, + reason=reason, + resumed=resumed, + ) + + _DEFAULT_MAX_CONCURRENT_READERS = 10 """Cap on concurrent parallel-safe tool calls. A turn that fans out many readers (e.g. dozens of FetchURL) overlaps freely up to this bound rather than opening an @@ -370,7 +421,8 @@ class _ReadWriteGate: a mutating tool (writer) waits for in-flight readers to drain and excludes everything while it runs. Writers hold the lock while draining, which also blocks new readers behind a queued writer — dispatch order stays deterministic - and writers cannot starve. + and writers cannot starve. Unflagged/plugin-style tools default to the + exclusive writer path unless they explicitly declare ``supports_parallel=True``. """ def __init__(self, max_concurrent_readers: int = _DEFAULT_MAX_CONCURRENT_READERS) -> None: @@ -382,6 +434,8 @@ def __init__(self, max_concurrent_readers: int = _DEFAULT_MAX_CONCURRENT_READERS @contextlib.asynccontextmanager async def shared(self) -> AsyncGenerator[None]: + # Only tools that opted into ``supports_parallel=True`` should enter this + # shared path; unflagged/plugin adapters stay exclusive by default. # Cap concurrent readers. Acquire the slot BEFORE the writer lock / counter # bump: a reader still queued here has not incremented _active_readers, so it # never holds _readers_drained open, and writers (which never touch the @@ -508,6 +562,21 @@ def find(self, tool_name_or_type: str | type[ToolType]) -> ToolType | None: def tools(self) -> list[Tool]: return [tool.base for tool in self._tool_dict.values() if self._is_tool_visible(tool)] + def set_work_dir_override(self, work_dir: HostPath | None) -> HostPath | None: + """Apply a process-local operational cwd override to this toolset's runtime and tools.""" + if self._runtime is None: + return None + self._runtime.work_dir_override = work_dir + effective_work_dir = self._runtime.work_dir + self._runtime.builtin_args = dataclass_replace( + self._runtime.builtin_args, + PYTHINKER_WORK_DIR=effective_work_dir, + ) + for tool in self._tool_dict.values(): + if hasattr(tool, "_work_dir"): + cast(Any, tool)._work_dir = effective_work_dir + return effective_work_dir + def _is_tool_visible(self, tool: ToolType) -> bool: """Return whether *tool* should be advertised to the model for this step. @@ -667,6 +736,13 @@ def handle(self, tool_call: ToolCall) -> HandleResult: if call_key in self._current_step_tasks: from pythinker_code.telemetry import track + _emit_tool_use_skipped_if_opted_in( + tool, + tool_call_id=tool_call.id, + tool_name=tool_call.function.name, + reason="dedup", + resumed=True, + ) track( "tool_call_dedup_detected", turn_id=self._turn_id, @@ -707,6 +783,14 @@ async def _await_dup() -> ToolResult: reminder_text = _make_reminder_text_2( tool_call.function.name, repeat_count, canonical_args ) + if reminder_text is not None: + _emit_tool_use_skipped_if_opted_in( + tool, + tool_call_id=tool_call.id, + tool_name=tool_call.function.name, + reason="dedup", + resumed=False, + ) async def _call(): started_ids_token = _current_tool_execution_started_ids.set(set[str]()) @@ -727,6 +811,12 @@ async def _call_with_lifecycle(): tool_input_dict, tool=tool, ): + _emit_tool_use_skipped_if_opted_in( + tool, + tool_call_id=tool_call.id, + tool_name=tool_call.function.name, + reason="policy", + ) return ToolResult(tool_call_id=tool_call.id, return_value=err) # --- PreToolUse --- @@ -745,6 +835,12 @@ async def _call_with_lifecycle(): ) for result in results: if result.action == "block": + _emit_tool_use_skipped_if_opted_in( + tool, + tool_call_id=tool_call.id, + tool_name=tool_call.function.name, + reason="policy", + ) return ToolResult( tool_call_id=tool_call.id, return_value=ToolError( diff --git a/src/pythinker_code/tools/agent/__init__.py b/src/pythinker_code/tools/agent/__init__.py index 0dd4de32..5d0386f3 100644 --- a/src/pythinker_code/tools/agent/__init__.py +++ b/src/pythinker_code/tools/agent/__init__.py @@ -22,7 +22,7 @@ from pythinker_code.subagents.usage import aggregate_findings, summarize_batch from pythinker_code.tools.utils import ToolResultStatus, load_desc, tool_status_line from pythinker_code.utils.logging import logger -from pythinker_code.wire.types import MCPStatusSnapshot +from pythinker_code.wire.types import MCPStatusSnapshot, SubagentToolFallback def _missing_required_mcp_servers( @@ -44,6 +44,38 @@ def _missing_required_mcp_servers( return [name for name in required if name not in connected] +def _emit_subagent_tool_fallback( + *, + reason: Literal[ + "unavailable_agent_type", + "mcp_unavailable", + "policy_denied", + "timeout", + "exception", + ], + requested_type: str, + runtime: Runtime, +) -> None: + try: + from pythinker_code.soul import get_wire_or_none + + if wire := get_wire_or_none(): + wire.soul_side.send( + SubagentToolFallback( + reason=reason, + requested_type=requested_type, + available_types=tuple(sorted(runtime.labor_market.builtin_types)), + ) + ) + except Exception as exc: # noqa: BLE001 - observability must not break Agent tool errors + logger.debug( + "Failed to emit subagent fallback event: {type} ({reason}): {error}", + type=requested_type, + reason=reason, + error=exc, + ) + + NAME = "Agent" MAX_FOREGROUND_TIMEOUT = 60 * 60 # 1 hour @@ -334,10 +366,20 @@ async def __call__(self, params: Params) -> ToolReturnValue: ) requested_type = params.subagent_type or "coder" if err := self.check_execution_policy(requested_type): + _emit_subagent_tool_fallback( + reason="policy_denied", + requested_type=requested_type, + runtime=self._runtime, + ) return err # Gate a FRESH spawn on the agent type's required MCP servers (resume is not a # fresh spawn — the instance already exists, so it is not re-gated). if params.resume is None and (err := self.check_required_mcp_servers(requested_type)): + _emit_subagent_tool_fallback( + reason="mcp_unavailable", + requested_type=requested_type, + runtime=self._runtime, + ) return err if params.fork_context and (params.resume is not None or params.run_in_background): return ToolError( @@ -375,6 +417,11 @@ async def __call__(self, params: Params) -> ToolReturnValue: return await asyncio.wait_for(runner.run(req), timeout=timeout) return await runner.run(req) except TimeoutError as exc: + _emit_subagent_tool_fallback( + reason="timeout", + requested_type=requested_type, + runtime=self._runtime, + ) # Note: TimeoutError from run_soul internals (e.g. aiohttp) is now caught # by run_soul_checked and converted to SoulRunFailure. This handler mainly # covers wait_for's task-level timeout and pre-run_soul TimeoutErrors. @@ -406,6 +453,11 @@ async def __call__(self, params: Params) -> ToolReturnValue: except KeyError as exc: # Hallucinated subagent type: routine model error, not a crash — # name the valid types so the model can self-correct. + _emit_subagent_tool_fallback( + reason="unavailable_agent_type", + requested_type=requested_type, + runtime=self._runtime, + ) return ToolError( message=( f"{exc.args[0] if exc.args else exc}. Available types: " @@ -418,6 +470,11 @@ async def __call__(self, params: Params) -> ToolReturnValue: report_handled_error(exc, site="tool.agent.foreground", tool="Agent") logger.exception("Foreground agent run failed") + _emit_subagent_tool_fallback( + reason="exception", + requested_type=requested_type, + runtime=self._runtime, + ) return ToolError(message=f"Failed to run agent: {exc}", brief="Agent failed") async def _run_in_background(self, params: Params) -> ToolReturnValue: @@ -563,6 +620,11 @@ async def _run_in_background(self, params: Params) -> ToolReturnValue: # Malformed resume id (store.instance_dir validates [A-Za-z0-9_-]{1,64}). return ToolError(message=str(exc), brief="Agent not found") except KeyError as exc: + _emit_subagent_tool_fallback( + reason="unavailable_agent_type", + requested_type=params.subagent_type or "coder", + runtime=self._runtime, + ) return ToolError( message=( f"{exc.args[0] if exc.args else exc}. Available types: " diff --git a/src/pythinker_code/tools/agent/description.md b/src/pythinker_code/tools/agent/description.md index b3758ad1..257fddb5 100644 --- a/src/pythinker_code/tools/agent/description.md +++ b/src/pythinker_code/tools/agent/description.md @@ -29,6 +29,13 @@ ${BUILTIN_AGENT_TYPES_MD} - Cross-check at least one load-bearing subagent finding before making changes from it. - The subagent result is only visible to you. If the user should see it, summarize it yourself. +**Prompt Hygiene** + +When spawning a fresh agent, brief it like a smart colleague who just walked in: include the goal, +what was tried, what is in and out of scope, the expected output contract, and how the result will +be verified. For lookups, pass the exact command or symbol; for investigations, pass the question, +not a prescribed sequence of steps. + **Agent Workflow Design** Use subagents as focused logical roles, not just extra tool capacity: @@ -84,3 +91,5 @@ Match the number of parallel agents to the task's independent subparts, not to a - Only genuinely broad, cross-cutting work → more, up to the `RunAgents` cap of 8. Prefer the fewest children that cover the independent objectives — the cap of 8 is a ceiling, not a target. Over-provisioning burns the multi-agent token premium (a fan-out can cost several times a single thread) and produces results you then have to reconcile. Do not launch a subagent for what one or two direct reads or greps would answer. + +When spawning a fresh agent, brief it like a smart colleague who just walked in — include the goal, what was tried, what is in/out of scope, the expected output contract, and how the result will be verified. Lookups: pass the exact command. Investigations: pass the question, not prescribed steps. diff --git a/src/pythinker_code/tools/todo/__init__.py b/src/pythinker_code/tools/todo/__init__.py index b608dbd7..9469ffd6 100644 --- a/src/pythinker_code/tools/todo/__init__.py +++ b/src/pythinker_code/tools/todo/__init__.py @@ -10,6 +10,7 @@ from pythinker_code.tools.display import TodoDisplayBlock, TodoDisplayItem from pythinker_code.tools.utils import load_desc from pythinker_code.utils.logging import logger +from pythinker_code.wire.types import TodoListUpdated TodoStatus = Literal["pending", "in_progress", "done", "cancelled"] _STATUS_ALIASES: dict[str, TodoStatus] = { @@ -85,6 +86,23 @@ def _normalize_single_in_progress(todos: list[Todo]) -> tuple[list[Todo], int]: return normalized, demoted +def _emit_todo_list_updated(todos: list[Todo]) -> None: + try: + from pythinker_code.soul import get_wire_or_none + + if wire := get_wire_or_none(): + complete = not todos or all(todo.status in {"done", "cancelled"} for todo in todos) + wire.soul_side.send( + TodoListUpdated( + items=tuple((todo.title, todo.status) for todo in todos), + complete=complete, + source="tool", + ) + ) + except Exception as exc: # noqa: BLE001 - observability must not break the todo tool + logger.debug("Failed to emit TodoListUpdated wire event: {error}", error=exc) + + class SetTodoList(CallableTool2[Params]): name: str = "SetTodoList" description: str = load_desc(Path(__file__).parent / "set_todo_list.md") @@ -162,6 +180,7 @@ async def _journal_todo_update(self, todos: list[Todo]) -> None: def _write_todos(self, todos: list[Todo]) -> ToolReturnValue: """Persist the todo list and return confirmation.""" self._save_todos(todos) + _emit_todo_list_updated(todos) items = [TodoDisplayItem(title=todo.title, status=todo.status) for todo in todos] return ToolReturnValue( @@ -176,6 +195,7 @@ def _write_todos(self, todos: list[Todo]) -> ToolReturnValue: def _read_todos(self) -> ToolReturnValue: """Return the current todo list as text output for the model.""" todos = self._load_todos() + _emit_todo_list_updated(todos) if not todos: return ToolReturnValue( is_error=False, diff --git a/src/pythinker_code/tools/tool_search/__init__.py b/src/pythinker_code/tools/tool_search/__init__.py new file mode 100644 index 00000000..6a0fc7f9 --- /dev/null +++ b/src/pythinker_code/tools/tool_search/__init__.py @@ -0,0 +1,74 @@ +from pathlib import Path +from typing import override + +from pydantic import BaseModel, Field +from pythinker_core.tooling import CallableTool2, ToolOk, ToolReturnValue + +from pythinker_code.soul.toolset import PythinkerToolset +from pythinker_code.tools.utils import load_desc + + +class Params(BaseModel): + query: str = Field(description="Keywords to search for in visible tool names and descriptions.") + max_results: int = Field( + default=8, + ge=1, + le=25, + description="Maximum number of matching tools to return.", + ) + + +def _score_tool(name: str, description: str, terms: list[str]) -> int: + haystack_name = name.lower() + haystack_desc = description.lower() + score = 0 + for term in terms: + if term in haystack_name: + score += 4 + if term in haystack_desc: + score += 1 + return score + + +def _short_description(description: str, *, limit: int = 160) -> str: + compact = " ".join(description.split()) + if len(compact) <= limit: + return compact + return compact[: limit - 3].rstrip() + "..." + + +class ToolSearch(CallableTool2[Params]): + name: str = "ToolSearch" + description: str = load_desc(Path(__file__).parent / "tool_search.md") + params: type[Params] = Params + supports_parallel: bool = True + + def __init__(self, toolset: PythinkerToolset) -> None: + super().__init__() + self._toolset = toolset + + @override + async def __call__(self, params: Params) -> ToolReturnValue: + query = " ".join(params.query.split()) + if not query: + return ToolOk(output="No visible tools matched an empty query.") + + terms = [term.lower() for term in query.split()] + matches: list[tuple[int, str, str]] = [] + for tool in self._toolset.tools: + score = _score_tool(tool.name, tool.description or "", terms) + if score: + matches.append((score, tool.name, tool.description or "No description provided.")) + + if not matches: + return ToolOk(output=f"No visible tools matched `{query}`.") + + matches.sort(key=lambda item: (-item[0], item[1].lower())) + lines = [ + f"- {name} - {_short_description(description)}" + for _, name, description in matches[: params.max_results] + ] + return ToolOk( + output="\n".join(lines), + message=f"Found {len(lines)} visible tool match(es) for `{query}`.", + ) diff --git a/src/pythinker_code/tools/tool_search/tool_search.md b/src/pythinker_code/tools/tool_search/tool_search.md new file mode 100644 index 00000000..2285109d --- /dev/null +++ b/src/pythinker_code/tools/tool_search/tool_search.md @@ -0,0 +1,7 @@ +Search the currently visible tool list by name and description. + +Use this when you are unsure which tool is available for a task or when deferred/hidden tool +loading means the initial prompt may not list every useful capability. The search only returns +tools visible under the current runtime and permission profile. + +Provide concise keywords such as `worktree`, `background task`, `read file`, or `web search`. diff --git a/src/pythinker_code/tools/worktree/__init__.py b/src/pythinker_code/tools/worktree/__init__.py new file mode 100644 index 00000000..c1e57e66 --- /dev/null +++ b/src/pythinker_code/tools/worktree/__init__.py @@ -0,0 +1,218 @@ +import re +from dataclasses import dataclass +from pathlib import Path +from typing import ClassVar, override + +from pydantic import BaseModel, Field +from pythinker_core.tooling import CallableTool2, ToolReturnValue +from pythinker_host.path import HostPath + +from pythinker_code.soul.agent import Runtime +from pythinker_code.soul.toolset import PythinkerToolset +from pythinker_code.subagents.worktree import WorktreeError, create_agent_worktree +from pythinker_code.tools.utils import ToolResultStatus, load_desc, tool_error + + +@dataclass(frozen=True, slots=True) +class _SessionWorktreeState: + original_work_dir: HostPath + worktree_path: HostPath + + +class EnterWorktreeParams(BaseModel): + name: str | None = Field( + default=None, + description=( + "Optional short suffix for the worktree directory. Ignored when `path` is provided." + ), + ) + path: str | None = Field( + default=None, + description=( + "Optional absolute destination path for the worktree. Defaults to a session worktrees " + "directory." + ), + ) + + +class ExitWorktreeParams(BaseModel): + pass + + +_ACTIVE_WORKTREES: dict[int, _SessionWorktreeState] = {} +_SAFE_NAME_RE = re.compile(r"[^A-Za-z0-9_.-]+") + + +def _safe_name(name: str | None) -> str: + if not name: + return "session" + return _SAFE_NAME_RE.sub("-", name).strip(".-") or "session" + + +def _default_worktree_path(runtime: Runtime, name: str | None) -> Path: + return runtime.session.dir / "worktrees" / _safe_name(name) + + +def _path_param_to_dest(runtime: Runtime, params: EnterWorktreeParams) -> Path | ToolReturnValue: + if params.path is None: + return _default_worktree_path(runtime, params.name) + raw = Path(params.path).expanduser() + if not raw.is_absolute(): + return tool_error( + "`path` must be absolute when provided.", + brief="Invalid worktree path", + status=ToolResultStatus.error, + ) + return raw + + +def _active_state(runtime: Runtime) -> _SessionWorktreeState | None: + """Return active worktree state, pruning stale process-local entries.""" + state_key = id(runtime) + state = _ACTIVE_WORKTREES.get(state_key) + if state is not None and runtime.work_dir != state.worktree_path: + _ACTIVE_WORKTREES.pop(state_key, None) + return None + return state + + +class EnterWorktree(CallableTool2[EnterWorktreeParams]): + name: str = "EnterWorktree" + description: str = load_desc(Path(__file__).parent / "enter_worktree.md") + params: type[EnterWorktreeParams] = EnterWorktreeParams + emits_tool_execution_started_after_approval: ClassVar[bool] = True + external_side_effect_tool: ClassVar[bool] = True + """Worktree creation mutates git state and must pass the external side-effect gate.""" + + def __init__(self, runtime: Runtime, toolset: PythinkerToolset) -> None: + super().__init__() + self._runtime = runtime + self._toolset = toolset + + @override + async def __call__(self, params: EnterWorktreeParams) -> ToolReturnValue: + if self._runtime.role != "root": + return tool_error( + "EnterWorktree is only available in the root session.", + brief="Worktree unavailable", + status=ToolResultStatus.denied, + ) + state_key = id(self._runtime) + state = _active_state(self._runtime) + if state is not None: + return tool_error( + f"A session worktree is already active at {state.worktree_path}. " + "Call ExitWorktree before entering another worktree.", + brief="Worktree already active", + ) + + dest_or_error = _path_param_to_dest(self._runtime, params) + if not isinstance(dest_or_error, Path): + return dest_or_error + dest = dest_or_error + original = self._runtime.work_dir + + approval = await self._runtime.approval.request( + self.name, + "create git worktree", + f"Create session git worktree `{dest}` from `{original}`", + ) + if not approval: + return approval.rejection_error() + + try: + from pythinker_code.soul.toolset import emit_current_tool_execution_started + + emit_current_tool_execution_started() + await create_agent_worktree(Path(str(original)), dest) + except WorktreeError as exc: + return tool_error( + str(exc), + brief="Worktree creation failed", + status=ToolResultStatus.failure, + ) + + worktree_host_path = HostPath.unsafe_from_local_path(dest.resolve()) + effective = self._toolset.set_work_dir_override(worktree_host_path) + if effective != worktree_host_path: + self._toolset.set_work_dir_override(None) + return tool_error( + "Worktree was created, but the session working directory could not be changed.", + brief="Worktree switch failed", + status=ToolResultStatus.failure, + ) + + _ACTIVE_WORKTREES[state_key] = _SessionWorktreeState( + original_work_dir=original, + worktree_path=worktree_host_path, + ) + output = "\n".join( + [ + "session_worktree: entered", + f"worktree_path: {worktree_host_path}", + f"original_work_dir: {original}", + "cleanup: retained until you remove it explicitly", + ] + ) + return ToolReturnValue( + is_error=False, + output=output, + message=f"Session working directory changed to {worktree_host_path}.", + display=[], + extras={"status": ToolResultStatus.success.value}, + ) + + +class ExitWorktree(CallableTool2[ExitWorktreeParams]): + name: str = "ExitWorktree" + description: str = load_desc(Path(__file__).parent / "exit_worktree.md") + params: type[ExitWorktreeParams] = ExitWorktreeParams + external_side_effect_tool: ClassVar[bool] = True + """Restoring session workdir is process-local but paired with a side-effecting tool.""" + + def __init__(self, runtime: Runtime, toolset: PythinkerToolset) -> None: + super().__init__() + self._runtime = runtime + self._toolset = toolset + + @override + async def __call__(self, params: ExitWorktreeParams) -> ToolReturnValue: + if self._runtime.role != "root": + return tool_error( + "ExitWorktree is only available in the root session.", + brief="Worktree unavailable", + status=ToolResultStatus.denied, + ) + state_key = id(self._runtime) + state = _active_state(self._runtime) + if state is None: + return tool_error( + "No session worktree is active.", + brief="No active worktree", + status=ToolResultStatus.failure, + ) + + effective = self._toolset.set_work_dir_override(None) + if effective != state.original_work_dir: + return tool_error( + "Could not restore the original working directory.", + brief="Worktree exit failed", + status=ToolResultStatus.failure, + ) + _ACTIVE_WORKTREES.pop(state_key, None) + output = "\n".join( + [ + "session_worktree: exited", + f"worktree_path: {state.worktree_path}", + f"restored_work_dir: {state.original_work_dir}", + "retained: true", + "cleanup: worktree was not deleted; remove it manually when finished", + ] + ) + return ToolReturnValue( + is_error=False, + output=output, + message=f"Session working directory restored to {state.original_work_dir}.", + display=[], + extras={"status": ToolResultStatus.success.value}, + ) diff --git a/src/pythinker_code/tools/worktree/enter_worktree.md b/src/pythinker_code/tools/worktree/enter_worktree.md new file mode 100644 index 00000000..f41df70e --- /dev/null +++ b/src/pythinker_code/tools/worktree/enter_worktree.md @@ -0,0 +1,9 @@ +Create a git worktree for the current repository and switch this root session's operational working +directory to it. + +Use this when you need to isolate a risky or parallel implementation attempt from the original +checkout. The switch is process-local: it affects this running session's tools, but it is not a +durable session migration. + +The tool never deletes worktrees. Use `ExitWorktree` to return to the original working directory; +remove the worktree manually after preserving or merging any work you want to keep. diff --git a/src/pythinker_code/tools/worktree/exit_worktree.md b/src/pythinker_code/tools/worktree/exit_worktree.md new file mode 100644 index 00000000..09bd980b --- /dev/null +++ b/src/pythinker_code/tools/worktree/exit_worktree.md @@ -0,0 +1,5 @@ +Return this root session from a previously entered session worktree to the original working +directory. + +This tool only restores the process-local working directory override. It intentionally leaves the +worktree directory intact so user work is never deleted silently. diff --git a/src/pythinker_code/wire/types.py b/src/pythinker_code/wire/types.py index 28353fd6..4a2e2d80 100644 --- a/src/pythinker_code/wire/types.py +++ b/src/pythinker_code/wire/types.py @@ -3,7 +3,14 @@ import asyncio from typing import Any, Literal, TypeGuard, cast -from pydantic import BaseModel, Field, field_serializer, field_validator, model_validator +from pydantic import ( + BaseModel, + ConfigDict, + Field, + field_serializer, + field_validator, + model_validator, +) from pythinker_core.chat_provider import TokenUsage from pythinker_core.message import ( AudioURLPart, @@ -286,6 +293,69 @@ class BtwEnd(BaseModel): """Error message if the side question failed.""" +class TodoListUpdated(BaseModel): + """Todo list state was persisted or refreshed (independent of ToolResult rendering).""" + + model_config = ConfigDict(frozen=True, extra="forbid") + + items: tuple[tuple[str, str], ...] + """Todo (title, status) pairs in display order.""" + complete: bool + """True when the list is closed (all done or cleared).""" + source: Literal["tool", "scratch", "compaction"] + """What produced this update.""" + + +class SubagentToolFallback(BaseModel): + """Agent tool launch was rejected before a subagent started.""" + + model_config = ConfigDict(frozen=True, extra="forbid") + + reason: Literal[ + "unavailable_agent_type", + "mcp_unavailable", + "policy_denied", + "timeout", + "exception", + ] + requested_type: str + available_types: tuple[str, ...] = () + """Known built-in types when the rejection is type-related.""" + + +class AgentListDelta(BaseModel): + """Dynamic agent-type listing for cache-stable prompt injection.""" + + model_config = ConfigDict(frozen=True, extra="forbid") + + items: tuple[str, ...] + """Formatted agent lines, or delta lines when ``complete`` is False.""" + complete: bool + """True closes the list; False means apply as a delta on the prior open list.""" + + +class ToolUseSkipped(BaseModel): + """A tool call did not execute normally (dedup, policy, interrupt, or busy skip).""" + + model_config = ConfigDict(frozen=True, extra="forbid") + + tool_call_id: str + tool_name: str + reason: Literal["dedup", "policy", "interrupt", "concurrent_inflight"] + resumed: bool = False + """True when the skip is a reuse/resume of an in-flight or prior result.""" + + +class ContextOverflowRecovered(BaseModel): + """Reactive context-overflow recovery attempt finished.""" + + model_config = ConfigDict(frozen=True, extra="forbid") + + outcome: Literal["recovered", "failed"] + trigger_step: int + """Step number that triggered the overflow rejection.""" + + class SubagentEvent(BaseModel): """ An event from a subagent. @@ -627,6 +697,11 @@ def resolved(self) -> bool: | PlanDisplay | BtwBegin | BtwEnd + | TodoListUpdated + | SubagentToolFallback + | AgentListDelta + | ToolUseSkipped + | ContextOverflowRecovered ) """Any event, including control flow and content/tooling events.""" @@ -781,6 +856,11 @@ def to_wire_message(self) -> WireMessage: "PlanDisplay", "BtwBegin", "BtwEnd", + "TodoListUpdated", + "SubagentToolFallback", + "AgentListDelta", + "ToolUseSkipped", + "ContextOverflowRecovered", "ApprovalRequest", "ToolCallRequest", "QuestionOption", diff --git a/tests/core/test_agent_list_injection.py b/tests/core/test_agent_list_injection.py new file mode 100644 index 00000000..17085788 --- /dev/null +++ b/tests/core/test_agent_list_injection.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +from pathlib import Path +from types import SimpleNamespace + +from pythinker_code.soul.agent import Runtime +from pythinker_code.subagents.models import AgentTypeDefinition, ToolPolicy +from pythinker_code.wire.types import AgentListDelta + + +def _type( + name: str, + when: str = "", + tools: tuple[str, ...] = (), +) -> AgentTypeDefinition: + return AgentTypeDefinition( + name=name, + description=f"{name} agent", + agent_file=Path(f"/tmp/{name}.yaml"), + when_to_use=when, + tool_policy=ToolPolicy(mode="allowlist", tools=tools) + if tools + else ToolPolicy(mode="inherit"), + ) + + +def test_format_agent_line_allowlist_only() -> None: + from pythinker_code.soul.dynamic_injections.agent_list import format_agent_line + + line = format_agent_line( + _type("explore", "Use for reconnaissance", ("pkg.tools:ReadFile", "pkg.tools:Glob")) + ) + + assert "`explore`" in line + assert "Use for reconnaissance" in line + assert "Tools: ReadFile, Glob" in line + + +def test_format_agent_line_no_restrictions() -> None: + from pythinker_code.soul.dynamic_injections.agent_list import format_agent_line + + line = format_agent_line(_type("coder", "Use for implementation")) + + assert "Tools: *" in line + + +async def test_provider_emits_root_agent_list_and_wire_delta(runtime: Runtime, monkeypatch) -> None: + from pythinker_code.soul.dynamic_injections.agent_list import AgentListInjectionProvider + + runtime.labor_market.add_builtin_type(_type("explore", "Use for reconnaissance")) + captured: list[object] = [] + monkeypatch.setattr( + "pythinker_code.soul.dynamic_injections.agent_list.wire_send", + lambda msg: captured.append(msg), + ) + soul = SimpleNamespace(runtime=runtime, is_subagent=False) + + injections = await AgentListInjectionProvider().get_injections([], soul) # type: ignore[arg-type] + + assert len(injections) == 1 + assert injections[0].type == "agent_list" + assert "`explore`" in injections[0].content + assert captured and isinstance(captured[0], AgentListDelta) + + +async def test_provider_is_root_only(runtime: Runtime) -> None: + from pythinker_code.soul.dynamic_injections.agent_list import AgentListInjectionProvider + + soul = SimpleNamespace(runtime=runtime, is_subagent=True) + + assert await AgentListInjectionProvider().get_injections([], soul) == [] # type: ignore[arg-type] diff --git a/tests/core/test_agent_spec.py b/tests/core/test_agent_spec.py index c3cb4075..98b6365b 100644 --- a/tests/core/test_agent_spec.py +++ b/tests/core/test_agent_spec.py @@ -36,6 +36,9 @@ def test_load_default_agent_spec(): "pythinker_code.tools.skill:ReadSkill", "pythinker_code.tools.ask_user:AskUserQuestion", "pythinker_code.tools.todo:SetTodoList", + "pythinker_code.tools.tool_search:ToolSearch", + "pythinker_code.tools.worktree:EnterWorktree", + "pythinker_code.tools.worktree:ExitWorktree", "pythinker_code.tools.goal:UpdateGoal", "pythinker_code.tools.progress:Progress", "pythinker_code.tools.suggest:Suggest", @@ -242,6 +245,9 @@ def test_load_default_agent_spec(): "pythinker_code.tools.skill:ReadSkill", "pythinker_code.tools.ask_user:AskUserQuestion", "pythinker_code.tools.todo:SetTodoList", + "pythinker_code.tools.tool_search:ToolSearch", + "pythinker_code.tools.worktree:EnterWorktree", + "pythinker_code.tools.worktree:ExitWorktree", "pythinker_code.tools.goal:UpdateGoal", "pythinker_code.tools.progress:Progress", "pythinker_code.tools.suggest:Suggest", @@ -370,6 +376,9 @@ def test_load_default_agent_spec(): "pythinker_code.tools.skill:ReadSkill", "pythinker_code.tools.ask_user:AskUserQuestion", "pythinker_code.tools.todo:SetTodoList", + "pythinker_code.tools.tool_search:ToolSearch", + "pythinker_code.tools.worktree:EnterWorktree", + "pythinker_code.tools.worktree:ExitWorktree", "pythinker_code.tools.goal:UpdateGoal", "pythinker_code.tools.progress:Progress", "pythinker_code.tools.suggest:Suggest", @@ -508,6 +517,9 @@ def test_load_default_agent_spec(): "pythinker_code.tools.skill:ReadSkill", "pythinker_code.tools.ask_user:AskUserQuestion", "pythinker_code.tools.todo:SetTodoList", + "pythinker_code.tools.tool_search:ToolSearch", + "pythinker_code.tools.worktree:EnterWorktree", + "pythinker_code.tools.worktree:ExitWorktree", "pythinker_code.tools.goal:UpdateGoal", "pythinker_code.tools.progress:Progress", "pythinker_code.tools.suggest:Suggest", @@ -631,6 +643,9 @@ def test_load_default_agent_spec(): "pythinker_code.tools.skill:ReadSkill", "pythinker_code.tools.ask_user:AskUserQuestion", "pythinker_code.tools.todo:SetTodoList", + "pythinker_code.tools.tool_search:ToolSearch", + "pythinker_code.tools.worktree:EnterWorktree", + "pythinker_code.tools.worktree:ExitWorktree", "pythinker_code.tools.goal:UpdateGoal", "pythinker_code.tools.progress:Progress", "pythinker_code.tools.suggest:Suggest", @@ -802,6 +817,9 @@ def test_load_agent_spec_default_extension(): "pythinker_code.tools.skill:ReadSkill", "pythinker_code.tools.ask_user:AskUserQuestion", "pythinker_code.tools.todo:SetTodoList", + "pythinker_code.tools.tool_search:ToolSearch", + "pythinker_code.tools.worktree:EnterWorktree", + "pythinker_code.tools.worktree:ExitWorktree", "pythinker_code.tools.goal:UpdateGoal", "pythinker_code.tools.progress:Progress", "pythinker_code.tools.suggest:Suggest", diff --git a/tests/core/test_config.py b/tests/core/test_config.py index cc46c9d7..3f50f443 100644 --- a/tests/core/test_config.py +++ b/tests/core/test_config.py @@ -52,6 +52,7 @@ def test_default_config_dump(): "max_consecutive_failures": 8, "max_truncation_recoveries": 3, "max_session_cost_usd": None, + "budget_nudge_ratio": 0.75, "max_retries_per_step": 3, "max_ralph_iterations": 0, "reserved_context_size": 50000, @@ -59,6 +60,7 @@ def test_default_config_dump(): "prune_trigger_ratio": 0.7, "prune_protect_last": 20, "prune_min_chars": 2000, + "prune_tool_result_max_chars": 0, }, "background": { "max_running_tasks": 4, @@ -88,6 +90,7 @@ def test_default_config_dump(): "injection_bus": True, "injection_ceiling_tokens": 2048, "harvest_on_compaction": False, + "harvest_on_stop": False, "journal_recaps": False, "consolidation": False, "durable_memory": False, diff --git a/tests/core/test_default_agent.py b/tests/core/test_default_agent.py index 0e3bc498..c519e0d9 100644 --- a/tests/core/test_default_agent.py +++ b/tests/core/test_default_agent.py @@ -309,6 +309,9 @@ async def test_default_agent_background_bash_guardrails(runtime: Runtime): "ReadSkill", "AskUserQuestion", "SetTodoList", + "ToolSearch", + "EnterWorktree", + "ExitWorktree", "UpdateGoal", "Progress", "Suggest", @@ -380,6 +383,13 @@ async def test_default_agent_background_bash_guardrails(runtime: Runtime): - Cross-check at least one load-bearing subagent finding before making changes from it. - The subagent result is only visible to you. If the user should see it, summarize it yourself. +**Prompt Hygiene** + +When spawning a fresh agent, brief it like a smart colleague who just walked in: include the goal, +what was tried, what is in and out of scope, the expected output contract, and how the result will +be verified. For lookups, pass the exact command or symbol; for investigations, pass the question, +not a prescribed sequence of steps. + **Agent Workflow Design** Use subagents as focused logical roles, not just extra tool capacity: @@ -435,6 +445,8 @@ async def test_default_agent_background_bash_guardrails(runtime: Runtime): - Only genuinely broad, cross-cutting work → more, up to the `RunAgents` cap of 8. Prefer the fewest children that cover the independent objectives — the cap of 8 is a ceiling, not a target. Over-provisioning burns the multi-agent token premium (a fan-out can cost several times a single thread) and produces results you then have to reconcile. Do not launch a subagent for what one or two direct reads or greps would answer. + +When spawning a fresh agent, brief it like a smart colleague who just walked in — include the goal, what was tried, what is in/out of scope, the expected output contract, and how the result will be verified. Lookups: pass the exact command. Investigations: pass the question, not prescribed steps. """ ) assert agent.toolset.tools[0].parameters == snapshot( diff --git a/tests/core/test_memory_phase_bcd.py b/tests/core/test_memory_phase_bcd.py index b085fbec..b4aeb36a 100644 --- a/tests/core/test_memory_phase_bcd.py +++ b/tests/core/test_memory_phase_bcd.py @@ -2,10 +2,16 @@ import time from pathlib import Path +from unittest.mock import AsyncMock +import pytest from pythinker_core.message import Message, TextPart +from pythinker_core.tooling.empty import EmptyToolset from pythinker_host.path import HostPath +import pythinker_code.soul.pythinkersoul as pythinkersoul_module # alias used for monkeypatch +from pythinker_code import scratchpad +from pythinker_code.config import Config from pythinker_code.memory.consolidation import generate_inbox_candidates from pythinker_code.memory.harvest import CompactionHarvester from pythinker_code.memory.recap import build_session_recap, content_hash @@ -17,18 +23,34 @@ ) from pythinker_code.project_memory import ProjectMemoryStore from pythinker_code.session_state import SessionState, TodoItemState +from pythinker_code.soul.agent import Agent, Runtime +from pythinker_code.soul.context import Context + +PythinkerSoul = pythinkersoul_module.PythinkerSoul +TurnOutcome = pythinkersoul_module.TurnOutcome def _hp(p: Path) -> HostPath: return HostPath.unsafe_from_local_path(p) +@pytest.fixture(autouse=True) +def _reset_scratchpad_verification(): + scratchpad._VERIFIED_WORK_DIRS.clear() + yield + scratchpad._VERIFIED_WORK_DIRS.clear() + + def test_content_hash_is_stable_and_normalized(): assert content_hash(tier="memory", title="T", body="Body") == content_hash( tier=" MEMORY ", title="t", body=" body " ) +def test_stop_time_memory_harvest_defaults_off(): + assert Config().memory.harvest_on_stop is False + + async def test_append_journal_prepends_and_deduplicates(tmp_path, monkeypatch): monkeypatch.setenv("PYTHINKER_SHARE_DIR", str(tmp_path / "share")) store = ProjectMemoryStore(_hp(tmp_path / "repo")) @@ -150,3 +172,140 @@ async def test_generate_inbox_candidates_ignores_corrupt_duplicate_file(tmp_path (inbox / f"{first[0].id}.json").write_text("{not json", encoding="utf-8") assert await generate_inbox_candidates(store, _hp(repo)) == [] + + +def _make_memory_soul(runtime: Runtime, tmp_path: Path) -> PythinkerSoul: + runtime.work_dir_override = _hp(tmp_path) + agent = Agent( + name="Memory Stop Agent", + system_prompt="Test prompt.", + toolset=EmptyToolset(), + runtime=runtime, + ) + return PythinkerSoul(agent, context=Context(file_backend=tmp_path / "history.jsonl")) + + +async def test_stop_time_memory_harvest_default_off_noops( + runtime: Runtime, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +): + monkeypatch.setattr("pythinker_code.scratchpad._is_local_host", lambda: True) + monkeypatch.setattr("pythinker_code.scratchpad._is_verified", lambda _wd: True) + monkeypatch.setattr(pythinkersoul_module, "wire_send", lambda _msg: None) + monkeypatch.setattr(runtime.oauth, "ensure_fresh", AsyncMock()) + soul = _make_memory_soul(runtime, tmp_path) + + async def fake_turn(user_message: Message) -> TurnOutcome: + await soul.context.append_message(user_message) + final = Message(role="assistant", content=[TextPart(text="Decision: do not persist")]) + await soul.context.append_message(final) + return TurnOutcome(stop_reason="no_tool_calls", final_message=final, step_count=1) + + monkeypatch.setattr(soul, "_turn", fake_turn) + + await soul.run("remember nothing") + + assert not (tmp_path / ".pythinker" / "scratch").exists() + + +async def test_stop_time_memory_harvest_opt_in_stages_sanitized_deduped_notes( + runtime: Runtime, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +): + runtime.config.memory.harvest_on_stop = True + monkeypatch.setattr("pythinker_code.scratchpad._is_local_host", lambda: True) + monkeypatch.setattr("pythinker_code.scratchpad._is_verified", lambda _wd: True) + monkeypatch.setattr(pythinkersoul_module, "wire_send", lambda _msg: None) + monkeypatch.setattr(runtime.oauth, "ensure_fresh", AsyncMock()) + soul = _make_memory_soul(runtime, tmp_path) + + async def fake_turn(user_message: Message) -> TurnOutcome: + await soul.context.append_message(user_message) + final = Message( + role="assistant", + content=[ + TextPart( + text=( + "Decision: stage this fact\n" + "Decision: stage this fact\n" + "Next: do not stage\n" + "Next: run focused memory tests" + ) + ) + ], + ) + await soul.context.append_message(final) + return TurnOutcome(stop_reason="no_tool_calls", final_message=final, step_count=1) + + monkeypatch.setattr(soul, "_turn", fake_turn) + + await soul.run("remember safely") + + files = list((tmp_path / ".pythinker" / "scratch").glob("*.md")) + assert len(files) == 1 + text = files[0].read_text(encoding="utf-8") + assert text.count("stage this fact") == 1 + assert "run focused memory tests" in text + assert "do not stage" not in text + assert "source:stop" in text + + +async def test_stop_time_memory_harvest_failure_does_not_break_turn( + runtime: Runtime, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +): + runtime.config.memory.harvest_on_stop = True + monkeypatch.setattr(pythinkersoul_module, "wire_send", lambda _msg: None) + monkeypatch.setattr(runtime.oauth, "ensure_fresh", AsyncMock()) + append_note = AsyncMock(side_effect=RuntimeError("scratch unavailable")) + monkeypatch.setattr( + "pythinker_code.scratchpad.append_scratch_note", + append_note, + ) + soul = _make_memory_soul(runtime, tmp_path) + + async def fake_turn(user_message: Message) -> TurnOutcome: + await soul.context.append_message(user_message) + final = Message(role="assistant", content=[TextPart(text="Decision: resilient turn")]) + await soul.context.append_message(final) + return TurnOutcome(stop_reason="no_tool_calls", final_message=final, step_count=1) + + monkeypatch.setattr(soul, "_turn", fake_turn) + + await soul.run("do not crash") + + append_note.assert_awaited_once() + assert soul.context.history[-1].extract_text(" ") == "Decision: resilient turn" + + +async def test_stop_time_memory_harvest_non_appended_note_does_not_rearm( + runtime: Runtime, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +): + runtime.config.memory.harvest_on_stop = True + monkeypatch.setattr(pythinkersoul_module, "wire_send", lambda _msg: None) + monkeypatch.setattr(runtime.oauth, "ensure_fresh", AsyncMock()) + append_note = AsyncMock( + return_value=scratchpad.ScratchpadAppendResult(False, "disabled_not_ignored") + ) + monkeypatch.setattr("pythinker_code.scratchpad.append_scratch_note", append_note) + soul = _make_memory_soul(runtime, tmp_path) + rearmed: list[str] = [] + monkeypatch.setattr(soul, "rearm_injection", lambda key: rearmed.append(key)) + + async def fake_turn(user_message: Message) -> TurnOutcome: + await soul.context.append_message(user_message) + final = Message(role="assistant", content=[TextPart(text="Decision: refused note")]) + await soul.context.append_message(final) + return TurnOutcome(stop_reason="no_tool_calls", final_message=final, step_count=1) + + monkeypatch.setattr(soul, "_turn", fake_turn) + + await soul.run("safe refusal") + + append_note.assert_awaited_once() + assert rearmed == [] diff --git a/tests/core/test_microcompact.py b/tests/core/test_microcompact.py new file mode 100644 index 00000000..8cae9eef --- /dev/null +++ b/tests/core/test_microcompact.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +import pytest +from pythinker_core.message import Message, TextPart +from pythinker_core.tooling.simple import SimpleToolset + +from pythinker_code.config import LoopControl +from pythinker_code.soul.agent import Agent, Runtime +from pythinker_code.soul.compaction import ( + cap_stale_tool_result_bodies, + estimate_text_tokens, +) +from pythinker_code.soul.context import Context +from pythinker_code.soul.pythinkersoul import PythinkerSoul + + +def _tool(text: str, call_id: str) -> Message: + return Message(role="tool", content=text, tool_call_id=call_id) + + +def _old_history(message: Message) -> list[Message]: + return [message, *[Message(role="user", content=f"m{i}") for i in range(8)]] + + +def test_below_budget_is_unchanged() -> None: + history = _old_history(_tool("x" * 80, "c1")) + + capped, freed = cap_stale_tool_result_bodies(history, protect_last=2, max_chars=100) + + assert freed == 0 + assert capped == history + + +def test_old_large_tool_output_is_capped_with_placeholder() -> None: + body = "a" * 120 + "END" + history = _old_history(_tool(body, "c1")) + + capped, freed = cap_stale_tool_result_bodies(history, protect_last=2, max_chars=80) + + tool_msg = capped[0] + text = tool_msg.extract_text("") + assert freed == len(body) - len(text) + assert len(text) <= 80 + assert text.startswith("a") + assert "tool output capped" in text + assert tool_msg.role == "tool" + assert tool_msg.tool_call_id == "c1" + assert len(capped) == len(history) + + +def test_recent_protected_messages_are_preserved() -> None: + body = "recent" * 40 + history = [Message(role="user", content="old"), _tool(body, "recent")] + + capped, freed = cap_stale_tool_result_bodies(history, protect_last=2, max_chars=50) + + assert freed == 0 + assert capped == history + + +def test_non_tool_messages_are_untouched() -> None: + assistant = Message(role="assistant", content=[TextPart(text="a" * 200)]) + history = _old_history(assistant) + + capped, freed = cap_stale_tool_result_bodies(history, protect_last=2, max_chars=50) + + assert freed == 0 + assert capped == history + + +def test_freed_token_accounting_is_sane() -> None: + history = _old_history(_tool("b" * 400, "c1")) + + capped, freed = cap_stale_tool_result_bodies(history, protect_last=2, max_chars=100) + + assert freed > 0 + assert estimate_text_tokens(capped) < estimate_text_tokens(history) + + +def test_loop_control_disables_microcompact_by_default() -> None: + assert LoopControl().prune_tool_result_max_chars == 0 + + +def _make_soul(runtime: Runtime, tmp_path) -> tuple[Context, PythinkerSoul]: + agent = Agent( + name="Microcompact", system_prompt="sys", toolset=SimpleToolset(), runtime=runtime + ) + context = Context(file_backend=tmp_path / "history.jsonl") + return context, PythinkerSoul(agent, context=context) + + +@pytest.mark.asyncio +async def test_prune_context_applies_tool_result_budget(runtime, tmp_path) -> None: + runtime.config.loop_control.prune_protect_last = 2 + runtime.config.loop_control.prune_min_chars = 10_000 + runtime.config.loop_control.prune_tool_result_max_chars = 100 + context, soul = _make_soul(runtime, tmp_path) + await context.write_system_prompt("sys") + await context.append_message( + [ + Message(role="user", content="go"), + Message(role="tool", content="x" * 500, tool_call_id="c1"), + Message(role="user", content="latest"), + Message(role="assistant", content=[TextPart(text="done")]), + ] + ) + before = soul.context.token_count + + did_prune = await soul.prune_context() + + tool_msg = next(m for m in soul.context.history if m.role == "tool") + text = tool_msg.extract_text("") + assert did_prune is True + assert len(text) <= 100 + assert "tool output capped" in text + assert tool_msg.tool_call_id == "c1" + assert soul.context.history[-1].extract_text("") == "done" + assert soul.context.token_count <= before diff --git a/tests/core/test_overflow_recovery.py b/tests/core/test_overflow_recovery.py index 2d470e6f..58ef885b 100644 --- a/tests/core/test_overflow_recovery.py +++ b/tests/core/test_overflow_recovery.py @@ -9,6 +9,7 @@ from __future__ import annotations +from contextvars import Token from unittest.mock import AsyncMock import pytest @@ -17,6 +18,20 @@ from pythinker_code.soul.agent import Agent, Runtime from pythinker_code.soul.context import Context from pythinker_code.soul.pythinkersoul import PythinkerSoul +from pythinker_code.wire import Wire + + +def _wire_context() -> Token[Wire | None]: + import pythinker_code.soul as soul_module + + wire = Wire() + return soul_module._current_wire.set(wire) + + +def _reset_wire_context(token: Token[Wire | None]) -> None: + import pythinker_code.soul as soul_module + + soul_module._current_wire.reset(token) def _make_soul(runtime: Runtime, tmp_path) -> PythinkerSoul: @@ -39,8 +54,12 @@ async def test_prunes_compacts_and_reports_recovered(self, runtime, tmp_path) -> soul = _make_soul(runtime, tmp_path) soul.prune_context = AsyncMock(return_value=True) # type: ignore[method-assign] soul.compact_context = AsyncMock() # type: ignore[method-assign] + wire_token = _wire_context() - recovered = await soul._recover_from_context_overflow(step_no=3) + try: + recovered = await soul._recover_from_context_overflow(step_no=3) + finally: + _reset_wire_context(wire_token) assert recovered is True soul.prune_context.assert_awaited_once() @@ -51,8 +70,12 @@ async def test_prune_failure_does_not_block_compaction(self, runtime, tmp_path) soul = _make_soul(runtime, tmp_path) soul.prune_context = AsyncMock(side_effect=RuntimeError("prune broke")) # type: ignore[method-assign] soul.compact_context = AsyncMock() # type: ignore[method-assign] + wire_token = _wire_context() - recovered = await soul._recover_from_context_overflow(step_no=3) + try: + recovered = await soul._recover_from_context_overflow(step_no=3) + finally: + _reset_wire_context(wire_token) assert recovered is True soul.compact_context.assert_awaited_once() @@ -62,7 +85,11 @@ async def test_compaction_failure_reports_not_recovered(self, runtime, tmp_path) soul = _make_soul(runtime, tmp_path) soul.prune_context = AsyncMock(return_value=False) # type: ignore[method-assign] soul.compact_context = AsyncMock(side_effect=RuntimeError("compact broke")) # type: ignore[method-assign] + wire_token = _wire_context() - recovered = await soul._recover_from_context_overflow(step_no=3) + try: + recovered = await soul._recover_from_context_overflow(step_no=3) + finally: + _reset_wire_context(wire_token) assert recovered is False diff --git a/tests/core/test_permission_profiles.py b/tests/core/test_permission_profiles.py index c81e8df3..5b9c38a9 100644 --- a/tests/core/test_permission_profiles.py +++ b/tests/core/test_permission_profiles.py @@ -451,7 +451,7 @@ async def test_plan_only_execution_profile_limits_subagent_types(runtime: Runtim with tool_call_context("Agent"): tool = AgentTool(runtime) denied = await tool( - tool.params(description="implement fix", prompt="write code", subagent_type="coder") + tool.params(description="implement fix", prompt="write code", subagent_type="mocker") ) assert denied.is_error diff --git a/tests/core/test_pythinkersoul_retry_recovery.py b/tests/core/test_pythinkersoul_retry_recovery.py index cce311c4..5e46d38d 100644 --- a/tests/core/test_pythinkersoul_retry_recovery.py +++ b/tests/core/test_pythinkersoul_retry_recovery.py @@ -27,7 +27,7 @@ from pythinker_code.soul.pythinkersoul import PythinkerSoul from pythinker_code.utils.aioqueue import QueueShutDown from pythinker_code.wire import Wire -from pythinker_code.wire.types import StepBegin, StepRetry +from pythinker_code.wire.types import ContextOverflowRecovered, StepBegin, StepRetry class StaticStreamedMessage: @@ -160,6 +160,62 @@ def with_thinking(self, effort: ThinkingEffort) -> Self: return self +class ContextOverflowThenSuccessProvider: + name = "context-overflow-then-success" + + def __init__(self) -> None: + self.generate_attempts = 0 + + @property + def model_name(self) -> str: + return "context-overflow-then-success" + + @property + def thinking_effort(self) -> ThinkingEffort | None: + return None + + async def generate( + self, + system_prompt: str, + tools: Sequence[Tool], + history: Sequence[Message], + ) -> StaticStreamedMessage: + self.generate_attempts += 1 + if self.generate_attempts == 1: + raise APIStatusError(413, "context length exceeded") + return StaticStreamedMessage([TextPart(text="recovered after overflow")]) + + def with_thinking(self, effort: ThinkingEffort) -> Self: + return self + + +class AlwaysContextOverflowProvider: + name = "always-context-overflow" + + def __init__(self) -> None: + self.generate_attempts = 0 + + @property + def model_name(self) -> str: + return "always-context-overflow" + + @property + def thinking_effort(self) -> ThinkingEffort | None: + return None + + async def generate( + self, + system_prompt: str, + tools: Sequence[Tool], + history: Sequence[Message], + ) -> StaticStreamedMessage: + self.generate_attempts += 1 + raise APIStatusError(413, "context length exceeded") + + def with_thinking(self, effort: ThinkingEffort) -> Self: + return self + + class PartialStreamThenStatusErrorProvider: name = "partial-stream-then-status-error" @@ -431,6 +487,119 @@ async def test_step_status_error_still_uses_tenacity_retries( assert context.history[-1].extract_text(" ").strip() == "status recovered" +@pytest.mark.asyncio +async def test_context_overflow_recovery_emits_recovered_event( + runtime: Runtime, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + runtime.config.loop_control.max_retries_per_step = 1 + provider = ContextOverflowThenSuccessProvider() + llm = LLM( + chat_provider=provider, + max_context_size=100_000, + capabilities=set(), + ) + soul, context = _make_soul(runtime, llm, tmp_path) + compact_calls = 0 + + async def fake_prune_context() -> bool: + return False + + async def fake_compact_context() -> None: + nonlocal compact_calls + compact_calls += 1 + + monkeypatch.setattr(soul, "prune_context", fake_prune_context) + monkeypatch.setattr(soul, "compact_context", fake_compact_context) + seen: list[object] = [] + + await run_soul( + soul, + "trigger context overflow", + lambda wire: _collect_ui_messages(wire, seen), + asyncio.Event(), + ) + + assert provider.generate_attempts == 2 + assert compact_calls == 1 + assert context.history[-1].extract_text(" ").strip() == "recovered after overflow" + assert [msg for msg in seen if isinstance(msg, ContextOverflowRecovered)] == [ + ContextOverflowRecovered(outcome="recovered", trigger_step=1) + ] + + +@pytest.mark.asyncio +async def test_context_overflow_recovery_emits_failed_event( + runtime: Runtime, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + runtime.config.loop_control.max_retries_per_step = 1 + provider = AlwaysContextOverflowProvider() + llm = LLM( + chat_provider=provider, + max_context_size=100_000, + capabilities=set(), + ) + soul, _ = _make_soul(runtime, llm, tmp_path) + + async def fake_compact_context() -> None: + raise RuntimeError("compact failed") + + monkeypatch.setattr(soul, "compact_context", fake_compact_context) + seen: list[object] = [] + + with pytest.raises(APIStatusError): + await run_soul( + soul, + "trigger context overflow", + lambda wire: _collect_ui_messages(wire, seen), + asyncio.Event(), + ) + + assert provider.generate_attempts == 1 + assert [msg for msg in seen if isinstance(msg, ContextOverflowRecovered)] == [ + ContextOverflowRecovered(outcome="failed", trigger_step=1) + ] + + +@pytest.mark.asyncio +async def test_context_overflow_recovery_is_one_shot_per_turn( + runtime: Runtime, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + runtime.config.loop_control.max_retries_per_step = 1 + provider = AlwaysContextOverflowProvider() + llm = LLM( + chat_provider=provider, + max_context_size=100_000, + capabilities=set(), + ) + soul, _ = _make_soul(runtime, llm, tmp_path) + compact_calls = 0 + + async def fake_prune_context() -> bool: + return False + + async def fake_compact_context() -> None: + nonlocal compact_calls + compact_calls += 1 + + monkeypatch.setattr(soul, "prune_context", fake_prune_context) + monkeypatch.setattr(soul, "compact_context", fake_compact_context) + seen: list[object] = [] + + with pytest.raises(APIStatusError): + await run_soul( + soul, + "trigger context overflow twice", + lambda wire: _collect_ui_messages(wire, seen), + asyncio.Event(), + ) + + assert provider.generate_attempts == 2 + assert compact_calls == 1 + assert [msg for msg in seen if isinstance(msg, ContextOverflowRecovered)] == [ + ContextOverflowRecovered(outcome="recovered", trigger_step=1) + ] + + @pytest.mark.asyncio async def test_step_retry_event_after_partial_stream(runtime: Runtime, tmp_path: Path) -> None: runtime.config.loop_control.max_retries_per_step = 2 @@ -526,3 +695,69 @@ async def test_step_connection_recovery_then_401_triggers_oauth_refresh( assert context.history[-1].extract_text(" ").strip() == "auth recovered" assert len(refresh_mock.await_args_list) == 2 assert any(call.kwargs.get("force") is True for call in refresh_mock.await_args_list) + + +class OverflowThenOkProvider: + name = "overflow-then-ok" + + def __init__(self) -> None: + self.generate_attempts = 0 + + @property + def model_name(self) -> str: + return "overflow-then-ok" + + @property + def thinking_effort(self) -> ThinkingEffort | None: + return None + + async def generate( + self, + system_prompt: str, + tools: Sequence[Tool], + history: Sequence[Message], + ) -> StaticStreamedMessage: + self.generate_attempts += 1 + if self.generate_attempts == 1: + raise APIStatusError(400, "context length exceeded") + return StaticStreamedMessage([TextPart(text="recovered after reactive compact")]) + + def on_retryable_error(self, error: BaseException) -> bool: + _ = error + return False + + def with_thinking(self, effort: ThinkingEffort) -> Self: + return self + + +class TestReactiveOverflowRecovery: + @pytest.mark.asyncio + async def test_context_overflow_triggers_one_shot_reactive_recovery( + self, runtime: Runtime, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + provider = OverflowThenOkProvider() + llm = LLM(chat_provider=provider, max_context_size=100_000, capabilities=set()) + soul, context = _make_soul(runtime, llm, tmp_path) + seen: list[object] = [] + compact_calls = 0 + + async def _count_compact() -> None: + nonlocal compact_calls + compact_calls += 1 + + monkeypatch.setattr(soul, "compact_context", _count_compact) + + await run_soul( + soul, + "overflow then recover", + lambda wire: _collect_ui_messages(wire, seen), + asyncio.Event(), + ) + + assert provider.generate_attempts == 2 + assert compact_calls == 1 + assert context.history[-1].extract_text(" ").strip() == "recovered after reactive compact" + recovered = [msg for msg in seen if isinstance(msg, ContextOverflowRecovered)] + assert len(recovered) == 1 + assert recovered[0].outcome == "recovered" + assert recovered[0].trigger_step == 1 diff --git a/tests/core/test_pythinkersoul_steer.py b/tests/core/test_pythinkersoul_steer.py index 694e91f4..f8a924a6 100644 --- a/tests/core/test_pythinkersoul_steer.py +++ b/tests/core/test_pythinkersoul_steer.py @@ -82,6 +82,18 @@ def _is_permissions_state_injection(message: Message) -> bool: ) +def _is_agent_list_injection(message: Message) -> bool: + return ( + message.role == "user" + and is_system_reminder_message(message) + and "Available agent types" in message.extract_text(" ") + ) + + +def _is_dynamic_injection(message: Message) -> bool: + return _is_permissions_state_injection(message) or _is_agent_list_injection(message) + + def _llm_with_capabilities(runtime: Runtime, capabilities: set[ModelCapability]) -> LLM: assert runtime.llm is not None return LLM( @@ -144,7 +156,7 @@ async def test_consume_pending_steers_appends_history_before_emitting_wire_event sent: list[SteerInput] = [] def fake_wire_send(msg) -> None: - persisted = [m for m in soul.context.history if not _is_permissions_state_injection(m)] + persisted = [m for m in soul.context.history if not _is_dynamic_injection(m)] assert persisted == [Message(role="user", content=[TextPart(text="Follow up now.")])] assert isinstance(msg, SteerInput) sent.append(msg) @@ -522,7 +534,7 @@ async def ui_loop(wire: Wire) -> None: await run_soul(soul, "original question", ui_loop, asyncio.Event()) - persisted = [m for m in soul.context.history if not _is_permissions_state_injection(m)] + persisted = [m for m in soul.context.history if not _is_dynamic_injection(m)] assert persisted == [ Message(role="user", content=[TextPart(text="original question")]), Message(role="assistant", content=[TextPart(text="first answer")]), diff --git a/tests/core/test_pythinkersoul_turn_balance.py b/tests/core/test_pythinkersoul_turn_balance.py index 0af7900d..930a11bb 100644 --- a/tests/core/test_pythinkersoul_turn_balance.py +++ b/tests/core/test_pythinkersoul_turn_balance.py @@ -3,6 +3,7 @@ import asyncio from pathlib import Path from types import SimpleNamespace +from unittest.mock import AsyncMock import pytest from pythinker_core import StepResult @@ -15,7 +16,7 @@ from pythinker_code.soul.approval import Approval from pythinker_code.soul.context import Context from pythinker_code.soul.dynamic_injection import DynamicInjection -from pythinker_code.soul.pythinkersoul import PythinkerSoul +from pythinker_code.soul.pythinkersoul import PythinkerSoul, TurnOutcome from pythinker_code.wire.types import StepBegin, StepInterrupted, TextPart, TurnBegin, TurnEnd @@ -119,6 +120,96 @@ async def fake_trigger(*args, **kwargs): ] +@pytest.mark.asyncio +async def test_turn_appends_budget_nudge_after_crossing_ratio( + runtime: Runtime, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + runtime.config.loop_control.max_session_cost_usd = 1.0 + runtime.config.loop_control.budget_nudge_ratio = 0.75 + soul = _make_soul(runtime, tmp_path) + soul._session_cost_usd = 0.70 + calls = 0 + + async def fake_agent_loop() -> TurnOutcome: + nonlocal calls + calls += 1 + soul._session_cost_usd = 0.80 + final_message = Message(role="assistant", content=[TextPart(text="done")]) + await soul.context.append_message(final_message) + return TurnOutcome(stop_reason="no_tool_calls", final_message=final_message, step_count=1) + + monkeypatch.setattr(soul, "_agent_loop", fake_agent_loop) + monkeypatch.setattr(soul, "_checkpoint", AsyncMock()) + + await soul.turn(Message(role="user", content=[TextPart(text="go")])) + + assert calls == 1 # nudge is context-only; it must not auto-continue the turn + nudge = soul.context.history[-1] + assert nudge.role == "user" + text = nudge.extract_text(" ") + assert "system-reminder" in text + assert "75%" in text + assert "spend ceiling" in text + assert len(text) <= 500 + + +@pytest.mark.asyncio +async def test_turn_does_not_append_budget_nudge_when_budget_exhausted( + runtime: Runtime, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + runtime.config.loop_control.max_session_cost_usd = 1.0 + runtime.config.loop_control.budget_nudge_ratio = 0.75 + soul = _make_soul(runtime, tmp_path) + soul._session_cost_usd = 0.99 + + async def fake_agent_loop() -> TurnOutcome: + soul._session_cost_usd = 1.00 + final_message = Message(role="assistant", content=[TextPart(text="budget hit")]) + await soul.context.append_message(final_message) + return TurnOutcome( + stop_reason="budget_exhausted", final_message=final_message, step_count=1 + ) + + monkeypatch.setattr(soul, "_agent_loop", fake_agent_loop) + monkeypatch.setattr(soul, "_checkpoint", AsyncMock()) + + await soul.turn(Message(role="user", content=[TextPart(text="go")])) + + assert "budget hit" in soul.context.history[-1].extract_text(" ") + assert "system-reminder" not in soul.context.history[-1].extract_text(" ") + + +@pytest.mark.asyncio +async def test_turn_does_not_append_budget_nudge_when_goal_auto_continue_enabled( + runtime: Runtime, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + runtime.config.loop_control.max_session_cost_usd = 1.0 + runtime.config.loop_control.budget_nudge_ratio = 0.75 + runtime.config.goal.auto_continue = True + soul = _make_soul(runtime, tmp_path) + soul._session_cost_usd = 0.70 + + async def fake_agent_loop() -> TurnOutcome: + soul._session_cost_usd = 0.80 + final_message = Message(role="assistant", content=[TextPart(text="goal continues")]) + await soul.context.append_message(final_message) + return TurnOutcome(stop_reason="no_tool_calls", final_message=final_message, step_count=1) + + monkeypatch.setattr(soul, "_agent_loop", fake_agent_loop) + monkeypatch.setattr(soul, "_checkpoint", AsyncMock()) + + await soul.turn(Message(role="user", content=[TextPart(text="go")])) + + assert "goal continues" in soul.context.history[-1].extract_text(" ") + assert "system-reminder" not in soul.context.history[-1].extract_text(" ") + + @pytest.mark.asyncio async def test_step_persists_assistant_message_when_tool_results_cancelled( runtime: Runtime, @@ -237,3 +328,33 @@ async def slow_grow(result, results): tool_messages = [m for m in history if m.role == "tool"] assert tool_messages, f"marker write was orphaned; history={history}" assert tool_messages[0].tool_call_id == tool_call.id + + +@pytest.mark.asyncio +async def test_token_budget_nudge_does_not_fire_when_under_threshold( + runtime: Runtime, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + runtime.config.loop_control.max_session_cost_usd = 10.0 + runtime.config.goal.auto_continue = False + soul = _make_soul(runtime, tmp_path) + soul._session_cost_usd = 0.5 + + async def fake_agent_loop() -> TurnOutcome: + return TurnOutcome( + stop_reason="no_tool_calls", + final_message=Message(role="assistant", content=[TextPart(text="done")]), + step_count=1, + ) + + async def fake_checkpoint() -> None: + return None + + monkeypatch.setattr(soul, "_agent_loop", fake_agent_loop) + monkeypatch.setattr(soul, "_checkpoint", fake_checkpoint) + + before = len(soul.context.history) + await soul.turn(Message(role="user", content=[TextPart(text="hello")])) + assert len(soul.context.history) == before + 1 + assert not any("spend ceiling" in message.extract_text(" ") for message in soul.context.history) diff --git a/tests/core/test_toolset.py b/tests/core/test_toolset.py index cfc7a625..9fa48a50 100644 --- a/tests/core/test_toolset.py +++ b/tests/core/test_toolset.py @@ -5,8 +5,9 @@ import asyncio import contextlib import json +from pathlib import Path from types import SimpleNamespace -from typing import Any, cast +from typing import Any, ClassVar, cast import mcp from pydantic import BaseModel @@ -14,7 +15,16 @@ from pythinker_core.tooling.error import ToolNotFoundError as PythinkerCoreToolNotFoundError from pythinker_code.soul.toolset import MCPTool, PythinkerToolset, _configure_mcp_client_stderr_log -from pythinker_code.wire.types import ToolCall, ToolResult +from pythinker_code.wire.types import ToolCall, ToolResult, ToolUseSkipped + + +class _RecordingWire: + def __init__(self, captured: list[object]) -> None: + self.soul_side = self + self._captured = captured + + def send(self, msg: object) -> None: + self._captured.append(msg) class DummyParams(BaseModel): @@ -309,6 +319,73 @@ async def test_same_step_dedup(): assert ts.end_step() == [("ToolA", '{"value":"x"}'), ("ToolA", '{"value":"x"}')] +async def test_same_step_dedup_default_off_does_not_emit_tool_use_skipped(monkeypatch): + """Unflagged tools still dedup, but do not emit the opt-in skip wire event.""" + ts = _make_toolset() + ts.begin_step([]) + captured: list[object] = [] + monkeypatch.setattr("pythinker_code.soul.get_wire_or_none", lambda: _RecordingWire(captured)) + + args = json.dumps({"value": "x"}) + result_1 = ts.handle( + ToolCall( + id="tc-dedup-1", + function=ToolCall.FunctionBody(name="ToolA", arguments=args), + ) + ) + result_2 = ts.handle( + ToolCall( + id="tc-dedup-2", + function=ToolCall.FunctionBody(name="ToolA", arguments=args), + ) + ) + assert isinstance(result_1, asyncio.Task) + assert isinstance(result_2, asyncio.Task) + + await asyncio.gather(result_1, result_2) + + assert [msg for msg in captured if isinstance(msg, ToolUseSkipped)] == [] + + +async def test_same_step_dedup_opt_in_emits_tool_use_skipped(monkeypatch): + """A same-step duplicate reuses the original task and emits only for opt-in tools.""" + ts = _make_toolset() + tool = ts.find("ToolA") + assert tool is not None + object.__setattr__(tool, "emits_tool_use_skipped", True) + ts.begin_step([]) + captured: list[object] = [] + monkeypatch.setattr("pythinker_code.soul.get_wire_or_none", lambda: _RecordingWire(captured)) + + args = json.dumps({"value": "x"}) + result_1 = ts.handle( + ToolCall( + id="tc-dedup-1", + function=ToolCall.FunctionBody(name="ToolA", arguments=args), + ) + ) + result_2 = ts.handle( + ToolCall( + id="tc-dedup-2", + function=ToolCall.FunctionBody(name="ToolA", arguments=args), + ) + ) + assert isinstance(result_1, asyncio.Task) + assert isinstance(result_2, asyncio.Task) + + await asyncio.gather(result_1, result_2) + + skipped = [msg for msg in captured if isinstance(msg, ToolUseSkipped)] + assert skipped == [ + ToolUseSkipped( + tool_call_id="tc-dedup-2", + tool_name="ToolA", + reason="dedup", + resumed=True, + ) + ] + + async def test_same_step_dedup_canonicalizes_argument_key_order(): """Equivalent JSON objects with different key order should share the original result.""" ts = _make_toolset() @@ -400,6 +477,77 @@ async def test_cross_step_duplicate_appends_reminder_at_three_consecutive(): assert "repeated_times" not in output +async def test_cross_step_duplicate_opt_in_emits_tool_use_skipped(monkeypatch): + """The cross-step dedup reminder emits a non-resumed skip event only for opt-in tools.""" + ts = _make_toolset() + tool = ts.find("ToolA") + assert tool is not None + object.__setattr__(tool, "emits_tool_use_skipped", True) + captured: list[object] = [] + monkeypatch.setattr("pythinker_code.soul.get_wire_or_none", lambda: _RecordingWire(captured)) + args = json.dumps({"value": "x"}) + previous_calls: list[tuple[str, str]] = [] + + for i in range(2): + ts.begin_step(previous_calls, step_no=i + 1) + result = ts.handle( + ToolCall( + id=f"tc-repeat-prior-{i}", + function=ToolCall.FunctionBody(name="ToolA", arguments=args), + ) + ) + assert isinstance(result, asyncio.Task) + _ = await result + previous_calls = ts.end_step() + + ts.begin_step(previous_calls, step_no=3) + result = ts.handle( + ToolCall( + id="tc-repeat-third", + function=ToolCall.FunctionBody(name="ToolA", arguments=args), + ) + ) + assert isinstance(result, asyncio.Task) + _ = await result + + assert [msg for msg in captured if isinstance(msg, ToolUseSkipped)] == [ + ToolUseSkipped( + tool_call_id="tc-repeat-third", + tool_name="ToolA", + reason="dedup", + resumed=False, + ) + ] + + +async def test_pre_tool_use_policy_block_opt_in_emits_tool_use_skipped(monkeypatch): + """Policy/PreToolUse blocks emit skip telemetry only when the tool opts in.""" + ts = _make_toolset() + tool = ts.find("ToolA") + assert tool is not None + object.__setattr__(tool, "emits_tool_use_skipped", True) + captured: list[object] = [] + monkeypatch.setattr("pythinker_code.soul.get_wire_or_none", lambda: _RecordingWire(captured)) + + async def fake_trigger(*_args: object, **_kwargs: object) -> list[SimpleNamespace]: + return [SimpleNamespace(action="block", reason="blocked")] + + monkeypatch.setattr(ts._hook_engine, "trigger", fake_trigger) + ts.begin_step([]) + result = ts.handle( + ToolCall( + id="tc-policy", + function=ToolCall.FunctionBody(name="ToolA", arguments="{}"), + ) + ) + assert isinstance(result, asyncio.Task) + _ = await result + + assert [msg for msg in captured if isinstance(msg, ToolUseSkipped)] == [ + ToolUseSkipped(tool_call_id="tc-policy", tool_name="ToolA", reason="policy") + ] + + async def test_cross_step_duplicate_uses_sparse_stronger_reminders(): """The stronger reminder appears at the fifth repeat and includes canonical args.""" ts = _make_toolset() @@ -596,3 +744,55 @@ def test_tool_defers_execution_started_reads_flag_only() -> None: # the explicit flag is the single contract. approval_only = SimpleNamespace(_approval=object()) assert _tool_defers_execution_started(cast(Any, approval_only)) is False + + +# --- ToolUseSkipped wire event (opt-in per tool) --- + + +class DummyToolEmitsSkipped(DummyToolA): + emits_tool_use_skipped: ClassVar[bool] = True + + +async def test_streaming_skip_when_concurrent_inflight_does_not_emit_when_queued( + tmp_path: Path, +) -> None: + """Exclusive tools queue behind in-flight calls; no ToolUseSkipped for that path.""" + from unittest.mock import patch + + from pythinker_code.hooks.engine import HookEngine + + events: list[tuple[str, str]] = [] + + class _SlowShell: + name = "Shell" + + async def call(self, arguments: object) -> ToolReturnValue: + events.append(("enter", "Shell")) + await asyncio.sleep(0.2) + events.append(("exit", "Shell")) + return ToolOk(output="ok") + + captured: list[object] = [] + toolset = PythinkerToolset() + toolset._hook_engine = HookEngine([], cwd=str(tmp_path)) + toolset._tool_dict["Shell"] = _SlowShell() # type: ignore[assignment] + + with patch("pythinker_code.soul.get_wire_or_none", return_value=_RecordingWire(captured)): + toolset.begin_step([]) + t1 = toolset.handle( + ToolCall(id="tc1", function=ToolCall.FunctionBody(name="Shell", arguments='{"n":1}')) + ) + t2 = toolset.handle( + ToolCall(id="tc2", function=ToolCall.FunctionBody(name="Shell", arguments='{"n":2}')) + ) + assert isinstance(t1, asyncio.Task) + assert isinstance(t2, asyncio.Task) + await asyncio.gather(t1, t2) + + assert events == [ + ("enter", "Shell"), + ("exit", "Shell"), + ("enter", "Shell"), + ("exit", "Shell"), + ] + assert not any(isinstance(e, type) and e.__name__ == "ToolUseSkipped" for e in captured) diff --git a/tests/core/test_toolset_concurrency.py b/tests/core/test_toolset_concurrency.py index 2a0e9e5a..693976a7 100644 --- a/tests/core/test_toolset_concurrency.py +++ b/tests/core/test_toolset_concurrency.py @@ -10,6 +10,7 @@ from __future__ import annotations import asyncio +import json from pathlib import Path from pythinker_core.tooling import ToolReturnValue @@ -111,6 +112,23 @@ async def test_writer_waits_for_inflight_readers(self, tmp_path: Path) -> None: assert events.index(("exit", "Read")) < events.index(("enter", "Write")) + async def test_unflagged_plugin_like_tool_runs_exclusively(self, tmp_path: Path) -> None: + events: list[tuple[str, str]] = [] + toolset = _toolset( + _RecordingTool("PluginA", events, parallel=False), + _RecordingTool("PluginB", events, parallel=False), + cwd=tmp_path, + ) + + await _dispatch(toolset, "PluginA", "PluginB") + + assert events == [ + ("enter", "PluginA"), + ("exit", "PluginA"), + ("enter", "PluginB"), + ("exit", "PluginB"), + ] + class TestParallelSafeFlags: def test_read_only_builtins_are_parallel_safe(self) -> None: @@ -171,7 +189,6 @@ async def reader() -> None: assert live == 2 # exactly the cap in-flight; the other 3 queued on the semaphore release.set() await asyncio.gather(*tasks) - assert peak == 2 async def test_read_gate_cap_does_not_block_writer_draining() -> None: @@ -206,3 +223,36 @@ async def writer() -> None: await asyncio.wait_for(writer_ran.wait(), timeout=1.0) # writer proceeds, no deadlock queued.cancel() await asyncio.gather(held, writer_task, queued, return_exceptions=True) + + +class TestPluginToolDefault: + async def test_plugin_tool_without_supports_parallel_runs_exclusively( + self, tmp_path: Path + ) -> None: + """Unflagged plugin/MCP tools default to exclusive so same-step mutation ordering + stays deterministic — mirrors blackbox partitionToolCalls isConcurrencySafe default.""" + events: list[tuple[str, str]] = [] + plugin = _RecordingTool("MyPlugin", events, parallel=False) + toolset = _toolset(plugin, cwd=tmp_path) + + tasks = [] + for index in range(2): + result = toolset.handle( + ToolCall( + id=f"tc_{index}", + function=ToolCall.FunctionBody( + name="MyPlugin", + arguments=json.dumps({"n": index}), + ), + ) + ) + assert isinstance(result, asyncio.Task) + tasks.append(result) + await asyncio.gather(*tasks) + + assert events == [ + ("enter", "MyPlugin"), + ("exit", "MyPlugin"), + ("enter", "MyPlugin"), + ("exit", "MyPlugin"), + ] diff --git a/tests/core/test_wire_types.py b/tests/core/test_wire_types.py new file mode 100644 index 00000000..7aa3794b --- /dev/null +++ b/tests/core/test_wire_types.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from pythinker_code.wire.types import ( + AgentListDelta, + ContextOverflowRecovered, + SubagentToolFallback, + TodoListUpdated, + ToolUseSkipped, + WireMessageEnvelope, +) + + +def test_todo_list_updated_round_trip_via_envelope() -> None: + evt = TodoListUpdated(items=(("Investigate", "in_progress"),), complete=False, source="tool") + env = WireMessageEnvelope.from_wire_message(evt) + assert env.type == "TodoListUpdated" + assert env.to_wire_message() == evt + + +def test_subagent_tool_fallback_round_trip() -> None: + evt = SubagentToolFallback( + reason="unavailable_agent_type", + requested_type="missing-coder", + available_types=("explore", "plan"), + ) + env = WireMessageEnvelope.from_wire_message(evt) + assert env.to_wire_message() == evt + + +def test_agent_list_delta_round_trip() -> None: + evt = AgentListDelta(items=("- explore: explore code (Tools: *)",), complete=True) + env = WireMessageEnvelope.from_wire_message(evt) + assert env.to_wire_message() == evt + + +def test_tool_use_skipped_round_trip() -> None: + evt = ToolUseSkipped( + tool_call_id="tc_1", + tool_name="Shell", + reason="concurrent_inflight", + resumed=True, + ) + env = WireMessageEnvelope.from_wire_message(evt) + assert env.to_wire_message() == evt + + +def test_context_overflow_recovered_round_trip() -> None: + evt = ContextOverflowRecovered(outcome="recovered", trigger_step=3) + env = WireMessageEnvelope.from_wire_message(evt) + assert env.to_wire_message() == evt + + +def test_new_wire_events_are_frozen_and_forbid_extra_fields() -> None: + evt = ToolUseSkipped(tool_call_id="tc_1", tool_name="Shell", reason="dedup") + + try: + evt.resumed = True # type: ignore[misc] + except Exception as exc: + assert type(exc).__name__ == "ValidationError" + else: # pragma: no cover - assertion clarity + raise AssertionError("ToolUseSkipped must be frozen") + + try: + TodoListUpdated.model_validate( + {"items": [], "complete": True, "source": "tool", "unexpected": True} + ) + except Exception as exc: + assert type(exc).__name__ == "ValidationError" + else: # pragma: no cover - assertion clarity + raise AssertionError("TodoListUpdated must reject extra fields") diff --git a/tests/tools/test_agent_tool.py b/tests/tools/test_agent_tool.py index a5bf4bba..3e1b2a0b 100644 --- a/tests/tools/test_agent_tool.py +++ b/tests/tools/test_agent_tool.py @@ -24,11 +24,21 @@ ApprovalRequest, MCPServerSnapshot, MCPStatusSnapshot, + SubagentToolFallback, TextPart, ) from tests.conftest import tool_call_context +class _RecordingWire: + def __init__(self, captured: list[object]) -> None: + self.soul_side = self + self._captured = captured + + def send(self, msg: object) -> None: + self._captured.append(msg) + + def _extract_agent_id(output: str) -> str: match = re.search(r"^agent_id: (\S+)$", output, re.MULTILINE) assert match is not None @@ -272,6 +282,55 @@ async def test_agent_tool_rejects_resume_when_instance_is_already_running(agent_ assert "cannot be resumed concurrently" in result.message +async def test_unknown_subagent_type_emits_fallback_wire_event( + agent_tool, monkeypatch: pytest.MonkeyPatch +): + captured: list[object] = [] + monkeypatch.setattr("pythinker_code.soul.get_wire_or_none", lambda: _RecordingWire(captured)) + + result = await agent_tool( + agent_tool.params( + description="unknown type", + prompt="look into parser issue", + subagent_type="does-not-exist", + ) + ) + + assert result.is_error + assert captured == [ + SubagentToolFallback( + reason="unavailable_agent_type", + requested_type="does-not-exist", + available_types=("mocker",), + ) + ] + + +async def test_policy_denied_subagent_type_emits_fallback_wire_event( + agent_tool, runtime, monkeypatch: pytest.MonkeyPatch +): + runtime.config.agent_execution_profile = "plan_only" + captured: list[object] = [] + monkeypatch.setattr("pythinker_code.soul.get_wire_or_none", lambda: _RecordingWire(captured)) + + result = await agent_tool( + agent_tool.params( + description="policy denied", + prompt="look into parser issue", + subagent_type="mocker", + ) + ) + + assert result.is_error + assert captured == [ + SubagentToolFallback( + reason="policy_denied", + requested_type="mocker", + available_types=("mocker",), + ) + ] + + async def test_agent_tool_keeps_result_when_summary_continuation_hits_max_steps( agent_tool, runtime, monkeypatch ): @@ -1451,7 +1510,7 @@ async def test_agent_tool_background_rejects_invalid_subagent_type(agent_tool, r assert result.is_error assert result.brief == "Invalid subagent type" - assert "Builtin subagent type not found" in result.message + assert "Builtin subagent type not found: does-not-exist" in result.message async def test_agent_tool_background_rejects_invalid_model_alias_before_start( diff --git a/tests/tools/test_todo.py b/tests/tools/test_todo.py index 1c0e3c66..8b36eae9 100644 --- a/tests/tools/test_todo.py +++ b/tests/tools/test_todo.py @@ -2,11 +2,23 @@ from __future__ import annotations +from unittest.mock import patch + import pytest from pythinker_code.scratchpad import session_scratch_path from pythinker_code.soul.agent import Runtime from pythinker_code.tools.todo import Params, SetTodoList, Todo +from pythinker_code.wire.types import TodoListUpdated + + +class _RecordingWire: + def __init__(self, captured: list[object]) -> None: + self.soul_side = self + self._captured = captured + + def send(self, msg: object) -> None: + self._captured.append(msg) @pytest.fixture @@ -129,6 +141,43 @@ async def test_read_mode_empty_list(self, set_todo_list_tool: SetTodoList): assert not result.is_error assert result.output # non-empty even when no todos + async def test_write_mode_emits_todo_list_updated_wire_event( + self, set_todo_list_tool: SetTodoList, monkeypatch: pytest.MonkeyPatch + ): + captured: list[object] = [] + monkeypatch.setattr( + "pythinker_code.soul.get_wire_or_none", lambda: _RecordingWire(captured) + ) + + result = await set_todo_list_tool( + Params(todos=[Todo(title="Investigate", status="in_progress")]) + ) + + assert not result.is_error + assert captured == [ + TodoListUpdated( + items=(("Investigate", "in_progress"),), + complete=False, + source="tool", + ) + ] + + async def test_read_mode_emits_todo_list_updated_wire_event( + self, set_todo_list_tool: SetTodoList, monkeypatch: pytest.MonkeyPatch + ): + await set_todo_list_tool(Params(todos=[Todo(title="Task A", status="done")])) + captured: list[object] = [] + monkeypatch.setattr( + "pythinker_code.soul.get_wire_or_none", lambda: _RecordingWire(captured) + ) + + result = await set_todo_list_tool(Params(todos=None)) + + assert not result.is_error + assert captured == [ + TodoListUpdated(items=(("Task A", "done"),), complete=True, source="tool") + ] + async def test_cancelled_status_is_accepted_and_persisted( self, set_todo_list_tool: SetTodoList, runtime: Runtime ): @@ -442,3 +491,18 @@ async def test_subagent_single_in_progress_not_normalized(self, runtime: Runtime assert not result.is_error assert "normalized" not in result.output + + +async def test_write_mode_emits_todo_list_updated_wire_event( + set_todo_list_tool: SetTodoList, +) -> None: + captured: list[object] = [] + with patch("pythinker_code.soul.get_wire_or_none", return_value=_RecordingWire(captured)): + result = await set_todo_list_tool( + Params(todos=[Todo(title="Investigate", status="in_progress")]) + ) + assert not result.is_error + updated = [e for e in captured if isinstance(e, TodoListUpdated)] + assert len(updated) == 1 + assert updated[0].items == (("Investigate", "in_progress"),) + assert updated[0].source == "tool" diff --git a/tests/tools/test_tool_descriptions.py b/tests/tools/test_tool_descriptions.py index d0670107..87d27ec0 100644 --- a/tests/tools/test_tool_descriptions.py +++ b/tests/tools/test_tool_descriptions.py @@ -57,6 +57,13 @@ def test_agent_description(agent_tool: AgentTool): - Cross-check at least one load-bearing subagent finding before making changes from it. - The subagent result is only visible to you. If the user should see it, summarize it yourself. +**Prompt Hygiene** + +When spawning a fresh agent, brief it like a smart colleague who just walked in: include the goal, +what was tried, what is in and out of scope, the expected output contract, and how the result will +be verified. For lookups, pass the exact command or symbol; for investigations, pass the question, +not a prescribed sequence of steps. + **Agent Workflow Design** Use subagents as focused logical roles, not just extra tool capacity: @@ -112,6 +119,8 @@ def test_agent_description(agent_tool: AgentTool): - Only genuinely broad, cross-cutting work → more, up to the `RunAgents` cap of 8. Prefer the fewest children that cover the independent objectives — the cap of 8 is a ceiling, not a target. Over-provisioning burns the multi-agent token premium (a fan-out can cost several times a single thread) and produces results you then have to reconcile. Do not launch a subagent for what one or two direct reads or greps would answer. + +When spawning a fresh agent, brief it like a smart colleague who just walked in — include the goal, what was tried, what is in/out of scope, the expected output contract, and how the result will be verified. Lookups: pass the exact command. Investigations: pass the question, not prescribed steps. """ ) diff --git a/tests/tools/test_tool_search.py b/tests/tools/test_tool_search.py new file mode 100644 index 00000000..03e5f1d5 --- /dev/null +++ b/tests/tools/test_tool_search.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +from pydantic import BaseModel +from pythinker_core.tooling import CallableTool2, ToolOk, ToolReturnValue + +from pythinker_code.soul.toolset import PythinkerToolset +from pythinker_code.tools.tool_search import Params, ToolSearch + + +class _Params(BaseModel): + value: str = "" + + +class ReadProjectFiles(CallableTool2[_Params]): + name: str = "ReadProjectFiles" + description: str = "Read files from the current workspace" + params: type[_Params] = _Params + + async def __call__(self, params: _Params) -> ToolReturnValue: + return ToolOk(output=params.value) + + +class RunProjectCommand(CallableTool2[_Params]): + name: str = "RunProjectCommand" + description: str = "Execute a command in the current workspace" + params: type[_Params] = _Params + + async def __call__(self, params: _Params) -> ToolReturnValue: + return ToolOk(output=params.value) + + +def _tool_search(toolset: PythinkerToolset) -> ToolSearch: + return ToolSearch(toolset) + + +async def test_tool_search_matches_tool_name() -> None: + toolset = PythinkerToolset() + toolset.add(ReadProjectFiles()) + toolset.add(RunProjectCommand()) + + result = await _tool_search(toolset)(Params(query="read")) + + assert not result.is_error + assert "ReadProjectFiles - Read files from the current workspace" in result.output + assert "RunProjectCommand" not in result.output + + +async def test_tool_search_matches_description() -> None: + toolset = PythinkerToolset() + toolset.add(ReadProjectFiles()) + toolset.add(RunProjectCommand()) + + result = await _tool_search(toolset)(Params(query="execute command")) + + assert not result.is_error + assert "RunProjectCommand - Execute a command in the current workspace" in result.output + + +async def test_tool_search_reports_no_matches() -> None: + toolset = PythinkerToolset() + toolset.add(ReadProjectFiles()) + + result = await _tool_search(toolset)(Params(query="browser")) + + assert not result.is_error + assert result.output == "No visible tools matched `browser`." + + +async def test_tool_search_excludes_hidden_tools() -> None: + toolset = PythinkerToolset() + toolset.add(ReadProjectFiles()) + toolset.add(RunProjectCommand()) + toolset.hide("ReadProjectFiles") + + result = await _tool_search(toolset)(Params(query="read files")) + + assert not result.is_error + assert "ReadProjectFiles" not in result.output + assert result.output == "No visible tools matched `read files`." diff --git a/tests/tools/test_worktree_tools.py b/tests/tools/test_worktree_tools.py new file mode 100644 index 00000000..2144fbd6 --- /dev/null +++ b/tests/tools/test_worktree_tools.py @@ -0,0 +1,173 @@ +from __future__ import annotations + +import subprocess +from dataclasses import replace +from pathlib import Path + +from pythinker_core.tooling import ToolResult +from pythinker_host.path import HostPath + +from pythinker_code.soul.approval import ApprovalResult +from pythinker_code.soul.toolset import PythinkerToolset +from pythinker_code.tools.worktree import EnterWorktree, EnterWorktreeParams, ExitWorktree +from pythinker_code.wire.types import ToolCall + + +def _git(repo: Path, *args: str) -> None: + subprocess.run(["git", "-C", str(repo), *args], check=True, capture_output=True, text=True) + + +def _init_repo(repo: Path) -> None: + repo.mkdir() + _git(repo, "init") + _git(repo, "config", "user.email", "test@example.com") + _git(repo, "config", "user.name", "Test User") + (repo / "README.md").write_text("# test\n", encoding="utf-8") + _git(repo, "add", "README.md") + _git(repo, "commit", "-m", "initial") + + +async def _approve_request(*args: object, **kwargs: object) -> ApprovalResult: + return ApprovalResult(True) + + +async def test_enter_worktree_switches_runtime_work_dir( + runtime, tmp_path: Path, monkeypatch +) -> None: + repo = tmp_path / "repo" + _init_repo(repo) + runtime.session.work_dir = HostPath.unsafe_from_local_path(repo) + runtime.builtin_args = replace( + runtime.builtin_args, PYTHINKER_WORK_DIR=HostPath.unsafe_from_local_path(repo) + ) + toolset = PythinkerToolset(runtime) + monkeypatch.setattr(runtime.approval, "request", _approve_request) + + result = await EnterWorktree(runtime, toolset)( + EnterWorktreeParams(name="phase-c", path=str(tmp_path / "session-worktree")) + ) + + worktree = tmp_path / "session-worktree" + assert not result.is_error + assert runtime.work_dir == HostPath.unsafe_from_local_path(worktree) + assert worktree.is_dir() + assert "worktree_path:" in result.output + assert "original_work_dir:" in result.output + + +async def test_exit_worktree_returns_to_original_work_dir( + runtime, tmp_path: Path, monkeypatch +) -> None: + repo = tmp_path / "repo" + _init_repo(repo) + runtime.session.work_dir = HostPath.unsafe_from_local_path(repo) + runtime.builtin_args = replace( + runtime.builtin_args, PYTHINKER_WORK_DIR=HostPath.unsafe_from_local_path(repo) + ) + toolset = PythinkerToolset(runtime) + enter = EnterWorktree(runtime, toolset) + exit_tool = ExitWorktree(runtime, toolset) + monkeypatch.setattr(runtime.approval, "request", _approve_request) + + enter_result = await enter(EnterWorktreeParams(path=str(tmp_path / "session-worktree"))) + exit_result = await exit_tool(exit_tool.params()) + + assert not enter_result.is_error + assert not exit_result.is_error + assert runtime.work_dir == HostPath.unsafe_from_local_path(repo) + assert (tmp_path / "session-worktree").is_dir() + assert "retained: true" in exit_result.output + + +async def test_enter_worktree_failure_does_not_change_work_dir( + runtime, tmp_path: Path, monkeypatch +) -> None: + non_repo = tmp_path / "not-a-repo" + non_repo.mkdir() + runtime.session.work_dir = HostPath.unsafe_from_local_path(non_repo) + runtime.builtin_args = replace( + runtime.builtin_args, PYTHINKER_WORK_DIR=HostPath.unsafe_from_local_path(non_repo) + ) + toolset = PythinkerToolset(runtime) + monkeypatch.setattr(runtime.approval, "request", _approve_request) + + result = await EnterWorktree(runtime, toolset)( + EnterWorktreeParams(path=str(tmp_path / "session-worktree")) + ) + + assert result.is_error + assert runtime.work_dir == HostPath.unsafe_from_local_path(non_repo) + assert runtime.work_dir_override is None + + +async def test_enter_worktree_rejected_approval_does_not_create_worktree( + runtime, tmp_path: Path, monkeypatch +) -> None: + repo = tmp_path / "repo" + _init_repo(repo) + runtime.session.work_dir = HostPath.unsafe_from_local_path(repo) + runtime.builtin_args = replace( + runtime.builtin_args, PYTHINKER_WORK_DIR=HostPath.unsafe_from_local_path(repo) + ) + toolset = PythinkerToolset(runtime) + dest = tmp_path / "session-worktree" + + async def reject_request(*args: object, **kwargs: object) -> ApprovalResult: + return ApprovalResult(False, feedback="not now") + + monkeypatch.setattr(runtime.approval, "request", reject_request) + + result = await EnterWorktree(runtime, toolset)(EnterWorktreeParams(path=str(dest))) + + assert result.is_error + assert "not now" in result.message + assert runtime.work_dir == HostPath.unsafe_from_local_path(repo) + assert runtime.work_dir_override is None + assert not dest.exists() + + +async def test_exit_worktree_without_active_worktree_is_error(runtime) -> None: + toolset = PythinkerToolset(runtime) + + result = await ExitWorktree(runtime, toolset)(ExitWorktree.params()) + + assert result.is_error + assert "No session worktree is active" in result.message + + +async def test_exit_worktree_is_root_only(runtime) -> None: + sub_runtime = runtime.copy_for_subagent(agent_id="child", subagent_type="coder") + toolset = PythinkerToolset(sub_runtime) + + result = await ExitWorktree(sub_runtime, toolset)(ExitWorktree.params()) + + assert result.is_error + assert "only available in the root session" in result.message + + +async def test_enter_worktree_respects_permission_profile(runtime, tmp_path: Path) -> None: + repo = tmp_path / "repo" + _init_repo(repo) + runtime.config.agent_execution_profile = "plan_only" + runtime.session.work_dir = HostPath.unsafe_from_local_path(repo) + runtime.builtin_args = replace( + runtime.builtin_args, PYTHINKER_WORK_DIR=HostPath.unsafe_from_local_path(repo) + ) + toolset = PythinkerToolset(runtime) + toolset.add(EnterWorktree(runtime, toolset)) + + handle_result = toolset.handle( + ToolCall( + id="enter-worktree", + function=ToolCall.FunctionBody( + name="EnterWorktree", + arguments=f'{{"path": "{tmp_path / "session-worktree"}"}}', + ), + ) + ) + result = handle_result if isinstance(handle_result, ToolResult) else await handle_result + + assert result.return_value.is_error + assert "permission profile blocks external tool" in result.return_value.message + assert runtime.work_dir == HostPath.unsafe_from_local_path(repo) + assert not (tmp_path / "session-worktree").exists() diff --git a/tests/utils/test_pyinstaller_utils.py b/tests/utils/test_pyinstaller_utils.py index c1d9fdb8..7e8071f1 100644 --- a/tests/utils/test_pyinstaller_utils.py +++ b/tests/utils/test_pyinstaller_utils.py @@ -252,10 +252,22 @@ def test_pyinstaller_datas(): "src/pythinker_code/tools/web/fetch.md", "pythinker_code/tools/web", ), + ( + "src/pythinker_code/tools/tool_search/tool_search.md", + "pythinker_code/tools/tool_search", + ), ( "src/pythinker_code/tools/web/search.md", "pythinker_code/tools/web", ), + ( + "src/pythinker_code/tools/worktree/enter_worktree.md", + "pythinker_code/tools/worktree", + ), + ( + "src/pythinker_code/tools/worktree/exit_worktree.md", + "pythinker_code/tools/worktree", + ), ] if has_rg_binary: expected_datas.append( @@ -330,11 +342,13 @@ def test_pyinstaller_hiddenimports(): "pythinker_code.tools.test", "pythinker_code.tools.think", "pythinker_code.tools.todo", + "pythinker_code.tools.tool_search", "pythinker_code.tools.utils", "pythinker_code.tools.web", "pythinker_code.tools.web._allowlist", "pythinker_code.tools.web.fetch", "pythinker_code.tools.web.search", + "pythinker_code.tools.worktree", "setproctitle", ] ) diff --git a/tests_e2e/test_wire_approvals_tools.py b/tests_e2e/test_wire_approvals_tools.py index 00466bc2..15edcdc2 100644 --- a/tests_e2e/test_wire_approvals_tools.py +++ b/tests_e2e/test_wire_approvals_tools.py @@ -177,6 +177,27 @@ def test_shell_approval_approve(tmp_path) -> None: }, }, }, + { + "method": "event", + "type": "AgentListDelta", + "payload": { + "items": [ + "- `code-reviewer`: Diff-focused code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, ReadSkill). When to use: Use to run a read-only, diff-focused, professional code review — severity-scored findings across correctness, security, reliability, performance, maintainability, and standards compliance, in any programming language — or a code-reviewr-derived PR artifact workflow on the current branch. It runs offline by design and never modifies the repository; third-party API claims it cannot verify from the repository come back under RISKS as needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `coder`: Good at general software engineering tasks. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent for non-trivial software engineering work that may require reading files, editing code, running commands, and returning a compact but technically complete summary to the parent agent. It delivers production-ready, idiomatic, verified changes in any language the project uses, with current-docs verification for third-party APIs, and never expands beyond its brief.", + "- `debugger`: Failure/log/stack-trace root-cause analysis with reproduction evidence. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, SmartSearch). When to use: Use for failing tests, stack traces, runtime errors, flaky failures, regressions, or debugging requests where the root cause should be found before editing code. Read-only and safe to fan out in parallel — one focused failure per instance — it returns the named mechanism, confidence, evidence, the recommended minimal fix, and the verification that would prove it.", + '- `explore`: Fast codebase exploration with prompt-enforced read-only behavior. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (e.g. "src/**/*.yaml"), search code for keywords (e.g. "database connection"), or answer questions about the codebase (e.g. "how does the auth module work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "thorough" for comprehensive analysis across multiple locations and naming conventions. Use this agent for any read-only exploration that will clearly require more than 3 tool calls. Prefer launching multiple explore agents concurrently when investigating independent questions. Absence claims come with the searches that back them.', + "- `implementer`: Scoped implementation with minimal edits and verification. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent when the required code change is already specified and should be implemented with minimal, idiomatic edits and a quick verification pass. It executes the spec faithfully — escalating instead of improvising when the spec does not match reality — and emits a block so the result can be chained directly into the verifier.", + "- `judge`: Independent final quality gate for answers, reports, and code-change summaries. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent as an independent final quality gate and advisor before delivering non-trivial code changes, reports, audits, or findings to the user. It judges the parent agent's evidence, actions, and proposed final answer — verifying claims against the packet's artifacts and local sources, and requiring the parent's citation for load-bearing external-API, version, and best-practice claims it cannot check offline — and recommends fixes without ever applying them.", + "- `plan`: Read-only implementation planning and architecture design. (Tools: SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent when the parent agent needs a step-by-step implementation plan, key file identification, and architectural trade-off analysis before code changes are made. It returns dependency-ordered, wave-parallelized tasks — each with artifacts, acceptance criteria, a specialist recommendation, and a proving verification — grounded in repository evidence and current third-party documentation.", + "- `planner`: Read-only recon planner that decomposes tasks into distinct parallel seeds. (Tools: Shell, ReadFile, Glob, Grep, SmartSearch). When to use: Use this agent before spawning N parallel workers on a large or open-ended task. It scouts the repository cheaply, partitions the problem space along one decomposition axis, and returns distinct, self-contained seeds so workers start from non-overlapping vantage points. A single-seed result signals the task is not worth parallelizing.", + "- `review`: Read-only code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent for direct, read-only code review after changes are made, or when the parent needs severity-scored findings before deciding what to fix. It reviews the diff/files itself with reads and searches — for the CLI/Reviewflow-driven review pipeline, use `code-reviewer` instead. Findings arrive BLOCKER-first with evidence, trigger conditions, and a dispatch-ready fix description; it runs offline by design, so third-party API claims it cannot verify from the repository are explicitly downgraded to needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `scout`: Read-only external docs, dependency-source, and API freshness researcher. (Tools: Shell, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent for external libraries, SDK docs, upstream source comparisons, API freshness checks, registry/package verification, and dependency behavior research — including verifying the `needs verification` third-party claims that offline reviewer/debugger agents return under RISKS. It returns version-pinned, source-cited facts — local installed source first, then official docs via live web research — with conflicts and unverifiable gaps reported explicitly instead of papered over.", + "- `security-reviewer`: Diff-focused security review with validated findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep). When to use: Use for security review: diff-only review on the current branch (default) or repo-wide vulnerability discovery via the security-scan pipeline. Can run in parallel with `code-reviewer`; for large diffs, scope each instance to the trust-boundary files of one subsystem. Returns reachability-validated findings — source → sink anchored, precondition-stated, CWE-classified, version-checked against the project's pins — with scanner hits treated as leads until verified. It runs offline by design, so advisory-dependent claims come back under RISKS as needs-verification items for the parent to check.", + '- `verifier`: Read-only validation runner for tests, lint, and builds. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent when the parent needs tests, lint, type checks, builds, or other validation gates run and reported without applying fixes — e.g. "run the tests", "does it build", post-edit gate checks, or re-running a suspected flaky suite. Not for fixing failures, writing tests, updating snapshots, or formatting: it is read-only by design and reports proposed fixes under RISKS instead of applying them.', + ], + "complete": True, + }, + }, {"method": "event", "type": "StepBegin", "payload": {"n": 2}}, { "method": "event", @@ -314,6 +335,27 @@ def test_shell_approval_reject(tmp_path) -> None: }, }, }, + { + "method": "event", + "type": "AgentListDelta", + "payload": { + "items": [ + "- `code-reviewer`: Diff-focused code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, ReadSkill). When to use: Use to run a read-only, diff-focused, professional code review — severity-scored findings across correctness, security, reliability, performance, maintainability, and standards compliance, in any programming language — or a code-reviewr-derived PR artifact workflow on the current branch. It runs offline by design and never modifies the repository; third-party API claims it cannot verify from the repository come back under RISKS as needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `coder`: Good at general software engineering tasks. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent for non-trivial software engineering work that may require reading files, editing code, running commands, and returning a compact but technically complete summary to the parent agent. It delivers production-ready, idiomatic, verified changes in any language the project uses, with current-docs verification for third-party APIs, and never expands beyond its brief.", + "- `debugger`: Failure/log/stack-trace root-cause analysis with reproduction evidence. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, SmartSearch). When to use: Use for failing tests, stack traces, runtime errors, flaky failures, regressions, or debugging requests where the root cause should be found before editing code. Read-only and safe to fan out in parallel — one focused failure per instance — it returns the named mechanism, confidence, evidence, the recommended minimal fix, and the verification that would prove it.", + '- `explore`: Fast codebase exploration with prompt-enforced read-only behavior. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (e.g. "src/**/*.yaml"), search code for keywords (e.g. "database connection"), or answer questions about the codebase (e.g. "how does the auth module work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "thorough" for comprehensive analysis across multiple locations and naming conventions. Use this agent for any read-only exploration that will clearly require more than 3 tool calls. Prefer launching multiple explore agents concurrently when investigating independent questions. Absence claims come with the searches that back them.', + "- `implementer`: Scoped implementation with minimal edits and verification. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent when the required code change is already specified and should be implemented with minimal, idiomatic edits and a quick verification pass. It executes the spec faithfully — escalating instead of improvising when the spec does not match reality — and emits a block so the result can be chained directly into the verifier.", + "- `judge`: Independent final quality gate for answers, reports, and code-change summaries. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent as an independent final quality gate and advisor before delivering non-trivial code changes, reports, audits, or findings to the user. It judges the parent agent's evidence, actions, and proposed final answer — verifying claims against the packet's artifacts and local sources, and requiring the parent's citation for load-bearing external-API, version, and best-practice claims it cannot check offline — and recommends fixes without ever applying them.", + "- `plan`: Read-only implementation planning and architecture design. (Tools: SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent when the parent agent needs a step-by-step implementation plan, key file identification, and architectural trade-off analysis before code changes are made. It returns dependency-ordered, wave-parallelized tasks — each with artifacts, acceptance criteria, a specialist recommendation, and a proving verification — grounded in repository evidence and current third-party documentation.", + "- `planner`: Read-only recon planner that decomposes tasks into distinct parallel seeds. (Tools: Shell, ReadFile, Glob, Grep, SmartSearch). When to use: Use this agent before spawning N parallel workers on a large or open-ended task. It scouts the repository cheaply, partitions the problem space along one decomposition axis, and returns distinct, self-contained seeds so workers start from non-overlapping vantage points. A single-seed result signals the task is not worth parallelizing.", + "- `review`: Read-only code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent for direct, read-only code review after changes are made, or when the parent needs severity-scored findings before deciding what to fix. It reviews the diff/files itself with reads and searches — for the CLI/Reviewflow-driven review pipeline, use `code-reviewer` instead. Findings arrive BLOCKER-first with evidence, trigger conditions, and a dispatch-ready fix description; it runs offline by design, so third-party API claims it cannot verify from the repository are explicitly downgraded to needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `scout`: Read-only external docs, dependency-source, and API freshness researcher. (Tools: Shell, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent for external libraries, SDK docs, upstream source comparisons, API freshness checks, registry/package verification, and dependency behavior research — including verifying the `needs verification` third-party claims that offline reviewer/debugger agents return under RISKS. It returns version-pinned, source-cited facts — local installed source first, then official docs via live web research — with conflicts and unverifiable gaps reported explicitly instead of papered over.", + "- `security-reviewer`: Diff-focused security review with validated findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep). When to use: Use for security review: diff-only review on the current branch (default) or repo-wide vulnerability discovery via the security-scan pipeline. Can run in parallel with `code-reviewer`; for large diffs, scope each instance to the trust-boundary files of one subsystem. Returns reachability-validated findings — source → sink anchored, precondition-stated, CWE-classified, version-checked against the project's pins — with scanner hits treated as leads until verified. It runs offline by design, so advisory-dependent claims come back under RISKS as needs-verification items for the parent to check.", + '- `verifier`: Read-only validation runner for tests, lint, and builds. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent when the parent needs tests, lint, type checks, builds, or other validation gates run and reported without applying fixes — e.g. "run the tests", "does it build", post-edit gate checks, or re-running a suspected flaky suite. Not for fixing failures, writing tests, updating snapshots, or formatting: it is read-only by design and reports proposed fixes under RISKS instead of applying them.', + ], + "complete": True, + }, + }, {"method": "event", "type": "TurnEnd", "payload": {}}, ] ) @@ -468,6 +510,27 @@ def test_approve_for_session(tmp_path) -> None: }, }, }, + { + "method": "event", + "type": "AgentListDelta", + "payload": { + "items": [ + "- `code-reviewer`: Diff-focused code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, ReadSkill). When to use: Use to run a read-only, diff-focused, professional code review — severity-scored findings across correctness, security, reliability, performance, maintainability, and standards compliance, in any programming language — or a code-reviewr-derived PR artifact workflow on the current branch. It runs offline by design and never modifies the repository; third-party API claims it cannot verify from the repository come back under RISKS as needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `coder`: Good at general software engineering tasks. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent for non-trivial software engineering work that may require reading files, editing code, running commands, and returning a compact but technically complete summary to the parent agent. It delivers production-ready, idiomatic, verified changes in any language the project uses, with current-docs verification for third-party APIs, and never expands beyond its brief.", + "- `debugger`: Failure/log/stack-trace root-cause analysis with reproduction evidence. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, SmartSearch). When to use: Use for failing tests, stack traces, runtime errors, flaky failures, regressions, or debugging requests where the root cause should be found before editing code. Read-only and safe to fan out in parallel — one focused failure per instance — it returns the named mechanism, confidence, evidence, the recommended minimal fix, and the verification that would prove it.", + '- `explore`: Fast codebase exploration with prompt-enforced read-only behavior. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (e.g. "src/**/*.yaml"), search code for keywords (e.g. "database connection"), or answer questions about the codebase (e.g. "how does the auth module work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "thorough" for comprehensive analysis across multiple locations and naming conventions. Use this agent for any read-only exploration that will clearly require more than 3 tool calls. Prefer launching multiple explore agents concurrently when investigating independent questions. Absence claims come with the searches that back them.', + "- `implementer`: Scoped implementation with minimal edits and verification. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent when the required code change is already specified and should be implemented with minimal, idiomatic edits and a quick verification pass. It executes the spec faithfully — escalating instead of improvising when the spec does not match reality — and emits a block so the result can be chained directly into the verifier.", + "- `judge`: Independent final quality gate for answers, reports, and code-change summaries. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent as an independent final quality gate and advisor before delivering non-trivial code changes, reports, audits, or findings to the user. It judges the parent agent's evidence, actions, and proposed final answer — verifying claims against the packet's artifacts and local sources, and requiring the parent's citation for load-bearing external-API, version, and best-practice claims it cannot check offline — and recommends fixes without ever applying them.", + "- `plan`: Read-only implementation planning and architecture design. (Tools: SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent when the parent agent needs a step-by-step implementation plan, key file identification, and architectural trade-off analysis before code changes are made. It returns dependency-ordered, wave-parallelized tasks — each with artifacts, acceptance criteria, a specialist recommendation, and a proving verification — grounded in repository evidence and current third-party documentation.", + "- `planner`: Read-only recon planner that decomposes tasks into distinct parallel seeds. (Tools: Shell, ReadFile, Glob, Grep, SmartSearch). When to use: Use this agent before spawning N parallel workers on a large or open-ended task. It scouts the repository cheaply, partitions the problem space along one decomposition axis, and returns distinct, self-contained seeds so workers start from non-overlapping vantage points. A single-seed result signals the task is not worth parallelizing.", + "- `review`: Read-only code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent for direct, read-only code review after changes are made, or when the parent needs severity-scored findings before deciding what to fix. It reviews the diff/files itself with reads and searches — for the CLI/Reviewflow-driven review pipeline, use `code-reviewer` instead. Findings arrive BLOCKER-first with evidence, trigger conditions, and a dispatch-ready fix description; it runs offline by design, so third-party API claims it cannot verify from the repository are explicitly downgraded to needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `scout`: Read-only external docs, dependency-source, and API freshness researcher. (Tools: Shell, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent for external libraries, SDK docs, upstream source comparisons, API freshness checks, registry/package verification, and dependency behavior research — including verifying the `needs verification` third-party claims that offline reviewer/debugger agents return under RISKS. It returns version-pinned, source-cited facts — local installed source first, then official docs via live web research — with conflicts and unverifiable gaps reported explicitly instead of papered over.", + "- `security-reviewer`: Diff-focused security review with validated findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep). When to use: Use for security review: diff-only review on the current branch (default) or repo-wide vulnerability discovery via the security-scan pipeline. Can run in parallel with `code-reviewer`; for large diffs, scope each instance to the trust-boundary files of one subsystem. Returns reachability-validated findings — source → sink anchored, precondition-stated, CWE-classified, version-checked against the project's pins — with scanner hits treated as leads until verified. It runs offline by design, so advisory-dependent claims come back under RISKS as needs-verification items for the parent to check.", + '- `verifier`: Read-only validation runner for tests, lint, and builds. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent when the parent needs tests, lint, type checks, builds, or other validation gates run and reported without applying fixes — e.g. "run the tests", "does it build", post-edit gate checks, or re-running a suspected flaky suite. Not for fixing failures, writing tests, updating snapshots, or formatting: it is read-only by design and reports proposed fixes under RISKS instead of applying them.', + ], + "complete": True, + }, + }, {"method": "event", "type": "StepBegin", "payload": {"n": 2}}, { "method": "event", @@ -683,6 +746,27 @@ def test_yolo_skips_approval(tmp_path) -> None: }, }, }, + { + "method": "event", + "type": "AgentListDelta", + "payload": { + "items": [ + "- `code-reviewer`: Diff-focused code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, ReadSkill). When to use: Use to run a read-only, diff-focused, professional code review — severity-scored findings across correctness, security, reliability, performance, maintainability, and standards compliance, in any programming language — or a code-reviewr-derived PR artifact workflow on the current branch. It runs offline by design and never modifies the repository; third-party API claims it cannot verify from the repository come back under RISKS as needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `coder`: Good at general software engineering tasks. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent for non-trivial software engineering work that may require reading files, editing code, running commands, and returning a compact but technically complete summary to the parent agent. It delivers production-ready, idiomatic, verified changes in any language the project uses, with current-docs verification for third-party APIs, and never expands beyond its brief.", + "- `debugger`: Failure/log/stack-trace root-cause analysis with reproduction evidence. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, SmartSearch). When to use: Use for failing tests, stack traces, runtime errors, flaky failures, regressions, or debugging requests where the root cause should be found before editing code. Read-only and safe to fan out in parallel — one focused failure per instance — it returns the named mechanism, confidence, evidence, the recommended minimal fix, and the verification that would prove it.", + '- `explore`: Fast codebase exploration with prompt-enforced read-only behavior. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (e.g. "src/**/*.yaml"), search code for keywords (e.g. "database connection"), or answer questions about the codebase (e.g. "how does the auth module work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "thorough" for comprehensive analysis across multiple locations and naming conventions. Use this agent for any read-only exploration that will clearly require more than 3 tool calls. Prefer launching multiple explore agents concurrently when investigating independent questions. Absence claims come with the searches that back them.', + "- `implementer`: Scoped implementation with minimal edits and verification. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent when the required code change is already specified and should be implemented with minimal, idiomatic edits and a quick verification pass. It executes the spec faithfully — escalating instead of improvising when the spec does not match reality — and emits a block so the result can be chained directly into the verifier.", + "- `judge`: Independent final quality gate for answers, reports, and code-change summaries. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent as an independent final quality gate and advisor before delivering non-trivial code changes, reports, audits, or findings to the user. It judges the parent agent's evidence, actions, and proposed final answer — verifying claims against the packet's artifacts and local sources, and requiring the parent's citation for load-bearing external-API, version, and best-practice claims it cannot check offline — and recommends fixes without ever applying them.", + "- `plan`: Read-only implementation planning and architecture design. (Tools: SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent when the parent agent needs a step-by-step implementation plan, key file identification, and architectural trade-off analysis before code changes are made. It returns dependency-ordered, wave-parallelized tasks — each with artifacts, acceptance criteria, a specialist recommendation, and a proving verification — grounded in repository evidence and current third-party documentation.", + "- `planner`: Read-only recon planner that decomposes tasks into distinct parallel seeds. (Tools: Shell, ReadFile, Glob, Grep, SmartSearch). When to use: Use this agent before spawning N parallel workers on a large or open-ended task. It scouts the repository cheaply, partitions the problem space along one decomposition axis, and returns distinct, self-contained seeds so workers start from non-overlapping vantage points. A single-seed result signals the task is not worth parallelizing.", + "- `review`: Read-only code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent for direct, read-only code review after changes are made, or when the parent needs severity-scored findings before deciding what to fix. It reviews the diff/files itself with reads and searches — for the CLI/Reviewflow-driven review pipeline, use `code-reviewer` instead. Findings arrive BLOCKER-first with evidence, trigger conditions, and a dispatch-ready fix description; it runs offline by design, so third-party API claims it cannot verify from the repository are explicitly downgraded to needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `scout`: Read-only external docs, dependency-source, and API freshness researcher. (Tools: Shell, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent for external libraries, SDK docs, upstream source comparisons, API freshness checks, registry/package verification, and dependency behavior research — including verifying the `needs verification` third-party claims that offline reviewer/debugger agents return under RISKS. It returns version-pinned, source-cited facts — local installed source first, then official docs via live web research — with conflicts and unverifiable gaps reported explicitly instead of papered over.", + "- `security-reviewer`: Diff-focused security review with validated findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep). When to use: Use for security review: diff-only review on the current branch (default) or repo-wide vulnerability discovery via the security-scan pipeline. Can run in parallel with `code-reviewer`; for large diffs, scope each instance to the trust-boundary files of one subsystem. Returns reachability-validated findings — source → sink anchored, precondition-stated, CWE-classified, version-checked against the project's pins — with scanner hits treated as leads until verified. It runs offline by design, so advisory-dependent claims come back under RISKS as needs-verification items for the parent to check.", + '- `verifier`: Read-only validation runner for tests, lint, and builds. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent when the parent needs tests, lint, type checks, builds, or other validation gates run and reported without applying fixes — e.g. "run the tests", "does it build", post-edit gate checks, or re-running a suspected flaky suite. Not for fixing failures, writing tests, updating snapshots, or formatting: it is read-only by design and reports proposed fixes under RISKS instead of applying them.', + ], + "complete": True, + }, + }, {"method": "event", "type": "StepBegin", "payload": {"n": 2}}, { "method": "event", @@ -997,6 +1081,32 @@ def test_display_block_todo(tmp_path) -> None: }, }, }, + { + "method": "event", + "type": "AgentListDelta", + "payload": { + "items": [ + "- `code-reviewer`: Diff-focused code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, ReadSkill). When to use: Use to run a read-only, diff-focused, professional code review — severity-scored findings across correctness, security, reliability, performance, maintainability, and standards compliance, in any programming language — or a code-reviewr-derived PR artifact workflow on the current branch. It runs offline by design and never modifies the repository; third-party API claims it cannot verify from the repository come back under RISKS as needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `coder`: Good at general software engineering tasks. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent for non-trivial software engineering work that may require reading files, editing code, running commands, and returning a compact but technically complete summary to the parent agent. It delivers production-ready, idiomatic, verified changes in any language the project uses, with current-docs verification for third-party APIs, and never expands beyond its brief.", + "- `debugger`: Failure/log/stack-trace root-cause analysis with reproduction evidence. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, SmartSearch). When to use: Use for failing tests, stack traces, runtime errors, flaky failures, regressions, or debugging requests where the root cause should be found before editing code. Read-only and safe to fan out in parallel — one focused failure per instance — it returns the named mechanism, confidence, evidence, the recommended minimal fix, and the verification that would prove it.", + '- `explore`: Fast codebase exploration with prompt-enforced read-only behavior. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (e.g. "src/**/*.yaml"), search code for keywords (e.g. "database connection"), or answer questions about the codebase (e.g. "how does the auth module work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "thorough" for comprehensive analysis across multiple locations and naming conventions. Use this agent for any read-only exploration that will clearly require more than 3 tool calls. Prefer launching multiple explore agents concurrently when investigating independent questions. Absence claims come with the searches that back them.', + "- `implementer`: Scoped implementation with minimal edits and verification. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent when the required code change is already specified and should be implemented with minimal, idiomatic edits and a quick verification pass. It executes the spec faithfully — escalating instead of improvising when the spec does not match reality — and emits a block so the result can be chained directly into the verifier.", + "- `judge`: Independent final quality gate for answers, reports, and code-change summaries. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent as an independent final quality gate and advisor before delivering non-trivial code changes, reports, audits, or findings to the user. It judges the parent agent's evidence, actions, and proposed final answer — verifying claims against the packet's artifacts and local sources, and requiring the parent's citation for load-bearing external-API, version, and best-practice claims it cannot check offline — and recommends fixes without ever applying them.", + "- `plan`: Read-only implementation planning and architecture design. (Tools: SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent when the parent agent needs a step-by-step implementation plan, key file identification, and architectural trade-off analysis before code changes are made. It returns dependency-ordered, wave-parallelized tasks — each with artifacts, acceptance criteria, a specialist recommendation, and a proving verification — grounded in repository evidence and current third-party documentation.", + "- `planner`: Read-only recon planner that decomposes tasks into distinct parallel seeds. (Tools: Shell, ReadFile, Glob, Grep, SmartSearch). When to use: Use this agent before spawning N parallel workers on a large or open-ended task. It scouts the repository cheaply, partitions the problem space along one decomposition axis, and returns distinct, self-contained seeds so workers start from non-overlapping vantage points. A single-seed result signals the task is not worth parallelizing.", + "- `review`: Read-only code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent for direct, read-only code review after changes are made, or when the parent needs severity-scored findings before deciding what to fix. It reviews the diff/files itself with reads and searches — for the CLI/Reviewflow-driven review pipeline, use `code-reviewer` instead. Findings arrive BLOCKER-first with evidence, trigger conditions, and a dispatch-ready fix description; it runs offline by design, so third-party API claims it cannot verify from the repository are explicitly downgraded to needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `scout`: Read-only external docs, dependency-source, and API freshness researcher. (Tools: Shell, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent for external libraries, SDK docs, upstream source comparisons, API freshness checks, registry/package verification, and dependency behavior research — including verifying the `needs verification` third-party claims that offline reviewer/debugger agents return under RISKS. It returns version-pinned, source-cited facts — local installed source first, then official docs via live web research — with conflicts and unverifiable gaps reported explicitly instead of papered over.", + "- `security-reviewer`: Diff-focused security review with validated findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep). When to use: Use for security review: diff-only review on the current branch (default) or repo-wide vulnerability discovery via the security-scan pipeline. Can run in parallel with `code-reviewer`; for large diffs, scope each instance to the trust-boundary files of one subsystem. Returns reachability-validated findings — source → sink anchored, precondition-stated, CWE-classified, version-checked against the project's pins — with scanner hits treated as leads until verified. It runs offline by design, so advisory-dependent claims come back under RISKS as needs-verification items for the parent to check.", + '- `verifier`: Read-only validation runner for tests, lint, and builds. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent when the parent needs tests, lint, type checks, builds, or other validation gates run and reported without applying fixes — e.g. "run the tests", "does it build", post-edit gate checks, or re-running a suspected flaky suite. Not for fixing failures, writing tests, updating snapshots, or formatting: it is read-only by design and reports proposed fixes under RISKS instead of applying them.', + ], + "complete": True, + }, + }, + { + "method": "event", + "type": "TodoListUpdated", + "payload": {"items": [["one", "pending"]], "complete": False, "source": "tool"}, + }, {"method": "event", "type": "StepBegin", "payload": {"n": 2}}, { "method": "event", @@ -1124,6 +1234,32 @@ def test_tool_call_part_streaming(tmp_path) -> None: }, }, }, + { + "method": "event", + "type": "AgentListDelta", + "payload": { + "items": [ + "- `code-reviewer`: Diff-focused code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, ReadSkill). When to use: Use to run a read-only, diff-focused, professional code review — severity-scored findings across correctness, security, reliability, performance, maintainability, and standards compliance, in any programming language — or a code-reviewr-derived PR artifact workflow on the current branch. It runs offline by design and never modifies the repository; third-party API claims it cannot verify from the repository come back under RISKS as needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `coder`: Good at general software engineering tasks. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent for non-trivial software engineering work that may require reading files, editing code, running commands, and returning a compact but technically complete summary to the parent agent. It delivers production-ready, idiomatic, verified changes in any language the project uses, with current-docs verification for third-party APIs, and never expands beyond its brief.", + "- `debugger`: Failure/log/stack-trace root-cause analysis with reproduction evidence. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, SmartSearch). When to use: Use for failing tests, stack traces, runtime errors, flaky failures, regressions, or debugging requests where the root cause should be found before editing code. Read-only and safe to fan out in parallel — one focused failure per instance — it returns the named mechanism, confidence, evidence, the recommended minimal fix, and the verification that would prove it.", + '- `explore`: Fast codebase exploration with prompt-enforced read-only behavior. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (e.g. "src/**/*.yaml"), search code for keywords (e.g. "database connection"), or answer questions about the codebase (e.g. "how does the auth module work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "thorough" for comprehensive analysis across multiple locations and naming conventions. Use this agent for any read-only exploration that will clearly require more than 3 tool calls. Prefer launching multiple explore agents concurrently when investigating independent questions. Absence claims come with the searches that back them.', + "- `implementer`: Scoped implementation with minimal edits and verification. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent when the required code change is already specified and should be implemented with minimal, idiomatic edits and a quick verification pass. It executes the spec faithfully — escalating instead of improvising when the spec does not match reality — and emits a block so the result can be chained directly into the verifier.", + "- `judge`: Independent final quality gate for answers, reports, and code-change summaries. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent as an independent final quality gate and advisor before delivering non-trivial code changes, reports, audits, or findings to the user. It judges the parent agent's evidence, actions, and proposed final answer — verifying claims against the packet's artifacts and local sources, and requiring the parent's citation for load-bearing external-API, version, and best-practice claims it cannot check offline — and recommends fixes without ever applying them.", + "- `plan`: Read-only implementation planning and architecture design. (Tools: SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent when the parent agent needs a step-by-step implementation plan, key file identification, and architectural trade-off analysis before code changes are made. It returns dependency-ordered, wave-parallelized tasks — each with artifacts, acceptance criteria, a specialist recommendation, and a proving verification — grounded in repository evidence and current third-party documentation.", + "- `planner`: Read-only recon planner that decomposes tasks into distinct parallel seeds. (Tools: Shell, ReadFile, Glob, Grep, SmartSearch). When to use: Use this agent before spawning N parallel workers on a large or open-ended task. It scouts the repository cheaply, partitions the problem space along one decomposition axis, and returns distinct, self-contained seeds so workers start from non-overlapping vantage points. A single-seed result signals the task is not worth parallelizing.", + "- `review`: Read-only code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent for direct, read-only code review after changes are made, or when the parent needs severity-scored findings before deciding what to fix. It reviews the diff/files itself with reads and searches — for the CLI/Reviewflow-driven review pipeline, use `code-reviewer` instead. Findings arrive BLOCKER-first with evidence, trigger conditions, and a dispatch-ready fix description; it runs offline by design, so third-party API claims it cannot verify from the repository are explicitly downgraded to needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `scout`: Read-only external docs, dependency-source, and API freshness researcher. (Tools: Shell, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent for external libraries, SDK docs, upstream source comparisons, API freshness checks, registry/package verification, and dependency behavior research — including verifying the `needs verification` third-party claims that offline reviewer/debugger agents return under RISKS. It returns version-pinned, source-cited facts — local installed source first, then official docs via live web research — with conflicts and unverifiable gaps reported explicitly instead of papered over.", + "- `security-reviewer`: Diff-focused security review with validated findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep). When to use: Use for security review: diff-only review on the current branch (default) or repo-wide vulnerability discovery via the security-scan pipeline. Can run in parallel with `code-reviewer`; for large diffs, scope each instance to the trust-boundary files of one subsystem. Returns reachability-validated findings — source → sink anchored, precondition-stated, CWE-classified, version-checked against the project's pins — with scanner hits treated as leads until verified. It runs offline by design, so advisory-dependent claims come back under RISKS as needs-verification items for the parent to check.", + '- `verifier`: Read-only validation runner for tests, lint, and builds. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent when the parent needs tests, lint, type checks, builds, or other validation gates run and reported without applying fixes — e.g. "run the tests", "does it build", post-edit gate checks, or re-running a suspected flaky suite. Not for fixing failures, writing tests, updating snapshots, or formatting: it is read-only by design and reports proposed fixes under RISKS instead of applying them.', + ], + "complete": True, + }, + }, + { + "method": "event", + "type": "TodoListUpdated", + "payload": {"items": [["a", "pending"]], "complete": False, "source": "tool"}, + }, {"method": "event", "type": "StepBegin", "payload": {"n": 2}}, { "method": "event", @@ -1234,6 +1370,27 @@ def test_default_agent_missing_tool(tmp_path) -> None: }, }, }, + { + "method": "event", + "type": "AgentListDelta", + "payload": { + "items": [ + "- `code-reviewer`: Diff-focused code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, ReadSkill). When to use: Use to run a read-only, diff-focused, professional code review — severity-scored findings across correctness, security, reliability, performance, maintainability, and standards compliance, in any programming language — or a code-reviewr-derived PR artifact workflow on the current branch. It runs offline by design and never modifies the repository; third-party API claims it cannot verify from the repository come back under RISKS as needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `coder`: Good at general software engineering tasks. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent for non-trivial software engineering work that may require reading files, editing code, running commands, and returning a compact but technically complete summary to the parent agent. It delivers production-ready, idiomatic, verified changes in any language the project uses, with current-docs verification for third-party APIs, and never expands beyond its brief.", + "- `debugger`: Failure/log/stack-trace root-cause analysis with reproduction evidence. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, SmartSearch). When to use: Use for failing tests, stack traces, runtime errors, flaky failures, regressions, or debugging requests where the root cause should be found before editing code. Read-only and safe to fan out in parallel — one focused failure per instance — it returns the named mechanism, confidence, evidence, the recommended minimal fix, and the verification that would prove it.", + '- `explore`: Fast codebase exploration with prompt-enforced read-only behavior. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (e.g. "src/**/*.yaml"), search code for keywords (e.g. "database connection"), or answer questions about the codebase (e.g. "how does the auth module work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "thorough" for comprehensive analysis across multiple locations and naming conventions. Use this agent for any read-only exploration that will clearly require more than 3 tool calls. Prefer launching multiple explore agents concurrently when investigating independent questions. Absence claims come with the searches that back them.', + "- `implementer`: Scoped implementation with minimal edits and verification. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent when the required code change is already specified and should be implemented with minimal, idiomatic edits and a quick verification pass. It executes the spec faithfully — escalating instead of improvising when the spec does not match reality — and emits a block so the result can be chained directly into the verifier.", + "- `judge`: Independent final quality gate for answers, reports, and code-change summaries. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent as an independent final quality gate and advisor before delivering non-trivial code changes, reports, audits, or findings to the user. It judges the parent agent's evidence, actions, and proposed final answer — verifying claims against the packet's artifacts and local sources, and requiring the parent's citation for load-bearing external-API, version, and best-practice claims it cannot check offline — and recommends fixes without ever applying them.", + "- `plan`: Read-only implementation planning and architecture design. (Tools: SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent when the parent agent needs a step-by-step implementation plan, key file identification, and architectural trade-off analysis before code changes are made. It returns dependency-ordered, wave-parallelized tasks — each with artifacts, acceptance criteria, a specialist recommendation, and a proving verification — grounded in repository evidence and current third-party documentation.", + "- `planner`: Read-only recon planner that decomposes tasks into distinct parallel seeds. (Tools: Shell, ReadFile, Glob, Grep, SmartSearch). When to use: Use this agent before spawning N parallel workers on a large or open-ended task. It scouts the repository cheaply, partitions the problem space along one decomposition axis, and returns distinct, self-contained seeds so workers start from non-overlapping vantage points. A single-seed result signals the task is not worth parallelizing.", + "- `review`: Read-only code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent for direct, read-only code review after changes are made, or when the parent needs severity-scored findings before deciding what to fix. It reviews the diff/files itself with reads and searches — for the CLI/Reviewflow-driven review pipeline, use `code-reviewer` instead. Findings arrive BLOCKER-first with evidence, trigger conditions, and a dispatch-ready fix description; it runs offline by design, so third-party API claims it cannot verify from the repository are explicitly downgraded to needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `scout`: Read-only external docs, dependency-source, and API freshness researcher. (Tools: Shell, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent for external libraries, SDK docs, upstream source comparisons, API freshness checks, registry/package verification, and dependency behavior research — including verifying the `needs verification` third-party claims that offline reviewer/debugger agents return under RISKS. It returns version-pinned, source-cited facts — local installed source first, then official docs via live web research — with conflicts and unverifiable gaps reported explicitly instead of papered over.", + "- `security-reviewer`: Diff-focused security review with validated findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep). When to use: Use for security review: diff-only review on the current branch (default) or repo-wide vulnerability discovery via the security-scan pipeline. Can run in parallel with `code-reviewer`; for large diffs, scope each instance to the trust-boundary files of one subsystem. Returns reachability-validated findings — source → sink anchored, precondition-stated, CWE-classified, version-checked against the project's pins — with scanner hits treated as leads until verified. It runs offline by design, so advisory-dependent claims come back under RISKS as needs-verification items for the parent to check.", + '- `verifier`: Read-only validation runner for tests, lint, and builds. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent when the parent needs tests, lint, type checks, builds, or other validation gates run and reported without applying fixes — e.g. "run the tests", "does it build", post-edit gate checks, or re-running a suspected flaky suite. Not for fixing failures, writing tests, updating snapshots, or formatting: it is read-only by design and reports proposed fixes under RISKS instead of applying them.', + ], + "complete": True, + }, + }, {"method": "event", "type": "StepBegin", "payload": {"n": 2}}, { "method": "event", @@ -1358,6 +1515,27 @@ def test_custom_agent_exclude_tool(tmp_path) -> None: }, }, }, + { + "method": "event", + "type": "AgentListDelta", + "payload": { + "items": [ + "- `code-reviewer`: Diff-focused code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, ReadSkill). When to use: Use to run a read-only, diff-focused, professional code review — severity-scored findings across correctness, security, reliability, performance, maintainability, and standards compliance, in any programming language — or a code-reviewr-derived PR artifact workflow on the current branch. It runs offline by design and never modifies the repository; third-party API claims it cannot verify from the repository come back under RISKS as needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `coder`: Good at general software engineering tasks. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent for non-trivial software engineering work that may require reading files, editing code, running commands, and returning a compact but technically complete summary to the parent agent. It delivers production-ready, idiomatic, verified changes in any language the project uses, with current-docs verification for third-party APIs, and never expands beyond its brief.", + "- `debugger`: Failure/log/stack-trace root-cause analysis with reproduction evidence. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, SmartSearch). When to use: Use for failing tests, stack traces, runtime errors, flaky failures, regressions, or debugging requests where the root cause should be found before editing code. Read-only and safe to fan out in parallel — one focused failure per instance — it returns the named mechanism, confidence, evidence, the recommended minimal fix, and the verification that would prove it.", + '- `explore`: Fast codebase exploration with prompt-enforced read-only behavior. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (e.g. "src/**/*.yaml"), search code for keywords (e.g. "database connection"), or answer questions about the codebase (e.g. "how does the auth module work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "thorough" for comprehensive analysis across multiple locations and naming conventions. Use this agent for any read-only exploration that will clearly require more than 3 tool calls. Prefer launching multiple explore agents concurrently when investigating independent questions. Absence claims come with the searches that back them.', + "- `implementer`: Scoped implementation with minimal edits and verification. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent when the required code change is already specified and should be implemented with minimal, idiomatic edits and a quick verification pass. It executes the spec faithfully — escalating instead of improvising when the spec does not match reality — and emits a block so the result can be chained directly into the verifier.", + "- `judge`: Independent final quality gate for answers, reports, and code-change summaries. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent as an independent final quality gate and advisor before delivering non-trivial code changes, reports, audits, or findings to the user. It judges the parent agent's evidence, actions, and proposed final answer — verifying claims against the packet's artifacts and local sources, and requiring the parent's citation for load-bearing external-API, version, and best-practice claims it cannot check offline — and recommends fixes without ever applying them.", + "- `plan`: Read-only implementation planning and architecture design. (Tools: SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent when the parent agent needs a step-by-step implementation plan, key file identification, and architectural trade-off analysis before code changes are made. It returns dependency-ordered, wave-parallelized tasks — each with artifacts, acceptance criteria, a specialist recommendation, and a proving verification — grounded in repository evidence and current third-party documentation.", + "- `planner`: Read-only recon planner that decomposes tasks into distinct parallel seeds. (Tools: Shell, ReadFile, Glob, Grep, SmartSearch). When to use: Use this agent before spawning N parallel workers on a large or open-ended task. It scouts the repository cheaply, partitions the problem space along one decomposition axis, and returns distinct, self-contained seeds so workers start from non-overlapping vantage points. A single-seed result signals the task is not worth parallelizing.", + "- `review`: Read-only code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent for direct, read-only code review after changes are made, or when the parent needs severity-scored findings before deciding what to fix. It reviews the diff/files itself with reads and searches — for the CLI/Reviewflow-driven review pipeline, use `code-reviewer` instead. Findings arrive BLOCKER-first with evidence, trigger conditions, and a dispatch-ready fix description; it runs offline by design, so third-party API claims it cannot verify from the repository are explicitly downgraded to needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `scout`: Read-only external docs, dependency-source, and API freshness researcher. (Tools: Shell, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent for external libraries, SDK docs, upstream source comparisons, API freshness checks, registry/package verification, and dependency behavior research — including verifying the `needs verification` third-party claims that offline reviewer/debugger agents return under RISKS. It returns version-pinned, source-cited facts — local installed source first, then official docs via live web research — with conflicts and unverifiable gaps reported explicitly instead of papered over.", + "- `security-reviewer`: Diff-focused security review with validated findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep). When to use: Use for security review: diff-only review on the current branch (default) or repo-wide vulnerability discovery via the security-scan pipeline. Can run in parallel with `code-reviewer`; for large diffs, scope each instance to the trust-boundary files of one subsystem. Returns reachability-validated findings — source → sink anchored, precondition-stated, CWE-classified, version-checked against the project's pins — with scanner hits treated as leads until verified. It runs offline by design, so advisory-dependent claims come back under RISKS as needs-verification items for the parent to check.", + '- `verifier`: Read-only validation runner for tests, lint, and builds. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent when the parent needs tests, lint, type checks, builds, or other validation gates run and reported without applying fixes — e.g. "run the tests", "does it build", post-edit gate checks, or re-running a suspected flaky suite. Not for fixing failures, writing tests, updating snapshots, or formatting: it is read-only by design and reports proposed fixes under RISKS instead of applying them.', + ], + "complete": True, + }, + }, {"method": "event", "type": "StepBegin", "payload": {"n": 2}}, { "method": "event", diff --git a/tests_e2e/test_wire_config.py b/tests_e2e/test_wire_config.py index 6abee0c6..0e3355a9 100644 --- a/tests_e2e/test_wire_config.py +++ b/tests_e2e/test_wire_config.py @@ -85,6 +85,27 @@ def test_config_string(tmp_path) -> None: "mcp_status": None, }, }, + { + "method": "event", + "type": "AgentListDelta", + "payload": { + "items": [ + "- `code-reviewer`: Diff-focused code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, ReadSkill). When to use: Use to run a read-only, diff-focused, professional code review — severity-scored findings across correctness, security, reliability, performance, maintainability, and standards compliance, in any programming language — or a code-reviewr-derived PR artifact workflow on the current branch. It runs offline by design and never modifies the repository; third-party API claims it cannot verify from the repository come back under RISKS as needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `coder`: Good at general software engineering tasks. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent for non-trivial software engineering work that may require reading files, editing code, running commands, and returning a compact but technically complete summary to the parent agent. It delivers production-ready, idiomatic, verified changes in any language the project uses, with current-docs verification for third-party APIs, and never expands beyond its brief.", + "- `debugger`: Failure/log/stack-trace root-cause analysis with reproduction evidence. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, SmartSearch). When to use: Use for failing tests, stack traces, runtime errors, flaky failures, regressions, or debugging requests where the root cause should be found before editing code. Read-only and safe to fan out in parallel — one focused failure per instance — it returns the named mechanism, confidence, evidence, the recommended minimal fix, and the verification that would prove it.", + '- `explore`: Fast codebase exploration with prompt-enforced read-only behavior. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (e.g. "src/**/*.yaml"), search code for keywords (e.g. "database connection"), or answer questions about the codebase (e.g. "how does the auth module work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "thorough" for comprehensive analysis across multiple locations and naming conventions. Use this agent for any read-only exploration that will clearly require more than 3 tool calls. Prefer launching multiple explore agents concurrently when investigating independent questions. Absence claims come with the searches that back them.', + "- `implementer`: Scoped implementation with minimal edits and verification. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent when the required code change is already specified and should be implemented with minimal, idiomatic edits and a quick verification pass. It executes the spec faithfully — escalating instead of improvising when the spec does not match reality — and emits a block so the result can be chained directly into the verifier.", + "- `judge`: Independent final quality gate for answers, reports, and code-change summaries. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent as an independent final quality gate and advisor before delivering non-trivial code changes, reports, audits, or findings to the user. It judges the parent agent's evidence, actions, and proposed final answer — verifying claims against the packet's artifacts and local sources, and requiring the parent's citation for load-bearing external-API, version, and best-practice claims it cannot check offline — and recommends fixes without ever applying them.", + "- `plan`: Read-only implementation planning and architecture design. (Tools: SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent when the parent agent needs a step-by-step implementation plan, key file identification, and architectural trade-off analysis before code changes are made. It returns dependency-ordered, wave-parallelized tasks — each with artifacts, acceptance criteria, a specialist recommendation, and a proving verification — grounded in repository evidence and current third-party documentation.", + "- `planner`: Read-only recon planner that decomposes tasks into distinct parallel seeds. (Tools: Shell, ReadFile, Glob, Grep, SmartSearch). When to use: Use this agent before spawning N parallel workers on a large or open-ended task. It scouts the repository cheaply, partitions the problem space along one decomposition axis, and returns distinct, self-contained seeds so workers start from non-overlapping vantage points. A single-seed result signals the task is not worth parallelizing.", + "- `review`: Read-only code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent for direct, read-only code review after changes are made, or when the parent needs severity-scored findings before deciding what to fix. It reviews the diff/files itself with reads and searches — for the CLI/Reviewflow-driven review pipeline, use `code-reviewer` instead. Findings arrive BLOCKER-first with evidence, trigger conditions, and a dispatch-ready fix description; it runs offline by design, so third-party API claims it cannot verify from the repository are explicitly downgraded to needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `scout`: Read-only external docs, dependency-source, and API freshness researcher. (Tools: Shell, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent for external libraries, SDK docs, upstream source comparisons, API freshness checks, registry/package verification, and dependency behavior research — including verifying the `needs verification` third-party claims that offline reviewer/debugger agents return under RISKS. It returns version-pinned, source-cited facts — local installed source first, then official docs via live web research — with conflicts and unverifiable gaps reported explicitly instead of papered over.", + "- `security-reviewer`: Diff-focused security review with validated findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep). When to use: Use for security review: diff-only review on the current branch (default) or repo-wide vulnerability discovery via the security-scan pipeline. Can run in parallel with `code-reviewer`; for large diffs, scope each instance to the trust-boundary files of one subsystem. Returns reachability-validated findings — source → sink anchored, precondition-stated, CWE-classified, version-checked against the project's pins — with scanner hits treated as leads until verified. It runs offline by design, so advisory-dependent claims come back under RISKS as needs-verification items for the parent to check.", + '- `verifier`: Read-only validation runner for tests, lint, and builds. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent when the parent needs tests, lint, type checks, builds, or other validation gates run and reported without applying fixes — e.g. "run the tests", "does it build", post-edit gate checks, or re-running a suspected flaky suite. Not for fixing failures, writing tests, updating snapshots, or formatting: it is read-only by design and reports proposed fixes under RISKS instead of applying them.', + ], + "complete": True, + }, + }, {"method": "event", "type": "TurnEnd", "payload": {}}, ] ) @@ -177,6 +198,27 @@ def test_model_override(tmp_path) -> None: "mcp_status": None, }, }, + { + "method": "event", + "type": "AgentListDelta", + "payload": { + "items": [ + "- `code-reviewer`: Diff-focused code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, ReadSkill). When to use: Use to run a read-only, diff-focused, professional code review — severity-scored findings across correctness, security, reliability, performance, maintainability, and standards compliance, in any programming language — or a code-reviewr-derived PR artifact workflow on the current branch. It runs offline by design and never modifies the repository; third-party API claims it cannot verify from the repository come back under RISKS as needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `coder`: Good at general software engineering tasks. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent for non-trivial software engineering work that may require reading files, editing code, running commands, and returning a compact but technically complete summary to the parent agent. It delivers production-ready, idiomatic, verified changes in any language the project uses, with current-docs verification for third-party APIs, and never expands beyond its brief.", + "- `debugger`: Failure/log/stack-trace root-cause analysis with reproduction evidence. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, SmartSearch). When to use: Use for failing tests, stack traces, runtime errors, flaky failures, regressions, or debugging requests where the root cause should be found before editing code. Read-only and safe to fan out in parallel — one focused failure per instance — it returns the named mechanism, confidence, evidence, the recommended minimal fix, and the verification that would prove it.", + '- `explore`: Fast codebase exploration with prompt-enforced read-only behavior. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (e.g. "src/**/*.yaml"), search code for keywords (e.g. "database connection"), or answer questions about the codebase (e.g. "how does the auth module work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "thorough" for comprehensive analysis across multiple locations and naming conventions. Use this agent for any read-only exploration that will clearly require more than 3 tool calls. Prefer launching multiple explore agents concurrently when investigating independent questions. Absence claims come with the searches that back them.', + "- `implementer`: Scoped implementation with minimal edits and verification. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent when the required code change is already specified and should be implemented with minimal, idiomatic edits and a quick verification pass. It executes the spec faithfully — escalating instead of improvising when the spec does not match reality — and emits a block so the result can be chained directly into the verifier.", + "- `judge`: Independent final quality gate for answers, reports, and code-change summaries. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent as an independent final quality gate and advisor before delivering non-trivial code changes, reports, audits, or findings to the user. It judges the parent agent's evidence, actions, and proposed final answer — verifying claims against the packet's artifacts and local sources, and requiring the parent's citation for load-bearing external-API, version, and best-practice claims it cannot check offline — and recommends fixes without ever applying them.", + "- `plan`: Read-only implementation planning and architecture design. (Tools: SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent when the parent agent needs a step-by-step implementation plan, key file identification, and architectural trade-off analysis before code changes are made. It returns dependency-ordered, wave-parallelized tasks — each with artifacts, acceptance criteria, a specialist recommendation, and a proving verification — grounded in repository evidence and current third-party documentation.", + "- `planner`: Read-only recon planner that decomposes tasks into distinct parallel seeds. (Tools: Shell, ReadFile, Glob, Grep, SmartSearch). When to use: Use this agent before spawning N parallel workers on a large or open-ended task. It scouts the repository cheaply, partitions the problem space along one decomposition axis, and returns distinct, self-contained seeds so workers start from non-overlapping vantage points. A single-seed result signals the task is not worth parallelizing.", + "- `review`: Read-only code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent for direct, read-only code review after changes are made, or when the parent needs severity-scored findings before deciding what to fix. It reviews the diff/files itself with reads and searches — for the CLI/Reviewflow-driven review pipeline, use `code-reviewer` instead. Findings arrive BLOCKER-first with evidence, trigger conditions, and a dispatch-ready fix description; it runs offline by design, so third-party API claims it cannot verify from the repository are explicitly downgraded to needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `scout`: Read-only external docs, dependency-source, and API freshness researcher. (Tools: Shell, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent for external libraries, SDK docs, upstream source comparisons, API freshness checks, registry/package verification, and dependency behavior research — including verifying the `needs verification` third-party claims that offline reviewer/debugger agents return under RISKS. It returns version-pinned, source-cited facts — local installed source first, then official docs via live web research — with conflicts and unverifiable gaps reported explicitly instead of papered over.", + "- `security-reviewer`: Diff-focused security review with validated findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep). When to use: Use for security review: diff-only review on the current branch (default) or repo-wide vulnerability discovery via the security-scan pipeline. Can run in parallel with `code-reviewer`; for large diffs, scope each instance to the trust-boundary files of one subsystem. Returns reachability-validated findings — source → sink anchored, precondition-stated, CWE-classified, version-checked against the project's pins — with scanner hits treated as leads until verified. It runs offline by design, so advisory-dependent claims come back under RISKS as needs-verification items for the parent to check.", + '- `verifier`: Read-only validation runner for tests, lint, and builds. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent when the parent needs tests, lint, type checks, builds, or other validation gates run and reported without applying fixes — e.g. "run the tests", "does it build", post-edit gate checks, or re-running a suspected flaky suite. Not for fixing failures, writing tests, updating snapshots, or formatting: it is read-only by design and reports proposed fixes under RISKS instead of applying them.', + ], + "complete": True, + }, + }, {"method": "event", "type": "TurnEnd", "payload": {}}, ] ) diff --git a/tests_e2e/test_wire_prompt.py b/tests_e2e/test_wire_prompt.py index 2050f0e9..4c2cf8e0 100644 --- a/tests_e2e/test_wire_prompt.py +++ b/tests_e2e/test_wire_prompt.py @@ -95,6 +95,27 @@ def test_basic_prompt_events(tmp_path) -> None: "mcp_status": None, }, }, + { + "method": "event", + "type": "AgentListDelta", + "payload": { + "items": [ + "- `code-reviewer`: Diff-focused code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, ReadSkill). When to use: Use to run a read-only, diff-focused, professional code review — severity-scored findings across correctness, security, reliability, performance, maintainability, and standards compliance, in any programming language — or a code-reviewr-derived PR artifact workflow on the current branch. It runs offline by design and never modifies the repository; third-party API claims it cannot verify from the repository come back under RISKS as needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `coder`: Good at general software engineering tasks. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent for non-trivial software engineering work that may require reading files, editing code, running commands, and returning a compact but technically complete summary to the parent agent. It delivers production-ready, idiomatic, verified changes in any language the project uses, with current-docs verification for third-party APIs, and never expands beyond its brief.", + "- `debugger`: Failure/log/stack-trace root-cause analysis with reproduction evidence. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, SmartSearch). When to use: Use for failing tests, stack traces, runtime errors, flaky failures, regressions, or debugging requests where the root cause should be found before editing code. Read-only and safe to fan out in parallel — one focused failure per instance — it returns the named mechanism, confidence, evidence, the recommended minimal fix, and the verification that would prove it.", + '- `explore`: Fast codebase exploration with prompt-enforced read-only behavior. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (e.g. "src/**/*.yaml"), search code for keywords (e.g. "database connection"), or answer questions about the codebase (e.g. "how does the auth module work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "thorough" for comprehensive analysis across multiple locations and naming conventions. Use this agent for any read-only exploration that will clearly require more than 3 tool calls. Prefer launching multiple explore agents concurrently when investigating independent questions. Absence claims come with the searches that back them.', + "- `implementer`: Scoped implementation with minimal edits and verification. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent when the required code change is already specified and should be implemented with minimal, idiomatic edits and a quick verification pass. It executes the spec faithfully — escalating instead of improvising when the spec does not match reality — and emits a block so the result can be chained directly into the verifier.", + "- `judge`: Independent final quality gate for answers, reports, and code-change summaries. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent as an independent final quality gate and advisor before delivering non-trivial code changes, reports, audits, or findings to the user. It judges the parent agent's evidence, actions, and proposed final answer — verifying claims against the packet's artifacts and local sources, and requiring the parent's citation for load-bearing external-API, version, and best-practice claims it cannot check offline — and recommends fixes without ever applying them.", + "- `plan`: Read-only implementation planning and architecture design. (Tools: SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent when the parent agent needs a step-by-step implementation plan, key file identification, and architectural trade-off analysis before code changes are made. It returns dependency-ordered, wave-parallelized tasks — each with artifacts, acceptance criteria, a specialist recommendation, and a proving verification — grounded in repository evidence and current third-party documentation.", + "- `planner`: Read-only recon planner that decomposes tasks into distinct parallel seeds. (Tools: Shell, ReadFile, Glob, Grep, SmartSearch). When to use: Use this agent before spawning N parallel workers on a large or open-ended task. It scouts the repository cheaply, partitions the problem space along one decomposition axis, and returns distinct, self-contained seeds so workers start from non-overlapping vantage points. A single-seed result signals the task is not worth parallelizing.", + "- `review`: Read-only code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent for direct, read-only code review after changes are made, or when the parent needs severity-scored findings before deciding what to fix. It reviews the diff/files itself with reads and searches — for the CLI/Reviewflow-driven review pipeline, use `code-reviewer` instead. Findings arrive BLOCKER-first with evidence, trigger conditions, and a dispatch-ready fix description; it runs offline by design, so third-party API claims it cannot verify from the repository are explicitly downgraded to needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `scout`: Read-only external docs, dependency-source, and API freshness researcher. (Tools: Shell, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent for external libraries, SDK docs, upstream source comparisons, API freshness checks, registry/package verification, and dependency behavior research — including verifying the `needs verification` third-party claims that offline reviewer/debugger agents return under RISKS. It returns version-pinned, source-cited facts — local installed source first, then official docs via live web research — with conflicts and unverifiable gaps reported explicitly instead of papered over.", + "- `security-reviewer`: Diff-focused security review with validated findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep). When to use: Use for security review: diff-only review on the current branch (default) or repo-wide vulnerability discovery via the security-scan pipeline. Can run in parallel with `code-reviewer`; for large diffs, scope each instance to the trust-boundary files of one subsystem. Returns reachability-validated findings — source → sink anchored, precondition-stated, CWE-classified, version-checked against the project's pins — with scanner hits treated as leads until verified. It runs offline by design, so advisory-dependent claims come back under RISKS as needs-verification items for the parent to check.", + '- `verifier`: Read-only validation runner for tests, lint, and builds. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent when the parent needs tests, lint, type checks, builds, or other validation gates run and reported without applying fixes — e.g. "run the tests", "does it build", post-edit gate checks, or re-running a suspected flaky suite. Not for fixing failures, writing tests, updating snapshots, or formatting: it is read-only by design and reports proposed fixes under RISKS instead of applying them.', + ], + "complete": True, + }, + }, {"method": "event", "type": "TurnEnd", "payload": {}}, ] ) @@ -322,6 +343,32 @@ def test_max_steps_reached(tmp_path) -> None: }, }, }, + { + "method": "event", + "type": "AgentListDelta", + "payload": { + "items": [ + "- `code-reviewer`: Diff-focused code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, ReadSkill). When to use: Use to run a read-only, diff-focused, professional code review — severity-scored findings across correctness, security, reliability, performance, maintainability, and standards compliance, in any programming language — or a code-reviewr-derived PR artifact workflow on the current branch. It runs offline by design and never modifies the repository; third-party API claims it cannot verify from the repository come back under RISKS as needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `coder`: Good at general software engineering tasks. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent for non-trivial software engineering work that may require reading files, editing code, running commands, and returning a compact but technically complete summary to the parent agent. It delivers production-ready, idiomatic, verified changes in any language the project uses, with current-docs verification for third-party APIs, and never expands beyond its brief.", + "- `debugger`: Failure/log/stack-trace root-cause analysis with reproduction evidence. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, SmartSearch). When to use: Use for failing tests, stack traces, runtime errors, flaky failures, regressions, or debugging requests where the root cause should be found before editing code. Read-only and safe to fan out in parallel — one focused failure per instance — it returns the named mechanism, confidence, evidence, the recommended minimal fix, and the verification that would prove it.", + '- `explore`: Fast codebase exploration with prompt-enforced read-only behavior. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (e.g. "src/**/*.yaml"), search code for keywords (e.g. "database connection"), or answer questions about the codebase (e.g. "how does the auth module work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "thorough" for comprehensive analysis across multiple locations and naming conventions. Use this agent for any read-only exploration that will clearly require more than 3 tool calls. Prefer launching multiple explore agents concurrently when investigating independent questions. Absence claims come with the searches that back them.', + "- `implementer`: Scoped implementation with minimal edits and verification. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent when the required code change is already specified and should be implemented with minimal, idiomatic edits and a quick verification pass. It executes the spec faithfully — escalating instead of improvising when the spec does not match reality — and emits a block so the result can be chained directly into the verifier.", + "- `judge`: Independent final quality gate for answers, reports, and code-change summaries. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent as an independent final quality gate and advisor before delivering non-trivial code changes, reports, audits, or findings to the user. It judges the parent agent's evidence, actions, and proposed final answer — verifying claims against the packet's artifacts and local sources, and requiring the parent's citation for load-bearing external-API, version, and best-practice claims it cannot check offline — and recommends fixes without ever applying them.", + "- `plan`: Read-only implementation planning and architecture design. (Tools: SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent when the parent agent needs a step-by-step implementation plan, key file identification, and architectural trade-off analysis before code changes are made. It returns dependency-ordered, wave-parallelized tasks — each with artifacts, acceptance criteria, a specialist recommendation, and a proving verification — grounded in repository evidence and current third-party documentation.", + "- `planner`: Read-only recon planner that decomposes tasks into distinct parallel seeds. (Tools: Shell, ReadFile, Glob, Grep, SmartSearch). When to use: Use this agent before spawning N parallel workers on a large or open-ended task. It scouts the repository cheaply, partitions the problem space along one decomposition axis, and returns distinct, self-contained seeds so workers start from non-overlapping vantage points. A single-seed result signals the task is not worth parallelizing.", + "- `review`: Read-only code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent for direct, read-only code review after changes are made, or when the parent needs severity-scored findings before deciding what to fix. It reviews the diff/files itself with reads and searches — for the CLI/Reviewflow-driven review pipeline, use `code-reviewer` instead. Findings arrive BLOCKER-first with evidence, trigger conditions, and a dispatch-ready fix description; it runs offline by design, so third-party API claims it cannot verify from the repository are explicitly downgraded to needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `scout`: Read-only external docs, dependency-source, and API freshness researcher. (Tools: Shell, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent for external libraries, SDK docs, upstream source comparisons, API freshness checks, registry/package verification, and dependency behavior research — including verifying the `needs verification` third-party claims that offline reviewer/debugger agents return under RISKS. It returns version-pinned, source-cited facts — local installed source first, then official docs via live web research — with conflicts and unverifiable gaps reported explicitly instead of papered over.", + "- `security-reviewer`: Diff-focused security review with validated findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep). When to use: Use for security review: diff-only review on the current branch (default) or repo-wide vulnerability discovery via the security-scan pipeline. Can run in parallel with `code-reviewer`; for large diffs, scope each instance to the trust-boundary files of one subsystem. Returns reachability-validated findings — source → sink anchored, precondition-stated, CWE-classified, version-checked against the project's pins — with scanner hits treated as leads until verified. It runs offline by design, so advisory-dependent claims come back under RISKS as needs-verification items for the parent to check.", + '- `verifier`: Read-only validation runner for tests, lint, and builds. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent when the parent needs tests, lint, type checks, builds, or other validation gates run and reported without applying fixes — e.g. "run the tests", "does it build", post-edit gate checks, or re-running a suspected flaky suite. Not for fixing failures, writing tests, updating snapshots, or formatting: it is read-only by design and reports proposed fixes under RISKS instead of applying them.', + ], + "complete": True, + }, + }, + { + "method": "event", + "type": "TodoListUpdated", + "payload": {"items": [["x", "pending"]], "complete": False, "source": "tool"}, + }, { "method": "event", "type": "TurnEnd", @@ -538,6 +585,27 @@ def test_concurrent_prompt_error(tmp_path) -> None: }, }, }, + { + "method": "event", + "type": "AgentListDelta", + "payload": { + "items": [ + "- `code-reviewer`: Diff-focused code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, ReadSkill). When to use: Use to run a read-only, diff-focused, professional code review — severity-scored findings across correctness, security, reliability, performance, maintainability, and standards compliance, in any programming language — or a code-reviewr-derived PR artifact workflow on the current branch. It runs offline by design and never modifies the repository; third-party API claims it cannot verify from the repository come back under RISKS as needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `coder`: Good at general software engineering tasks. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent for non-trivial software engineering work that may require reading files, editing code, running commands, and returning a compact but technically complete summary to the parent agent. It delivers production-ready, idiomatic, verified changes in any language the project uses, with current-docs verification for third-party APIs, and never expands beyond its brief.", + "- `debugger`: Failure/log/stack-trace root-cause analysis with reproduction evidence. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, SmartSearch). When to use: Use for failing tests, stack traces, runtime errors, flaky failures, regressions, or debugging requests where the root cause should be found before editing code. Read-only and safe to fan out in parallel — one focused failure per instance — it returns the named mechanism, confidence, evidence, the recommended minimal fix, and the verification that would prove it.", + '- `explore`: Fast codebase exploration with prompt-enforced read-only behavior. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (e.g. "src/**/*.yaml"), search code for keywords (e.g. "database connection"), or answer questions about the codebase (e.g. "how does the auth module work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "thorough" for comprehensive analysis across multiple locations and naming conventions. Use this agent for any read-only exploration that will clearly require more than 3 tool calls. Prefer launching multiple explore agents concurrently when investigating independent questions. Absence claims come with the searches that back them.', + "- `implementer`: Scoped implementation with minimal edits and verification. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent when the required code change is already specified and should be implemented with minimal, idiomatic edits and a quick verification pass. It executes the spec faithfully — escalating instead of improvising when the spec does not match reality — and emits a block so the result can be chained directly into the verifier.", + "- `judge`: Independent final quality gate for answers, reports, and code-change summaries. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent as an independent final quality gate and advisor before delivering non-trivial code changes, reports, audits, or findings to the user. It judges the parent agent's evidence, actions, and proposed final answer — verifying claims against the packet's artifacts and local sources, and requiring the parent's citation for load-bearing external-API, version, and best-practice claims it cannot check offline — and recommends fixes without ever applying them.", + "- `plan`: Read-only implementation planning and architecture design. (Tools: SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent when the parent agent needs a step-by-step implementation plan, key file identification, and architectural trade-off analysis before code changes are made. It returns dependency-ordered, wave-parallelized tasks — each with artifacts, acceptance criteria, a specialist recommendation, and a proving verification — grounded in repository evidence and current third-party documentation.", + "- `planner`: Read-only recon planner that decomposes tasks into distinct parallel seeds. (Tools: Shell, ReadFile, Glob, Grep, SmartSearch). When to use: Use this agent before spawning N parallel workers on a large or open-ended task. It scouts the repository cheaply, partitions the problem space along one decomposition axis, and returns distinct, self-contained seeds so workers start from non-overlapping vantage points. A single-seed result signals the task is not worth parallelizing.", + "- `review`: Read-only code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent for direct, read-only code review after changes are made, or when the parent needs severity-scored findings before deciding what to fix. It reviews the diff/files itself with reads and searches — for the CLI/Reviewflow-driven review pipeline, use `code-reviewer` instead. Findings arrive BLOCKER-first with evidence, trigger conditions, and a dispatch-ready fix description; it runs offline by design, so third-party API claims it cannot verify from the repository are explicitly downgraded to needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `scout`: Read-only external docs, dependency-source, and API freshness researcher. (Tools: Shell, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent for external libraries, SDK docs, upstream source comparisons, API freshness checks, registry/package verification, and dependency behavior research — including verifying the `needs verification` third-party claims that offline reviewer/debugger agents return under RISKS. It returns version-pinned, source-cited facts — local installed source first, then official docs via live web research — with conflicts and unverifiable gaps reported explicitly instead of papered over.", + "- `security-reviewer`: Diff-focused security review with validated findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep). When to use: Use for security review: diff-only review on the current branch (default) or repo-wide vulnerability discovery via the security-scan pipeline. Can run in parallel with `code-reviewer`; for large diffs, scope each instance to the trust-boundary files of one subsystem. Returns reachability-validated findings — source → sink anchored, precondition-stated, CWE-classified, version-checked against the project's pins — with scanner hits treated as leads until verified. It runs offline by design, so advisory-dependent claims come back under RISKS as needs-verification items for the parent to check.", + '- `verifier`: Read-only validation runner for tests, lint, and builds. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent when the parent needs tests, lint, type checks, builds, or other validation gates run and reported without applying fixes — e.g. "run the tests", "does it build", post-edit gate checks, or re-running a suspected flaky suite. Not for fixing failures, writing tests, updating snapshots, or formatting: it is read-only by design and reports proposed fixes under RISKS instead of applying them.', + ], + "complete": True, + }, + }, {"method": "event", "type": "StepBegin", "payload": {"n": 2}}, { "method": "event", diff --git a/tests_e2e/test_wire_protocol.py b/tests_e2e/test_wire_protocol.py index ea00ae1f..9aeb0652 100644 --- a/tests_e2e/test_wire_protocol.py +++ b/tests_e2e/test_wire_protocol.py @@ -568,6 +568,27 @@ def handle_request(msg: dict[str, Any]) -> dict[str, Any]: }, }, }, + { + "method": "event", + "type": "AgentListDelta", + "payload": { + "items": [ + "- `code-reviewer`: Diff-focused code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, ReadSkill). When to use: Use to run a read-only, diff-focused, professional code review — severity-scored findings across correctness, security, reliability, performance, maintainability, and standards compliance, in any programming language — or a code-reviewr-derived PR artifact workflow on the current branch. It runs offline by design and never modifies the repository; third-party API claims it cannot verify from the repository come back under RISKS as needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `coder`: Good at general software engineering tasks. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent for non-trivial software engineering work that may require reading files, editing code, running commands, and returning a compact but technically complete summary to the parent agent. It delivers production-ready, idiomatic, verified changes in any language the project uses, with current-docs verification for third-party APIs, and never expands beyond its brief.", + "- `debugger`: Failure/log/stack-trace root-cause analysis with reproduction evidence. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, SmartSearch). When to use: Use for failing tests, stack traces, runtime errors, flaky failures, regressions, or debugging requests where the root cause should be found before editing code. Read-only and safe to fan out in parallel — one focused failure per instance — it returns the named mechanism, confidence, evidence, the recommended minimal fix, and the verification that would prove it.", + '- `explore`: Fast codebase exploration with prompt-enforced read-only behavior. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (e.g. "src/**/*.yaml"), search code for keywords (e.g. "database connection"), or answer questions about the codebase (e.g. "how does the auth module work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "thorough" for comprehensive analysis across multiple locations and naming conventions. Use this agent for any read-only exploration that will clearly require more than 3 tool calls. Prefer launching multiple explore agents concurrently when investigating independent questions. Absence claims come with the searches that back them.', + "- `implementer`: Scoped implementation with minimal edits and verification. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent when the required code change is already specified and should be implemented with minimal, idiomatic edits and a quick verification pass. It executes the spec faithfully — escalating instead of improvising when the spec does not match reality — and emits a block so the result can be chained directly into the verifier.", + "- `judge`: Independent final quality gate for answers, reports, and code-change summaries. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent as an independent final quality gate and advisor before delivering non-trivial code changes, reports, audits, or findings to the user. It judges the parent agent's evidence, actions, and proposed final answer — verifying claims against the packet's artifacts and local sources, and requiring the parent's citation for load-bearing external-API, version, and best-practice claims it cannot check offline — and recommends fixes without ever applying them.", + "- `plan`: Read-only implementation planning and architecture design. (Tools: SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent when the parent agent needs a step-by-step implementation plan, key file identification, and architectural trade-off analysis before code changes are made. It returns dependency-ordered, wave-parallelized tasks — each with artifacts, acceptance criteria, a specialist recommendation, and a proving verification — grounded in repository evidence and current third-party documentation.", + "- `planner`: Read-only recon planner that decomposes tasks into distinct parallel seeds. (Tools: Shell, ReadFile, Glob, Grep, SmartSearch). When to use: Use this agent before spawning N parallel workers on a large or open-ended task. It scouts the repository cheaply, partitions the problem space along one decomposition axis, and returns distinct, self-contained seeds so workers start from non-overlapping vantage points. A single-seed result signals the task is not worth parallelizing.", + "- `review`: Read-only code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent for direct, read-only code review after changes are made, or when the parent needs severity-scored findings before deciding what to fix. It reviews the diff/files itself with reads and searches — for the CLI/Reviewflow-driven review pipeline, use `code-reviewer` instead. Findings arrive BLOCKER-first with evidence, trigger conditions, and a dispatch-ready fix description; it runs offline by design, so third-party API claims it cannot verify from the repository are explicitly downgraded to needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `scout`: Read-only external docs, dependency-source, and API freshness researcher. (Tools: Shell, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent for external libraries, SDK docs, upstream source comparisons, API freshness checks, registry/package verification, and dependency behavior research — including verifying the `needs verification` third-party claims that offline reviewer/debugger agents return under RISKS. It returns version-pinned, source-cited facts — local installed source first, then official docs via live web research — with conflicts and unverifiable gaps reported explicitly instead of papered over.", + "- `security-reviewer`: Diff-focused security review with validated findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep). When to use: Use for security review: diff-only review on the current branch (default) or repo-wide vulnerability discovery via the security-scan pipeline. Can run in parallel with `code-reviewer`; for large diffs, scope each instance to the trust-boundary files of one subsystem. Returns reachability-validated findings — source → sink anchored, precondition-stated, CWE-classified, version-checked against the project's pins — with scanner hits treated as leads until verified. It runs offline by design, so advisory-dependent claims come back under RISKS as needs-verification items for the parent to check.", + '- `verifier`: Read-only validation runner for tests, lint, and builds. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent when the parent needs tests, lint, type checks, builds, or other validation gates run and reported without applying fixes — e.g. "run the tests", "does it build", post-edit gate checks, or re-running a suspected flaky suite. Not for fixing failures, writing tests, updating snapshots, or formatting: it is read-only by design and reports proposed fixes under RISKS instead of applying them.', + ], + "complete": True, + }, + }, {"method": "event", "type": "StepBegin", "payload": {"n": 2}}, { "method": "event", @@ -643,6 +664,27 @@ def test_prompt_without_initialize(tmp_path) -> None: "mcp_status": None, }, }, + { + "method": "event", + "type": "AgentListDelta", + "payload": { + "items": [ + "- `code-reviewer`: Diff-focused code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, ReadSkill). When to use: Use to run a read-only, diff-focused, professional code review — severity-scored findings across correctness, security, reliability, performance, maintainability, and standards compliance, in any programming language — or a code-reviewr-derived PR artifact workflow on the current branch. It runs offline by design and never modifies the repository; third-party API claims it cannot verify from the repository come back under RISKS as needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `coder`: Good at general software engineering tasks. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent for non-trivial software engineering work that may require reading files, editing code, running commands, and returning a compact but technically complete summary to the parent agent. It delivers production-ready, idiomatic, verified changes in any language the project uses, with current-docs verification for third-party APIs, and never expands beyond its brief.", + "- `debugger`: Failure/log/stack-trace root-cause analysis with reproduction evidence. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, SmartSearch). When to use: Use for failing tests, stack traces, runtime errors, flaky failures, regressions, or debugging requests where the root cause should be found before editing code. Read-only and safe to fan out in parallel — one focused failure per instance — it returns the named mechanism, confidence, evidence, the recommended minimal fix, and the verification that would prove it.", + '- `explore`: Fast codebase exploration with prompt-enforced read-only behavior. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (e.g. "src/**/*.yaml"), search code for keywords (e.g. "database connection"), or answer questions about the codebase (e.g. "how does the auth module work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "thorough" for comprehensive analysis across multiple locations and naming conventions. Use this agent for any read-only exploration that will clearly require more than 3 tool calls. Prefer launching multiple explore agents concurrently when investigating independent questions. Absence claims come with the searches that back them.', + "- `implementer`: Scoped implementation with minimal edits and verification. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent when the required code change is already specified and should be implemented with minimal, idiomatic edits and a quick verification pass. It executes the spec faithfully — escalating instead of improvising when the spec does not match reality — and emits a block so the result can be chained directly into the verifier.", + "- `judge`: Independent final quality gate for answers, reports, and code-change summaries. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent as an independent final quality gate and advisor before delivering non-trivial code changes, reports, audits, or findings to the user. It judges the parent agent's evidence, actions, and proposed final answer — verifying claims against the packet's artifacts and local sources, and requiring the parent's citation for load-bearing external-API, version, and best-practice claims it cannot check offline — and recommends fixes without ever applying them.", + "- `plan`: Read-only implementation planning and architecture design. (Tools: SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent when the parent agent needs a step-by-step implementation plan, key file identification, and architectural trade-off analysis before code changes are made. It returns dependency-ordered, wave-parallelized tasks — each with artifacts, acceptance criteria, a specialist recommendation, and a proving verification — grounded in repository evidence and current third-party documentation.", + "- `planner`: Read-only recon planner that decomposes tasks into distinct parallel seeds. (Tools: Shell, ReadFile, Glob, Grep, SmartSearch). When to use: Use this agent before spawning N parallel workers on a large or open-ended task. It scouts the repository cheaply, partitions the problem space along one decomposition axis, and returns distinct, self-contained seeds so workers start from non-overlapping vantage points. A single-seed result signals the task is not worth parallelizing.", + "- `review`: Read-only code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent for direct, read-only code review after changes are made, or when the parent needs severity-scored findings before deciding what to fix. It reviews the diff/files itself with reads and searches — for the CLI/Reviewflow-driven review pipeline, use `code-reviewer` instead. Findings arrive BLOCKER-first with evidence, trigger conditions, and a dispatch-ready fix description; it runs offline by design, so third-party API claims it cannot verify from the repository are explicitly downgraded to needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `scout`: Read-only external docs, dependency-source, and API freshness researcher. (Tools: Shell, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent for external libraries, SDK docs, upstream source comparisons, API freshness checks, registry/package verification, and dependency behavior research — including verifying the `needs verification` third-party claims that offline reviewer/debugger agents return under RISKS. It returns version-pinned, source-cited facts — local installed source first, then official docs via live web research — with conflicts and unverifiable gaps reported explicitly instead of papered over.", + "- `security-reviewer`: Diff-focused security review with validated findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep). When to use: Use for security review: diff-only review on the current branch (default) or repo-wide vulnerability discovery via the security-scan pipeline. Can run in parallel with `code-reviewer`; for large diffs, scope each instance to the trust-boundary files of one subsystem. Returns reachability-validated findings — source → sink anchored, precondition-stated, CWE-classified, version-checked against the project's pins — with scanner hits treated as leads until verified. It runs offline by design, so advisory-dependent claims come back under RISKS as needs-verification items for the parent to check.", + '- `verifier`: Read-only validation runner for tests, lint, and builds. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent when the parent needs tests, lint, type checks, builds, or other validation gates run and reported without applying fixes — e.g. "run the tests", "does it build", post-edit gate checks, or re-running a suspected flaky suite. Not for fixing failures, writing tests, updating snapshots, or formatting: it is read-only by design and reports proposed fixes under RISKS instead of applying them.', + ], + "complete": True, + }, + }, {"method": "event", "type": "TurnEnd", "payload": {}}, ] ) diff --git a/tests_e2e/test_wire_sessions.py b/tests_e2e/test_wire_sessions.py index b3b14727..8a7d5fd0 100644 --- a/tests_e2e/test_wire_sessions.py +++ b/tests_e2e/test_wire_sessions.py @@ -152,7 +152,7 @@ def test_continue_session_appends(tmp_path) -> None: "context_after": context_after, "wire_before": wire_before, "wire_after": wire_after, - } == snapshot({"context_before": 6, "context_after": 11, "wire_before": 6, "wire_after": 11}) + } == snapshot({"context_before": 6, "context_after": 11, "wire_before": 7, "wire_after": 13}) assert _read_roles(context_file) == snapshot( [ "_system_prompt", @@ -304,8 +304,8 @@ def test_manual_compact(tmp_path) -> None: "method": "event", "type": "StatusUpdate", "payload": { - "context_usage": 0.00168, - "context_tokens": 168, + "context_usage": 0.01913, + "context_tokens": 1913, "max_context_tokens": 100000, "token_usage": None, "message_id": None, @@ -431,7 +431,7 @@ def test_replay_streams_wire_history(tmp_path) -> None: assert resp.get("result") == snapshot( { "status": "finished", - "events": 12, + "events": 13, "requests": 0, } ) @@ -502,6 +502,27 @@ def test_replay_streams_wire_history(tmp_path) -> None: }, }, }, + { + "method": "event", + "type": "AgentListDelta", + "payload": { + "items": [ + "- `code-reviewer`: Diff-focused code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, ReadSkill). When to use: Use to run a read-only, diff-focused, professional code review — severity-scored findings across correctness, security, reliability, performance, maintainability, and standards compliance, in any programming language — or a code-reviewr-derived PR artifact workflow on the current branch. It runs offline by design and never modifies the repository; third-party API claims it cannot verify from the repository come back under RISKS as needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `coder`: Good at general software engineering tasks. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent for non-trivial software engineering work that may require reading files, editing code, running commands, and returning a compact but technically complete summary to the parent agent. It delivers production-ready, idiomatic, verified changes in any language the project uses, with current-docs verification for third-party APIs, and never expands beyond its brief.", + "- `debugger`: Failure/log/stack-trace root-cause analysis with reproduction evidence. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, SmartSearch). When to use: Use for failing tests, stack traces, runtime errors, flaky failures, regressions, or debugging requests where the root cause should be found before editing code. Read-only and safe to fan out in parallel — one focused failure per instance — it returns the named mechanism, confidence, evidence, the recommended minimal fix, and the verification that would prove it.", + '- `explore`: Fast codebase exploration with prompt-enforced read-only behavior. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (e.g. "src/**/*.yaml"), search code for keywords (e.g. "database connection"), or answer questions about the codebase (e.g. "how does the auth module work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "thorough" for comprehensive analysis across multiple locations and naming conventions. Use this agent for any read-only exploration that will clearly require more than 3 tool calls. Prefer launching multiple explore agents concurrently when investigating independent questions. Absence claims come with the searches that back them.', + "- `implementer`: Scoped implementation with minimal edits and verification. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent when the required code change is already specified and should be implemented with minimal, idiomatic edits and a quick verification pass. It executes the spec faithfully — escalating instead of improvising when the spec does not match reality — and emits a block so the result can be chained directly into the verifier.", + "- `judge`: Independent final quality gate for answers, reports, and code-change summaries. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent as an independent final quality gate and advisor before delivering non-trivial code changes, reports, audits, or findings to the user. It judges the parent agent's evidence, actions, and proposed final answer — verifying claims against the packet's artifacts and local sources, and requiring the parent's citation for load-bearing external-API, version, and best-practice claims it cannot check offline — and recommends fixes without ever applying them.", + "- `plan`: Read-only implementation planning and architecture design. (Tools: SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent when the parent agent needs a step-by-step implementation plan, key file identification, and architectural trade-off analysis before code changes are made. It returns dependency-ordered, wave-parallelized tasks — each with artifacts, acceptance criteria, a specialist recommendation, and a proving verification — grounded in repository evidence and current third-party documentation.", + "- `planner`: Read-only recon planner that decomposes tasks into distinct parallel seeds. (Tools: Shell, ReadFile, Glob, Grep, SmartSearch). When to use: Use this agent before spawning N parallel workers on a large or open-ended task. It scouts the repository cheaply, partitions the problem space along one decomposition axis, and returns distinct, self-contained seeds so workers start from non-overlapping vantage points. A single-seed result signals the task is not worth parallelizing.", + "- `review`: Read-only code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent for direct, read-only code review after changes are made, or when the parent needs severity-scored findings before deciding what to fix. It reviews the diff/files itself with reads and searches — for the CLI/Reviewflow-driven review pipeline, use `code-reviewer` instead. Findings arrive BLOCKER-first with evidence, trigger conditions, and a dispatch-ready fix description; it runs offline by design, so third-party API claims it cannot verify from the repository are explicitly downgraded to needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `scout`: Read-only external docs, dependency-source, and API freshness researcher. (Tools: Shell, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent for external libraries, SDK docs, upstream source comparisons, API freshness checks, registry/package verification, and dependency behavior research — including verifying the `needs verification` third-party claims that offline reviewer/debugger agents return under RISKS. It returns version-pinned, source-cited facts — local installed source first, then official docs via live web research — with conflicts and unverifiable gaps reported explicitly instead of papered over.", + "- `security-reviewer`: Diff-focused security review with validated findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep). When to use: Use for security review: diff-only review on the current branch (default) or repo-wide vulnerability discovery via the security-scan pipeline. Can run in parallel with `code-reviewer`; for large diffs, scope each instance to the trust-boundary files of one subsystem. Returns reachability-validated findings — source → sink anchored, precondition-stated, CWE-classified, version-checked against the project's pins — with scanner hits treated as leads until verified. It runs offline by design, so advisory-dependent claims come back under RISKS as needs-verification items for the parent to check.", + '- `verifier`: Read-only validation runner for tests, lint, and builds. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent when the parent needs tests, lint, type checks, builds, or other validation gates run and reported without applying fixes — e.g. "run the tests", "does it build", post-edit gate checks, or re-running a suspected flaky suite. Not for fixing failures, writing tests, updating snapshots, or formatting: it is read-only by design and reports proposed fixes under RISKS instead of applying them.', + ], + "complete": True, + }, + }, {"method": "event", "type": "StepBegin", "payload": {"n": 2}}, { "method": "event", diff --git a/tests_e2e/test_wire_skills_mcp.py b/tests_e2e/test_wire_skills_mcp.py index 3048aa3c..3ca4a9e0 100644 --- a/tests_e2e/test_wire_skills_mcp.py +++ b/tests_e2e/test_wire_skills_mcp.py @@ -122,6 +122,27 @@ def test_skill_prompt_injects_skill_text(tmp_path) -> None: "mcp_status": None, }, }, + { + "method": "event", + "type": "AgentListDelta", + "payload": { + "items": [ + "- `code-reviewer`: Diff-focused code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, ReadSkill). When to use: Use to run a read-only, diff-focused, professional code review — severity-scored findings across correctness, security, reliability, performance, maintainability, and standards compliance, in any programming language — or a code-reviewr-derived PR artifact workflow on the current branch. It runs offline by design and never modifies the repository; third-party API claims it cannot verify from the repository come back under RISKS as needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `coder`: Good at general software engineering tasks. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent for non-trivial software engineering work that may require reading files, editing code, running commands, and returning a compact but technically complete summary to the parent agent. It delivers production-ready, idiomatic, verified changes in any language the project uses, with current-docs verification for third-party APIs, and never expands beyond its brief.", + "- `debugger`: Failure/log/stack-trace root-cause analysis with reproduction evidence. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, SmartSearch). When to use: Use for failing tests, stack traces, runtime errors, flaky failures, regressions, or debugging requests where the root cause should be found before editing code. Read-only and safe to fan out in parallel — one focused failure per instance — it returns the named mechanism, confidence, evidence, the recommended minimal fix, and the verification that would prove it.", + '- `explore`: Fast codebase exploration with prompt-enforced read-only behavior. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (e.g. "src/**/*.yaml"), search code for keywords (e.g. "database connection"), or answer questions about the codebase (e.g. "how does the auth module work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "thorough" for comprehensive analysis across multiple locations and naming conventions. Use this agent for any read-only exploration that will clearly require more than 3 tool calls. Prefer launching multiple explore agents concurrently when investigating independent questions. Absence claims come with the searches that back them.', + "- `implementer`: Scoped implementation with minimal edits and verification. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent when the required code change is already specified and should be implemented with minimal, idiomatic edits and a quick verification pass. It executes the spec faithfully — escalating instead of improvising when the spec does not match reality — and emits a block so the result can be chained directly into the verifier.", + "- `judge`: Independent final quality gate for answers, reports, and code-change summaries. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent as an independent final quality gate and advisor before delivering non-trivial code changes, reports, audits, or findings to the user. It judges the parent agent's evidence, actions, and proposed final answer — verifying claims against the packet's artifacts and local sources, and requiring the parent's citation for load-bearing external-API, version, and best-practice claims it cannot check offline — and recommends fixes without ever applying them.", + "- `plan`: Read-only implementation planning and architecture design. (Tools: SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent when the parent agent needs a step-by-step implementation plan, key file identification, and architectural trade-off analysis before code changes are made. It returns dependency-ordered, wave-parallelized tasks — each with artifacts, acceptance criteria, a specialist recommendation, and a proving verification — grounded in repository evidence and current third-party documentation.", + "- `planner`: Read-only recon planner that decomposes tasks into distinct parallel seeds. (Tools: Shell, ReadFile, Glob, Grep, SmartSearch). When to use: Use this agent before spawning N parallel workers on a large or open-ended task. It scouts the repository cheaply, partitions the problem space along one decomposition axis, and returns distinct, self-contained seeds so workers start from non-overlapping vantage points. A single-seed result signals the task is not worth parallelizing.", + "- `review`: Read-only code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent for direct, read-only code review after changes are made, or when the parent needs severity-scored findings before deciding what to fix. It reviews the diff/files itself with reads and searches — for the CLI/Reviewflow-driven review pipeline, use `code-reviewer` instead. Findings arrive BLOCKER-first with evidence, trigger conditions, and a dispatch-ready fix description; it runs offline by design, so third-party API claims it cannot verify from the repository are explicitly downgraded to needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `scout`: Read-only external docs, dependency-source, and API freshness researcher. (Tools: Shell, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent for external libraries, SDK docs, upstream source comparisons, API freshness checks, registry/package verification, and dependency behavior research — including verifying the `needs verification` third-party claims that offline reviewer/debugger agents return under RISKS. It returns version-pinned, source-cited facts — local installed source first, then official docs via live web research — with conflicts and unverifiable gaps reported explicitly instead of papered over.", + "- `security-reviewer`: Diff-focused security review with validated findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep). When to use: Use for security review: diff-only review on the current branch (default) or repo-wide vulnerability discovery via the security-scan pipeline. Can run in parallel with `code-reviewer`; for large diffs, scope each instance to the trust-boundary files of one subsystem. Returns reachability-validated findings — source → sink anchored, precondition-stated, CWE-classified, version-checked against the project's pins — with scanner hits treated as leads until verified. It runs offline by design, so advisory-dependent claims come back under RISKS as needs-verification items for the parent to check.", + '- `verifier`: Read-only validation runner for tests, lint, and builds. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent when the parent needs tests, lint, type checks, builds, or other validation gates run and reported without applying fixes — e.g. "run the tests", "does it build", post-edit gate checks, or re-running a suspected flaky suite. Not for fixing failures, writing tests, updating snapshots, or formatting: it is read-only by design and reports proposed fixes under RISKS instead of applying them.', + ], + "complete": True, + }, + }, {"method": "event", "type": "TurnEnd", "payload": {}}, ] ) @@ -215,6 +236,27 @@ def test_flow_skill(tmp_path) -> None: "mcp_status": None, }, }, + { + "method": "event", + "type": "AgentListDelta", + "payload": { + "items": [ + "- `code-reviewer`: Diff-focused code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, ReadSkill). When to use: Use to run a read-only, diff-focused, professional code review — severity-scored findings across correctness, security, reliability, performance, maintainability, and standards compliance, in any programming language — or a code-reviewr-derived PR artifact workflow on the current branch. It runs offline by design and never modifies the repository; third-party API claims it cannot verify from the repository come back under RISKS as needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `coder`: Good at general software engineering tasks. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent for non-trivial software engineering work that may require reading files, editing code, running commands, and returning a compact but technically complete summary to the parent agent. It delivers production-ready, idiomatic, verified changes in any language the project uses, with current-docs verification for third-party APIs, and never expands beyond its brief.", + "- `debugger`: Failure/log/stack-trace root-cause analysis with reproduction evidence. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, SmartSearch). When to use: Use for failing tests, stack traces, runtime errors, flaky failures, regressions, or debugging requests where the root cause should be found before editing code. Read-only and safe to fan out in parallel — one focused failure per instance — it returns the named mechanism, confidence, evidence, the recommended minimal fix, and the verification that would prove it.", + '- `explore`: Fast codebase exploration with prompt-enforced read-only behavior. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (e.g. "src/**/*.yaml"), search code for keywords (e.g. "database connection"), or answer questions about the codebase (e.g. "how does the auth module work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "thorough" for comprehensive analysis across multiple locations and naming conventions. Use this agent for any read-only exploration that will clearly require more than 3 tool calls. Prefer launching multiple explore agents concurrently when investigating independent questions. Absence claims come with the searches that back them.', + "- `implementer`: Scoped implementation with minimal edits and verification. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent when the required code change is already specified and should be implemented with minimal, idiomatic edits and a quick verification pass. It executes the spec faithfully — escalating instead of improvising when the spec does not match reality — and emits a block so the result can be chained directly into the verifier.", + "- `judge`: Independent final quality gate for answers, reports, and code-change summaries. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent as an independent final quality gate and advisor before delivering non-trivial code changes, reports, audits, or findings to the user. It judges the parent agent's evidence, actions, and proposed final answer — verifying claims against the packet's artifacts and local sources, and requiring the parent's citation for load-bearing external-API, version, and best-practice claims it cannot check offline — and recommends fixes without ever applying them.", + "- `plan`: Read-only implementation planning and architecture design. (Tools: SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent when the parent agent needs a step-by-step implementation plan, key file identification, and architectural trade-off analysis before code changes are made. It returns dependency-ordered, wave-parallelized tasks — each with artifacts, acceptance criteria, a specialist recommendation, and a proving verification — grounded in repository evidence and current third-party documentation.", + "- `planner`: Read-only recon planner that decomposes tasks into distinct parallel seeds. (Tools: Shell, ReadFile, Glob, Grep, SmartSearch). When to use: Use this agent before spawning N parallel workers on a large or open-ended task. It scouts the repository cheaply, partitions the problem space along one decomposition axis, and returns distinct, self-contained seeds so workers start from non-overlapping vantage points. A single-seed result signals the task is not worth parallelizing.", + "- `review`: Read-only code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent for direct, read-only code review after changes are made, or when the parent needs severity-scored findings before deciding what to fix. It reviews the diff/files itself with reads and searches — for the CLI/Reviewflow-driven review pipeline, use `code-reviewer` instead. Findings arrive BLOCKER-first with evidence, trigger conditions, and a dispatch-ready fix description; it runs offline by design, so third-party API claims it cannot verify from the repository are explicitly downgraded to needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `scout`: Read-only external docs, dependency-source, and API freshness researcher. (Tools: Shell, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent for external libraries, SDK docs, upstream source comparisons, API freshness checks, registry/package verification, and dependency behavior research — including verifying the `needs verification` third-party claims that offline reviewer/debugger agents return under RISKS. It returns version-pinned, source-cited facts — local installed source first, then official docs via live web research — with conflicts and unverifiable gaps reported explicitly instead of papered over.", + "- `security-reviewer`: Diff-focused security review with validated findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep). When to use: Use for security review: diff-only review on the current branch (default) or repo-wide vulnerability discovery via the security-scan pipeline. Can run in parallel with `code-reviewer`; for large diffs, scope each instance to the trust-boundary files of one subsystem. Returns reachability-validated findings — source → sink anchored, precondition-stated, CWE-classified, version-checked against the project's pins — with scanner hits treated as leads until verified. It runs offline by design, so advisory-dependent claims come back under RISKS as needs-verification items for the parent to check.", + '- `verifier`: Read-only validation runner for tests, lint, and builds. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent when the parent needs tests, lint, type checks, builds, or other validation gates run and reported without applying fixes — e.g. "run the tests", "does it build", post-edit gate checks, or re-running a suspected flaky suite. Not for fixing failures, writing tests, updating snapshots, or formatting: it is read-only by design and reports proposed fixes under RISKS instead of applying them.', + ], + "complete": True, + }, + }, {"method": "event", "type": "TurnEnd", "payload": {}}, {"method": "event", "type": "TurnEnd", "payload": {}}, ] @@ -425,6 +467,27 @@ def ping(text: str) -> str: }, }, }, + { + "method": "event", + "type": "AgentListDelta", + "payload": { + "items": [ + "- `code-reviewer`: Diff-focused code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, ReadSkill). When to use: Use to run a read-only, diff-focused, professional code review — severity-scored findings across correctness, security, reliability, performance, maintainability, and standards compliance, in any programming language — or a code-reviewr-derived PR artifact workflow on the current branch. It runs offline by design and never modifies the repository; third-party API claims it cannot verify from the repository come back under RISKS as needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `coder`: Good at general software engineering tasks. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent for non-trivial software engineering work that may require reading files, editing code, running commands, and returning a compact but technically complete summary to the parent agent. It delivers production-ready, idiomatic, verified changes in any language the project uses, with current-docs verification for third-party APIs, and never expands beyond its brief.", + "- `debugger`: Failure/log/stack-trace root-cause analysis with reproduction evidence. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep, SmartSearch). When to use: Use for failing tests, stack traces, runtime errors, flaky failures, regressions, or debugging requests where the root cause should be found before editing code. Read-only and safe to fan out in parallel — one focused failure per instance — it returns the named mechanism, confidence, evidence, the recommended minimal fix, and the verification that would prove it.", + '- `explore`: Fast codebase exploration with prompt-enforced read-only behavior. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (e.g. "src/**/*.yaml"), search code for keywords (e.g. "database connection"), or answer questions about the codebase (e.g. "how does the auth module work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "thorough" for comprehensive analysis across multiple locations and naming conventions. Use this agent for any read-only exploration that will clearly require more than 3 tool calls. Prefer launching multiple explore agents concurrently when investigating independent questions. Absence claims come with the searches that back them.', + "- `implementer`: Scoped implementation with minimal edits and verification. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, WriteFile, StrReplaceFile, ReadSkill, SearchWeb, FetchURL, mcp__context7__resolve-library-id, mcp__context7__query-docs). When to use: Use this agent when the required code change is already specified and should be implemented with minimal, idiomatic edits and a quick verification pass. It executes the spec faithfully — escalating instead of improvising when the spec does not match reality — and emits a block so the result can be chained directly into the verifier.", + "- `judge`: Independent final quality gate for answers, reports, and code-change summaries. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent as an independent final quality gate and advisor before delivering non-trivial code changes, reports, audits, or findings to the user. It judges the parent agent's evidence, actions, and proposed final answer — verifying claims against the packet's artifacts and local sources, and requiring the parent's citation for load-bearing external-API, version, and best-practice claims it cannot check offline — and recommends fixes without ever applying them.", + "- `plan`: Read-only implementation planning and architecture design. (Tools: SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent when the parent agent needs a step-by-step implementation plan, key file identification, and architectural trade-off analysis before code changes are made. It returns dependency-ordered, wave-parallelized tasks — each with artifacts, acceptance criteria, a specialist recommendation, and a proving verification — grounded in repository evidence and current third-party documentation.", + "- `planner`: Read-only recon planner that decomposes tasks into distinct parallel seeds. (Tools: Shell, ReadFile, Glob, Grep, SmartSearch). When to use: Use this agent before spawning N parallel workers on a large or open-ended task. It scouts the repository cheaply, partitions the problem space along one decomposition axis, and returns distinct, self-contained seeds so workers start from non-overlapping vantage points. A single-seed result signals the task is not worth parallelizing.", + "- `review`: Read-only code review with severity-scored findings. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent for direct, read-only code review after changes are made, or when the parent needs severity-scored findings before deciding what to fix. It reviews the diff/files itself with reads and searches — for the CLI/Reviewflow-driven review pipeline, use `code-reviewer` instead. Findings arrive BLOCKER-first with evidence, trigger conditions, and a dispatch-ready fix description; it runs offline by design, so third-party API claims it cannot verify from the repository are explicitly downgraded to needs-verification items for the parent to check. For diffs above roughly 1,500 changed lines or 25 files, dispatch one instance per subsystem with an explicit file list and synthesize, instead of one instance for the whole diff.", + "- `scout`: Read-only external docs, dependency-source, and API freshness researcher. (Tools: Shell, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill, SearchWeb, FetchURL). When to use: Use this agent for external libraries, SDK docs, upstream source comparisons, API freshness checks, registry/package verification, and dependency behavior research — including verifying the `needs verification` third-party claims that offline reviewer/debugger agents return under RISKS. It returns version-pinned, source-cited facts — local installed source first, then official docs via live web research — with conflicts and unverifiable gaps reported explicitly instead of papered over.", + "- `security-reviewer`: Diff-focused security review with validated findings. (Tools: Shell, SetTodoList, ReadFile, Glob, Grep). When to use: Use for security review: diff-only review on the current branch (default) or repo-wide vulnerability discovery via the security-scan pipeline. Can run in parallel with `code-reviewer`; for large diffs, scope each instance to the trust-boundary files of one subsystem. Returns reachability-validated findings — source → sink anchored, precondition-stated, CWE-classified, version-checked against the project's pins — with scanner hits treated as leads until verified. It runs offline by design, so advisory-dependent claims come back under RISKS as needs-verification items for the parent to check.", + '- `verifier`: Read-only validation runner for tests, lint, and builds. (Tools: Shell, SetTodoList, ReadFile, ReadMediaFile, Glob, Grep, SmartSearch, ReadSkill). When to use: Use this agent when the parent needs tests, lint, type checks, builds, or other validation gates run and reported without applying fixes — e.g. "run the tests", "does it build", post-edit gate checks, or re-running a suspected flaky suite. Not for fixing failures, writing tests, updating snapshots, or formatting: it is read-only by design and reports proposed fixes under RISKS instead of applying them.', + ], + "complete": True, + }, + }, {"method": "event", "type": "StepBegin", "payload": {"n": 2}}, { "method": "event",