brainiac-basecamp 0.0.21 → 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: 25a59b3d406964ab51bf4c1d34e63f01cf87368afcf79a1ecd8ce95b304b9ec6
4
- data.tar.gz: d7e7f94f7b9b6e6a7c1d4480fcf46c6330a9d3928c4afd723c994703e149c70a
3
+ metadata.gz: afa545126394e953d63caa1cc6f70658f9c0c1333c77abb6a704230b72d6f8cb
4
+ data.tar.gz: 1fe95fc5d1433efa9b0b526686da1349141de314d5de59c25073e0798d05af25
5
5
  SHA512:
6
- metadata.gz: a9529e9564869624ac8712d157fbcafae3693018a6106c7f1ea99797fab5fed3e22d967b4c9f5fb4e8319c722d4d1afebb27732c152e9fca4c7b68626ef36ea6
7
- data.tar.gz: 6cf76c4f8a5b3eaef3f571983f1130a6a79de6a68a22d0a8b08d8fb3d6cb245133e76bd396dcfc80f20ee2be0271757a1e3337ef5797331cc6c984ede6629131
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
@@ -284,6 +286,21 @@ module Brainiac
284
286
  puts " Worktree: #{worktree_path}"
285
287
  puts ""
286
288
 
289
+ # Pull latest changes before deploying
290
+ puts "Pulling latest changes..."
291
+ pull_output, pull_status = Open3.capture2e("git", "pull", "--ff-only", chdir: worktree_path)
292
+ if pull_status.success?
293
+ if pull_output.include?("Already up to date")
294
+ puts "Already up to date."
295
+ else
296
+ puts pull_output.lines.first(5).join
297
+ end
298
+ else
299
+ puts "⚠️ Git pull failed (continuing anyway):"
300
+ puts pull_output.lines.first(3).join
301
+ end
302
+ puts ""
303
+
287
304
  # Run deploy
288
305
  success = run_deploy(worktree_path, deploy_env)
289
306
 
@@ -309,6 +326,189 @@ module Brainiac
309
326
  end
310
327
 
311
328
  save_epics(epics)
329
+
330
+ # Change into the worktree directory
331
+ puts ""
332
+ puts "Entering worktree..."
333
+ Dir.chdir(worktree_path)
334
+ exec ENV.fetch("SHELL", "/bin/bash")
335
+ end
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
312
512
  end
313
513
 
314
514
  def cmd_link(args)
@@ -642,6 +842,7 @@ module Brainiac
642
842
  reset task <card-number> [--to <status>] Reset a task to a given status (default: pending)
643
843
  reset epic <todolist-id> [--to <status>] Reset entire epic or all tasks within it
644
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
645
846
  webhook sync Ensure webhooks have all required event types
646
847
 
647
848
  Config file: ~/.brainiac/basecamp.json
@@ -989,6 +1190,112 @@ module Brainiac
989
1190
  }))
990
1191
  end
991
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
+
992
1299
  # --- Deploy helpers ---
993
1300
 
994
1301
  def list_epics_for_deploy
@@ -1129,7 +1436,7 @@ module Brainiac
1129
1436
  end
1130
1437
 
1131
1438
  def self.completions
1132
- %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]
1133
1440
  end
1134
1441
  end
1135
1442
  end
@@ -3,7 +3,7 @@
3
3
  module Brainiac
4
4
  module Plugins
5
5
  module Basecamp
6
- VERSION = "0.0.21"
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.21
4
+ version: 0.0.23
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Davis