Class: Card
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Card
- Includes:
- ModelLabeling
- Defined in:
- app/models/card.rb
Constant Summary collapse
- STATUSES =
%w[ draft discussing queued working needs_input blocked failed work_complete in_review changes_requested approved done archived ].freeze
- LEGAL_STATUSES =
Which statuses a card may hold while sitting in each column archetype. The column move is the trigger; this map keeps the state machine honest (§3).
{ "inbox" => %w[draft archived], "planning" => %w[draft discussing archived], "execution" => %w[queued working needs_input blocked failed work_complete archived], "review" => %w[in_review changes_requested approved archived], "terminal" => %w[done blocked archived] # blocked: merge gate refused (CI red/pending) }.freeze
- STATUS_GLYPHS =
Card-face status glyphs. Keyed on status, except
ready_for_approval?(a derived plan-park state, not a status) which pre-empts the ❓ below. { "working" => "⚡", "needs_input" => "❓", "failed" => "✖", "work_complete" => "✅", "done" => "✓", "queued" => "⏳", "discussing" => "💬", "in_review" => "👁", "approved" => "👍", "changes_requested" => "🔁" }.freeze
- ARCHIVE_RESTORE =
Where an unarchived card lands: back in its column, in that column's most inert legal status — nothing fires, no agent starts.
{ "inbox" => "draft", "planning" => "discussing", "execution" => "work_complete", "review" => "in_review", "terminal" => "done" }.freeze
- PERMISSION_MODES =
Per-card model/effort override (card #33). Nil fields mean "use the column default", so existing cards are unaffected. Read fresh at every segment spawn (start, restart, resume) by the runner and the planning assistant, so a change takes effect on the next segment — never on an already-running one. Permission override (nil = board default): "bypass" forces full autonomy for this card, "ask" forces the restricted file-tools mode.
["bypass", "ask"].freeze
Instance Method Summary collapse
- #assistant_cost ⇒ Object
-
#awaiting_assistant? ⇒ Boolean
Is the planning assistant expected to post next? True right after entering a planning column (kickoff inspection pending) or after a user message.
-
#compact_working? ⇒ Boolean
A technical "compact" journal is being (re)generated in the background (§card #34) — the AI-readable context a resuming agent reads to skip re-exploration.
-
#config_overridden? ⇒ Boolean
Does this card override either half of the column's "how much brain" pair? Drives the de-magic marker on the card face: the board must never hide that a card will spend on a model other than its column's default.
- #default_branch_name ⇒ Object
- #effective_effort ⇒ Object
- #effective_model ⇒ Object
-
#effective_model_label ⇒ Object
"Opus - High*" — the effective model label for card faces and footers, with a trailing * when overridden so the override is visible without opening the card.
- #effective_permission_bypass? ⇒ Boolean
-
#latest_progress ⇒ Object
Latest one-line progress event, shown live on the card face (§6).
- #log!(kind, actor: "system", run: nil, **payload) ⇒ Object
- #needs_attention? ⇒ Boolean
-
#planning_ready? ⇒ Boolean
Planning chip state (card #25): the assistant ends discussion by posting a "Ready for execution" brief — the SAME marker Agent::Runner promotes into worker briefings.
-
#ready_for_approval? ⇒ Boolean
The agent has proposed a plan and is parked waiting on the user's approve click — distinct from a genuine question.
- #running? ⇒ Boolean
-
#status_glyph ⇒ Object
Card-face glyph: a bell when a plan is awaiting approval, else the plain per-status mapping.
-
#summary_working? ⇒ Boolean
A customer-friendly summary is being (re)generated in the background (§card #35).
-
#thinking? ⇒ Boolean
Some AI is expected to write to this card imminently.
-
#to_param ⇒ Object
URLs speak card numbers, matching every other surface (header #N, branches, PR titles) — not database ids, which drift after deletions.
-
#total_cost ⇒ Object
Running tally across every run on the card — the closed-card cost footer (card #20).
- #total_output_tokens ⇒ Object
Methods included from ModelLabeling
#model_label_of, #model_short_of
Instance Method Details
#assistant_cost ⇒ Object
131 |
# File 'app/models/card.rb', line 131 def assistant_cost = ai_calls.where(kind: "assistant").sum(:cost) |
#awaiting_assistant? ⇒ Boolean
Is the planning assistant expected to post next? True right after entering a planning column (kickoff inspection pending) or after a user message.
135 136 137 138 139 140 |
# File 'app/models/card.rb', line 135 def awaiting_assistant? return false unless column.planning? && column.ai? last = events.where(kind: %w[user_message assistant_message error column_move]).order(:id).last return false if last&.payload&.[]("note") # a note-only message expects no reply last.present? && %w[user_message column_move].include?(last.kind) end |
#compact_working? ⇒ Boolean
A technical "compact" journal is being (re)generated in the background (§card #34) — the AI-readable context a resuming agent reads to skip re-exploration.
68 |
# File 'app/models/card.rb', line 68 def compact_working? = compact_status == "working" |
#config_overridden? ⇒ Boolean
Does this card override either half of the column's "how much brain" pair? Drives the de-magic marker on the card face: the board must never hide that a card will spend on a model other than its column's default.
113 |
# File 'app/models/card.rb', line 113 def config_overridden? = model.present? || effort.present? |
#default_branch_name ⇒ Object
160 161 162 |
# File 'app/models/card.rb', line 160 def default_branch_name "cardinal/#{number}-#{title.parameterize[0, 40]}" end |
#effective_effort ⇒ Object
108 |
# File 'app/models/card.rb', line 108 def effective_effort = effort.presence || column.effort |
#effective_model ⇒ Object
107 |
# File 'app/models/card.rb', line 107 def effective_model = model.presence || column.model |
#effective_model_label ⇒ Object
"Opus - High*" — the effective model label for card faces and footers, with a trailing * when overridden so the override is visible without opening the card. Nil when no model resolves (AI off / unset), matching Column#model_label.
118 119 120 121 122 |
# File 'app/models/card.rb', line 118 def effective_model_label label = model_label_of(effective_model, effective_effort) return if label.blank? config_overridden? ? "#{label}*" : label end |
#effective_permission_bypass? ⇒ Boolean
99 100 101 102 103 104 105 |
# File 'app/models/card.rb', line 99 def case when "bypass" then true when "ask" then false else board. end end |
#latest_progress ⇒ Object
Latest one-line progress event, shown live on the card face (§6).
87 88 89 |
# File 'app/models/card.rb', line 87 def latest_progress events.where(kind: "progress").last&.payload&.[]("text") end |
#log!(kind, actor: "system", run: nil, **payload) ⇒ Object
164 165 166 |
# File 'app/models/card.rb', line 164 def log!(kind, actor: "system", run: nil, **payload) events.create!(kind:, actor:, run:, payload:) end |
#needs_attention? ⇒ Boolean
61 |
# File 'app/models/card.rb', line 61 def needs_attention? = %w[needs_input blocked failed work_complete].include?(status) |
#planning_ready? ⇒ Boolean
Planning chip state (card #25): the assistant ends discussion by posting a "Ready for execution" brief — the SAME marker Agent::Runner promotes into worker briefings. When that's the assistant's latest word, the card face flips from "replied" to "ready".
146 147 148 149 |
# File 'app/models/card.rb', line 146 def planning_ready? discussing? && events.where(kind: "assistant_message").order(:id).last&.text.to_s.match?(/ready for execution/i) end |
#ready_for_approval? ⇒ Boolean
The agent has proposed a plan and is parked waiting on the user's approve
click — distinct from a genuine question. Same underlying needs_input
status; the plan-phase park is the signal (mirrors the work panel, §detail).
The status guard means the run query only fires for already-parked cards.
76 77 78 |
# File 'app/models/card.rb', line 76 def ready_for_approval? needs_input? && runs.needs_input.order(:id).last&.phase == "plan" end |
#running? ⇒ Boolean
70 |
# File 'app/models/card.rb', line 70 def running? = %w[queued working needs_input].include?(status) |
#status_glyph ⇒ Object
Card-face glyph: a bell when a plan is awaiting approval, else the plain per-status mapping.
82 83 84 |
# File 'app/models/card.rb', line 82 def status_glyph ready_for_approval? ? "🔔" : STATUS_GLYPHS[status] end |
#summary_working? ⇒ Boolean
A customer-friendly summary is being (re)generated in the background (§card #35).
64 |
# File 'app/models/card.rb', line 64 def summary_working? = summary_status == "working" |
#thinking? ⇒ Boolean
Some AI is expected to write to this card imminently.
152 153 154 |
# File 'app/models/card.rb', line 152 def thinking? awaiting_assistant? || working? end |
#to_param ⇒ Object
URLs speak card numbers, matching every other surface (header #N, branches, PR titles) — not database ids, which drift after deletions.
158 |
# File 'app/models/card.rb', line 158 def to_param = number.to_s |
#total_cost ⇒ Object
Running tally across every run on the card — the closed-card cost footer (card #20). Sums stopped/restarted segments so the total reflects real spend. Honest money: worker runs PLUS every one-shot call made on this card's behalf (planning assistant, ai_task, summary/compact) — see AiCall.
128 |
# File 'app/models/card.rb', line 128 def total_cost = runs.sum(:cost) + ai_calls.sum(:cost) |
#total_output_tokens ⇒ Object
129 |
# File 'app/models/card.rb', line 129 def total_output_tokens = runs.sum(:output_tokens) + ai_calls.sum(:output_tokens) |