brainiac-basecamp 0.0.1
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/README.md +322 -0
- data/lib/brainiac/plugins/basecamp/cli.rb +411 -0
- data/lib/brainiac/plugins/basecamp/client.rb +174 -0
- data/lib/brainiac/plugins/basecamp/config.rb +115 -0
- data/lib/brainiac/plugins/basecamp/epic.rb +200 -0
- data/lib/brainiac/plugins/basecamp/epic_branch.rb +271 -0
- data/lib/brainiac/plugins/basecamp/hooks.rb +820 -0
- data/lib/brainiac/plugins/basecamp/metadata.rb +20 -0
- data/lib/brainiac/plugins/basecamp/orchestrator.rb +722 -0
- data/lib/brainiac/plugins/basecamp/prompts.rb +41 -0
- data/lib/brainiac/plugins/basecamp/review_gate.rb +383 -0
- data/lib/brainiac/plugins/basecamp/version.rb +9 -0
- data/lib/brainiac/plugins/basecamp/webhook.rb +189 -0
- data/lib/brainiac/plugins/basecamp.rb +314 -0
- data/lib/brainiac_basecamp.rb +4 -0
- metadata +126 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "basecamp/version"
|
|
4
|
+
require_relative "basecamp/metadata"
|
|
5
|
+
require_relative "basecamp/config"
|
|
6
|
+
require_relative "basecamp/client"
|
|
7
|
+
require_relative "basecamp/epic"
|
|
8
|
+
require_relative "basecamp/epic_branch"
|
|
9
|
+
require_relative "basecamp/review_gate"
|
|
10
|
+
require_relative "basecamp/orchestrator"
|
|
11
|
+
require_relative "basecamp/webhook"
|
|
12
|
+
require_relative "basecamp/hooks"
|
|
13
|
+
require_relative "basecamp/prompts"
|
|
14
|
+
require_relative "basecamp/cli"
|
|
15
|
+
|
|
16
|
+
module Brainiac
|
|
17
|
+
module Plugins
|
|
18
|
+
module Basecamp
|
|
19
|
+
class << self
|
|
20
|
+
# Called by Brainiac plugin system during server startup.
|
|
21
|
+
#
|
|
22
|
+
# @param app [Sinatra::Application] The running Brainiac server
|
|
23
|
+
def register(app)
|
|
24
|
+
Config.load!
|
|
25
|
+
|
|
26
|
+
# Register lifecycle hooks
|
|
27
|
+
Hooks.register_all!
|
|
28
|
+
|
|
29
|
+
# Register channel prompt (for when agents need Basecamp awareness)
|
|
30
|
+
Brainiac.register_channel_prompt(:basecamp, Prompts::CHANNEL)
|
|
31
|
+
|
|
32
|
+
# Set up routes
|
|
33
|
+
setup_routes(app)
|
|
34
|
+
|
|
35
|
+
# Log active epics on startup and resume them
|
|
36
|
+
active = Orchestrator.active_epics
|
|
37
|
+
if active.any?
|
|
38
|
+
LOG.info "[Basecamp] #{active.size} active epic(s) in progress"
|
|
39
|
+
active.each { |e| LOG.info "[Basecamp] - #{e['title']} (#{e['tasks']&.count { |t| t['status'] == 'complete' }}/#{e['tasks']&.size} complete)" }
|
|
40
|
+
|
|
41
|
+
# Resume active epics in background after server is ready
|
|
42
|
+
Thread.new do
|
|
43
|
+
sleep 10 # Wait for server to fully start
|
|
44
|
+
resume_active_epics(active)
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
LOG.error "[Basecamp] Error resuming epics: #{e.message}" if defined?(LOG)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
LOG.info "[Basecamp] Plugin registered (webhook: /basecamp, review_gate: #{Config.review_gate})"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Resume active epics on server startup.
|
|
56
|
+
# For each epic, checks task states and takes appropriate action.
|
|
57
|
+
def resume_active_epics(epics)
|
|
58
|
+
epics.each do |epic|
|
|
59
|
+
LOG.info "[Basecamp] Resuming epic: #{epic['title']}" if defined?(LOG)
|
|
60
|
+
|
|
61
|
+
# Check if all tasks are complete — finalize the epic
|
|
62
|
+
if epic["tasks"]&.all? { |t| t["status"] == "complete" }
|
|
63
|
+
LOG.info "[Basecamp] Resume: all tasks complete — finalizing epic" if defined?(LOG)
|
|
64
|
+
|
|
65
|
+
# Mark each Basecamp todo as complete (in case they weren't marked during normal flow)
|
|
66
|
+
epic["tasks"].each do |task|
|
|
67
|
+
Orchestrator.send(:mark_todo_complete, epic, task["fizzy_card"])
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
Orchestrator.send(:complete_epic, epic)
|
|
71
|
+
Orchestrator.send(:save_epic, epic)
|
|
72
|
+
next
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
epic["tasks"]&.each do |task|
|
|
76
|
+
card_number = task["fizzy_card"]
|
|
77
|
+
status = task["status"]
|
|
78
|
+
LOG.info "[Basecamp] Resume: task ##{card_number} status=#{status}" if defined?(LOG)
|
|
79
|
+
|
|
80
|
+
case status
|
|
81
|
+
when "in_review", "in_flight"
|
|
82
|
+
# Sync gate state from GitHub and decide next action
|
|
83
|
+
resume_in_review_task(epic, task)
|
|
84
|
+
when "final_decision"
|
|
85
|
+
# Final decision was pending — check if we can merge
|
|
86
|
+
resume_final_decision_task(epic, task)
|
|
87
|
+
when "pending"
|
|
88
|
+
# Check if dependencies are met and dispatch
|
|
89
|
+
# (This is handled by dispatch_unblocked_tasks normally)
|
|
90
|
+
next
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Dispatch any unblocked tasks
|
|
95
|
+
Orchestrator.send(:dispatch_unblocked_tasks, epic)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def resume_in_review_task(epic, task)
|
|
100
|
+
card_number = task["fizzy_card"]
|
|
101
|
+
pr_number = task["pr_number"]
|
|
102
|
+
|
|
103
|
+
unless pr_number
|
|
104
|
+
LOG.info "[Basecamp] Task ##{card_number} has no PR yet — skipping resume" if defined?(LOG)
|
|
105
|
+
return
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Get project config for repo path
|
|
109
|
+
project_key = task["project"]
|
|
110
|
+
projects_file = File.join(ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac")), "projects.json")
|
|
111
|
+
projects = File.exist?(projects_file) ? JSON.parse(File.read(projects_file)) : {}
|
|
112
|
+
repo_path = projects.dig(project_key, "repo_path")
|
|
113
|
+
|
|
114
|
+
unless repo_path
|
|
115
|
+
LOG.warn "[Basecamp] No repo_path for project #{project_key}" if defined?(LOG)
|
|
116
|
+
return
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Sync gate approvals from GitHub
|
|
120
|
+
sync_result = ReviewGate.sync_from_github(task, repo_path: repo_path)
|
|
121
|
+
if sync_result[:synced]
|
|
122
|
+
changes = sync_result[:changes] || {}
|
|
123
|
+
if changes[:approvals_added]&.any?
|
|
124
|
+
LOG.info "[Basecamp] Resume: synced approvals for ##{card_number}: #{changes[:approvals_added].join(', ')}" if defined?(LOG)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Check if all gates have approved
|
|
129
|
+
if ReviewGate.all_gates_passed?(task)
|
|
130
|
+
LOG.info "[Basecamp] Resume: all gates passed for ##{card_number} — dispatching final decision" if defined?(LOG)
|
|
131
|
+
task["status"] = "final_decision"
|
|
132
|
+
task["awaiting_final_decision"] = true
|
|
133
|
+
Hooks.send(:save_epic_state, epic)
|
|
134
|
+
Hooks.send(:dispatch_final_decision, epic, task, {})
|
|
135
|
+
else
|
|
136
|
+
# Gates haven't all approved — check if we need to dispatch fixes or wait
|
|
137
|
+
approvals = task["gate_approvals"]&.size || 0
|
|
138
|
+
changes = task["changes_requested_by"]&.size || 0
|
|
139
|
+
LOG.info "[Basecamp] Resume: ##{card_number} has #{approvals} approvals, #{changes} changes_requested — waiting" if defined?(LOG)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def resume_final_decision_task(epic, task)
|
|
144
|
+
card_number = task["fizzy_card"]
|
|
145
|
+
LOG.info "[Basecamp] Resume: checking final_decision task ##{card_number}, awaiting=#{task['awaiting_final_decision']}" if defined?(LOG)
|
|
146
|
+
|
|
147
|
+
# If awaiting_final_decision is set, re-dispatch
|
|
148
|
+
# Also handle the case where it's nil but gates are all approved (stale state)
|
|
149
|
+
if task["awaiting_final_decision"] || ReviewGate.all_gates_passed?(task)
|
|
150
|
+
LOG.info "[Basecamp] Resume: dispatching final decision for ##{card_number}" if defined?(LOG)
|
|
151
|
+
task["awaiting_final_decision"] = true
|
|
152
|
+
Hooks.send(:save_epic_state, epic)
|
|
153
|
+
Hooks.send(:dispatch_final_decision, epic, task, {})
|
|
154
|
+
else
|
|
155
|
+
LOG.info "[Basecamp] Resume: final_decision task ##{card_number} not ready (awaiting=#{task['awaiting_final_decision']}, gates_passed=#{ReviewGate.all_gates_passed?(task)})" if defined?(LOG)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def setup_routes(app)
|
|
160
|
+
setup_webhook_route(app)
|
|
161
|
+
setup_api_routes(app)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def setup_webhook_route(app)
|
|
165
|
+
app.post "/basecamp" do
|
|
166
|
+
content_type :json
|
|
167
|
+
request.body.rewind
|
|
168
|
+
payload_body = request.body.read
|
|
169
|
+
|
|
170
|
+
begin
|
|
171
|
+
payload = JSON.parse(payload_body)
|
|
172
|
+
rescue JSON::ParserError => e
|
|
173
|
+
LOG.error "[Basecamp] Invalid JSON: #{e.message}"
|
|
174
|
+
halt 400, { error: "Invalid JSON" }.to_json
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
status_code, body = Webhook.handle(payload)
|
|
178
|
+
halt status_code, body
|
|
179
|
+
rescue StandardError => e
|
|
180
|
+
LOG.error "[Basecamp] Unhandled error: #{e.message}\n#{e.backtrace.first(5).join("\n")}"
|
|
181
|
+
halt 500, { error: e.message }.to_json
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def setup_api_routes(app)
|
|
186
|
+
# Status endpoint
|
|
187
|
+
app.get "/api/basecamp" do
|
|
188
|
+
content_type :json
|
|
189
|
+
config = Config.current
|
|
190
|
+
{
|
|
191
|
+
enabled: true,
|
|
192
|
+
review_gate: config["review_gate"],
|
|
193
|
+
bot_accounts: config["bot_accounts"].keys,
|
|
194
|
+
project_mappings: config["project_mappings"].keys,
|
|
195
|
+
active_epics: Orchestrator.active_epics.size,
|
|
196
|
+
total_epics: Orchestrator.all_epics.size
|
|
197
|
+
}.to_json
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# List epics
|
|
201
|
+
app.get "/api/basecamp/epics" do
|
|
202
|
+
content_type :json
|
|
203
|
+
epics = params["status"] == "all" ? Orchestrator.all_epics : Orchestrator.active_epics
|
|
204
|
+
{ epics: epics }.to_json
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Get specific epic with dependency graph
|
|
208
|
+
app.get "/api/basecamp/epics/:id" do
|
|
209
|
+
content_type :json
|
|
210
|
+
epic = Orchestrator.find_epic(params["id"])
|
|
211
|
+
halt 404, { error: "Epic not found" }.to_json unless epic
|
|
212
|
+
|
|
213
|
+
# Build dependency graph from current state
|
|
214
|
+
tasks = (epic["tasks"] || []).map do |t|
|
|
215
|
+
Epic::Task.new(
|
|
216
|
+
todo_id: t["todo_id"],
|
|
217
|
+
title: t["title"],
|
|
218
|
+
fizzy_card: t["fizzy_card"],
|
|
219
|
+
depends_on: t["depends_on"] || [],
|
|
220
|
+
status: t["status"]&.to_sym || :pending,
|
|
221
|
+
completed: t["status"] == "complete"
|
|
222
|
+
)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
epic.merge("dependency_graph" => Epic.dependency_graph(tasks)).to_json
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Manually trigger an epic (for testing or when webhooks aren't set up)
|
|
229
|
+
app.post "/api/basecamp/epics" do
|
|
230
|
+
content_type :json
|
|
231
|
+
request.body.rewind
|
|
232
|
+
|
|
233
|
+
begin
|
|
234
|
+
payload = JSON.parse(request.body.read)
|
|
235
|
+
rescue JSON::ParserError
|
|
236
|
+
halt 400, { error: "Invalid JSON" }.to_json
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
todolist_id = payload["todolist_id"]
|
|
240
|
+
project_id = payload["project_id"]
|
|
241
|
+
agent = payload["agent"]
|
|
242
|
+
title = payload["title"]
|
|
243
|
+
|
|
244
|
+
halt 400, { error: "Missing required fields: todolist_id, project_id, agent, title" }.to_json unless todolist_id && project_id && agent && title
|
|
245
|
+
|
|
246
|
+
Thread.new do
|
|
247
|
+
Orchestrator.start_epic(
|
|
248
|
+
todolist_id: todolist_id,
|
|
249
|
+
project_id: project_id,
|
|
250
|
+
agent: agent,
|
|
251
|
+
title: title
|
|
252
|
+
)
|
|
253
|
+
rescue StandardError => e
|
|
254
|
+
LOG.error "[Basecamp:API] Epic start failed: #{e.message}" if defined?(LOG)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
{ status: "starting", todolist_id: todolist_id, agent: agent }.to_json
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Pause/resume an epic
|
|
261
|
+
app.post "/api/basecamp/epics/:id/pause" do
|
|
262
|
+
content_type :json
|
|
263
|
+
epic = Orchestrator.find_epic(params["id"])
|
|
264
|
+
halt 404, { error: "Epic not found" }.to_json unless epic
|
|
265
|
+
|
|
266
|
+
epic["status"] = "paused"
|
|
267
|
+
epic["paused_at"] = Time.now.iso8601
|
|
268
|
+
epic["updated_at"] = Time.now.iso8601
|
|
269
|
+
|
|
270
|
+
# Save via the epics file
|
|
271
|
+
save_epic_via_api(epic)
|
|
272
|
+
{ status: "paused", epic_id: epic["id"] }.to_json
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
app.post "/api/basecamp/epics/:id/resume" do
|
|
276
|
+
content_type :json
|
|
277
|
+
epic = Orchestrator.find_epic(params["id"])
|
|
278
|
+
halt 404, { error: "Epic not found" }.to_json unless epic
|
|
279
|
+
|
|
280
|
+
epic["status"] = "active"
|
|
281
|
+
epic["resumed_at"] = Time.now.iso8601
|
|
282
|
+
epic["updated_at"] = Time.now.iso8601
|
|
283
|
+
|
|
284
|
+
save_epic_via_api(epic)
|
|
285
|
+
|
|
286
|
+
# Re-trigger dispatch
|
|
287
|
+
Thread.new { Orchestrator.send(:resolve_and_dispatch, epic) }
|
|
288
|
+
|
|
289
|
+
{ status: "resumed", epic_id: epic["id"] }.to_json
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def save_epic_via_api(epic)
|
|
294
|
+
epics_file = File.join(
|
|
295
|
+
ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac")),
|
|
296
|
+
"basecamp_epics.json"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
all = if File.exist?(epics_file)
|
|
300
|
+
data = JSON.parse(File.read(epics_file))
|
|
301
|
+
data["epics"] || []
|
|
302
|
+
else
|
|
303
|
+
[]
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
idx = all.index { |e| e["id"] == epic["id"] }
|
|
307
|
+
all[idx] = epic if idx
|
|
308
|
+
|
|
309
|
+
File.write(epics_file, JSON.pretty_generate({ "epics" => all, "updated_at" => Time.now.iso8601 }))
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: brainiac-basecamp
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Andy Davis
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: brainiac
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.0.23
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.0.23
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: minitest
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '5.25'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '5.25'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rake
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '13.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '13.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rubocop
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.75'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.75'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rubocop-performance
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '1.25'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '1.25'
|
|
82
|
+
description: Manages epics in Basecamp with autonomous agent orchestration. Tracks
|
|
83
|
+
dependencies between Fizzy cards, dispatches agents in sequence, and syncs completion
|
|
84
|
+
status bidirectionally.
|
|
85
|
+
executables: []
|
|
86
|
+
extensions: []
|
|
87
|
+
extra_rdoc_files: []
|
|
88
|
+
files:
|
|
89
|
+
- README.md
|
|
90
|
+
- lib/brainiac/plugins/basecamp.rb
|
|
91
|
+
- lib/brainiac/plugins/basecamp/cli.rb
|
|
92
|
+
- lib/brainiac/plugins/basecamp/client.rb
|
|
93
|
+
- lib/brainiac/plugins/basecamp/config.rb
|
|
94
|
+
- lib/brainiac/plugins/basecamp/epic.rb
|
|
95
|
+
- lib/brainiac/plugins/basecamp/epic_branch.rb
|
|
96
|
+
- lib/brainiac/plugins/basecamp/hooks.rb
|
|
97
|
+
- lib/brainiac/plugins/basecamp/metadata.rb
|
|
98
|
+
- lib/brainiac/plugins/basecamp/orchestrator.rb
|
|
99
|
+
- lib/brainiac/plugins/basecamp/prompts.rb
|
|
100
|
+
- lib/brainiac/plugins/basecamp/review_gate.rb
|
|
101
|
+
- lib/brainiac/plugins/basecamp/version.rb
|
|
102
|
+
- lib/brainiac/plugins/basecamp/webhook.rb
|
|
103
|
+
- lib/brainiac_basecamp.rb
|
|
104
|
+
homepage: https://github.com/stowzilla/brainiac-basecamp
|
|
105
|
+
licenses:
|
|
106
|
+
- MIT
|
|
107
|
+
metadata:
|
|
108
|
+
rubygems_mfa_required: 'true'
|
|
109
|
+
rdoc_options: []
|
|
110
|
+
require_paths:
|
|
111
|
+
- lib
|
|
112
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '3.4'
|
|
117
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
118
|
+
requirements:
|
|
119
|
+
- - ">="
|
|
120
|
+
- !ruby/object:Gem::Version
|
|
121
|
+
version: '0'
|
|
122
|
+
requirements: []
|
|
123
|
+
rubygems_version: 3.6.9
|
|
124
|
+
specification_version: 4
|
|
125
|
+
summary: Basecamp epic orchestration plugin for Brainiac
|
|
126
|
+
test_files: []
|