monkeybot-cli 0.5.1__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/PKG-INFO +3 -3
  2. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/pyproject.toml +3 -3
  3. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/skills/monkeybot/SKILL.md +3 -3
  4. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/skills/monkeybot/references/config-sections.md +8 -20
  5. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/chat_session.py +3 -3
  6. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/chat_tui.py +5 -5
  7. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/commands/chat.py +2 -3
  8. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/commands/doctor.py +153 -0
  9. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/commands/validate.py +2 -13
  10. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/realtime/talk_ui.py +0 -1
  11. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/scaffold.py +12 -3
  12. monkeybot_cli-0.7.0/src/monkeybot_cli/scaffold_defaults/browser/SKILL.md +41 -0
  13. monkeybot_cli-0.7.0/src/monkeybot_cli/scaffold_defaults/goal/SKILL.md +31 -0
  14. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/scaffold_defaults/monkeybot.example.yaml +25 -8
  15. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/tests/test_chat_e2e.py +5 -1
  16. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/tests/test_chat_session.py +2 -2
  17. monkeybot_cli-0.7.0/tests/test_doctor.py +217 -0
  18. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/tests/test_gateway_health.py +2 -2
  19. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/tests/test_scaffold.py +8 -1
  20. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/uv.lock +11 -3
  21. monkeybot_cli-0.5.1/src/monkeybot_cli/scaffold_defaults/browser/SKILL.md +0 -16
  22. monkeybot_cli-0.5.1/tests/test_doctor.py +0 -85
  23. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/.gitignore +0 -0
  24. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/__init__.py +0 -0
  25. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/chat_file_index.py +0 -0
  26. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/chat_local_shell.py +0 -0
  27. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/chat_renderer.py +0 -0
  28. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/chat_status_bar.py +0 -0
  29. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/chat_theme.py +0 -0
  30. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/chat_tool_display.py +0 -0
  31. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/chat_tui_widgets.py +0 -0
  32. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/commands/__init__.py +0 -0
  33. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/commands/loop.py +0 -0
  34. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/commands/new.py +0 -0
  35. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/commands/refresh.py +0 -0
  36. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/commands/run_cmd.py +0 -0
  37. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/commands/talk.py +0 -0
  38. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/compat.py +0 -0
  39. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/config_resolve.py +0 -0
  40. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/exit_commands.py +0 -0
  41. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/extras_catalog.py +0 -0
  42. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/gateway_health.py +0 -0
  43. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/main.py +0 -0
  44. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/opensandbox_lifecycle.py +0 -0
  45. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/output.py +0 -0
  46. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/process_tree.py +0 -0
  47. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/providers.py +0 -0
  48. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/realtime/__init__.py +0 -0
  49. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/realtime/audio_io.py +0 -0
  50. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/realtime/client.py +0 -0
  51. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/realtime/gateway_manager.py +0 -0
  52. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/realtime/push_to_talk.py +0 -0
  53. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/realtime/session.py +0 -0
  54. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/realtime/session_controller.py +0 -0
  55. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/realtime/wire_encode.py +0 -0
  56. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/runtime_python.py +0 -0
  57. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/scaffold_defaults/AGENT.md +0 -0
  58. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/scaffold_defaults/Dockerfile +0 -0
  59. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/scaffold_defaults/__init__.py +0 -0
  60. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/scaffold_defaults/command_allowlist.yaml +0 -0
  61. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/scaffold_defaults/dockerignore +0 -0
  62. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/scaffold_defaults/env.example +0 -0
  63. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/scaffold_defaults/image-generator/SKILL.md +0 -0
  64. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/scaffold_defaults/image-generator/generate_image.py +0 -0
  65. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/scaffold_defaults/loop/SKILL.md +0 -0
  66. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/scaffold_defaults/mcp.json +0 -0
  67. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/scaffold_defaults/opensandbox.docker.toml +0 -0
  68. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/scaffold_defaults/otel-collector.example.yaml +0 -0
  69. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/scaffold_defaults/permissions.yaml +0 -0
  70. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/session_controller.py +0 -0
  71. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/src/monkeybot_cli/terminal_markdown.py +0 -0
  72. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/tests/test_chat_continue.py +0 -0
  73. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/tests/test_chat_errors.py +0 -0
  74. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/tests/test_chat_file_index.py +0 -0
  75. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/tests/test_chat_grounding.py +0 -0
  76. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/tests/test_chat_local_shell.py +0 -0
  77. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/tests/test_chat_renderer_parity.py +0 -0
  78. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/tests/test_chat_status_bar.py +0 -0
  79. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/tests/test_chat_theme.py +0 -0
  80. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/tests/test_chat_tool_display.py +0 -0
  81. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/tests/test_chat_tui.py +0 -0
  82. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/tests/test_cli.py +0 -0
  83. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/tests/test_config_resolve.py +0 -0
  84. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/tests/test_extras_catalog.py +0 -0
  85. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/tests/test_opensandbox_lifecycle.py +0 -0
  86. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/tests/test_realtime_session_controller.py +0 -0
  87. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/tests/test_run_cmd.py +0 -0
  88. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/tests/test_runtime_python.py +0 -0
  89. {monkeybot_cli-0.5.1 → monkeybot_cli-0.7.0}/tests/test_terminal_markdown.py +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: monkeybot-cli
3
- Version: 0.5.1
3
+ Version: 0.7.0
4
4
  Summary: CLI to create, configure, validate, and chat with monkeybot agents.
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: httpx>=0.27.0
7
- Requires-Dist: monkeybot-browser-mcp<1,>=0.2.0
8
- Requires-Dist: monkeybot[cli]<4,>=3.0.0
7
+ Requires-Dist: monkeybot-browser-mcp<1,>=0.5.0
8
+ Requires-Dist: monkeybot[cli]<4,>=3.1.0
9
9
  Requires-Dist: python-dotenv>=1.0.0
10
10
  Requires-Dist: pyyaml>=6.0.2
11
11
  Requires-Dist: textual>=8.2.8
@@ -4,14 +4,14 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "monkeybot-cli"
7
- version = "0.5.1"
7
+ version = "0.7.0"
8
8
  description = "CLI to create, configure, validate, and chat with monkeybot agents."
9
9
  requires-python = ">=3.11"
10
10
  dependencies = [
11
11
  # `cli` pulls realtime + typer so `monkeybot talk` works out of the box (text mode).
12
12
  # Audio mic/PTT needs the agent (or this env) to also install monkeybot[cli-realtime].
13
- "monkeybot[cli]>=3.0.0,<4",
14
- "monkeybot-browser-mcp>=0.2.0,<1",
13
+ "monkeybot[cli]>=3.1.0,<4",
14
+ "monkeybot-browser-mcp>=0.5.0,<1",
15
15
  "httpx>=0.27.0",
16
16
  "pyyaml>=6.0.2",
17
17
  "python-dotenv>=1.0.0",
@@ -56,7 +56,7 @@ npx skills add human-plus-machine/monkeybot --skill monkeybot
56
56
  - `monkeybot_config/monkeybot.yaml` — non-secret settings (model, paths, gateway, behavior).
57
57
  - `.env` — secrets and machine-local paths (API keys, GCP project, DB URL).
58
58
  - sidecars: `monkeybot_config/AGENT.md` (system prompt), `mcp.json` (MCP servers), `command_allowlist.yaml`.
59
- - **Precedence — important:** environment variables and `.env` win over `monkeybot.yaml`. If a user edits YAML but nothing changes, suspect a stale `.env` shadowing it (the YAML→env mapping lives in `runtime_env.py:ENV_MAP`).
59
+ - **Precedence — important:** environment variables and `.env` win over `monkeybot.yaml` except `model.*` (provider, name, sampling, context window, summarization model, max turns, cache retention), which is YAML-only. If a user edits YAML but nothing changes, suspect a stale `.env` shadowing it (the YAML→env mapping lives in `runtime_env.py:ENV_MAP`).
60
60
  - **Defaults are fine on day one.** Most first-time users only touch `model`, `.env` credentials, and `AGENT.md`.
61
61
 
62
62
  ## Running CLI commands
@@ -112,7 +112,7 @@ uv sync
112
112
  | `aws_bedrock` | `AWS_ACCESS_KEY_ID` / `AWS_PROFILE` + `AWS_REGION` | `monkeybot[bedrock]` |
113
113
  | `huggingface` | `HF_TOKEN` (or `HUGGINGFACE_API_KEY`) | `monkeybot[huggingface]` |
114
114
  | `ollama-cloud` | `OLLAMA_API_KEY` | `monkeybot[ollama]` |
115
- | `ollama-local` | None required — `OLLAMA_BASE_URL` (default `http://localhost:11434`) | `monkeybot[ollama]` |
115
+ | `ollama-local` | None required — `OLLAMA_BASE_URL` (default `http://localhost:11434`). Requests send `keep_alive` 24h so prefix KV cache survives idle. | `monkeybot[ollama]` |
116
116
  | `ollama` | Legacy auto-route (key + blank URL = cloud; explicit URL wins) | `monkeybot[ollama]` |
117
117
 
118
118
  **Agent-first dependencies.** The CLI is thin — it does **not** install provider/storage extras globally. `monkeybot new` scaffolds a `pyproject.toml` with the selected provider (and any `--with` extras). Run plain `uv sync` in the agent directory. `monkeybot run` / `chat` spawn the gateway from that project's interpreter (`.venv/bin/python`, else `uv run python`), and `doctor` checks extras in that same interpreter. For a config-only tree (just `monkeybot_config/`, no `pyproject.toml`) the gateway uses the CLI's interpreter when it already has MonkeyBot 3.x (and MemPalace if memory is on). If memory is enabled and that interpreter cannot import MemPalace, `run` / `chat` provision a cached CLI-managed venv under `~/.cache/monkeybot/runtimes/` holding `monkeybot[memory]` pinned to the running core (never rewrites a `pyproject.toml`). `doctor` reuses that cache when it is already present. Otherwise extras must be installed in the CLI env (`uv tool install --with 'monkeybot[<extra>]' monkeybot-cli`).
@@ -160,7 +160,7 @@ Decision → config map:
160
160
  | "Run untrusted code" | `sandbox.enabled` + `SANDBOX_API_KEY` |
161
161
  | "Use specialist agents" | `subagents.personas` + `monkeybot_config/agents/*.md` |
162
162
  | "Connect external tools" | `mcp.json` (`mcpServers` object), then `validate --check-mcp` |
163
- | "Control cost / context size" | `model.*`, `context_curation.*` |
163
+ | "Control cost / context size" | `model.*` |
164
164
  | "Restrict dangerous commands" | `tools.denied_patterns`, `command_allowlist.yaml` |
165
165
  | "Multiple environments" | top-level `includes:` fragments |
166
166
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Deep reference for every `monkeybot.yaml` section. Load this only when a user needs to customize beyond Tier 1. The canonical, fully-commented template lives at `cli/src/monkeybot_cli/scaffold_defaults/monkeybot.example.yaml` in the monkeybot repo (copied to `monkeybot_config/monkeybot.example.yaml` when you scaffold); this file adds the **"when would I change this?"** context the comments don't.
4
4
 
5
- **Precedence:** env vars and `.env` win over YAML. The YAML→env mapping is `ENV_MAP` in `src/monkeybot/core/config/runtime_env.py`. If a YAML edit has no effect, check for a shadowing env var.
5
+ **Precedence:** env vars and `.env` win over YAML for `ENV_MAP` keys. `model.*` (provider, name, temperature, max_tokens, thinking_budget, context_window, summarization_model, max_turns, cache_retention) is YAML-only — leftover `MODEL_*` env vars are ignored. The YAML→env mapping is `ENV_MAP` in `src/monkeybot/core/config/runtime_env.py`. If a YAML edit has no effect, check for a shadowing env var (except `model.*`).
6
6
 
7
7
  ---
8
8
 
@@ -34,6 +34,8 @@ Validate check ids: `paths.agent_md.exists`, `paths.skills_path.exists`, `paths.
34
34
 
35
35
  ## `model`
36
36
 
37
+ YAML-only for provider, name, sampling, context window, summarization model, max turns, and cache retention. Leftover `MODEL_*` / `MAX_TURNS` / `CONTEXT_SUMMARIZATION_MODEL` env vars are ignored.
38
+
37
39
  | Field | Default | When to change |
38
40
  |---|---|---|
39
41
  | `provider` | `gemini` | Switch LLM vendor (see provider table in SKILL.md) |
@@ -41,9 +43,11 @@ Validate check ids: `paths.agent_md.exists`, `paths.skills_path.exists`, `paths.
41
43
  | `temperature` | `0.7` | Lower for deterministic output, higher for creative |
42
44
  | `max_tokens` | `60000` | Cap per-response length |
43
45
  | `thinking_budget` | `-1` | Gemini: `-1` model default, `0` off, `N` token budget. Ollama reasoning models: `-1` server default, `0` off (`reasoning_effort: none`) |
44
- | `context_window` | `1000000` | Summarization trigger (tokens); also drives soft-spill / `read_file` char budgets |
46
+ | `context_window` | `1000000` | Summarization trigger (tokens); also drives soft-spill / `read_file` char budgets. **Not** Ollama `num_ctx` |
47
+ | `keep_alive` | `24h` (ollama-local only) | How long local Ollama keeps the model (and KV prefix cache) loaded. YAML only. Set `"0"` to omit. See `docs/ollama-local.md` |
48
+ | `num_ctx` | (unset) | Optional pinned Ollama `num_ctx`. YAML only. Omit = server default. Do not copy `context_window` |
45
49
  | `max_turns` | `1000` | Hard cap on turns per run |
46
- | `summarization_model` | (main model) | Cheaper model for history summarization (env `CONTEXT_SUMMARIZATION_MODEL`) |
50
+ | `summarization_model` | (main model) | Cheaper model for history summarization (YAML-only)
47
51
 
48
52
  Validate check ids: `model.provider.supported`, `model.name.present`. Supported YAML providers: `gemini`/`vertex`, `openai`, `anthropic`, `vertex-claude`, `huggingface`, `ollama-cloud`, `ollama-local`, `ollama`, `aws_bedrock`, `fake`.
49
53
 
@@ -71,22 +75,6 @@ Required when `memory_storage_uri` is `gcs://…` or `provider: vertex-claude` (
71
75
  | `graceful_shutdown_timeout_sec` | `5` | Allow longer drain on shutdown |
72
76
  | `cors_allow_origins` | `http://localhost:5173` | **Custom web UI** — set its origin, or `"*"` for any |
73
77
 
74
- ## `context_curation`
75
-
76
- Trims memory injected into context. `enabled: true` by default.
77
-
78
- Recent window by default; LLM curator only when the index is token-heavy. On curator failure, falls back to the window.
79
-
80
- | Field | Default | Notes |
81
- |---|---|---|
82
- | `memory_window_lines` | `12` | Recent index lines injected; also caps curator-selected lines |
83
- | `memory_index_cap` | `200` | Organizer keeps this many INDEX.md entries; older rows move to `INDEX.archive.md` |
84
- | `memory_token_threshold` | `2000` | Call curator when estimated index tokens exceed this |
85
- | `curator_model` | `gemini-3-flash` | Separate small model; empty = main model |
86
- | `timeout_sec` | `10` | Curator call timeout |
87
-
88
- When the prompt shows fewer entries than exist, a structural confidence score triggers a `search_memory` nudge. Skill names are always shown in full in the prompt; use `list_skills` to get the skills root path.
89
-
90
78
  ## `memory`
91
79
 
92
80
  MemPalace capture, wake-up, and search teaching are on by default. Turn them off with `memory.enabled: false` or `MONKEYBOT_MEMORY_HOOK_ENABLED=0`. The vector stack is the optional `monkeybot[memory]` extra — omit it (and set `enabled: false`) when you do not want chromadb/onnxruntime.
@@ -176,4 +164,4 @@ Validate check: `config.includes.resolve`.
176
164
 
177
165
  ## `fake_provider`
178
166
 
179
- Test-only. `events_json` feeds `MODEL_PROVIDER=fake` scripted runs (env `MONKEYBOT_FAKE_PROVIDER_EVENTS`). Not for production.
167
+ Test-only. `events_json` feeds `model.provider: fake` scripted runs (env `MONKEYBOT_FAKE_PROVIDER_EVENTS`). Not for production.
@@ -229,7 +229,7 @@ class ChatSessionController:
229
229
  self.usage = UsageStore()
230
230
  self.session_id: str | None = None
231
231
  self.stream_error = False
232
- self.transcript_report_dir: str | None = None
232
+ self.transcript_dir: str | None = None
233
233
  self._client: httpx.AsyncClient | None = None
234
234
  self._event_queue: asyncio.Queue[str | None] = asyncio.Queue()
235
235
  self._stream_task: asyncio.Task[None] | None = None
@@ -924,9 +924,9 @@ class ChatSessionController:
924
924
  resp = await self._client.delete(f"{self.base}/sessions/{self.session_id}")
925
925
  if resp.status_code == 200:
926
926
  data = resp.json()
927
- report = data.get("transcript_report_dir") if isinstance(data, dict) else None
927
+ report = data.get("transcript_dir") if isinstance(data, dict) else None
928
928
  if isinstance(report, str) and report.strip():
929
- self.transcript_report_dir = report.strip()
929
+ self.transcript_dir = report.strip()
930
930
  except Exception:
931
931
  logger.warning(
932
932
  "session DELETE on close failed session_id=%s",
@@ -1680,8 +1680,8 @@ class ChatApp(App[int]):
1680
1680
  src = session_dir / "transcript.ndjson"
1681
1681
  if not src.is_file():
1682
1682
  self._mount_system(
1683
- "No trace file found — enable transcript capture on the gateway and restart "
1684
- "(external gateways need MONKEYBOT_TRANSCRIPT_ENABLED=1)",
1683
+ "No trace file found — set runtime.transcript_enabled: true in monkeybot.yaml "
1684
+ "and restart the gateway",
1685
1685
  error=True,
1686
1686
  )
1687
1687
  return
@@ -1829,10 +1829,10 @@ class ChatApp(App[int]):
1829
1829
  async def _close_session_and_exit(self) -> None:
1830
1830
  # Must not be named `_shutdown` — that shadows Textual.App._shutdown.
1831
1831
  await self._controller.close()
1832
- report_dir = getattr(self._controller, "transcript_report_dir", None)
1832
+ report_dir = getattr(self._controller, "transcript_dir", None)
1833
1833
  if isinstance(report_dir, str) and report_dir:
1834
- self._mount_system(f"Transcript report → {report_dir}")
1835
- print(f"Transcript report → {report_dir}", flush=True)
1834
+ self._mount_system(f"Transcript → {report_dir}")
1835
+ print(f"Transcript → {report_dir}", flush=True)
1836
1836
  self._exit_code = 1 if self._controller.stream_error else self._exit_code
1837
1837
  self.exit(self._exit_code)
1838
1838
 
@@ -626,9 +626,9 @@ async def _plain_chat_session(
626
626
  await controller.submit(user_line)
627
627
  finally:
628
628
  await controller.close()
629
- if controller.transcript_report_dir:
629
+ if controller.transcript_dir:
630
630
  print(
631
- f"{_DIM}Transcript report → {controller.transcript_report_dir}{_RESET}",
631
+ f"{_DIM}Transcript → {controller.transcript_dir}{_RESET}",
632
632
  flush=True,
633
633
  )
634
634
  await renderer.stop_io_worker()
@@ -678,7 +678,6 @@ def _spawn_gateway(config_path: Path | None, agent_root: Path, port: int) -> _Sp
678
678
  env["MONKEYBOT_CONFIG"] = str(config_path)
679
679
  env["PORT"] = str(port)
680
680
  env.setdefault("LOG_LEVEL", "error")
681
- env.setdefault("MONKEYBOT_TRANSCRIPT_ENABLED", "1")
682
681
  log_file = tempfile.NamedTemporaryFile(
683
682
  mode="w+",
684
683
  prefix="monkeybot-gateway-",
@@ -192,6 +192,146 @@ def _add_layout_checks(report: CommandReport, layout: AgentLayout) -> None:
192
192
  )
193
193
 
194
194
 
195
+ _OLLAMA_LOCAL_DOCS = "docs/ollama-local.md"
196
+ _OLLAMA_NUM_CTX_LARGE = 32_768
197
+ _OLLAMA_REASONING_TAGS = ("qwen3", "gemma4", "gemma-4", "deepseek-r1", "qwq", "magistral")
198
+
199
+
200
+ def _is_local_ollama_provider(provider: str) -> bool:
201
+ """True for ollama-local and legacy ``ollama`` (not ollama-cloud)."""
202
+ key = provider.strip().lower().replace("_", "-")
203
+ return key in {"ollama-local", "ollama"}
204
+
205
+
206
+ def _looks_like_reasoning_model(model_name: str) -> bool:
207
+ name = model_name.lower()
208
+ return any(tag in name for tag in _OLLAMA_REASONING_TAGS)
209
+
210
+
211
+ def _parse_int(raw: object) -> int | None:
212
+ if raw is None or raw == "":
213
+ return None
214
+ try:
215
+ return int(raw)
216
+ except (TypeError, ValueError):
217
+ return None
218
+
219
+
220
+ def _strict_positive_int(raw: object) -> tuple[int | None, bool]:
221
+ """Return ``(value, invalid)``. Unset is ``(None, False)``; garbage or ``<1`` is invalid."""
222
+ if raw is None or raw == "":
223
+ return None, False
224
+ if isinstance(raw, bool) or not isinstance(raw, int):
225
+ return None, True
226
+ if raw < 1:
227
+ return None, True
228
+ return raw, False
229
+
230
+
231
+ def _add_ollama_local_checks(
232
+ report: CommandReport,
233
+ *,
234
+ provider: str,
235
+ model_name: str,
236
+ thinking_budget: object,
237
+ num_ctx: object,
238
+ ) -> None:
239
+ """Warn on local-Ollama prefix-cache traps. Always emit the four check ids."""
240
+ local = _is_local_ollama_provider(provider)
241
+ mlx = "-mlx" in model_name.lower()
242
+ check(
243
+ report,
244
+ id="ollama.local.mlx_runner",
245
+ category="provider",
246
+ severity="warning",
247
+ passed=not mlx,
248
+ skip=not local,
249
+ message=(
250
+ f"model.name {model_name!r} looks like an MLX pack; prefix KV cache "
251
+ "often does not reuse between agent steps"
252
+ if mlx
253
+ else "model.name is not an MLX pack"
254
+ ),
255
+ field="model.name",
256
+ value=model_name or None,
257
+ remediation=(
258
+ f"Prefer a GGUF tag for tool-calling loops. See {_OLLAMA_LOCAL_DOCS}." if mlx else None
259
+ ),
260
+ docs=_OLLAMA_LOCAL_DOCS,
261
+ )
262
+ budget = _parse_int(thinking_budget)
263
+ if budget is None:
264
+ budget = -1
265
+ reasoning = _looks_like_reasoning_model(model_name)
266
+ thinking_on_default = budget == -1
267
+ check(
268
+ report,
269
+ id="ollama.local.thinking_default",
270
+ category="provider",
271
+ severity="warning",
272
+ passed=not thinking_on_default,
273
+ skip=not local or not reasoning,
274
+ message=(
275
+ "thinking_budget is -1 (server default); reasoning models generate "
276
+ "thinking tokens before the first visible reply"
277
+ if thinking_on_default
278
+ else f"thinking_budget is {budget}"
279
+ ),
280
+ field="model.thinking_budget",
281
+ value=budget,
282
+ remediation=(
283
+ f"Set thinking_budget: 0 to send reasoning_effort: none. See {_OLLAMA_LOCAL_DOCS}."
284
+ if thinking_on_default
285
+ else None
286
+ ),
287
+ docs=_OLLAMA_LOCAL_DOCS,
288
+ )
289
+ ctx, ctx_invalid = _strict_positive_int(num_ctx)
290
+ check(
291
+ report,
292
+ id="ollama.local.num_ctx_invalid",
293
+ category="provider",
294
+ severity="error",
295
+ passed=not ctx_invalid,
296
+ skip=not local or (ctx is None and not ctx_invalid),
297
+ message=(
298
+ f"num_ctx must be a positive integer, got {num_ctx!r}"
299
+ if ctx_invalid
300
+ else (f"num_ctx is {ctx}" if ctx is not None else "num_ctx is unset")
301
+ ),
302
+ field="model.num_ctx",
303
+ value=num_ctx,
304
+ remediation=(
305
+ f"Set a positive integer num_ctx (e.g. 8192), or omit it. See {_OLLAMA_LOCAL_DOCS}."
306
+ if ctx_invalid
307
+ else None
308
+ ),
309
+ docs=_OLLAMA_LOCAL_DOCS,
310
+ )
311
+ large = ctx is not None and ctx > _OLLAMA_NUM_CTX_LARGE
312
+ check(
313
+ report,
314
+ id="ollama.local.num_ctx_large",
315
+ category="provider",
316
+ severity="warning",
317
+ passed=not large,
318
+ skip=not local or ctx is None or ctx_invalid,
319
+ message=(
320
+ f"num_ctx {ctx} is large for a local runner; prefill will be slow"
321
+ if large
322
+ else (f"num_ctx is {ctx}" if ctx is not None else "num_ctx is unset")
323
+ ),
324
+ field="model.num_ctx",
325
+ value=ctx,
326
+ remediation=(
327
+ f"Pin a modest num_ctx (e.g. 8192). Do not copy context_window. See {_OLLAMA_LOCAL_DOCS}."
328
+ if large
329
+ else None
330
+ ),
331
+ docs=_OLLAMA_LOCAL_DOCS,
332
+ )
333
+
334
+
195
335
  def run_doctor(args: argparse.Namespace) -> int:
196
336
  cwd = Path(args.cwd).expanduser().resolve() if args.cwd else None
197
337
  config_path = resolve_config(args.config, cwd=cwd)
@@ -324,6 +464,19 @@ def run_doctor(args: argparse.Namespace) -> int:
324
464
  skip=True,
325
465
  )
326
466
 
467
+ thinking_raw = model.get("thinking_budget") if isinstance(model, dict) else None
468
+ if thinking_raw is None:
469
+ thinking_raw = "-1"
470
+ num_ctx_raw = model.get("num_ctx") if isinstance(model, dict) else None
471
+ model_name = str(model.get("name", "")).strip() if isinstance(model, dict) else ""
472
+ _add_ollama_local_checks(
473
+ report,
474
+ provider=provider,
475
+ model_name=model_name,
476
+ thinking_budget=thinking_raw,
477
+ num_ctx=num_ctx_raw,
478
+ )
479
+
327
480
  runtime_cfg = doc.get("runtime") if isinstance(doc.get("runtime"), dict) else {}
328
481
  port = int(runtime_cfg.get("port", 8080)) if isinstance(runtime_cfg, dict) else 8080
329
482
  free = _port_free(port)
@@ -17,7 +17,7 @@ from monkeybot.core.config import (
17
17
  validate_monkeybot_yaml_doc,
18
18
  validate_provider_env,
19
19
  )
20
- from monkeybot.core.config.runtime_env import ENV_MAP
20
+ from monkeybot.core.config.runtime_env import _flatten_config
21
21
  from monkeybot.core.config.settings import ConfigError, normalize_model_provider
22
22
  from monkeybot.core.config.yaml_loader import load_monkeybot_yaml_dict
23
23
  from monkeybot.core.layout import resolve_agent_root
@@ -35,18 +35,7 @@ def _resolve_path(base: Path, rel: str) -> Path:
35
35
 
36
36
 
37
37
  def _flatten_to_env(doc: dict[str, Any]) -> dict[str, str]:
38
- out: dict[str, str] = {}
39
- for (section, key), env_name in ENV_MAP.items():
40
- sec = doc.get(section)
41
- if not isinstance(sec, dict) or key not in sec:
42
- continue
43
- raw = sec[key]
44
- if raw is None:
45
- continue
46
- if isinstance(raw, bool):
47
- out[env_name] = "true" if raw else "false"
48
- else:
49
- out[env_name] = str(raw)
38
+ out = _flatten_config(doc)
50
39
  gcp = doc.get("gcp") if isinstance(doc.get("gcp"), dict) else {}
51
40
  if isinstance(gcp, dict) and gcp.get("project_id"):
52
41
  out["GCP_PROJECT_ID"] = str(gcp["project_id"])
@@ -78,7 +78,6 @@ def _spawn_combined_gateway(
78
78
  env["MONKEYBOT_CONFIG"] = str(config_path)
79
79
  env["PORT"] = str(port)
80
80
  env.setdefault("LOG_LEVEL", "error")
81
- env.setdefault("MONKEYBOT_TRANSCRIPT_ENABLED", "1")
82
81
  log_file = tempfile.NamedTemporaryFile(
83
82
  mode="w+",
84
83
  prefix="monkeybot-gateway-",
@@ -176,6 +176,13 @@ def install_loop_skill(dest: Path, *, force: bool) -> str:
176
176
  return _install_file(target, resources.files(_DEFAULTS_PKG) / "loop" / "SKILL.md", force=force)
177
177
 
178
178
 
179
+ def install_goal_skill(dest: Path, *, force: bool) -> str:
180
+ """Install the bundled native-goal procedure into ``skills/goal``."""
181
+ target = dest / "skills" / "goal" / "SKILL.md"
182
+ target.parent.mkdir(parents=True, exist_ok=True)
183
+ return _install_file(target, resources.files(_DEFAULTS_PKG) / "goal" / "SKILL.md", force=force)
184
+
185
+
179
186
  def install_env_example(dest: Path, *, force: bool) -> str:
180
187
  env_example = dest / ".env.example"
181
188
  if env_example.exists() and not force:
@@ -399,6 +406,7 @@ def run_refresh(*, dest: Path) -> list[str]:
399
406
  report.append(refresh_permissions_if_default(cfg_dir))
400
407
  report.append(refresh_monkeybot_yaml(cfg_dir))
401
408
  report.extend(ensure_memory(dest, force=False))
409
+ report.append(f" skills/goal/SKILL.md: {install_goal_skill(dest, force=False)}")
402
410
  return report
403
411
 
404
412
 
@@ -412,11 +420,12 @@ def run_new(
412
420
  ) -> list[str]:
413
421
  """Full scaffold: config bundle, empty skills root, writable state, and image files.
414
422
 
415
- Capability skills (``browser``, ``image-generator``, ``loop``) are packaged
423
+ Optional capability skills (``browser``, ``image-generator``, ``loop``) are packaged
416
424
  under ``scaffold_defaults`` but not installed into new agents for now —
417
425
  the Mac Main Agent loads them from ``~/.monkeybot/.internal/skills``
418
426
  instead. Re-enable via ``install_*_skill`` when custom agents should get
419
- them again.
427
+ them again. The ``goal`` skill is installed because native goal tools are
428
+ intentionally exposed only for explicit ``/goal`` invocations.
420
429
  """
421
430
  cfg_dir = dest / "monkeybot_config"
422
431
  report = install_config_bundle(cfg_dir, force=force)
@@ -426,7 +435,7 @@ def run_new(
426
435
  )
427
436
  report.extend(ensure_memory(dest, force=force))
428
437
  report.extend(ensure_workspace(dest, force=force))
429
- report.append(" skills/: ensured (empty — capability skills deferred)")
438
+ report.append(f" skills/goal/SKILL.md: {install_goal_skill(dest, force=force)}")
430
439
  report.append(f" .env.example: {install_env_example(dest, force=force)}")
431
440
  report.extend(install_container_files(dest, force=force))
432
441
  report.append(
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: browser
3
+ description: Control a real browser through the optional bundled browser MCP server.
4
+ ---
5
+
6
+ # Browser
7
+
8
+ When browser tools are needed, call `enable_mcp("browser")` first. Use the
9
+ name from the harness MCP catalog — do not read config files. Progressive
10
+ disclosure still requires `enable_mcp` before `browser__*` schemas are advertised.
11
+
12
+ Use indexed DOM tools before screenshots. Prefer `browser_act` with
13
+ `do: fill_form` for multi-field forms, `browser_click_text` when the visible
14
+ label is known, `browser_act` for a batch of steps, and `browser_extract` for
15
+ structured scraping. Browser screenshots (JPEG under
16
+ `browser/Screenshots/`) and agent-written playbooks are workspace data:
17
+ `browser/Screenshots/` and `browser/playbooks/`.
18
+ They are not trusted skills and may be discarded with an ephemeral workspace.
19
+
20
+ If `browser_list_playbooks` returns `flows`, call `browser_run_playbook` instead of
21
+ re-planning. On `failed_step`, continue by hand and append a corrected
22
+ `playbook` YAML fence. Secrets only via `{do: login, expected_origin}`.
23
+
24
+ `browser_input_by_index` fills in-page by default; pass `mode="keys"` for
25
+ comboboxes and fields that only listen to keydown. `browser_get_elements`
26
+ is viewport-first; indices remain valid until navigation. Actions return
27
+ an `observation` (diff by default) — read that instead of calling
28
+ `get_elements` again. `browser_goto` returns a full observation.
29
+ Use `browser_get_text` to read page copy.
30
+
31
+ When the user asked to sign in on the Spaces in-app browser, call
32
+ `browser_login` instead of typing a password. It returns `{ok, loggedIn, origin}`
33
+ only. Pass `expected_origin` so a login cannot land on the wrong site, and check
34
+ the returned `origin` before reporting success.
35
+
36
+ Tabs use aliases (`t1`, `t2`, …). Reads (`get_elements`, `page_info`, `js`,
37
+ `read_tabs`) never move focus; actions do. At most five agent-controlled tabs —
38
+ if you hit the cap, ask the user which to close, then `browser_close_tab`. Close
39
+ tabs you opened. `browser_login` still targets the tab the user has focused.
40
+
41
+ Finish browser sessions with `browser_stop`.
@@ -0,0 +1,31 @@
1
+ ---
2
+ name: goal
3
+ description: Create one durable objective that continues across scheduled turns until completed.
4
+ ---
5
+
6
+ # Goal
7
+
8
+ Use this skill only when the user explicitly invokes `/goal` with an objective
9
+ that may require multiple turns.
10
+
11
+ ## Start
12
+
13
+ 1. Preserve the user's complete objective without narrowing its scope.
14
+ 2. Call `create_goal` exactly once with that objective.
15
+ 3. Do the first concrete increment of work in the same turn.
16
+ 4. Keep the returned goal id; every `update_goal` call must include it.
17
+
18
+ ## Continue
19
+
20
+ - Goal continuations are scheduled automatically.
21
+ - Treat the stored objective as authoritative.
22
+ - Make measurable progress and verify results before declaring completion.
23
+ - Temporary session-busy deferrals are normal and are not goal failures.
24
+ - Persistent execution failures stop the goal after the retry limit so it
25
+ cannot consume resources indefinitely.
26
+
27
+ ## Complete
28
+
29
+ Call `update_goal` with the returned `goal_id` and `status="complete"` only
30
+ after evidence proves every requirement is satisfied. Otherwise leave the goal
31
+ active. Use the `/goals` control plane to pause, resume, or stop it manually.
@@ -2,7 +2,9 @@
2
2
  #
3
3
  # cp monkeybot_config/monkeybot.example.yaml monkeybot_config/monkeybot.yaml
4
4
  #
5
- # Precedence: environment variables (and optional root `.env`) win over values here.
5
+ # Precedence: environment variables (and optional root `.env`) win over values here
6
+ # except ``model.*`` (provider, name, temperature, max_tokens, thinking_budget,
7
+ # context_window, summarization_model, max_turns, cache_retention) which are YAML-only.
6
8
  #
7
9
  # Secrets are not committed here. Create a `.env` in the repo root when you need keys or
8
10
  # machine-local paths, for example:
@@ -29,7 +31,7 @@ runtime:
29
31
  # Internal debugging only (not agent-visible): write every AgentEvent and raw
30
32
  # provider request/response for each session to
31
33
  # {workspace_root}/.monkeybot/transcripts/{UTC}_{session_id}/transcript.ndjson.
32
- # (env: MONKEYBOT_TRANSCRIPT_ENABLED)
34
+ # YAML-only (not overridable via environment).
33
35
  transcript_enabled: true
34
36
 
35
37
  # Required: choose the conversational harness for this deployment.
@@ -45,7 +47,7 @@ realtime:
45
47
  # Optional model override for the realtime session. Live-only models (e.g.
46
48
  # gemini-3.1-flash-live-preview) cannot be used for turn-based work, so the
47
49
  # realtime session can point to a different model than model.name. When omitted,
48
- # the realtime session falls back to model.name and MODEL_PROVIDER.
50
+ # the realtime session falls back to model.name and model.provider.
49
51
  # model:
50
52
  # name: gemini-3.1-flash-live-preview
51
53
  # provider: google_genai
@@ -98,10 +100,16 @@ model:
98
100
  # Summarisation trigger (tokens). Also drives soft-spill / read_file char budgets.
99
101
  context_window: 1000000
100
102
  max_turns: 1000
101
- # Prompt-cache session hints: none | short | long (env: MODEL_CACHE_RETENTION)
103
+ # Prompt-cache session hints: none | short | long (YAML-only)
102
104
  # cache_retention: short
103
- # Optional cheaper model for sync history summarization (env: CONTEXT_SUMMARIZATION_MODEL); omit = main model
105
+ # Optional cheaper model for sync history summarization; omit = main model
104
106
  # summarization_model: gemini-3-flash
107
+ # Ollama-local only (YAML). Default 24h so the KV prefix cache survives idle.
108
+ # Set "0" to omit. Distinct from the Ollama daemon env OLLAMA_KEEP_ALIVE.
109
+ # keep_alive: 24h
110
+ # Ollama-local only (YAML). Pin llama num_ctx; omit = server default.
111
+ # Do NOT copy context_window here — that budget is summarization/spill, often 1M.
112
+ # num_ctx: 8192
105
113
 
106
114
  # Non-secret GCP identifiers (prefer .env for secrets / ADC path).
107
115
  # gcp:
@@ -127,8 +135,6 @@ memory:
127
135
  backend: chroma
128
136
  embedding_model: embeddinggemma-300m
129
137
 
130
- # context_curation is unused after MemPalace wake-up replaced INDEX.md + curator.
131
-
132
138
  emission:
133
139
  # Terse emission-style guidance injected into the cached harness prefix.
134
140
  # Two named modes:
@@ -247,7 +253,18 @@ scheduler:
247
253
  # Env override: MONKEYBOT_SCHEDULER_ENABLED (1 | true | yes | on).
248
254
  enabled: false
249
255
 
250
- # Optional JSON for MODEL_PROVIDER=fake tests (env: MONKEYBOT_FAKE_PROVIDER_EVENTS)
256
+ # Background verifier (YAML-only). Off by default: ledger costs one model call
257
+ # per human message. `enabled: true` turns on omitted ledger/tracker flags;
258
+ # omitted judge.enabled turns on only when the tracker is on. Explicit
259
+ # `enabled: false` still opts a nested section out. Omitted ledger.model /
260
+ # judge.model inherit model.name (same fallback as the agent). The live
261
+ # worker is session-bound ProviderJudge. Realtime loops ignore this block.
262
+ # verifier:
263
+ # enabled: true
264
+ # escalation:
265
+ # max_severity: nudge
266
+
267
+ # Optional JSON for model.provider: fake tests (env: MONKEYBOT_FAKE_PROVIDER_EVENTS)
251
268
  # fake_provider:
252
269
  # events_json: '[[]]'
253
270
 
@@ -25,6 +25,11 @@ def _free_port() -> int:
25
25
  def test_chat_repl_round_trip_with_fake_gateway(tmp_path: Path) -> None:
26
26
  port = _free_port()
27
27
  base = f"http://127.0.0.1:{port}"
28
+ (tmp_path / "monkeybot_config").mkdir()
29
+ (tmp_path / "monkeybot_config" / "monkeybot.yaml").write_text(
30
+ "model:\n provider: fake\n name: fake\n",
31
+ encoding="utf-8",
32
+ )
28
33
  agent = tmp_path / "AGENT.md"
29
34
  agent.write_text("# Test agent\nYou are a test assistant.\n", encoding="utf-8")
30
35
  policy = tmp_path / "command_allowlist.yaml"
@@ -40,7 +45,6 @@ def test_chat_repl_round_trip_with_fake_gateway(tmp_path: Path) -> None:
40
45
  "DB_URL": f"sqlite:///{tmp_path / 'mb.db'}",
41
46
  "MCP_CONFIG": str(tmp_path / "no_mcp.json"),
42
47
  "MEMORY_PATH": str(tmp_path / "memory"),
43
- "MODEL_PROVIDER": "fake",
44
48
  "PORT": str(port),
45
49
  "SKILLS_PATH": str(tmp_path / "skills"),
46
50
  "MONKEYBOT_CHAT_PLAIN": "1",
@@ -430,7 +430,7 @@ def test_close_deletes_session_and_captures_report_dir() -> None:
430
430
  200,
431
431
  json={
432
432
  "deleted": True,
433
- "transcript_report_dir": "/tmp/ws/.monkeybot/transcripts/20260714T150000Z_sess-close",
433
+ "transcript_dir": "/tmp/ws/.monkeybot/transcripts/20260714T150000Z_sess-close",
434
434
  },
435
435
  request=request,
436
436
  )
@@ -441,7 +441,7 @@ def test_close_deletes_session_and_captures_report_dir() -> None:
441
441
  await controller.close()
442
442
  client.delete.assert_awaited_once_with("http://localhost:8080/sessions/sess-close")
443
443
  assert (
444
- controller.transcript_report_dir
444
+ controller.transcript_dir
445
445
  == "/tmp/ws/.monkeybot/transcripts/20260714T150000Z_sess-close"
446
446
  )
447
447
  client.aclose.assert_awaited_once()