brainiac-basecamp 0.0.22 โ†’ 0.0.23

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cbdaed237c170e246708af6b6f249cc90897b58aa3551eeb563e2a154145b49f
4
- data.tar.gz: ab6c55d1ef2f88e26194059cb453fda42c3660777ab34813d15b6e88fd5afaf5
3
+ metadata.gz: afa545126394e953d63caa1cc6f70658f9c0c1333c77abb6a704230b72d6f8cb
4
+ data.tar.gz: 1fe95fc5d1433efa9b0b526686da1349141de314d5de59c25073e0798d05af25
5
5
  SHA512:
6
- metadata.gz: 46f0a19b34106661205fc2cc0dc6b921511a04a7a3a2b6df2665c8d1034afc937029ac5b00b11fe3e7f42b52d8fe24bd48ef29705b15211ed76deb8e05bfde8d
7
- data.tar.gz: a278fc0d456893561a8e320ee223510ef338296605a66e1871099514f711cf3989e2d000f3864371097105c3af6a0afd332ddf87a70910870f45a488cbb1f509
6
+ metadata.gz: e68209ddcbc0ff9a4f4889d68354f424638323b99ed5c252c870e55f848680ebc752b58b9b24f97ad20f5e9f4ad7c6a96ad8f18d645f48c7ef3d299a064dc460
7
+ data.tar.gz: b94eee6029d0c89b60b4e6582af63164ac31035de1994f069c70f05afa98838443ae4380c47d21ef573d09664ff22bc3e896aab6e34f1c3b66e2225b50c94260
@@ -37,6 +37,8 @@ module Brainiac
37
37
  cmd_set(args)
38
38
  when "reset"
39
39
  cmd_reset(args)
40
+ when "scrap"
41
+ cmd_scrap(args)
40
42
  when "webhook", "webhooks"
41
43
  cmd_webhook(args)
42
44
  else
@@ -332,6 +334,183 @@ module Brainiac
332
334
  exec ENV.fetch("SHELL", "/bin/bash")
333
335
  end
334
336
 
337
+ def cmd_scrap(args)
338
+ todolist_id = args.shift
339
+
340
+ dry_run = args.delete("--dry-run") || args.delete("-n")
341
+ confirmed = args.delete("--confirm") || args.delete("-y")
342
+ keep_cards = args.delete("--keep-cards")
343
+
344
+ unless todolist_id
345
+ puts "Usage: brainiac basecamp scrap <todolist-id> [options]"
346
+ puts ""
347
+ puts "Scrap an epic โ€” full teardown of all related resources."
348
+ puts ""
349
+ puts "Options:"
350
+ puts " --confirm, -y Skip confirmation prompt"
351
+ puts " --dry-run, -n Show what would happen without doing it"
352
+ puts " --keep-cards Don't close the Fizzy cards (keep them for re-planning)"
353
+ puts ""
354
+ puts "This will:"
355
+ puts " 1. Close all Fizzy cards in the epic (unless --keep-cards)"
356
+ puts " 2. Close any open PRs targeting the epic branch"
357
+ puts " 3. Delete the epic branch from all repos"
358
+ puts " 4. Mark all Basecamp todos complete with a ๐Ÿ—‘๏ธ comment"
359
+ puts " 5. Clean up epic memory files"
360
+ puts " 6. Remove the epic from state"
361
+ puts ""
362
+ puts "Active epics:"
363
+ scrappable_statuses = %w[active cancelled]
364
+ load_epics.select { |e| scrappable_statuses.include?(e["status"]) }.each do |e|
365
+ puts " #{e['basecamp_todolist_id']} โ€” #{e['title']} [#{e['status']}]"
366
+ end
367
+ return
368
+ end
369
+
370
+ epics = load_epics
371
+ epic = epics.find { |e| e["basecamp_todolist_id"] == todolist_id.to_s }
372
+
373
+ unless epic
374
+ puts "โŒ No epic found with todolist ID #{todolist_id}"
375
+ puts ""
376
+ puts "Known epics:"
377
+ epics.each do |e|
378
+ puts " #{e['basecamp_todolist_id']} โ€” #{e['title']} [#{e['status']}]"
379
+ end
380
+ return
381
+ end
382
+
383
+ tasks = epic["tasks"] || []
384
+ epic_branches = epic["epic_branches"] || {}
385
+ card_numbers = tasks.filter_map { |t| t["fizzy_card"] }
386
+
387
+ # Preview what we're about to do
388
+ puts dry_run ? "๐Ÿ” DRY RUN โ€” nothing will be changed" : "๐Ÿ—‘๏ธ Scrapping epic: #{epic['title']}"
389
+ puts ""
390
+ puts "Epic: #{epic['title']}"
391
+ puts "Status: #{epic['status']}"
392
+ puts "Todolist: #{epic['basecamp_todolist_id']}"
393
+ puts "Tasks: #{tasks.size}"
394
+ puts ""
395
+
396
+ actions = []
397
+
398
+ # 1. Fizzy cards to close
399
+ unless keep_cards
400
+ if card_numbers.any?
401
+ actions << { type: :close_cards, cards: card_numbers }
402
+ puts " ๐Ÿ“‹ Close #{card_numbers.size} Fizzy card(s): #{card_numbers.map { |c| "##{c}" }.join(', ')}"
403
+ end
404
+ else
405
+ puts " ๐Ÿ“‹ Keep Fizzy cards (--keep-cards)"
406
+ end
407
+
408
+ # 2. Close open PRs and delete epic branches
409
+ if epic_branches.any?
410
+ epic_branches.each do |project_key, branch_name|
411
+ actions << { type: :close_prs_and_delete_branch, project: project_key, branch: branch_name }
412
+ puts " ๐ŸŒฟ Delete branch '#{branch_name}' in #{project_key} (close open PRs first)"
413
+ end
414
+ end
415
+
416
+ # 3. Mark Basecamp todos complete
417
+ todo_ids = tasks.filter_map { |t| t["todo_id"] }
418
+ if todo_ids.any?
419
+ actions << { type: :complete_todos, todo_ids: todo_ids, project_id: epic["basecamp_project_id"] }
420
+ puts " โœ“ Mark #{todo_ids.size} Basecamp todo(s) complete"
421
+ end
422
+
423
+ # 4. Clean up epic memory
424
+ actions << { type: :cleanup_memory, todolist_id: todolist_id }
425
+ puts " ๐Ÿงน Clean up epic memory"
426
+
427
+ # 5. Remove from state
428
+ actions << { type: :remove_from_state, epic_id: epic["id"] }
429
+ puts " ๐Ÿ’พ Remove epic from state file"
430
+
431
+ puts ""
432
+
433
+ if dry_run
434
+ puts "No changes made. Remove --dry-run to execute."
435
+ return
436
+ end
437
+
438
+ # Confirm unless --confirm passed
439
+ unless confirmed
440
+ print "Proceed? [y/N] "
441
+ answer = $stdin.gets&.strip&.downcase
442
+ unless %w[y yes].include?(answer)
443
+ puts "Aborted."
444
+ return
445
+ end
446
+ end
447
+
448
+ puts ""
449
+ puts "Scrapping..."
450
+
451
+ errors = []
452
+
453
+ actions.each do |action|
454
+ case action[:type]
455
+ when :close_cards
456
+ action[:cards].each do |card_number|
457
+ result = scrap_close_fizzy_card(card_number)
458
+ if result
459
+ puts " โœ“ Closed Fizzy card ##{card_number}"
460
+ else
461
+ errors << "Failed to close Fizzy card ##{card_number}"
462
+ puts " โœ— Failed to close Fizzy card ##{card_number}"
463
+ end
464
+ end
465
+
466
+ when :close_prs_and_delete_branch
467
+ project_key = action[:project]
468
+ branch_name = action[:branch]
469
+ repo_path = resolve_repo_path(project_key)
470
+
471
+ if repo_path
472
+ # Close any open PRs targeting or from the epic branch
473
+ closed_prs = scrap_close_epic_prs(repo_path, branch_name)
474
+ closed_prs.each { |pr| puts " โœ“ Closed PR ##{pr} in #{project_key}" }
475
+
476
+ # Delete the remote branch
477
+ if scrap_delete_branch(repo_path, branch_name)
478
+ puts " โœ“ Deleted branch '#{branch_name}' in #{project_key}"
479
+ else
480
+ errors << "Failed to delete branch '#{branch_name}' in #{project_key}"
481
+ puts " โœ— Failed to delete branch '#{branch_name}' in #{project_key}"
482
+ end
483
+ else
484
+ errors << "Could not resolve repo path for project '#{project_key}'"
485
+ puts " โœ— Could not resolve repo path for '#{project_key}'"
486
+ end
487
+
488
+ when :complete_todos
489
+ action[:todo_ids].each do |todo_id|
490
+ scrap_complete_todo(todo_id, action[:project_id], epic["agent"])
491
+ end
492
+ puts " โœ“ Marked #{action[:todo_ids].size} todo(s) complete in Basecamp"
493
+
494
+ when :cleanup_memory
495
+ EpicMemory.cleanup(action[:todolist_id], archive: false)
496
+ puts " โœ“ Cleaned up epic memory"
497
+
498
+ when :remove_from_state
499
+ epics.reject! { |e| e["id"] == action[:epic_id] }
500
+ save_epics(epics)
501
+ puts " โœ“ Removed epic from state"
502
+ end
503
+ end
504
+
505
+ puts ""
506
+ if errors.empty?
507
+ puts "โœ… Epic scrapped successfully."
508
+ else
509
+ puts "โš ๏ธ Epic scrapped with #{errors.size} error(s):"
510
+ errors.each { |e| puts " - #{e}" }
511
+ end
512
+ end
513
+
335
514
  def cmd_link(args)
336
515
  fizzy_card = args.shift
337
516
  basecamp_url = args.shift
@@ -663,6 +842,7 @@ module Brainiac
663
842
  reset task <card-number> [--to <status>] Reset a task to a given status (default: pending)
664
843
  reset epic <todolist-id> [--to <status>] Reset entire epic or all tasks within it
665
844
  reset gates <card-number> Clear gate approvals and re-dispatch gates
845
+ scrap <todolist-id> [--confirm] Scrap an epic: close cards, delete branches, clean up
666
846
  webhook sync Ensure webhooks have all required event types
667
847
 
668
848
  Config file: ~/.brainiac/basecamp.json
@@ -1010,6 +1190,112 @@ module Brainiac
1010
1190
  }))
1011
1191
  end
1012
1192
 
1193
+ # --- Scrap helpers ---
1194
+
1195
+ # Close a Fizzy card via CLI.
1196
+ def scrap_close_fizzy_card(card_number)
1197
+ stdout, stderr, status = Open3.capture3("fizzy", "card", "close", card_number.to_s)
1198
+ status.success?
1199
+ rescue StandardError => e
1200
+ false
1201
+ end
1202
+
1203
+ # Close all open PRs that reference the epic branch (head or base).
1204
+ #
1205
+ # @param repo_path [String] Path to the repo
1206
+ # @param branch_name [String] Epic branch name
1207
+ # @return [Array<Integer>] Closed PR numbers
1208
+ def scrap_close_epic_prs(repo_path, branch_name)
1209
+ closed = []
1210
+
1211
+ # Find PRs where epic branch is the base (task PRs targeting epic branch)
1212
+ stdout, _stderr, status = Open3.capture3(
1213
+ "gh", "pr", "list", "--base", branch_name, "--state", "open",
1214
+ "--json", "number", "--jq", ".[].number",
1215
+ chdir: repo_path
1216
+ )
1217
+ if status.success? && !stdout.strip.empty?
1218
+ stdout.strip.split("\n").each do |pr_num|
1219
+ close_pr(repo_path, pr_num.strip.to_i, "Scrapped as part of epic teardown")
1220
+ closed << pr_num.strip.to_i
1221
+ end
1222
+ end
1223
+
1224
+ # Find PRs where epic branch is the head (final PR to main)
1225
+ stdout, _stderr, status = Open3.capture3(
1226
+ "gh", "pr", "list", "--head", branch_name, "--state", "open",
1227
+ "--json", "number", "--jq", ".[].number",
1228
+ chdir: repo_path
1229
+ )
1230
+ if status.success? && !stdout.strip.empty?
1231
+ stdout.strip.split("\n").each do |pr_num|
1232
+ close_pr(repo_path, pr_num.strip.to_i, "Scrapped as part of epic teardown")
1233
+ closed << pr_num.strip.to_i
1234
+ end
1235
+ end
1236
+
1237
+ closed
1238
+ rescue StandardError
1239
+ closed
1240
+ end
1241
+
1242
+ # Close a single PR with a comment.
1243
+ def close_pr(repo_path, pr_number, comment)
1244
+ Open3.capture3(
1245
+ "gh", "pr", "close", pr_number.to_s, "--comment", "๐Ÿ—‘๏ธ #{comment}",
1246
+ chdir: repo_path
1247
+ )
1248
+ rescue StandardError
1249
+ # Best effort
1250
+ end
1251
+
1252
+ # Delete an epic branch from the remote.
1253
+ def scrap_delete_branch(repo_path, branch_name)
1254
+ # Delete remote branch
1255
+ _stdout, _stderr, status = Open3.capture3(
1256
+ "git", "push", "origin", "--delete", branch_name,
1257
+ chdir: repo_path
1258
+ )
1259
+
1260
+ # Also delete local branch if it exists
1261
+ Open3.capture3("git", "branch", "-D", branch_name, chdir: repo_path)
1262
+
1263
+ status.success?
1264
+ rescue StandardError
1265
+ false
1266
+ end
1267
+
1268
+ # Mark a Basecamp todo complete with a scrap comment.
1269
+ def scrap_complete_todo(todo_id, project_id, agent)
1270
+ # Add a comment first
1271
+ Client.run_safe(
1272
+ "comments", "create", todo_id.to_s,
1273
+ "๐Ÿ—‘๏ธ Scrapped โ€” epic torn down via `brainiac basecamp scrap`",
1274
+ "--in", project_id.to_s, "--json",
1275
+ profile: agent&.downcase
1276
+ )
1277
+
1278
+ # Mark complete
1279
+ Client.run_safe(
1280
+ "todos", "complete", todo_id.to_s,
1281
+ "--in", project_id.to_s, "--json",
1282
+ profile: agent&.downcase
1283
+ )
1284
+ rescue StandardError
1285
+ # Best effort
1286
+ end
1287
+
1288
+ # Resolve a project key to its repo path from projects.json.
1289
+ def resolve_repo_path(project_key)
1290
+ projects_file = File.join(BRAINIAC_DIR, "projects.json")
1291
+ return nil unless File.exist?(projects_file)
1292
+
1293
+ projects = JSON.parse(File.read(projects_file))
1294
+ projects.dig(project_key, "repo_path")
1295
+ rescue JSON::ParserError
1296
+ nil
1297
+ end
1298
+
1013
1299
  # --- Deploy helpers ---
1014
1300
 
1015
1301
  def list_epics_for_deploy
@@ -1150,7 +1436,7 @@ module Brainiac
1150
1436
  end
1151
1437
 
1152
1438
  def self.completions
1153
- %w[setup config status epics deploy link bot projects set reset webhook]
1439
+ %w[setup config status epics deploy link bot projects set reset scrap webhook]
1154
1440
  end
1155
1441
  end
1156
1442
  end
@@ -3,7 +3,7 @@
3
3
  module Brainiac
4
4
  module Plugins
5
5
  module Basecamp
6
- VERSION = "0.0.22"
6
+ VERSION = "0.0.23"
7
7
  end
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brainiac-basecamp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.22
4
+ version: 0.0.23
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Davis