agentboard 0.2.0

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 (159) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +62 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +316 -0
  5. data/Rakefile +6 -0
  6. data/app/assets/stylesheets/agentboard/application.css +510 -0
  7. data/app/controllers/agentboard/agents_controller.rb +10 -0
  8. data/app/controllers/agentboard/analytics_controller.rb +37 -0
  9. data/app/controllers/agentboard/application_controller.rb +90 -0
  10. data/app/controllers/agentboard/approvals_controller.rb +15 -0
  11. data/app/controllers/agentboard/assets_controller.rb +32 -0
  12. data/app/controllers/agentboard/board_controller.rb +46 -0
  13. data/app/controllers/agentboard/captures_controller.rb +17 -0
  14. data/app/controllers/agentboard/comments_controller.rb +9 -0
  15. data/app/controllers/agentboard/criteria_controller.rb +17 -0
  16. data/app/controllers/agentboard/dependencies_controller.rb +20 -0
  17. data/app/controllers/agentboard/executions_controller.rb +48 -0
  18. data/app/controllers/agentboard/milestones_controller.rb +23 -0
  19. data/app/controllers/agentboard/pulse_controller.rb +12 -0
  20. data/app/controllers/agentboard/sessions_controller.rb +19 -0
  21. data/app/controllers/agentboard/settings_controller.rb +9 -0
  22. data/app/controllers/agentboard/tickets_controller.rb +139 -0
  23. data/app/controllers/agentboard/workflow_runs_controller.rb +16 -0
  24. data/app/controllers/agentboard/workflows_controller.rb +62 -0
  25. data/app/helpers/agentboard/application_helper.rb +196 -0
  26. data/app/javascript/agentboard/application.js +227 -0
  27. data/app/javascript/agentboard/capture.js +299 -0
  28. data/app/jobs/agentboard/application_job.rb +4 -0
  29. data/app/models/agentboard/acceptance_criterion.rb +23 -0
  30. data/app/models/agentboard/agent_profile.rb +16 -0
  31. data/app/models/agentboard/agent_session.rb +30 -0
  32. data/app/models/agentboard/application_record.rb +13 -0
  33. data/app/models/agentboard/approval.rb +29 -0
  34. data/app/models/agentboard/artifact.rb +11 -0
  35. data/app/models/agentboard/comment.rb +9 -0
  36. data/app/models/agentboard/event.rb +37 -0
  37. data/app/models/agentboard/execution.rb +51 -0
  38. data/app/models/agentboard/idempotency_key.rb +5 -0
  39. data/app/models/agentboard/milestone.rb +7 -0
  40. data/app/models/agentboard/outbox_message.rb +11 -0
  41. data/app/models/agentboard/project.rb +43 -0
  42. data/app/models/agentboard/repository_snapshot.rb +5 -0
  43. data/app/models/agentboard/session_ticket_link.rb +18 -0
  44. data/app/models/agentboard/ticket.rb +81 -0
  45. data/app/models/agentboard/ticket_dependency.rb +34 -0
  46. data/app/models/agentboard/ticket_lease.rb +11 -0
  47. data/app/models/agentboard/usage_record.rb +10 -0
  48. data/app/models/agentboard/verification_result.rb +12 -0
  49. data/app/models/agentboard/webhook_delivery.rb +7 -0
  50. data/app/models/agentboard/workflow_definition.rb +20 -0
  51. data/app/models/agentboard/workflow_run.rb +21 -0
  52. data/app/models/agentboard/workflow_step.rb +12 -0
  53. data/app/services/agentboard/approvals.rb +76 -0
  54. data/app/services/agentboard/captures.rb +113 -0
  55. data/app/services/agentboard/completion_policy.rb +117 -0
  56. data/app/services/agentboard/current.rb +5 -0
  57. data/app/services/agentboard/data_governance.rb +68 -0
  58. data/app/services/agentboard/doctor.rb +133 -0
  59. data/app/services/agentboard/event_store.rb +88 -0
  60. data/app/services/agentboard/evidence.rb +52 -0
  61. data/app/services/agentboard/executions.rb +198 -0
  62. data/app/services/agentboard/hook_ingestion.rb +100 -0
  63. data/app/services/agentboard/idempotency.rb +28 -0
  64. data/app/services/agentboard/lease_service.rb +107 -0
  65. data/app/services/agentboard/mcp_server.rb +138 -0
  66. data/app/services/agentboard/mcp_tools.rb +341 -0
  67. data/app/services/agentboard/outbox.rb +44 -0
  68. data/app/services/agentboard/pull_requests.rb +65 -0
  69. data/app/services/agentboard/runtime/execution_runner.rb +352 -0
  70. data/app/services/agentboard/runtime/ipc_handler.rb +55 -0
  71. data/app/services/agentboard/runtime/reconciler.rb +42 -0
  72. data/app/services/agentboard/runtime/supervisor.rb +96 -0
  73. data/app/services/agentboard/session_association.rb +44 -0
  74. data/app/services/agentboard/session_correlation.rb +56 -0
  75. data/app/services/agentboard/session_credential.rb +44 -0
  76. data/app/services/agentboard/ticket_advisor.rb +60 -0
  77. data/app/services/agentboard/tickets.rb +218 -0
  78. data/app/services/agentboard/verification_service.rb +60 -0
  79. data/app/services/agentboard/workflow_engine.rb +261 -0
  80. data/app/views/agentboard/agents/index.html.erb +51 -0
  81. data/app/views/agentboard/analytics/show.html.erb +78 -0
  82. data/app/views/agentboard/approvals/_approval.html.erb +27 -0
  83. data/app/views/agentboard/approvals/index.html.erb +31 -0
  84. data/app/views/agentboard/board/_card.html.erb +36 -0
  85. data/app/views/agentboard/board/show.html.erb +73 -0
  86. data/app/views/agentboard/executions/index.html.erb +33 -0
  87. data/app/views/agentboard/executions/show.html.erb +126 -0
  88. data/app/views/agentboard/milestones/index.html.erb +28 -0
  89. data/app/views/agentboard/settings/show.html.erb +33 -0
  90. data/app/views/agentboard/shared/_dag.html.erb +24 -0
  91. data/app/views/agentboard/tickets/_form.html.erb +38 -0
  92. data/app/views/agentboard/tickets/edit.html.erb +5 -0
  93. data/app/views/agentboard/tickets/index.html.erb +51 -0
  94. data/app/views/agentboard/tickets/new.html.erb +6 -0
  95. data/app/views/agentboard/tickets/show.html.erb +343 -0
  96. data/app/views/agentboard/tickets/suggestions.html.erb +21 -0
  97. data/app/views/agentboard/workflow_runs/show.html.erb +28 -0
  98. data/app/views/agentboard/workflows/index.html.erb +49 -0
  99. data/app/views/agentboard/workflows/show.html.erb +15 -0
  100. data/app/views/layouts/agentboard/application.html.erb +93 -0
  101. data/config/routes.rb +49 -0
  102. data/db/migrate/20261114000001_create_agentboard_core.rb +144 -0
  103. data/db/migrate/20261114000002_create_agentboard_runtime.rb +213 -0
  104. data/db/migrate/20261115000001_create_agentboard_webhook_deliveries.rb +18 -0
  105. data/exe/agentboard +7 -0
  106. data/lib/agentboard/agent_container.rb +124 -0
  107. data/lib/agentboard/authorization.rb +58 -0
  108. data/lib/agentboard/capture/middleware.rb +54 -0
  109. data/lib/agentboard/capture.rb +57 -0
  110. data/lib/agentboard/cli.rb +818 -0
  111. data/lib/agentboard/command_policy.rb +44 -0
  112. data/lib/agentboard/configuration.rb +160 -0
  113. data/lib/agentboard/container_sandbox.rb +65 -0
  114. data/lib/agentboard/context_builder.rb +243 -0
  115. data/lib/agentboard/egress_proxy.rb +95 -0
  116. data/lib/agentboard/engine.rb +27 -0
  117. data/lib/agentboard/errors.rb +42 -0
  118. data/lib/agentboard/event_schema.rb +37 -0
  119. data/lib/agentboard/extensions.rb +58 -0
  120. data/lib/agentboard/git/repository.rb +92 -0
  121. data/lib/agentboard/git/worktree_manager.rb +107 -0
  122. data/lib/agentboard/git.rb +4 -0
  123. data/lib/agentboard/ids.rb +33 -0
  124. data/lib/agentboard/json_log_formatter.rb +28 -0
  125. data/lib/agentboard/path_guard.rb +27 -0
  126. data/lib/agentboard/process_ancestry.rb +40 -0
  127. data/lib/agentboard/providers/base.rb +107 -0
  128. data/lib/agentboard/providers/claude_code.rb +219 -0
  129. data/lib/agentboard/providers/codex.rb +177 -0
  130. data/lib/agentboard/providers/compatibility/claude.yml +13 -0
  131. data/lib/agentboard/providers/compatibility/codex.yml +10 -0
  132. data/lib/agentboard/providers/config_editor.rb +106 -0
  133. data/lib/agentboard/providers/cursor.rb +40 -0
  134. data/lib/agentboard/providers/fake.rb +59 -0
  135. data/lib/agentboard/providers/fake_agent_script.rb +47 -0
  136. data/lib/agentboard/providers/generic_cli.rb +35 -0
  137. data/lib/agentboard/providers/registry.rb +31 -0
  138. data/lib/agentboard/providers/types.rb +32 -0
  139. data/lib/agentboard/providers.rb +26 -0
  140. data/lib/agentboard/rails_introspection.rb +49 -0
  141. data/lib/agentboard/redactor.rb +62 -0
  142. data/lib/agentboard/result.rb +46 -0
  143. data/lib/agentboard/runtime/ipc.rb +193 -0
  144. data/lib/agentboard/runtime.rb +1 -0
  145. data/lib/agentboard/state_machines.rb +71 -0
  146. data/lib/agentboard/subprocess.rb +168 -0
  147. data/lib/agentboard/templates/Dockerfile.agent +21 -0
  148. data/lib/agentboard/verification.rb +79 -0
  149. data/lib/agentboard/version.rb +3 -0
  150. data/lib/agentboard/workflows/condition.rb +166 -0
  151. data/lib/agentboard/workflows/definition.rb +208 -0
  152. data/lib/agentboard/workflows.rb +2 -0
  153. data/lib/agentboard.rb +73 -0
  154. data/lib/generators/agentboard/install/install_generator.rb +67 -0
  155. data/lib/generators/agentboard/install/templates/config.yml +81 -0
  156. data/lib/generators/agentboard/install/templates/feature_delivery.yml +25 -0
  157. data/lib/generators/agentboard/install/templates/initializer.rb +23 -0
  158. data/lib/tasks/agentboard_tasks.rake +6 -0
  159. metadata +285 -0
@@ -0,0 +1,126 @@
1
+ <% e = @execution %>
2
+ <% content_for :title, e.label %>
3
+ <% content_for :crumbs do %>
4
+ <%= link_to "Executions", executions_path %><span class="sep">/</span><%= link_to e.ticket.key, ticket_path(e.ticket) %><span class="sep">/</span><span>exec-<%= e.attempt.to_s.rjust(3, "0") %></span>
5
+ <% end %>
6
+ <% content_for :actions do %>
7
+ <% if e.active? && can?("execution.cancel") %>
8
+ <%= button_to cancel_execution_path(e), class: "btn btn-sm btn-danger", form: { style: "display:inline" } do %><%= icon("stop", size: 12) %> Cancel<% end %>
9
+ <% elsif e.terminal? && e.provider_key != "local" && can?("execution.start") %>
10
+ <% if e.metadata["provider_session_id"] && Agentboard::Providers.registry.registered?(e.provider_key) && Agentboard::Providers.registry.fetch(e.provider_key).supports?(:session_resume) && e.worktree_path && File.directory?(e.worktree_path) %>
11
+ <details class="popover">
12
+ <summary class="btn btn-sm"><%= icon("play", size: 12) %> Resume session…</summary>
13
+ <div class="popover-panel" style="min-width:320px">
14
+ <%= form_with url: resume_execution_path(e), class: "stack-sm", style: "padding:6px" do %>
15
+ <textarea name="message" class="textarea" style="min-height:70px" placeholder="Message to the agent (default: Continue the task.)"></textarea>
16
+ <button class="btn btn-primary btn-sm">Resume in the same worktree</button>
17
+ <% end %>
18
+ </div>
19
+ </details>
20
+ <% end %>
21
+ <%= button_to retry_execution_path(e), class: "btn btn-sm", form: { style: "display:inline" } do %><%= icon("refresh", size: 12) %> Retry as new attempt<% end %>
22
+ <% end %>
23
+ <% end %>
24
+
25
+ <div class="row mb" style="gap:12px">
26
+ <%= provider_avatar(e.provider_key, size: 34) %>
27
+ <div class="grow">
28
+ <h1 style="display:flex;gap:10px;align-items:center"><%= e.action.humanize %> <span class="muted" style="font-weight:500"><%= e.ticket.title %></span></h1>
29
+ <div class="row muted" style="font-size:12.5px;margin-top:2px">
30
+ <%= execution_pill(e.status) %>
31
+ <span><%= Agentboard::Providers.registry.registered?(e.provider_key) ? Agentboard::Providers.registry.adapter_class(e.provider_key).display_name : e.provider_key %></span>
32
+ <% if e.branch %><span>·</span><%= icon("branch", size: 13) %><code><%= e.branch %></code><% end %>
33
+ <span>·</span><span class="prov <%= e.metadata["isolation"] == "container" ? "prov-git-observed" : "prov-provider-reported" %>" title="<%= e.metadata["isolation"] == "container" ? "Ran in #{e.metadata["image"]} on an internal network with an egress allowlist" : "Ran as a trusted local subprocess" %>"><%= e.metadata["isolation"] == "container" ? "container" : "local process" %></span>
34
+ <% if e.metadata["provider_session_id"] %><span>·</span><span title="Provider session id">session <code><%= e.metadata["provider_session_id"].to_s[0, 12] %></code></span><% end %>
35
+ </div>
36
+ </div>
37
+ </div>
38
+
39
+ <% if e.status == "queued" && !@daemon[:running] %>
40
+ <div class="callout callout-warn mb"><%= icon("clock", size: 16) %><div>Queued. Start the runtime to pick it up: <code>bundle exec agentboard daemon start</code></div></div>
41
+ <% end %>
42
+ <% if @pending_approvals.any? %>
43
+ <div class="card mb" style="border-color:var(--c-amber)">
44
+ <div class="card-head"><h2><%= icon("shield", size: 15) %> The agent is waiting for your permission</h2><span class="muted" style="margin-left:auto;font-size:12px">It continues as soon as you decide</span></div>
45
+ <div class="card-body stack"><% @pending_approvals.each do |a| %><%= render "agentboard/approvals/approval", approval: a %><% end %></div>
46
+ </div>
47
+ <% end %>
48
+ <% if e.failure_message.present? %>
49
+ <div class="callout <%= e.status == "interrupted" ? "callout-warn" : "callout-bad" %> mb"><%= icon("alert", size: 16) %><div><strong><%= e.failure_code.to_s.humanize %>.</strong> <%= e.failure_message %></div></div>
50
+ <% end %>
51
+
52
+ <div class="exec-hero mb">
53
+ <div><div class="stat-label">Duration</div><div class="stat-value"><%= icon("clock", size: 14, klass: "muted") %><%= duration(e.duration) %></div></div>
54
+ <div><div class="stat-label">Base → head</div><div class="stat-value"><%= sha_tag(e.base_sha) %> <%= icon("arrow-right", size: 12, klass: "faint") %> <%= sha_tag(e.head_sha) %></div></div>
55
+ <div><div class="stat-label">Checks</div><div class="stat-value"><%= @results.count(&:passed?) %><span class="muted">/<%= @results.size %></span></div></div>
56
+ <div><div class="stat-label">Tokens (reported)</div><div class="stat-value">
57
+ <% tin = @usage.sum { |u| u.input_tokens.to_i }; tout = @usage.sum { |u| u.output_tokens.to_i } %>
58
+ <%= @usage.any? ? "#{number_with_delimiter(tin)} / #{number_with_delimiter(tout)}" : "—" %></div></div>
59
+ <div><div class="stat-label">Resources</div><div class="stat-value" style="font-size:13px">
60
+ <% if (r = e.metadata["resources"]) %><%= number_to_human_size(r["peak_rss_kb"].to_i * 1024) %> peak · <%= r["cpu_seconds"] %>s CPU<% else %>—<% end %></div></div>
61
+ <div><div class="stat-label">Heartbeat</div><div class="stat-value" style="font-size:13px"><%= e.heartbeat_at ? ago(e.heartbeat_at) : "—" %><% if e.pid && e.active? %> <span class="faint mono" style="font-size:11px">pid <%= e.pid %></span><% end %></div></div>
62
+ </div>
63
+
64
+ <div class="detail" style="grid-template-columns:minmax(0,1fr) 380px">
65
+ <div class="stack">
66
+ <div class="console" data-controller="log" data-log-url-value="<%= log_execution_path(e) %>" data-log-active-value="<%= e.active? %>">
67
+ <div class="console-head"><span class="dots"><i></i><i></i><i></i></span><%= icon("terminal", size: 13) %> agent output <span style="margin-left:auto">redacted<%= " · truncated" if e.log_truncated %></span></div>
68
+ <pre class="console-body" data-log-target="body"></pre>
69
+ </div>
70
+ <div class="card">
71
+ <div class="card-head"><h2><%= icon("pulse", size: 15) %> Event stream</h2><span class="muted" style="margin-left:auto;font-size:12px">Model reasoning is never captured</span></div>
72
+ <div class="stream">
73
+ <% if @events.empty? %><div class="empty" style="padding:20px">No events yet.</div><% end %>
74
+ <% @events.each do |ev| %>
75
+ <% ic, tone = event_icon(ev) %>
76
+ <div class="stream-row is-<%= tone %>">
77
+ <time><%= ev.occurred_at.strftime("%H:%M:%S") %></time>
78
+ <%= icon(ic, size: 14) %>
79
+ <div style="min-width:0;word-break:break-word"><%= event_title(ev) %></div>
80
+ <%= provenance_badge(ev.provenance) %>
81
+ </div>
82
+ <% end %>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ <div class="stack">
87
+ <div class="card">
88
+ <div class="card-head"><h3><%= icon("flask", size: 14) %> Verification</h3></div>
89
+ <div class="card-body-flush">
90
+ <% if @results.empty? %><div class="empty" style="padding:18px"><span>Runs after the agent finishes, against the snapshot commit.</span></div><% end %>
91
+ <% @results.each do |r| %>
92
+ <div class="check-row" style="flex-wrap:wrap">
93
+ <%= execution_pill(r.status) %><strong class="mono" style="font-size:12.5px"><%= r.name %></strong>
94
+ <span class="muted" style="font-size:12px"><%= r.required ? "required" : "optional" %> · <%= r.duration_seconds ? "#{r.duration_seconds.round(1)}s" : "…" %></span>
95
+ <span style="margin-left:auto"><%= sha_tag(r.commit_sha) %></span>
96
+ <% if r.output_excerpt.present? %>
97
+ <details class="output" style="flex-basis:100%"><summary>Show output</summary><pre><%= r.output_excerpt %></pre></details>
98
+ <% end %>
99
+ </div>
100
+ <% end %>
101
+ </div>
102
+ </div>
103
+ <div class="card">
104
+ <div class="card-head"><h3><%= icon("layers", size: 14) %> Artifacts</h3></div>
105
+ <div class="card-body-flush">
106
+ <% if @artifacts.empty? %><div class="empty" style="padding:18px">None yet.</div><% end %>
107
+ <% @artifacts.each do |a| %>
108
+ <div class="list-row"><%= icon(a.kind == "commit" ? "commit" : (a.kind == "context" ? "layers" : "file"), size: 14, klass: "muted") %>
109
+ <span class="grow truncate"><%= a.title.presence || a.kind.humanize %><% if a.kind == "commit" && a.metadata["files"].present? %><br><span class="faint mono" style="font-size:11px"><%= Array(a.metadata["files"]).first(4).join(", ") %></span><% end %></span>
110
+ <%= provenance_badge(a.provenance) %></div>
111
+ <% end %>
112
+ </div>
113
+ </div>
114
+ <div class="card"><div class="card-body">
115
+ <dl class="kv">
116
+ <dt>Worktree</dt><dd class="mono truncate" style="font-size:11.5px" title="<%= e.worktree_path %>"><%= e.worktree_path ? e.worktree_path.sub(Agentboard.config.root_path.to_s + "/", "") : "—" %></dd>
117
+ <dt>Context</dt><dd><% if e.context_digest %><code title="<%= e.context_digest %>"><%= e.context_digest[0, 10] %></code><% else %>—<% end %></dd>
118
+ <dt>Exit code</dt><dd><%= e.exit_code.nil? ? "—" : e.exit_code %></dd>
119
+ <dt>Requested by</dt><dd><%= e.metadata["requested_by"] || "—" %></dd>
120
+ <% if e.metadata["resume_of"] %><dt>Resumes</dt><dd><%= link_to "previous attempt", execution_path(e.metadata["resume_of"]), class: "link" %></dd><% end %>
121
+ <% if e.metadata["retry_of"] %><dt>Retry of</dt><dd><%= link_to "previous attempt", execution_path(e.metadata["retry_of"]), class: "link" %></dd><% end %>
122
+ <% if (cost = @usage.find(&:reported_cost)) %><dt>Reported cost</dt><dd><%= number_to_currency(cost.reported_cost, unit: cost.currency == "USD" ? "$" : "#{cost.currency} ", precision: 4) %> <span class="faint" style="font-size:11px">provider-reported</span></dd><% end %>
123
+ </dl>
124
+ </div></div>
125
+ </div>
126
+ </div>
@@ -0,0 +1,28 @@
1
+ <% content_for :title, "Milestones" %>
2
+ <% content_for :crumbs do %><%= icon("flag", size: 16, klass: "muted") %> <span>Milestones</span><% end %>
3
+ <div class="content-narrow">
4
+ <div class="page-head"><div><h1>Milestones</h1><p>Group tickets into deliverables and track evidence-backed progress.</p></div></div>
5
+ <div class="card mb">
6
+ <%= form_with url: milestones_path, scope: :milestone, class: "card-body row" do |f| %>
7
+ <%= f.text_field :name, class: "input", placeholder: "Milestone name, e.g. Checkout v2", required: true %>
8
+ <%= f.date_field :target_date, class: "input", style: "width:170px" %>
9
+ <button class="btn btn-primary"><%= icon("plus", size: 14) %> Add</button>
10
+ <% end %>
11
+ </div>
12
+ <div class="card card-body-flush">
13
+ <% if @milestones.empty? %>
14
+ <div class="empty"><%= icon("flag", size: 26) %><strong>No milestones yet</strong><span>Create one above, then pick it on a ticket.</span></div>
15
+ <% end %>
16
+ <% @milestones.each do |m| %>
17
+ <% counts = @progress.select { |(mid, _), _| mid == m.id }.transform_keys(&:last) %>
18
+ <% total = counts.values.sum; done = counts["done"].to_i %>
19
+ <div class="list-row" style="padding:12px 16px">
20
+ <%= icon("flag", size: 15, klass: m.status == "open" ? nil : "faint") %>
21
+ <span class="grow"><strong style="font-weight:600"><%= m.name %></strong><br><span class="faint" style="font-size:12px"><%= m.target_date ? "Due #{m.target_date.strftime("%b %-d, %Y")}" : "No date" %><%= " · overdue" if m.target_date && m.target_date < Date.current && m.status == "open" && done < total %></span></span>
22
+ <span class="row" style="width:200px"><%= meter(done, total, klass: "meter-lg") %><span class="muted nowrap"><%= done %>/<%= total %></span></span>
23
+ <%= link_to "Board", board_path(milestone: m.id), class: "btn btn-sm btn-ghost" %>
24
+ <%= button_to m.status == "open" ? "Close" : "Reopen", milestone_path(m), method: :patch, params: { status: m.status == "open" ? "closed" : "open" }, class: "btn btn-sm", form: { style: "display:inline" } %>
25
+ </div>
26
+ <% end %>
27
+ </div>
28
+ </div>
@@ -0,0 +1,33 @@
1
+ <% content_for :title, "Settings" %>
2
+ <% content_for :crumbs do %><%= icon("settings", size: 16, klass: "muted") %> <span>Settings</span><% end %>
3
+ <div class="content-narrow">
4
+ <div class="page-head"><div><h1>Settings & diagnostics</h1><p>Edit <code>config/agentboard.yml</code> or <code>.agentboard/config.yml</code>; this page is read-only.</p></div></div>
5
+ <div class="card mb">
6
+ <div class="card-head"><h2><%= icon("shield", size: 15) %> Doctor</h2></div>
7
+ <div class="card-body-flush">
8
+ <% @doctor.each do |c| %>
9
+ <div class="doctor-row"><span class="<%= c.status %>"><%= icon({ "ok" => "check", "warn" => "alert", "fail" => "x" }[c.status], size: 15, stroke: 2.5) %></span><strong style="font-weight:550"><%= c.name %></strong><span class="muted"><%= c.detail %></span></div>
10
+ <% end %>
11
+ </div>
12
+ </div>
13
+ <div class="detail" style="grid-template-columns:minmax(0,1fr) minmax(0,1fr)">
14
+ <div class="card"><div class="card-head"><h3>Security & completion policy</h3></div><div class="card-body">
15
+ <dl class="kv">
16
+ <% @config.security.each do |k, v| %><dt><%= k.humanize %></dt><dd><code><%= v.inspect %></code></dd><% end %>
17
+ <% @config.completion.each do |k, v| %><dt><%= k.humanize %></dt><dd><code><%= v.inspect %></code></dd><% end %>
18
+ </dl>
19
+ </div></div>
20
+ <div class="card"><div class="card-head"><h3>Approved command catalog</h3></div><div class="card-body-flush">
21
+ <% @config.verification_catalog.each do |c| %>
22
+ <div class="list-row"><strong class="mono" style="font-size:12.5px;width:110px"><%= c.name %></strong><code class="grow truncate"><%= c.argv.join(" ") %></code><span class="muted" style="font-size:12px"><%= c.required ? "required" : "optional" %> · <%= c.timeout_seconds %>s</span></div>
23
+ <% end %>
24
+ </div></div>
25
+ <div class="card"><div class="card-head"><h3>Runtime</h3></div><div class="card-body">
26
+ <dl class="kv"><% @config.runtime.each do |k, v| %><dt><%= k.humanize %></dt><dd><code><%= v.inspect %></code></dd><% end %></dl>
27
+ </div></div>
28
+ <div class="card"><div class="card-head"><h3>Event outbox</h3></div><div class="card-body">
29
+ <dl class="kv"><% %w[pending delivered dead].each do |s| %><dt><%= s.humanize %></dt><dd><%= @outbox[s].to_i %></dd><% end %></dl>
30
+ <p class="hint">At-least-once delivery with bounded retries; dead letters need attention.</p>
31
+ </div></div>
32
+ </div>
33
+ </div>
@@ -0,0 +1,24 @@
1
+ <%# locals: definition (Workflows::Definition), steps (hash key => WorkflowStep or nil) %>
2
+ <% depth = {} %>
3
+ <% definition.topological_order.each { |id| s = definition.step(id); depth[id] = s.depends_on.map { |d| depth[d] + 1 }.max || 0 } %>
4
+ <% layers = depth.group_by { |_, d| d }.sort.map { |_, ids| ids.map(&:first) } %>
5
+ <% type_icons = { "agent" => "bot", "command" => "terminal", "verification" => "flask", "human_approval" => "shield", "git_commit" => "commit", "git_pull_request" => "branch", "condition" => "workflow", "external" => "link" } %>
6
+ <div class="dag">
7
+ <% layers.each do |ids| %>
8
+ <div class="dag-layer">
9
+ <% ids.each do |id| %>
10
+ <% s = definition.step(id); st = steps && steps[id] %>
11
+ <div class="dag-node st-<%= st&.status || "idle" %>">
12
+ <div class="type"><%= icon(type_icons.fetch(s.type, "dot"), size: 11) %><%= s.type.tr("_", " ") %></div>
13
+ <div class="name"><%= s.id %></div>
14
+ <div class="row" style="gap:6px;font-size:11.5px;color:var(--muted)">
15
+ <% if s.provider %><%= provider_avatar(s.provider, size: 16) %><%= s.action %><% elsif s.command %><code><%= s.command %></code><% end %>
16
+ <% if st %><span style="margin-left:auto"><%= execution_pill(st.status) %></span><% end %>
17
+ </div>
18
+ <% if s.condition %><div class="faint mono" style="font-size:10.5px;margin-top:5px">if <%= s.condition %></div><% end %>
19
+ <% if st && st.attempt > 1 %><div class="faint" style="font-size:11px;margin-top:3px">attempt <%= st.attempt %></div><% end %>
20
+ </div>
21
+ <% end %>
22
+ </div>
23
+ <% end %>
24
+ </div>
@@ -0,0 +1,38 @@
1
+ <%= form_with model: ticket, url: (ticket.persisted? ? ticket_path(ticket) : tickets_path), scope: :ticket, method: (ticket.persisted? ? :patch : :post) do |f| %>
2
+ <% if ticket.persisted? %><%= hidden_field_tag :lock_version, ticket.lock_version %><% end %>
3
+ <div class="card">
4
+ <div class="card-body stack gap-lg">
5
+ <%= f.text_field :title, class: "input input-title", placeholder: "Ticket title", required: true, autofocus: true, maxlength: 255 %>
6
+ <div class="field">
7
+ <label for="ticket_description">Description</label>
8
+ <%= f.text_area :description, class: "textarea", rows: 7, placeholder: "What needs to happen, and why? Agents treat this as untrusted data, not as instructions that change policy." %>
9
+ </div>
10
+ <% unless ticket.persisted? %>
11
+ <div class="field">
12
+ <label for="ticket_criteria">Acceptance criteria <span class="faint" style="font-weight:400">— one per line</span></label>
13
+ <%= f.text_area :criteria, class: "textarea", rows: 4, placeholder: "Users can reset their password from the login page\nReset links expire after 30 minutes" %>
14
+ <span class="hint">A ticket needs at least one criterion before it can move to Ready.</span>
15
+ </div>
16
+ <% end %>
17
+ <div class="form-grid">
18
+ <div class="field"><label>Type</label><%= f.select :kind, Agentboard::Ticket::KINDS.map { |k| [k.capitalize, k] }, {}, class: "select" %></div>
19
+ <div class="field"><label>Priority</label><%= f.select :priority, Agentboard::Ticket::PRIORITIES.map { |v, l| [l, v] }, {}, class: "select" %></div>
20
+ <% if (milestones = current_project.milestones.where(status: "open").order(:target_date)).any? %>
21
+ <div class="field"><label>Milestone</label><%= f.select :milestone_id, milestones.map { |m| [m.name, m.id] }, { include_blank: "None" }, class: "select" %></div>
22
+ <% end %>
23
+ <% if params[:parent].present? && (parent = current_project.tickets.find_by(id: params[:parent])) %>
24
+ <%= f.hidden_field :parent_id, value: parent.id %>
25
+ <div class="field"><label>Subtask of</label><div class="input" style="display:flex;align-items:center"><%= parent.key %> — <%= parent.title.truncate(30) %></div></div>
26
+ <% end %>
27
+ <div class="field"><label>Labels</label><%= f.text_field :labels, value: Array(ticket.labels).join(", "), class: "input", placeholder: "auth, api" %></div>
28
+ <% unless ticket.persisted? %>
29
+ <div class="field"><label>Start in</label><%= f.select :status, [["Backlog", "backlog"], ["Ready", "ready"]], {}, class: "select" %></div>
30
+ <% end %>
31
+ </div>
32
+ </div>
33
+ <div class="modal-foot" style="border-radius:0 0 14px 14px">
34
+ <%= link_to "Cancel", (ticket.persisted? ? ticket_path(ticket) : board_path), class: "btn btn-ghost" %>
35
+ <%= f.button class: "btn btn-primary" do %><%= icon("check", size: 14) %> <%= ticket.persisted? ? "Save changes" : "Create ticket" %><% end %>
36
+ </div>
37
+ </div>
38
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <% content_for :title, "Edit #{@ticket.key}" %>
2
+ <% content_for :crumbs do %><%= link_to "Board", board_path %><span class="sep">/</span><%= link_to @ticket.key, ticket_path(@ticket) %><span class="sep">/</span><span>Edit</span><% end %>
3
+ <div class="content-narrow" style="max-width:760px;margin:0 auto">
4
+ <%= render "form", ticket: @ticket %>
5
+ </div>
@@ -0,0 +1,51 @@
1
+ <% content_for :title, "Backlog" %>
2
+ <% content_for :crumbs do %><%= icon("list", size: 16, klass: "muted") %> <span>Backlog</span><% end %>
3
+ <% content_for :actions do %>
4
+ <%= form_with url: tickets_path, method: :get, class: "search", data: { controller: "autosubmit", turbo_action: "replace" } do %>
5
+ <%= icon("search", size: 14) %>
6
+ <%= hidden_field_tag :status, @status if @status %>
7
+ <input class="input" type="search" name="q" value="<%= params[:q] %>" placeholder="Filter tickets…" data-hotkey="search" data-action="input->autosubmit#submit" autocomplete="off">
8
+ <% end %>
9
+ <%= link_to new_ticket_path, class: "btn btn-primary" do %><%= icon("plus", size: 14) %> New ticket<% end %>
10
+ <% end %>
11
+
12
+ <div class="tabs">
13
+ <%= link_to tickets_path(q: params[:q]), class: "tab #{"is-active" unless @status}" do %>All <span class="count"><%= @counts.values.sum %></span><% end %>
14
+ <% Agentboard::StateMachines::Ticket::STATES.each do |s| %>
15
+ <% next if @counts[s].to_i.zero? && @status != s %>
16
+ <%= link_to tickets_path(status: s, q: params[:q]), class: "tab #{"is-active" if @status == s}" do %>
17
+ <%= status_glyph(s) %> <%= Agentboard::StateMachines::Ticket::LABELS[s] %> <span class="count"><%= @counts[s].to_i %></span>
18
+ <% end %>
19
+ <% end %>
20
+ </div>
21
+
22
+ <div class="card card-body-flush">
23
+ <% if @tickets.empty? %>
24
+ <div class="empty">
25
+ <%= icon("inbox", size: 28) %>
26
+ <strong>No tickets here yet</strong>
27
+ <span>Create a ticket, or let an agent pick up work through MCP.</span>
28
+ <%= link_to "Create ticket", new_ticket_path, class: "btn btn-primary mt-sm" %>
29
+ </div>
30
+ <% else %>
31
+ <table class="table">
32
+ <thead><tr><th style="width:34px"></th><th style="width:90px">Key</th><th>Title</th><th style="width:130px">Status</th><th style="width:120px">Criteria</th><th style="width:120px">Agent</th><th style="width:120px" class="right">Updated</th></tr></thead>
33
+ <tbody>
34
+ <% @tickets.each do |t| %>
35
+ <% done, total = t.criteria_progress %>
36
+ <% running = t.executions.find { |e| Agentboard::StateMachines::Execution::ACTIVE.include?(e.status) } %>
37
+ <% lease = t.leases.find { |l| l.released_at.nil? && l.expires_at > Time.current } %>
38
+ <tr>
39
+ <td><%= priority_icon(t.priority) %></td>
40
+ <td class="mono muted" style="font-size:12px"><%= link_to t.key, ticket_path(t) %></td>
41
+ <td><%= link_to ticket_path(t), class: "row-link" do %><span class="truncate" style="font-weight:550"><%= t.title %></span><%= kind_badge(t.kind) %><% t.labels.first(2).each do |l| %><span class="label-chip"><%= l %></span><% end %><% end %></td>
42
+ <td><%= status_pill(t.status) %></td>
43
+ <td><% if total.positive? %><span class="row"><%= meter(done, total) %><span class="muted"><%= done %>/<%= total %></span></span><% else %><span class="faint">—</span><% end %></td>
44
+ <td><% if running %><span class="row"><%= provider_avatar(running.provider_key, size: 20) %><span class="muted"><%= running.status.humanize.downcase %></span></span><% elsif lease %><span class="row"><%= avatar(lease.holder_label, kind: lease.agent_session_id ? "agent" : "human", size: 20) %><span class="muted truncate"><%= lease.holder_label %></span></span><% else %><span class="faint">—</span><% end %></td>
45
+ <td class="right muted" style="font-size:12px"><%= ago(t.updated_at) %></td>
46
+ </tr>
47
+ <% end %>
48
+ </tbody>
49
+ </table>
50
+ <% end %>
51
+ </div>
@@ -0,0 +1,6 @@
1
+ <% content_for :title, "New ticket" %>
2
+ <% content_for :crumbs do %><%= link_to "Board", board_path %><span class="sep">/</span><span>New ticket</span><% end %>
3
+ <div class="content-narrow" style="max-width:760px;margin:0 auto">
4
+ <div class="page-head"><div><h1>New ticket</h1><p>Tickets are the unit of work agents claim, implement and hand back with evidence.</p></div></div>
5
+ <%= render "form", ticket: @ticket %>
6
+ </div>