cardinal-ai 0.0.1 → 0.2.3

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.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -0
  3. data/README.md +50 -29
  4. data/Rakefile +6 -0
  5. data/app/assets/stylesheets/application.css +10 -0
  6. data/app/assets/stylesheets/cardinal.css +514 -0
  7. data/app/controllers/application_controller.rb +7 -0
  8. data/app/controllers/boards_controller.rb +5 -0
  9. data/app/controllers/cards_controller.rb +129 -0
  10. data/app/controllers/columns_controller.rb +95 -0
  11. data/app/controllers/messages_controller.rb +25 -0
  12. data/app/controllers/runs_controller.rb +58 -0
  13. data/app/helpers/application_helper.rb +35 -0
  14. data/app/javascript/application.js +2 -0
  15. data/app/javascript/controllers/application.js +7 -0
  16. data/app/javascript/controllers/autosave_controller.js +28 -0
  17. data/app/javascript/controllers/board_column_controller.js +96 -0
  18. data/app/javascript/controllers/clipboard_controller.js +18 -0
  19. data/app/javascript/controllers/composer_controller.js +10 -0
  20. data/app/javascript/controllers/index.js +3 -0
  21. data/app/javascript/controllers/modal_controller.js +43 -0
  22. data/app/javascript/controllers/scroll_controller.js +44 -0
  23. data/app/javascript/controllers/tags_controller.js +49 -0
  24. data/app/javascript/controllers/theme_controller.js +43 -0
  25. data/app/javascript/controllers/tooltip_controller.js +37 -0
  26. data/app/jobs/ai_task_job.rb +26 -0
  27. data/app/jobs/application_job.rb +7 -0
  28. data/app/jobs/assistant_reply_job.rb +132 -0
  29. data/app/jobs/mark_pr_ready_job.rb +18 -0
  30. data/app/jobs/merge_pr_job.rb +27 -0
  31. data/app/jobs/resume_run_job.rb +30 -0
  32. data/app/jobs/start_run_job.rb +13 -0
  33. data/app/mailers/application_mailer.rb +4 -0
  34. data/app/models/agent_session.rb +8 -0
  35. data/app/models/application_record.rb +3 -0
  36. data/app/models/artifact.rb +8 -0
  37. data/app/models/board.rb +60 -0
  38. data/app/models/card.rb +83 -0
  39. data/app/models/column.rb +83 -0
  40. data/app/models/event.rb +44 -0
  41. data/app/models/run.rb +28 -0
  42. data/app/services/agent/runner.rb +379 -0
  43. data/app/services/agent/workspace.rb +138 -0
  44. data/app/services/card_transition.rb +97 -0
  45. data/app/services/claude_cli.rb +89 -0
  46. data/app/services/rules/compiler.rb +55 -0
  47. data/app/services/rules.rb +67 -0
  48. data/app/services/run_sweeper.rb +52 -0
  49. data/app/views/boards/show.html.erb +79 -0
  50. data/app/views/cards/_card.html.erb +48 -0
  51. data/app/views/cards/_detail.html.erb +190 -0
  52. data/app/views/cards/_tag_picker.html.erb +12 -0
  53. data/app/views/cards/new.html.erb +35 -0
  54. data/app/views/cards/show.html.erb +3 -0
  55. data/app/views/columns/_column.html.erb +25 -0
  56. data/app/views/columns/edit.html.erb +126 -0
  57. data/app/views/events/_event.html.erb +29 -0
  58. data/app/views/layouts/application.html.erb +46 -0
  59. data/app/views/layouts/mailer.html.erb +13 -0
  60. data/app/views/layouts/mailer.text.erb +1 -0
  61. data/app/views/pwa/manifest.json.erb +22 -0
  62. data/app/views/pwa/service-worker.js +26 -0
  63. data/bin/rails +4 -0
  64. data/bin/rake +4 -0
  65. data/cardinal.md +686 -0
  66. data/config/application.rb +60 -0
  67. data/config/boot.rb +13 -0
  68. data/config/bundler-audit.yml +5 -0
  69. data/config/cable.yml +13 -0
  70. data/config/ci.rb +20 -0
  71. data/config/credentials.yml.enc +1 -0
  72. data/config/database.yml +31 -0
  73. data/config/environment.rb +5 -0
  74. data/config/environments/development.rb +78 -0
  75. data/config/environments/production.rb +89 -0
  76. data/config/environments/test.rb +53 -0
  77. data/config/importmap.rb +6 -0
  78. data/config/initializers/assets.rb +7 -0
  79. data/config/initializers/cardinal_bootstrap.rb +12 -0
  80. data/config/initializers/cardinal_instance.rb +20 -0
  81. data/config/initializers/content_security_policy.rb +29 -0
  82. data/config/initializers/filter_parameter_logging.rb +8 -0
  83. data/config/initializers/inflections.rb +16 -0
  84. data/config/initializers/run_sweeper.rb +17 -0
  85. data/config/locales/en.yml +31 -0
  86. data/config/puma.rb +42 -0
  87. data/config/routes.rb +22 -0
  88. data/config/storage.yml +27 -0
  89. data/config.ru +6 -0
  90. data/db/migrate/20260703000001_create_cardinal_schema.rb +78 -0
  91. data/db/migrate/20260703000002_add_agent_runner_fields.rb +7 -0
  92. data/db/migrate/20260704000001_add_parent_to_cards.rb +5 -0
  93. data/db/migrate/20260704000002_add_assistant_session_to_cards.rb +5 -0
  94. data/db/seeds.rb +19 -0
  95. data/docker/agent/Dockerfile +16 -0
  96. data/exe/cardinal +111 -0
  97. data/lib/cardinal/version.rb +1 -1
  98. data/public/400.html +135 -0
  99. data/public/404.html +135 -0
  100. data/public/406-unsupported-browser.html +135 -0
  101. data/public/422.html +135 -0
  102. data/public/500.html +135 -0
  103. data/public/icon.png +0 -0
  104. data/public/icon.svg +3 -0
  105. data/public/robots.txt +1 -0
  106. data/vendor/javascript/sortablejs.js +3378 -0
  107. metadata +235 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 446132d856f0db6104e2df65f61510d970b1b1044ac86eb49b7e87da174ef04b
4
- data.tar.gz: 48b6a1a1014d1dc6b354fac38aba8ab4dd49cbdba8c921fddaae884f9e425d16
3
+ metadata.gz: decfcea83bc4294b6cf14e947fa1586dee954dfaa30250b67df7d1c29377c191
4
+ data.tar.gz: f6718005edcdeda0d1608d13d93211c7785af98ca2297ef418d96457724c6d1d
5
5
  SHA512:
6
- metadata.gz: 7d83e1cc64403965c0856577ae139effc8d73ea971aa2f25e7a73cc1c4b82419bd90f6b00c283ba84d5b545ec6a5cf4827bd5e7d675e7a7a1a180544b96ed5cd
7
- data.tar.gz: 9fbe3ab30ea42683d7726bbabb8d8ccfdb9329cc0429487c885341634a450dfe9952f7e8af29bcf2c0e1d1088dfac3997a38803dceebb4f144c977d8309dcd55
6
+ metadata.gz: 7c106ef6387b43103682e495071e3dfa1d04701eea84c2192f9c67c132ae623a5fa9614294e7e59606e0fad300cf9147008013906d6d9981799e4e3ea0740c89
7
+ data.tar.gz: 1e2b72553d1bd2c6fd678bd7a3bd8c49c5ec6acc902026546d51d7e0fa9fe783803285e4f2933319f52f1193f1850cf3dc516fb6439b402ef97daa78393f96e3
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jason Ellis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -1,43 +1,64 @@
1
- # Cardinal 🐦‍🔥
1
+ # Cardinal AI 🐦‍🔥
2
2
 
3
3
  **A Kanban board where dragging a card to "In Progress" hires an AI to actually do the task.**
4
4
 
5
- Cardinal is a Kanban board where the cards do the work. It's a little tool you fire up
6
- inside any code repo, and it gives you a board like Trello but the columns aren't just
7
- labels, they're rules. The far-left column is where you dump ideas. The next one has an AI
8
- assistant that helps you think each idea through. And when you drag a card into
9
- "In Progress," that card *becomes* its own AI agent — it spins up in a sandbox, writes the
10
- code on its own branch, reports its progress right on the card, and asks you questions when
11
- it's stuck. When it's done, you drag the card to Review, look at the pull request it made,
12
- and either send it back with notes or drag it to Done — which merges the code.
5
+ Cardinal AI puts a board on any of your projects. You write down what needs doing, chat
6
+ with an assistant to sharpen the idea, and then drag the card forwardat which point an
7
+ AI agent picks it up, does the work on its own branch, asks you questions when it's stuck,
8
+ and hands you a pull request to review. Approve it, drag the card to Done, and the work is
9
+ merged. You never leave the board.
13
10
 
14
- It's not an app you sign up for. It's more like having a small dev team living in your
15
- repo, and the board is how you manage them. Dragging a card left to right literally *is*
16
- assigning the work, supervising it, and shipping it.
11
+ ## What you need
17
12
 
18
- ## Status
13
+ - **Ruby 3.2 or newer** (`ruby -v` to check)
14
+ - **The Claude CLI**, signed in — this is the AI: `npm install -g @anthropic-ai/claude-code`, then `claude` once to log in
15
+ - **git**, and for pull requests the **GitHub CLI** (`gh auth login`)
19
16
 
20
- Early but real: the full card lifecycle works end to end. Cards become agents in
21
- execution columns (plan approval → work → questions back to you → draft PR), you review
22
- and request changes (revision runs on the same branch), and dragging to Done squash-merges
23
- the PR. Column rules, one-shot AI maintenance agents, a policy editor behind every
24
- column's gear icon, run heartbeats + sweeping, and `cardinal up` for spinning a board up
25
- inside any repo. PRs #2 and #3 of this very repo were written by Cardinal cards. The
26
- design document — architecture, decisions, roadmap — lives in [cardinal.md](cardinal.md).
17
+ ## Install
27
18
 
28
- ## Stack
19
+ ```sh
20
+ gem install cardinal-ai
21
+ ```
29
22
 
30
- Rails 8.1 · Ruby 3.4 · SQLite (the whole instance lives in `.cardinal/`) · Hotwire.
31
- No database server, no Redis, no sign-in.
23
+ ## Use it
32
24
 
33
- ## Running it
25
+ Go to any project that lives in git, and start Cardinal:
34
26
 
35
27
  ```sh
36
- bundle install
37
- bin/rails db:prepare db:seed
38
- bin/rails server
28
+ cd your-project
29
+ cardinal
39
30
  ```
40
31
 
41
- Then open http://localhost:3000 (or set `PORT`).
32
+ The first time, a browser window asks **which Claude account this board should work as** —
33
+ pick one, and it's remembered for this project only. Then open **http://localhost:4000**.
34
+
35
+ That's the whole setup. Now:
36
+
37
+ 1. **Add a card** for something you want done, in plain English.
38
+ 2. **Drag it to Planning** — an assistant reads your card *and your code*, then asks the
39
+ questions that make the task clear. Chat until it feels right.
40
+ 3. **Drag it to In Progress** — an agent studies the repo and proposes a plan. One click
41
+ to approve. Then it works: you can watch its progress live on the card, and it will
42
+ stop and ask you if it hits a real decision.
43
+ 4. **Review** — read the final report and the pull request. Say what's wrong in the
44
+ card's conversation to send it back, or approve.
45
+ 5. **QA** — the pull request goes live for formal review on GitHub.
46
+ 6. **Drag to Done** — the pull request merges. Shipped.
47
+
48
+ Every column has a ⚙ gear where you can change the rules — which AI model works there,
49
+ how many cards can run at once, spending limits, and what happens when a card arrives
50
+ (written in plain English; Cardinal figures out the rest).
51
+
52
+ ## Good to know
53
+
54
+ - Everything Cardinal knows about a project lives in a `.cardinal/` folder inside it,
55
+ invisible to git. Delete the folder and Cardinal was never there.
56
+ - Each project's board can use a **different Claude account** (`cardinal login` to switch,
57
+ `cardinal logout` to unlink).
58
+ - Agents can only push to their own card branches — merging is always your drag.
59
+ - AI usage bills the Claude account you linked, the same as using Claude Code.
60
+
61
+ ## For developers
42
62
 
43
- *Drag it to Done.*
63
+ The architecture and design history live in [cardinal.md](cardinal.md). The engine is a
64
+ Rails 8 app — clone, `bundle install`, `bin/rails test`. MIT licensed.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require_relative "config/application"
5
+
6
+ Rails.application.load_tasks
@@ -0,0 +1,10 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css.
3
+ *
4
+ * With Propshaft, assets are served efficiently without preprocessing steps. You can still include
5
+ * application-wide styles in this file, but keep in mind that CSS precedence will follow the standard
6
+ * cascading order, meaning styles declared later in the document or manifest will override earlier ones,
7
+ * depending on specificity.
8
+ *
9
+ * Consider organizing styles into separate files for maintainability.
10
+ */
@@ -0,0 +1,514 @@
1
+ :root {
2
+ --bg: #16181d;
3
+ --surface: #1e2128;
4
+ --surface-2: #262a33;
5
+ --border: #333845;
6
+ --text: #d7dae0;
7
+ --text-dim: #8b91a0;
8
+ --accent: #c94f4f; /* cardinal red */
9
+ --amber: #d9a441;
10
+ --green: #5fae6f;
11
+ --red: #d66;
12
+ --blue: #6b93c9;
13
+ }
14
+
15
+ /* Light theme — light greys, dark grey text. Applied via data-theme="light"
16
+ on <html> (see the theme controller + head boot script). Only the palette
17
+ and a few hardcoded dark-specific colors below need to flip; every surface
18
+ drives its colors through these variables. */
19
+ [data-theme="light"] {
20
+ --bg: #eceef1;
21
+ --surface: #f6f7f9;
22
+ --surface-2: #ffffff;
23
+ --border: #d3d7de;
24
+ --text: #23262c;
25
+ --text-dim: #646b78;
26
+ --accent: #c1443f; /* cardinal red */
27
+ --amber: #b5791b;
28
+ --green: #3f8a50;
29
+ --red: #c23b30;
30
+ --blue: #4d76ad;
31
+ }
32
+
33
+ * { box-sizing: border-box; }
34
+
35
+ body {
36
+ margin: 0;
37
+ background: var(--bg);
38
+ color: var(--text);
39
+ font: 14px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
40
+ }
41
+
42
+ a { color: var(--blue); text-decoration: none; }
43
+
44
+ /* ── Top bar ─────────────────────────────────────────── */
45
+ .topbar {
46
+ display: flex;
47
+ align-items: center;
48
+ justify-content: space-between;
49
+ padding: 10px 18px;
50
+ border-bottom: 1px solid var(--border);
51
+ background: var(--surface);
52
+ }
53
+ .topbar h1 { font-size: 16px; margin: 0; font-weight: 600; }
54
+ .topbar h1 .sep { color: var(--accent); margin: 0 4px; }
55
+ .topbar-right { display: flex; align-items: center; gap: 14px; }
56
+
57
+ .theme-toggle {
58
+ background: transparent; border: 1px solid var(--border); border-radius: 6px;
59
+ color: var(--text-dim); font-weight: 600; padding: 5px 10px; line-height: 1;
60
+ }
61
+ .theme-toggle:hover { color: var(--text); border-color: var(--text-dim); }
62
+
63
+ .attention summary {
64
+ cursor: pointer;
65
+ color: var(--amber);
66
+ font-weight: 600;
67
+ list-style: none;
68
+ }
69
+ .auth-chip {
70
+ font-size: 11px; color: var(--text-dim); border: 1px solid var(--border);
71
+ border-radius: 10px; padding: 2px 9px; cursor: default;
72
+ }
73
+
74
+ .attention { position: relative; }
75
+ .attention-list {
76
+ position: absolute; right: 0; top: 26px; z-index: 20;
77
+ background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px;
78
+ padding: 8px 12px; margin: 0; min-width: 300px; max-height: 60vh; overflow-y: auto;
79
+ }
80
+ .attention ul { list-style: none; margin: 0 0 6px; padding: 0; }
81
+ .attention li { padding: 4px 0; }
82
+ .attn-part { margin-left: 10px; }
83
+ .attn-part:first-child { margin-left: 0; }
84
+ .attn-part.working-part { color: var(--blue); }
85
+ .attn-header {
86
+ font-size: 10px; text-transform: uppercase; letter-spacing: .07em;
87
+ color: var(--text-dim); margin: 8px 0 2px; font-weight: 700;
88
+ }
89
+ .attn-header:first-child { margin-top: 0; }
90
+ .attn-working { display: flex; align-items: center; gap: 6px; }
91
+ .pulse-dot {
92
+ display: inline-block; width: 7px; height: 7px; border-radius: 50%;
93
+ background: var(--blue); animation: pulse-dot 1.6s ease-in-out infinite;
94
+ flex-shrink: 0;
95
+ }
96
+ @keyframes pulse-dot {
97
+ 0%, 100% { opacity: .35; transform: scale(.85); }
98
+ 50% { opacity: 1; transform: scale(1.15); }
99
+ }
100
+
101
+ .add-card {
102
+ cursor: pointer; color: var(--text-dim); font-weight: 700;
103
+ line-height: 1; padding: 0 4px; margin-bottom: 6px; text-decoration: none;
104
+ }
105
+ .add-card:hover { color: var(--text); }
106
+
107
+ button, input[type="submit"] {
108
+ background: var(--accent); color: #fff; border: 0; border-radius: 6px;
109
+ padding: 6px 12px; cursor: pointer; font-weight: 600;
110
+ }
111
+
112
+ .new-column { position: relative; }
113
+ .new-column summary { cursor: pointer; color: var(--text-dim); font-weight: 600; list-style: none; }
114
+ .new-column summary:hover { color: var(--text); }
115
+ .new-column-form {
116
+ position: absolute; right: 0; top: 28px; z-index: 20;
117
+ background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px;
118
+ padding: 10px; display: flex; flex-direction: column; gap: 8px; min-width: 220px;
119
+ }
120
+ .new-column-form input[type="text"], .new-column-form select {
121
+ background: var(--surface); border: 1px solid var(--border); border-radius: 6px;
122
+ color: var(--text); padding: 6px 10px;
123
+ }
124
+
125
+ /* ── Board ───────────────────────────────────────────── */
126
+ .board {
127
+ display: flex;
128
+ gap: 12px;
129
+ padding: 14px;
130
+ align-items: stretch;
131
+ overflow-x: auto;
132
+ height: calc(100vh - 53px);
133
+ }
134
+
135
+ .column {
136
+ background: var(--surface);
137
+ border: 1px solid var(--border);
138
+ border-radius: 10px;
139
+ min-width: 250px;
140
+ width: 250px;
141
+ padding: 10px;
142
+ display: flex;
143
+ flex-direction: column;
144
+ }
145
+ .column-execution { background: #1c2a21; border-color: #375844; }
146
+ .column-review { background: #24211c; border-color: #4a4232; }
147
+ [data-theme="light"] .column-execution { background: #e9f3ec; border-color: #bcd8c4; }
148
+ [data-theme="light"] .column-review { background: #f6f1e6; border-color: #e3d7bf; }
149
+ .column-header { display: flex; justify-content: space-between; align-items: center; }
150
+ .column-title { display: flex; align-items: center; gap: 2px; }
151
+ .column-header h2 { font-size: 13px; text-transform: uppercase; letter-spacing: .06em; margin: 2px 4px 8px; color: var(--text-dim); }
152
+ .gear { cursor: pointer; color: var(--text-dim); }
153
+ .drop-hint { display: none; font-size: 11px; color: var(--amber); margin: 0 4px 8px; }
154
+ body.dragging .drop-hint { display: block; }
155
+ .ticker { font-size: 11px; color: var(--text-dim); margin: 0 4px 8px; }
156
+
157
+ .cards { min-height: 40px; display: flex; flex-direction: column; gap: 8px; flex: 1; overflow-y: auto; }
158
+ .cards-clickable { cursor: pointer; }
159
+ .agent-chip { color: var(--blue); }
160
+
161
+ /* ── Cards ───────────────────────────────────────────── */
162
+ .card {
163
+ background: var(--surface-2);
164
+ border: 1px solid var(--border);
165
+ border-radius: 8px;
166
+ padding: 9px 10px;
167
+ cursor: grab;
168
+ }
169
+ .card-link { color: inherit; display: block; }
170
+
171
+ /* PR footer: the card's bottom edge is the link out to GitHub. The left
172
+ slot is reserved for ticket integrations (Asana/Trello). */
173
+ .card-footer {
174
+ display: flex; justify-content: space-between; align-items: center;
175
+ margin: 8px -10px -9px; padding: 4px 10px 5px;
176
+ background: var(--bg);
177
+ border-top: 1px solid rgba(255, 255, 255, .09);
178
+ border-radius: 0 0 7px 7px;
179
+ font-size: 11px; color: var(--text-dim); font-weight: 600;
180
+ }
181
+ .card-footer:hover .footer-pr { color: var(--blue); }
182
+ .footer-left { min-width: 1px; }
183
+ .card-ghost { opacity: .4; }
184
+ .card-title { font-weight: 600; }
185
+ .card-number { color: var(--text-dim); font-weight: 400; }
186
+ .card-number-sub { color: var(--text-dim); font-weight: 400; font-size: 11px; margin-left: 6px; }
187
+ .status-glyph { float: right; }
188
+ .card-progress { margin: 5px 0 0; font-size: 12px; color: var(--text-dim); overflow: hidden; word-break: break-all; }
189
+ .card-meta { margin-top: 6px; display: flex; flex-wrap: wrap; gap: 4px; }
190
+ .tag, .chip {
191
+ font-size: 11px; padding: 1px 7px; border-radius: 10px;
192
+ background: var(--surface); border: 1px solid var(--border); color: var(--text-dim);
193
+ }
194
+
195
+ /* Status treatments (§6) */
196
+ .card.status-working { animation: breathe 2.2s ease-in-out infinite; }
197
+ .card.status-queued { opacity: .65; border-style: dashed; }
198
+
199
+ .working-line { color: var(--blue); }
200
+ .spinner {
201
+ display: inline-block; width: 9px; height: 9px;
202
+ border: 2px solid var(--blue); border-top-color: transparent; border-radius: 50%;
203
+ animation: spin 0.9s linear infinite; vertical-align: -1px;
204
+ }
205
+ @keyframes spin { to { transform: rotate(360deg); } }
206
+ .card.status-needs_input { border-color: var(--amber); box-shadow: 0 0 0 1px var(--amber); }
207
+ .card.status-needs_input .attention-text { color: var(--amber); font-weight: 600; }
208
+ .card.status-blocked, .card.status-failed { border-color: var(--red); }
209
+ .card.status-failed .attention-text { color: var(--red); font-weight: 600; }
210
+ .card.status-work_complete { border-color: var(--green); }
211
+ .card.status-work_complete .attention-text { color: var(--green); font-weight: 600; }
212
+ .card.status-in_review { border-color: var(--blue); }
213
+ .card.status-approved { border-color: var(--green); box-shadow: 0 0 0 1px var(--green); }
214
+ .card.status-approved .approved-text { color: var(--green); font-weight: 600; }
215
+ .card.status-changes_requested { border-color: var(--amber); border-style: dashed; }
216
+ .card.status-done, .card.status-archived { opacity: .55; }
217
+
218
+ @keyframes breathe {
219
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(107, 147, 201, 0); border-color: var(--border); }
220
+ 50% { box-shadow: 0 0 0 3px rgba(107, 147, 201, .3); border-color: var(--blue); }
221
+ }
222
+
223
+ /* Bounce-back on a move the destination's accept policy forbids (card #15) */
224
+ .card.move-rejected { animation: reject-flash 0.6s ease; }
225
+ @keyframes reject-flash {
226
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(201, 107, 107, 0); border-color: var(--border); }
227
+ 15%, 55% { box-shadow: 0 0 0 3px rgba(201, 107, 107, .45); border-color: var(--red); }
228
+ }
229
+
230
+ /* ── Card modal ──────────────────────────────────────── */
231
+ .modal-backdrop {
232
+ position: fixed; inset: 0; z-index: 50;
233
+ background: rgba(0, 0, 0, .6);
234
+ display: flex; align-items: center; justify-content: center;
235
+ padding: 3vh 3vw;
236
+ }
237
+ .modal {
238
+ background: var(--bg);
239
+ border: 1px solid var(--border);
240
+ border-radius: 12px;
241
+ width: 100%; height: 100%;
242
+ display: flex; flex-direction: column;
243
+ overflow: hidden;
244
+ box-shadow: 0 20px 60px rgba(0, 0, 0, .5);
245
+ }
246
+ [data-theme="light"] .modal { box-shadow: 0 20px 60px rgba(0, 0, 0, .18); }
247
+ .modal-header {
248
+ display: flex; justify-content: space-between; align-items: center;
249
+ padding: 14px 18px; border-bottom: 1px solid var(--border);
250
+ background: var(--surface); border-radius: 12px 12px 0 0;
251
+ flex-shrink: 0;
252
+ }
253
+ .modal-body { overflow-y: auto; }
254
+ .modal-header h1 { font-size: 17px; margin: 0; font-weight: 600; }
255
+ .modal-header-right { display: flex; align-items: center; gap: 14px; }
256
+ .modal-close {
257
+ background: var(--surface-2); color: var(--text-dim);
258
+ font-size: 15px; line-height: 1; padding: 7px 10px;
259
+ }
260
+ .modal-close:hover { color: var(--text); }
261
+ .modal-sm { max-width: 580px; height: auto; max-height: 88vh; }
262
+ .modal-body { padding: 16px 18px; }
263
+ .modal-body h3 { font-size: 12px; text-transform: uppercase; letter-spacing: .05em; color: var(--text-dim); margin: 16px 0 6px; }
264
+ .modal pre {
265
+ background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
266
+ padding: 10px 12px; font-size: 12px; overflow-x: auto; color: var(--text);
267
+ }
268
+ .git-line { margin: 0; font-size: 12px; color: var(--text-dim); display: inline-flex; align-items: center; gap: 6px; }
269
+ .branch-base { font-family: ui-monospace, monospace; }
270
+ .git-arrow { color: var(--text-dim); }
271
+ .branch-pill {
272
+ display: inline-flex; align-items: center; gap: 4px;
273
+ background: var(--surface-2); border: 1px solid var(--border); border-radius: 6px;
274
+ padding: 2px 4px 2px 8px;
275
+ }
276
+ .branch-pill code { font-size: 12px; font-family: ui-monospace, monospace; color: var(--text); }
277
+ .copy-btn {
278
+ background: none; border: 0; color: var(--text-dim); cursor: pointer;
279
+ font-size: 12px; padding: 0 4px; border-radius: 4px;
280
+ }
281
+ .copy-btn:hover { color: var(--text); background: var(--surface); }
282
+ .copy-btn.copied { color: var(--green); }
283
+ .pr-link { font-weight: 700; }
284
+ .pr-state { font-size: 11px; }
285
+ .status-chip { margin-left: 8px; }
286
+
287
+ .card-edit { display: flex; flex-direction: column; gap: 4px; margin-bottom: 18px; }
288
+ .card-edit label { font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: var(--text-dim); margin-top: 6px; }
289
+ .card-edit .hint { text-transform: none; letter-spacing: 0; }
290
+ .card-edit input[type="text"], .card-edit textarea {
291
+ background: var(--surface-2); border: 1px solid var(--border); border-radius: 6px;
292
+ color: var(--text); padding: 6px 10px; font: inherit;
293
+ }
294
+ .card-edit input[type="submit"] { margin-top: 10px; align-self: flex-end; }
295
+ .card-edit .locked-field { margin: 4px 0 0; font-size: 12px; word-break: break-all; }
296
+ .card-edit .locked-field code { font-family: ui-monospace, monospace; color: var(--text); }
297
+ .card-edit-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 10px; }
298
+ .card-edit-actions input[type="submit"] { margin-top: 0; }
299
+ .btn-cancel { background: transparent; color: var(--text-dim); border: 1px solid var(--border); }
300
+ .btn-cancel:hover { color: var(--text); border-color: var(--text-dim); }
301
+
302
+ .related-list { list-style: none; margin: 0 0 8px; padding: 0; font-size: 13px; }
303
+ .related-list li { padding: 3px 0; }
304
+ .child-card-link { display: inline-block; font-size: 12px; color: var(--text-dim); margin-bottom: 14px; }
305
+ .child-card-link:hover { color: var(--text); }
306
+
307
+ .tag-chips { display: flex; flex-wrap: wrap; gap: 5px; margin: 2px 0 6px; }
308
+ .tag-chip {
309
+ font-size: 11px; padding: 2px 9px; border-radius: 10px; cursor: pointer;
310
+ background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim);
311
+ font-weight: 400;
312
+ }
313
+ .tag-chip.on { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }
314
+ .new-tag-input {
315
+ background: var(--surface-2); border: 1px dashed var(--border); border-radius: 6px;
316
+ color: var(--text); padding: 4px 10px; font-size: 12px; width: 100%;
317
+ }
318
+
319
+ .detail-panes { display: flex; gap: 16px; padding: 16px; flex: 1; min-height: 0; }
320
+ .timeline { flex: 2; display: flex; flex-direction: column; min-height: 0; position: relative; }
321
+
322
+ .new-messages-pill {
323
+ display: none;
324
+ position: absolute; left: 50%; bottom: 78px; transform: translateX(-50%);
325
+ z-index: 10; padding: 5px 14px; border-radius: 14px;
326
+ background: var(--blue); color: #fff; font-size: 12px; font-weight: 600;
327
+ box-shadow: 0 4px 14px rgba(0, 0, 0, .4); cursor: pointer; border: 0;
328
+ }
329
+ .new-messages-pill.visible { display: block; }
330
+ .timeline-scroll { flex: 1; overflow-y: auto; min-height: 0; padding-right: 6px; }
331
+ .work-panel {
332
+ flex: 1; background: var(--surface); border: 1px solid var(--border);
333
+ border-radius: 10px; padding: 12px 14px; overflow-y: auto;
334
+ }
335
+ .work-panel h3 { margin: 0 0 8px; font-size: 13px; text-transform: uppercase; color: var(--text-dim); }
336
+
337
+ .zoom-tabs { display: flex; gap: 4px; margin-bottom: 12px; }
338
+ .zoom-tabs a { padding: 4px 12px; border-radius: 6px; color: var(--text-dim); }
339
+ .zoom-tabs a.active { background: var(--surface-2); color: var(--text); }
340
+
341
+ .event { display: flex; gap: 10px; padding: 8px 4px; border-bottom: 1px solid var(--surface-2); }
342
+
343
+ /* Column moves are chapter markers in the card's story */
344
+ .stage-divider {
345
+ display: flex; align-items: center; gap: 10px;
346
+ margin: 18px 0 8px; color: var(--accent);
347
+ font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .08em;
348
+ }
349
+ .stage-divider::before, .stage-divider::after {
350
+ content: ""; flex: 1; height: 1px; background: var(--border);
351
+ }
352
+ .stage-divider time { color: var(--text-dim); font-weight: 400; letter-spacing: 0; text-transform: none; }
353
+
354
+ /* A move the accept policy blocked — logged but never happened (card #15) */
355
+ .move-rejected-row {
356
+ display: flex; align-items: center; gap: 10px;
357
+ margin: 10px 0; padding: 4px 8px;
358
+ border-left: 2px solid var(--red); border-radius: 3px;
359
+ background: rgba(201, 107, 107, .08); color: var(--red); font-size: 12px;
360
+ }
361
+ .move-rejected-row time { margin-left: auto; color: var(--text-dim); }
362
+ .event-actor { width: 22px; text-align: center; }
363
+ .event-body { flex: 1; }
364
+ .event-body p { margin: 0 0 4px; }
365
+ .event-body time { font-size: 11px; color: var(--text-dim); }
366
+
367
+ /* Markdown inside timeline events */
368
+ .event-body h1, .event-body h2, .event-body h3 {
369
+ font-size: 13px; margin: 12px 0 4px; color: var(--text);
370
+ text-transform: uppercase; letter-spacing: .04em;
371
+ }
372
+ .event-body ul, .event-body ol { margin: 4px 0 8px 20px; padding: 0; }
373
+ .event-body li { margin: 2px 0; }
374
+ .event-body code {
375
+ background: var(--surface); border: 1px solid var(--border); border-radius: 4px;
376
+ padding: 0 4px; font-size: 12px; font-family: ui-monospace, monospace;
377
+ }
378
+ .event-body pre {
379
+ background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
380
+ padding: 8px 10px; overflow-x: auto; margin: 6px 0 8px;
381
+ }
382
+ .event-body pre code { background: none; border: 0; padding: 0; }
383
+ .event-body blockquote {
384
+ margin: 6px 0; padding: 2px 10px; border-left: 2px solid var(--border);
385
+ color: var(--text-dim);
386
+ }
387
+ .event-body table { border-collapse: collapse; margin: 6px 0; font-size: 12px; }
388
+ .event-body th, .event-body td { border: 1px solid var(--border); padding: 3px 8px; }
389
+ .event-kind { color: var(--text-dim); font-size: 12px; }
390
+ .event-description {
391
+ display: block; /* .event is flex for icon+body; raw markdown children must stack */
392
+ background: var(--surface); border-radius: 8px; border-bottom: 0; padding: 10px 12px;
393
+ }
394
+ .actor-agent .event-body p { color: #b9c3d8; }
395
+ [data-theme="light"] .actor-agent .event-body p { color: #3a4a63; }
396
+
397
+ .empty { color: var(--text-dim); font-style: italic; }
398
+
399
+ /* AI typing indicator */
400
+ .event.typing { border-bottom: 0; }
401
+ .typing-dots { display: inline-flex; gap: 4px; padding: 8px 2px; align-items: center; }
402
+ .typing-dots span {
403
+ width: 6px; height: 6px; border-radius: 50%;
404
+ background: var(--text-dim); animation: typing-blink 1.2s infinite;
405
+ }
406
+ .typing-dots span:nth-child(2) { animation-delay: .2s; }
407
+ .typing-dots span:nth-child(3) { animation-delay: .4s; }
408
+ .typing-dots.mini { padding: 0 1px; gap: 3px; }
409
+ .typing-dots.mini span { width: 4px; height: 4px; }
410
+ .thinking-chip { display: inline-flex; align-items: center; gap: 3px; }
411
+ @keyframes typing-blink {
412
+ 0%, 80%, 100% { opacity: .25; }
413
+ 40% { opacity: 1; }
414
+ }
415
+
416
+ .message-form { display: flex; gap: 8px; margin-top: 12px; align-items: flex-end; flex-shrink: 0; }
417
+ .message-form textarea {
418
+ flex: 1; background: var(--surface-2); border: 1px solid var(--border);
419
+ border-radius: 8px; color: var(--text); padding: 8px 10px; font: inherit; resize: vertical;
420
+ }
421
+ .actor-assistant .event-body p { color: #d8cfc0; }
422
+ [data-theme="light"] .actor-assistant .event-body p { color: #5a4e3c; }
423
+
424
+ .run-list { list-style: none; padding: 0; margin: 0 0 10px; }
425
+ .run-list .run { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px solid var(--surface-2); font-size: 13px; }
426
+ .run-running { color: var(--blue); }
427
+ .run-succeeded { color: var(--green); }
428
+ .run-failed, .run-cancelled { color: var(--red); }
429
+ .cancel-btn { font-size: 11px; padding: 3px 8px; background: var(--red); }
430
+ .event-final_report .event-body { background: rgba(95, 174, 111, .07); border-radius: 8px; padding: 8px 10px; }
431
+ .event-plan_proposed .event-body { background: rgba(107, 147, 201, .08); border-radius: 8px; padding: 8px 10px; }
432
+ .event-question .event-body { background: rgba(217, 164, 65, .1); border-radius: 8px; padding: 8px 10px; }
433
+ .event-error .event-body { background: rgba(214, 102, 102, .08); border-radius: 8px; padding: 8px 10px; }
434
+ .event-error .event-body > p:first-of-type { color: var(--red); }
435
+ .error-detail summary { font-size: 11px; color: var(--text-dim); cursor: pointer; }
436
+ .error-detail pre {
437
+ font-size: 11px; max-height: 180px; overflow: auto;
438
+ white-space: pre-wrap; overflow-wrap: anywhere;
439
+ }
440
+
441
+ .panel-callout { border-radius: 8px; padding: 10px 12px; margin-bottom: 12px; font-size: 13px; }
442
+ .panel-callout p { margin: 0 0 8px; }
443
+ .callout-plan { border: 1px solid var(--blue); background: rgba(107, 147, 201, .08); }
444
+ .callout-question { border: 1px solid var(--amber); background: rgba(217, 164, 65, .08); }
445
+ .callout-restart { border: 1px solid var(--accent); background: rgba(201, 79, 79, .08); }
446
+ .approve-btn { background: var(--green); }
447
+ .pr-view-btn {
448
+ display: block; width: 100%; box-sizing: border-box; text-align: center;
449
+ background: var(--green); color: #fff; border-radius: 6px;
450
+ padding: 8px 12px; font-weight: 600; margin-bottom: 12px;
451
+ }
452
+ .restart-btn { background: var(--accent); }
453
+ .align-right { display: flex; justify-content: flex-end; }
454
+ .autosave-status { font-size: 11px; color: var(--green); text-transform: none; letter-spacing: 0; margin-left: 6px; }
455
+
456
+ .field-row { display: flex; gap: 10px; }
457
+ .field-row > div { flex: 1; display: flex; flex-direction: column; gap: 4px; }
458
+ .field-row input, .field-row select {
459
+ background: var(--surface-2); border: 1px solid var(--border); border-radius: 6px;
460
+ color: var(--text); padding: 6px 10px; width: 100%;
461
+ }
462
+ .check-row { display: flex; align-items: center; gap: 8px; margin: 10px 0; font-size: 13px; color: var(--text); text-transform: none; letter-spacing: 0; }
463
+ .mono { font-family: ui-monospace, monospace; font-size: 12px; }
464
+ .form-error { color: var(--red); font-size: 13px; }
465
+ .delete-column { margin-top: 6px; }
466
+ .color-cell { flex: 0 0 auto !important; }
467
+ .color-row { display: flex; align-items: center; gap: 6px; }
468
+ .color-row input[type="color"] {
469
+ width: 42px; height: 30px; padding: 2px; border: 1px solid var(--border);
470
+ border-radius: 6px; background: var(--surface-2); cursor: pointer;
471
+ }
472
+ .check-row.inline { margin: 0; font-size: 12px; color: var(--text-dim); display: flex; gap: 4px; }
473
+ .accepts-from { display: flex; flex-wrap: wrap; gap: 6px 14px; margin: 4px 0 6px; }
474
+ .panel-advanced { margin-top: 18px; border-top: 1px solid var(--surface-2); padding-top: 10px; }
475
+ .delete-card { margin: 4px 0 16px; opacity: .7; }
476
+ .delete-card:hover { opacity: 1; }
477
+ .delete-card[disabled] { opacity: .3; cursor: not-allowed; }
478
+ .default-rule {
479
+ font-size: 12px; color: var(--text-dim); margin: 2px 0 6px;
480
+ padding: 7px 10px; background: var(--surface); border-left: 2px solid var(--accent);
481
+ border-radius: 0 6px 6px 0; text-transform: none; letter-spacing: 0;
482
+ }
483
+ .advanced-rules { margin: 8px 0; }
484
+ .advanced-rules summary { cursor: pointer; font-size: 12px; color: var(--text-dim); }
485
+ .advanced-rules textarea { width: 100%; }
486
+
487
+ /* (i) tooltips */
488
+ .info {
489
+ cursor: help; position: relative; display: inline-flex;
490
+ align-items: center; justify-content: center;
491
+ width: 14px; height: 14px; margin-left: 5px;
492
+ border: 1px solid var(--border); border-radius: 50%;
493
+ color: var(--text-dim); font-size: 10px; font-style: italic; font-weight: 600;
494
+ text-transform: none; vertical-align: middle;
495
+ }
496
+ .info:hover, .info:focus { color: var(--text); border-color: var(--text-dim); outline: none; }
497
+
498
+ .tooltip-pop {
499
+ position: fixed; z-index: 100; max-width: 260px;
500
+ background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px;
501
+ padding: 8px 10px; color: var(--text);
502
+ font-size: 12px; font-style: normal; font-weight: 400;
503
+ letter-spacing: 0; text-transform: none; line-height: 1.4; white-space: normal;
504
+ box-shadow: 0 6px 20px rgba(0, 0, 0, .4);
505
+ pointer-events: none;
506
+ }
507
+
508
+ /* Light overrides for components added on main after the theme branch was cut */
509
+ [data-theme="light"] .card-footer { background: #e2e5ea; border-top-color: rgba(0, 0, 0, .1); }
510
+
511
+ /* Accept-policy visibility while dragging + rejected-drop flash */
512
+ .column.drop-blocked { opacity: .45; }
513
+ .column.drop-blocked .drop-hint { color: var(--red); }
514
+ .column.drop-blocked .drop-hint::before { content: "✗ won't accept from here — "; }
@@ -0,0 +1,7 @@
1
+ class ApplicationController < ActionController::Base
2
+ # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
3
+ allow_browser versions: :modern
4
+
5
+ # Changes to the importmap will invalidate the etag for HTML responses
6
+ stale_when_importmap_changes
7
+ end
@@ -0,0 +1,5 @@
1
+ class BoardsController < ApplicationController
2
+ def show
3
+ @board = Board.includes(columns: :cards).first!
4
+ end
5
+ end