Class: Column
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Column
- Includes:
- ModelLabeling
- Defined in:
- app/models/column.rb
Constant Summary collapse
- ARCHETYPES =
%w[inbox planning execution review terminal].freeze
- ARCHETYPE_TEMPLATES =
Archetypes are TEMPLATES, not magic: choosing one stamps concrete, editable values into the policy fields. Nothing falls back to these at runtime — what the gear modal shows is everything there is.
{ "inbox" => {}, "planning" => { "on_entry" => [{ "action" => "assistant_greeting" }], "on_entry_text" => "The planning assistant reads the card and opens the discussion.", "instructions" => "Drive toward crisp acceptance criteria. Open with the 2-3 sharpest questions." }, "execution" => { "on_entry" => [{ "action" => "start_agent_run" }], "on_entry_text" => "Assign a dedicated worker agent to the card and start a run." }, "review" => {}, "terminal" => { "on_entry" => [{ "action" => "merge_pr" }], "on_entry_text" => "Merge the card's PR and ship it." } }.freeze
- FOOTER_COMPUTES =
Aggregations a footer row may compute over the column's cards (card #18). A compute key not listed here renders blank, so config that outruns the code degrades gracefully instead of erroring.
%w[sum_cost sum_tokens count_cards model].freeze
- BUILT_IN_ROLES =
The built-in role contract for AI servicing this archetype — shown read-only in the gear modal so the Instructions field is understood as ADDING to this, never replacing it. Enforced in code, not editable.
{ "planning" => "Plans only, never implements: read-only tools (physically cannot change files), " \ "drives toward a Ready-for-execution brief, and hands off — approval means " \ "\"finalize the brief\", not \"do it\".", "execution" => "Full toolset in an isolated checkout of the card's branch. Commits as it goes but " \ "never pushes (the runner pushes); merges the default branch itself on conflict; " \ "parks with a QUESTION: when genuinely blocked; ends with a final report." }.freeze
- AI_MODES =
What "Use AI" concretely means here — the §5 tier distinction, visible.
{ "planning" => "a shared planning assistant joins each card's conversation", "execution" => "a dedicated worker agent is assigned to each card", "review" => "allow AI on-entry rules (ai_task) in this column", "terminal" => "allow AI on-entry rules (ai_task) in this column" }.freeze
Instance Method Summary collapse
-
#accepts?(source_column) ⇒ Boolean
Which columns may move cards INTO this one (§ accept policy, card #15).
-
#ai? ⇒ Boolean
Does any AI service this column? Explicit per-column switch (default ON for back-compat); the inbox/Tasks intake is never AI, unconditionally.
- #ai_mode_description ⇒ Object
- #archetype_template ⇒ Object
- #at_wip_limit? ⇒ Boolean
-
#at_wip_limit_exceeded? ⇒ Boolean
Post-claim variant (§ races): a starter first claims working atomically, THEN checks — over-subscription reads as strictly more running than allowed.
- #built_in_role ⇒ Object
-
#drag_hint ⇒ Object
One-line consequence shown while dragging a card over this column (§14.1).
-
#footer_rows ⇒ Object
Rows rendered under the cards (card #18).
-
#footer_value(compute) ⇒ Object
Aggregate a single footer row over the runs/cards in this column (card #18).
-
#kick_queue ⇒ Object
Start the next queued card when a run slot frees up.
-
#model_label ⇒ Object
"Opus - High" — human label for cost footers (card #20).
-
#model_short ⇒ Object
"claude-sonnet-4-6" → "sonnet", for compact chips on card faces.
- #queued_count ⇒ Object
- #running_count ⇒ Object
-
#safe_color ⇒ Object
Only ever emit a validated hex color into inline styles.
-
#seed_archetype_template ⇒ Object
Stamp template values into any policy field the creator left blank.
-
#shell_access? ⇒ Boolean
Worker shell access (execution columns).
Methods included from ModelLabeling
#model_label_of, #model_short_of
Instance Method Details
#accepts?(source_column) ⇒ Boolean
Which columns may move cards INTO this one (§ accept policy, card #15). Stored as an array of column-id strings. EXPLICIT ONLY: an empty list means this column accepts from nowhere — there is no permissive default.
73 74 75 |
# File 'app/models/column.rb', line 73 def accepts?(source_column) Array(accepts_from).map(&:to_s).include?(source_column.id.to_s) end |
#ai? ⇒ Boolean
Does any AI service this column? Explicit per-column switch (default ON for back-compat); the inbox/Tasks intake is never AI, unconditionally. When false the column is inert AI-wise: no assistant, no worker runs, no ai_task rules — cards there are human work.
56 57 58 59 |
# File 'app/models/column.rb', line 56 def ai? return false if inbox? policy["ai"] != false end |
#ai_mode_description ⇒ Object
174 |
# File 'app/models/column.rb', line 174 def ai_mode_description = AI_MODES[archetype] |
#archetype_template ⇒ Object
34 |
# File 'app/models/column.rb', line 34 def archetype_template = ARCHETYPE_TEMPLATES.fetch(archetype, {}) |
#at_wip_limit? ⇒ Boolean
122 123 124 |
# File 'app/models/column.rb', line 122 def at_wip_limit? execution? && concurrency_limit.present? && running_count >= concurrency_limit.to_i end |
#at_wip_limit_exceeded? ⇒ Boolean
Post-claim variant (§ races): a starter first claims working atomically, THEN checks — over-subscription reads as strictly more running than allowed.
128 129 130 |
# File 'app/models/column.rb', line 128 def at_wip_limit_exceeded? execution? && concurrency_limit.present? && running_count > concurrency_limit.to_i end |
#built_in_role ⇒ Object
164 |
# File 'app/models/column.rb', line 164 def built_in_role = BUILT_IN_ROLES[archetype] |
#drag_hint ⇒ Object
One-line consequence shown while dragging a card over this column (§14.1).
184 185 186 187 188 189 190 191 192 |
# File 'app/models/column.rb', line 184 def drag_hint case archetype when "inbox" then "Parked — no agent activity" when "planning" then "The board assistant will join the discussion" when "execution" then "An agent will be assigned and start work" when "review" then "Work stops — ready for your verdict" when "terminal" then "Closes it — PR merged and branch deleted, if there is one" end end |
#footer_rows ⇒ Object
Rows rendered under the cards (card #18). Footer config is an array of => "Total cost:", "compute" => "sum_cost" hashes; each row pairs static label text with an optional computed aggregate over this column's cards. Returns [] when unconfigured, so existing columns render no footer. No auto-rows (de-magic): the model row that used to be hardcoded for AI columns is now the "model" compute — visible in the gear, deletable.
83 84 85 86 87 88 89 90 |
# File 'app/models/column.rb', line 83 def Array().filter_map do |row| label = row["label"].to_s value = (row["compute"]) next if label.blank? && value.blank? { label:, value: } end end |
#footer_value(compute) ⇒ Object
Aggregate a single footer row over the runs/cards in this column (card #18). Unknown keys return "" so the row shows just its static label.
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'app/models/column.rb', line 134 def (compute) case compute.to_s when "sum_cost" "$%.2f" % (column_runs.sum(:cost) + column_ai_calls.sum(:cost)) when "sum_tokens" ActiveSupport::NumberHelper.number_to_delimited( column_runs.sum("input_tokens + output_tokens") + column_ai_calls.sum("input_tokens + output_tokens")) when "count_cards" cards.active.count.to_s when "model" # The column's active AI model, short form. Blank when AI is off or no # model is set — the row then shows just its label, telling the truth. ai? ? model_short.to_s : "" else "" end end |
#kick_queue ⇒ Object
Start the next queued card when a run slot frees up. A queued card whose run parked and already has its answer recorded resumes instead of starting fresh.
95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'app/models/column.rb', line 95 def kick_queue return unless ai? return if at_wip_limit? next_card = cards.where(status: "queued").order(:position).first return unless next_card parked = next_card.runs.where(status: "needs_input").order(:id).last if parked&.briefing&.key?("pending_resume") ResumeRunJob.perform_later(parked.id, "") else StartRunJob.perform_later(next_card.id) end end |
#model_label ⇒ Object
"Opus - High" — human label for cost footers (card #20). Effort is optional, so a model with no configured effort renders just "Opus".
114 |
# File 'app/models/column.rb', line 114 def model_label = model_label_of(model, effort) |
#model_short ⇒ Object
"claude-sonnet-4-6" → "sonnet", for compact chips on card faces.
110 |
# File 'app/models/column.rb', line 110 def model_short = model_short_of(model) |
#queued_count ⇒ Object
120 |
# File 'app/models/column.rb', line 120 def queued_count = cards.where(status: "queued").count |
#running_count ⇒ Object
119 |
# File 'app/models/column.rb', line 119 def running_count = cards.where(status: "working").count |
#safe_color ⇒ Object
Only ever emit a validated hex color into inline styles.
48 49 50 |
# File 'app/models/column.rb', line 48 def safe_color color if color.to_s.match?(/\A#\h{6}\z/) end |
#seed_archetype_template ⇒ Object
Stamp template values into any policy field the creator left blank.
177 178 179 180 181 |
# File 'app/models/column.rb', line 177 def seed_archetype_template archetype_template.each do |key, value| policy[key] = value if policy[key].blank? end end |
#shell_access? ⇒ Boolean
Worker shell access (execution columns). ON (default): the agent gets the full toolset — shell, git, everything — inside its workspace clone, which means it can also touch the host beyond the clone. OFF: file tools only (read/search/edit); it physically cannot execute commands, and Cardinal commits and pushes its edits for it.
66 67 68 |
# File 'app/models/column.rb', line 66 def shell_access? policy["shell"] != false end |