Просмотр исходного кода

refactor(skills/fleet-ops): unbroken tree connectors, icon-after-glyph

The previous grouped-tree layout put the state icon at column 0, where
the tree's vertical line should run. The eye lost the scaffold every
two rows. Re-root the tree at the header rule: groups become first-class
branches with ├─/└─ at column 0, icons sit AFTER the connector, and
children continue under │ without interruption.

Before:
  ⏳ RUNNING   (1)
    └─ alpha                            1s
  ✅ READY     (1)               <- visual scaffold restarts every group
    └─ beta                             0s

After:
  ├─ ⏳ RUNNING   (1)
  │  └─ alpha                           1s
  └─ ✅ READY     (1)
     └─ beta                            0s

term.sh: term_tree_branch / term_tree_last replaced with composable
term_tree_node (prefix, connector, label, meta) + term_tree_indent
that returns the 3-col continuation segment ("│  " or "   ") for an
ancestor. This composes to N levels — DESIGN.md now shows both a
2-level (groups → leaves) and 3-level (groups → branches → leaves)
example.

Tree-control rule documented in DESIGN.md: connectors are the scaffold,
nothing breaks the vertical, icons go on the label side.

fleet-ops e2e: 20/20 still passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0xDarkMatter 2 месяцев назад
Родитель
Сommit
69246e6fb1
3 измененных файлов с 122 добавлено и 71 удалено
  1. 63 37
      docs/DESIGN.md
  2. 29 24
      skills/_lib/term.sh
  3. 30 10
      skills/fleet-ops/scripts/fleet.sh

+ 63 - 37
docs/DESIGN.md

@@ -74,51 +74,79 @@ terminal width so the header reads as the section's banner.
 
 
 ### Grouped tree (default body)
 ### Grouped tree (default body)
 
 
-State icon + group label + count, then children under tree connectors.
-Empty groups are omitted — never render a group with `(0)`.
+**Tree-control rule:** the connectors `├─ │ └─` are the scaffold. They
+run in their own column from the top of the body to the last leaf, and
+**nothing breaks the vertical**. Icons and labels live to the right of
+the connector, not between it and its parent's `│`. If you find yourself
+wanting to put an icon where the `│` should continue, you don't have a
+tree — you have a list with decorations.
 
 
-```
-── fleet ─────────────────────────────────────────────────────  4 lanes · 3 active
-
-  ⏳ RUNNING   (2)
-    ├─ feat/auth-rewrite             12m
-    └─ spike/wasm-eval               34m
+#### 2-level: groups → leaves
 
 
-  ✅ READY     (1)
-    └─ fix/cache-bust                2m
+The default for state-bucketed views (lanes, PR checks, jobs).
 
 
-  🚀 LANDED    (1)
-    └─ chore/bump-deps               1h
+```
+── fleet ─────────────────────────────────────────────────────  4 lanes · 3 active
+├─ ⏳ RUNNING   (2)
+│  ├─ feat/auth-rewrite             12m
+│  └─ spike/wasm-eval               34m
+├─ ✅ READY     (1)
+│  └─ fix/cache-bust                2m
+└─ 🚀 LANDED    (1)
+   └─ chore/bump-deps               1h
 ```
 ```
 
 
+Notice: the `│` running down column 0 is unbroken from the first group
+to the last child of the second-last group. The `└─` on `LANDED`
+terminates the vertical cleanly. Empty groups are omitted — never
+render `(0)`.
+
 Why grouped instead of flat: when ten lanes are in flight, scanning a
 Why grouped instead of flat: when ten lanes are in flight, scanning a
 flat table for "what's actually ready to land?" forces your eyes to do
 flat table for "what's actually ready to land?" forces your eyes to do
 the filtering. Grouping does it for you, and the count tells you at a
 the filtering. Grouping does it for you, and the count tells you at a
 glance whether the answer is none, one, or twelve.
 glance whether the answer is none, one, or twelve.
 
 
+#### 3-level: groups → branches → leaves
+
+For hierarchies with intermediate structure — repos with branches with
+files, projects with packages with tests, lanes with commits with
+patches. Same rule: connectors don't break.
+
+```
+── repo ──────────────────────────────────────────────────────  X:/Forge/claude-mods · 2 worktrees
+├─ 📦 main
+│  ├─ src/
+│  │  ├─ index.ts                   modified
+│  │  └─ utils/
+│  │     ├─ format.ts               modified
+│  │     └─ parse.ts                added
+│  └─ README.md                     clean
+└─ 🌿 feat/auth-rewrite
+   └─ src/
+      ├─ auth.ts                    new
+      └─ middleware/
+         └─ session.ts              modified
+```
+
+Each level adds a 3-column indent: `│  ` while the ancestor still has
+siblings to render, `   ` once the ancestor is on its last sibling. The
+helpers in `term.sh` (`term_tree_node`, `term_tree_indent`,
+`term_tree_connector`) compose this prefix so you don't have to count
+spaces.
+
 ### Flat status table (escape hatch)
 ### Flat status table (escape hatch)
 
 
 When the data is genuinely flat — `git status`-style fields, a single
 When the data is genuinely flat — `git status`-style fields, a single
 PR's checks — drop the tree. Glyph-first, no nested tables.
 PR's checks — drop the tree. Glyph-first, no nested tables.
 
 
 ```
 ```
+── push-gate ─────────────────────────────────────────────────  refusing
   ✅  secret scan        clean
   ✅  secret scan        clean
   ✅  forbidden files    none
   ✅  forbidden files    none
   ❌  divergence         3 ahead, 1 behind
   ❌  divergence         3 ahead, 1 behind
 ```
 ```
 
 
-### Plain tree (filesystem-style, no state grouping)
-
-For hierarchies that aren't keyed on state — a directory tree, an
-include graph. Use sparingly; the grouped-tree above is preferred for
-state.
-
-```
-repo/
-├─ main                           clean
-├─ feat/auth-rewrite              ahead 3, dirty
-└─ fix/cache-bust                 behind 1
-```
+The header rule still anchors the section; only the body is flat.
 
 
 ### Section divider
 ### Section divider
 
 
@@ -166,25 +194,23 @@ Disabled when stdout isn't a TTY, or `NO_COLOR` is set. Forced on with
 ────────────────────────────────────────────────────────────────
 ────────────────────────────────────────────────────────────────
 ```
 ```
 
 
-### After — rule on top, grouped tree as default
+### After — rule on top, grouped tree with unbroken connectors
 
 
 ```
 ```
 ── fleet ─────────────────────────────────────────────────────  3 lanes · 2 active
 ── fleet ─────────────────────────────────────────────────────  3 lanes · 2 active
-
-  ⏳ RUNNING   (1)
-    └─ feat/auth-rewrite             12m
-
-  ✅ READY     (1)
-    └─ fix/cache-bust                2m
-
-  🚀 LANDED    (1)
-    └─ chore/bump-deps               1h
+├─ ⏳ RUNNING   (1)
+│  └─ feat/auth-rewrite             12m
+├─ ✅ READY     (1)
+│  └─ fix/cache-bust                2m
+└─ 🚀 LANDED    (1)
+   └─ chore/bump-deps               1h
 ```
 ```
 
 
 The header rule survives — it's the strongest visual cue that you're
 The header rule survives — it's the strongest visual cue that you're
-inside a skill's output. The flat table gives way to grouped state, so
-"what's ready" and "what's still running" answer themselves before you
-read a single branch name.
+inside a skill's output. The flat table gives way to a tree where
+groups are first-class branches: the `│` runs uninterrupted from the
+first group to the last leaf above the terminating `└─`, and icons
+sit *after* the connector instead of breaking it.
 
 
 ### `git-ops/status` reformatted in the same language
 ### `git-ops/status` reformatted in the same language
 
 

+ 29 - 24
skills/_lib/term.sh

@@ -169,39 +169,44 @@ TERM_TREE_BRANCH=""    # ├─  /  +-
 TERM_TREE_LAST=""      # └─  /  `-
 TERM_TREE_LAST=""      # └─  /  `-
 TERM_TREE_VERT=""      # │   /  |
 TERM_TREE_VERT=""      # │   /  |
 
 
-# term_group_header <icon> <LABEL> <count>  — "  ⏳ RUNNING   (3)"
-# Use as the parent line above term_tree_branch / term_tree_last children.
-term_group_header() {
-  local icon=$1 label=$2 count=$3
-  printf '  %s %-9s %s\n' "$icon" "$label" "$(term_color dim "($count)")"
+# Tree-control philosophy: the connectors (├─ │ └─) are the scaffold.
+# Icons and labels sit AFTER the connector, never between it and the
+# vertical line of its parent. To render a tree:
+#
+#   term_tree_node "" "$(term_tree_connector $i $last)" "⏳ RUNNING (3)"
+#   term_tree_node "│  " "$(term_tree_connector $j $last)" "feat/auth" "12m"
+#
+# `prefix` is what comes before this row's connector — built by walking
+# the ancestor chain and appending TERM_TREE_VERT+"  " for non-last
+# ancestors, or three spaces for last ancestors.
+
+# term_tree_connector <idx> <last_idx>  — echo branch or last glyph.
+term_tree_connector() {
+  if [[ "$1" -eq "$2" ]]; then printf '%s' "$TERM_TREE_LAST"
+  else printf '%s' "$TERM_TREE_BRANCH"; fi
 }
 }
 
 
-# term_tree_branch <label> [meta]  — "    ├─ label                meta"
-term_tree_branch() {
-  local label=$1 meta=${2:-}
-  if [[ -n "$meta" ]]; then
-    printf '    %s %-32s %s\n' "$TERM_TREE_BRANCH" "$label" "$(term_color dim "$meta")"
-  else
-    printf '    %s %s\n' "$TERM_TREE_BRANCH" "$label"
-  fi
+# term_tree_indent <is_last>  — echo the 3-col continuation segment for
+# this ancestor: "│  " when more siblings follow, "   " when last.
+term_tree_indent() {
+  if [[ "$1" -eq 1 ]]; then printf '   '
+  else printf '%s  ' "$TERM_TREE_VERT"; fi
 }
 }
 
 
-# term_tree_last <label> [meta]  — "    └─ label                meta"
-term_tree_last() {
-  local label=$1 meta=${2:-}
+# term_tree_node <prefix> <connector> <label> [meta]
+#   prefix:    ancestor-chain string (built from term_tree_indent calls)
+#   connector: result of term_tree_connector for THIS row
+#   label:     visible text (may include leading icon — won't break the line)
+#   meta:      optional dim trailing text
+term_tree_node() {
+  local prefix=$1 conn=$2 label=$3 meta=${4:-}
   if [[ -n "$meta" ]]; then
   if [[ -n "$meta" ]]; then
-    printf '    %s %-32s %s\n' "$TERM_TREE_LAST" "$label" "$(term_color dim "$meta")"
+    printf '%s%s %-32s %s\n' "$prefix" "$conn" "$label" "$(term_color dim "$meta")"
   else
   else
-    printf '    %s %s\n' "$TERM_TREE_LAST" "$label"
+    printf '%s%s %s\n' "$prefix" "$conn" "$label"
   fi
   fi
 }
 }
 
 
-# term_tree_connector <idx> <last_idx>  — echo branch or last, for loops.
-term_tree_connector() {
-  if [[ "$1" -eq "$2" ]]; then printf '%s' "$TERM_TREE_LAST"
-  else printf '%s' "$TERM_TREE_BRANCH"; fi
-}
-
 # term_table_row <c1> <c2> <c3>  — fixed-width 3-col row.
 # term_table_row <c1> <c2> <c3>  — fixed-width 3-col row.
 term_table_row() {
 term_table_row() {
   printf '  %-2s  %-32s %-10s %s\n' "${1:-}" "${2:-}" "${3:-}" "${4:-}"
   printf '  %-2s  %-32s %-10s %s\n' "${1:-}" "${2:-}" "${3:-}" "${4:-}"

+ 30 - 10
skills/fleet-ops/scripts/fleet.sh

@@ -174,30 +174,50 @@ cmd_fleet() {
     return
     return
   fi
   fi
 
 
+  # Build list of non-empty group indices so we know which is "last" at
+  # the top level — the tree's vertical needs to terminate cleanly.
+  local active_groups=()
   local i
   local i
   for i in 0 1 2 3 4; do
   for i in 0 1 2 3 4; do
+    [[ ${state_counts[$i]} -gt 0 ]] && active_groups+=("$i")
+  done
+
+  local g_idx=0
+  local g_last=$(( ${#active_groups[@]} - 1 ))
+  for i in "${active_groups[@]}"; do
     local n=${state_counts[$i]}
     local n=${state_counts[$i]}
-    [[ $n -eq 0 ]] && continue
     local state=${order[$i]}
     local state=${order[$i]}
     local icon
     local icon
     icon=$(term_state_icon "$state")
     icon=$(term_state_icon "$state")
     [[ -z "$icon" || "$icon" == "?" ]] && icon="$ICON_UNKNOWN"
     [[ -z "$icon" || "$icon" == "?" ]] && icon="$ICON_UNKNOWN"
-    echo ""
-    term_group_header "$icon" "$state" "$n"
+
+    # Group line — connector at column 0, then icon + label live to its right.
+    local g_conn
+    g_conn=$(term_tree_connector "$g_idx" "$g_last")
+    local group_label
+    group_label="$icon $state"
+    term_tree_node "" "$g_conn" "$group_label" "($n)"
+
+    # Children indent = continuation of this group's connector.
+    local child_prefix
+    if [[ $g_idx -eq $g_last ]]; then
+      child_prefix=$(term_tree_indent 1)
+    else
+      child_prefix=$(term_tree_indent 0)
+    fi
 
 
     local lines="${state_buckets[$i]}"
     local lines="${state_buckets[$i]}"
-    local idx=0 last_idx=$((n - 1))
+    local c_idx=0 c_last=$((n - 1))
     local branch age meta
     local branch age meta
     while IFS='|' read -r branch age meta; do
     while IFS='|' read -r branch age meta; do
       [[ -z "$branch" ]] && continue
       [[ -z "$branch" ]] && continue
-      local connector
-      connector=$(term_tree_connector "$idx" "$last_idx")
-      local label="$branch"
-      local meta_str="$age"
+      local c_conn meta_str="$age"
+      c_conn=$(term_tree_connector "$c_idx" "$c_last")
       [[ -n "$meta" ]] && meta_str="$age  $meta"
       [[ -n "$meta" ]] && meta_str="$age  $meta"
-      printf '    %s %-32s %s\n' "$connector" "$label" "$(term_color dim "$meta_str")"
-      idx=$((idx+1))
+      term_tree_node "$child_prefix" "$c_conn" "$branch" "$meta_str"
+      c_idx=$((c_idx+1))
     done <<< "$lines"
     done <<< "$lines"
+    g_idx=$((g_idx+1))
   done
   done
   echo ""
   echo ""
 }
 }