Class: Agent::Runner
- Inherits:
-
Object
- Object
- Agent::Runner
- 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
-
#card ⇒ Object
readonly
Returns the value of attribute card.
-
#column ⇒ Object
readonly
Returns the value of attribute column.
-
#run ⇒ Object
readonly
Returns the value of attribute run.
Class Method Summary collapse
Instance Method Summary collapse
-
#initialize(run) ⇒ Runner
constructor
A new instance of Runner.
- #resume(message, approve: false) ⇒ Object
- #start ⇒ Object
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
#card ⇒ Object (readonly)
Returns the value of attribute card.
51 52 53 |
# File 'app/services/agent/runner.rb', line 51 def card @card end |
#column ⇒ Object (readonly)
Returns the value of attribute column.
51 52 53 |
# File 'app/services/agent/runner.rb', line 51 def column @column end |
#run ⇒ Object (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, , approve: false) = new(run).resume(, 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(, 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.", (.present? ? "Notes from the user to fold in as you execute:\n\n#{}" : 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#{}\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#{}\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 |
#start ⇒ Object
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 |