cardinal-ai 0.2.4 → 0.2.6

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.
@@ -30,6 +30,21 @@ module Agent
30
30
  - Finish with a concise report: what you did, what to check, any open questions.
31
31
  RULES
32
32
 
33
+ # Column shell access OFF: the agent can read, search, and edit — nothing
34
+ # else. Enforced by the CLI tool list, not just these words.
35
+ RESTRICTED_EXECUTE_RULES = <<~RULES.freeze
36
+ ## Rules
37
+ - You have FILE TOOLS ONLY: read, search, edit, write. You cannot run shell
38
+ commands or git — do not attempt to; Cardinal commits and pushes your edits for you.
39
+ - Work only inside this repository checkout (you are already on the card's branch).
40
+ - Stay strictly within the card's scope. Prefer the smallest reasonable interpretation and note assumptions.
41
+ - If something must be RUN to finish the job (tests, generators, installs), do the file
42
+ work, then list the exact commands in your final report for the user to run.
43
+ - If you are blocked on a decision only the user can make, output a single line starting with
44
+ "QUESTION:" followed by the question, then stop immediately. Do not guess on genuinely ambiguous choices.
45
+ - Finish with a concise report: what you did, what to check, any open questions.
46
+ RULES
47
+
33
48
  def self.start(run) = new(run).start
34
49
  def self.resume(run, message, approve: false) = new(run).resume(message, approve: approve)
35
50
 
@@ -59,7 +74,7 @@ module Agent
59
74
  begin_segment!
60
75
  if run.phase == "plan" && approve
61
76
  run.update!(phase: "execute")
62
- stream_agent(prompt: "Your plan is approved — execute it now.\n\n#{EXECUTE_RULES}",
77
+ stream_agent(prompt: "Your plan is approved — execute it now.\n\n#{execute_rules}",
63
78
  mode: "execute", resuming: true)
64
79
  elsif run.phase == "plan"
65
80
  stream_agent(prompt: "Feedback on your plan:\n\n#{message}\n\nRevise the plan accordingly, present it, and stop again for approval. Stay in read-only mode.",
@@ -106,6 +121,9 @@ module Agent
106
121
  cmd += ["--max-turns", "3", "--tools", ""]
107
122
  else
108
123
  cmd += ["--max-turns", (column.max_turns.presence || DEFAULT_EXECUTE_TURNS).to_s]
124
+ # Shell access off: restrict to file tools — the sandbox is the tool
125
+ # list itself, not a request in the prompt.
126
+ cmd += ["--tools", "Read,Glob,Grep,Edit,Write"] unless column.shell_access?
109
127
  end
110
128
  cmd += ["--model", column.model] if column.model.present?
111
129
  cmd += ["--effort", column.effort] if column.effort.present?
@@ -205,6 +223,11 @@ module Agent
205
223
 
206
224
  def conclude_execute(workspace, result)
207
225
  accumulate_usage(result)
226
+ # A shell-less agent cannot commit its own edits — do it for it before
227
+ # any commit counting or salvage below.
228
+ unless column.shell_access?
229
+ workspace.commit_all!("Card ##{card.number}: #{card.title.truncate(60)}\n\nCommitted by Cardinal for a shell-less agent.")
230
+ end
208
231
  unless result[:success]
209
232
  # Budget exhaustion isn't failure — park and offer to continue (§8).
210
233
  # The session survives; an answer resumes it with a fresh turn budget.
@@ -329,15 +352,19 @@ module Agent
329
352
  ## Brief
330
353
  #{card.description.presence || "(no description — infer scope from the title and conversation)"}
331
354
 
332
- #{"## Brief from planning (authoritative — refined with the user)\n#{planning_brief}\n" if planning_brief}
355
+ #{repo_brief_section}#{"## Brief from planning (authoritative — refined with the user)\n#{planning_brief}\n" if planning_brief}
333
356
  ## Card conversation so far
334
357
  #{conversation_excerpt.presence || "(none)"}
335
358
 
336
359
  #{"## Column instructions\n#{column.instructions}\n" if column.instructions.present?}
337
- #{EXECUTE_RULES}
360
+ #{execute_rules}
338
361
  PROMPT
339
362
  end
340
363
 
364
+ def execute_rules
365
+ column.shell_access? ? EXECUTE_RULES : RESTRICTED_EXECUTE_RULES
366
+ end
367
+
341
368
  def plan_prompt
342
369
  <<~PROMPT
343
370
  You are the dedicated worker agent for card ##{card.number} of the Cardinal board: "#{card.title}".
@@ -346,7 +373,7 @@ module Agent
346
373
  ## Brief
347
374
  #{card.description.presence || "(no description — infer scope from the title and conversation)"}
348
375
 
349
- #{"## Brief from planning (authoritative — refined with the user)\n#{planning_brief}\n" if planning_brief}
376
+ #{repo_brief_section}#{"## Brief from planning (authoritative — refined with the user)\n#{planning_brief}\n" if planning_brief}
350
377
  ## Card conversation so far
351
378
  #{conversation_excerpt.presence || "(none)"}
352
379
 
@@ -363,6 +390,14 @@ module Agent
363
390
  PROMPT
364
391
  end
365
392
 
393
+ # The one-time repo deep dive (card #12), injected ahead of the planning
394
+ # brief so the agent starts oriented instead of re-exploring the tree each
395
+ # run. Empty string (not nil) when there's no brief, so the heredoc stays clean.
396
+ def repo_brief_section
397
+ brief = card.board.repo_brief.presence or return ""
398
+ "## Repo brief\n#{brief.strip}\n\n"
399
+ end
400
+
366
401
  # The planning assistant's distilled "Ready for execution" brief, if the
367
402
  # conversation produced one — the most load-bearing artifact of planning.
368
403
  def planning_brief
@@ -74,6 +74,15 @@ module Agent
74
74
  end
75
75
  end
76
76
 
77
+ # Shell-less agents (column shell access off) edit files but cannot run
78
+ # git — the runner commits on their behalf when a segment ends.
79
+ def commit_all!(message)
80
+ return false if git_out(path, "status", "--porcelain").strip.empty?
81
+ git!(path, "add", "-A")
82
+ git!(path, "commit", "--quiet", "-m", message)
83
+ true
84
+ end
85
+
77
86
  # How the runner should spawn the agent process for this workspace.
78
87
  def agent_spawn(cmd) = [cmd, { chdir: path.to_s }]
79
88
 
@@ -0,0 +1,48 @@
1
+ <%= turbo_frame_tag "modal", data: { turbo_permanent: true } do %>
2
+ <div class="modal-backdrop" data-controller="modal" data-action="click->modal#backdrop">
3
+ <div class="modal modal-sm">
4
+ <header class="modal-header">
5
+ <h1>🔍 Repo brief</h1>
6
+ <button type="button" class="modal-close" data-action="modal#close" title="Close (Esc)">✕</button>
7
+ </header>
8
+ <div class="modal-body">
9
+ <p class="hint">
10
+ A deep dive is a one-shot, read-only agent (Read/Glob/Grep only — it cannot change
11
+ anything) that maps this repo into the brief below. Every worker agent gets the brief
12
+ in its prompt, so runs skip re-exploring the codebase. It costs one bounded AI call
13
+ using the planning column's model.
14
+ </p>
15
+
16
+ <% if @board.brief? %>
17
+ <p class="brief-meta">
18
+ Generated <%= @board.brief_generated_at&.strftime("%b %-d, %H:%M") %>
19
+ from <code><%= @board.brief_sha&.first(7) %></code>
20
+ with <%= @board.brief_model %>
21
+ <% behind = @board.commits_behind_brief %>
22
+ <% if behind&.positive? %>
23
+ · <span class="brief-behind"><%= pluralize(behind, "commit") %> behind HEAD</span>
24
+ <% else %>
25
+ · current with HEAD
26
+ <% end %>
27
+ </p>
28
+
29
+ <div class="brief-content">
30
+ <%= render_markdown(@board.repo_brief.to_s) %>
31
+ </div>
32
+ <% else %>
33
+ <p class="brief-meta">No brief yet — run a deep dive from the topbar.</p>
34
+ <% end %>
35
+
36
+ <div class="card-edit-actions">
37
+ <%= button_to "↻ Regenerate brief", deep_dive_board_path(force: 1),
38
+ form: { data: { turbo_frame: "_top" } },
39
+ disabled: @board.brief_working? %>
40
+ </div>
41
+ <p class="hint">
42
+ Regenerating replaces the brief. When the brief already matches HEAD the deep dive
43
+ button won't re-run it — only this button forces a fresh dive.
44
+ </p>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ <% end %>
@@ -0,0 +1,29 @@
1
+ <%= turbo_frame_tag "modal", data: { turbo_permanent: true } do %>
2
+ <div class="modal-backdrop" data-controller="modal" data-action="click->modal#backdrop">
3
+ <div class="modal modal-sm" data-controller="autosave">
4
+ <header class="modal-header">
5
+ <h1>⚙ Board settings
6
+ <span class="autosave-status" data-autosave-target="status"></span></h1>
7
+ <button type="button" class="modal-close" data-action="modal#close" title="Close (Esc)">✕</button>
8
+ </header>
9
+ <div class="modal-body">
10
+ <div id="board-form-errors"></div>
11
+
12
+ <%= form_with model: @board, url: board_path(autosave: 1), class: "card-edit",
13
+ data: { autosave_target: "form" } do |f| %>
14
+ <label>Board name</label>
15
+ <%= f.text_field :name %>
16
+
17
+ <label>Default branch <%= info_tip("The branch agents fork card branches from and Done merges pull requests toward. Detected from the repo at first boot; fix it here if that guess was wrong.") %></label>
18
+ <%= f.text_field :default_branch, placeholder: "main" %>
19
+ <% end %>
20
+
21
+ <label>Repository</label>
22
+ <p class="locked-field"><code><%= @board.repo_url.presence || "(no remote)" %></code></p>
23
+
24
+ <label>Local checkout</label>
25
+ <p class="locked-field"><code><%= @board.local_path %></code></p>
26
+ </div>
27
+ </div>
28
+ </div>
29
+ <% end %>
@@ -1,7 +1,42 @@
1
1
  <%= turbo_stream_from @board %>
2
2
 
3
3
  <header class="topbar">
4
- <h1>Cardinal AI <span class="sep">▸</span> <%= @board.name %></h1>
4
+ <div class="topbar-left">
5
+ <h1>Cardinal AI <span class="sep">▸</span> <span id="board-name"><%= @board.name %></span></h1>
6
+ <%= link_to "⚙", edit_board_path, data: { turbo_frame: "modal" }, class: "gear",
7
+ title: "Board settings — name, default branch" %>
8
+ <%= button_to "⇣ Pull", pull_board_path, form_class: "pull-form",
9
+ form: { title: "git pull --ff-only in #{@board.local_path} — fetch what Done has merged" },
10
+ data: { turbo_submits_with: "⇣ Pulling…" }, class: "theme-toggle" %>
11
+ <%# Repo deep dive (card #12): one-shot read-only mapping of the repo, stored
12
+ as a brief and injected into worker prompts. The button greys→reds as the
13
+ brief falls behind HEAD; a stale brief over-anchors, so it nags at 10+.
14
+ Once a brief exists the button opens it for inspection (the modal holds
15
+ the Regenerate action) — a fresh brief is never silently re-run. %>
16
+ <% behind = @board.commits_behind_brief %>
17
+ <% if @board.brief_working? %>
18
+ <button type="button" class="deep-dive working" disabled>
19
+ <span class="pulse-dot"></span> Deep dive · working…
20
+ </button>
21
+ <% elsif @board.brief? %>
22
+ <%= link_to brief_board_path, data: { turbo_frame: "modal" },
23
+ class: "deep-dive#{' stale-critical' if @board.brief_stale?}",
24
+ style: "color: #{@board.brief_staleness_color}; border-color: #{@board.brief_staleness_color}",
25
+ title: "Repo brief from #{@board.brief_sha&.first(7)} · #{@board.brief_generated_at&.to_date} — click to inspect" do %>
26
+ <% if behind&.positive? %>
27
+ 🔍 Repo brief · <%= behind %> commit<%= "s" if behind != 1 %> behind
28
+ <% else %>
29
+ 🔍 Repo brief
30
+ <% end %>
31
+ <% end %>
32
+ <% else %>
33
+ <%= button_to deep_dive_board_path, class: "deep-dive",
34
+ title: "Map this repo into a brief agents reuse — saves exploration on every run" do %>
35
+ 🔍 Deep dive
36
+ <% end %>
37
+ <% end %>
38
+ <span id="repo-pull-status"></span>
39
+ </div>
5
40
  <div class="topbar-right">
6
41
  <button type="button" class="theme-toggle"
7
42
  data-controller="theme" data-action="theme#toggle">☀ Light</button>
@@ -36,10 +36,10 @@
36
36
  <% end %>
37
37
  <% if card.parent_id %><span class="chip">↑ sub</span><% end %>
38
38
  <% if card.children.any? %><span class="chip">↳ <%= card.children.where(status: %w[done archived]).count %>/<%= card.children.count %></span><% end %>
39
- <% if card.branch_name && card.pr_url.blank? %><span class="chip branch">🌿 <%= card.branch_name.delete_prefix("cardinal/") %></span><% end %>
39
+ <% if card.branch_name.present? && card.pr_url.blank? %><span class="chip branch">🌿 <%= card.branch_name.delete_prefix("cardinal/") %></span><% end %>
40
40
  </div>
41
41
  <% end %>
42
- <% if card.pr_url %>
42
+ <% if card.pr_url.present? %>
43
43
  <a class="card-footer" href="<%= card.pr_url %>" target="_blank" rel="noopener" title="Open the pull request on GitHub">
44
44
  <span class="footer-left"><%# future: Asana / Trello / linked-ticket slot %></span>
45
45
  <span class="footer-pr">GitHub #<%= card.pr_url[%r{/pull/(\d+)}, 1] %> ↗</span>
@@ -8,7 +8,7 @@
8
8
  <span class="card-number-sub" title="Card number — how branches, PRs, and other cards refer to this card">#<%= @card.number %></span>
9
9
  </h1>
10
10
  <div class="modal-header-right">
11
- <% if @card.branch_name %>
11
+ <% if @card.branch_name.present? %>
12
12
  <span class="git-line">
13
13
  <span class="branch-base"><%= @card.board.default_branch %></span>
14
14
  <span class="git-arrow">→</span>
@@ -17,7 +17,7 @@
17
17
  <button type="button" class="copy-btn" data-clipboard-target="button"
18
18
  data-action="clipboard#copy" title="Copy branch name">⧉</button>
19
19
  </span>
20
- <% if @card.pr_url %>
20
+ <% if @card.pr_url.present? %>
21
21
  <span class="git-arrow">→</span>
22
22
  <%= link_to "##{@card.pr_url[%r{/pull/(\d+)}, 1]}", @card.pr_url, target: "_blank", rel: "noopener", class: "pr-link" %>
23
23
  <% if @card.pr_state.present? %><span class="pr-state">(<%= @card.pr_state %>)</span><% end %>
@@ -113,7 +113,7 @@
113
113
 
114
114
  <% if @card.column.review? %>
115
115
  <h3>Review</h3>
116
- <% if @card.pr_url %>
116
+ <% if @card.pr_url.present? %>
117
117
  <%= link_to "View Pull Request", @card.pr_url, target: "_blank", rel: "noopener", class: "pr-view-btn" %>
118
118
  <% end %>
119
119
  <% if @card.in_review? %>
@@ -168,7 +168,7 @@
168
168
  </li>
169
169
  <% end %>
170
170
  </ul>
171
- <% if @card.pr_url %>
171
+ <% if @card.pr_url.present? %>
172
172
  <p>🌿 <%= link_to "View pull request", @card.pr_url, target: "_blank" %><%= " (#{@card.pr_state})" if @card.pr_state.present? %></p>
173
173
  <% end %>
174
174
  <% else %>
@@ -22,4 +22,15 @@
22
22
  <%= render "cards/card", card: card %>
23
23
  <% end %>
24
24
  </div>
25
+ <% rows = column.footer_rows %>
26
+ <% if rows.any? %>
27
+ <footer class="column-footer">
28
+ <% rows.each do |row| %>
29
+ <div class="footer-row">
30
+ <span class="footer-label"><%= row[:label] %></span>
31
+ <span class="footer-value"><%= row[:value] %></span>
32
+ </div>
33
+ <% end %>
34
+ </footer>
35
+ <% end %>
25
36
  </section>
@@ -105,6 +105,14 @@
105
105
  <%= info_tip("The agent's first pass is read-only: it explores and proposes a plan, then waits. You approve with one click or reply to redirect. Your main safety rail.") %>
106
106
  </label>
107
107
 
108
+ <% if @column.execution? %>
109
+ <label class="check-row">
110
+ <%= f.check_box :shell, checked: @column.shell_access? %>
111
+ Shell access — the agent may run commands (tests, builds, git)
112
+ <%= info_tip("Unchecked: the agent gets file tools only (read, search, edit, write) and physically cannot execute anything — Cardinal commits and pushes its edits for it. Safer, but it can't run your tests or generators, and the agent process runs on this machine either way.") %>
113
+ </label>
114
+ <% end %>
115
+
108
116
  </div>
109
117
 
110
118
  <label>On-entry rules <%= info_tip("What happens when a card lands in this column, in plain English — e.g. \"start the worker agent\" or \"have an AI suggest tags and a better title\". Cardinal compiles it to rule actions when you save. Blank = the archetype's default behavior shown below.") %></label>
@@ -125,6 +133,11 @@
125
133
  </details>
126
134
  </div>
127
135
  <% end %>
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>
138
+ <%= f.text_area :footer_text, rows: 3, class: "mono",
139
+ value: footer_config_text(@column),
140
+ placeholder: "Total cost: | sum_cost" %>
128
141
  <% end %>
129
142
 
130
143
  <details class="advanced-rules panel-advanced">
@@ -22,7 +22,7 @@ require "securerandom"
22
22
 
23
23
  if ENV["CARDINAL_GEM"] == "1"
24
24
  # Installed-gem mode: no Bundler — load what Bundler.require would have.
25
- %w[propshaft importmap-rails turbo-rails stimulus-rails redcarpet sqlite3].each { |g| require g }
25
+ %w[propshaft importmap-rails turbo-rails stimulus-rails redcarpet sqlite3 solid_queue solid_cable].each { |g| require g }
26
26
  else
27
27
  # Require the gems listed in Gemfile, including any gems
28
28
  # you've limited to :test, :development, or :production.
data/config/cable.yml CHANGED
@@ -1,5 +1,13 @@
1
+ # Jobs run in SolidQueue worker processes (forked by the puma plugin), so
2
+ # Turbo broadcasts cross process boundaries — the async adapter can't carry
3
+ # them. SolidCable (SQLite-backed pubsub) works in dev and prod alike.
1
4
  development:
2
- adapter: async
5
+ adapter: solid_cable
6
+ connects_to:
7
+ database:
8
+ writing: cable
9
+ polling_interval: 0.1.seconds
10
+ message_retention: 1.day
3
11
 
4
12
  test:
5
13
  adapter: test
data/config/database.yml CHANGED
@@ -6,8 +6,17 @@ default: &default
6
6
  timeout: 5000
7
7
 
8
8
  development:
9
- <<: *default
10
- database: <%= ENV.fetch("CARDINAL_DATA_DIR", ".cardinal") %>/cardinal.db
9
+ primary:
10
+ <<: *default
11
+ database: <%= ENV.fetch("CARDINAL_DATA_DIR", ".cardinal") %>/cardinal.db
12
+ queue:
13
+ <<: *default
14
+ database: <%= ENV.fetch("CARDINAL_DATA_DIR", ".cardinal") %>/queue.sqlite3
15
+ migrations_paths: db/queue_migrate
16
+ cable:
17
+ <<: *default
18
+ database: <%= ENV.fetch("CARDINAL_DATA_DIR", ".cardinal") %>/cable.sqlite3
19
+ migrations_paths: db/cable_migrate
11
20
 
12
21
  test:
13
22
  <<: *default
@@ -1,6 +1,11 @@
1
1
  require "active_support/core_ext/integer/time"
2
2
 
3
3
  Rails.application.configure do
4
+ # Durable jobs even in dev (dogfooding runs real agent work): SolidQueue in
5
+ # its own SQLite db; the puma plugin supervises workers — no extra service.
6
+ config.active_job.queue_adapter = :solid_queue
7
+ config.solid_queue.connects_to = { database: { writing: :queue } }
8
+
4
9
  # Settings specified here will take precedence over those in config/application.rb.
5
10
 
6
11
  # Make code changes take effect immediately without server restart.
@@ -50,7 +50,8 @@ Rails.application.configure do
50
50
  # config.cache_store = :mem_cache_store
51
51
 
52
52
  # Replace the default in-process and non-durable queuing backend for Active Job.
53
- # config.active_job.queue_adapter = :resque
53
+ config.active_job.queue_adapter = :solid_queue
54
+ config.solid_queue.connects_to = { database: { writing: :queue } }
54
55
 
55
56
  # Ignore bad email addresses and do not raise email delivery errors.
56
57
  # Set this to true and configure the email server for immediate delivery to raise delivery errors.
data/config/puma.rb CHANGED
@@ -35,7 +35,7 @@ port ENV.fetch("PORT", 3000)
35
35
  plugin :tmp_restart
36
36
 
37
37
  # Run the Solid Queue supervisor inside of Puma for single-server deployments.
38
- plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"]
38
+ plugin :solid_queue if ENV.fetch("SOLID_QUEUE_IN_PUMA", "1") == "1" && !ENV["RAILS_ENV"].to_s.start_with?("test")
39
39
 
40
40
  # Specify the PID file. Defaults to tmp/pids/server.pid in development.
41
41
  # In other environments, only set the PID file if requested.
data/config/queue.yml ADDED
@@ -0,0 +1,18 @@
1
+ default: &default
2
+ dispatchers:
3
+ - polling_interval: 1
4
+ batch_size: 500
5
+ workers:
6
+ - queues: "*"
7
+ threads: 3
8
+ processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %>
9
+ polling_interval: 0.1
10
+
11
+ development:
12
+ <<: *default
13
+
14
+ test:
15
+ <<: *default
16
+
17
+ production:
18
+ <<: *default
@@ -0,0 +1,15 @@
1
+ # examples:
2
+ # periodic_cleanup:
3
+ # class: CleanSoftDeletedRecordsJob
4
+ # queue: background
5
+ # args: [ 1000, { batch_size: 500 } ]
6
+ # schedule: every hour
7
+ # periodic_cleanup_with_command:
8
+ # command: "SoftDeletedRecord.due.delete_all"
9
+ # priority: 2
10
+ # schedule: at 5am every day
11
+
12
+ production:
13
+ clear_solid_queue_finished_jobs:
14
+ command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)"
15
+ schedule: every hour at minute 12
data/config/routes.rb CHANGED
@@ -1,7 +1,11 @@
1
1
  Rails.application.routes.draw do
2
2
  root "boards#show"
3
3
 
4
- resource :board, only: :show
4
+ resource :board, only: [:show, :edit, :update] do
5
+ post :deep_dive
6
+ post :pull
7
+ get :brief
8
+ end
5
9
  resources :cards, only: [:new, :create, :show, :update, :destroy] do
6
10
  member do
7
11
  patch :move
@@ -0,0 +1,11 @@
1
+ ActiveRecord::Schema[7.1].define(version: 1) do
2
+ create_table "solid_cable_messages", force: :cascade do |t|
3
+ t.binary "channel", limit: 1024, null: false
4
+ t.binary "payload", limit: 536870912, null: false
5
+ t.datetime "created_at", null: false
6
+ t.integer "channel_hash", limit: 8, null: false
7
+ t.index ["channel"], name: "index_solid_cable_messages_on_channel"
8
+ t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash"
9
+ t.index ["created_at"], name: "index_solid_cable_messages_on_created_at"
10
+ end
11
+ end
@@ -0,0 +1,8 @@
1
+ class AddRepoBriefToBoards < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :boards, :brief_sha, :string
4
+ add_column :boards, :brief_generated_at, :datetime
5
+ add_column :boards, :brief_model, :string
6
+ add_column :boards, :brief_status, :string
7
+ end
8
+ end
@@ -0,0 +1,129 @@
1
+ ActiveRecord::Schema[7.1].define(version: 1) do
2
+ create_table "solid_queue_blocked_executions", force: :cascade do |t|
3
+ t.bigint "job_id", null: false
4
+ t.string "queue_name", null: false
5
+ t.integer "priority", default: 0, null: false
6
+ t.string "concurrency_key", null: false
7
+ t.datetime "expires_at", null: false
8
+ t.datetime "created_at", null: false
9
+ t.index [ "concurrency_key", "priority", "job_id" ], name: "index_solid_queue_blocked_executions_for_release"
10
+ t.index [ "expires_at", "concurrency_key" ], name: "index_solid_queue_blocked_executions_for_maintenance"
11
+ t.index [ "job_id" ], name: "index_solid_queue_blocked_executions_on_job_id", unique: true
12
+ end
13
+
14
+ create_table "solid_queue_claimed_executions", force: :cascade do |t|
15
+ t.bigint "job_id", null: false
16
+ t.bigint "process_id"
17
+ t.datetime "created_at", null: false
18
+ t.index [ "job_id" ], name: "index_solid_queue_claimed_executions_on_job_id", unique: true
19
+ t.index [ "process_id", "job_id" ], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id"
20
+ end
21
+
22
+ create_table "solid_queue_failed_executions", force: :cascade do |t|
23
+ t.bigint "job_id", null: false
24
+ t.text "error"
25
+ t.datetime "created_at", null: false
26
+ t.index [ "job_id" ], name: "index_solid_queue_failed_executions_on_job_id", unique: true
27
+ end
28
+
29
+ create_table "solid_queue_jobs", force: :cascade do |t|
30
+ t.string "queue_name", null: false
31
+ t.string "class_name", null: false
32
+ t.text "arguments"
33
+ t.integer "priority", default: 0, null: false
34
+ t.string "active_job_id"
35
+ t.datetime "scheduled_at"
36
+ t.datetime "finished_at"
37
+ t.string "concurrency_key"
38
+ t.datetime "created_at", null: false
39
+ t.datetime "updated_at", null: false
40
+ t.index [ "active_job_id" ], name: "index_solid_queue_jobs_on_active_job_id"
41
+ t.index [ "class_name" ], name: "index_solid_queue_jobs_on_class_name"
42
+ t.index [ "finished_at" ], name: "index_solid_queue_jobs_on_finished_at"
43
+ t.index [ "queue_name", "finished_at" ], name: "index_solid_queue_jobs_for_filtering"
44
+ t.index [ "scheduled_at", "finished_at" ], name: "index_solid_queue_jobs_for_alerting"
45
+ end
46
+
47
+ create_table "solid_queue_pauses", force: :cascade do |t|
48
+ t.string "queue_name", null: false
49
+ t.datetime "created_at", null: false
50
+ t.index [ "queue_name" ], name: "index_solid_queue_pauses_on_queue_name", unique: true
51
+ end
52
+
53
+ create_table "solid_queue_processes", force: :cascade do |t|
54
+ t.string "kind", null: false
55
+ t.datetime "last_heartbeat_at", null: false
56
+ t.bigint "supervisor_id"
57
+ t.integer "pid", null: false
58
+ t.string "hostname"
59
+ t.text "metadata"
60
+ t.datetime "created_at", null: false
61
+ t.string "name", null: false
62
+ t.index [ "last_heartbeat_at" ], name: "index_solid_queue_processes_on_last_heartbeat_at"
63
+ t.index [ "name", "supervisor_id" ], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true
64
+ t.index [ "supervisor_id" ], name: "index_solid_queue_processes_on_supervisor_id"
65
+ end
66
+
67
+ create_table "solid_queue_ready_executions", force: :cascade do |t|
68
+ t.bigint "job_id", null: false
69
+ t.string "queue_name", null: false
70
+ t.integer "priority", default: 0, null: false
71
+ t.datetime "created_at", null: false
72
+ t.index [ "job_id" ], name: "index_solid_queue_ready_executions_on_job_id", unique: true
73
+ t.index [ "priority", "job_id" ], name: "index_solid_queue_poll_all"
74
+ t.index [ "queue_name", "priority", "job_id" ], name: "index_solid_queue_poll_by_queue"
75
+ end
76
+
77
+ create_table "solid_queue_recurring_executions", force: :cascade do |t|
78
+ t.bigint "job_id", null: false
79
+ t.string "task_key", null: false
80
+ t.datetime "run_at", null: false
81
+ t.datetime "created_at", null: false
82
+ t.index [ "job_id" ], name: "index_solid_queue_recurring_executions_on_job_id", unique: true
83
+ t.index [ "task_key", "run_at" ], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true
84
+ end
85
+
86
+ create_table "solid_queue_recurring_tasks", force: :cascade do |t|
87
+ t.string "key", null: false
88
+ t.string "schedule", null: false
89
+ t.string "command", limit: 2048
90
+ t.string "class_name"
91
+ t.text "arguments"
92
+ t.string "queue_name"
93
+ t.integer "priority", default: 0
94
+ t.boolean "static", default: true, null: false
95
+ t.text "description"
96
+ t.datetime "created_at", null: false
97
+ t.datetime "updated_at", null: false
98
+ t.index [ "key" ], name: "index_solid_queue_recurring_tasks_on_key", unique: true
99
+ t.index [ "static" ], name: "index_solid_queue_recurring_tasks_on_static"
100
+ end
101
+
102
+ create_table "solid_queue_scheduled_executions", force: :cascade do |t|
103
+ t.bigint "job_id", null: false
104
+ t.string "queue_name", null: false
105
+ t.integer "priority", default: 0, null: false
106
+ t.datetime "scheduled_at", null: false
107
+ t.datetime "created_at", null: false
108
+ t.index [ "job_id" ], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true
109
+ t.index [ "scheduled_at", "priority", "job_id" ], name: "index_solid_queue_dispatch_all"
110
+ end
111
+
112
+ create_table "solid_queue_semaphores", force: :cascade do |t|
113
+ t.string "key", null: false
114
+ t.integer "value", default: 1, null: false
115
+ t.datetime "expires_at", null: false
116
+ t.datetime "created_at", null: false
117
+ t.datetime "updated_at", null: false
118
+ t.index [ "expires_at" ], name: "index_solid_queue_semaphores_on_expires_at"
119
+ t.index [ "key", "value" ], name: "index_solid_queue_semaphores_on_key_and_value"
120
+ t.index [ "key" ], name: "index_solid_queue_semaphores_on_key", unique: true
121
+ end
122
+
123
+ add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
124
+ add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
125
+ add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
126
+ add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
127
+ add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
128
+ add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
129
+ end
data/exe/cardinal CHANGED
@@ -106,6 +106,9 @@ end
106
106
  # scaffolding inside the engine/gem directory — read-only for sudo installs).
107
107
  ENV["PIDFILE"] = File.join(TARGET, ".cardinal", "tmp", "server.pid")
108
108
  ENV["CARDINAL_SWEEPER"] = "1" # the sweeper initializer keys off Rails::Server otherwise
109
+ # Localhost only: the board drives your logged-in gh/claude sessions, so it
110
+ # must not be reachable by others on the network. CARDINAL_HOST=0.0.0.0
111
+ # deliberately exposes it (e.g. to browse from a phone on your LAN).
109
112
  exec(RbConfig.ruby, "-S", "puma",
110
- "-b", "tcp://0.0.0.0:#{ENV["PORT"]}",
113
+ "-b", "tcp://#{ENV.fetch("CARDINAL_HOST", "127.0.0.1")}:#{ENV["PORT"]}",
111
114
  File.join(HOME_DIR, "config.ru"))
@@ -1,3 +1,3 @@
1
1
  module Cardinal
2
- VERSION = "0.2.4"
2
+ VERSION = "0.2.6"
3
3
  end