brainiac-basecamp 0.0.16 → 0.0.17

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: 4d77f4d0787e023edeec79754c9958f4ee09f036250038ef7cc4ae1dc9961a20
4
- data.tar.gz: 74d8aba9539c7526eed62e209b8a84cabe9ecf26482f8873f8c1581a2a70a86a
3
+ metadata.gz: 632f37b6a52ba851b1f16430cc9f2af1a34b0343adcdc486e950d78470d01eb8
4
+ data.tar.gz: 7af23b5ee546eacb7b5d51b0dab4879dc642d7f0e8548ba4d94451e2aadc6518
5
5
  SHA512:
6
- metadata.gz: 198ee07060302e70cf71612ede20b9232efda88f91090273dcee7606264364f381ad7646443a8965319a2437cb5a7f46d6e1e1b92e2683aa87fb176028fd97d2
7
- data.tar.gz: 9194582cc89b6a9cb05721c0369f80762b656776b29e5ad5f97662a6ae460364d486c7d0d15b20bd36e131b9e41c74b9e66d71910155e30b206d0865bd03c967
6
+ metadata.gz: 4d69200fb71f7d04244f0bfcd3d1e8b9ecc97920cb9f1488a96a3fab6da51117af64dd4a47a0ba3a4f8857ce2c36fd5ec23efcf48e35efa5b34f29f86e3af72d
7
+ data.tar.gz: c2253c409cdc64c02ec853dfca0623cc483ddd527b6f8135d9af9eb7ee256745a86a5992f998f6fcc73d10175593150f2288beb6437a4fc25e3c2ce7c278d8e1
@@ -30,6 +30,8 @@ module Brainiac
30
30
  cmd_projects(args)
31
31
  when "set"
32
32
  cmd_set(args)
33
+ when "reset"
34
+ cmd_reset(args)
33
35
  else
34
36
  print_help
35
37
  end
@@ -172,8 +174,23 @@ module Brainiac
172
174
 
173
175
  status_icon = epic["status"] == "active" ? "🚀" : "✅"
174
176
  puts "#{status_icon} #{epic['title']}"
175
- puts " Agent: #{epic['agent']} | Tasks: #{complete}/#{total} complete, #{in_flight} in-flight"
177
+ puts " Todolist: #{epic['basecamp_todolist_id']} | Agent: #{epic['agent']}"
178
+ puts " Tasks: #{complete}/#{total} complete, #{in_flight} in-flight"
176
179
  puts " Started: #{epic['started_at']}"
180
+
181
+ # Show per-task detail if verbose or few tasks
182
+ if args.include?("--verbose") || args.include?("-v")
183
+ tasks.each do |t|
184
+ icon = case t["status"]
185
+ when "complete" then "✅"
186
+ when "in_flight" then "🚀"
187
+ when "in_review" then "👀"
188
+ when "final_decision" then "⚡"
189
+ else "⬜"
190
+ end
191
+ puts " #{icon} ##{t['fizzy_card']} — #{t['title']} [#{t['status']}]"
192
+ end
193
+ end
177
194
  puts ""
178
195
  end
179
196
  end
@@ -317,9 +334,12 @@ module Brainiac
317
334
  projects map <key> <basecamp-id> Map a Brainiac project to Basecamp
318
335
  projects list List project mappings
319
336
  projects unmap <key> Remove a project mapping
320
- set fizzy-account-id <id> Set Fizzy account ID (for card URLs)
337
+ set fizzy-account-id <id> Set Fizzy account ID (for card URLs)
321
338
  set review-gate <mode> Set review gate (on_complete or on_pr_merge)
322
339
  set epic-prefix <prefix> Set epic todolist prefix (default: "Epic:")
340
+ reset task <card-number> [--to <status>] Reset a task to a given status (default: pending)
341
+ reset epic <todolist-id> [--to <status>] Reset entire epic or all tasks within it
342
+ reset gates <card-number> Clear gate approvals and re-dispatch gates
323
343
 
324
344
  Config file: ~/.brainiac/basecamp.json
325
345
  Epics state: ~/.brainiac/basecamp_epics.json
@@ -327,6 +347,13 @@ module Brainiac
327
347
  Review gate modes:
328
348
  on_complete — Advance to next task as soon as agent finishes (default)
329
349
  on_pr_merge — Wait for PR to be merged before advancing (review gate)
350
+
351
+ Task statuses (for reset --to):
352
+ pending — Not yet started, waiting for dependencies
353
+ in_flight — Agent is actively working on it
354
+ in_review — PR open, gates reviewing
355
+ final_decision — Gates passed, awaiting implementation agent merge decision
356
+ complete — Done
330
357
  HELP
331
358
  end
332
359
 
@@ -385,6 +412,273 @@ module Brainiac
385
412
  end
386
413
  end
387
414
 
415
+ def cmd_reset(args)
416
+ subcommand = args.shift
417
+
418
+ case subcommand
419
+ when "task"
420
+ cmd_reset_task(args)
421
+ when "epic"
422
+ cmd_reset_epic(args)
423
+ when "gates"
424
+ cmd_reset_gates(args)
425
+ else
426
+ puts <<~HELP
427
+ Usage: brainiac basecamp reset <subcommand>
428
+
429
+ Subcommands:
430
+ task <card-number> [--to <status>] Reset a task's status (default: pending)
431
+ epic <todolist-id> [--to <status>] Reset entire epic or all its tasks
432
+ gates <card-number> Clear gate approvals and re-dispatch
433
+
434
+ Task statuses:
435
+ pending, in_flight, in_review, final_decision, complete
436
+
437
+ Examples:
438
+ brainiac basecamp reset task 1234 # Reset card #1234 to pending
439
+ brainiac basecamp reset task 1234 --to in_flight # Reset to in_flight
440
+ brainiac basecamp reset gates 1234 # Clear gates, re-dispatch reviewers
441
+ brainiac basecamp reset epic 10233212224 # Reset all tasks to pending
442
+ brainiac basecamp reset epic 10233212224 --to pending # Same as above
443
+ HELP
444
+ end
445
+ end
446
+
447
+ def cmd_reset_task(args)
448
+ card_number = args.shift&.gsub(/^#/, "")
449
+ unless card_number
450
+ puts "Usage: brainiac basecamp reset task <card-number> [--to <status>]"
451
+ return
452
+ end
453
+
454
+ target_status = parse_to_flag(args) || "pending"
455
+ unless valid_task_status?(target_status)
456
+ puts "Error: Invalid status '#{target_status}'"
457
+ puts "Valid statuses: pending, in_flight, in_review, final_decision, complete"
458
+ return
459
+ end
460
+
461
+ epics = load_epics
462
+ epic = epics.find do |e|
463
+ e["status"] == "active" &&
464
+ e["tasks"]&.any? { |t| t["fizzy_card"] == card_number.to_i }
465
+ end
466
+
467
+ unless epic
468
+ puts "Error: Card ##{card_number} not found in any active epic"
469
+ return
470
+ end
471
+
472
+ task = epic["tasks"].find { |t| t["fizzy_card"] == card_number.to_i }
473
+ old_status = task["status"]
474
+
475
+ # Reset the task
476
+ reset_task_to(task, target_status)
477
+
478
+ save_epics(epics)
479
+ puts "✓ Reset card ##{card_number} from '#{old_status}' → '#{target_status}'"
480
+ puts " Epic: #{epic['title']}"
481
+
482
+ return unless target_status == "pending"
483
+
484
+ puts " The task will be picked up on next dispatch cycle (restart brainiac or wait for next event)"
485
+ end
486
+
487
+ def cmd_reset_gates(args)
488
+ card_number = args.shift&.gsub(/^#/, "")
489
+ unless card_number
490
+ puts "Usage: brainiac basecamp reset gates <card-number>"
491
+ return
492
+ end
493
+
494
+ epics = load_epics
495
+ epic = epics.find do |e|
496
+ e["status"] == "active" &&
497
+ e["tasks"]&.any? { |t| t["fizzy_card"] == card_number.to_i }
498
+ end
499
+
500
+ unless epic
501
+ puts "Error: Card ##{card_number} not found in any active epic"
502
+ return
503
+ end
504
+
505
+ task = epic["tasks"].find { |t| t["fizzy_card"] == card_number.to_i }
506
+
507
+ # Clear all gate-related state
508
+ old_approvals = task["gate_approvals"]&.size || 0
509
+ old_changes = task["changes_requested_by"]&.size || 0
510
+
511
+ task["gate_approvals"] = []
512
+ task["changes_requested_by"] = []
513
+ task["gates_dispatched_at"] = nil
514
+ task["awaiting_final_decision"] = nil
515
+ task["changes_debounce_started"] = nil
516
+ task["gate_redispatch_counts"] = {}
517
+ task["last_redispatch_at"] = nil
518
+ task["status"] = "in_review"
519
+ epic["updated_at"] = Time.now.iso8601
520
+
521
+ save_epics(epics)
522
+ puts "✓ Cleared gates for card ##{card_number}"
523
+ puts " Removed #{old_approvals} approval(s), #{old_changes} changes_requested"
524
+ puts " Status set to 'in_review' — gates will be re-dispatched on next health check"
525
+ puts ""
526
+ puts " To force immediate re-dispatch, restart brainiac:"
527
+ puts " brainiac restart"
528
+ end
529
+
530
+ def cmd_reset_epic(args)
531
+ todolist_id = args.shift
532
+ unless todolist_id
533
+ puts "Usage: brainiac basecamp reset epic <todolist-id> [--to <status>]"
534
+ puts ""
535
+ puts "Options:"
536
+ puts " --to <status> Reset all tasks to this status (default: pending)"
537
+ puts " --reactivate Reset a completed epic back to active"
538
+ puts ""
539
+ puts "Find your todolist ID with: brainiac basecamp epics --all"
540
+ return
541
+ end
542
+
543
+ target_status = parse_to_flag(args) || "pending"
544
+ reactivate = args.include?("--reactivate")
545
+
546
+ unless valid_task_status?(target_status)
547
+ puts "Error: Invalid status '#{target_status}'"
548
+ puts "Valid statuses: pending, in_flight, in_review, final_decision, complete"
549
+ return
550
+ end
551
+
552
+ epics = load_epics
553
+ epic = epics.find { |e| e["basecamp_todolist_id"] == todolist_id.to_s }
554
+
555
+ unless epic
556
+ puts "Error: No epic found with todolist ID #{todolist_id}"
557
+ puts ""
558
+ puts "Active epics:"
559
+ epics.select { |e| e["status"] == "active" }.each do |e|
560
+ puts " #{e['basecamp_todolist_id']} — #{e['title']}"
561
+ end
562
+ return
563
+ end
564
+
565
+ if epic["status"] != "active" && !reactivate
566
+ puts "Error: Epic '#{epic['title']}' is #{epic['status']} (not active)"
567
+ puts " Use --reactivate to set it back to active"
568
+ return
569
+ end
570
+
571
+ # Reactivate if needed
572
+ if epic["status"] != "active" && reactivate
573
+ epic["status"] = "active"
574
+ epic["completed_at"] = nil
575
+ puts "✓ Reactivated epic '#{epic['title']}'"
576
+ end
577
+
578
+ # Reset all non-complete tasks (unless resetting to pending, in which case reset all)
579
+ tasks_to_reset = if target_status == "pending"
580
+ epic["tasks"]
581
+ else
582
+ epic["tasks"].reject { |t| t["status"] == "complete" }
583
+ end
584
+
585
+ tasks_to_reset.each { |t| reset_task_to(t, target_status) }
586
+ epic["updated_at"] = Time.now.iso8601
587
+
588
+ save_epics(epics)
589
+ puts "✓ Reset #{tasks_to_reset.size} task(s) → '#{target_status}' in epic '#{epic['title']}'"
590
+ puts ""
591
+ epic["tasks"].each do |t|
592
+ icon = case t["status"]
593
+ when "complete" then "✅"
594
+ when "in_flight" then "🚀"
595
+ when "in_review" then "👀"
596
+ when "final_decision" then "⚡"
597
+ else "⬜"
598
+ end
599
+ puts " #{icon} ##{t['fizzy_card']} — #{t['title']} [#{t['status']}]"
600
+ end
601
+ end
602
+
603
+ # --- Reset helpers ---
604
+
605
+ def valid_task_status?(status)
606
+ %w[pending in_flight in_review final_decision complete].include?(status)
607
+ end
608
+
609
+ def parse_to_flag(args)
610
+ idx = args.index("--to")
611
+ return nil unless idx
612
+
613
+ args.delete_at(idx) # remove --to
614
+ args.delete_at(idx) # remove the value (now at same index)
615
+ end
616
+
617
+ def reset_task_to(task, status)
618
+ task["status"] = status
619
+
620
+ case status
621
+ when "pending"
622
+ # Clear all progress state
623
+ task["dispatched_at"] = nil
624
+ task["completed_at"] = nil
625
+ task["pr_number"] = nil
626
+ task["pr_repo"] = nil
627
+ task["gate_approvals"] = []
628
+ task["changes_requested_by"] = []
629
+ task["gates_dispatched_at"] = nil
630
+ task["awaiting_final_decision"] = nil
631
+ task["changes_debounce_started"] = nil
632
+ task["gate_redispatch_counts"] = {}
633
+ task["last_redispatch_at"] = nil
634
+ task["last_reviewed_sha"] = nil
635
+ when "in_flight"
636
+ # Keep PR info but clear gate state
637
+ task["completed_at"] = nil
638
+ task["gate_approvals"] = []
639
+ task["changes_requested_by"] = []
640
+ task["gates_dispatched_at"] = nil
641
+ task["awaiting_final_decision"] = nil
642
+ task["changes_debounce_started"] = nil
643
+ task["gate_redispatch_counts"] = {}
644
+ task["last_redispatch_at"] = nil
645
+ task["dispatched_at"] ||= Time.now.iso8601
646
+ when "in_review"
647
+ # Clear gate approvals but keep PR info
648
+ task["completed_at"] = nil
649
+ task["gate_approvals"] = []
650
+ task["changes_requested_by"] = []
651
+ task["gates_dispatched_at"] = nil
652
+ task["awaiting_final_decision"] = nil
653
+ task["changes_debounce_started"] = nil
654
+ task["gate_redispatch_counts"] = {}
655
+ task["last_redispatch_at"] = nil
656
+ when "final_decision"
657
+ task["completed_at"] = nil
658
+ task["awaiting_final_decision"] = true
659
+ when "complete"
660
+ task["completed_at"] ||= Time.now.iso8601
661
+ end
662
+ end
663
+
664
+ def load_epics
665
+ epics_file = File.join(BRAINIAC_DIR, "basecamp_epics.json")
666
+ return [] unless File.exist?(epics_file)
667
+
668
+ data = JSON.parse(File.read(epics_file))
669
+ data["epics"] || []
670
+ rescue JSON::ParserError
671
+ []
672
+ end
673
+
674
+ def save_epics(epics)
675
+ epics_file = File.join(BRAINIAC_DIR, "basecamp_epics.json")
676
+ File.write(epics_file, JSON.pretty_generate({
677
+ "epics" => epics,
678
+ "updated_at" => Time.now.iso8601
679
+ }))
680
+ end
681
+
388
682
  def load_config
389
683
  if File.exist?(CONFIG_FILE)
390
684
  JSON.parse(File.read(CONFIG_FILE))
@@ -404,7 +698,7 @@ module Brainiac
404
698
  end
405
699
 
406
700
  def self.completions
407
- %w[setup config status epics link bot projects set]
701
+ %w[setup config status epics link bot projects set reset]
408
702
  end
409
703
  end
410
704
  end
@@ -3,7 +3,7 @@
3
3
  module Brainiac
4
4
  module Plugins
5
5
  module Basecamp
6
- VERSION = "0.0.16"
6
+ VERSION = "0.0.17"
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.16
4
+ version: 0.0.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Davis