Class: Pikuri::Code::Extension

Inherits:
Object
  • Object
show all
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").

Returns:

  • (String)
<<~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.

Returns:

  • (String)
'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.

Returns:

  • (String)
'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

Constructor Details

#initialize(filesystem:, confirmer:, sandbox: Bash::Sandbox::NONE, read_only: nil, passive_detector: nil) ⇒ Extension

Returns a new instance of Extension.

Parameters:

  • filesystem (Pikuri::Workspace::Filesystem)
  • confirmer (Pikuri::Workspace::Confirmer)
  • sandbox (Pikuri::Code::Bash::Sandbox) (defaults to: Bash::Sandbox::NONE)
  • read_only (Pikuri::Workspace::ReadOnly, nil) (defaults to: nil)

    the shared read-only flag (same instance handed to Workspace::Extension). When present, plan-mode tools + prompt + reminder + event are wired; nil leaves plan mode unavailable.

  • passive_detector (#passive?, nil) (defaults to: nil)

    passive-command pre-approval predicate handed to Bash only (Bash::PassiveCommandDetector); the exit_plan_mode gate always uses the bare confirmer.



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.

Parameters:

  • ctx (Pikuri::Agent::ExtensionContext)


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.

Parameters:

  • c (Pikuri::Agent::Configurator)


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.

Parameters:

  • _ctx (Pikuri::Agent::ExtensionContext)
  • _content (String)

Returns:

  • (String, nil)


157
158
159
160
161
# File 'lib/pikuri/code/extension.rb', line 157

def on_user_message(_ctx, _content)
  return nil unless @read_only&.active?

  PLAN_MODE_REMINDER
end

#system_prompt_snippetsArray<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.

Returns:

  • (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.



134
# File 'lib/pikuri/code/extension.rb', line 134

def system_prompt_snippets = @read_only ? [PLAN_MODE_PROMPT] : []