dostuff 0.2.2__tar.gz → 0.2.4__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.
- {dostuff-0.2.2 → dostuff-0.2.4}/PKG-INFO +200 -4
- {dostuff-0.2.2 → dostuff-0.2.4}/README.md +199 -3
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/agent/loop.py +66 -9
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/agent/permissions.py +30 -2
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/agent/run_tool.py +100 -10
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/cli.py +6 -3
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/cli_tui.py +71 -9
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/config.py +14 -2
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/headless.py +36 -1
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/agent/constants.py +1 -0
- dostuff-0.2.4/dostuff/helpers/agent/get_model_token_limit.py +54 -0
- dostuff-0.2.4/dostuff/helpers/hooks/__init__.py +10 -0
- dostuff-0.2.4/dostuff/helpers/hooks/dispatch.py +139 -0
- dostuff-0.2.4/dostuff/helpers/hooks/events.py +84 -0
- dostuff-0.2.4/dostuff/helpers/hooks/load_hooks.py +248 -0
- dostuff-0.2.4/dostuff/helpers/hooks/run_hook.py +186 -0
- dostuff-0.2.4/dostuff/helpers/hooks/trust.py +157 -0
- dostuff-0.2.4/dostuff/helpers/models/opencode.py +200 -0
- dostuff-0.2.4/dostuff/helpers/ui/doctor.py +191 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/templates/config.example.yaml +41 -1
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/tools/delegate_to_subagent.py +4 -1
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/tools/settings.py +11 -2
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff.egg-info/PKG-INFO +200 -4
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff.egg-info/SOURCES.txt +7 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/pyproject.toml +1 -1
- {dostuff-0.2.2 → dostuff-0.2.4}/tests/test_cli_commands.py +99 -0
- dostuff-0.2.4/tests/test_hooks.py +1601 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/tests/test_opencode.py +43 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/tests/test_permissions.py +25 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/tests/test_tui.py +314 -0
- dostuff-0.2.2/dostuff/helpers/agent/get_model_token_limit.py +0 -29
- dostuff-0.2.2/dostuff/helpers/models/opencode.py +0 -76
- dostuff-0.2.2/dostuff/helpers/ui/doctor.py +0 -47
- {dostuff-0.2.2 → dostuff-0.2.4}/LICENSE +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/__init__.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/__main__.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/agent/call_agent.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/agent/instructions.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/agent/append_step.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/agent/consume_stream.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/agent/context_usage.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/agent/environment_context.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/agent/file_mentions.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/agent/load_identity.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/agent/load_project_instructions.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/agent/loop_check_cancelled.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/agent/loop_emit.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/agent/manage_context.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/agent/model_cost.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/agent/pastes.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/agent/quick_memory.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/agent/slash_suggestion.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/agent/tool_output.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/mcp/load_mcp_config.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/mcp/mcp_oauth.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/mcp/server_config.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/mcp/token_store.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/memory/export_session.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/memory/resolve_memory_operation.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/memory/shell_step.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/models/add_model_entry.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/models/list_models.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/skills/custom_commands.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/skills/discover_skills.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/skills/skill_command.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/tools/diff.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/tools/generate_tool_schema.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/tools/project_root.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/tools/resolve_safe_path.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/ui/__init__.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/ui/emit.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/ui/prompt_history.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/helpers/ui/terminal_signals.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/lib/checkpoints.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/lib/exceptions.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/lib/mcp/mcp_client.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/lib/mcp/mcp_client_registration_store.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/lib/mcp/mcp_tool_registry_store.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/lib/memory/chroma.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/lib/memory/episodic_memory_store.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/lib/memory/semantic_memory_store.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/lib/memory/session_store.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/lib/memory/stores.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/lib/memory/types.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/lib/model.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/lib/tracing.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/lib/worktree.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/memory/__init__.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/setup_wizard.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/skills.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/templates/env.example +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/tools/__init__.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/tools/bash/bash_command.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/tools/definitions.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/tools/files/delete_file.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/tools/files/edit_file.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/tools/files/list_files.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/tools/files/read_file.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/tools/files/write_file.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/tools/get_current_datetime.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/tools/mcp/call_mcp_tool.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/tools/mcp/get_mcp_tool_details.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/tools/mcp/search_mcp_tools.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/tools/memory/save_episode.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/tools/memory/save_memory.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/tools/search/glob.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/tools/search/grep.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/tools/search/walk.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/tools/todo_write.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/tools/web_fetch.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff/tools/web_search.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff.egg-info/dependency_links.txt +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff.egg-info/entry_points.txt +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff.egg-info/requires.txt +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/dostuff.egg-info/top_level.txt +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/setup.cfg +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/tests/test_checkpoints_todos_caching.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/tests/test_config_memory_mcp.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/tests/test_file_mentions.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/tests/test_file_tools.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/tests/test_loop.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/tests/test_mcp_e2e.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/tests/test_mcp_oauth.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/tests/test_optional_memory_and_instructions.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/tests/test_pastes.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/tests/test_roadmap_021.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/tests/test_session_store.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/tests/test_settings_tools.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/tests/test_setup_wizard.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/tests/test_skill_command.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/tests/test_slash_suggestion.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/tests/test_subagent.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/tests/test_terminal_signals.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/tests/test_tool_schemas.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/tests/test_web_fetch.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/tests/test_web_search.py +0 -0
- {dostuff-0.2.2 → dostuff-0.2.4}/tests/test_worktree.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dostuff
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: DoStuff - a terminal coding agent for any LLM: TUI and headless mode, permissions, MCP, skills, web tools, sessions and git worktrees
|
|
5
5
|
Author-email: Varun Kumawat <varunkumawatleap2@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -84,6 +84,7 @@ DoStuff is a terminal AI agent that runs anywhere `pip` runs. It gives you a ful
|
|
|
84
84
|
- **Project instructions** — `AGENTS.md` / `CLAUDE.md` from `~/.dostuff/`, the repo root and every folder down to the current one, combined
|
|
85
85
|
- **Coding tools** — `read_file` (line numbers, offset/limit), `edit_file` (exact replacement), `write_file`, `grep`, `glob`, `list_files`, `delete_file`, `bash_command`, `web_fetch` (read a URL as text), `web_search` (free, no API key)
|
|
86
86
|
- **Permission layer** — mutating tool calls ask for approval (allow once / always this session / deny with a note); read-only shell commands run without asking; modes `ask` / `allowlist` / `auto`; `/undo` reverts the agent's file changes
|
|
87
|
+
- **Hooks** — shell commands on `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `Stop` and `SubagentStop` that can block a tool call or prompt, feed a formatter's output back to the model, or keep the turn going; Claude Code's hook I/O contract, and project hooks run only once you trust them. See [Hooks](#hooks)
|
|
87
88
|
- **File sandbox** — file tools stay inside the project directory and never read `.env*` files or `~/.dostuff/`
|
|
88
89
|
- **Skills loader** — drop a `SKILL.md` in the right folder, restart, done; run one with `/<skill-name> [args]`
|
|
89
90
|
- **Subagents** — `delegate_to_subagent` runs a bounded task with a restricted tool set; its approval prompts and ESC go through the same TUI
|
|
@@ -203,7 +204,7 @@ In the TUI:
|
|
|
203
204
|
| `dostuff sessions rm <id>... [-y]` | Delete sessions and their `/undo` checkpoints |
|
|
204
205
|
| `dostuff sessions prune [--older-than 30d] [--dry-run] [-y]` | Delete empty sessions, and sessions not used for the given time (`45m`, `12h`, `30d`, `2w`) |
|
|
205
206
|
| `dostuff export [<id>] [-o file\|-]` | Save a session (default: latest here) as Markdown: prompts, answers, tool calls and results |
|
|
206
|
-
| `dostuff -p "<prompt>"` | Headless: run one prompt, print the answer to stdout, exit. Progress goes to stderr. Piped stdin is appended to the prompt (`-p -` reads the prompt from stdin). `--session <id>` continues a session, `--output-format json` (or `--json`) prints `{result, session_id, usage, ...}`, `--output-format stream-json` prints one JSON event per line (`init`, `assistant`, `tool_call`, `tool_result`, `usage`, `error`, then `result`), `-q` hides progress, `--permission-mode ask\|allowlist\|auto\|plan`. `--max-turns N` stops after N model calls (exit code 1). `--allowed-tools` (comma-separated or repeated) runs matching calls without asking in any mode, in the `permissions.allow` syntax (`"npm test"`, `web_fetch:docs.python.org`) or a tool name for every call of it (`write_file`, `bash_command`). Without a terminal, other calls that need approval are denied |
|
|
207
|
+
| `dostuff -p "<prompt>"` | Headless: run one prompt, print the answer to stdout, exit. Progress goes to stderr. Piped stdin is appended to the prompt (`-p -` reads the prompt from stdin). `--session <id>` continues a session, `--output-format json` (or `--json`) prints `{result, session_id, usage, ...}`, `--output-format stream-json` prints one JSON event per line (`init`, `assistant`, `tool_call`, `tool_result`, `usage`, `error`, then `result`), `-q` hides progress, `--permission-mode ask\|allowlist\|auto\|plan`. `--max-turns N` stops after N model calls (exit code 1). `--allowed-tools` (comma-separated or repeated) runs matching calls without asking in any mode, in the `permissions.allow` syntax (`"npm test"`, `web_fetch:docs.python.org`) or a tool name for every call of it (`write_file`, `bash_command`). Without a terminal, other calls that need approval are denied. `--trust-project-hooks` runs the project's [hooks](#project-hooks-and-trust) without a saved trust answer |
|
|
207
208
|
| `dostuff mcp list` | Show MCP servers and the file each comes from |
|
|
208
209
|
| `dostuff mcp add <name> [-e K=V] -- <command> [args...]` | Add a stdio server; `--url <url> [-H "Name: value"]` for HTTP; `--project` writes `./.dostuff/mcp_config.json` |
|
|
209
210
|
| `dostuff mcp remove <name> [--project]` | Remove a server |
|
|
@@ -243,7 +244,8 @@ In the TUI:
|
|
|
243
244
|
| `/status` | Session, directory, model, permission mode, tokens, context use, MCP servers |
|
|
244
245
|
| `/context` | What fills the context window: system prompt, tools, MCP tools, project instructions, compaction summary and messages, with tokens, % and a bar |
|
|
245
246
|
| `/memory [project\|global]` | Edit `AGENTS.md` (this project's or `~/.dostuff/AGENTS.md`) in the same editor as `/config`; `Ctrl+S` saves and the agent follows it from the next message |
|
|
246
|
-
| `/doctor` | The `dostuff doctor` checks (config, API key, MCP config, memory), with a fix for each problem |
|
|
247
|
+
| `/doctor` | The `dostuff doctor` checks (config, API key, MCP config, memory, hooks), with a fix for each problem |
|
|
248
|
+
| `/hooks [r]` | List the configured hooks per event: file, matcher, timeout, `fail_closed`, and whether each runs (`user` / `trusted` / `untrusted` / `disabled` / `broken`). `/hooks r` re-reads the config files. See [Hooks](#hooks) |
|
|
247
249
|
| `/<skill> [args]` | Run a skill: every discovered skill is a command (listed in the `/` menu and `/help`). See [Skills](#skills) |
|
|
248
250
|
| `/<command> [args]` | Run a custom command from `.dostuff/commands/` or `~/.dostuff/commands/`. See [Custom commands](#custom-commands) |
|
|
249
251
|
| `/rename <title>` | Name this session (shown by `dostuff sessions`) |
|
|
@@ -285,6 +287,11 @@ In the TUI:
|
|
|
285
287
|
| `read_settings` | read | never (MCP `env` / header values are shown as `<redacted>`) |
|
|
286
288
|
| `edit_config`, `set_mcp_server`, `remove_mcp_server` | settings | **always, in every mode, with no "always" option** (shows the diff or the server's command) |
|
|
287
289
|
|
|
290
|
+
File tools writing under a `.dostuff` directory, and `bash_command` calls whose command or `cwd` names `.dostuff`, are
|
|
291
|
+
settings changes too, so they always ask. That way auto mode can't quietly add a hook to `~/.dostuff/config.yaml`. Paths
|
|
292
|
+
into subagent worktrees (`.dostuff/worktrees/…`) are exempt. The shell check matches the text `.dostuff`, so a name the
|
|
293
|
+
command builds indirectly is not caught.
|
|
294
|
+
|
|
288
295
|
`permissions.mode` in `config.yaml`:
|
|
289
296
|
|
|
290
297
|
- `ask` (default) — as in the table above.
|
|
@@ -320,6 +327,193 @@ API keys stay in `.env` files, which the agent can't read or change. Subagents d
|
|
|
320
327
|
|
|
321
328
|
---
|
|
322
329
|
|
|
330
|
+
## Hooks
|
|
331
|
+
|
|
332
|
+
Hooks are shell commands DoStuff runs at fixed points of a session: block a tool call, feed a formatter's output back
|
|
333
|
+
to the model, keep the turn going until the tests pass. They are configured under `hooks:` in `config.yaml` and use
|
|
334
|
+
Claude Code's I/O contract (stdin JSON, exit codes, JSON output), so an existing Claude Code hook script works once its
|
|
335
|
+
matcher and tool argument names are renamed (see [the table below](#coming-from-claude-code)).
|
|
336
|
+
|
|
337
|
+
> [!WARNING]
|
|
338
|
+
> Hooks run with your privileges, **unsandboxed**, and are not permission-checked: they are your config, not model
|
|
339
|
+
> output. Read a project's hooks before you trust them.
|
|
340
|
+
|
|
341
|
+
| Event | When | Matcher is matched against | What a hook can do |
|
|
342
|
+
|---|---|---|---|
|
|
343
|
+
| `SessionStart` | Before the first turn of a session, and after a compaction | the source: `startup`, `resume`, `clear`, `compact` | Add context for the rest of the session |
|
|
344
|
+
| `UserPromptSubmit` | After you send a prompt, before it is stored or sent | — | Reject the prompt, or add context for this turn |
|
|
345
|
+
| `PreToolUse` | Before each tool call, before any approval prompt | the tool name | `allow`, `deny` or `ask`; rewrite the arguments |
|
|
346
|
+
| `PostToolUse` | After a tool call ran | the tool name | Append text to the result the model sees |
|
|
347
|
+
| `Stop` | When the agent is about to give its final answer | — | Block, so the turn continues with your reason |
|
|
348
|
+
| `SubagentStop` | Same, for a `delegate_to_subagent` run | — | Same |
|
|
349
|
+
|
|
350
|
+
### Config
|
|
351
|
+
|
|
352
|
+
`matcher` is a Python regex that must match the whole name (`edit_file` does not match `edit_file_v2`); empty or `*`
|
|
353
|
+
matches everything. `if` (tool events only) is searched in a `bash_command`'s `command`. Inline flags such as `(?i)`
|
|
354
|
+
go first in a pattern. `timeout` (default 30 s, 1–300) and `fail_closed` can be set on a group or on one hook. This is
|
|
355
|
+
the example from the spec, and the two scripts it runs are below:
|
|
356
|
+
|
|
357
|
+
```yaml
|
|
358
|
+
hooks:
|
|
359
|
+
project_trust: ask # ask | allow | deny (D4) — only read from ~/.dostuff/config.yaml
|
|
360
|
+
stop_block_limit: 3 # Stop blocks honoured per turn before the loop is allowed to end (1–10)
|
|
361
|
+
SessionStart:
|
|
362
|
+
- matcher: "startup|resume|clear" # regex, full match against the source; empty or "*" = all
|
|
363
|
+
hooks:
|
|
364
|
+
- type: command
|
|
365
|
+
command: "git status --short"
|
|
366
|
+
PreToolUse:
|
|
367
|
+
- matcher: "bash_command" # regex, full match against the tool name
|
|
368
|
+
if: "(?i)^\\s*git\\s+push" # optional regex, searched in tool_input.command (shell tool only)
|
|
369
|
+
fail_closed: true
|
|
370
|
+
timeout: 10
|
|
371
|
+
hooks:
|
|
372
|
+
- type: command
|
|
373
|
+
name: block-main-push
|
|
374
|
+
command: "python .dostuff/hooks/block_main_push.py"
|
|
375
|
+
PostToolUse:
|
|
376
|
+
- matcher: "write_file|edit_file"
|
|
377
|
+
hooks:
|
|
378
|
+
# the script reads tool_input.path from the stdin JSON and runs the formatter on it;
|
|
379
|
+
# its stdout is fed back to the model (D5)
|
|
380
|
+
- type: command
|
|
381
|
+
name: format
|
|
382
|
+
command: "python .dostuff/hooks/format_file.py"
|
|
383
|
+
Stop:
|
|
384
|
+
- hooks:
|
|
385
|
+
- { type: command, name: tests, command: "python .dostuff/hooks/run_tests.py" }
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
- **`fail_closed: true`** (`PreToolUse` and `UserPromptSubmit` only) is for guardrails: if the hook crashes or times
|
|
389
|
+
out, the call is denied (or the prompt rejected) instead of going ahead. A `fail_closed` hook with a config mistake,
|
|
390
|
+
such as a bad regex, is kept and denies everything it matches; any other invalid hook is dropped with a warning.
|
|
391
|
+
- **`stop_block_limit`** (1–10) bounds how many times `Stop` hooks can continue one turn; after that the turn ends with
|
|
392
|
+
a note.
|
|
393
|
+
- User hooks come first, then the project's are added after them; a project file can't remove or reorder yours.
|
|
394
|
+
|
|
395
|
+
```python
|
|
396
|
+
# .dostuff/hooks/block_main_push.py: PreToolUse on bash_command; the group's `if` already matched `git push`
|
|
397
|
+
import json
|
|
398
|
+
import subprocess
|
|
399
|
+
import sys
|
|
400
|
+
|
|
401
|
+
payload = json.load(sys.stdin)
|
|
402
|
+
words = payload["tool_input"].get("command", "").replace(":", " ").split()
|
|
403
|
+
branch = subprocess.run(["git", "branch", "--show-current"], capture_output=True, text=True,
|
|
404
|
+
cwd=payload["cwd"]).stdout.strip()
|
|
405
|
+
if "main" in words or branch == "main":
|
|
406
|
+
print("Don't push to main: push a feature branch and open a pull request.", file=sys.stderr)
|
|
407
|
+
sys.exit(2) # exit 2 = deny; stderr is the reason the model sees
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
```python
|
|
411
|
+
# .dostuff/hooks/format_file.py: PostToolUse on write_file|edit_file; stdout is appended to the tool result
|
|
412
|
+
import json
|
|
413
|
+
import subprocess
|
|
414
|
+
import sys
|
|
415
|
+
|
|
416
|
+
payload = json.load(sys.stdin)
|
|
417
|
+
path = payload["tool_input"].get("path", "")
|
|
418
|
+
if path.endswith(".py"):
|
|
419
|
+
result = subprocess.run(["ruff", "format", path], capture_output=True, text=True, cwd=payload["cwd"])
|
|
420
|
+
print((result.stdout + result.stderr).strip())
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
```python
|
|
424
|
+
# .dostuff/hooks/run_tests.py: Stop; exit 2 sends the failures back and the turn continues
|
|
425
|
+
import json
|
|
426
|
+
import subprocess
|
|
427
|
+
import sys
|
|
428
|
+
|
|
429
|
+
payload = json.load(sys.stdin)
|
|
430
|
+
result = subprocess.run([sys.executable, "-m", "pytest", "-q", "-x"], capture_output=True, text=True,
|
|
431
|
+
cwd=payload["cwd"])
|
|
432
|
+
if result.returncode not in (0, 5): # 5: no tests collected
|
|
433
|
+
print("Tests fail; fix them before finishing:\n" + result.stdout[-3000:], file=sys.stderr)
|
|
434
|
+
sys.exit(2)
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### Input and output
|
|
438
|
+
|
|
439
|
+
Each hook runs through the same shell as `bash_command`, in the project directory (a worktree subagent's own
|
|
440
|
+
worktree), with no terminal. stdin is one JSON object:
|
|
441
|
+
|
|
442
|
+
```json
|
|
443
|
+
{"hook_event_name": "PreToolUse", "session_id": "…", "turn_id": "…", "cwd": "…", "turn_type": "interactive_loop",
|
|
444
|
+
"tool_name": "bash_command", "tool_input": {"command": "git push origin main"}, "tool_use_id": "…"}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
| Event | Extra keys |
|
|
448
|
+
|---|---|
|
|
449
|
+
| `PreToolUse`, `PostToolUse` | `tool_name`, `tool_input`, `tool_use_id`; `PostToolUse` also `tool_response` (the result, already truncated) |
|
|
450
|
+
| `UserPromptSubmit` | `prompt` |
|
|
451
|
+
| `SessionStart` | `source` |
|
|
452
|
+
| `Stop`, `SubagentStop` | `last_assistant_message`, `stop_hook_active` (true once a Stop hook already continued this turn) |
|
|
453
|
+
|
|
454
|
+
The environment adds `DOSTUFF_HOOK_EVENT`, `DOSTUFF_SESSION_ID`, `DOSTUFF_TURN_ID`, `DOSTUFF_PROJECT_ROOT` and
|
|
455
|
+
`DOSTUFF_TURN_TYPE`. Output is capped at `loop.max_tool_output_chars`.
|
|
456
|
+
|
|
457
|
+
| Exit code | Meaning |
|
|
458
|
+
|---|---|
|
|
459
|
+
| `0` | Success. Plain stdout is context (`SessionStart`, `UserPromptSubmit`, `PostToolUse`); JSON on stdout can carry a decision (below) |
|
|
460
|
+
| `2` | Block. stderr (or stdout) is the reason: `PreToolUse` denies the call, `PostToolUse` appends it to the result, `UserPromptSubmit` rejects the prompt, `Stop` continues the turn |
|
|
461
|
+
| other | The hook failed: an error line in the transcript, and the run goes on (unless `fail_closed`) |
|
|
462
|
+
|
|
463
|
+
JSON on stdout (exit 0), in either Claude Code spelling:
|
|
464
|
+
|
|
465
|
+
```json
|
|
466
|
+
{"decision": "block", "reason": "…"}
|
|
467
|
+
{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "allow|deny|ask",
|
|
468
|
+
"permissionDecisionReason": "…", "updatedInput": {"command": "…"},
|
|
469
|
+
"additionalContext": "…"}}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
When several hooks match, they run one after another, each on the original input: the strictest decision wins
|
|
473
|
+
(`deny` > `ask` > `allow`), contexts are joined in config order, and the last `updatedInput` wins. Plan mode still
|
|
474
|
+
applies: a hook `allow` never runs a call plan mode refused, and settings changes still ask. Rewritten arguments are
|
|
475
|
+
checked again, so a rewrite can't skip the prompt the new call needs. Each hook's effect shows in the transcript as a
|
|
476
|
+
line like `⚓ PreToolUse bash_command → deny (reason)` (a `hook` event in `--output-format stream-json`). ESC kills a
|
|
477
|
+
running hook.
|
|
478
|
+
|
|
479
|
+
### Project hooks and trust
|
|
480
|
+
|
|
481
|
+
Hooks in `./.dostuff/config.yaml` come with the repo, so they are code from whoever wrote it. They run only once you
|
|
482
|
+
trust them, set by `hooks.project_trust` in **`~/.dostuff/config.yaml`** (a project file can't set it):
|
|
483
|
+
|
|
484
|
+
- `ask` (default): before the first project hook would run, the TUI lists every project hook's command and file and
|
|
485
|
+
asks. A yes is saved in `~/.dostuff/trusted_hooks.json` for that project, until the hooks block or any file in
|
|
486
|
+
`.dostuff/hooks/` changes; then it asks again. A no skips them for the session.
|
|
487
|
+
- `allow` runs them without asking; `deny` never runs them.
|
|
488
|
+
- Headless (`dostuff -p`) can't ask: it honours a saved yes, runs them with `--trust-project-hooks`, and otherwise
|
|
489
|
+
skips them with a warning. If a skipped hook is `fail_closed`, it refuses to start (exit code 2) instead.
|
|
490
|
+
|
|
491
|
+
Keep hook scripts in `.dostuff/hooks/`: trust covers the files there, and the model can't edit anything under
|
|
492
|
+
`.dostuff/` without a settings prompt. A script elsewhere in the repo can be changed after you trusted it.
|
|
493
|
+
|
|
494
|
+
`/hooks` lists every hook per event with its file, matcher, timeout, `fail_closed` and whether it runs (`user`,
|
|
495
|
+
`trusted`, `untrusted`, `disabled` or `broken`); `/hooks r` re-reads the config files. `dostuff doctor` reports
|
|
496
|
+
invalid hooks, commands not found on `PATH` and project scripts outside `.dostuff/hooks/`.
|
|
497
|
+
|
|
498
|
+
### Coming from Claude Code
|
|
499
|
+
|
|
500
|
+
The stdin envelope, exit codes, JSON output, event names and the `matcher` + `hooks: [{type: command, command}]`
|
|
501
|
+
nesting are the same. Tool names and some argument names differ, so rename the matcher and any `tool_input` keys:
|
|
502
|
+
|
|
503
|
+
| Claude Code | DoStuff `tool_name` | `tool_input` keys that differ |
|
|
504
|
+
|---|---|---|
|
|
505
|
+
| `Bash` | `bash_command` | none (`command`, plus `cwd`, `timeout`) |
|
|
506
|
+
| `Write` | `write_file` | `file_path` → `path` |
|
|
507
|
+
| `Edit` | `edit_file` | `file_path` → `path`, `old_string`/`new_string` → `old_str`/`new_str` |
|
|
508
|
+
| `Read` | `read_file` | `file_path` → `path` |
|
|
509
|
+
| `Grep` / `Glob` | `grep` / `glob` | per tool |
|
|
510
|
+
| `mcp__<server>__<tool>` | `call_mcp_tool` | server and tool are inside `tool_input` |
|
|
511
|
+
|
|
512
|
+
Not supported yet: `http`, `prompt` and `agent` hook types, and the `Notification`, `PreCompact`, `SessionEnd` and
|
|
513
|
+
`SubagentStart` events.
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
323
517
|
## Configuration
|
|
324
518
|
|
|
325
519
|
Config is layered, with later sources overriding earlier ones:
|
|
@@ -352,7 +546,7 @@ models:
|
|
|
352
546
|
loop:
|
|
353
547
|
max_iterations: 100
|
|
354
548
|
subagent_max_iterations: 25
|
|
355
|
-
# context_window: 128000 # override when litellm
|
|
549
|
+
# context_window: 128000 # override when dostuff/litellm don't know the model
|
|
356
550
|
max_tool_output_chars: 30000 # longer tool results keep head + tail
|
|
357
551
|
max_retries: 6 # retries on rate limits, timeouts, 5xx (0 = none)
|
|
358
552
|
retry_max_delay: 60 # cap on each wait, seconds; Retry-After is honored
|
|
@@ -416,6 +610,8 @@ or `/model` inside the TUI. Model lists come from litellm's catalog (chat models
|
|
|
416
610
|
|
|
417
611
|
OpenCode models (`opencode/<id>` for Zen, `opencode-go/<id>` for a Go subscription) use the ids from the [OpenCode docs](https://opencode.ai/docs/zen/). litellm has no OpenCode provider, so DoStuff sends each model to the gateway endpoint for its family: Claude models (and a few Qwen / MiniMax models) to the Anthropic Messages endpoint, GPT / Grok / Muse models to the Responses endpoint, Gemini models to the Gemini endpoint, and all other models to chat completions. One console key in `OPENCODE_API_KEY` works for Zen and Go. OpenCode only allows its free models from its own app, so use paid models or Go.
|
|
418
612
|
|
|
613
|
+
The gateways don't publish a model's context window in `/v1/models`, so `helpers/models/opencode.py` carries one per model, taken from [models.dev](https://models.dev)'s `opencode` and `opencode-go` entries (the same id can differ per gateway: `opencode/minimax-m3` is 512k, `opencode-go/minimax-m3` is 1M). `/context`, the status bar and auto-compaction size themselves from it; set `loop.context_window` to override.
|
|
614
|
+
|
|
419
615
|
---
|
|
420
616
|
|
|
421
617
|
## MCP servers
|
|
@@ -36,6 +36,7 @@ DoStuff is a terminal AI agent that runs anywhere `pip` runs. It gives you a ful
|
|
|
36
36
|
- **Project instructions** — `AGENTS.md` / `CLAUDE.md` from `~/.dostuff/`, the repo root and every folder down to the current one, combined
|
|
37
37
|
- **Coding tools** — `read_file` (line numbers, offset/limit), `edit_file` (exact replacement), `write_file`, `grep`, `glob`, `list_files`, `delete_file`, `bash_command`, `web_fetch` (read a URL as text), `web_search` (free, no API key)
|
|
38
38
|
- **Permission layer** — mutating tool calls ask for approval (allow once / always this session / deny with a note); read-only shell commands run without asking; modes `ask` / `allowlist` / `auto`; `/undo` reverts the agent's file changes
|
|
39
|
+
- **Hooks** — shell commands on `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `Stop` and `SubagentStop` that can block a tool call or prompt, feed a formatter's output back to the model, or keep the turn going; Claude Code's hook I/O contract, and project hooks run only once you trust them. See [Hooks](#hooks)
|
|
39
40
|
- **File sandbox** — file tools stay inside the project directory and never read `.env*` files or `~/.dostuff/`
|
|
40
41
|
- **Skills loader** — drop a `SKILL.md` in the right folder, restart, done; run one with `/<skill-name> [args]`
|
|
41
42
|
- **Subagents** — `delegate_to_subagent` runs a bounded task with a restricted tool set; its approval prompts and ESC go through the same TUI
|
|
@@ -155,7 +156,7 @@ In the TUI:
|
|
|
155
156
|
| `dostuff sessions rm <id>... [-y]` | Delete sessions and their `/undo` checkpoints |
|
|
156
157
|
| `dostuff sessions prune [--older-than 30d] [--dry-run] [-y]` | Delete empty sessions, and sessions not used for the given time (`45m`, `12h`, `30d`, `2w`) |
|
|
157
158
|
| `dostuff export [<id>] [-o file\|-]` | Save a session (default: latest here) as Markdown: prompts, answers, tool calls and results |
|
|
158
|
-
| `dostuff -p "<prompt>"` | Headless: run one prompt, print the answer to stdout, exit. Progress goes to stderr. Piped stdin is appended to the prompt (`-p -` reads the prompt from stdin). `--session <id>` continues a session, `--output-format json` (or `--json`) prints `{result, session_id, usage, ...}`, `--output-format stream-json` prints one JSON event per line (`init`, `assistant`, `tool_call`, `tool_result`, `usage`, `error`, then `result`), `-q` hides progress, `--permission-mode ask\|allowlist\|auto\|plan`. `--max-turns N` stops after N model calls (exit code 1). `--allowed-tools` (comma-separated or repeated) runs matching calls without asking in any mode, in the `permissions.allow` syntax (`"npm test"`, `web_fetch:docs.python.org`) or a tool name for every call of it (`write_file`, `bash_command`). Without a terminal, other calls that need approval are denied |
|
|
159
|
+
| `dostuff -p "<prompt>"` | Headless: run one prompt, print the answer to stdout, exit. Progress goes to stderr. Piped stdin is appended to the prompt (`-p -` reads the prompt from stdin). `--session <id>` continues a session, `--output-format json` (or `--json`) prints `{result, session_id, usage, ...}`, `--output-format stream-json` prints one JSON event per line (`init`, `assistant`, `tool_call`, `tool_result`, `usage`, `error`, then `result`), `-q` hides progress, `--permission-mode ask\|allowlist\|auto\|plan`. `--max-turns N` stops after N model calls (exit code 1). `--allowed-tools` (comma-separated or repeated) runs matching calls without asking in any mode, in the `permissions.allow` syntax (`"npm test"`, `web_fetch:docs.python.org`) or a tool name for every call of it (`write_file`, `bash_command`). Without a terminal, other calls that need approval are denied. `--trust-project-hooks` runs the project's [hooks](#project-hooks-and-trust) without a saved trust answer |
|
|
159
160
|
| `dostuff mcp list` | Show MCP servers and the file each comes from |
|
|
160
161
|
| `dostuff mcp add <name> [-e K=V] -- <command> [args...]` | Add a stdio server; `--url <url> [-H "Name: value"]` for HTTP; `--project` writes `./.dostuff/mcp_config.json` |
|
|
161
162
|
| `dostuff mcp remove <name> [--project]` | Remove a server |
|
|
@@ -195,7 +196,8 @@ In the TUI:
|
|
|
195
196
|
| `/status` | Session, directory, model, permission mode, tokens, context use, MCP servers |
|
|
196
197
|
| `/context` | What fills the context window: system prompt, tools, MCP tools, project instructions, compaction summary and messages, with tokens, % and a bar |
|
|
197
198
|
| `/memory [project\|global]` | Edit `AGENTS.md` (this project's or `~/.dostuff/AGENTS.md`) in the same editor as `/config`; `Ctrl+S` saves and the agent follows it from the next message |
|
|
198
|
-
| `/doctor` | The `dostuff doctor` checks (config, API key, MCP config, memory), with a fix for each problem |
|
|
199
|
+
| `/doctor` | The `dostuff doctor` checks (config, API key, MCP config, memory, hooks), with a fix for each problem |
|
|
200
|
+
| `/hooks [r]` | List the configured hooks per event: file, matcher, timeout, `fail_closed`, and whether each runs (`user` / `trusted` / `untrusted` / `disabled` / `broken`). `/hooks r` re-reads the config files. See [Hooks](#hooks) |
|
|
199
201
|
| `/<skill> [args]` | Run a skill: every discovered skill is a command (listed in the `/` menu and `/help`). See [Skills](#skills) |
|
|
200
202
|
| `/<command> [args]` | Run a custom command from `.dostuff/commands/` or `~/.dostuff/commands/`. See [Custom commands](#custom-commands) |
|
|
201
203
|
| `/rename <title>` | Name this session (shown by `dostuff sessions`) |
|
|
@@ -237,6 +239,11 @@ In the TUI:
|
|
|
237
239
|
| `read_settings` | read | never (MCP `env` / header values are shown as `<redacted>`) |
|
|
238
240
|
| `edit_config`, `set_mcp_server`, `remove_mcp_server` | settings | **always, in every mode, with no "always" option** (shows the diff or the server's command) |
|
|
239
241
|
|
|
242
|
+
File tools writing under a `.dostuff` directory, and `bash_command` calls whose command or `cwd` names `.dostuff`, are
|
|
243
|
+
settings changes too, so they always ask. That way auto mode can't quietly add a hook to `~/.dostuff/config.yaml`. Paths
|
|
244
|
+
into subagent worktrees (`.dostuff/worktrees/…`) are exempt. The shell check matches the text `.dostuff`, so a name the
|
|
245
|
+
command builds indirectly is not caught.
|
|
246
|
+
|
|
240
247
|
`permissions.mode` in `config.yaml`:
|
|
241
248
|
|
|
242
249
|
- `ask` (default) — as in the table above.
|
|
@@ -272,6 +279,193 @@ API keys stay in `.env` files, which the agent can't read or change. Subagents d
|
|
|
272
279
|
|
|
273
280
|
---
|
|
274
281
|
|
|
282
|
+
## Hooks
|
|
283
|
+
|
|
284
|
+
Hooks are shell commands DoStuff runs at fixed points of a session: block a tool call, feed a formatter's output back
|
|
285
|
+
to the model, keep the turn going until the tests pass. They are configured under `hooks:` in `config.yaml` and use
|
|
286
|
+
Claude Code's I/O contract (stdin JSON, exit codes, JSON output), so an existing Claude Code hook script works once its
|
|
287
|
+
matcher and tool argument names are renamed (see [the table below](#coming-from-claude-code)).
|
|
288
|
+
|
|
289
|
+
> [!WARNING]
|
|
290
|
+
> Hooks run with your privileges, **unsandboxed**, and are not permission-checked: they are your config, not model
|
|
291
|
+
> output. Read a project's hooks before you trust them.
|
|
292
|
+
|
|
293
|
+
| Event | When | Matcher is matched against | What a hook can do |
|
|
294
|
+
|---|---|---|---|
|
|
295
|
+
| `SessionStart` | Before the first turn of a session, and after a compaction | the source: `startup`, `resume`, `clear`, `compact` | Add context for the rest of the session |
|
|
296
|
+
| `UserPromptSubmit` | After you send a prompt, before it is stored or sent | — | Reject the prompt, or add context for this turn |
|
|
297
|
+
| `PreToolUse` | Before each tool call, before any approval prompt | the tool name | `allow`, `deny` or `ask`; rewrite the arguments |
|
|
298
|
+
| `PostToolUse` | After a tool call ran | the tool name | Append text to the result the model sees |
|
|
299
|
+
| `Stop` | When the agent is about to give its final answer | — | Block, so the turn continues with your reason |
|
|
300
|
+
| `SubagentStop` | Same, for a `delegate_to_subagent` run | — | Same |
|
|
301
|
+
|
|
302
|
+
### Config
|
|
303
|
+
|
|
304
|
+
`matcher` is a Python regex that must match the whole name (`edit_file` does not match `edit_file_v2`); empty or `*`
|
|
305
|
+
matches everything. `if` (tool events only) is searched in a `bash_command`'s `command`. Inline flags such as `(?i)`
|
|
306
|
+
go first in a pattern. `timeout` (default 30 s, 1–300) and `fail_closed` can be set on a group or on one hook. This is
|
|
307
|
+
the example from the spec, and the two scripts it runs are below:
|
|
308
|
+
|
|
309
|
+
```yaml
|
|
310
|
+
hooks:
|
|
311
|
+
project_trust: ask # ask | allow | deny (D4) — only read from ~/.dostuff/config.yaml
|
|
312
|
+
stop_block_limit: 3 # Stop blocks honoured per turn before the loop is allowed to end (1–10)
|
|
313
|
+
SessionStart:
|
|
314
|
+
- matcher: "startup|resume|clear" # regex, full match against the source; empty or "*" = all
|
|
315
|
+
hooks:
|
|
316
|
+
- type: command
|
|
317
|
+
command: "git status --short"
|
|
318
|
+
PreToolUse:
|
|
319
|
+
- matcher: "bash_command" # regex, full match against the tool name
|
|
320
|
+
if: "(?i)^\\s*git\\s+push" # optional regex, searched in tool_input.command (shell tool only)
|
|
321
|
+
fail_closed: true
|
|
322
|
+
timeout: 10
|
|
323
|
+
hooks:
|
|
324
|
+
- type: command
|
|
325
|
+
name: block-main-push
|
|
326
|
+
command: "python .dostuff/hooks/block_main_push.py"
|
|
327
|
+
PostToolUse:
|
|
328
|
+
- matcher: "write_file|edit_file"
|
|
329
|
+
hooks:
|
|
330
|
+
# the script reads tool_input.path from the stdin JSON and runs the formatter on it;
|
|
331
|
+
# its stdout is fed back to the model (D5)
|
|
332
|
+
- type: command
|
|
333
|
+
name: format
|
|
334
|
+
command: "python .dostuff/hooks/format_file.py"
|
|
335
|
+
Stop:
|
|
336
|
+
- hooks:
|
|
337
|
+
- { type: command, name: tests, command: "python .dostuff/hooks/run_tests.py" }
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
- **`fail_closed: true`** (`PreToolUse` and `UserPromptSubmit` only) is for guardrails: if the hook crashes or times
|
|
341
|
+
out, the call is denied (or the prompt rejected) instead of going ahead. A `fail_closed` hook with a config mistake,
|
|
342
|
+
such as a bad regex, is kept and denies everything it matches; any other invalid hook is dropped with a warning.
|
|
343
|
+
- **`stop_block_limit`** (1–10) bounds how many times `Stop` hooks can continue one turn; after that the turn ends with
|
|
344
|
+
a note.
|
|
345
|
+
- User hooks come first, then the project's are added after them; a project file can't remove or reorder yours.
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
# .dostuff/hooks/block_main_push.py: PreToolUse on bash_command; the group's `if` already matched `git push`
|
|
349
|
+
import json
|
|
350
|
+
import subprocess
|
|
351
|
+
import sys
|
|
352
|
+
|
|
353
|
+
payload = json.load(sys.stdin)
|
|
354
|
+
words = payload["tool_input"].get("command", "").replace(":", " ").split()
|
|
355
|
+
branch = subprocess.run(["git", "branch", "--show-current"], capture_output=True, text=True,
|
|
356
|
+
cwd=payload["cwd"]).stdout.strip()
|
|
357
|
+
if "main" in words or branch == "main":
|
|
358
|
+
print("Don't push to main: push a feature branch and open a pull request.", file=sys.stderr)
|
|
359
|
+
sys.exit(2) # exit 2 = deny; stderr is the reason the model sees
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
```python
|
|
363
|
+
# .dostuff/hooks/format_file.py: PostToolUse on write_file|edit_file; stdout is appended to the tool result
|
|
364
|
+
import json
|
|
365
|
+
import subprocess
|
|
366
|
+
import sys
|
|
367
|
+
|
|
368
|
+
payload = json.load(sys.stdin)
|
|
369
|
+
path = payload["tool_input"].get("path", "")
|
|
370
|
+
if path.endswith(".py"):
|
|
371
|
+
result = subprocess.run(["ruff", "format", path], capture_output=True, text=True, cwd=payload["cwd"])
|
|
372
|
+
print((result.stdout + result.stderr).strip())
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
```python
|
|
376
|
+
# .dostuff/hooks/run_tests.py: Stop; exit 2 sends the failures back and the turn continues
|
|
377
|
+
import json
|
|
378
|
+
import subprocess
|
|
379
|
+
import sys
|
|
380
|
+
|
|
381
|
+
payload = json.load(sys.stdin)
|
|
382
|
+
result = subprocess.run([sys.executable, "-m", "pytest", "-q", "-x"], capture_output=True, text=True,
|
|
383
|
+
cwd=payload["cwd"])
|
|
384
|
+
if result.returncode not in (0, 5): # 5: no tests collected
|
|
385
|
+
print("Tests fail; fix them before finishing:\n" + result.stdout[-3000:], file=sys.stderr)
|
|
386
|
+
sys.exit(2)
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Input and output
|
|
390
|
+
|
|
391
|
+
Each hook runs through the same shell as `bash_command`, in the project directory (a worktree subagent's own
|
|
392
|
+
worktree), with no terminal. stdin is one JSON object:
|
|
393
|
+
|
|
394
|
+
```json
|
|
395
|
+
{"hook_event_name": "PreToolUse", "session_id": "…", "turn_id": "…", "cwd": "…", "turn_type": "interactive_loop",
|
|
396
|
+
"tool_name": "bash_command", "tool_input": {"command": "git push origin main"}, "tool_use_id": "…"}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
| Event | Extra keys |
|
|
400
|
+
|---|---|
|
|
401
|
+
| `PreToolUse`, `PostToolUse` | `tool_name`, `tool_input`, `tool_use_id`; `PostToolUse` also `tool_response` (the result, already truncated) |
|
|
402
|
+
| `UserPromptSubmit` | `prompt` |
|
|
403
|
+
| `SessionStart` | `source` |
|
|
404
|
+
| `Stop`, `SubagentStop` | `last_assistant_message`, `stop_hook_active` (true once a Stop hook already continued this turn) |
|
|
405
|
+
|
|
406
|
+
The environment adds `DOSTUFF_HOOK_EVENT`, `DOSTUFF_SESSION_ID`, `DOSTUFF_TURN_ID`, `DOSTUFF_PROJECT_ROOT` and
|
|
407
|
+
`DOSTUFF_TURN_TYPE`. Output is capped at `loop.max_tool_output_chars`.
|
|
408
|
+
|
|
409
|
+
| Exit code | Meaning |
|
|
410
|
+
|---|---|
|
|
411
|
+
| `0` | Success. Plain stdout is context (`SessionStart`, `UserPromptSubmit`, `PostToolUse`); JSON on stdout can carry a decision (below) |
|
|
412
|
+
| `2` | Block. stderr (or stdout) is the reason: `PreToolUse` denies the call, `PostToolUse` appends it to the result, `UserPromptSubmit` rejects the prompt, `Stop` continues the turn |
|
|
413
|
+
| other | The hook failed: an error line in the transcript, and the run goes on (unless `fail_closed`) |
|
|
414
|
+
|
|
415
|
+
JSON on stdout (exit 0), in either Claude Code spelling:
|
|
416
|
+
|
|
417
|
+
```json
|
|
418
|
+
{"decision": "block", "reason": "…"}
|
|
419
|
+
{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "allow|deny|ask",
|
|
420
|
+
"permissionDecisionReason": "…", "updatedInput": {"command": "…"},
|
|
421
|
+
"additionalContext": "…"}}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
When several hooks match, they run one after another, each on the original input: the strictest decision wins
|
|
425
|
+
(`deny` > `ask` > `allow`), contexts are joined in config order, and the last `updatedInput` wins. Plan mode still
|
|
426
|
+
applies: a hook `allow` never runs a call plan mode refused, and settings changes still ask. Rewritten arguments are
|
|
427
|
+
checked again, so a rewrite can't skip the prompt the new call needs. Each hook's effect shows in the transcript as a
|
|
428
|
+
line like `⚓ PreToolUse bash_command → deny (reason)` (a `hook` event in `--output-format stream-json`). ESC kills a
|
|
429
|
+
running hook.
|
|
430
|
+
|
|
431
|
+
### Project hooks and trust
|
|
432
|
+
|
|
433
|
+
Hooks in `./.dostuff/config.yaml` come with the repo, so they are code from whoever wrote it. They run only once you
|
|
434
|
+
trust them, set by `hooks.project_trust` in **`~/.dostuff/config.yaml`** (a project file can't set it):
|
|
435
|
+
|
|
436
|
+
- `ask` (default): before the first project hook would run, the TUI lists every project hook's command and file and
|
|
437
|
+
asks. A yes is saved in `~/.dostuff/trusted_hooks.json` for that project, until the hooks block or any file in
|
|
438
|
+
`.dostuff/hooks/` changes; then it asks again. A no skips them for the session.
|
|
439
|
+
- `allow` runs them without asking; `deny` never runs them.
|
|
440
|
+
- Headless (`dostuff -p`) can't ask: it honours a saved yes, runs them with `--trust-project-hooks`, and otherwise
|
|
441
|
+
skips them with a warning. If a skipped hook is `fail_closed`, it refuses to start (exit code 2) instead.
|
|
442
|
+
|
|
443
|
+
Keep hook scripts in `.dostuff/hooks/`: trust covers the files there, and the model can't edit anything under
|
|
444
|
+
`.dostuff/` without a settings prompt. A script elsewhere in the repo can be changed after you trusted it.
|
|
445
|
+
|
|
446
|
+
`/hooks` lists every hook per event with its file, matcher, timeout, `fail_closed` and whether it runs (`user`,
|
|
447
|
+
`trusted`, `untrusted`, `disabled` or `broken`); `/hooks r` re-reads the config files. `dostuff doctor` reports
|
|
448
|
+
invalid hooks, commands not found on `PATH` and project scripts outside `.dostuff/hooks/`.
|
|
449
|
+
|
|
450
|
+
### Coming from Claude Code
|
|
451
|
+
|
|
452
|
+
The stdin envelope, exit codes, JSON output, event names and the `matcher` + `hooks: [{type: command, command}]`
|
|
453
|
+
nesting are the same. Tool names and some argument names differ, so rename the matcher and any `tool_input` keys:
|
|
454
|
+
|
|
455
|
+
| Claude Code | DoStuff `tool_name` | `tool_input` keys that differ |
|
|
456
|
+
|---|---|---|
|
|
457
|
+
| `Bash` | `bash_command` | none (`command`, plus `cwd`, `timeout`) |
|
|
458
|
+
| `Write` | `write_file` | `file_path` → `path` |
|
|
459
|
+
| `Edit` | `edit_file` | `file_path` → `path`, `old_string`/`new_string` → `old_str`/`new_str` |
|
|
460
|
+
| `Read` | `read_file` | `file_path` → `path` |
|
|
461
|
+
| `Grep` / `Glob` | `grep` / `glob` | per tool |
|
|
462
|
+
| `mcp__<server>__<tool>` | `call_mcp_tool` | server and tool are inside `tool_input` |
|
|
463
|
+
|
|
464
|
+
Not supported yet: `http`, `prompt` and `agent` hook types, and the `Notification`, `PreCompact`, `SessionEnd` and
|
|
465
|
+
`SubagentStart` events.
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
275
469
|
## Configuration
|
|
276
470
|
|
|
277
471
|
Config is layered, with later sources overriding earlier ones:
|
|
@@ -304,7 +498,7 @@ models:
|
|
|
304
498
|
loop:
|
|
305
499
|
max_iterations: 100
|
|
306
500
|
subagent_max_iterations: 25
|
|
307
|
-
# context_window: 128000 # override when litellm
|
|
501
|
+
# context_window: 128000 # override when dostuff/litellm don't know the model
|
|
308
502
|
max_tool_output_chars: 30000 # longer tool results keep head + tail
|
|
309
503
|
max_retries: 6 # retries on rate limits, timeouts, 5xx (0 = none)
|
|
310
504
|
retry_max_delay: 60 # cap on each wait, seconds; Retry-After is honored
|
|
@@ -368,6 +562,8 @@ or `/model` inside the TUI. Model lists come from litellm's catalog (chat models
|
|
|
368
562
|
|
|
369
563
|
OpenCode models (`opencode/<id>` for Zen, `opencode-go/<id>` for a Go subscription) use the ids from the [OpenCode docs](https://opencode.ai/docs/zen/). litellm has no OpenCode provider, so DoStuff sends each model to the gateway endpoint for its family: Claude models (and a few Qwen / MiniMax models) to the Anthropic Messages endpoint, GPT / Grok / Muse models to the Responses endpoint, Gemini models to the Gemini endpoint, and all other models to chat completions. One console key in `OPENCODE_API_KEY` works for Zen and Go. OpenCode only allows its free models from its own app, so use paid models or Go.
|
|
370
564
|
|
|
565
|
+
The gateways don't publish a model's context window in `/v1/models`, so `helpers/models/opencode.py` carries one per model, taken from [models.dev](https://models.dev)'s `opencode` and `opencode-go` entries (the same id can differ per gateway: `opencode/minimax-m3` is 512k, `opencode-go/minimax-m3` is 1M). `/context`, the status bar and auto-compaction size themselves from it; set `loop.context_window` to override.
|
|
566
|
+
|
|
371
567
|
---
|
|
372
568
|
|
|
373
569
|
## MCP servers
|