Class: Agent::Runner

Inherits:
Object
  • Object
show all
Defined in:
app/services/agent/runner.rb

Overview

Drives one Run through its phases (cardinal.md §4, §11, §17):

start  → plan phase (read-only, --permission-mode plan) when the column
       requires approval, else straight to execute
park   → plan_proposed or QUESTION: → run + card go needs_input
resume → same claude session (--resume) with the user's answer,
       approval, or plan feedback
finish → push branch, ensure draft PR, final report, work_complete

The subprocess is the Claude Agent runtime (claude -p, stream-json). Heartbeats are written while streaming; RunSweeper reaps silent runs.

Constant Summary collapse

STRIP_ENV =
%w[ANTHROPIC_API_KEY CLAUDECODE CLAUDE_CODE_ENTRYPOINT GH_TOKEN GITHUB_TOKEN].freeze
HEARTBEAT_EVERY =

seconds

10
PLAN_TURNS =
20
DEFAULT_EXECUTE_TURNS =

turn caps are runaway guards, not work limits

80
EXECUTE_RULES =
<<~RULES.freeze
  ## Rules
  - You have the FULL toolset now: shell (bash, git), file editing, everything. Run
    commands yourself — never ask who should run them.
  - Work only inside this repository checkout (you are already on the card's branch).
  - If the branch conflicts with origin's default branch, merge it into the card
    branch yourself and resolve the conflicts as part of the work.
  - Commit your work as you go with clear messages. Do NOT push — the runner pushes for you.
  - Stay strictly within the card's scope. Prefer the smallest reasonable interpretation and note assumptions.
  - If you are blocked on a decision only the user can make, output a single line starting with
    "QUESTION:" followed by the question, then stop immediately. Do not guess on genuinely ambiguous choices.
  - Finish with a concise report: what you did, what to check, any open questions.
RULES
RESTRICTED_EXECUTE_RULES =

Column shell access OFF: the agent can read, search, and edit — nothing else. Enforced by the CLI tool list, not just these words.

<<~RULES.freeze
  ## Rules
  - You have FILE TOOLS ONLY: read, search, edit, write. You cannot run shell
    commands or git — do not attempt to; Cardinal commits and pushes your edits for you.
  - Work only inside this repository checkout (you are already on the card's branch).
  - Stay strictly within the card's scope. Prefer the smallest reasonable interpretation and note assumptions.
  - If something must be RUN to finish the job (tests, generators, installs), do the file
    work, then list the exact commands in your final report for the user to run.
  - If you are blocked on a decision only the user can make, output a single line starting with
    "QUESTION:" followed by the question, then stop immediately. Do not guess on genuinely ambiguous choices.
  - Finish with a concise report: what you did, what to check, any open questions.
RULES

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(run) ⇒ Runner

Returns a new instance of Runner.



53
54
55
56
57
# File 'app/services/agent/runner.rb', line 53

def initialize(run)
  @run = run
  @card = run.card
  @column = card.column
end

Instance Attribute Details

#cardObject (readonly)

Returns the value of attribute card.



51
52
53
# File 'app/services/agent/runner.rb', line 51

def card
  @card
end

#columnObject (readonly)

Returns the value of attribute column.



51
52
53
# File 'app/services/agent/runner.rb', line 51

def column
  @column
end

#runObject (readonly)

Returns the value of attribute run.



51
52
53
# File 'app/services/agent/runner.rb', line 51

def run
  @run
end

Class Method Details

.resume(run, message, approve: false) ⇒ Object



49
# File 'app/services/agent/runner.rb', line 49

def self.resume(run, message, approve: false) = new(run).resume(message, approve: approve)

.start(run) ⇒ Object



48
# File 'app/services/agent/runner.rb', line 48

def self.start(run) = new(run).start

Instance Method Details

#resume(message, approve: false) ⇒ Object



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'app/services/agent/runner.rb', line 73

def resume(message, approve: false)
  begin_segment!
  if run.phase == "plan" && approve
    run.update!(phase: "execute")
    # Approve-with-notes: an approval may carry final adjustments — fold
    # them into the execute prompt instead of silently dropping them.
    prompt = ["Your plan is approved — execute it now.",
              (message.present? ? "Notes from the user to fold in as you execute:\n\n#{message}" : nil),
              execute_rules].compact.join("\n\n")
    stream_agent(prompt: prompt, mode: "execute", resuming: true)
  elsif run.phase == "plan"
    stream_agent(prompt: "Feedback on your plan:\n\n#{message}\n\nRevise the plan accordingly, present it, and stop again for approval. Stay in read-only mode.",
                 mode: "plan", resuming: true)
  else
    stream_agent(prompt: "Answer from the user:\n\n#{message}\n\nContinue the work. The same rules apply (commit, don't push, QUESTION: if blocked again, final report when done).",
                 mode: "execute", resuming: true)
  end
rescue => e
  record_failure(e)
ensure
  column.kick_queue if column.execution?
end

#startObject



59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'app/services/agent/runner.rb', line 59

def start
  begin_segment!(first: true)
  if plan_gated?
    run.update!(phase: "plan")
    stream_agent(prompt: plan_prompt, mode: "plan")
  else
    stream_agent(prompt: briefing_prompt, mode: "execute")
  end
rescue => e
  record_failure(e)
ensure
  column.kick_queue if column.execution?
end