Module: Rules
- Defined in:
- app/services/rules.rb,
app/services/rules/compiler.rb
Overview
Column rules (cardinal.md §17): a column's on_entry policy is a list of rule actions fired when a card lands in it. Archetypes stamp starting rules at creation (templates, not magic) — any column can carry any rules after that, including one-shot AI maintenance tasks.
Defined Under Namespace
Modules: Compiler
Constant Summary collapse
- DEFAULT_DESCRIPTIONS =
Shown in the gear modal so the archetype's built-in behavior is visible, not implied (the on-entry box being blank doesn't mean nothing happens).
{ "inbox" => "Nothing — cards park here untouched.", "planning" => "The planning assistant inspects the card and opens the conversation: it reads the title and description, then asks its sharpest clarifying questions to improve the card before execution. Tune its focus with the Instructions field above.", "execution" => "A dedicated worker agent is assigned to the card and a run starts (plan-first if plan approval is on).", "review" => "Nothing automatic — the card waits for your verdict.", "terminal" => "The card's PR is squash-merged and its branch deleted. A card with no PR is simply closed — planning can send work straight here to terminate it." }.freeze
- AI_ACTIONS =
%w[assistant_greeting start_agent_run ai_task].freeze
- ACTION_DESCRIPTIONS =
Human names for compiled rule actions — so "currently active" behavior is readable in the gear modal without opening the JSON drawer (no-magic).
{ "assistant_greeting" => "the assistant opens the discussion", "start_agent_run" => "assign a worker agent and start a run", "ai_task" => "run a one-shot AI task", "mark_pr_ready" => "take the PR out of draft", "merge_pr" => "merge the PR and ship", "set_status" => "set the card's status" }.freeze
Class Method Summary collapse
- .apply(rule, card, column) ⇒ Object
- .describe(rules) ⇒ Object
- .each_rule(configured, &block) ⇒ Object
- .fire_entry(card, column) ⇒ Object
Class Method Details
.apply(rule, card, column) ⇒ Object
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'app/services/rules.rb', line 53 def self.apply(rule, card, column) if AI_ACTIONS.include?(rule["action"]) && !column.ai? card.log!("status_change", text: "AI is off for #{column.name} — skipped #{rule["action"]}") return end case rule["action"] when "assistant_greeting" # Contextual opener: the assistant reads the card and asks targeted # questions (AssistantReplyJob falls back to a canned line without a key). AssistantReplyJob.perform_later(card, kickoff: true) when "start_agent_run" card.update!(branch_name: card.branch_name.presence || card.default_branch_name) card.log!("status_change", text: "Queued for execution on #{card.branch_name}") StartRunJob.perform_later(card.id) when "ai_task" # One-shot maintenance agent: a bounded Messages API call whose prompt # comes from the rule config. No workspace, no session, no tools. AiTaskJob.perform_later(card.id, rule["prompt"].to_s, rule["model"]) when "mark_pr_ready" if card.pr_url.present? card.log!("status_change", text: "Taking the PR out of draft…") MarkPrReadyJob.perform_later(card.id) else card.log!("status_change", text: "No PR to mark ready") end when "merge_pr" if card.pr_url.present? card.log!("status_change", text: "Shipping: merging #{card.pr_url}") MergePrJob.perform_later(card.id) else card.log!("status_change", text: "Card finalized (no PR to merge)") end when "set_status" card.update!(status: rule["status"]) if Card::STATUSES.include?(rule["status"]) else card.log!("error", text: "Unknown column rule: #{rule["action"].inspect}") end end |
.describe(rules) ⇒ Object
45 46 47 48 49 50 51 |
# File 'app/services/rules.rb', line 45 def self.describe(rules) normalized = rules.is_a?(Hash) || rules.is_a?(String) ? [rules] : Array(rules) normalized.map { |r| r.is_a?(String) ? { "action" => r } : r }.map do |rule| base = ACTION_DESCRIPTIONS[rule["action"]] || rule["action"].to_s rule["action"] == "ai_task" && rule["prompt"].present? ? "#{base} (“#{rule["prompt"].truncate(60)}”)" : base end.join("; then ") end |
.each_rule(configured, &block) ⇒ Object
26 27 28 29 30 |
# File 'app/services/rules.rb', line 26 def self.each_rule(configured, &block) rules = configured.presence || [] rules = [rules] if rules.is_a?(Hash) || rules.is_a?(String) rules.map { |r| r.is_a?(String) ? { "action" => r } : r }.each(&block) end |
.fire_entry(card, column) ⇒ Object
20 21 22 23 24 |
# File 'app/services/rules.rb', line 20 def self.fire_entry(card, column) each_rule(column.policy["on_entry"]) do |rule| apply(rule, card, column) end end |