kward 0.82.0 → 0.84.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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +96 -15
  3. data/CONTRIBUTING.md +74 -0
  4. data/Gemfile.lock +8 -2
  5. data/README.md +21 -1
  6. data/Rakefile +46 -2
  7. data/SECURITY.md +31 -0
  8. data/doc/agent-tools.md +1 -0
  9. data/doc/api.md +4 -0
  10. data/doc/composer.md +2 -2
  11. data/doc/configuration.md +82 -20
  12. data/doc/editor.md +47 -22
  13. data/doc/files.md +12 -7
  14. data/doc/getting-started.md +3 -0
  15. data/doc/pan.md +19 -15
  16. data/doc/permissions.md +1 -0
  17. data/doc/platform-support.md +48 -0
  18. data/doc/releasing.md +12 -4
  19. data/doc/rpc.md +2 -2
  20. data/doc/sandboxing.md +5 -1
  21. data/doc/security.md +4 -3
  22. data/doc/shell.md +101 -60
  23. data/doc/tabs.md +3 -0
  24. data/doc/troubleshooting.md +12 -2
  25. data/doc/usage.md +7 -6
  26. data/kward.gemspec +5 -4
  27. data/lib/kward/agent.rb +7 -3
  28. data/lib/kward/ansi.rb +110 -10
  29. data/lib/kward/auth/anthropic_oauth.rb +7 -7
  30. data/lib/kward/cli/auth_commands.rb +34 -13
  31. data/lib/kward/cli/commands.rb +83 -62
  32. data/lib/kward/cli/doctor.rb +39 -17
  33. data/lib/kward/cli/hook_commands.rb +22 -12
  34. data/lib/kward/cli/interactive_turn.rb +144 -21
  35. data/lib/kward/cli/project_skills_commands.rb +8 -4
  36. data/lib/kward/cli/prompt_interface.rb +27 -0
  37. data/lib/kward/cli/rendering.rb +15 -9
  38. data/lib/kward/cli/runtime_helpers.rb +260 -55
  39. data/lib/kward/cli/slash_commands.rb +21 -12
  40. data/lib/kward/cli/tabs.rb +91 -13
  41. data/lib/kward/cli/tool_summaries.rb +14 -0
  42. data/lib/kward/cli.rb +55 -7
  43. data/lib/kward/cli_transcript_formatter.rb +11 -4
  44. data/lib/kward/compaction/token_estimator.rb +12 -6
  45. data/lib/kward/config_files.rb +94 -63
  46. data/lib/kward/detached_run.rb +44 -0
  47. data/lib/kward/editor_prompt.rb +46 -0
  48. data/lib/kward/editor_prompt_session.rb +28 -0
  49. data/lib/kward/interactive_pty_runner.rb +102 -28
  50. data/lib/kward/{ekwsh.rb → kwsh.rb} +103 -24
  51. data/lib/kward/kwshrc.rb +233 -0
  52. data/lib/kward/markdown_code_block.rb +136 -0
  53. data/lib/kward/model/client.rb +35 -39
  54. data/lib/kward/model/model_info.rb +3 -2
  55. data/lib/kward/model/payloads.rb +0 -2
  56. data/lib/kward/model/provider_catalog.rb +5 -0
  57. data/lib/kward/model/stream_parser.rb +20 -4
  58. data/lib/kward/pan/index.html.erb +3 -3
  59. data/lib/kward/pan/server.rb +23 -3
  60. data/lib/kward/persistent_shell_session.rb +843 -0
  61. data/lib/kward/project_files.rb +19 -6
  62. data/lib/kward/prompt_interface/composer_renderer.rb +45 -41
  63. data/lib/kward/prompt_interface/composer_state.rb +33 -24
  64. data/lib/kward/prompt_interface/editor/auto_indent.rb +27 -22
  65. data/lib/kward/prompt_interface/editor/controller.rb +99 -33
  66. data/lib/kward/prompt_interface/editor/endwise.rb +13 -4
  67. data/lib/kward/prompt_interface/editor/modes/modern.rb +4 -0
  68. data/lib/kward/prompt_interface/editor/modes/vibe.rb +298 -47
  69. data/lib/kward/prompt_interface/editor/renderer.rb +109 -6
  70. data/lib/kward/prompt_interface/editor/runner.rb +362 -0
  71. data/lib/kward/prompt_interface/editor/runner_state.rb +78 -0
  72. data/lib/kward/prompt_interface/editor/state.rb +21 -10
  73. data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +68 -6
  74. data/lib/kward/prompt_interface/editor/vibe_state.rb +3 -3
  75. data/lib/kward/prompt_interface/editor/word_completion.rb +124 -0
  76. data/lib/kward/prompt_interface/file_overlay.rb +92 -22
  77. data/lib/kward/prompt_interface/key_handler.rb +75 -0
  78. data/lib/kward/prompt_interface/overlay_renderer.rb +7 -5
  79. data/lib/kward/prompt_interface/project_browser.rb +443 -17
  80. data/lib/kward/prompt_interface/runtime_state.rb +55 -2
  81. data/lib/kward/prompt_interface/screen.rb +2 -2
  82. data/lib/kward/prompt_interface/selection_prompt.rb +3 -1
  83. data/lib/kward/prompt_interface/slash_overlay.rb +19 -4
  84. data/lib/kward/prompt_interface/transcript_renderer.rb +12 -7
  85. data/lib/kward/prompt_interface.rb +169 -18
  86. data/lib/kward/prompts/commands.rb +2 -1
  87. data/lib/kward/prompts.rb +1 -1
  88. data/lib/kward/pty_output_sink.rb +47 -0
  89. data/lib/kward/rpc/auth_manager.rb +1 -1
  90. data/lib/kward/rpc/server.rb +2 -1
  91. data/lib/kward/rpc/transcript_normalizer.rb +7 -3
  92. data/lib/kward/scratchpad_languages.rb +74 -0
  93. data/lib/kward/scratchpad_runner.rb +155 -29
  94. data/lib/kward/shell_prompt.rb +52 -0
  95. data/lib/kward/shell_prompt_session.rb +58 -0
  96. data/lib/kward/terminal_keys.rb +13 -0
  97. data/lib/kward/terminal_text.rb +121 -0
  98. data/lib/kward/text_matcher.rb +18 -0
  99. data/lib/kward/tools/open_editor.rb +41 -0
  100. data/lib/kward/tools/prepare_shell_command.rb +28 -0
  101. data/lib/kward/tools/registry.rb +75 -8
  102. data/lib/kward/tools/replace_editor_buffer.rb +30 -0
  103. data/lib/kward/tools/run_shell_command.rb +24 -6
  104. data/lib/kward/tools/tool_call.rb +2 -1
  105. data/lib/kward/version.rb +1 -1
  106. data/templates/default/fulldoc/html/css/kward.css +0 -125
  107. data/templates/default/fulldoc/html/images/kward_workflow.svg +52 -0
  108. data/templates/default/fulldoc/html/setup.rb +1 -1
  109. data/templates/default/kward_navigation.rb +1 -0
  110. data/templates/default/layout/html/footer.erb +10 -0
  111. data/templates/default/layout/html/headers.erb +23 -0
  112. data/templates/default/layout/html/layout.erb +6 -18
  113. data/templates/default/layout/html/setup.rb +41 -2
  114. metadata +44 -8
  115. data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
data/doc/releasing.md CHANGED
@@ -60,13 +60,21 @@ If preparation fails before the commit, the command restores the version, lockfi
60
60
  A pushed `v*` tag starts `.github/workflows/release.yml`. The workflow:
61
61
 
62
62
  1. Checks that the tag, gem version, and changelog heading agree.
63
- 2. Runs the full test suite and generated-documentation checks against Ruby 3.4.
63
+ 2. Installs and enables Bubblewrap, then runs the full test suite and generated-documentation checks against Ruby 3.4 using the same Linux sandbox setup as normal CI.
64
64
  3. Builds the gem and verifies its packaged files.
65
65
  4. Publishes through RubyGems trusted publishing.
66
- 5. Verifies the local gem checksum against the artifact served by RubyGems.org.
67
- 6. Creates `Kward VERSION` as a GitHub Release using that version's changelog section and attaches the verified gem.
66
+ 5. Downloads the canonical published gem from RubyGems.org, waiting for propagation when necessary, and verifies its checksum against the RubyGems API.
67
+ 6. Creates `Kward VERSION` as a GitHub Release using that version's changelog section and attaches the verified RubyGems artifact.
68
68
 
69
- The publishing job uses the protected `release` environment. If RubyGems.org already has the version after a partially completed workflow, a rerun rebuilds the gem and verifies that it exactly matches the published checksum before continuing. For an existing GitHub Release, the workflow downloads and compares the gem, uploads it when missing, and publishes an unfinished draft. This makes normal workflow reruns safe without silently replacing mismatched artifacts.
69
+ The publishing job uses the protected `release` environment. If RubyGems.org already has the version after a partially completed workflow, a rerun downloads and verifies the canonical published gem before continuing. This also accommodates trusted-publishing attestations that can change the published gem bytes. For an existing GitHub Release, the workflow downloads and compares the gem, uploads it when missing, and publishes an unfinished draft. This makes normal workflow reruns safe without silently replacing mismatched artifacts.
70
+
71
+ If the workflow itself needs a fix after a tag has already been pushed, commit and push the fix to `main`, then recover the existing tag with the updated workflow:
72
+
73
+ ```bash
74
+ gh workflow run Release --ref main -f tag=v0.82.0
75
+ ```
76
+
77
+ Manual recovery still checks out and verifies the tagged source before publishing. The `release` environment permits automatic version-tag runs and manual recovery runs from `main` only.
70
78
 
71
79
  Follow the run from the repository's **Actions → Release** page. Installation can be checked after publication with:
72
80
 
data/doc/rpc.md CHANGED
@@ -80,11 +80,11 @@ Read `capabilities` at runtime instead of assuming every feature is available. I
80
80
  - `mcp`: local stdio MCP server support through the shared `mcpServers` config. RPC exposes MCP tools to turns and advertises discovery with `methods: ["tools/list", "mcp/status"]`, `toolMetadata: true`, and `serverStatus: true`. It does not support MCP resources, prompts, sampling, or Streamable HTTP.
81
81
  - `startupResources`: supported startup resource listing for context, skills, prompts, and plugins.
82
82
  - `extensionUi`: question bridge support via `ui/question` and `ui/answerQuestion`, plus plugin footer updates via `ui/footer`; other UI primitives are explicitly unsupported.
83
- - `composer`: composer-only UI features. Interactive session diff totals are explicitly unsupported over RPC (`composer.sessionDiff.supported: false`) because RPC clients already receive per-tool diff results and no live composer status payload is exposed. Clipboard copy is also unsupported over RPC (`composer.copy.supported: false`) because UI clients own clipboard access.
83
+ - `composer`: composer-only UI features. Interactive session diff totals are explicitly unsupported over RPC (`composer.sessionDiff.supported: false`) because RPC clients already receive per-tool diff results and no live composer status payload is exposed. Clipboard copy is also unsupported over RPC (`composer.copy.supported: false`) because UI clients own clipboard access. Vibe editor prompts are unsupported over RPC (`composer.editorPrompt.supported: false`) because RPC has no live integrated editor buffer.
84
84
  - `security`: trusted-local behavior and optional per-turn tool approval. By default, RPC turns have no workspace mutation guard or tool approval, so shell commands and file changes can run. Clients can inspect file-tool guardrails through `capabilities.events.tools.workspaceGuardrails` and `runtime/state.workspaceGuardrailsEnabled`. `security.sandbox` reports the command sandbox mode, enforcement backend, and filesystem and network capabilities; session pinning and one-time elevation are unsupported. See [Command sandboxing](sandboxing.md) for the boundary and its limits.
85
85
  - `export`: supported transcript export formats. Currently `markdown` and `html`; default is `markdown`.
86
86
  - `starterPack`: explicitly unsupported (`supported: false`, reason `cliOnlyInstallCommand`). Use `kward init` from the shell.
87
- - `shell`: explicitly unsupported (`supported: false`, reason `interactiveTuiOnly`) because `/shell`, `!command`, `/capture`, and PTY handoff require the local TUI. Model-requested `run_shell_command` tools remain bounded, captured, and noninteractive over RPC.
87
+ - `shell`: explicitly unsupported (`supported: false`, reason `interactiveTuiOnly`) because `/shell`, its transient `?` shell assistant, `!command`, `/capture`, and PTY handoff require the local TUI. Model-requested `run_shell_command` tools remain bounded, captured, and noninteractive over RPC.
88
88
  - `logging`: local redacted telemetry, its directory and enabled categories, `logging/stats` and `logging/tokenCsv`, bucketed `usageCsv` support, JSONL storage, and 10 MB rotation with manual retention. Logs contain redacted metadata only. Configuration uses the `logging` key and `KWARD_LOGGING` environment prefix.
89
89
 
90
90
  ### `shutdown`
data/doc/sandboxing.md CHANGED
@@ -106,12 +106,16 @@ this interface.
106
106
  ## Boundaries and limits
107
107
 
108
108
  The current implementation protects **model-requested command workers only**.
109
+ The transient `?` shell assistant is intentionally not one of those workers: its
110
+ `run_shell_command` calls use the active user-owned `/shell` process so shell
111
+ state can persist.
112
+
109
113
  It does not sandbox:
110
114
 
111
115
  - the Kward Ruby host process;
112
116
  - model-provider, search-provider, or RPC traffic;
113
117
  - trusted Ruby plugins;
114
- - MCP servers, lifecycle hooks, `/shell`, `!command`, `/capture`, or `/pty`.
118
+ - MCP servers, lifecycle hooks, `/shell`, `?` shell-agent commands, `!command`, `/capture`, or `/pty`.
115
119
 
116
120
  Sandboxed command workers receive a minimal environment: Kward preserves only
117
121
  basic terminal, locale, and path variables, then supplies a private `HOME` and
data/doc/security.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Kward can read code, edit files, run commands, call model and search providers, and load local extensions. That makes it useful, but it also means you should treat it like a developer tool running with your account—not like a sandbox.
4
4
 
5
- This guide explains the trust boundaries and gives you a safe way to start work in an unfamiliar repository.
5
+ This guide explains the trust boundaries and gives you a safe way to start work in an unfamiliar repository. Report suspected vulnerabilities privately through the [security policy](https://github.com/kaiwood/kward/blob/main/SECURITY.md).
6
6
 
7
7
  ## The short version
8
8
 
@@ -56,8 +56,9 @@ Kward's built-in file tools normally resolve paths inside the active workspace.
56
56
 
57
57
  These protections reduce accidental edits. They do not contain the whole process:
58
58
 
59
- - With `sandbox.mode: off` (the default), `run_shell_command`, `!command`, `/capture`, `/shell`, and `/pty` run with your user permissions.
60
- - A non-off [command sandbox](sandboxing.md) restricts only model-requested `run_shell_command` workers and their descendants. It does not cover `!command`, `/capture`, `/shell`, or `/pty`.
59
+ - With `sandbox.mode: off` (the default), `run_shell_command`, `!command`, `/capture`, `/shell`, `/pty`, and editor buffer runners run with your user permissions.
60
+ - Editor buffer runners are user-directed and execute the current in-memory buffer, including normal files and scratchpads, through the configured `editor.runners` binary without a shell. They are not covered by the command sandbox.
61
+ - A non-off [command sandbox](sandboxing.md) restricts only model-requested `run_shell_command` workers and their descendants. It does not cover `!command`, `/capture`, `/shell`, or `/pty`, including commands that the transient `?` shell assistant runs through the user's persistent `/shell` process.
61
62
  - External `/shell` commands, `!command`, and `/pty` receive an interactive PTY. Kward forwards a conservative set of line-oriented controls into the inline region, then grants full-terminal passthrough when a child emits screen-oriented or unknown controls. Full passthrough bypasses transcript control-sequence sanitization, so run only commands you trust with terminal access. `capture <command>` and `/capture <command>` sanitize their captured output.
62
63
  - Plugins, command hooks, and MCP servers are local processes with the same general operating-system access.
63
64
  - Read-before-edit applies to Kward's file tools, not to arbitrary shell commands or extension code.
data/doc/shell.md CHANGED
@@ -4,6 +4,7 @@ Kward lets you run your own shell commands without leaving the interactive TUI.
4
4
 
5
5
  - **For one quick command, use `!command`.** It runs from the active workspace and returns you to the normal composer when it finishes.
6
6
  - **For a longer stretch of shell work, use `/shell`.** It keeps its directory, environment, aliases, and history between commands.
7
+ - **To ask Kward for help from inside `/shell`, prefix the request with `?`.** It can inspect the latest shell output, run an explicitly requested state change, or prepare a command in the shell prompt for your confirmation.
7
8
  - **For bounded output in the transcript view, use `/capture <command>`.** Inside `/shell`, use `capture <command>` for the same kind of readable output while keeping the shell's current state.
8
9
 
9
10
  Ordinary `!command` input and external commands inside `/shell` receive an interactive PTY. Pagers, Vim, SSH, REPLs, password prompts, and other interactive tools therefore work without a `pty` prefix.
@@ -20,15 +21,14 @@ Prefix a command with `!` in the normal composer:
20
21
 
21
22
  The command runs from the active workspace root and begins in an inline PTY region above a frozen composer. Line-oriented output, single-line carriage-return progress, and synchronized-output update brackets scroll the transcript area naturally while keyboard input belongs to the child process. If the child emits alternate-screen, clear-screen, absolute cursor, or unknown terminal controls, Kward conservatively hides the composer and switches permanently to full-terminal passthrough for the rest of that command. Pagers and full-screen applications therefore retain the complete terminal without relying on a command-name allowlist.
22
23
 
23
- When an inline command exits without reading input, safe output is mirrored into the transient transcript view so a repaint cannot hide it. Carriage-return and horizontal-cursor progress redraws are reduced to their final visible lines, while an unterminated synchronized-output update is closed before Kward redraws. If the child reads input, Kward retains only output captured before the first forwarded input byte; this prevents echoed passwords, OTPs, or other input from entering tab state. Shell output is never added to the AI conversation or sent to the model.
24
+ When an inline command exits without reading input, safe output is mirrored into the transient transcript view so a repaint cannot hide it. Carriage-return and horizontal-cursor progress redraws are reduced to their final visible lines, while an unterminated synchronized-output update is closed before Kward redraws. If the child reads input, Kward retains only output captured before the first forwarded input byte; this prevents echoed passwords, OTPs, or other input from entering tab state. Output from one-off commands is never added to the AI conversation or sent to the model.
24
25
 
25
- Shell output can leave transient text in the transcript area. **After the command finishes, press Ctrl+L to redraw the durable conversation and clear that transient `!command` output.** While an interactive command is still running, the composer remains frozen and keyboard input—including Ctrl+L and Kward's tab shortcuts—belongs to the child process.
26
+ Shell output can leave transient text in the transcript area. **After the command finishes, press Ctrl+L to redraw the durable conversation and clear that transient `!command` output.** While an interactive command owns the terminal, the composer remains frozen and ordinary keyboard input belongs to the child process. Kward's tab shortcuts are intercepted separately; switching tabs detaches the command and lets it continue in the originating tab's bounded background state.
26
27
 
27
- Configured `ekwsh.yml` aliases also work after `!`:
28
+ Configured `kwshrc` aliases also work after `!`:
28
29
 
29
- ```yaml
30
- aliases:
31
- glog: "git log --decorate --stat --graph"
30
+ ```sh
31
+ alias glog='git log --decorate --stat --graph'
32
32
  ```
33
33
 
34
34
  ```text
@@ -37,9 +37,8 @@ aliases:
37
37
 
38
38
  An alias that resolves to `kward edit <filename>` opens Kward's integrated editor in the current session instead of starting a nested Kward process:
39
39
 
40
- ```yaml
41
- aliases:
42
- vibe: "kward edit"
40
+ ```sh
41
+ alias vibe='kward edit'
43
42
  ```
44
43
 
45
44
  ```text
@@ -90,6 +89,31 @@ Leave shell mode with `exit`, `logout`, or Ctrl+D on an empty prompt.
90
89
 
91
90
  `cd` changes only the embedded shell's directory. It does not change Kward's workspace root or the directory used by the model's other tools.
92
91
 
92
+ ## Ask the shell agent
93
+
94
+ While `/shell` is active, start a submitted line with `?` to ask the transient shell assistant:
95
+
96
+ ```text
97
+ ? why did the last command fail?
98
+ ? show me which process is listening on port 3000
99
+ ? prepare a command to find Ruby files changed today
100
+ ```
101
+
102
+ The assistant receives the current shell directory, the last command, its exit status, and bounded output from that command. Output is sent to the model only because you explicitly asked with `?`; it is sanitized and bounded before being included, and shell-agent turns are not added to the normal session history.
103
+
104
+ If you explicitly ask the assistant to change shell state, it can use the active shell session:
105
+
106
+ ```text
107
+ ? cd into test
108
+ ? set RAILS_ENV to test
109
+ ```
110
+
111
+ For a suggestion or prepared command, the assistant uses `prepare_shell_command`. The command is placed in the shell composer but is not run until you press `Enter`. Running a command directly and preparing one are deliberately separate actions.
112
+
113
+ If you explicitly ask it to open an existing workspace file, the assistant uses `open_editor` to open Kward's integrated editor. Opening the editor does not modify or save the file.
114
+
115
+ The shell assistant cannot safely run commands that require terminal input. Ask it to prepare those commands instead. The local `/shell` session keeps one interactive shell process alive, so directory changes, variables, functions, aliases, and other shell state persist between commands. The one-off `!command` and `/capture` workflows remain separate. SSH remains available through the normal interactive PTY handoff, but shell-agent prompting resumes after that SSH session exits.
116
+
93
117
  ## Interactive and captured commands
94
118
 
95
119
  External commands inside `/shell` are interactive by default:
@@ -101,19 +125,19 @@ ruby
101
125
  ssh example.com
102
126
  ```
103
127
 
104
- Kward gives each command the terminal, forwards keyboard input, and restores the shell prompt when the command exits. It prints the submitted command but no PTY start message or exit-status summary. The line-oriented Git commands `git fetch`, `git ls-remote`, `git push`, `git remote`, and `git status` keep the shell prompt visible as a frozen display. Safe, line-oriented output from commands that did not read keyboard input is kept in the transient transcript view; full-screen and genuinely interactive output stays terminal-owned.
128
+ Kward gives each command the terminal, forwards keyboard input, and restores the shell prompt when the command exits. Interactive commands inherit your normal pager configuration, so commands such as `git log` can open `less` in full-screen mode. Kward suppresses Git paging only for noninteractive shell-agent and `capture` executions. It prints the submitted command but no PTY start message or exit-status summary. The line-oriented Git commands `git fetch`, `git ls-remote`, `git push`, `git remote`, and `git status` keep the shell prompt visible as a frozen display. Safe, line-oriented output from commands that did not read keyboard input is kept in the transient transcript view; full-screen and genuinely interactive output stays terminal-owned.
105
129
 
106
130
  Use `capture` inside `/shell` when you want ordinary, readable output in Kward's transcript area instead of direct terminal control:
107
131
 
108
132
  ```sh
109
133
  capture git status --short
110
- capture bundle exec ruby -Itest test/test_ekwsh.rb
134
+ capture bundle exec ruby -Itest test/test_kwsh.rb
111
135
  ```
112
136
 
113
- An `ekwsh` `capture` command:
137
+ An `kwsh` `capture` command:
114
138
 
115
139
  - does not receive keyboard input,
116
- - uses the timeout and output-size limit from `ekwsh.yml`,
140
+ - uses kwsh's built-in timeout and output-size limits,
117
141
  - preserves safe color and styling,
118
142
  - strips controls that could corrupt Kward's TUI,
119
143
  - can be cancelled with Ctrl+C.
@@ -139,9 +163,9 @@ Each Kward tab owns its `/shell` state. Switching away and back restores that ta
139
163
  - runtime aliases,
140
164
  - shell prompt and transcript view.
141
165
 
142
- Shell commands use a separate, workspace-scoped history rather than the normal chat-prompt history. Configure its size with `history_limit` in `ekwsh.yml`.
166
+ Shell commands use a separate, workspace-scoped history rather than the normal chat-prompt history. The shell-history limit is a built-in kwsh default.
143
167
 
144
- Kward's tab shortcuts work at the shell prompt and while a captured command is running. During an interactive command, the child owns every key; exit or interrupt it before switching Kward tabs.
168
+ Kward's tab shortcuts work at the shell prompt and while a shell command is running. Switching tabs detaches the running command instead of interrupting it; bounded output and completion state remain owned by the originating tab and are restored when you return. While detached, the command no longer receives terminal input. Explicit cancellation or shutdown still terminates detached work. Bounded output from shell-agent `?` turns is also retained in the tab's transient runtime view, so it is restored when you switch away and back without being added to session history. Ctrl+L clears this transient shell and shell-agent output.
145
169
 
146
170
  ## Completion
147
171
 
@@ -179,13 +203,14 @@ When several candidates match, repeated Tab presses cycle through them and wrap
179
203
 
180
204
  ## Built-ins
181
205
 
182
- `ekwsh` handles a small set of commands itself so their state can persist:
206
+ The persistent `/shell` process handles these commands in-session so their state can persist:
183
207
 
184
208
  | Built-in | What it does |
185
209
  | --- | --- |
186
210
  | `cd [dir]` | Change the shell directory. Supports `cd`, `cd -`, and relative paths. |
187
211
  | `pwd` | Print the shell directory. |
188
212
  | `export KEY[=value]` | Set an environment variable. `export` and `export -p` list variables. |
213
+ | `source <file>` / `. <file>` | Parse and apply aliases and exports from an rc file immediately. |
189
214
  | `unset KEY` | Remove an environment variable. |
190
215
  | `alias [name]` | List, inspect, or create aliases. |
191
216
  | `unalias name` / `unalias -a` | Remove aliases. |
@@ -196,81 +221,97 @@ When several candidates match, repeated Tab presses cycle through them and wrap
196
221
 
197
222
  Built-ins take precedence over aliases and executables.
198
223
 
199
- ## Configure ekwsh
224
+ ## Configure kwsh
200
225
 
201
- Global shell configuration lives at:
226
+ Global shell configuration lives in these optional rc files, loaded in order:
202
227
 
203
228
  ```text
204
- ~/.kward/ekwsh.yml
229
+ ~/.kward/kwshrc
230
+ ~/.kwshrc
231
+ ```
232
+
233
+ When `KWARD_CONFIG_PATH` selects another main config file, Kward reads the first file beside that config file instead. The later file overrides earlier aliases and exported variables with the same names. The rc format currently supports declarative `alias`, `export`, and `source` directives:
234
+
235
+ ```sh
236
+ alias ll='ls -la'
237
+ alias gs="git status --short"
238
+ export BUNDLE_WITHOUT=production
239
+ export PATH="$HOME/bin:$PATH"
240
+ source ~/.kward/kwsh-aliases
205
241
  ```
206
242
 
207
- When `KWARD_CONFIG_PATH` selects another main config file, Kward reads `ekwsh.yml` from the same directory.
243
+ `source` reads another rc file without executing it; relative paths in rc-file directives are resolved from the file containing the directive. When entered as a `/shell` builtin, the path is resolved from the current shell directory and its aliases and exports are applied immediately, without restarting Kward. Unsupported shell scripting is ignored for now.
208
244
 
209
- A practical configuration might look like this:
245
+ The transient shell assistant normally follows the active conversation's model and reasoning effort. Configure it in the main JSON file:
210
246
 
211
- ```yaml
212
- shell: /bin/sh
213
- timeout_seconds: 300
214
- max_output_bytes: 1048576
215
- history_limit: 1000
247
+ ```json
248
+ {
249
+ "shell": {
250
+ "agent": {
251
+ "provider": "openrouter",
252
+ "model": "openai/gpt-5.6-sol",
253
+ "reasoning_effort": "none"
254
+ }
255
+ }
256
+ }
257
+ ```
258
+
259
+ The optional `provider` uses the lowercase configuration IDs listed in [Model providers](providers.md). When omitted, the shell assistant follows the active conversation. If a provider is explicitly configured without a model or reasoning effort, Kward uses that provider's defaults instead of inheriting the active conversation's values.
216
260
 
217
- env:
218
- FORCE_COLOR: "1"
219
- BUNDLE_WITHOUT: "production"
220
- RAILS_ENV: "test"
261
+ Override those settings with environment variables:
221
262
 
222
- aliases:
223
- ll: "ls -la"
224
- gs: "git status --short"
225
- gd: "git diff --color=always"
226
- glog: "git log --decorate --stat --graph"
227
- be: "bundle exec"
228
- t: "bundle exec ruby -Itest"
263
+ ```sh
264
+ export KWSH_PROVIDER="openrouter"
265
+ export KWSH_MODE="openai/gpt-5.6-sol"
266
+ export KWSH_REASONING="none"
229
267
  ```
230
268
 
231
- ### Settings
269
+ Environment variables take precedence over the JSON settings, and empty values are ignored.
270
+
271
+ ### Runtime defaults
272
+
273
+ These runtime settings are built into kwsh and are not configurable through rc files:
232
274
 
233
275
  | Setting | Default | What it does |
234
276
  | --- | --- | --- |
235
- | `shell` | `/bin/sh` | POSIX-compatible shell used with `-c`. It must be an absolute executable path. |
236
- | `timeout_seconds` | `300` | Maximum runtime for one captured command. |
237
- | `max_output_bytes` | `1048576` | Maximum output retained for one captured command. |
277
+ | `shell` | `/bin/sh` | Absolute executable path for the persistent `/shell` process and interactive shell commands. |
278
+ | `timeout_seconds` | `300` | Maximum runtime for one captured or shell-agent command. |
279
+ | `max_output_bytes` | `1048576` | Maximum output retained for one captured or shell-agent command. |
238
280
  | `history_limit` | `1000` | Maximum shell-history entries per workspace. |
239
281
 
240
- Invalid or relative `shell` paths fall back to `/bin/sh`. These timeout and output limits apply only to `capture` inside `/shell`, not interactive commands or the separate `/capture` slash command.
282
+ Invalid or relative `shell` paths fall back to `/bin/sh`. These timeout and output limits apply to `capture` inside `/shell` and shell-agent commands, not user-owned interactive commands or the separate `/capture` slash command.
241
283
 
242
284
  ### Environment
243
285
 
244
- Configured `env` values are applied when `/shell` starts. Keys must be valid environment-variable names; nil values and invalid keys are ignored, and other values are converted to strings.
286
+ `export` values from rc files are applied when `/shell` starts and are also available to leading-`!` commands. Keys must be valid environment-variable names; invalid keys are ignored. Values support shell quoting and simple `$VAR`/`${VAR}` expansion.
245
287
 
246
288
  Kward also supplies conservative terminal defaults:
247
289
 
248
290
  ```sh
249
- CLICOLOR=1
250
- COLORTERM=truecolor
251
- TERM=xterm-256color # only when TERM is missing or dumb
291
+ export CLICOLOR=1
292
+ export COLORTERM=truecolor
293
+ export TERM=xterm-256color # only when TERM is missing or dumb
252
294
  ```
253
295
 
254
296
  It does not force color. Set `FORCE_COLOR`, `CLICOLOR_FORCE`, or a command-specific option such as `--color=always` when needed.
255
297
 
256
- When rbenv is available, Kward adds its shims and bin directories to `PATH` and supplies `RBENV_ROOT` if it was missing. This lets `ruby`, `bundle`, and `./exe/kward` use the selected Ruby without sourcing shell startup files.
298
+ When rbenv is available, Kward adds its shims and bin directories to `PATH` and supplies `RBENV_ROOT` if it was missing before starting `/shell`. The configured interactive shell may also load its normal startup files.
257
299
 
258
300
  ### Aliases
259
301
 
260
302
  Aliases replace the first command word once and append any remaining arguments:
261
303
 
262
- ```yaml
263
- aliases:
264
- ll: "ls -la"
265
- t: "bundle exec ruby -Itest"
304
+ ```sh
305
+ alias ll='ls -la'
306
+ alias t='bundle exec ruby -Itest'
266
307
  ```
267
308
 
268
309
  ```sh
269
310
  ll lib
270
311
  # runs: ls -la lib
271
312
 
272
- t test/test_ekwsh.rb
273
- # runs: bundle exec ruby -Itest test/test_ekwsh.rb
313
+ t test/test_kwsh.rb
314
+ # runs: bundle exec ruby -Itest test/test_kwsh.rb
274
315
  ```
275
316
 
276
317
  Configured aliases work inside `/shell` and after `!`. Aliases created with the `alias` built-in belong only to the current `/shell` session and are not shared with one-shot `!command` input.
@@ -281,17 +322,17 @@ Aliases are intentionally simple: they do not expand recursively and are not she
281
322
 
282
323
  ## Terminal output and safety
283
324
 
284
- Interactive commands write directly to your terminal so full-screen tools can work. When a command does not read keyboard input and emits only line-oriented text plus safe color sequences, Kward mirrors that output into the transient transcript view after the command exits. Other interactive output is not sanitized and may contain terminal control sequences, so run only commands you trust with terminal access.
325
+ Interactive commands write directly to your terminal so full-screen tools can work. When a command does not read keyboard input and emits only line-oriented text plus safe color sequences, Kward mirrors that output into the transient transcript view after the command exits. Other interactive output is not sanitized and may contain terminal control sequences, so run only commands you trust with terminal access. Safe bounded output is included in a shell-agent request only when you explicitly use `?`; it is never sent to the model for ordinary shell commands.
285
326
 
286
327
  Commands run with `capture` inside `/shell` preserve safe ANSI color and style sequences while removing cursor movement, clear-screen controls, title changes, alternate-screen controls, and similar sequences that could damage the TUI transcript.
287
328
 
288
329
  ## Limitations
289
330
 
290
- `ekwsh` manages shell-like state, but it is not a persistent login shell or terminal emulator:
331
+ The local `/shell` session is persistent, but it is not a complete terminal emulator or a remote-shell protocol:
291
332
 
292
- - each external command runs separately through the configured shell,
293
- - there is no job control; a stopped child is terminated rather than leaving the terminal stranded,
294
- - shell functions do not persist and shell startup files are not sourced,
295
- - there is no login-shell readline integration,
333
+ - the one-off `!command` and `/capture` workflows do not share `/shell` state,
334
+ - shell state is held by the live process and is not serialized across Kward restarts,
335
+ - a stopped or unresponsive command can still require Ctrl+C or shell-session cleanup,
296
336
  - full-screen terminal state is not retained after an interactive command exits,
297
- - safe line-oriented output may remain in the transient TUI transcript, but full-screen terminal state is not retained and no shell output becomes part of the AI conversation.
337
+ - while an interactive SSH session owns the terminal, Kward cannot safely intercept `?` or provide remote cwd/completion context; shell-agent prompting resumes after SSH exits,
338
+ - safe line-oriented output may remain in the transient TUI transcript; only bounded safe output from an explicit `?` request enters the transient shell-agent context and none of it is added to the normal session history.
data/doc/tabs.md CHANGED
@@ -39,6 +39,8 @@ A normal session tab can be activated in a linked Git worktree after you have re
39
39
 
40
40
  ```text
41
41
  /tab worktree
42
+ # or
43
+ /worktree
42
44
  ```
43
45
 
44
46
  When enabled, Kward keeps the same tab and transcript but rebuilds its agent against a new worktree. The tab label includes the worktree branch. The worktree is created from `HEAD`, so Kward warns when the original workspace is dirty and leaves those existing changes in the original checkout; it does not copy them automatically. `/tab worktree activate` is an explicit alias for the same action.
@@ -98,6 +100,7 @@ Tabs keep the conversations separate, so context from one tab does not automatic
98
100
  | `/tab worktree merge` | Merge the current worktree branch into the branch checked out in its original workspace |
99
101
  | `/tab worktree merge abort` | Abort a conflicted worktree merge in the original workspace |
100
102
  | `/tab worktree remove` | Remove a clean linked worktree and keep its branch |
103
+ | `/worktree …` | Alias for `/tab worktree …` on the active tab |
101
104
  | `/tab move left` | Move the current tab one slot left |
102
105
  | `/tab move right` | Move the current tab one slot right |
103
106
  | `/tab move <number>` | Move the current tab to a numbered position |
@@ -10,14 +10,24 @@ This page covers common issues and how to diagnose them. When something is not w
10
10
  kward doctor
11
11
  ```
12
12
 
13
- It checks that your config file is readable and valid JSON, that the config and session directories are writable, that the workspace exists, which provider and model are active, and which credentials are configured. Use it as the first diagnostic step for any unexpected behavior.
13
+ It checks that your config file is readable and valid JSON, that the config and session directories are writable, that the workspace exists, which provider and model are active, and which credentials are configured. Core check failures produce a nonzero exit status, so the command can also guard setup scripts. Optional features such as Pan are reported separately and do not make an otherwise healthy setup fail.
14
14
 
15
- `kward auth status` gives a focused view of credentials only, also without printing secrets:
15
+ `kward auth status` gives a focused view of configured credentials only, also without printing secrets:
16
16
 
17
17
  ```bash
18
18
  kward auth status
19
19
  ```
20
20
 
21
+ Use `kward auth status --all` when you also want to see every unconfigured provider.
22
+
23
+ Kward keeps normal command failures concise. To include a Ruby backtrace while diagnosing an unexpected failure, rerun the command with debug errors enabled:
24
+
25
+ ```bash
26
+ KWARD_DEBUG=1 kward "Repeat the failing task"
27
+ ```
28
+
29
+ Backtraces can include local paths and implementation details. Review them before sharing them publicly.
30
+
21
31
  ## Authentication errors and token expiration
22
32
 
23
33
  OAuth tokens expire. Kward refreshes access tokens automatically when a refresh token is available, but if the refresh token is missing or expired, requests fail with a provider-specific error:
data/doc/usage.md CHANGED
@@ -66,7 +66,7 @@ Inside interactive mode, ask Kward to run a command:
66
66
  Run the focused test for the CLI status command.
67
67
  ```
68
68
 
69
- Or run an interactive PTY command yourself from the composer by prefixing it with `!`. Aliases configured in `ekwsh.yml` are expanded here too:
69
+ Or run an interactive PTY command yourself from the composer by prefixing it with `!`. Aliases configured in `kwshrc` are expanded here too:
70
70
 
71
71
  ```text
72
72
  !git status --short
@@ -78,7 +78,7 @@ For several commands, enter the embedded Kward shell:
78
78
  /shell
79
79
  ```
80
80
 
81
- `/shell` opens `ekwsh`, a Kward-native command mode that preserves state such as the current directory, environment variables, and aliases between commands. External commands receive an interactive PTY by default, so `git log`, `less`, Vim, SSH, and REPLs work without a prefix. Use `capture <command>` inside `/shell` or `/capture <command>` from the normal composer for bounded, transcript-friendly output. See [Embedded shell](shell.md) for built-ins, completion, configuration, ANSI handling, PTY passthrough, and limitations.
81
+ `/shell` opens `kwsh`, a Kward-native command mode with one persistent local shell process. It preserves state such as the current directory, environment variables, functions, and aliases between commands. Prefix a line with `?` inside `/shell` to ask a transient shell assistant about the latest output, execute an explicit state change, or prepare a command without running it. External commands receive an interactive PTY by default, so `git log`, `less`, Vim, SSH, and REPLs work without a prefix. Use `capture <command>` inside `/shell` or `/capture <command>` from the normal composer for bounded, transcript-friendly output. See [Embedded shell](shell.md) for built-ins, completion, configuration, ANSI handling, PTY passthrough, and limitations.
82
82
 
83
83
  ## Shell commands
84
84
 
@@ -123,7 +123,7 @@ Slash commands run local actions in the current session. Most do not send a prom
123
123
  | `/git` | review uncommitted changes, stage files, and commit. |
124
124
  | `/diff` | open the file changes recorded in the current session. |
125
125
  | `/files` | browse project files in a nested tree and open them in the editor. |
126
- | `/shell` | run stateful workspace commands with interactive PTY support. |
126
+ | `/shell` | run stateful workspace commands with interactive PTY support and ask the shell agent with `?`. |
127
127
  | `/capture <command>` | run a bounded one-shot command and retain its output in the transcript. |
128
128
  | `/pty <command>` | explicitly hand the terminal to an interactive command; retained for compatibility with existing workflows. |
129
129
  | `/settings` | configure models, accounts, memory, interface, tools, context, personalization, and logging. |
@@ -143,6 +143,7 @@ Slash commands run local actions in the current session. Most do not send a prom
143
143
  | `/tab worktree merge` | merge a clean worktree branch into the branch checked out in its original workspace. |
144
144
  | `/tab worktree merge abort` | abort a conflicted worktree merge in the original workspace. |
145
145
  | `/tab worktree remove` | remove a clean linked worktree while keeping its branch. |
146
+ | `/worktree …` | alias for `/tab worktree …` on the active tab. |
146
147
  | `/session` | open the saved sessions picker or continue a previous session by path. |
147
148
  | `/resume` | alias for `/session`. |
148
149
  | `/session name <name>` | name or clear the current session. |
@@ -159,7 +160,7 @@ Slash commands run local actions in the current session. Most do not send a prom
159
160
  | `/skill <name>` | activate a configured skill explicitly for the current session. |
160
161
  | `/stats [range]` | summarize enabled local telemetry. |
161
162
  | `/hooks ...` | inspect, diagnose, trust, or untrust lifecycle hooks. |
162
- | `/scratchpad [text|markdown|ruby]` | open an unsaved editor buffer. |
163
+ | `/scratchpad [language|help]` | open an unsaved, syntax-highlighted editor buffer; `/scratchpad` alone opens plain text. |
163
164
  | `/redraw` | fix terminal drawing after resize or glitches. |
164
165
  | `/reload` | reload installed plugins. |
165
166
  | `/exit` | leave Kward. |
@@ -262,13 +263,13 @@ This screenshot shows the broken layout. Find the likely CSS issue.
262
263
 
263
264
  ## Pan mode
264
265
 
265
- Pan mode starts a mobile-friendly LAN web UI:
266
+ Pan mode starts a mobile-friendly local web UI:
266
267
 
267
268
  ```bash
268
269
  kward --working-directory ~/code/project pan
269
270
  ```
270
271
 
271
- Use it only on trusted networks. It exposes file, shell, web, and configured extension tools through a browser UI and requires credentials configured in `config.json`. Pan saves conversations to the normal workspace session store; its session drawer can create, resume, rename, and delete sessions. Session changes are disabled while turns are active or queued. See [Pan mode](pan.md) for setup, browser workflows, security, and limitations.
272
+ Pan binds to loopback by default. Exposing it to a trusted LAN requires an explicit host setting and prints a warning because Pan uses plain HTTP. It exposes file, shell, web, and configured extension tools through a browser UI and requires a configured username plus a config or environment password. Pan saves conversations to the normal workspace session store; its session drawer can create, resume, rename, and delete sessions. Session changes are disabled while turns are active or queued. See [Pan mode](pan.md) for setup, browser workflows, security, and limitations.
272
273
 
273
274
  ## RPC backend
274
275
 
data/kward.gemspec CHANGED
@@ -6,16 +6,16 @@ Gem::Specification.new do |spec|
6
6
  spec.authors = ["Kai Wood"]
7
7
  spec.email = ["kai.wood@icloud.com"]
8
8
 
9
- spec.summary = "An extendable Ruby CLI coding agent."
10
- spec.description = "Kward is a Ruby CLI coding agent with local workspace tools, configurable prompts, web search, sessions, and an experimental JSON-RPC backend."
11
- spec.homepage = "https://github.com/kaiwood/kward"
9
+ spec.summary = "An extensible Ruby coding agent for your terminal."
10
+ spec.description = "Kward is an extensible Ruby coding agent with workspace tools, resumable sessions, multiple model providers, a local browser UI, and JSON-RPC integrations."
11
+ spec.homepage = "https://kaiwood.github.io/kward/"
12
12
  spec.license = "MIT"
13
13
  spec.required_ruby_version = ">= 3.4"
14
14
 
15
15
  spec.metadata["rubygems_mfa_required"] = "true"
16
16
  spec.metadata["source_code_uri"] = "https://github.com/kaiwood/kward"
17
17
  spec.metadata["changelog_uri"] = "https://github.com/kaiwood/kward/blob/main/CHANGELOG.md"
18
- spec.metadata["documentation_uri"] = "https://github.com/kaiwood/kward#readme"
18
+ spec.metadata["documentation_uri"] = "https://kaiwood.github.io/kward/"
19
19
  spec.metadata["bug_tracker_uri"] = "https://github.com/kaiwood/kward/issues"
20
20
 
21
21
  spec.files = Dir.chdir(__dir__) do
@@ -34,4 +34,5 @@ Gem::Specification.new do |spec|
34
34
  spec.add_dependency "tty-prompt"
35
35
  spec.add_dependency "tty-reader"
36
36
  spec.add_dependency "tty-screen"
37
+ spec.add_dependency "unicode-display_width"
37
38
  end
data/lib/kward/agent.rb CHANGED
@@ -28,10 +28,11 @@ module Kward
28
28
  # lowest layer that owns the behavior, and use `Agent` only for cross-step turn
29
29
  # coordination.
30
30
  class Agent
31
- def initialize(client:, tool_registry: ToolRegistry.new, conversation: Conversation.new, telemetry_logger: nil, warning_sink: nil, hook_manager: nil, hook_context: nil)
31
+ def initialize(client:, tool_registry: ToolRegistry.new, conversation: Conversation.new, telemetry_logger: nil, warning_sink: nil, hook_manager: nil, hook_context: nil, strict_provider: false)
32
32
  @client = client
33
33
  @tool_registry = tool_registry
34
34
  @conversation = conversation
35
+ @strict_provider = strict_provider == true
35
36
  @warning_sink = warning_sink
36
37
  @telemetry_logger = telemetry_logger || TelemetryLogger.new(warning_sink: warning_sink)
37
38
  @hook_manager = hook_manager
@@ -84,6 +85,7 @@ module Kward
84
85
  end
85
86
  loop do
86
87
  cancellation&.raise_if_cancelled!
88
+ auto_compact_if_needed
87
89
  begin
88
90
  message = chat(on_reasoning_delta: on_reasoning_delta, on_retry: on_retry, cancellation: cancellation, steering: steering, options: options, tool_registry: tool_registry) do |event|
89
91
  yield event if block_given?
@@ -240,7 +242,8 @@ module Kward
240
242
  tools: registry.schemas,
241
243
  provider: options[:provider] || @conversation.provider,
242
244
  model: options[:model] || @conversation.model,
243
- reasoning: options[:reasoning] || @conversation.reasoning_effort
245
+ reasoning: options[:reasoning] || @conversation.reasoning_effort,
246
+ provider_required: @strict_provider
244
247
  }
245
248
  before = run_hook("model_request_before", payload: request)
246
249
  request = DeepCopy.merge(request, before.payload) if before.decision.modify?
@@ -260,7 +263,8 @@ module Kward
260
263
  steering: steering,
261
264
  provider: request[:provider] || request["provider"],
262
265
  model: request[:model] || request["model"],
263
- reasoning: request[:reasoning] || request["reasoning"]
266
+ reasoning: request[:reasoning] || request["reasoning"],
267
+ provider_required: request[:provider_required] || request["provider_required"]
264
268
  }
265
269
  )
266
270
  run_hook("model_response_after_parse", payload: { message: response })