claude-plugin-viban 1.3.11 → 1.3.12

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viban",
3
- "version": "1.3.11",
3
+ "version": "1.3.12",
4
4
  "description": "Terminal Kanban TUI for AI-human collaborative issue tracking",
5
5
  "author": {
6
6
  "name": "happy-nut"
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![npm version](https://badge.fury.io/js/claude-plugin-viban.svg)](https://www.npmjs.com/package/claude-plugin-viban)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
8
 
9
- ![viban screenshot](assets/screenshot.png)
9
+ ![viban](assets/viban.png)
10
10
 
11
11
  ## Why viban?
12
12
 
@@ -17,6 +17,8 @@
17
17
 
18
18
  ## Recommended Workflow
19
19
 
20
+ ![recommended workflow](assets/screenshot.png)
21
+
20
22
  The most effective way to use viban is with **multiple terminal sessions**:
21
23
 
22
24
  ```
Binary file
package/bin/viban CHANGED
@@ -601,9 +601,10 @@ build_column_lines() {
601
601
  # Sort: review by updated_at desc, others by effective order (ordered cards first, then priority)
602
602
  local sort_expr='sort_by(if .order != null then [0, .order] else [1, ({"P0":0,"P1":1,"P2":2,"P3":3}[.priority // "P3"] // 3), .id] end)'
603
603
  [[ "$st" == "review" ]] && sort_expr='sort_by(.updated_at) | reverse'
604
- local issues_data=$(printf '%s' "$json_data" | jq -r --arg s "$st" "
604
+ local done_ids_tui=$(printf '%s' "$json_data" | jq '[.issues[]|select(.status=="done")|.id]')
605
+ local issues_data=$(printf '%s' "$json_data" | jq -r --arg s "$st" --argjson done "$done_ids_tui" "
605
606
  .issues | map(select(.status==\$s)) | $sort_expr |
606
- .[] | \"\\(.id)\t\\(.title)\t\\((.description // \"\") | gsub(\"[\\n\\t\\r]\"; \" \"))\t\\(.priority // \"P3\")\t\\(.type // \"\")\t\\(.external_id // \"\")\"")
607
+ .[] | \"\\(.id)\t\\(.title)\t\\((.description // \"\") | gsub(\"[\\n\\t\\r]\"; \" \"))\t\\(.priority // \"P3\")\t\\(.type // \"\")\t\\(.external_id // \"\")\t\\(if ((.blocked_by // []) | length > 0 and any(. as \$b | \$done | index(\$b) == null)) then \"blocked\" else \"\" end)\"")
607
608
  local count=0
608
609
  # Count total issues (not capped) for overflow indicator
609
610
  if [[ -n "$issues_data" ]]; then
@@ -639,7 +640,8 @@ build_column_lines() {
639
640
  [[ "$st" == "in_progress" ]] && _spinner_w=2
640
641
  local _cc _bc _pfx _did
641
642
 
642
- while IFS=$'\t' read -r _id _title _desc _priority _type _ext_id; do
643
+ local -a _blocked_flags=()
644
+ while IFS=$'\t' read -r _id _title _desc _priority _type _ext_id _blocked; do
643
645
  [[ -z "$_id" ]] && continue
644
646
  (( ${#_ids} >= CACHED_MAX_TASKS )) && break
645
647
  [[ -z "$_priority" || "$_priority" == "null" ]] && _priority="P3"
@@ -648,6 +650,7 @@ build_column_lines() {
648
650
 
649
651
  _ids+=("$_id"); _titles+=("$_title"); _descs+=("$_desc")
650
652
  _priorities+=("$_priority"); _types+=("$_type"); _ext_ids+=("$_ext_id")
653
+ _blocked_flags+=("$_blocked")
651
654
 
652
655
  # Per-card title width limit (use display ID length)
653
656
  _did=$(display_id "$_id" "$_ext_id")
@@ -742,10 +745,11 @@ build_column_lines() {
742
745
  desc_pad=$((card_inner - ${_desc_cws[$_i]}))
743
746
  (( desc_pad < 0 )) && desc_pad=0
744
747
 
745
- # Priority and type tags
748
+ # Priority, type, and blocked tags
746
749
  priority_tag="[$priority]"
747
750
  priority_color="${PRIORITY_COLOR[$priority]:-$A_DIM}"
748
751
  type_tag="" type_color="" tags_w=0
752
+ local blocked_tag="" blocked_color=""
749
753
  if [[ -n "$issue_type" ]]; then
750
754
  type_tag="[${TYPE_LABEL[$issue_type]:-$issue_type}]"
751
755
  type_color="${TYPE_COLOR[$issue_type]:-$A_DIM}"
@@ -753,6 +757,11 @@ build_column_lines() {
753
757
  else
754
758
  tags_w=${#priority_tag}
755
759
  fi
760
+ if [[ "${_blocked_flags[$_i]}" == "blocked" ]]; then
761
+ blocked_tag=" BLOCKED"
762
+ blocked_color="\033[38;2;255;69;58m"
763
+ tags_w=$((tags_w + 8))
764
+ fi
756
765
  tags_pad=$((card_inner - tags_w - 2))
757
766
 
758
767
  border_color="$A_DIM"
@@ -769,9 +778,9 @@ build_column_lines() {
769
778
  printf " ${border_color}│${A_RESET}${text_color}%s${A_RESET}%${title_pad}s${border_color}│${A_RESET} \n" "$title_content" ""
770
779
  printf " ${border_color}│${A_RESET}${desc_color}%s${A_RESET}%${desc_pad}s${border_color}│${A_RESET} \n" "$desc_content" ""
771
780
  if [[ -n "$type_tag" ]]; then
772
- printf " ${border_color}│${A_RESET} ${priority_color}%s${A_RESET} ${type_color}%s${A_RESET}%${tags_pad}s${border_color}│${A_RESET} \n" "$priority_tag" "$type_tag" ""
781
+ printf " ${border_color}│${A_RESET} ${priority_color}%s${A_RESET} ${type_color}%s${A_RESET}${blocked_color}%s${A_RESET}%${tags_pad}s${border_color}│${A_RESET} \n" "$priority_tag" "$type_tag" "$blocked_tag" ""
773
782
  else
774
- printf " ${border_color}│${A_RESET} ${priority_color}%s${A_RESET}%${tags_pad}s${border_color}│${A_RESET} \n" "$priority_tag" ""
783
+ printf " ${border_color}│${A_RESET} ${priority_color}%s${A_RESET}${blocked_color}%s${A_RESET}%${tags_pad}s${border_color}│${A_RESET} \n" "$priority_tag" "$blocked_tag" ""
775
784
  fi
776
785
  printf " ${border_color}╰%s╯${A_RESET} \n" "$border"
777
786
 
@@ -1375,12 +1384,32 @@ move_card_status() {
1375
1384
  # CLI commands
1376
1385
  cmd_list() {
1377
1386
  init_json
1387
+ local _done_ids=$(jq '[.issues[]|select(.status=="done")|.id]' "$VIBAN_JSON")
1388
+ local filter_status=""
1389
+ [[ "$1" == "--status" && -n "$2" ]] && filter_status="$2"
1390
+
1378
1391
  echo ""
1379
- for st in $VIBAN_STATUSES; do
1380
- gum style --foreground "${STATUS_COLOR[$st]}" --bold "● ${STATUS_LABEL[$st]} ($(count_issues_by_status "$st"))"
1381
- get_issues_by_status "$st" | jq -r '.[]|" \(if .external_id then .external_id else "#\(.id)" end) [\(.priority // "P3")]\(if .type then " [\(.type | ascii_upcase)]" else "" end) \(.title)"'
1392
+ if [[ -n "$filter_status" ]]; then
1393
+ local count=$(jq -r --arg s "$filter_status" '[.issues[]|select(.status==$s)]|length' "$VIBAN_JSON")
1394
+ echo "$filter_status ($count)"
1395
+ jq -r --arg s "$filter_status" --argjson done "$_done_ids" '.issues|map(select(.status==$s))|sort_by(.updated_at)|reverse|.[]|" \(if .external_id then .external_id else "#\(.id)" end) [\(.priority // "P3")]\(if .type then " [\(.type | ascii_upcase)]" else "" end)\(if ((.blocked_by // []) | length > 0 and any(. as $b | $done | index($b) == null)) then " [BLOCKED]" else "" end) \(.title)"' "$VIBAN_JSON"
1382
1396
  echo ""
1383
- done
1397
+ else
1398
+ for st in $VIBAN_STATUSES; do
1399
+ gum style --foreground "${STATUS_COLOR[$st]}" --bold "● ${STATUS_LABEL[$st]} ($(count_issues_by_status "$st"))"
1400
+ get_issues_by_status "$st" | jq -r '.[]|" \(if .external_id then .external_id else "#\(.id)" end) [\(.priority // "P3")]\(if .type then " [\(.type | ascii_upcase)]" else "" end) \(.title)"'
1401
+ echo ""
1402
+ done
1403
+ fi
1404
+ }
1405
+
1406
+ cmd_history() {
1407
+ init_json
1408
+ local count=$(jq '[.issues[]|select(.status=="done")]|length' "$VIBAN_JSON")
1409
+ echo ""
1410
+ echo "● Done ($count)"
1411
+ jq -r '.issues|map(select(.status=="done"))|sort_by(.updated_at)|reverse|.[]|" \(if .external_id then .external_id else "#\(.id)" end) [\(.priority // "P3")]\(if .type then " [\(.type | ascii_upcase)]" else "" end) \(.title) (\(.updated_at | split("T")[0]))"' "$VIBAN_JSON"
1412
+ echo ""
1384
1413
  }
1385
1414
 
1386
1415
  cmd_priority() {
@@ -1412,7 +1441,7 @@ cmd_add() {
1412
1441
  [[ -z "$1" ]] && { echo "Usage: viban add \"title\" [\"description\"] [priority] [type] [attachments...]"; exit 1; }
1413
1442
 
1414
1443
  # Support both positional and named args (--title, --description, --priority, --type, --ext-id)
1415
- local title="" desc="" priority="P3" issue_type="" ext_id=""
1444
+ local title="" desc="" priority="P3" issue_type="" ext_id="" parent_id=""
1416
1445
  local -a attachments=()
1417
1446
  local positional=()
1418
1447
 
@@ -1424,6 +1453,7 @@ cmd_add() {
1424
1453
  --priority) priority="$2"; shift 2 ;;
1425
1454
  --type) issue_type="$2"; shift 2 ;;
1426
1455
  --ext-id|--external-id) ext_id="$2"; shift 2 ;;
1456
+ --parent) parent_id="$2"; shift 2 ;;
1427
1457
  --attach|--attachments) shift; while [[ $# -gt 0 && "$1" != --* ]]; do attachments+=("$1"); shift; done ;;
1428
1458
  --*) shift 2 2>/dev/null || shift ;; # skip unknown flags
1429
1459
  *) positional+=("$1"); shift ;;
@@ -1441,6 +1471,32 @@ cmd_add() {
1441
1471
 
1442
1472
  [[ -z "$title" ]] && { echo "Usage: viban add \"title\" [\"description\"] [priority] [type]"; exit 1; }
1443
1473
 
1474
+ # Duplicate detection: warn on similar titles (word-level Jaccard >= 0.5)
1475
+ local duplicates=$(jq -r --arg title "$title" '
1476
+ def words: ascii_downcase | gsub("[^a-z0-9가-힣\\s]"; " ") | split(" ") | map(select(length > 1)) | unique;
1477
+ ($title | words) as $new_words |
1478
+ if ($new_words | length) == 0 then empty else
1479
+ .issues[] | select(.status != "done") |
1480
+ (.title | words) as $existing_words |
1481
+ ([$new_words[] | select(. as $w | $existing_words | index($w) != null)] | length) as $overlap |
1482
+ ([$new_words[], $existing_words[]] | unique | length) as $union |
1483
+ select($union > 0 and ($overlap / $union) >= 0.5) |
1484
+ "\(.id)\t\(.title)"
1485
+ end
1486
+ ' "$VIBAN_JSON")
1487
+ if [[ -n "$duplicates" ]]; then
1488
+ echo "⚠ Potential duplicate(s):"
1489
+ while IFS=$'\t' read -r dup_id dup_title; do
1490
+ echo " #$dup_id $dup_title"
1491
+ done <<< "$duplicates"
1492
+ fi
1493
+
1494
+ # Validate parent exists if specified
1495
+ if [[ -n "$parent_id" ]]; then
1496
+ local parent_exists=$(jq -r --argjson id "$parent_id" '.issues[]|select((.id|tonumber)==$id)|.id//empty' "$VIBAN_JSON")
1497
+ [[ -z "$parent_exists" ]] && { echo "Error: Parent issue #$parent_id not found"; exit 1; }
1498
+ fi
1499
+
1444
1500
  local id=$(get_next_id) now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1445
1501
  # Validate priority
1446
1502
  [[ ! "$priority" =~ ^P[0-3]$ ]] && priority="P3"
@@ -1455,7 +1511,7 @@ cmd_add() {
1455
1511
  # Order is only assigned when manually moved
1456
1512
  local tmpjson=$(mktemp)
1457
1513
  printf '%s' "$desc" > "$tmpjson"
1458
- jq --arg id "$id" --arg title "$title" --rawfile desc "$tmpjson" --arg priority "$priority" --arg issue_type "$issue_type" --arg ext_id "$ext_id" --argjson attachments "$attachments_json" --arg now "$now" '
1514
+ jq --arg id "$id" --arg title "$title" --rawfile desc "$tmpjson" --arg priority "$priority" --arg issue_type "$issue_type" --arg ext_id "$ext_id" --arg parent "$parent_id" --argjson attachments "$attachments_json" --arg now "$now" '
1459
1515
  .next_id = ((.next_id // 0) + 1) |
1460
1516
  .issues += [{
1461
1517
  id:($id|tonumber),
@@ -1465,6 +1521,7 @@ cmd_add() {
1465
1521
  priority:$priority,
1466
1522
  type:(if $issue_type == "" then null else $issue_type end),
1467
1523
  external_id:(if $ext_id == "" then null else $ext_id end),
1524
+ parent_id:(if $parent == "" then null else ($parent|tonumber) end),
1468
1525
  attachments:$attachments,
1469
1526
  assigned_to:null,
1470
1527
  created_at:$now,
@@ -1489,13 +1546,19 @@ cmd_add() {
1489
1546
  [[ -n "$issue_type" ]] && type_info=" [$issue_type]"
1490
1547
  local attach_info=""
1491
1548
  [[ ${#attachments[@]} -gt 0 ]] && attach_info=" +${#attachments[@]} files"
1492
- echo "✓ $(display_id "$id" "$ext_id") added ($priority)$type_info$attach_info"
1549
+ local parent_info=""
1550
+ [[ -n "$parent_id" ]] && parent_info=" (child of #$parent_id)"
1551
+ echo "✓ $(display_id "$id" "$ext_id") added ($priority)$type_info$attach_info$parent_info"
1493
1552
  }
1494
1553
 
1495
1554
  cmd_assign() {
1496
1555
  init_json
1497
1556
  local session="${1:-$(echo $RANDOM | md5 | head -c 8)}"
1498
- local issue=$(jq -r '.issues|map(select(.status=="backlog"))|sort_by(if .order != null then [0, .order] else [1, ({"P0":0,"P1":1,"P2":2,"P3":3}[.priority // "P3"] // 3), .id] end)|first' "$VIBAN_JSON")
1557
+ local done_ids=$(jq '[.issues[]|select(.status=="done")|.id]' "$VIBAN_JSON")
1558
+ local issue=$(jq -r --argjson done "$done_ids" '
1559
+ .issues|map(select(.status=="backlog"))|map(select(
1560
+ (.blocked_by // []) | length == 0 or all(. as $b | $done | index($b) != null)
1561
+ ))|sort_by(if .order != null then [0, .order] else [1, ({"P0":0,"P1":1,"P2":2,"P3":3}[.priority // "P3"] // 3), .id] end)|first' "$VIBAN_JSON")
1499
1562
  [[ "$issue" == "null" || -z "$issue" ]] && { echo "No backlog"; exit 1; }
1500
1563
  local id=$(printf '%s' "$issue" | jq -r '.id') now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1501
1564
  local ext_id=$(printf '%s' "$issue" | jq -r '.external_id // ""')
@@ -1530,10 +1593,10 @@ cmd_review() {
1530
1593
 
1531
1594
  cmd_done() {
1532
1595
  init_json
1533
- [[ -z "$1" ]] && { echo "Usage: viban done <id> [--remove]"; exit 1; }
1596
+ [[ -z "$1" ]] && { echo "Usage: viban done <id> [--purge]"; exit 1; }
1534
1597
  local id="$1"
1535
1598
  local remove=false
1536
- [[ "$2" == "--remove" ]] && remove=true
1599
+ [[ "$2" == "--remove" || "$2" == "--purge" ]] && remove=true
1537
1600
 
1538
1601
  # Cleanup worktree if exists
1539
1602
  local repo_root=$(git rev-parse --show-toplevel 2>/dev/null)
@@ -1571,7 +1634,70 @@ cmd_done() {
1571
1634
  fi
1572
1635
  }
1573
1636
 
1574
- cmd_get() { init_json; jq --argjson id "$1" '.issues[]|select((.id|tonumber)==$id)' "$VIBAN_JSON"; }
1637
+ cmd_move() {
1638
+ init_json
1639
+ [[ -z "$1" || -z "$2" ]] && { echo "Usage: viban move <id> <status>"; exit 1; }
1640
+ local id="$1"
1641
+ local new_status="$2"
1642
+
1643
+ # Validate status
1644
+ local valid_statuses="backlog in_progress review done"
1645
+ if [[ ! " $valid_statuses " == *" $new_status "* ]]; then
1646
+ echo "Error: Invalid status '$new_status'. Valid: backlog, in_progress, review, done"
1647
+ exit 1
1648
+ fi
1649
+
1650
+ # Verify issue exists
1651
+ local cur_status=$(jq -r --argjson id "$id" '.issues[]|select((.id|tonumber)==$id)|.status//empty' "$VIBAN_JSON")
1652
+ [[ -z "$cur_status" ]] && { echo "Error: Issue #$id not found"; exit 1; }
1653
+
1654
+ if [[ "$cur_status" == "$new_status" ]]; then
1655
+ echo "Issue #$id is already in $new_status"
1656
+ return 0
1657
+ fi
1658
+
1659
+ local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1660
+ jq --argjson id "$id" --arg s "$new_status" --arg now "$now" \
1661
+ '(.issues[]|select((.id|tonumber)==$id)) |= . + {status:$s,updated_at:$now}' \
1662
+ "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
1663
+
1664
+ echo "✓ $(display_id "$id" "$(get_ext_id "$id")") → $new_status"
1665
+ }
1666
+
1667
+ cmd_comment() {
1668
+ init_json
1669
+ [[ -z "$1" || -z "$2" ]] && { echo "Usage: viban comment <id> \"message\""; exit 1; }
1670
+ local id="$1"
1671
+ shift
1672
+ local message="$*"
1673
+
1674
+ # Verify issue exists
1675
+ local exists=$(jq -r --argjson id "$id" '.issues[]|select((.id|tonumber)==$id)|.id//empty' "$VIBAN_JSON")
1676
+ [[ -z "$exists" ]] && { echo "Error: Issue #$id not found"; exit 1; }
1677
+
1678
+ local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1679
+ jq --argjson id "$id" --arg msg "$message" --arg now "$now" \
1680
+ '(.issues[]|select((.id|tonumber)==$id)) |= . + {comments:((.comments // []) + [{text:$msg,created_at:$now}]),updated_at:$now}' \
1681
+ "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
1682
+
1683
+ local count=$(jq -r --argjson id "$id" '.issues[]|select((.id|tonumber)==$id)|.comments|length' "$VIBAN_JSON")
1684
+ echo "✓ comment #$count added to $(display_id "$id" "$(get_ext_id "$id")")"
1685
+ }
1686
+
1687
+ cmd_get() {
1688
+ init_json
1689
+ local id="$1"
1690
+ # Output issue JSON
1691
+ jq --argjson id "$id" '.issues[]|select((.id|tonumber)==$id)' "$VIBAN_JSON"
1692
+ # Show sub-tasks if any
1693
+ local subtasks=$(jq -r --argjson id "$id" '[.issues[]|select(.parent_id==$id)]|length' "$VIBAN_JSON")
1694
+ if [[ "$subtasks" -gt 0 ]]; then
1695
+ local done_count=$(jq -r --argjson id "$id" '[.issues[]|select(.parent_id==$id and .status=="done")]|length' "$VIBAN_JSON")
1696
+ echo ""
1697
+ echo "Sub-tasks: $done_count/$subtasks done ($((done_count * 100 / subtasks))%)"
1698
+ jq -r --argjson id "$id" '.issues[]|select(.parent_id==$id)|" #\(.id) [\(.status)] \(.title)"' "$VIBAN_JSON"
1699
+ fi
1700
+ }
1575
1701
 
1576
1702
  cmd_attach() {
1577
1703
  init_json
@@ -1599,6 +1725,97 @@ cmd_attach() {
1599
1725
  echo "✓ $(display_id "$id" "$(get_ext_id "$id")"): ${#files[@]} file(s) attached"
1600
1726
  }
1601
1727
 
1728
+ cmd_link() {
1729
+ init_json
1730
+ [[ -z "$1" || "$2" != "blocks" || -z "$3" ]] && { echo "Usage: viban link <id> blocks <id>"; exit 1; }
1731
+ local blocker_id="$1" blocked_id="$3"
1732
+
1733
+ # Verify both issues exist
1734
+ local b1=$(jq -r --argjson id "$blocker_id" '.issues[]|select((.id|tonumber)==$id)|.id//empty' "$VIBAN_JSON")
1735
+ local b2=$(jq -r --argjson id "$blocked_id" '.issues[]|select((.id|tonumber)==$id)|.id//empty' "$VIBAN_JSON")
1736
+ [[ -z "$b1" ]] && { echo "Error: Issue #$blocker_id not found"; exit 1; }
1737
+ [[ -z "$b2" ]] && { echo "Error: Issue #$blocked_id not found"; exit 1; }
1738
+ [[ "$blocker_id" == "$blocked_id" ]] && { echo "Error: Cannot block self"; exit 1; }
1739
+
1740
+ local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1741
+ jq --argjson bid "$blocked_id" --argjson rid "$blocker_id" --arg now "$now" \
1742
+ '(.issues[]|select((.id|tonumber)==$bid)) |= . + {blocked_by:((.blocked_by // []) | if index($rid) then . else . + [$rid] end),updated_at:$now}' \
1743
+ "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
1744
+
1745
+ echo "✓ #$blocker_id blocks #$blocked_id"
1746
+ }
1747
+
1748
+ cmd_unlink() {
1749
+ init_json
1750
+ [[ -z "$1" || "$2" != "blocks" || -z "$3" ]] && { echo "Usage: viban unlink <id> blocks <id>"; exit 1; }
1751
+ local blocker_id="$1" blocked_id="$3"
1752
+
1753
+ local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1754
+ jq --argjson bid "$blocked_id" --argjson rid "$blocker_id" --arg now "$now" \
1755
+ '(.issues[]|select((.id|tonumber)==$bid)) |= . + {blocked_by:((.blocked_by // []) - [$rid]),updated_at:$now}' \
1756
+ "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
1757
+
1758
+ echo "✓ #$blocker_id no longer blocks #$blocked_id"
1759
+ }
1760
+
1761
+ cmd_stats() {
1762
+ init_json
1763
+ local now_epoch=$(date +%s)
1764
+ local week_ago_epoch=$((now_epoch - 604800))
1765
+
1766
+ # Total by status
1767
+ echo ""
1768
+ echo "Board Summary"
1769
+ echo "─────────────"
1770
+ local backlog_n=$(jq '[.issues[]|select(.status=="backlog")]|length' "$VIBAN_JSON")
1771
+ local wip_n=$(jq '[.issues[]|select(.status=="in_progress")]|length' "$VIBAN_JSON")
1772
+ local review_n=$(jq '[.issues[]|select(.status=="review")]|length' "$VIBAN_JSON")
1773
+ local done_n=$(jq '[.issues[]|select(.status=="done")]|length' "$VIBAN_JSON")
1774
+ local total_n=$(jq '.issues|length' "$VIBAN_JSON")
1775
+ echo " Backlog: $backlog_n In Progress: $wip_n Review: $review_n Done: $done_n Total: $total_n"
1776
+
1777
+ # P0/P1 open count
1778
+ local p0_n=$(jq '[.issues[]|select(.status!="done" and .priority=="P0")]|length' "$VIBAN_JSON")
1779
+ local p1_n=$(jq '[.issues[]|select(.status!="done" and .priority=="P1")]|length' "$VIBAN_JSON")
1780
+ echo " Open P0: $p0_n Open P1: $p1_n"
1781
+
1782
+ # Issues added/closed this week
1783
+ echo ""
1784
+ echo "This Week (last 7 days)"
1785
+ echo "───────────────────────"
1786
+ local week_ago_iso=$(date -u -r $week_ago_epoch +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -d "@$week_ago_epoch" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null)
1787
+ local added_week=$(jq -r --arg since "$week_ago_iso" '[.issues[]|select(.created_at >= $since)]|length' "$VIBAN_JSON")
1788
+ local closed_week=$(jq -r --arg since "$week_ago_iso" '[.issues[]|select(.status=="done" and .updated_at >= $since)]|length' "$VIBAN_JSON")
1789
+ echo " Added: $added_week Completed: $closed_week"
1790
+
1791
+ # Average cycle time (created_at → updated_at for done issues)
1792
+ echo ""
1793
+ echo "Cycle Time"
1794
+ echo "──────────"
1795
+ local avg_hours=$(jq -r '
1796
+ [.issues[]|select(.status=="done")|
1797
+ ((.updated_at|split("T")[0]|split("-")|map(tonumber)) as [$y2,$m2,$d2] |
1798
+ (.created_at|split("T")[0]|split("-")|map(tonumber)) as [$y1,$m1,$d1] |
1799
+ (($y2-$y1)*365 + ($m2-$m1)*30 + ($d2-$d1)) * 24)
1800
+ ] | if length == 0 then null else (add / length | floor) end
1801
+ ' "$VIBAN_JSON")
1802
+ if [[ "$avg_hours" == "null" || -z "$avg_hours" ]]; then
1803
+ echo " Average: no completed issues"
1804
+ elif [[ "$avg_hours" -lt 24 ]]; then
1805
+ echo " Average: <1 day"
1806
+ else
1807
+ echo " Average: $((avg_hours / 24)) days"
1808
+ fi
1809
+
1810
+ # Oldest open issue
1811
+ echo ""
1812
+ echo "Oldest Open Issue"
1813
+ echo "─────────────────"
1814
+ local oldest=$(jq -r '[.issues[]|select(.status!="done")]|sort_by(.created_at)|first|if . then "#\(.id) [\(.priority)] \(.title) (created \(.created_at|split("T")[0]))" else "none" end' "$VIBAN_JSON")
1815
+ echo " $oldest"
1816
+ echo ""
1817
+ }
1818
+
1602
1819
  cmd_migrate() {
1603
1820
  init_json
1604
1821
  echo "Migrating issues..."
@@ -1684,15 +1901,21 @@ main() {
1684
1901
  check_deps
1685
1902
  init_json
1686
1903
  case "${1:-}" in
1687
- list) cmd_list;;
1904
+ list) shift; cmd_list "$@";;
1905
+ history) cmd_history;;
1688
1906
  add) shift; cmd_add "$@";;
1689
1907
  attach) shift; cmd_attach "$@";;
1690
1908
  assign) cmd_assign "$2";;
1691
1909
  review) cmd_review "$2";;
1692
1910
  done) cmd_done "$2" "$3";;
1911
+ move) cmd_move "$2" "$3";;
1693
1912
  get) cmd_get "$2";;
1913
+ comment) shift; cmd_comment "$@";;
1914
+ link) cmd_link "$2" "$3" "$4";;
1915
+ unlink) cmd_unlink "$2" "$3" "$4";;
1694
1916
  edit) [[ -z "$2" ]] && { echo "Usage: viban edit <id>"; exit 1; }; edit_issue "$2";;
1695
1917
  priority) cmd_priority "$2" "$3";;
1918
+ stats) cmd_stats;;
1696
1919
  migrate) cmd_migrate;;
1697
1920
  sync) shift; cmd_sync "$@";;
1698
1921
  --version|-v)
@@ -1717,14 +1940,21 @@ main() {
1717
1940
  echo ""
1718
1941
  echo " viban TUI"
1719
1942
  echo " viban list Show board"
1720
- echo " viban add \"title\" [\"desc\"] [P0-P3] [type] [files...] Add task"
1943
+ echo " viban list --status <s> Filter by status"
1944
+ echo " viban history Show completed issues"
1945
+ echo " viban add \"title\" [\"desc\"] [P0-P3] [type] [--parent <id>] Add task"
1721
1946
  echo " viban attach <id> <file1> [file2...] Attach files to task"
1722
1947
  echo " viban priority <id> <P0-P3> Set priority"
1723
1948
  echo " viban assign Assign first backlog (by priority)"
1724
1949
  echo " viban review → Human Review"
1725
- echo " viban done <id> [--remove] Complete (--remove to delete)"
1950
+ echo " viban move <id> <status> Move to status (backlog,in_progress,review,done)"
1951
+ echo " viban done <id> [--purge] Complete (--purge to permanently delete)"
1952
+ echo " viban comment <id> \"msg\" Add comment to task"
1953
+ echo " viban link <id> blocks <id> Add dependency"
1954
+ echo " viban unlink <id> blocks <id> Remove dependency"
1726
1955
  echo " viban edit <id> Edit task in editor"
1727
1956
  echo " viban get <id> Get task details (JSON)"
1957
+ echo " viban stats Show throughput metrics and statistics"
1728
1958
  echo " viban migrate Migrate: extract type from title"
1729
1959
  echo " viban sync Sync with external issue tracker (GitHub, etc.)"
1730
1960
  echo " viban update Update to latest version (if available)"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin-viban",
3
- "version": "1.3.11",
3
+ "version": "1.3.12",
4
4
  "description": "Terminal Kanban TUI for AI-human collaborative issue tracking",
5
5
  "main": "bin/viban",
6
6
  "bin": {