Class: Pikuri::Code::Extension
- Inherits:
-
Object
- Object
- Pikuri::Code::Extension
- Includes:
- Agent::Extension
- Defined in:
- lib/pikuri/code/extension.rb
Overview
An Agent::Extension wiring pikuri-code's own tools: Bash, the plan-mode pair (EnterPlanMode / ExitPlanMode), and GitClone as a sub-agent-only tool. Owns only this surface — callers add Skill / Tasks / Mcp / SubAgent extensions themselves.
Plan mode is opt-in via the read-only flag
The plan-mode tools, PLAN_MODE_PROMPT, and the per-turn reminder are
wired only when the host passes a Workspace::ReadOnly flag via
read_only: — and it must be the same instance handed to the
file-editing tools (via Workspace::Extension's read_only:), so
their refusal and this reminder stay in lockstep. With no flag, Bash +
GitClone still install but plan mode is unavailable. The transition is a
PlanModeChanged domain event emitted from #bind — a pikuri-code
concern, not a core event (the loop never reads plan mode).
Usage
read_only = Pikuri::Workspace::ReadOnly.new
Pikuri::Agent.new(...) do |c|
c.add_extension Pikuri::Workspace::Extension.new(
filesystem:, confirmer:, read_only: read_only
)
c.add_extension Pikuri::Code::Extension.new(
filesystem: filesystem,
confirmer: confirmer,
sandbox: sandbox,
read_only: read_only
)
end
Constant Summary collapse
- TOOL_CLASSES =
Tool classes whose pre-registration is rejected — the extension is the single owner of these, so a manually pre-registered copy would shadow the wiring done here.
[Bash, EnterPlanMode, ExitPlanMode].freeze
- PLAN_MODE_PROMPT =
Static plan-mode explainer, appended once (and so prefix-cached) when plan mode is wired: mechanics + active-behavior contract, paid once. The per-turn PLAN_MODE_REMINDER is then a one-line re-assertion at the uncached tail. An extension-owned snippet that names its own tools (the persona/extension carve-out to "no tool names in a main prompt").
<<~PROMPT <plan_mode> You can work in plan mode — a read-only posture for researching and designing a change before writing any of it. Reach for enter_plan_mode before a non-trivial change (new features, multi-file edits, refactors, anything with several viable approaches); the user can also turn plan mode on directly. While it is active you will see a short reminder each turn. While in plan mode: - The file-editing tools are disabled and will refuse — do not try to create, overwrite, or edit files. - Explore freely: read files, search the codebase, and run read-only shell commands. - Work out a concrete plan — which files change, the change to each, and any trade-offs — and write it out. - Call exit_plan_mode with that plan to present it for approval. Only start implementing once the user approves. - If a requirement is genuinely ambiguous, ask the user directly rather than guessing. </plan_mode> PROMPT
- PLAN_MODE_REMINDER =
One-line per-turn re-assertion, injected as a reference block while plan mode is active (see #on_user_message). Deliberately tiny — it lands at the uncached tail, while the substantive prose lives cached in PLAN_MODE_PROMPT. Inlined rather than in
prompts/because it's one line. 'Reminder: you are in plan mode (read-only) — research, design, ' \ 'and brainstorm, but do not edit files.'
- PLAN_MODE_READONLY_MESSAGE =
Reason clause for a write/edit refusal when plan mode is why the workspace is read-only. Handed to Workspace::ReadOnly (via the host) so a refused Write/Edit names the recovery path (+exit_plan_mode+) instead of the generic Workspace::ReadOnly::DEFAULT_MESSAGE, which can't assume plan mode is the cause.
'you are in plan mode (read-only). Add this change to your plan and ' \ 'call exit_plan_mode to present it; you can apply it once the user approves.'
Instance Method Summary collapse
-
#bind(ctx) ⇒ void
Bridge plan-mode transitions onto the listener stream so UI chrome can render the posture — both model-driven (EnterPlanMode / ExitPlanMode) and host-driven flips.
- #configure(c) ⇒ void
-
#initialize(filesystem:, confirmer:, sandbox: Bash::Sandbox::NONE, read_only: nil, passive_detector: nil) ⇒ Extension
constructor
A new instance of Extension.
-
#on_user_message(_ctx, _content) ⇒ String?
Inject PLAN_MODE_REMINDER on every turn plan mode is active — the soft half of the defense, re-asserting the posture even when the host engaged it and the model never saw a tool call.
-
#system_prompt_snippets ⇒ Array<String>
The plan-mode prompt, only when the read-only flag is wired (so the prompt and the plan-mode tools appear together); otherwise none.
Constructor Details
#initialize(filesystem:, confirmer:, sandbox: Bash::Sandbox::NONE, read_only: nil, passive_detector: nil) ⇒ Extension
Returns a new instance of Extension.
96 97 98 99 100 101 102 103 |
# File 'lib/pikuri/code/extension.rb', line 96 def initialize(filesystem:, confirmer:, sandbox: Bash::Sandbox::NONE, read_only: nil, passive_detector: nil) @filesystem = filesystem @confirmer = confirmer @sandbox = sandbox @read_only = read_only @passive_detector = passive_detector end |
Instance Method Details
#bind(ctx) ⇒ void
This method returns an undefined value.
Bridge plan-mode transitions onto the listener stream so UI chrome can render the posture — both model-driven (Pikuri::Code::EnterPlanMode / Pikuri::Code::ExitPlanMode) and host-driven flips. No-op when plan mode isn't wired.
142 143 144 145 146 147 |
# File 'lib/pikuri/code/extension.rb', line 142 def bind(ctx) return nil unless @read_only @read_only.on_change { |active| ctx.emit_event(PlanModeChanged.new(active: active)) } nil end |
#configure(c) ⇒ void
This method returns an undefined value.
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/pikuri/code/extension.rb', line 107 def configure(c) TOOL_CLASSES.each do |cls| if c.tools.any?(cls) raise "#{cls} cannot be pre-registered when adding Pikuri::Code::Extension" end end c.add_tool Bash.new(filesystem: @filesystem, confirmer: @confirmer, sandbox: @sandbox, passive_detector: @passive_detector) # Plan mode rides the host-owned read-only flag; only wire its # tools when one was supplied (see the class header). The prompt # half is contributed by {#system_prompt_snippets}. if @read_only c.add_tool EnterPlanMode.new(read_only: @read_only) c.add_tool ExitPlanMode.new(read_only: @read_only, confirmer: @confirmer) end # Sub-agent-only (never visible to the parent) — it exists solely for # the GIT_REPO_RESEARCHER persona. c.add_sub_agent_tool GitClone.new(filesystem: @filesystem) nil end |
#on_user_message(_ctx, _content) ⇒ String?
Inject PLAN_MODE_REMINDER on every turn plan mode is active — the
soft half of the defense, re-asserting the posture
even when the host engaged it and the model never saw a tool call.
nil (inject nothing) when plan mode is off or unwired.
157 158 159 160 161 |
# File 'lib/pikuri/code/extension.rb', line 157 def (_ctx, _content) return nil unless @read_only&.active? PLAN_MODE_REMINDER end |
#system_prompt_snippets ⇒ Array<String>
Returns the plan-mode prompt, only when the read-only flag is wired (so the prompt and the plan-mode tools appear together); otherwise none.
134 |
# File 'lib/pikuri/code/extension.rb', line 134 def system_prompt_snippets = @read_only ? [PLAN_MODE_PROMPT] : [] |