cardinal-ai 0.2.6 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '079e999400eeaa39a8e7f7d8efdc4b3b114923cf0aaf190dcd15a6117a97d7b2'
4
- data.tar.gz: 61343cc2e5c42fbf43f3eeb223da83af60d1e634f8424e69d032c9afd287aa02
3
+ metadata.gz: c16d402a271ff7a720232c12b775f43784eadd3268c1ce417fd2939d777be42e
4
+ data.tar.gz: 808ae0945e6f6104b3ef103468ce78c36406af25c78da223cd380a02336d5ef8
5
5
  SHA512:
6
- metadata.gz: 1acb0d147c37615d1daf946a8e3aebbbdec636511cf6781a835ed4b73b61b1f3b57aff903273b160b75df66b263857cc83e7ef38f8b71841a7f8f3ebea8877e6
7
- data.tar.gz: 5ab7bddf1b68f55466825914a1d0eb7a697e02a5c7e0e3a4d0dcfe306ee673c65a17f9f5e2bf09425c002ccd02809d32ce30e2f0a2ececdd2c93b8d0139932c5
6
+ metadata.gz: 4bd7c1c24d5969321f07753a3c14e2a09ecc561537dfd6f6365502b07c54aafbf93c22f84ed8a0eec9782ce1b9135606f2bbdf3fd0afc8c21f34d06d87896197
7
+ data.tar.gz: 84270e725e825a11eb81abd67de4bbec459cc736fc9cf1e43b41e926f4a0df346f7df15f0afa906ec2b669671ae74f7cc0287a1ac0c7eb25d630d120b1da3ad4
@@ -215,7 +215,18 @@ body.dragging .drop-hint { display: block; }
215
215
  font-size: 11px; color: var(--text-dim); font-weight: 600;
216
216
  }
217
217
  .card-footer:hover .footer-pr { color: var(--blue); }
218
- .footer-left { min-width: 1px; }
218
+ .footer-left { min-width: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
219
+ .footer-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
220
+ .footer-cost { white-space: nowrap; }
221
+ .footer-pr { color: var(--text-dim); }
222
+
223
+ /* Open-card cost tally: sits at the foot of the work panel, live-updating. */
224
+ .work-footer {
225
+ display: flex; justify-content: space-between; align-items: center; gap: 8px;
226
+ margin-top: 16px; padding-top: 10px; border-top: 1px solid var(--border);
227
+ font-size: 12px; color: var(--text-dim); font-weight: 600;
228
+ }
229
+ .work-footer .footer-cost { white-space: nowrap; }
219
230
  .card-ghost { opacity: .4; }
220
231
  .card-title { font-weight: 600; }
221
232
  .card-number { color: var(--text-dim); font-weight: 400; }
@@ -374,6 +385,16 @@ body.dragging .drop-hint { display: block; }
374
385
  .zoom-tabs a { padding: 4px 12px; border-radius: 6px; color: var(--text-dim); }
375
386
  .zoom-tabs a.active { background: var(--surface-2); color: var(--text); }
376
387
 
388
+ .summary-panel .summary-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
389
+ .summary-panel .summary-head h3 { margin: 0; }
390
+ .summary-generate { white-space: nowrap; }
391
+ .summary-blurb { margin: 6px 0 12px; }
392
+ .summary-form textarea {
393
+ width: 100%; background: var(--surface-2); border: 1px solid var(--border);
394
+ border-radius: 6px; color: var(--text); padding: 8px 10px; font: inherit; resize: vertical;
395
+ }
396
+ .summary-stamp { margin-top: 8px; }
397
+
377
398
  .event { display: flex; gap: 10px; padding: 8px 4px; border-bottom: 1px solid var(--surface-2); }
378
399
 
379
400
  /* Column moves are chapter markers in the card's story */
@@ -1,5 +1,5 @@
1
1
  class CardsController < ApplicationController
2
- before_action :set_card, only: [:show, :update, :move, :approve, :destroy]
2
+ before_action :set_card, only: [:show, :update, :move, :approve, :summarize, :destroy]
3
3
 
4
4
  def new
5
5
  @board = Board.first!
@@ -30,10 +30,11 @@ class CardsController < ApplicationController
30
30
  end
31
31
 
32
32
  def show
33
- @zoom = params[:zoom].presence_in(%w[conversation activity debug]) || "conversation"
33
+ @zoom = params[:zoom].presence_in(%w[conversation activity debug summary]) || "conversation"
34
34
  @events = case @zoom
35
35
  when "conversation" then @card.events.conversation
36
36
  when "activity" then @card.events.activity
37
+ when "summary" then Event.none # the Summary tab shows the card summary, not events
37
38
  else @card.events
38
39
  end
39
40
 
@@ -83,6 +84,19 @@ class CardsController < ApplicationController
83
84
  redirect_to card_path(@card)
84
85
  end
85
86
 
87
+ # Generate a customer-friendly summary on demand (card #35). Non-blocking,
88
+ # mirroring the board's deep dive: flip the card into its "working" state,
89
+ # morph the Summary panel so the button reflects it, and let SummaryJob do the
90
+ # one-shot synthesis in the background. Skipped when one is already running.
91
+ def summarize
92
+ unless @card.summary_working?
93
+ @card.update!(summary_status: "working")
94
+ SummaryJob.perform_later(@card)
95
+ end
96
+ render turbo_stream: turbo_stream.replace("card_summary",
97
+ partial: "cards/summary_panel", locals: { card: @card })
98
+ end
99
+
86
100
  def move
87
101
  from_column = @card.column
88
102
  to_column = @card.board.columns.find(params[:column_id])
@@ -106,7 +120,7 @@ class CardsController < ApplicationController
106
120
  end
107
121
 
108
122
  def card_params
109
- attrs = params.require(:card).permit(:title, :description, :tags, :branch_name, :pr_url)
123
+ attrs = params.require(:card).permit(:title, :description, :tags, :branch_name, :pr_url, :summary)
110
124
  attrs[:tags] = attrs[:tags].to_s.split(",").map(&:strip).reject(&:blank?) if attrs.key?(:tags)
111
125
  attrs.to_h.symbolize_keys
112
126
  end
@@ -114,7 +128,7 @@ class CardsController < ApplicationController
114
128
  # Changelog in the activity timeline (the mechanism already exists). A burst
115
129
  # of autosaves coalesces into one entry instead of one per pause-in-typing.
116
130
  def log_changelog!
117
- changed = @card.previous_changes.keys & %w[title description tags branch_name pr_url]
131
+ changed = @card.previous_changes.keys & %w[title description tags branch_name pr_url summary]
118
132
  return if changed.empty?
119
133
 
120
134
  last = @card.events.order(:id).last
@@ -0,0 +1,88 @@
1
+ # On-demand, customer-friendly card summary (card #35): a one-shot, tool-less
2
+ # Claude call (the same cheap tier the planning assistant and deep dive use)
3
+ # that compresses everything a card did — its brief, timeline, runs, and code
4
+ # commits — into a couple of non-technical lines you can drop into a customer
5
+ # chat. Generation is user-triggered only; the result persists on the card and
6
+ # stays fully editable. A prior summary (possibly hand-edited) rides along as
7
+ # context so a regeneration refines rather than discards what the user cared about.
8
+ class SummaryJob < ApplicationJob
9
+ queue_as :default
10
+
11
+ FALLBACK_MODEL = "claude-haiku-4-5-20251001".freeze
12
+
13
+ SYSTEM = <<~SYS.freeze
14
+ You write short, non-technical status updates for customers. Given everything
15
+ that happened on a work item, produce a plain-language recap the reader can
16
+ drop straight into a Teams or Slack message — what was asked for and what was
17
+ delivered, in outcome terms. No jargon, no file names, no code, no headings.
18
+ A couple of sentences up to a short paragraph. Write only the recap itself.
19
+ SYS
20
+
21
+ def perform(card)
22
+ return clear_working(card) unless ClaudeCli.available?
23
+
24
+ model = card.board.columns.find_by(archetype: "planning")&.model.presence || FALLBACK_MODEL
25
+ summary = ClaudeCli.prompt(build_prompt(card), system: SYSTEM, model: model, max_turns: 1)
26
+
27
+ card.update!(summary: summary.to_s.strip, summary_generated_at: Time.current, summary_status: nil)
28
+ card.broadcast_replace_to card, target: "card_summary",
29
+ partial: "cards/summary_panel", locals: { card: card }
30
+ rescue StandardError
31
+ # A failed generation must not leave the button stuck on "Generating…".
32
+ clear_working(card)
33
+ end
34
+
35
+ private
36
+
37
+ def clear_working(card)
38
+ card.update!(summary_status: nil)
39
+ card.broadcast_replace_to card, target: "card_summary",
40
+ partial: "cards/summary_panel", locals: { card: card }
41
+ end
42
+
43
+ def build_prompt(card)
44
+ parts = ["Work item ##{card.number}: #{card.title}"]
45
+ parts << "Tags: #{card.tags.join(", ")}" if card.tags.any?
46
+ parts << "\nDescription:\n#{card.description}" if card.description.present?
47
+
48
+ timeline = card.events.activity.filter_map { |e| event_line(e) }
49
+ parts << "\nWhat happened (timeline):\n#{timeline.join("\n")}" if timeline.any?
50
+
51
+ runs = card.runs.order(:id).map { |r| "- Run ##{r.id}: #{r.status}#{" (#{r.phase})" if r.phase.present?}" }
52
+ parts << "\nRuns:\n#{runs.join("\n")}" if runs.any?
53
+
54
+ commits = commit_lines(card)
55
+ parts << "\nCode changes (commit messages):\n#{commits.join("\n")}" if commits.any?
56
+
57
+ if card.summary.present?
58
+ parts << "\nThe user's current summary is below. They may have edited it by hand, " \
59
+ "so treat its wording and emphasis as a signal of what they care about — " \
60
+ "refine and update it with any new work rather than starting from scratch:\n#{card.summary}"
61
+ end
62
+
63
+ parts.join("\n")
64
+ end
65
+
66
+ def event_line(event)
67
+ text = event.payload["text"].to_s.strip
68
+ return nil if text.blank?
69
+ "- #{event.actor}: #{text.truncate(400)}"
70
+ end
71
+
72
+ # Commit messages for the card's branch, read from the per-card workspace
73
+ # checkout when it still exists. The checkout isn't guaranteed to be present
74
+ # (it's left in place after a run but may be pruned), so this is best-effort —
75
+ # the timeline already narrates the work when commits are unavailable.
76
+ def commit_lines(card)
77
+ return [] if card.branch_name.blank?
78
+ path = Agent::Workspace::Local.new(card).path
79
+ return [] unless File.directory?(path.join(".git"))
80
+
81
+ base = "origin/#{card.board.default_branch}"
82
+ out, ok = Open3.capture2e("git", "-C", path.to_s, "log", "--oneline", "--no-decorate", "#{base}..HEAD")
83
+ return [] unless ok.success?
84
+ out.lines.map(&:strip).reject(&:blank?).map { |l| "- #{l}" }
85
+ rescue StandardError
86
+ []
87
+ end
88
+ end
data/app/models/board.rb CHANGED
@@ -10,6 +10,7 @@ class Board < ApplicationRecord
10
10
  policy: { "ai" => true, "model" => "claude-haiku-4-5-20251001", "plan_approval" => false,
11
11
  "on_entry" => [{ "action" => "assistant_greeting" }],
12
12
  "on_entry_text" => "The planning assistant reads the card and opens the discussion.",
13
+ "footer" => [{ "label" => "Model:", "compute" => "model" }],
13
14
  "accepts_from_names" => ["Tasks", "In Progress", "Review", "QA"] } },
14
15
  { name: "In Progress", archetype: "execution",
15
16
  policy: { "ai" => true, "model" => "claude-opus-4-8", "effort" => "high",
@@ -18,6 +19,7 @@ class Board < ApplicationRecord
18
19
  "tools" => %w[read edit run_commands git_commit_push],
19
20
  "on_entry" => [{ "action" => "start_agent_run" }],
20
21
  "accepts_from_names" => ["Planning", "Review", "QA"],
22
+ "footer" => [{ "label" => "Model:", "compute" => "model" }],
21
23
  "instructions" => "Follow repo conventions. Write tests when the repo has a suite." } },
22
24
  { name: "Review", archetype: "review",
23
25
  policy: { "ai" => true, "plan_approval" => false,
data/app/models/card.rb CHANGED
@@ -39,6 +39,9 @@ class Card < ApplicationRecord
39
39
 
40
40
  def needs_attention? = %w[needs_input blocked failed work_complete].include?(status)
41
41
 
42
+ # A customer-friendly summary is being (re)generated in the background (§card #35).
43
+ def summary_working? = summary_status == "working"
44
+
42
45
  def running? = %w[queued working needs_input].include?(status)
43
46
 
44
47
  # Latest one-line progress event, shown live on the card face (§6).
@@ -46,6 +49,11 @@ class Card < ApplicationRecord
46
49
  events.where(kind: "progress").last&.payload&.[]("text")
47
50
  end
48
51
 
52
+ # Running tally across every run on the card — the closed-card cost footer
53
+ # (card #20). Sums stopped/restarted segments so the total reflects real spend.
54
+ def total_cost = runs.sum(:cost)
55
+ def total_output_tokens = runs.sum(:output_tokens)
56
+
49
57
  # Is the planning assistant expected to post next? True right after entering
50
58
  # a planning column (kickoff inspection pending) or after a user message.
51
59
  def awaiting_assistant?
data/app/models/column.rb CHANGED
@@ -40,7 +40,7 @@ class Column < ApplicationRecord
40
40
  # Aggregations a footer row may compute over the column's cards (card #18).
41
41
  # A compute key not listed here renders blank, so config that outruns the
42
42
  # code degrades gracefully instead of erroring.
43
- FOOTER_COMPUTES = %w[sum_cost sum_tokens count_cards].freeze
43
+ FOOTER_COMPUTES = %w[sum_cost sum_tokens count_cards model].freeze
44
44
 
45
45
  # Only ever emit a validated hex color into inline styles.
46
46
  def safe_color
@@ -76,18 +76,15 @@ class Column < ApplicationRecord
76
76
  # {"label" => "Total cost:", "compute" => "sum_cost"} hashes; each row pairs
77
77
  # static label text with an optional computed aggregate over this column's
78
78
  # cards. Returns [] when unconfigured, so existing columns render no footer.
79
+ # No auto-rows (de-magic): the model row that used to be hardcoded for AI
80
+ # columns is now the "model" compute — visible in the gear, deletable.
79
81
  def footer_rows
80
- rows = Array(footer).filter_map do |row|
82
+ Array(footer).filter_map do |row|
81
83
  label = row["label"].to_s
82
84
  value = footer_value(row["compute"])
83
85
  next if label.blank? && value.blank?
84
86
  { label:, value: }
85
87
  end
86
- # AI columns advertise their active model as a final auto-row (card #32).
87
- # Guarded on model presence so an AI column without one adds nothing,
88
- # rather than emitting a "Model:" row with a blank value.
89
- rows << { label: "Model:", value: model_short } if ai? && model.present?
90
- rows
91
88
  end
92
89
 
93
90
  # Start the next queued card when a run slot frees up. A queued card whose
@@ -112,6 +109,14 @@ class Column < ApplicationRecord
112
109
  model.to_s[/claude-([a-z]+)/, 1] || model
113
110
  end
114
111
 
112
+ # "Opus - High" — human label for cost footers (card #20). Effort is optional,
113
+ # so a model with no configured effort renders just "Opus".
114
+ def model_label
115
+ return if model.blank?
116
+ label = model_short.to_s.capitalize
117
+ effort.present? ? "#{label} - #{effort.to_s.capitalize}" : label
118
+ end
119
+
115
120
  validates :name, presence: true
116
121
  validates :position, presence: true
117
122
 
@@ -138,6 +143,10 @@ class Column < ApplicationRecord
138
143
  ActiveSupport::NumberHelper.number_to_delimited(column_runs.sum("input_tokens + output_tokens"))
139
144
  when "count_cards"
140
145
  cards.count.to_s
146
+ when "model"
147
+ # The column's active AI model, short form. Blank when AI is off or no
148
+ # model is set — the row then shows just its label, telling the truth.
149
+ ai? ? model_short.to_s : ""
141
150
  else
142
151
  ""
143
152
  end
@@ -35,7 +35,7 @@
35
35
 
36
36
  <div class="card-edit-actions">
37
37
  <%= button_to "↻ Regenerate brief", deep_dive_board_path(force: 1),
38
- form: { data: { turbo_frame: "_top" } },
38
+ form: { data: { turbo_frame: "_top", action: "turbo:submit-end->modal#closeOnSuccess" } },
39
39
  disabled: @board.brief_working? %>
40
40
  </div>
41
41
  <p class="hint">
@@ -31,18 +31,23 @@
31
31
  <% elsif card.discussing? && card.events.conversation.last&.actor == "assistant" %>
32
32
  <span class="chip agent-chip">🪶 replied</span>
33
33
  <% end %>
34
- <% if (card.running? || card.needs_attention?) && (last_run = card.runs.order(:id).last) && (last_run.cost.to_f.positive? || last_run.output_tokens.positive?) %>
35
- <span class="chip">$<%= last_run.cost.round(2) %><%= " · #{(last_run.output_tokens / 1000.0).round(1)}k out" if card.working? %></span>
36
- <% end %>
37
34
  <% if card.parent_id %><span class="chip">↑ sub</span><% end %>
38
35
  <% if card.children.any? %><span class="chip">↳ <%= card.children.where(status: %w[done archived]).count %>/<%= card.children.count %></span><% end %>
39
36
  <% if card.branch_name.present? && card.pr_url.blank? %><span class="chip branch">🌿 <%= card.branch_name.delete_prefix("cardinal/") %></span><% end %>
40
37
  </div>
41
38
  <% end %>
42
- <% if card.pr_url.present? %>
43
- <a class="card-footer" href="<%= card.pr_url %>" target="_blank" rel="noopener" title="Open the pull request on GitHub">
44
- <span class="footer-left"><%# future: Asana / Trello / linked-ticket slot %></span>
45
- <span class="footer-pr">GitHub #<%= card.pr_url[%r{/pull/(\d+)}, 1] %> ↗</span>
46
- </a>
39
+ <% has_cost = card.total_cost.positive? || card.total_output_tokens.positive? %>
40
+ <% if has_cost || card.pr_url.present? %>
41
+ <div class="card-footer">
42
+ <span class="footer-left"><%= card.column.model_label if has_cost %></span>
43
+ <span class="footer-right">
44
+ <% if has_cost %>
45
+ <span class="footer-cost">$<%= card.total_cost.round(2) %> · <%= card.total_output_tokens %> out</span>
46
+ <% end %>
47
+ <% if card.pr_url.present? %>
48
+ <a class="footer-pr" href="<%= card.pr_url %>" target="_blank" rel="noopener" title="Open the pull request on GitHub">GitHub #<%= card.pr_url[%r{/pull/(\d+)}, 1] %> ↗</a>
49
+ <% end %>
50
+ </span>
51
+ </div>
47
52
  <% end %>
48
53
  </article>
@@ -31,12 +31,17 @@
31
31
  <div class="detail-panes">
32
32
  <section class="timeline" data-controller="scroll">
33
33
  <nav class="zoom-tabs">
34
- <% %w[conversation activity debug].each do |zoom| %>
34
+ <% %w[conversation activity debug summary].each do |zoom| %>
35
35
  <%= link_to zoom.capitalize, card_path(@card, zoom: zoom),
36
36
  class: ("active" if @zoom == zoom) %>
37
37
  <% end %>
38
38
  </nav>
39
39
 
40
+ <% if @zoom == "summary" %>
41
+ <div class="timeline-scroll">
42
+ <%= render "cards/summary_panel", card: @card %>
43
+ </div>
44
+ <% else %>
40
45
  <div class="timeline-scroll" data-scroll-target="scroller">
41
46
  <% if @card.description.present? %>
42
47
  <div class="event event-description"><%= render_markdown @card.description %></div>
@@ -65,6 +70,7 @@
65
70
  data: { controller: "composer", action: "keydown->composer#keydown" },
66
71
  placeholder: (@card.column.planning? ? "Discuss this card with the planning assistant…" : "Add a note to this card…") + " (Enter sends, Shift+Enter for a new line)" %>
67
72
  <% end %>
73
+ <% end %>
68
74
  </section>
69
75
 
70
76
  <aside class="work-panel">
@@ -175,6 +181,13 @@
175
181
  <p class="empty">No runs yet — drag the card into an execution column to assign an agent.</p>
176
182
  <% end %>
177
183
 
184
+ <% if latest && (latest.cost.positive? || latest.output_tokens.positive?) %>
185
+ <div class="work-footer">
186
+ <span class="footer-left"><%= @card.column.model_label %></span>
187
+ <span class="footer-cost">$<%= latest.cost.round(2) %> · <%= latest.output_tokens %> out</span>
188
+ </div>
189
+ <% end %>
190
+
178
191
  <details class="advanced-rules panel-advanced">
179
192
  <summary>Advanced</summary>
180
193
  <p class="hint">Deleting removes the card and its entire history (events, runs, workspace). The remote branch and PR, if any, are left untouched.</p>
@@ -0,0 +1,28 @@
1
+ <div id="card_summary" class="summary-panel" data-controller="autosave">
2
+ <div class="summary-head">
3
+ <h3>Customer summary <span class="autosave-status" data-autosave-target="status"></span></h3>
4
+ <% if card.summary_working? %>
5
+ <button type="button" class="deep-dive working summary-generate" disabled>
6
+ <span class="pulse-dot"></span> Generating…
7
+ </button>
8
+ <% else %>
9
+ <%= button_to summarize_card_path(card), class: "deep-dive summary-generate",
10
+ title: "Compress everything this card did into a couple of non-technical lines" do %>
11
+ ✨ <%= card.summary.present? ? "Regenerate" : "Generate summary" %>
12
+ <% end %>
13
+ <% end %>
14
+ </div>
15
+
16
+ <p class="hint summary-blurb">A plain-language recap of what this card delivered — ready to drop into a customer chat. Fully editable; your edits are respected when you regenerate.</p>
17
+
18
+ <%= form_with model: card, class: "summary-form",
19
+ data: { autosave_target: "form", action: "input->autosave#save change->autosave#save" } do |f| %>
20
+ <%= hidden_field_tag :autosave, "1" %>
21
+ <%= f.text_area :summary, rows: 10, placeholder: "No summary yet — click Generate summary, or write your own.",
22
+ disabled: card.summary_working? %>
23
+ <% end %>
24
+
25
+ <% if card.summary_generated_at.present? %>
26
+ <p class="hint summary-stamp">Last generated <%= time_ago_in_words(card.summary_generated_at) %> ago</p>
27
+ <% end %>
28
+ </div>
@@ -134,7 +134,7 @@
134
134
  </div>
135
135
  <% end %>
136
136
 
137
- <label>Footer <%= info_tip("A summary strip under the cards. One row per line as \"Label | compute\", where compute is sum_cost (total run cost), sum_tokens (total input+output tokens), count_cards, or blank for a static label. Example: \"Total cost: | sum_cost\".") %></label>
137
+ <label>Footer <%= info_tip("A summary strip under the cards. One row per line as \"Label | compute\", where compute is sum_cost (total run cost), sum_tokens (total input+output tokens), count_cards, model (this column's active AI model), or blank for a static label. Example: \"Model: | model\".") %></label>
138
138
  <%= f.text_area :footer_text, rows: 3, class: "mono",
139
139
  value: footer_config_text(@column),
140
140
  placeholder: "Total cost: | sum_cost" %>
data/config/routes.rb CHANGED
@@ -10,6 +10,7 @@ Rails.application.routes.draw do
10
10
  member do
11
11
  patch :move
12
12
  post :approve
13
+ post :summarize
13
14
  end
14
15
  resources :messages, only: [:create]
15
16
  end
@@ -0,0 +1,7 @@
1
+ class AddSummaryToCards < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :cards, :summary, :text
4
+ add_column :cards, :summary_generated_at, :datetime
5
+ add_column :cards, :summary_status, :string
6
+ end
7
+ end
@@ -1,3 +1,3 @@
1
1
  module Cardinal
2
- VERSION = "0.2.6"
2
+ VERSION = "0.2.7"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cardinal-ai
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.6
4
+ version: 0.2.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jason Ellis
@@ -190,6 +190,7 @@ files:
190
190
  - app/jobs/merge_pr_job.rb
191
191
  - app/jobs/resume_run_job.rb
192
192
  - app/jobs/start_run_job.rb
193
+ - app/jobs/summary_job.rb
193
194
  - app/mailers/application_mailer.rb
194
195
  - app/models/agent_session.rb
195
196
  - app/models/application_record.rb
@@ -211,6 +212,7 @@ files:
211
212
  - app/views/boards/show.html.erb
212
213
  - app/views/cards/_card.html.erb
213
214
  - app/views/cards/_detail.html.erb
215
+ - app/views/cards/_summary_panel.html.erb
214
216
  - app/views/cards/_tag_picker.html.erb
215
217
  - app/views/cards/new.html.erb
216
218
  - app/views/cards/show.html.erb
@@ -231,7 +233,6 @@ files:
231
233
  - config/bundler-audit.yml
232
234
  - config/cable.yml
233
235
  - config/ci.rb
234
- - config/credentials.yml.enc
235
236
  - config/database.yml
236
237
  - config/environment.rb
237
238
  - config/environments/development.rb
@@ -257,6 +258,7 @@ files:
257
258
  - db/migrate/20260704000001_add_parent_to_cards.rb
258
259
  - db/migrate/20260704000002_add_assistant_session_to_cards.rb
259
260
  - db/migrate/20260704120000_add_repo_brief_to_boards.rb
261
+ - db/migrate/20260704130000_add_summary_to_cards.rb
260
262
  - db/queue_schema.rb
261
263
  - db/seeds.rb
262
264
  - docker/agent/Dockerfile
@@ -1 +0,0 @@
1
- gRbdqLwcuHenzfNXCmiHivELrqct/iaMFWc11UxhtcwPIAyQYYOpoUGziW347shIgbHjK4zVpJQ/la/Te51SO6G1NgGZyJZrFRKB8kebFYcqktQzMRTGRl0ykjWf3y0iN6YPcYC6C/0M3Zr7hasRYG8hX9MOnLFjYUYQw/yZRpM/RR2XBslITX1FubnB/CZrzQzz9WDSYG+TOmR5IUBdn28ZrQxSz8u3oXzuhwibUGf2GNffWM4nHpDmHAiwDq7o8yMrZHipJiiUmo3ffe7YbLR+RdTy3AmIatai8wCEruPvzopfV3hn8b5alhk6g8wZQMW29rV5zwKJhnfIJ/IH8LJJWtudfckYkhh4KDam6TppBdqc8rXWCqodYIt2voYM/ARkQt+CTgVZQCLjlvW2Qm4NTzQCcZZkMllQ7vvpsyZ/vzpaRyYSHIl/9Hzip/orWM7g/SNIm44mDLC8+6IoiVVDSomOaQVEEfRKO7Rny3KahfXpqEiUSfmf--B7qWHwiaPzgA3yLD--x/FY/DNjNt+OyLs4ca9tQQ==