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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +62 -0
- data/MIT-LICENSE +20 -0
- data/README.md +316 -0
- data/Rakefile +6 -0
- data/app/assets/stylesheets/agentboard/application.css +510 -0
- data/app/controllers/agentboard/agents_controller.rb +10 -0
- data/app/controllers/agentboard/analytics_controller.rb +37 -0
- data/app/controllers/agentboard/application_controller.rb +90 -0
- data/app/controllers/agentboard/approvals_controller.rb +15 -0
- data/app/controllers/agentboard/assets_controller.rb +32 -0
- data/app/controllers/agentboard/board_controller.rb +46 -0
- data/app/controllers/agentboard/captures_controller.rb +17 -0
- data/app/controllers/agentboard/comments_controller.rb +9 -0
- data/app/controllers/agentboard/criteria_controller.rb +17 -0
- data/app/controllers/agentboard/dependencies_controller.rb +20 -0
- data/app/controllers/agentboard/executions_controller.rb +48 -0
- data/app/controllers/agentboard/milestones_controller.rb +23 -0
- data/app/controllers/agentboard/pulse_controller.rb +12 -0
- data/app/controllers/agentboard/sessions_controller.rb +19 -0
- data/app/controllers/agentboard/settings_controller.rb +9 -0
- data/app/controllers/agentboard/tickets_controller.rb +139 -0
- data/app/controllers/agentboard/workflow_runs_controller.rb +16 -0
- data/app/controllers/agentboard/workflows_controller.rb +62 -0
- data/app/helpers/agentboard/application_helper.rb +196 -0
- data/app/javascript/agentboard/application.js +227 -0
- data/app/javascript/agentboard/capture.js +299 -0
- data/app/jobs/agentboard/application_job.rb +4 -0
- data/app/models/agentboard/acceptance_criterion.rb +23 -0
- data/app/models/agentboard/agent_profile.rb +16 -0
- data/app/models/agentboard/agent_session.rb +30 -0
- data/app/models/agentboard/application_record.rb +13 -0
- data/app/models/agentboard/approval.rb +29 -0
- data/app/models/agentboard/artifact.rb +11 -0
- data/app/models/agentboard/comment.rb +9 -0
- data/app/models/agentboard/event.rb +37 -0
- data/app/models/agentboard/execution.rb +51 -0
- data/app/models/agentboard/idempotency_key.rb +5 -0
- data/app/models/agentboard/milestone.rb +7 -0
- data/app/models/agentboard/outbox_message.rb +11 -0
- data/app/models/agentboard/project.rb +43 -0
- data/app/models/agentboard/repository_snapshot.rb +5 -0
- data/app/models/agentboard/session_ticket_link.rb +18 -0
- data/app/models/agentboard/ticket.rb +81 -0
- data/app/models/agentboard/ticket_dependency.rb +34 -0
- data/app/models/agentboard/ticket_lease.rb +11 -0
- data/app/models/agentboard/usage_record.rb +10 -0
- data/app/models/agentboard/verification_result.rb +12 -0
- data/app/models/agentboard/webhook_delivery.rb +7 -0
- data/app/models/agentboard/workflow_definition.rb +20 -0
- data/app/models/agentboard/workflow_run.rb +21 -0
- data/app/models/agentboard/workflow_step.rb +12 -0
- data/app/services/agentboard/approvals.rb +76 -0
- data/app/services/agentboard/captures.rb +113 -0
- data/app/services/agentboard/completion_policy.rb +117 -0
- data/app/services/agentboard/current.rb +5 -0
- data/app/services/agentboard/data_governance.rb +68 -0
- data/app/services/agentboard/doctor.rb +133 -0
- data/app/services/agentboard/event_store.rb +88 -0
- data/app/services/agentboard/evidence.rb +52 -0
- data/app/services/agentboard/executions.rb +198 -0
- data/app/services/agentboard/hook_ingestion.rb +100 -0
- data/app/services/agentboard/idempotency.rb +28 -0
- data/app/services/agentboard/lease_service.rb +107 -0
- data/app/services/agentboard/mcp_server.rb +138 -0
- data/app/services/agentboard/mcp_tools.rb +341 -0
- data/app/services/agentboard/outbox.rb +44 -0
- data/app/services/agentboard/pull_requests.rb +65 -0
- data/app/services/agentboard/runtime/execution_runner.rb +352 -0
- data/app/services/agentboard/runtime/ipc_handler.rb +55 -0
- data/app/services/agentboard/runtime/reconciler.rb +42 -0
- data/app/services/agentboard/runtime/supervisor.rb +96 -0
- data/app/services/agentboard/session_association.rb +44 -0
- data/app/services/agentboard/session_correlation.rb +56 -0
- data/app/services/agentboard/session_credential.rb +44 -0
- data/app/services/agentboard/ticket_advisor.rb +60 -0
- data/app/services/agentboard/tickets.rb +218 -0
- data/app/services/agentboard/verification_service.rb +60 -0
- data/app/services/agentboard/workflow_engine.rb +261 -0
- data/app/views/agentboard/agents/index.html.erb +51 -0
- data/app/views/agentboard/analytics/show.html.erb +78 -0
- data/app/views/agentboard/approvals/_approval.html.erb +27 -0
- data/app/views/agentboard/approvals/index.html.erb +31 -0
- data/app/views/agentboard/board/_card.html.erb +36 -0
- data/app/views/agentboard/board/show.html.erb +73 -0
- data/app/views/agentboard/executions/index.html.erb +33 -0
- data/app/views/agentboard/executions/show.html.erb +126 -0
- data/app/views/agentboard/milestones/index.html.erb +28 -0
- data/app/views/agentboard/settings/show.html.erb +33 -0
- data/app/views/agentboard/shared/_dag.html.erb +24 -0
- data/app/views/agentboard/tickets/_form.html.erb +38 -0
- data/app/views/agentboard/tickets/edit.html.erb +5 -0
- data/app/views/agentboard/tickets/index.html.erb +51 -0
- data/app/views/agentboard/tickets/new.html.erb +6 -0
- data/app/views/agentboard/tickets/show.html.erb +343 -0
- data/app/views/agentboard/tickets/suggestions.html.erb +21 -0
- data/app/views/agentboard/workflow_runs/show.html.erb +28 -0
- data/app/views/agentboard/workflows/index.html.erb +49 -0
- data/app/views/agentboard/workflows/show.html.erb +15 -0
- data/app/views/layouts/agentboard/application.html.erb +93 -0
- data/config/routes.rb +49 -0
- data/db/migrate/20261114000001_create_agentboard_core.rb +144 -0
- data/db/migrate/20261114000002_create_agentboard_runtime.rb +213 -0
- data/db/migrate/20261115000001_create_agentboard_webhook_deliveries.rb +18 -0
- data/exe/agentboard +7 -0
- data/lib/agentboard/agent_container.rb +124 -0
- data/lib/agentboard/authorization.rb +58 -0
- data/lib/agentboard/capture/middleware.rb +54 -0
- data/lib/agentboard/capture.rb +57 -0
- data/lib/agentboard/cli.rb +818 -0
- data/lib/agentboard/command_policy.rb +44 -0
- data/lib/agentboard/configuration.rb +160 -0
- data/lib/agentboard/container_sandbox.rb +65 -0
- data/lib/agentboard/context_builder.rb +243 -0
- data/lib/agentboard/egress_proxy.rb +95 -0
- data/lib/agentboard/engine.rb +27 -0
- data/lib/agentboard/errors.rb +42 -0
- data/lib/agentboard/event_schema.rb +37 -0
- data/lib/agentboard/extensions.rb +58 -0
- data/lib/agentboard/git/repository.rb +92 -0
- data/lib/agentboard/git/worktree_manager.rb +107 -0
- data/lib/agentboard/git.rb +4 -0
- data/lib/agentboard/ids.rb +33 -0
- data/lib/agentboard/json_log_formatter.rb +28 -0
- data/lib/agentboard/path_guard.rb +27 -0
- data/lib/agentboard/process_ancestry.rb +40 -0
- data/lib/agentboard/providers/base.rb +107 -0
- data/lib/agentboard/providers/claude_code.rb +219 -0
- data/lib/agentboard/providers/codex.rb +177 -0
- data/lib/agentboard/providers/compatibility/claude.yml +13 -0
- data/lib/agentboard/providers/compatibility/codex.yml +10 -0
- data/lib/agentboard/providers/config_editor.rb +106 -0
- data/lib/agentboard/providers/cursor.rb +40 -0
- data/lib/agentboard/providers/fake.rb +59 -0
- data/lib/agentboard/providers/fake_agent_script.rb +47 -0
- data/lib/agentboard/providers/generic_cli.rb +35 -0
- data/lib/agentboard/providers/registry.rb +31 -0
- data/lib/agentboard/providers/types.rb +32 -0
- data/lib/agentboard/providers.rb +26 -0
- data/lib/agentboard/rails_introspection.rb +49 -0
- data/lib/agentboard/redactor.rb +62 -0
- data/lib/agentboard/result.rb +46 -0
- data/lib/agentboard/runtime/ipc.rb +193 -0
- data/lib/agentboard/runtime.rb +1 -0
- data/lib/agentboard/state_machines.rb +71 -0
- data/lib/agentboard/subprocess.rb +168 -0
- data/lib/agentboard/templates/Dockerfile.agent +21 -0
- data/lib/agentboard/verification.rb +79 -0
- data/lib/agentboard/version.rb +3 -0
- data/lib/agentboard/workflows/condition.rb +166 -0
- data/lib/agentboard/workflows/definition.rb +208 -0
- data/lib/agentboard/workflows.rb +2 -0
- data/lib/agentboard.rb +73 -0
- data/lib/generators/agentboard/install/install_generator.rb +67 -0
- data/lib/generators/agentboard/install/templates/config.yml +81 -0
- data/lib/generators/agentboard/install/templates/feature_delivery.yml +25 -0
- data/lib/generators/agentboard/install/templates/initializer.rb +23 -0
- data/lib/tasks/agentboard_tasks.rake +6 -0
- metadata +285 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
<% t = @ticket %>
|
|
2
|
+
<% content_for :title, "#{t.key} #{t.title}" %>
|
|
3
|
+
<% content_for :crumbs do %>
|
|
4
|
+
<%= link_to "Board", board_path %><span class="sep">/</span><span class="mono"><%= t.key %></span>
|
|
5
|
+
<% end %>
|
|
6
|
+
<% content_for :actions do %>
|
|
7
|
+
<% if can?("ticket.write") %><%= link_to edit_ticket_path(t), class: "btn btn-sm" do %><%= icon("edit", size: 13) %> Edit<% end %><% end %>
|
|
8
|
+
<% end %>
|
|
9
|
+
<% lease = t.active_lease %>
|
|
10
|
+
<% next_states = Agentboard::StateMachines::Ticket.next_states(t.status) - ["claimed"] %>
|
|
11
|
+
|
|
12
|
+
<div class="detail">
|
|
13
|
+
<div class="detail-main">
|
|
14
|
+
<div class="row" style="gap:10px">
|
|
15
|
+
<%= status_pill(t.status) %><%= priority_icon(t.priority) %><span class="muted"><%= t.priority_label %></span><%= kind_badge(t.kind) %>
|
|
16
|
+
<% t.labels.each do |l| %><span class="label-chip" style="--h:<%= Digest::MD5.hexdigest(l)[0, 2].to_i(16) * 360 / 255 %>"><%= l %></span><% end %>
|
|
17
|
+
</div>
|
|
18
|
+
<h1 class="ticket-title"><%= t.title %></h1>
|
|
19
|
+
|
|
20
|
+
<% if t.status == "blocked" %>
|
|
21
|
+
<div class="callout callout-bad mb"><%= icon("ban", size: 16) %><div><strong>Blocked.</strong> <%= t.blocked_reason %></div></div>
|
|
22
|
+
<% end %>
|
|
23
|
+
|
|
24
|
+
<div class="prose mb"><%= t.description %></div>
|
|
25
|
+
|
|
26
|
+
<% t.artifacts.select { |a| a.kind == "capture" }.first(1).each do |cap| %>
|
|
27
|
+
<% m = cap.metadata %>
|
|
28
|
+
<section class="card" id="capture">
|
|
29
|
+
<div class="card-head"><h2><%= icon("eye", size: 15) %> Captured from the app</h2>
|
|
30
|
+
<span class="muted" style="margin-left:auto;font-size:12px">by <%= m["captured_by"] %> · <%= ago(cap.created_at) %></span></div>
|
|
31
|
+
<div class="card-body stack-sm">
|
|
32
|
+
<dl class="kv">
|
|
33
|
+
<dt>Page</dt><dd class="mono" style="font-size:12px"><%= m["url"] || "—" %></dd>
|
|
34
|
+
<dt>Route</dt><dd><code><%= m["route"] || "—" %></code></dd>
|
|
35
|
+
<dt>Viewport</dt><dd class="muted"><%= m.dig("viewport", "width") %>×<%= m.dig("viewport", "height") %></dd>
|
|
36
|
+
</dl>
|
|
37
|
+
<% Array(m["elements"]).each_with_index do |el, i| %>
|
|
38
|
+
<div class="list-row" style="padding:8px 0;align-items:flex-start">
|
|
39
|
+
<span class="pill pill-review" style="height:20px"><%= i + 1 %></span>
|
|
40
|
+
<span class="grow" style="min-width:0">
|
|
41
|
+
<code style="word-break:break-all"><%= el["selector"] %></code>
|
|
42
|
+
<% if el["template"] %><div style="font-size:12px;color:var(--c-green)" class="mono">↳ <%= el["template"] %></div><% end %>
|
|
43
|
+
<% if el["text"].present? %><div class="muted" style="font-size:12.5px">“<%= el["text"].truncate(140) %>”</div><% end %>
|
|
44
|
+
</span>
|
|
45
|
+
</div>
|
|
46
|
+
<% end %>
|
|
47
|
+
<% if (shot = t.artifacts.find { |a| a.kind == "screenshot" }) %>
|
|
48
|
+
<img src="<%= screenshot_capture_path(shot) %>" alt="Screenshot of the captured area" style="max-width:100%;border-radius:10px;border:1px solid var(--border);margin-top:6px">
|
|
49
|
+
<% end %>
|
|
50
|
+
<% if Array(m["console_errors"]).any? %>
|
|
51
|
+
<details class="output"><summary><%= m["console_errors"].size %> console error(s)</summary><pre><%= m["console_errors"].join("\n") %></pre></details>
|
|
52
|
+
<% end %>
|
|
53
|
+
<p class="hint" style="margin:4px 0 0">Agents receive these templates first in their context.</p>
|
|
54
|
+
</div>
|
|
55
|
+
</section>
|
|
56
|
+
<% end %>
|
|
57
|
+
|
|
58
|
+
<div class="row row-wrap mb" style="gap:6px">
|
|
59
|
+
<% unless can?("ticket.transition") %><span class="hint">Read-only: your role (<%= current_actor.role %>) cannot change tickets.</span><% next_states = [] %><% end %>
|
|
60
|
+
<% if t.status == "ready" && can?("ticket.claim") %>
|
|
61
|
+
<%= button_to transition_ticket_path(t), params: { to: "claimed" }, class: "btn btn-sm", form: { style: "display:inline" } do %><%= icon("lock", size: 13) %> Claim<% end %>
|
|
62
|
+
<% end %>
|
|
63
|
+
<% next_states.each do |to| %>
|
|
64
|
+
<% if %w[blocked cancelled reopened].include?(to) || (to == "ready" && t.status == "backlog" && t.acceptance_criteria.none?) %>
|
|
65
|
+
<details class="popover">
|
|
66
|
+
<summary class="btn btn-sm <%= "btn-danger" if %w[blocked cancelled].include?(to) %>"><%= status_glyph(to) %> <%= Agentboard::StateMachines::Ticket::LABELS[to] %>…</summary>
|
|
67
|
+
<div class="popover-panel" style="left:0;right:auto;min-width:300px">
|
|
68
|
+
<%= form_with url: transition_ticket_path(t), method: :post, class: "stack-sm", style: "padding:6px" do %>
|
|
69
|
+
<%= hidden_field_tag :to, to %>
|
|
70
|
+
<% if to == "ready" %><%= hidden_field_tag :override_criteria, "1" %><span class="hint">No acceptance criteria yet. Override with a reason (recorded in the audit log).</span><% end %>
|
|
71
|
+
<input class="input" name="reason" placeholder="Reason (required)" required autofocus>
|
|
72
|
+
<button class="btn btn-primary btn-sm">Move to <%= Agentboard::StateMachines::Ticket::LABELS[to] %></button>
|
|
73
|
+
<% end %>
|
|
74
|
+
</div>
|
|
75
|
+
</details>
|
|
76
|
+
<% elsif to == "testing" && !t.artifacts.where(kind: %w[commit diff pull_request]).exists? %>
|
|
77
|
+
<%= button_to transition_ticket_path(t), params: { to: to, verification_requested: "1" }, class: "btn btn-sm", form: { style: "display:inline" } do %><%= status_glyph(to) %> Request verification<% end %>
|
|
78
|
+
<% elsif to == "done" && !can?("ticket.complete") %>
|
|
79
|
+
<span class="btn btn-sm is-disabled" title="Completing tickets needs the maintainer role"><%= icon("lock", size: 13) %> Mark done</span>
|
|
80
|
+
<% elsif to == "done" %>
|
|
81
|
+
<%= button_to complete_ticket_path(t), params: { lock_version: t.lock_version }, class: "btn btn-sm #{@completion[:ok] ? "btn-success" : "is-disabled"}", form: { style: "display:inline" }, title: @completion[:missing].join("\n") do %><%= icon("check", size: 13) %> Mark done<% end %>
|
|
82
|
+
<% else %>
|
|
83
|
+
<%= button_to transition_ticket_path(t), params: { to: to, lock_version: t.lock_version }, class: "btn btn-sm", form: { style: "display:inline" } do %><%= status_glyph(to) %> <%= Agentboard::StateMachines::Ticket::LABELS[to] %><% end %>
|
|
84
|
+
<% end %>
|
|
85
|
+
<% end %>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<section class="card" id="criteria">
|
|
89
|
+
<div class="card-head">
|
|
90
|
+
<h2><%= icon("check", size: 15) %> Acceptance criteria</h2>
|
|
91
|
+
<% done, total = t.criteria_progress %>
|
|
92
|
+
<span class="row muted" style="margin-left:auto"><%= meter(done, total) %> <%= done %>/<%= total %></span>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="card-body">
|
|
95
|
+
<div class="criteria">
|
|
96
|
+
<% t.acceptance_criteria.each do |c| %>
|
|
97
|
+
<div class="criterion">
|
|
98
|
+
<span class="cbox is-<%= c.state %>"><% if c.state == "verified" %><%= icon("check", size: 12, stroke: 3) %><% elsif c.state == "failed" %><%= icon("x", size: 12, stroke: 3) %><% elsif c.state == "waived" %>–<% end %></span>
|
|
99
|
+
<div class="criterion-text">
|
|
100
|
+
<div><%= c.description %></div>
|
|
101
|
+
<div class="faint" style="font-size:11.5px"><%= c.verification_kind %><% if c.verified_by %> · <%= c.state %> by <%= c.verified_by %> <%= ago(c.verified_at) %><% end %></div>
|
|
102
|
+
</div>
|
|
103
|
+
<div class="criterion-actions" <%= "hidden" unless can?("ticket.write") %>>
|
|
104
|
+
<% { "verified" => "check", "failed" => "x", "waived" => "ban", "pending" => "refresh" }.except(c.state).each do |state, ic| %>
|
|
105
|
+
<%= button_to ticket_criterion_path(t, c), method: :patch, params: { state: state }, class: "btn btn-ghost btn-sm btn-icon", title: "Mark #{state}", form: { style: "display:inline" } do %><%= icon(ic, size: 13) %><% end %>
|
|
106
|
+
<% end %>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
<% end %>
|
|
110
|
+
</div>
|
|
111
|
+
<%= form_with url: ticket_criteria_path(t), scope: :criterion, class: "row mt-sm", hidden: !can?("ticket.write") do |f| %>
|
|
112
|
+
<%= f.text_field :description, class: "input", placeholder: "Add a criterion…", required: true %>
|
|
113
|
+
<%= f.select :verification_kind, Agentboard::AcceptanceCriterion::VERIFICATION_KINDS, {}, class: "select", style: "width:120px" %>
|
|
114
|
+
<button class="btn"><%= icon("plus", size: 14) %></button>
|
|
115
|
+
<% end %>
|
|
116
|
+
</div>
|
|
117
|
+
</section>
|
|
118
|
+
|
|
119
|
+
<% if t.subtasks.any? || t.parent %>
|
|
120
|
+
<section class="card">
|
|
121
|
+
<div class="card-head"><h2><%= icon("layers", size: 15) %> <%= t.parent ? "Part of" : "Subtasks" %></h2>
|
|
122
|
+
<% unless t.parent %><%= link_to new_ticket_path(parent: t.id), class: "btn btn-ghost btn-sm", style: "margin-left:auto" do %><%= icon("plus", size: 12) %> Subtask<% end %><% end %></div>
|
|
123
|
+
<div class="card-body-flush">
|
|
124
|
+
<% (t.parent ? [t.parent] : t.subtasks.includes(:project).ordered).each do |sub| %>
|
|
125
|
+
<%= link_to ticket_path(sub), class: "list-row" do %><%= status_glyph(sub.status) %><span class="mono muted" style="font-size:12px"><%= sub.key %></span><span class="grow truncate"><%= sub.title %></span><%= status_pill(sub.status) %><% end %>
|
|
126
|
+
<% end %>
|
|
127
|
+
</div>
|
|
128
|
+
</section>
|
|
129
|
+
<% end %>
|
|
130
|
+
|
|
131
|
+
<section class="card">
|
|
132
|
+
<div class="card-head"><h2><%= icon("sparkles", size: 15) %> Suggestions</h2><span class="muted" style="margin-left:auto;font-size:12px">From static analysis · nothing changes until you accept</span></div>
|
|
133
|
+
<div class="card-body-flush">
|
|
134
|
+
<turbo-frame id="ticket-suggestions" src="<%= suggestions_ticket_path(t) %>" loading="lazy">
|
|
135
|
+
<div class="empty" style="padding:16px"><span>Analysing the repository…</span></div>
|
|
136
|
+
</turbo-frame>
|
|
137
|
+
</div>
|
|
138
|
+
</section>
|
|
139
|
+
|
|
140
|
+
<section class="card">
|
|
141
|
+
<div class="card-head">
|
|
142
|
+
<h2><%= icon("flask", size: 15) %> Verification</h2>
|
|
143
|
+
<span class="muted" style="margin-left:auto;font-size:12px">head <%= sha_tag(@head_sha) %></span>
|
|
144
|
+
<%= button_to verify_ticket_path(t), class: "btn btn-sm", form: { style: "display:inline", hidden: !can?("execution.start") }, title: "Run the approved command catalog against the repository HEAD" do %><%= icon("play", size: 12) %> Verify HEAD<% end %>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="card-body-flush">
|
|
147
|
+
<% if @checks[:checks].empty? %>
|
|
148
|
+
<div class="empty" style="padding:20px"><span><%= @checks[:missing].first || "No required checks configured" %></span></div>
|
|
149
|
+
<% else %>
|
|
150
|
+
<% @checks[:checks].each do |c| %>
|
|
151
|
+
<div class="check-row">
|
|
152
|
+
<%= execution_pill(c[:status]) %>
|
|
153
|
+
<strong class="mono" style="font-size:12.5px"><%= c[:name] %></strong>
|
|
154
|
+
<span class="muted grow" style="font-size:12px"><%= c[:status] == "stale" ? "passed only on an older commit — re-run for the current head" : (c[:status] == "missing" ? "not run for this head" : "") %></span>
|
|
155
|
+
<%= sha_tag(c[:commit_sha]) %>
|
|
156
|
+
</div>
|
|
157
|
+
<% end %>
|
|
158
|
+
<% end %>
|
|
159
|
+
</div>
|
|
160
|
+
</section>
|
|
161
|
+
|
|
162
|
+
<section class="card">
|
|
163
|
+
<div class="card-head"><h2><%= icon("commit", size: 15) %> Evidence</h2><span class="muted" style="margin-left:auto;font-size:12px"><%= @artifacts.size %> artifacts</span></div>
|
|
164
|
+
<div class="card-body-flush">
|
|
165
|
+
<% if @artifacts.empty? %>
|
|
166
|
+
<div class="empty" style="padding:20px"><span>Commits, diffs, test reports and PRs linked by agents or the runtime appear here.</span></div>
|
|
167
|
+
<% end %>
|
|
168
|
+
<% @artifacts.each do |a| %>
|
|
169
|
+
<div class="list-row">
|
|
170
|
+
<%= icon({ "commit" => "commit", "pull_request" => "branch", "test_report" => "flask", "context" => "layers", "log" => "terminal" }.fetch(a.kind, "file"), size: 15, klass: "muted") %>
|
|
171
|
+
<span class="grow truncate">
|
|
172
|
+
<strong style="font-weight:550"><%= a.title.presence || a.kind.humanize %></strong>
|
|
173
|
+
<% if a.commit_sha %> <%= sha_tag(a.commit_sha) %><% end %>
|
|
174
|
+
<% if a.kind == "commit" && a.metadata["verified_in_repo"] == false %><span class="pill pill-blocked" style="height:18px;font-size:11px">not in repo</span><% end %>
|
|
175
|
+
</span>
|
|
176
|
+
<%= provenance_badge(a.provenance) %>
|
|
177
|
+
<span class="faint" style="font-size:12px"><%= ago(a.created_at) %></span>
|
|
178
|
+
</div>
|
|
179
|
+
<% end %>
|
|
180
|
+
<details class="popover" style="padding:8px 14px">
|
|
181
|
+
<summary class="btn btn-ghost btn-sm"><%= icon("branch", size: 13) %> Link pull request manually</summary>
|
|
182
|
+
<%= form_with url: link_pr_ticket_path(t), class: "stack-sm mt-sm" do %>
|
|
183
|
+
<input class="input" name="url" placeholder="https://github.com/owner/repo/pull/123" required>
|
|
184
|
+
<div class="row">
|
|
185
|
+
<select name="state" class="select" style="width:130px"><option>open</option><option>merged</option><option>closed</option></select>
|
|
186
|
+
<input class="input mono" name="merge_commit_sha" placeholder="merge commit SHA (required for merged)">
|
|
187
|
+
<button class="btn btn-sm">Link</button>
|
|
188
|
+
</div>
|
|
189
|
+
<span class="hint">A manual merge is accepted only if the merge commit exists locally and contains the reviewed head. Prefer the signed GitHub webhook.</span>
|
|
190
|
+
<% end %>
|
|
191
|
+
</details>
|
|
192
|
+
</div>
|
|
193
|
+
</section>
|
|
194
|
+
|
|
195
|
+
<section class="card">
|
|
196
|
+
<div class="card-head"><h2><%= icon("terminal", size: 15) %> Executions</h2></div>
|
|
197
|
+
<div class="card-body-flush">
|
|
198
|
+
<% if @executions.empty? %>
|
|
199
|
+
<div class="empty" style="padding:20px"><span>No executions yet. Start one from the panel on the right, or let a developer's own agent session work on it through MCP.</span></div>
|
|
200
|
+
<% end %>
|
|
201
|
+
<% @executions.each do |e| %>
|
|
202
|
+
<%= link_to execution_path(e), class: "list-row" do %>
|
|
203
|
+
<%= provider_avatar(e.provider_key) %>
|
|
204
|
+
<span class="grow"><strong style="font-weight:550"><%= e.action.humanize %></strong> <span class="muted">· attempt <%= e.attempt %> · <%= e.provider_key %></span></span>
|
|
205
|
+
<% if e.verification_results.any? %><span class="muted" style="font-size:12px"><%= e.verification_results.count(&:passed?) %>/<%= e.verification_results.size %> checks</span><% end %>
|
|
206
|
+
<%= execution_pill(e.status) %>
|
|
207
|
+
<span class="faint" style="font-size:12px;width:90px;text-align:right"><%= ago(e.created_at) %></span>
|
|
208
|
+
<% end %>
|
|
209
|
+
<% end %>
|
|
210
|
+
</div>
|
|
211
|
+
</section>
|
|
212
|
+
|
|
213
|
+
<section class="card" id="activity">
|
|
214
|
+
<div class="card-head"><h2><%= icon("pulse", size: 15) %> Activity</h2>
|
|
215
|
+
<span class="row" style="margin-left:auto;gap:4px"><% %w[confirmed provider_reported git_observed inferred].each do |p| %><%= provenance_badge(p) %><% end %></span>
|
|
216
|
+
</div>
|
|
217
|
+
<div class="card-body">
|
|
218
|
+
<%= form_with url: ticket_comments_path(t), scope: :comment, class: "composer mb", hidden: !can?("ticket.comment") do |f| %>
|
|
219
|
+
<%= f.text_area :body, class: "textarea", placeholder: "Leave a comment… (secrets are redacted automatically)", required: true %>
|
|
220
|
+
<div class="row"><span class="hint grow">Comments are visible to agents through MCP.</span><button class="btn btn-primary btn-sm">Comment</button></div>
|
|
221
|
+
<% end %>
|
|
222
|
+
<div class="timeline">
|
|
223
|
+
<% comments = t.comments.index_by(&:id) %>
|
|
224
|
+
<% @events.each do |e| %>
|
|
225
|
+
<% ic, tone = event_icon(e) %>
|
|
226
|
+
<div class="tl-item">
|
|
227
|
+
<span class="tl-icon <%= tone %>"><%= icon(ic, size: 13) %></span>
|
|
228
|
+
<div class="tl-body">
|
|
229
|
+
<div class="tl-line">
|
|
230
|
+
<span class="tl-who"><%= actor_label(e) %></span>
|
|
231
|
+
<span><%= event_title(e) %></span>
|
|
232
|
+
<% unless e.provenance == "confirmed" %><%= provenance_badge(e.provenance) %><% end %>
|
|
233
|
+
<span class="tl-when"><%= ago(e.occurred_at) %></span>
|
|
234
|
+
</div>
|
|
235
|
+
<% if e.event_type == "ticket.commented" && (c = comments[e.payload["comment_id"]]) %>
|
|
236
|
+
<div class="comment-bubble"><%= c.body %></div>
|
|
237
|
+
<% end %>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
<% end %>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
</section>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
<aside class="detail-side">
|
|
247
|
+
<div class="card"><div class="card-body">
|
|
248
|
+
<dl class="kv">
|
|
249
|
+
<dt>Status</dt><dd><%= status_pill(t.status) %></dd>
|
|
250
|
+
<dt>Priority</dt><dd><%= priority_icon(t.priority) %> <%= t.priority_label %></dd>
|
|
251
|
+
<dt>Lease</dt><dd><% if lease %><%= avatar(lease.holder_label, kind: lease.agent_session_id ? "agent" : "human", size: 20) %><span class="truncate"><%= lease.holder_label %></span><span class="faint" style="font-size:11.5px">until <%= lease.expires_at.strftime("%H:%M") %> · #<%= lease.fencing_token %></span><% else %><span class="faint">Unclaimed</span><% end %></dd>
|
|
252
|
+
<dt>Depends on</dt>
|
|
253
|
+
<dd>
|
|
254
|
+
<% t.dependencies.includes(:depends_on_ticket).each do |d| %>
|
|
255
|
+
<span class="row" style="gap:4px"><%= status_glyph(d.depends_on_ticket.status) %><%= link_to d.depends_on_ticket.key, ticket_path(d.depends_on_ticket), class: "link mono", style: "font-size:12px" %>
|
|
256
|
+
<%= button_to ticket_dependency_path(t, d), method: :delete, class: "btn btn-ghost btn-sm btn-icon", form: { style: "display:inline" }, title: "Remove" do %><%= icon("x", size: 11) %><% end %></span>
|
|
257
|
+
<% end %>
|
|
258
|
+
<details class="popover"><summary class="btn btn-ghost btn-sm"><%= icon("plus", size: 12) %> Add</summary>
|
|
259
|
+
<div class="popover-panel" style="left:0;right:auto">
|
|
260
|
+
<%= form_with url: ticket_dependencies_path(t), class: "row", style: "padding:6px" do %>
|
|
261
|
+
<select name="depends_on" class="select"><% @other_tickets.each do |o| %><option value="<%= o.key %>"><%= o.key %> — <%= o.title.truncate(40) %></option><% end %></select>
|
|
262
|
+
<button class="btn btn-sm btn-primary">Add</button>
|
|
263
|
+
<% end %>
|
|
264
|
+
</div>
|
|
265
|
+
</details>
|
|
266
|
+
</dd>
|
|
267
|
+
<dt>Milestone</dt><dd><% if t.milestone %><%= icon("flag", size: 13, klass: "muted") %> <%= t.milestone.name %><% else %><span class="faint">None</span><% end %></dd>
|
|
268
|
+
<% if t.subtasks.none? && t.parent.nil? %><dt>Subtasks</dt><dd><%= link_to new_ticket_path(parent: t.id), class: "btn btn-ghost btn-sm" do %><%= icon("plus", size: 12) %> Add<% end %></dd><% end %>
|
|
269
|
+
<dt>Created</dt><dd class="muted"><%= ago(t.created_at) %></dd>
|
|
270
|
+
<dt>Version</dt><dd class="muted mono" style="font-size:12px">v<%= t.lock_version %></dd>
|
|
271
|
+
</dl>
|
|
272
|
+
</div></div>
|
|
273
|
+
|
|
274
|
+
<div class="card">
|
|
275
|
+
<div class="card-head"><h3><%= icon("shield", size: 14) %> Completion gates</h3></div>
|
|
276
|
+
<div class="card-body">
|
|
277
|
+
<% gates = [] %>
|
|
278
|
+
<% gates << ["Acceptance criteria verified", t.acceptance_criteria.any? && t.acceptance_criteria.all?(&:satisfied?)] if Agentboard.config.completion["require_criteria_verified"] %>
|
|
279
|
+
<% gates << ["Required checks pass at head", @checks[:ok]] if Agentboard.config.completion["require_checks"] %>
|
|
280
|
+
<% gates << ["Human review approved for head", @completion.dig(:review, :status) == "approved"] if Agentboard.config.completion["require_approval"] %>
|
|
281
|
+
<% gates << ["Merged PR verified", @completion.dig(:pull_request, :state) == "merged"] if Agentboard::CompletionPolicy.merged_pr_required?(t) %>
|
|
282
|
+
<% gates.each do |label, ok| %>
|
|
283
|
+
<div class="gate <%= ok ? "ok" : "no" %>"><%= icon(ok ? "check" : "x", size: 14, stroke: 2.5) %><span><%= label %></span></div>
|
|
284
|
+
<% end %>
|
|
285
|
+
<div class="divider"></div>
|
|
286
|
+
<p class="hint" style="margin:0">An agent can request completion but cannot bypass these gates. Evidence is checked against persisted records only.</p>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<% pending = @approvals.select(&:pending?) %>
|
|
291
|
+
<% if pending.any? %>
|
|
292
|
+
<div class="card">
|
|
293
|
+
<div class="card-head"><h3><%= icon("shield", size: 14) %> Awaiting your approval</h3></div>
|
|
294
|
+
<div class="card-body stack">
|
|
295
|
+
<% pending.each do |a| %>
|
|
296
|
+
<%= render "agentboard/approvals/approval", approval: a %>
|
|
297
|
+
<% end %>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
<% end %>
|
|
301
|
+
|
|
302
|
+
<div class="card" <%= "hidden" unless can?("execution.start") %>>
|
|
303
|
+
<div class="card-head"><h3><%= icon("play", size: 14) %> Run an agent</h3></div>
|
|
304
|
+
<div class="card-body">
|
|
305
|
+
<%= form_with url: run_ticket_path(t), class: "stack-sm" do %>
|
|
306
|
+
<%= hidden_field_tag :idempotency_key, SecureRandom.uuid %>
|
|
307
|
+
<div class="form-grid" style="grid-template-columns:1fr 1fr">
|
|
308
|
+
<div class="field"><label>Provider</label>
|
|
309
|
+
<select name="provider" class="select"><% @providers.each do |p| %><option value="<%= p %>" <%= "selected" if p == "fake" %>><%= Agentboard::Providers.registry.adapter_class(p).display_name %></option><% end %></select></div>
|
|
310
|
+
<div class="field"><label>Action</label>
|
|
311
|
+
<select name="action_name" class="select"><% %w[implement plan review fix_tests summarize].each do |a| %><option value="<%= a %>"><%= a.humanize %></option><% end %></select></div>
|
|
312
|
+
</div>
|
|
313
|
+
<button class="btn btn-primary" <%= "disabled" unless Agentboard::Executions::RUNNABLE_TICKET_STATES.include?(t.status) %>><%= icon("play", size: 13) %> Start execution</button>
|
|
314
|
+
<span class="hint">Runs in an isolated worktree; results are verified independently.</span>
|
|
315
|
+
<% end %>
|
|
316
|
+
<% if @workflows.any? %>
|
|
317
|
+
<div class="divider"></div>
|
|
318
|
+
<%= form_with url: run_ticket_path(t), class: "row" do %>
|
|
319
|
+
<select name="workflow" class="select"><% @workflows.each do |w| %><option value="<%= w.name %>"><%= w.name %> v<%= w.version %></option><% end %></select>
|
|
320
|
+
<button class="btn"><%= icon("workflow", size: 13) %> Run</button>
|
|
321
|
+
<% end %>
|
|
322
|
+
<% end %>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<% if @sessions.any? %>
|
|
327
|
+
<div class="card">
|
|
328
|
+
<div class="card-head"><h3><%= icon("bot", size: 14) %> Agent sessions</h3></div>
|
|
329
|
+
<div class="card-body-flush">
|
|
330
|
+
<% @sessions.each do |link| %>
|
|
331
|
+
<div class="list-row">
|
|
332
|
+
<%= provider_avatar(link.agent_session.provider_key, size: 20) %>
|
|
333
|
+
<span class="grow truncate"><%= link.agent_session.display_name %><br><span class="faint" style="font-size:11.5px"><%= link.source.humanize %> · <%= link.agent_session.trust_level %><%= " · ended" if link.ended_at %></span></span>
|
|
334
|
+
<% if link.confidence == "needs_confirmation" && link.ended_at.nil? %>
|
|
335
|
+
<%= button_to "Confirm", session_path(link.agent_session), method: :patch, params: { ticket_key: t.key }, class: "btn btn-sm" %>
|
|
336
|
+
<% end %>
|
|
337
|
+
</div>
|
|
338
|
+
<% end %>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
<% end %>
|
|
342
|
+
</aside>
|
|
343
|
+
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<turbo-frame id="ticket-suggestions">
|
|
2
|
+
<% if @suggestions.empty? %>
|
|
3
|
+
<div class="empty" style="padding:16px"><span>No suggestions for this ticket.</span></div>
|
|
4
|
+
<% else %>
|
|
5
|
+
<% labels = { "file" => "Relevant file", "complexity" => "Estimated size", "criterion" => "Criterion", "subtask" => "Subtask", "dependency" => "Possible dependency" } %>
|
|
6
|
+
<% @suggestions.group_by(&:kind).each do |kind, items| %>
|
|
7
|
+
<div class="section-title" style="margin:12px 16px 4px"><%= labels[kind] || kind %></div>
|
|
8
|
+
<% items.each do |s| %>
|
|
9
|
+
<div class="list-row" style="padding:7px 16px">
|
|
10
|
+
<span class="grow" style="min-width:0">
|
|
11
|
+
<span class="<%= "mono" if kind == "file" %>" style="font-size:<%= kind == "file" ? "12px" : "13px" %>"><%= s.text %></span>
|
|
12
|
+
<span class="faint" style="font-size:11.5px;display:block"><%= s.reason %></span>
|
|
13
|
+
</span>
|
|
14
|
+
<% unless kind == "file" %>
|
|
15
|
+
<%= button_to accept_suggestion_ticket_path(@ticket), params: { kind: kind, value: s.value }, class: "btn btn-sm", form: { style: "display:inline", data: { turbo_frame: "_top" } } do %><%= icon("plus", size: 12) %> Accept<% end %>
|
|
16
|
+
<% end %>
|
|
17
|
+
</div>
|
|
18
|
+
<% end %>
|
|
19
|
+
<% end %>
|
|
20
|
+
<% end %>
|
|
21
|
+
</turbo-frame>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<% content_for :title, "Workflow run" %>
|
|
2
|
+
<% content_for :crumbs do %><%= link_to "Workflows", workflows_path %><span class="sep">/</span><%= link_to @run.ticket.key, ticket_path(@run.ticket) %><span class="sep">/</span><span><%= @run.workflow_definition.name %></span><% end %>
|
|
3
|
+
<% content_for :actions do %>
|
|
4
|
+
<% unless @run.terminal? %><%= button_to cancel_workflow_run_path(@run), class: "btn btn-sm btn-danger", form: { style: "display:inline" } do %><%= icon("stop", size: 12) %> Cancel run<% end %><% end %>
|
|
5
|
+
<% end %>
|
|
6
|
+
<div class="row mb"><h1><%= @run.workflow_definition.name %></h1><%= execution_pill(@run.status) %><span class="muted">v<%= @run.workflow_definition.version %> · started <%= ago(@run.started_at) %></span></div>
|
|
7
|
+
<% if @run.status == "waiting_for_approval" %>
|
|
8
|
+
<div class="callout callout-warn mb"><%= icon("shield", size: 16) %><div>Waiting for a human approval. <%= link_to "Review approvals →", approvals_path, class: "link" %></div></div>
|
|
9
|
+
<% end %>
|
|
10
|
+
<div class="card mb"><div class="card-body"><%= render "agentboard/shared/dag", definition: @definition, steps: @steps %></div></div>
|
|
11
|
+
<div class="card card-body-flush">
|
|
12
|
+
<table class="table table-compact">
|
|
13
|
+
<thead><tr><th>Step</th><th>Attempt</th><th>Status</th><th>Head</th><th>Execution</th><th>Error</th><th class="right">Finished</th></tr></thead>
|
|
14
|
+
<tbody>
|
|
15
|
+
<% @all_steps.each do |s| %>
|
|
16
|
+
<tr>
|
|
17
|
+
<td class="mono"><%= s.step_key %> <span class="faint"><%= s.step_type %></span></td>
|
|
18
|
+
<td><%= s.attempt %></td>
|
|
19
|
+
<td><%= execution_pill(s.status) %></td>
|
|
20
|
+
<td><%= sha_tag(s.output["head_sha"]) %></td>
|
|
21
|
+
<td><% if (ex = s.executions.max_by(&:created_at)) %><%= link_to "exec-#{ex.attempt}", execution_path(ex), class: "link" %><% else %><span class="faint">—</span><% end %></td>
|
|
22
|
+
<td class="muted" style="font-size:12px"><%= s.error.to_s.truncate(80) %><% if s.available_at && s.status == "pending" %>retry <%= ago(s.available_at).to_s.sub(" ago", "").html_safe %><% end %></td>
|
|
23
|
+
<td class="right muted"><%= ago(s.finished_at) if s.finished_at %></td>
|
|
24
|
+
</tr>
|
|
25
|
+
<% end %>
|
|
26
|
+
</tbody>
|
|
27
|
+
</table>
|
|
28
|
+
</div>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<% content_for :title, "Workflows" %>
|
|
2
|
+
<% content_for :crumbs do %><%= icon("workflow", size: 16, klass: "muted") %> <span>Workflows</span><% end %>
|
|
3
|
+
<% content_for :actions do %>
|
|
4
|
+
<%= link_to workflows_path(sync: 1), class: "btn btn-sm" do %><%= icon("refresh", size: 13) %> Sync .agentboard/workflows<% end %>
|
|
5
|
+
<% end %>
|
|
6
|
+
<div class="detail" style="grid-template-columns:minmax(0,1fr) minmax(0,1fr)">
|
|
7
|
+
<div class="stack">
|
|
8
|
+
<div class="card">
|
|
9
|
+
<div class="card-head"><h2><%= icon("layers", size: 15) %> Definitions</h2></div>
|
|
10
|
+
<div class="card-body-flush">
|
|
11
|
+
<% if @definitions.empty? %><div class="empty"><%= icon("workflow", size: 26) %><strong>No workflows yet</strong><span>Register one with the editor, or add YAML files to <code>.agentboard/workflows/</code>.</span></div><% end %>
|
|
12
|
+
<% @definitions.each do |name, versions| %>
|
|
13
|
+
<% latest = versions.first %>
|
|
14
|
+
<%= link_to workflow_path(latest), class: "list-row" do %>
|
|
15
|
+
<%= icon("workflow", size: 15, klass: "muted") %>
|
|
16
|
+
<span class="grow"><strong class="mono" style="font-size:13px"><%= name %></strong> <span class="muted">v<%= latest.version %> · <%= latest.definition["steps"].size %> steps</span></span>
|
|
17
|
+
<code title="<%= latest.digest %>"><%= latest.digest[0, 8] %></code>
|
|
18
|
+
<% end %>
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="card">
|
|
23
|
+
<div class="card-head"><h2><%= icon("play", size: 15) %> Recent runs</h2></div>
|
|
24
|
+
<div class="card-body-flush">
|
|
25
|
+
<% if @runs.empty? %><div class="empty" style="padding:20px">No runs yet.</div><% end %>
|
|
26
|
+
<% @runs.each do |r| %>
|
|
27
|
+
<%= link_to workflow_run_path(r), class: "list-row" do %>
|
|
28
|
+
<span class="mono" style="font-size:12px"><%= r.ticket.key %></span>
|
|
29
|
+
<span class="grow truncate"><%= r.workflow_definition.name %> <span class="muted">v<%= r.workflow_definition.version %></span></span>
|
|
30
|
+
<span class="muted" style="font-size:12px"><%= r.steps.count { |s| s.status == "succeeded" } %>/<%= r.workflow_definition.definition["steps"].size %></span>
|
|
31
|
+
<%= execution_pill(r.status) %>
|
|
32
|
+
<span class="faint" style="font-size:12px"><%= ago(r.created_at) %></span>
|
|
33
|
+
<% end %>
|
|
34
|
+
<% end %>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="card" data-controller="workflow-validator" data-workflow-validator-url-value="<%= validate_workflows_path %>">
|
|
39
|
+
<div class="card-head"><h2><%= icon("edit", size: 15) %> Editor</h2><span class="muted" style="margin-left:auto;font-size:12px">YAML · safe-loaded · validated as a DAG</span></div>
|
|
40
|
+
<%= form_with url: workflows_path, class: "card-body stack" do %>
|
|
41
|
+
<textarea name="yaml" class="textarea code" spellcheck="false" data-workflow-validator-target="input" data-action="input->workflow-validator#schedule keydown->workflow-validator#indent"><%= @yaml %></textarea>
|
|
42
|
+
<pre class="validator" data-workflow-validator-target="output">Validating…</pre>
|
|
43
|
+
<div class="row">
|
|
44
|
+
<span class="hint grow">Commands reference the approved catalog by name — never shell strings.</span>
|
|
45
|
+
<button class="btn btn-primary" <%= "disabled" unless can?("workflow.manage") %>><%= icon("check", size: 14) %> Register version</button>
|
|
46
|
+
</div>
|
|
47
|
+
<% end %>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<% content_for :title, @definition.name %>
|
|
2
|
+
<% content_for :crumbs do %><%= link_to "Workflows", workflows_path %><span class="sep">/</span><span class="mono"><%= @definition.name %></span><span class="faint">v<%= @definition.version %></span><% end %>
|
|
3
|
+
<div class="card mb">
|
|
4
|
+
<div class="card-head"><h2><%= icon("workflow", size: 15) %> <%= @definition.name %></h2><span class="muted">v<%= @definition.version %> · digest <code><%= @definition.digest[0, 12] %></code> · immutable once used</span></div>
|
|
5
|
+
<div class="card-body"><%= render "agentboard/shared/dag", definition: @parsed, steps: nil %></div>
|
|
6
|
+
</div>
|
|
7
|
+
<div class="detail" style="grid-template-columns:minmax(0,1fr) 340px">
|
|
8
|
+
<div class="card"><div class="card-head"><h3>Source</h3></div><div class="card-body"><pre class="code-block"><%= @definition.source_yaml.presence || JSON.pretty_generate(@definition.definition) %></pre></div></div>
|
|
9
|
+
<div class="card"><div class="card-head"><h3><%= icon("play", size: 14) %> Start a run</h3></div>
|
|
10
|
+
<%= form_with url: start_workflow_path(@definition), class: "card-body stack-sm" do %>
|
|
11
|
+
<select name="ticket_key" class="select"><% current_project.tickets.open.ordered.limit(200).each do |t| %><option value="<%= t.key %>"><%= t.key %> — <%= t.title.truncate(42) %></option><% end %></select>
|
|
12
|
+
<button class="btn btn-primary"><%= icon("play", size: 13) %> Start workflow</button>
|
|
13
|
+
<% end %>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title><%= [content_for(:title), "AgentBoard"].compact.join(" · ") %></title>
|
|
7
|
+
<%= csrf_meta_tags %>
|
|
8
|
+
<meta name="turbo-refresh-method" content="morph">
|
|
9
|
+
<meta name="turbo-refresh-scroll" content="preserve">
|
|
10
|
+
<meta name="color-scheme" content="light dark">
|
|
11
|
+
<script>(function(){var t=localStorage.getItem("agentboard-theme");if(t)document.documentElement.dataset.theme=t})()</script>
|
|
12
|
+
<link rel="stylesheet" href="<%= asset_url("agentboard.css") %>">
|
|
13
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%235e5ce6'/><path d='M9 22V10h4v12zm5 0V14h4v8zm5 0v-6h4v6z' fill='white'/></svg>">
|
|
14
|
+
<script type="importmap">
|
|
15
|
+
<%= raw({ imports: {
|
|
16
|
+
"@hotwired/turbo" => asset_url("turbo.js"),
|
|
17
|
+
"@hotwired/stimulus" => asset_url("stimulus.js"),
|
|
18
|
+
"agentboard" => asset_url("agentboard.js")
|
|
19
|
+
} }.to_json) %>
|
|
20
|
+
</script>
|
|
21
|
+
<script type="module">import "agentboard"</script>
|
|
22
|
+
</head>
|
|
23
|
+
<body class="ab" data-controller="hotkeys theme">
|
|
24
|
+
<div class="shell" data-controller="pulse" data-pulse-url-value="<%= pulse_path %>">
|
|
25
|
+
<aside class="sidebar">
|
|
26
|
+
<div class="brand">
|
|
27
|
+
<span class="brand-mark"><%= icon("kanban", size: 15, stroke: 2.4) %></span>
|
|
28
|
+
<div>
|
|
29
|
+
<div class="brand-name">AgentBoard</div>
|
|
30
|
+
<div class="brand-project"><span class="project-key"><%= current_project.key %></span><span class="truncate"><%= current_project.name %></span></div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
<%= link_to new_ticket_path, class: "btn btn-sm", style: "margin: 0 4px 6px; justify-content:flex-start", data: { hotkey: "new-ticket" } do %>
|
|
34
|
+
<%= icon("plus", size: 14) %> New ticket <kbd style="margin-left:auto">C</kbd>
|
|
35
|
+
<% end %>
|
|
36
|
+
<div class="nav-section">Work</div>
|
|
37
|
+
<%= nav_link "Board", board_path, "kanban", active: controller_name == "board", kbd: "G B" %>
|
|
38
|
+
<%= nav_link "Backlog", tickets_path, "list", active: controller_name == "tickets" && action_name == "index", kbd: "G L" %>
|
|
39
|
+
<%= nav_link "Milestones", milestones_path, "flag", active: controller_name == "milestones" %>
|
|
40
|
+
<%= nav_link "Approvals", approvals_path, "shield", active: controller_name == "approvals", count: pending_approvals_count %>
|
|
41
|
+
<div class="nav-section">Agents</div>
|
|
42
|
+
<%= nav_link "Executions", executions_path, "terminal", active: controller_name == "executions", kbd: "G E" %>
|
|
43
|
+
<%= nav_link "Workflows", workflows_path, "workflow", active: %w[workflows workflow_runs].include?(controller_name), kbd: "G W" %>
|
|
44
|
+
<%= nav_link "Agents", agents_path, "bot", active: controller_name == "agents", kbd: "G A" %>
|
|
45
|
+
<div class="nav-section">Insight</div>
|
|
46
|
+
<%= nav_link "Analytics", analytics_path, "chart", active: controller_name == "analytics" %>
|
|
47
|
+
<%= nav_link "Settings", settings_path, "settings", active: controller_name == "settings" %>
|
|
48
|
+
<span hidden>
|
|
49
|
+
<a href="<%= board_path %>" data-hotkey-target="board"></a><a href="<%= tickets_path %>" data-hotkey-target="backlog"></a>
|
|
50
|
+
<a href="<%= executions_path %>" data-hotkey-target="executions"></a><a href="<%= agents_path %>" data-hotkey-target="agents"></a>
|
|
51
|
+
<a href="<%= workflows_path %>" data-hotkey-target="workflows"></a><a href="<%= approvals_path %>" data-hotkey-target="approvals"></a>
|
|
52
|
+
</span>
|
|
53
|
+
<div class="sidebar-footer">
|
|
54
|
+
<% daemon = (@daemon ||= Agentboard::Runtime::DaemonControl.status) %>
|
|
55
|
+
<%= link_to executions_path, class: "runtime-chip #{daemon[:running] ? "is-on" : ""}", title: daemon[:running] ? "Runtime daemon pid #{daemon[:pid]}" : "Start with: bundle exec agentboard daemon start" do %>
|
|
56
|
+
<span class="led"></span>
|
|
57
|
+
<span class="grow"><%= daemon[:running] ? "Runtime online" : "Runtime offline" %></span>
|
|
58
|
+
<span class="muted"><span id="runtime-active"><%= daemon.dig(:health, "active") || 0 %></span> active</span>
|
|
59
|
+
<% end %>
|
|
60
|
+
<div class="row" title="<%= current_actor.role %>">
|
|
61
|
+
<%= avatar(current_actor.label, size: 22) %>
|
|
62
|
+
<span class="truncate" style="font-size:12.5px;font-weight:550"><%= current_actor.label %></span>
|
|
63
|
+
<span class="faint" style="font-size:11px;margin-left:auto"><%= current_actor.role %></span>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="row" style="justify-content:space-between">
|
|
66
|
+
<span class="faint" style="font-size:11.5px">v<%= Agentboard::VERSION %> · <%= Agentboard::Extensions.extensions.key?("agentboard-pro") && Agentboard::Extensions.extensions.values.any? { |e| e.enabled_features.any? } ? "Pro" : "Community" %></span>
|
|
67
|
+
<button class="btn btn-ghost btn-sm btn-icon" data-action="theme#toggle" title="Toggle theme"><%= icon("moon", size: 14) %></button>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</aside>
|
|
71
|
+
<main class="main">
|
|
72
|
+
<header class="topbar">
|
|
73
|
+
<nav class="crumbs"><%= yield :crumbs %></nav>
|
|
74
|
+
<div class="topbar-actions"><%= yield :actions %></div>
|
|
75
|
+
</header>
|
|
76
|
+
<% edition_notices.each do |notice| %>
|
|
77
|
+
<div class="callout callout-warn" style="margin:12px 22px 0"><%= icon("alert", size: 16) %><div><%= notice %></div></div>
|
|
78
|
+
<% end %>
|
|
79
|
+
<%= yield :toolbar %>
|
|
80
|
+
<%= content_for?(:bare) ? yield : tag.div(yield, class: "content") %>
|
|
81
|
+
</main>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="toasts">
|
|
84
|
+
<% flash.each do |kind, message| %>
|
|
85
|
+
<div class="toast toast-<%= kind == "alert" ? "alert" : "notice" %>" data-controller="toast">
|
|
86
|
+
<%= icon(kind == "alert" ? "alert" : "check", size: 16) %>
|
|
87
|
+
<div class="grow"><%= message %></div>
|
|
88
|
+
<button class="btn btn-ghost btn-sm btn-icon" data-action="toast#dismiss" aria-label="Dismiss"><%= icon("x", size: 13) %></button>
|
|
89
|
+
</div>
|
|
90
|
+
<% end %>
|
|
91
|
+
</div>
|
|
92
|
+
</body>
|
|
93
|
+
</html>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Agentboard::Engine.routes.draw do
|
|
2
|
+
root to: "board#show"
|
|
3
|
+
|
|
4
|
+
get "board", to: "board#show", as: :board
|
|
5
|
+
get "pulse", to: "pulse#show", as: :pulse
|
|
6
|
+
get "assets/:name", to: "assets#show", as: :engine_asset, constraints: { name: /[a-z]+\.(css|js)/ }
|
|
7
|
+
|
|
8
|
+
resources :tickets, param: :key, constraints: { key: /[A-Z][A-Z0-9_-]*-\d+/ } do
|
|
9
|
+
member do
|
|
10
|
+
post :transition
|
|
11
|
+
post :move
|
|
12
|
+
post :verify
|
|
13
|
+
post :run
|
|
14
|
+
post :complete
|
|
15
|
+
post :link_pr
|
|
16
|
+
get :suggestions
|
|
17
|
+
post :accept_suggestion
|
|
18
|
+
end
|
|
19
|
+
resources :comments, only: :create
|
|
20
|
+
resources :criteria, only: %i[create update], controller: "criteria"
|
|
21
|
+
resources :dependencies, only: %i[create destroy]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
resources :executions, only: %i[index show] do
|
|
25
|
+
member do
|
|
26
|
+
post :cancel
|
|
27
|
+
post :retry
|
|
28
|
+
post :resume
|
|
29
|
+
get :log
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
resources :milestones, only: %i[index create update]
|
|
34
|
+
resources :captures, only: :create do
|
|
35
|
+
member { get :screenshot }
|
|
36
|
+
end
|
|
37
|
+
resources :approvals, only: %i[index update]
|
|
38
|
+
resources :workflows, only: %i[index show create] do
|
|
39
|
+
collection { post :validate }
|
|
40
|
+
member { post :start }
|
|
41
|
+
end
|
|
42
|
+
resources :workflow_runs, only: :show do
|
|
43
|
+
member { post :cancel }
|
|
44
|
+
end
|
|
45
|
+
resources :sessions, only: :update
|
|
46
|
+
get "agents", to: "agents#index", as: :agents
|
|
47
|
+
get "analytics", to: "analytics#show", as: :analytics
|
|
48
|
+
get "settings", to: "settings#show", as: :settings
|
|
49
|
+
end
|