forge-workflow 0.0.3 → 0.0.5

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.
Files changed (209) hide show
  1. package/.claude/commands/dev.md +340 -314
  2. package/.claude/commands/plan.md +521 -478
  3. package/.claude/commands/premerge.md +176 -179
  4. package/.claude/commands/research.md +42 -42
  5. package/.claude/commands/review.md +442 -442
  6. package/.claude/commands/rollback.md +721 -721
  7. package/.claude/commands/ship.md +164 -134
  8. package/.claude/commands/sonarcloud.md +152 -152
  9. package/.claude/commands/status.md +48 -77
  10. package/.claude/commands/validate.md +282 -237
  11. package/.claude/commands/verify.md +221 -221
  12. package/.claude/rules/greptile-review-process.md +285 -285
  13. package/.claude/rules/workflow.md +105 -105
  14. package/.claude/scripts/greptile-resolve.sh +526 -526
  15. package/.claude/scripts/load-env.sh +32 -32
  16. package/.cline/workflows/dev.md +337 -311
  17. package/.cline/workflows/plan.md +518 -475
  18. package/.cline/workflows/premerge.md +173 -176
  19. package/.cline/workflows/research.md +39 -39
  20. package/.cline/workflows/review.md +439 -439
  21. package/.cline/workflows/rollback.md +718 -718
  22. package/.cline/workflows/ship.md +161 -131
  23. package/.cline/workflows/sonarcloud.md +146 -146
  24. package/.cline/workflows/status.md +45 -74
  25. package/.cline/workflows/validate.md +279 -234
  26. package/.cline/workflows/verify.md +218 -218
  27. package/.codex/config.toml +11 -11
  28. package/.codex/skills/dev/SKILL.md +340 -314
  29. package/.codex/skills/plan/SKILL.md +521 -478
  30. package/.codex/skills/premerge/SKILL.md +176 -179
  31. package/.codex/skills/research/SKILL.md +42 -42
  32. package/.codex/skills/review/SKILL.md +442 -442
  33. package/.codex/skills/rollback/SKILL.md +721 -721
  34. package/.codex/skills/ship/SKILL.md +164 -134
  35. package/.codex/skills/sonarcloud/SKILL.md +149 -149
  36. package/.codex/skills/status/SKILL.md +48 -77
  37. package/.codex/skills/validate/SKILL.md +282 -237
  38. package/.codex/skills/verify/SKILL.md +221 -221
  39. package/.cursor/commands/dev.md +337 -311
  40. package/.cursor/commands/plan.md +518 -475
  41. package/.cursor/commands/premerge.md +173 -176
  42. package/.cursor/commands/research.md +39 -39
  43. package/.cursor/commands/review.md +439 -439
  44. package/.cursor/commands/rollback.md +718 -718
  45. package/.cursor/commands/ship.md +161 -131
  46. package/.cursor/commands/sonarcloud.md +146 -146
  47. package/.cursor/commands/status.md +45 -74
  48. package/.cursor/commands/validate.md +279 -234
  49. package/.cursor/commands/verify.md +218 -218
  50. package/.cursor/rules/permissions-guidance.mdc +37 -37
  51. package/.forge/hooks/check-tdd.js +240 -240
  52. package/.github/PLUGIN_TEMPLATE.json +32 -32
  53. package/.github/prompts/dev.prompt.md +342 -316
  54. package/.github/prompts/plan.prompt.md +523 -480
  55. package/.github/prompts/premerge.prompt.md +178 -181
  56. package/.github/prompts/research.prompt.md +44 -44
  57. package/.github/prompts/review.prompt.md +444 -444
  58. package/.github/prompts/rollback.prompt.md +723 -723
  59. package/.github/prompts/ship.prompt.md +166 -136
  60. package/.github/prompts/sonarcloud.prompt.md +151 -151
  61. package/.github/prompts/status.prompt.md +50 -79
  62. package/.github/prompts/validate.prompt.md +284 -239
  63. package/.github/prompts/verify.prompt.md +223 -223
  64. package/.github/workflows/beads-to-github.yml +56 -0
  65. package/.github/workflows/github-to-beads.yml +97 -0
  66. package/.kilocode/workflows/dev.md +341 -315
  67. package/.kilocode/workflows/plan.md +522 -479
  68. package/.kilocode/workflows/premerge.md +177 -180
  69. package/.kilocode/workflows/research.md +43 -43
  70. package/.kilocode/workflows/review.md +443 -443
  71. package/.kilocode/workflows/rollback.md +722 -722
  72. package/.kilocode/workflows/ship.md +165 -135
  73. package/.kilocode/workflows/sonarcloud.md +150 -150
  74. package/.kilocode/workflows/status.md +49 -78
  75. package/.kilocode/workflows/validate.md +283 -238
  76. package/.kilocode/workflows/verify.md +222 -222
  77. package/.mcp.json.example +12 -12
  78. package/.opencode/commands/dev.md +340 -314
  79. package/.opencode/commands/plan.md +521 -478
  80. package/.opencode/commands/premerge.md +176 -179
  81. package/.opencode/commands/research.md +42 -42
  82. package/.opencode/commands/review.md +442 -442
  83. package/.opencode/commands/rollback.md +721 -721
  84. package/.opencode/commands/ship.md +164 -134
  85. package/.opencode/commands/sonarcloud.md +149 -149
  86. package/.opencode/commands/status.md +48 -77
  87. package/.opencode/commands/validate.md +282 -237
  88. package/.opencode/commands/verify.md +221 -221
  89. package/.roo/commands/dev.md +341 -315
  90. package/.roo/commands/plan.md +522 -479
  91. package/.roo/commands/premerge.md +177 -180
  92. package/.roo/commands/research.md +43 -43
  93. package/.roo/commands/review.md +443 -443
  94. package/.roo/commands/rollback.md +722 -722
  95. package/.roo/commands/ship.md +165 -135
  96. package/.roo/commands/sonarcloud.md +150 -150
  97. package/.roo/commands/status.md +49 -78
  98. package/.roo/commands/validate.md +283 -238
  99. package/.roo/commands/verify.md +222 -222
  100. package/AGENTS.md +175 -169
  101. package/CLAUDE.md +100 -99
  102. package/LICENSE +21 -21
  103. package/README.md +429 -414
  104. package/bin/forge-cmd.js +313 -313
  105. package/bin/{forge-validate.js → forge-preflight.js} +309 -303
  106. package/bin/forge.js +4596 -4232
  107. package/docs/AGENT_INSTALL_PROMPT.md +342 -342
  108. package/docs/BEADS_GITHUB_SYNC.md +251 -0
  109. package/docs/ENHANCED_ONBOARDING.md +602 -602
  110. package/docs/EXAMPLES.md +482 -482
  111. package/docs/GREPTILE_SETUP.md +400 -400
  112. package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
  113. package/docs/ROADMAP.md +359 -359
  114. package/docs/SETUP.md +663 -632
  115. package/docs/TOOLCHAIN.md +630 -630
  116. package/docs/VALIDATION.md +363 -363
  117. package/install.sh +40 -1058
  118. package/lefthook.yml +39 -39
  119. package/lib/agents/README.md +198 -198
  120. package/lib/agents/claude.plugin.json +28 -28
  121. package/lib/agents/cline.plugin.json +22 -22
  122. package/lib/agents/codex.plugin.json +19 -19
  123. package/lib/agents/copilot.plugin.json +24 -24
  124. package/lib/agents/cursor.plugin.json +25 -25
  125. package/lib/agents/kilocode.plugin.json +22 -22
  126. package/lib/agents/opencode.plugin.json +20 -20
  127. package/lib/agents/roo.plugin.json +23 -23
  128. package/lib/agents-config.js +2112 -2112
  129. package/lib/beads-health-check.js +143 -0
  130. package/lib/beads-setup.js +341 -0
  131. package/lib/beads-sync-scaffold.js +260 -0
  132. package/lib/commands/dev.js +513 -513
  133. package/lib/commands/plan.js +692 -692
  134. package/lib/commands/recommend.js +119 -119
  135. package/lib/commands/ship.js +377 -377
  136. package/lib/commands/status.js +378 -378
  137. package/lib/commands/validate.js +602 -602
  138. package/lib/context-merge.js +359 -359
  139. package/lib/dep-guard/analyzer.js +294 -294
  140. package/lib/dep-guard/behavior-detector.js +98 -98
  141. package/lib/dep-guard/contract-detector.js +162 -162
  142. package/lib/dep-guard/import-detector.js +498 -498
  143. package/lib/dep-guard/path-utils.js +13 -13
  144. package/lib/dep-guard/rubric.js +120 -120
  145. package/lib/dep-guard/task-parser.js +318 -318
  146. package/lib/detect-agent.js +191 -0
  147. package/lib/detect-worktree.js +47 -0
  148. package/lib/file-hash.js +26 -0
  149. package/lib/husky-migration.js +450 -0
  150. package/lib/lefthook-check.js +65 -0
  151. package/lib/pat-setup.js +207 -0
  152. package/lib/plugin-catalog.js +350 -350
  153. package/lib/plugin-manager.js +166 -166
  154. package/lib/plugin-recommender.js +141 -141
  155. package/lib/project-discovery.js +491 -491
  156. package/lib/setup-action-log.js +139 -0
  157. package/lib/setup-summary-renderer.js +106 -0
  158. package/lib/setup-utils.js +96 -0
  159. package/lib/setup.js +192 -118
  160. package/lib/smart-merge.js +64 -0
  161. package/lib/symlink-utils.js +81 -0
  162. package/lib/workflow-profiles.js +197 -197
  163. package/package.json +131 -129
  164. package/scripts/beads-context.sh +291 -0
  165. package/scripts/beads-context.test.js +563 -0
  166. package/scripts/behavioral-judge.sh +378 -0
  167. package/scripts/benchmark.js +85 -0
  168. package/scripts/branch-protection.js +183 -0
  169. package/scripts/check-agents.js +172 -0
  170. package/scripts/commitlint.js +42 -0
  171. package/scripts/conflict-detect.sh +323 -0
  172. package/scripts/dep-guard-analyze.js +71 -0
  173. package/scripts/dep-guard.sh +811 -0
  174. package/scripts/eval_win.py +249 -0
  175. package/scripts/file-index.sh +399 -0
  176. package/scripts/github-beads-sync/comment.mjs +64 -0
  177. package/scripts/github-beads-sync/config.mjs +148 -0
  178. package/scripts/github-beads-sync/github-api.mjs +131 -0
  179. package/scripts/github-beads-sync/index.mjs +332 -0
  180. package/scripts/github-beads-sync/label-mapper.mjs +54 -0
  181. package/scripts/github-beads-sync/mapping.mjs +78 -0
  182. package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
  183. package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
  184. package/scripts/github-beads-sync/run-bd.mjs +159 -0
  185. package/scripts/github-beads-sync/sanitize.mjs +121 -0
  186. package/scripts/github-beads-sync.config.json +26 -0
  187. package/scripts/improve-command.js +375 -0
  188. package/scripts/lib/eval-runner.js +229 -0
  189. package/scripts/lib/eval-schema.js +135 -0
  190. package/scripts/lib/eval-storage.js +78 -0
  191. package/scripts/lib/grading.js +203 -0
  192. package/scripts/lib/transcript-parser.js +63 -0
  193. package/scripts/lint.js +47 -0
  194. package/scripts/migrate-to-bun-test.js +412 -0
  195. package/scripts/run-command-eval.js +236 -0
  196. package/scripts/smart-status.sh +782 -0
  197. package/scripts/sync-commands.js +571 -0
  198. package/scripts/sync-utils.sh +460 -0
  199. package/scripts/test-dashboard.js +123 -0
  200. package/scripts/test.js +44 -0
  201. package/scripts/validate.sh +94 -0
  202. package/skills/parallel-deep-research/SKILL.md +108 -108
  203. package/skills/parallel-deep-research/evals/README.md +27 -27
  204. package/skills/parallel-deep-research/evals/evals.json +62 -62
  205. package/skills/sonarcloud-analysis/SKILL.md +171 -171
  206. package/skills/sonarcloud-analysis/evals/README.md +27 -27
  207. package/skills/sonarcloud-analysis/evals/evals.json +50 -50
  208. package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
  209. package/docs/WORKFLOW.md +0 -400
@@ -0,0 +1,782 @@
1
+ #!/usr/bin/env bash
2
+ # smart-status.sh — Workflow intelligence scoring engine
3
+ #
4
+ # Reads issues via `bd list --json --limit 0`, computes a composite score
5
+ # for each issue, and outputs them sorted by score descending.
6
+ #
7
+ # Composite score:
8
+ # priority_weight * unblock_chain * type_weight * status_boost * epic_proximity * staleness_boost
9
+ #
10
+ # Usage:
11
+ # smart-status.sh [--json]
12
+ #
13
+ # Environment:
14
+ # BD_CMD — override the bd command (for testing with mocks)
15
+ # GIT_CMD — override the git command (for testing with mocks)
16
+ # DEFAULT_BRANCH — override the default branch name (default: auto-detect)
17
+ #
18
+ # Cross-platform: bash 3.2 compatible (no associative arrays, no mapfile).
19
+ # OWASP A03: All variables quoted, sanitize() strips injection patterns.
20
+
21
+ set -euo pipefail
22
+
23
+ # ── Helpers ──────────────────────────────────────────────────────────────
24
+
25
+ # Sanitize a string: strip shell-injection patterns (OWASP A03)
26
+ # Removes: double quotes, $(...), backticks, semicolons, and newlines
27
+ sanitize() {
28
+ local val="$1"
29
+ # Remove double quotes
30
+ val="${val//\"/}"
31
+ # Remove $(...) command substitution patterns (loop handles nested)
32
+ val="$(printf '%s' "$val" | sed -e ':loop' -e 's/\$([^()]*)//g' -e 't loop')"
33
+ # Remove backtick command substitution
34
+ val="${val//\`/}"
35
+ # Remove semicolons (command chaining)
36
+ val="${val//;/}"
37
+ # Collapse newlines to spaces
38
+ val="$(printf '%s' "$val" | tr '\n' ' ')"
39
+ printf '%s' "$val"
40
+ }
41
+
42
+ # ── Source cross-dev awareness scripts ────────────────────────────────
43
+ # file-index.sh: file_index_read for reading file index entries
44
+ # sync-utils.sh: get_session_identity for current developer identity
45
+
46
+ _SMART_STATUS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
47
+ if [ -f "$_SMART_STATUS_DIR/file-index.sh" ]; then
48
+ source "$_SMART_STATUS_DIR/file-index.sh"
49
+ fi
50
+ if [ -f "$_SMART_STATUS_DIR/sync-utils.sh" ]; then
51
+ source "$_SMART_STATUS_DIR/sync-utils.sh"
52
+ fi
53
+
54
+ # ── Dependency check ────────────────────────────────────────────────────
55
+
56
+ if ! command -v jq &>/dev/null; then
57
+ echo "Error: jq is required but not found." >&2
58
+ echo "Install jq:" >&2
59
+ echo " macOS: brew install jq" >&2
60
+ echo " Ubuntu: sudo apt-get install jq" >&2
61
+ echo " Windows: winget install jqlang.jq" >&2
62
+ exit 1
63
+ fi
64
+
65
+ # Warn if jq < 1.6 (fromdateiso8601 and sub/2 require 1.6+)
66
+ _jq_version="$(jq --version 2>/dev/null | sed 's/jq-//')" || true
67
+ if [ -n "$_jq_version" ]; then
68
+ _jq_major="${_jq_version%%.*}"
69
+ _jq_minor="${_jq_version#*.}"; _jq_minor="${_jq_minor%%.*}"
70
+ if [ "${_jq_major:-0}" -lt 1 ] || { [ "${_jq_major:-0}" -eq 1 ] && [ "${_jq_minor:-0}" -lt 6 ]; }; then
71
+ echo "Warning: jq $_jq_version detected — staleness features require jq 1.6+. Team Activity may not display." >&2
72
+ fi
73
+ fi
74
+
75
+ # ── Configuration ───────────────────────────────────────────────────────
76
+
77
+ BD="${BD_CMD:-bd}"
78
+ GIT="${GIT_CMD:-git}"
79
+ JSON_MODE=0
80
+
81
+ # Auto-detect default branch (master or main), with override via DEFAULT_BRANCH env
82
+ if [ -n "${DEFAULT_BRANCH:-}" ]; then
83
+ BASE_BRANCH="$DEFAULT_BRANCH"
84
+ elif "$GIT" rev-parse --verify master >/dev/null 2>&1; then
85
+ BASE_BRANCH="master"
86
+ elif "$GIT" rev-parse --verify main >/dev/null 2>&1; then
87
+ BASE_BRANCH="main"
88
+ else
89
+ BASE_BRANCH="master"
90
+ fi
91
+
92
+ # Parse arguments (bash 3.2 compatible — no associative arrays)
93
+ for arg in "$@"; do
94
+ case "$arg" in
95
+ --json) JSON_MODE=1 ;;
96
+ --help|-h)
97
+ echo "Usage: smart-status.sh [--json]"
98
+ echo ""
99
+ echo "Scores and ranks issues by composite priority score."
100
+ echo ""
101
+ echo "Options:"
102
+ echo " --json Output raw scored JSON array"
103
+ echo " --help Show this help message"
104
+ exit 0
105
+ ;;
106
+ *)
107
+ echo "Error: unknown argument: $(sanitize "$arg")" >&2
108
+ exit 1
109
+ ;;
110
+ esac
111
+ done
112
+
113
+ # ── Fetch issues ────────────────────────────────────────────────────────
114
+
115
+ ISSUES_JSON="$("$BD" list --json --limit 0 2>/dev/null || echo '[]')"
116
+
117
+ # Bail early on empty
118
+ if [ "$(printf '%s' "$ISSUES_JSON" | jq 'length')" = "0" ]; then
119
+ if [ "$JSON_MODE" = "1" ]; then
120
+ echo '{"sessions":[],"issues":[],"team_activity":[]}'
121
+ else
122
+ echo "No issues found."
123
+ fi
124
+ exit 0
125
+ fi
126
+
127
+ # ── Identify epics and fetch their children ─────────────────────────────
128
+
129
+ # Extract epic IDs (type == "epic")
130
+ EPIC_IDS="$(printf '%s' "$ISSUES_JSON" | jq -r '.[] | select(.type == "epic") | .id')"
131
+
132
+ # Build a JSON object mapping epic_id -> { closed, total }
133
+ EPIC_STATS="{}"
134
+ if [ -n "$EPIC_IDS" ]; then
135
+ # Process each epic (bash 3.2 compatible — read line by line)
136
+ while IFS= read -r epic_id; do
137
+ [ -z "$epic_id" ] && continue
138
+ CHILDREN_JSON="$("$BD" children "$epic_id" --json 2>/dev/null || echo '[]')"
139
+ TOTAL="$(printf '%s' "$CHILDREN_JSON" | jq 'length')"
140
+ CLOSED="$(printf '%s' "$CHILDREN_JSON" | jq '[.[] | select(.status == "closed")] | length')"
141
+ EPIC_STATS="$(printf '%s' "$EPIC_STATS" | jq --arg id "$epic_id" --argjson total "$TOTAL" --argjson closed "$CLOSED" '. + {($id): {total: $total, closed: $closed}}')"
142
+ done <<EOF
143
+ $EPIC_IDS
144
+ EOF
145
+ fi
146
+
147
+ # ── Score issues with jq ────────────────────────────────────────────────
148
+
149
+ SCORED_JSON="$(printf '%s' "$ISSUES_JSON" | jq --argjson epic_stats "$EPIC_STATS" '
150
+ # First pass: build reverse dependency map (who does each issue unblock?)
151
+ # For each issue X, find all issues Y where Y.dependencies[].depends_on_id == X.id
152
+ (reduce .[] as $dep_issue ({};
153
+ reduce ($dep_issue.dependencies // [] | .[]) as $dep (.;
154
+ .[$dep.depends_on_id] += [$dep_issue.id]
155
+ )
156
+ )) as $dependents_map |
157
+
158
+ [.[] | . as $issue |
159
+
160
+ # Priority weight: P0=5, P1=4, P2=3, P3=2, P4=1, default=1
161
+ (if .priority == "P0" then 5
162
+ elif .priority == "P1" then 4
163
+ elif .priority == "P2" then 3
164
+ elif .priority == "P3" then 2
165
+ elif .priority == "P4" then 1
166
+ else 1 end) as $priority_weight |
167
+
168
+ # Unblock chain: dependent_count + 1 (min 1).
169
+ # NOTE: dependent_count is the authoritative server-side count. The "Unblocks:"
170
+ # annotation (computed below from local response) may show fewer items when
171
+ # bd list omits closed or filtered-out issues. This is an intentional trade-off.
172
+ (((.dependent_count // 0) + 1) | if . < 1 then 1 else . end) as $unblock_chain |
173
+
174
+ # Type weight: bug=1.2, feature=1.0, task=0.8, default=1.0
175
+ (if .type == "bug" then 1.2
176
+ elif .type == "feature" then 1.0
177
+ elif .type == "task" then 0.8
178
+ else 1.0 end) as $type_weight |
179
+
180
+ # Status boost: in_progress=1.5, open=1.0, default=1.0
181
+ (if .status == "in_progress" then 1.5
182
+ elif .status == "open" then 1.0
183
+ else 1.0 end) as $status_boost |
184
+
185
+ # Epic proximity: if issue has parent_id that is an epic, compute
186
+ # 1.0 + (closed_siblings / total_siblings) * 0.5
187
+ (if (.parent_id // "") != "" and ($epic_stats[.parent_id] // null) != null then
188
+ $epic_stats[.parent_id] as $es |
189
+ if $es.total > 0 then
190
+ 1.0 + (($es.closed / $es.total) * 0.5)
191
+ else 1.0 end
192
+ else 1.0 end) as $epic_proximity |
193
+
194
+ # Staleness boost based on updated_at
195
+ # 0-7d=1.0, 7-14d=1.1, 14-30d=1.2, 30+d=1.5
196
+ # Truncate to first 19 chars (YYYY-MM-DDTHH:MM:SS) + "Z" for fromdateiso8601.
197
+ # Handles fractional seconds (.NNN), timezone offsets (+05:30), or both.
198
+ (if .updated_at then
199
+ ((.updated_at[:19] + "Z") | fromdateiso8601) as $ts |
200
+ ((now - $ts) / 86400) as $days |
201
+ if $days >= 30 then 1.5
202
+ elif $days >= 14 then 1.2
203
+ elif $days >= 7 then 1.1
204
+ else 1.0 end
205
+ else 1.0 end) as $staleness_boost |
206
+
207
+ # Composite score
208
+ ($priority_weight * $unblock_chain * $type_weight * $status_boost * $epic_proximity * $staleness_boost) as $score |
209
+
210
+ # Compute dependents list (which issues does this one unblock?)
211
+ ($dependents_map[$issue.id] // []) as $dependents_list |
212
+
213
+ # Output scored issue
214
+ $issue + {
215
+ score: ($score * 100 | round / 100),
216
+ priority_weight: $priority_weight,
217
+ unblock_chain: $unblock_chain,
218
+ type_weight: $type_weight,
219
+ status_boost: $status_boost,
220
+ epic_proximity: (($epic_proximity * 100 | round) / 100),
221
+ staleness_boost: $staleness_boost,
222
+ dependents: $dependents_list
223
+ }
224
+ ] | sort_by(-.score)
225
+ ')"
226
+
227
+ # ── Session detection ─────────────────────────────────────────────────
228
+
229
+ # Parse git worktree list --porcelain to find active sessions
230
+ # Format: blocks separated by blank lines, each block has:
231
+ # worktree <path>
232
+ # HEAD <sha>
233
+ # branch refs/heads/<branch>
234
+
235
+ WORKTREE_PORCELAIN="$("$GIT" worktree list --porcelain 2>/dev/null || echo '')"
236
+
237
+ # Collect non-default-branch worktree branches and paths
238
+ # Stored as newline-delimited "branch<TAB>path" entries — safe for paths with |, spaces, etc.
239
+ SESSION_ENTRIES=""
240
+ SESSION_COUNT=0
241
+
242
+ _wt_path=""
243
+ _wt_branch=""
244
+ while IFS= read -r line; do
245
+ case "$line" in
246
+ worktree\ *)
247
+ _wt_path="${line#worktree }"
248
+ ;;
249
+ branch\ *)
250
+ _wt_branch="${line#branch refs/heads/}"
251
+ ;;
252
+ "")
253
+ # End of block — process if we have a branch that is not the default branch
254
+ if [ -n "$_wt_branch" ] && [ "$_wt_branch" != "$BASE_BRANCH" ]; then
255
+ SESSION_COUNT=$((SESSION_COUNT + 1))
256
+ if [ -n "$SESSION_ENTRIES" ]; then
257
+ SESSION_ENTRIES="${SESSION_ENTRIES}
258
+ ${_wt_branch} ${_wt_path}"
259
+ else
260
+ SESSION_ENTRIES="${_wt_branch} ${_wt_path}"
261
+ fi
262
+ fi
263
+ _wt_path=""
264
+ _wt_branch=""
265
+ ;;
266
+ esac
267
+ done <<EOF
268
+ ${WORKTREE_PORCELAIN}
269
+
270
+ EOF
271
+
272
+ # Get in-progress issues for matching (only if we have sessions)
273
+ IN_PROGRESS_JSON="[]"
274
+ if [ "$SESSION_COUNT" -gt 0 ]; then
275
+ IN_PROGRESS_JSON="$("$BD" list --status in_progress --json --limit 0 2>/dev/null || echo '')"
276
+ # Fallback: only if bd command failed (empty output), not if result is legitimately empty array
277
+ if [ -z "$IN_PROGRESS_JSON" ] || ! printf '%s' "$IN_PROGRESS_JSON" | jq empty 2>/dev/null; then
278
+ IN_PROGRESS_JSON="$(printf '%s' "$ISSUES_JSON" | jq '[.[] | select(.status == "in_progress")]')"
279
+ fi
280
+ fi
281
+
282
+ # Build sessions data: for each branch, find matching in-progress issues
283
+ # Match by: branch slug (after feat/, fix/, docs/) appears in issue title (case-insensitive, hyphen-to-space)
284
+ SESSIONS_JSON="[]"
285
+ if [ "$SESSION_COUNT" -gt 0 ]; then
286
+ # Process each session (newline-delimited "branch<TAB>path" entries)
287
+ while IFS=' ' read -r _branch _path; do
288
+ [ -z "$_branch" ] && continue
289
+
290
+ # Extract slug from branch name (strip any <prefix>/ convention)
291
+ _slug="$_branch"
292
+ case "$_slug" in
293
+ */*) _slug="${_slug##*/}" ;;
294
+ esac
295
+
296
+ # Convert slug hyphens to spaces for title matching
297
+ _slug_spaces="$(printf '%s' "$_slug" | tr '-' ' ')"
298
+
299
+ # Find matching in-progress issues: title contains slug words (case-insensitive)
300
+ _matched_ids="$(printf '%s' "$IN_PROGRESS_JSON" | jq -r --arg slug "$_slug_spaces" '
301
+ [.[] | select(
302
+ (.title | ascii_downcase | contains($slug | ascii_downcase))
303
+ ) | .id] | join(",")
304
+ ')"
305
+
306
+ _issue_count=0
307
+ if [ -n "$_matched_ids" ]; then
308
+ # Count commas + 1
309
+ _issue_count="$(printf '%s' "$_matched_ids" | tr -cd ',' | wc -c)"
310
+ _issue_count=$((_issue_count + 1))
311
+ fi
312
+
313
+ # Build session JSON entry
314
+ if [ -n "$_matched_ids" ]; then
315
+ _ids_json="$(printf '%s' "$_matched_ids" | jq -R 'split(",")')"
316
+ else
317
+ _ids_json="[]"
318
+ fi
319
+
320
+ SESSIONS_JSON="$(printf '%s' "$SESSIONS_JSON" | jq \
321
+ --arg branch "$_branch" \
322
+ --arg path "$_path" \
323
+ --argjson ids "$_ids_json" \
324
+ --argjson count "$_issue_count" \
325
+ '. + [{branch: $branch, path: $path, issue_ids: $ids, issue_count: $count}]'
326
+ )"
327
+
328
+ done <<SESSIONS_EOF
329
+ $SESSION_ENTRIES
330
+ SESSIONS_EOF
331
+ fi
332
+
333
+ # ── File-level conflict detection ────────────────────────────────────────
334
+ # For each active worktree branch, get changed files via:
335
+ # git diff <base-branch>...<branch> --name-only --
336
+ # Uses -- separator to prevent argument injection (OWASP A03)
337
+
338
+ # ALL_BRANCH_FILES: newline-delimited list of "branch<TAB>file1<TAB>file2..."
339
+ ALL_BRANCH_FILES=""
340
+ if [ "$SESSION_COUNT" -ge 2 ]; then
341
+ # Read branches from SESSION_ENTRIES (newline-delimited "branch<TAB>path")
342
+ while IFS=' ' read -r _branch _path; do
343
+ [ -z "$_branch" ] && continue
344
+
345
+ # Get changed files for this branch vs BASE_BRANCH (-- prevents injection)
346
+ _files="$("$GIT" diff "${BASE_BRANCH}...${_branch}" --name-only -- 2>/dev/null || echo '')"
347
+ # Collapse to tab-delimited (tabs can't appear in filenames), strip empty lines
348
+ _files_csv="$(printf '%s' "$_files" | tr '\n' '\t' | sed 's/\t$//' | sed 's/^\t//')"
349
+
350
+ if [ -n "$ALL_BRANCH_FILES" ]; then
351
+ ALL_BRANCH_FILES="${ALL_BRANCH_FILES}
352
+ ${_branch}:${_files_csv}"
353
+ else
354
+ ALL_BRANCH_FILES="${_branch}:${_files_csv}"
355
+ fi
356
+ done <<CONFLICT_EOF
357
+ $SESSION_ENTRIES
358
+ CONFLICT_EOF
359
+
360
+ # Build conflict map: find files that appear in multiple branches
361
+ # and add changed_files + conflicts to SESSIONS_JSON
362
+ while IFS= read -r _entry; do
363
+ [ -z "$_entry" ] && continue
364
+ _branch="${_entry%%:*}"
365
+ _files_csv="${_entry#*:}"
366
+
367
+ # Convert tab-delimited files to JSON array
368
+ if [ -n "$_files_csv" ]; then
369
+ _files_json="$(printf '%s' "$_files_csv" | jq -R 'split("\t")')"
370
+ else
371
+ _files_json="[]"
372
+ fi
373
+
374
+ # Find conflicts: files in this branch that also appear in other branches
375
+ _conflicts_json="[]"
376
+ while IFS= read -r _other_entry; do
377
+ [ -z "$_other_entry" ] && continue
378
+ _other_branch="${_other_entry%%:*}"
379
+ _other_files="${_other_entry#*:}"
380
+
381
+ # Skip self
382
+ [ "$_other_branch" = "$_branch" ] && continue
383
+ [ -z "$_other_files" ] && continue
384
+
385
+ # Find intersection
386
+ _other_json="$(printf '%s' "$_other_files" | jq -R 'split("\t")')"
387
+ _overlap="$(jq -n --argjson a "$_files_json" --argjson b "$_other_json" \
388
+ '[$a[] as $f | select($b | index($f))] | unique')"
389
+ _overlap_len="$(printf '%s' "$_overlap" | jq 'length')"
390
+ if [ "$_overlap_len" -gt 0 ]; then
391
+ _conflicts_json="$(jq -n \
392
+ --argjson existing "$_conflicts_json" \
393
+ --arg other_branch "$_other_branch" \
394
+ --argjson files "$_overlap" \
395
+ '$existing + [{branch: $other_branch, files: $files}]')"
396
+ fi
397
+ done <<INNER_EOF
398
+ $ALL_BRANCH_FILES
399
+ INNER_EOF
400
+
401
+ # Update SESSIONS_JSON: add changed_files and conflicts to matching branch entry
402
+ SESSIONS_JSON="$(printf '%s' "$SESSIONS_JSON" | jq \
403
+ --arg branch "$_branch" \
404
+ --argjson files "$_files_json" \
405
+ --argjson conflicts "$_conflicts_json" \
406
+ '[.[] | if .branch == $branch then . + {changed_files: $files, conflicts: $conflicts} else . end]'
407
+ )"
408
+ done <<OUTER_EOF
409
+ $ALL_BRANCH_FILES
410
+ OUTER_EOF
411
+ fi
412
+
413
+ # ── Tier 2: git merge-tree conflict detection ──────────────────────────
414
+ # If git >= 2.38 and there are file overlaps from Tier 1, run merge-tree
415
+ # to detect actual merge conflicts vs mere file overlap.
416
+
417
+ TIER2_ENABLED=0
418
+ if [ "$SESSION_COUNT" -ge 2 ]; then
419
+ # Check git version >= 2.38 (merge-tree --write-tree requires it)
420
+ _git_ver="$("$GIT" --version 2>/dev/null || echo '')"
421
+ _git_major=0
422
+ _git_minor=0
423
+ # Parse "git version X.Y.Z..." -> extract major and minor
424
+ _ver_nums="${_git_ver#git version }"
425
+ _git_major="${_ver_nums%%.*}"
426
+ _ver_rest="${_ver_nums#*.}"
427
+ _git_minor="${_ver_rest%%.*}"
428
+ # Validate they are numbers
429
+ case "$_git_major" in ''|*[!0-9]*) _git_major=0 ;; esac
430
+ case "$_git_minor" in ''|*[!0-9]*) _git_minor=0 ;; esac
431
+
432
+ if [ "$_git_major" -gt 2 ] || { [ "$_git_major" -eq 2 ] && [ "$_git_minor" -ge 38 ]; }; then
433
+ TIER2_ENABLED=1
434
+ fi
435
+ fi
436
+
437
+ # For each pair of branches with file overlaps, run merge-tree
438
+ if [ "$TIER2_ENABLED" = "1" ]; then
439
+ # Collect all branch pairs that have conflicts in SESSIONS_JSON
440
+ _conflict_pairs="$(printf '%s' "$SESSIONS_JSON" | jq -r '
441
+ [.[] | select((.conflicts // []) | length > 0) |
442
+ .branch as $b |
443
+ .conflicts[] |
444
+ [$b, .branch] | sort | join(" ")
445
+ ] | unique | .[]
446
+ ')"
447
+
448
+ if [ -n "$_conflict_pairs" ]; then
449
+ # Process each unique pair
450
+ while IFS= read -r _pair; do
451
+ [ -z "$_pair" ] && continue
452
+ _b1="${_pair%% *}"
453
+ _b2="${_pair#* }"
454
+
455
+ # Run merge-tree; exit 1 = actual conflict, exit 0 = clean merge
456
+ _mt_output=""
457
+ _mt_exit=0
458
+ _mt_output="$("$GIT" merge-tree --write-tree --name-only --no-messages -- "$_b1" "$_b2" 2>/dev/null)" || _mt_exit=$?
459
+
460
+ if [ "$_mt_exit" -ne 0 ] && [ -n "$_mt_output" ]; then
461
+ # Real conflict detected — parse conflicted file names
462
+ # merge-tree outputs a tree SHA on first line, then filenames
463
+ # With --name-only, conflicted files are listed after the SHA line
464
+ _conflict_files=""
465
+ _line_num=0
466
+ while IFS= read -r _cf_line; do
467
+ _line_num=$((_line_num + 1))
468
+ # Skip the first line (tree SHA output from merge-tree)
469
+ if [ "$_line_num" -eq 1 ]; then continue; fi
470
+ # Skip empty lines
471
+ if [ -z "$_cf_line" ]; then continue; fi
472
+ if [ -n "$_conflict_files" ]; then
473
+ _conflict_files="${_conflict_files} ${_cf_line}"
474
+ else
475
+ _conflict_files="${_cf_line}"
476
+ fi
477
+ done <<MTEOF
478
+ $_mt_output
479
+ MTEOF
480
+
481
+ if [ -n "$_conflict_files" ]; then
482
+ _cf_json="$(printf '%s' "$_conflict_files" | jq -R 'split("\t")')"
483
+
484
+ # Add merge_conflicts to both branches in SESSIONS_JSON
485
+ for _target_branch in "$_b1" "$_b2"; do
486
+ _other_branch="$_b1"
487
+ [ "$_target_branch" = "$_b1" ] && _other_branch="$_b2"
488
+
489
+ SESSIONS_JSON="$(printf '%s' "$SESSIONS_JSON" | jq \
490
+ --arg branch "$_target_branch" \
491
+ --arg other "$_other_branch" \
492
+ --argjson files "$_cf_json" \
493
+ '[.[] | if .branch == $branch then
494
+ . + {merge_conflicts: ((.merge_conflicts // []) + [{branch: $other, files: $files}])}
495
+ else . end]'
496
+ )"
497
+ done
498
+ fi
499
+ fi
500
+ done <<PAIRSEOF
501
+ $_conflict_pairs
502
+ PAIRSEOF
503
+ fi
504
+ fi
505
+
506
+ # ── Cross-developer team activity ────────────────────────────────────────
507
+ # Read file index entries from all developers, identify OTHER developers'
508
+ # in-progress work, compute module overlaps, and flag stale claims.
509
+
510
+ TEAM_ACTIVITY_JSON="[]"
511
+
512
+ # Only proceed if file_index_read and get_session_identity are available
513
+ if command -v file_index_read &>/dev/null && command -v get_session_identity &>/dev/null; then
514
+ _file_index_all="$(file_index_read 2>/dev/null || echo '[]')"
515
+ _file_index_len="$(printf '%s' "$_file_index_all" | jq 'length')"
516
+
517
+ if [ "$_file_index_len" -gt 0 ]; then
518
+ _my_identity="$(get_session_identity 2>/dev/null || echo '')"
519
+
520
+ # Allow override via SMART_STATUS_IDENTITY env (for testing)
521
+ if [ -n "${SMART_STATUS_IDENTITY:-}" ]; then
522
+ _my_identity="$SMART_STATUS_IDENTITY"
523
+ fi
524
+
525
+ if [ -n "$_my_identity" ]; then
526
+ # Build team activity: filter out current dev, compute overlaps and staleness
527
+ # Current dev's modules (for overlap detection)
528
+ _my_modules="$(printf '%s' "$_file_index_all" | jq -c --arg me "$_my_identity" \
529
+ '[.[] | select(.developer == $me) | .modules // [] | .[]] | unique')" || _my_modules="[]"
530
+
531
+ # STALENESS_THRESHOLD_SECS = 48 hours = 172800 seconds
532
+ TEAM_ACTIVITY_JSON="$(printf '%s' "$_file_index_all" | jq -c --arg me "$_my_identity" \
533
+ --argjson my_modules "$_my_modules" \
534
+ --arg now_ts "$(date +%s)" '
535
+ [.[] | select(.developer != $me) |
536
+ . as $entry |
537
+ # Compute staleness
538
+ (if .updated_at then
539
+ ((.updated_at | sub("\\.[0-9]+Z$"; "Z")) | fromdateiso8601) as $ts |
540
+ (($now_ts | tonumber) - $ts) as $age_secs |
541
+ {
542
+ age_secs: $age_secs,
543
+ stale: ($age_secs > 172800),
544
+ days_ago: (($age_secs / 86400) | floor)
545
+ }
546
+ else
547
+ { age_secs: 0, stale: false, days_ago: 0 }
548
+ end) as $staleness |
549
+ # Compute module overlaps
550
+ ([$entry.modules // [] | .[] | select(. as $m | $my_modules | index($m))] | unique) as $overlaps |
551
+ {
552
+ developer: $entry.developer,
553
+ issue_id: $entry.issue_id,
554
+ modules: ($entry.modules // []),
555
+ files: ($entry.files // []),
556
+ stale: $staleness.stale,
557
+ days_ago: $staleness.days_ago,
558
+ overlapping_modules: $overlaps,
559
+ updated_at: $entry.updated_at
560
+ }
561
+ ] | sort_by(.developer) | group_by(.developer) | map({
562
+ developer: .[0].developer,
563
+ issues: [.[] | {
564
+ issue_id: .issue_id,
565
+ modules: .modules,
566
+ files: .files,
567
+ stale: .stale,
568
+ days_ago: .days_ago,
569
+ overlapping_modules: .overlapping_modules
570
+ }]
571
+ })
572
+ ')" || TEAM_ACTIVITY_JSON="[]"
573
+ fi
574
+ fi
575
+ fi
576
+
577
+ # ── Output ──────────────────────────────────────────────────────────────
578
+
579
+ if [ "$JSON_MODE" = "1" ]; then
580
+ # Always output consistent shape: {sessions: [...], issues: [...], team_activity: [...]}
581
+ jq -n --argjson sessions "$SESSIONS_JSON" --argjson issues "$SCORED_JSON" \
582
+ --argjson team_activity "$TEAM_ACTIVITY_JSON" \
583
+ '{sessions: $sessions, issues: $issues, team_activity: $team_activity}'
584
+ else
585
+ # Grouped, colored output
586
+ # NO_COLOR support: https://no-color.org/
587
+ if [ -n "${NO_COLOR:-}" ]; then
588
+ C_RESET="" C_GREEN="" C_YELLOW="" C_RED="" C_DIM="" C_BOLD="" C_CYAN=""
589
+ else
590
+ C_RESET=$'\033[0m' C_GREEN=$'\033[32m' C_YELLOW=$'\033[33m'
591
+ C_RED=$'\033[31m' C_DIM=$'\033[2m' C_BOLD=$'\033[1m' C_CYAN=$'\033[36m'
592
+ fi
593
+
594
+ # ── Active Sessions (before grouped output) ──
595
+ if [ "$SESSION_COUNT" -gt 0 ]; then
596
+ printf '%s%s=== ACTIVE SESSIONS ===%s\n' "$C_BOLD" "$C_CYAN" "$C_RESET"
597
+ printf '%s' "$SESSIONS_JSON" | jq -r '.[] |
598
+ .branch as $branch |
599
+ .issue_ids as $ids |
600
+ .issue_count as $count |
601
+ .changed_files as $files |
602
+ .conflicts as $conflicts |
603
+ .merge_conflicts as $merge_conflicts |
604
+ # Branch line
605
+ (if $count > 0 then
606
+ " " + $branch + " -> " + ($ids | join(", ")) + " (" + ($count | tostring) + " issue" + (if $count > 1 then "s" else "" end) + ")"
607
+ else
608
+ " " + $branch + " (untracked)"
609
+ end),
610
+ # Changed files line (up to 3, then +N more)
611
+ (if ($files // [] | length) > 0 then
612
+ ($files | length) as $total |
613
+ (if $total > 3 then
614
+ " Changed: " + ($files[:3] | join(", ")) + " (+" + (($total - 3) | tostring) + " more)"
615
+ else
616
+ " Changed: " + ($files | join(", "))
617
+ end)
618
+ else empty end),
619
+ # Tier 2: Merge conflict annotations (real conflicts from merge-tree)
620
+ (if ($merge_conflicts // [] | length) > 0 then
621
+ ($merge_conflicts[] |
622
+ .branch as $other |
623
+ .files[] |
624
+ " !! Merge conflict: " + . + " (" + $branch + " vs " + $other + ")"
625
+ )
626
+ else empty end),
627
+ # Tier 1: Conflict risk annotations (file overlap only, excluding merge conflicts)
628
+ (if ($conflicts // [] | length) > 0 then
629
+ # Build set of files that have real merge conflicts
630
+ ([$merge_conflicts // [] | .[] | .files[]] | unique) as $mc_files |
631
+ ($conflicts[] |
632
+ .branch as $other |
633
+ .files[] |
634
+ # Only show Conflict risk if NOT already a merge conflict
635
+ if ($mc_files | index(.)) then empty
636
+ else " ! Conflict risk: " + . + " (" + $other + " in-progress)"
637
+ end
638
+ )
639
+ else empty end)
640
+ '
641
+ printf '\n'
642
+ fi
643
+
644
+ # ── Team Activity (cross-developer visibility) ──
645
+ _ta_len="$(printf '%s' "$TEAM_ACTIVITY_JSON" | jq 'length')"
646
+ if [ "$_ta_len" -gt 0 ]; then
647
+ printf '%s%s── Team Activity ──────────────────────────────────────%s\n' "$C_BOLD" "$C_CYAN" "$C_RESET"
648
+ printf '%s' "$TEAM_ACTIVITY_JSON" | jq -r '
649
+ .[] |
650
+ .developer as $dev |
651
+ " " + $dev + ":",
652
+ (.issues[] |
653
+ .issue_id as $iid |
654
+ .modules as $mods |
655
+ .stale as $stale |
656
+ .days_ago as $days |
657
+ .overlapping_modules as $overlaps |
658
+ # Issue line with optional stale annotation
659
+ (if $stale then
660
+ " " + $iid + " (in_progress, stale: claimed " + ($days | tostring) + " days ago)"
661
+ else
662
+ " " + $iid + " (in_progress)" +
663
+ (if ($mods | length) > 0 then
664
+ " — touching " + ($mods | join(", "))
665
+ else "" end)
666
+ end),
667
+ # Overlap or no-overlap line
668
+ (if ($overlaps | length) > 0 then
669
+ " ! Overlap: " + ($overlaps | join(", ")) + " (you are also working here)"
670
+ else
671
+ " No overlaps with your work"
672
+ end)
673
+ )
674
+ '
675
+ printf '\n'
676
+ fi
677
+
678
+ # Use jq to assign groups and format, then bash to colorize
679
+ printf '%s\n' "$SCORED_JSON" | jq -r --arg now "$(date +%s)" '
680
+ # Assign group to each issue
681
+ [.[] | . as $item |
682
+ # Compute days since updated_at
683
+ (if .updated_at then
684
+ ((.updated_at[:19] + "Z") | fromdateiso8601) as $ts |
685
+ (($now | tonumber) - $ts) / 86400 | floor
686
+ else 0 end) as $days_ago |
687
+ # Group assignment (priority order: RESUME > UNBLOCK_CHAINS > BLOCKED > BACKLOG > READY_WORK)
688
+ # Note: in_progress issues with active blockers still show in RESUME (you started
689
+ # the work, you need to see it) but get a "BLOCKED" annotation in the output.
690
+ (if .status == "in_progress" then "RESUME"
691
+ elif (.dependent_count // 0) >= 2 then "UNBLOCK_CHAINS"
692
+ elif (.dependency_count // 0) > 0 and .status != "closed" then "BLOCKED"
693
+ elif .priority == "P4" then "BACKLOG"
694
+ else "READY_WORK"
695
+ end) as $group |
696
+ # Flag in_progress issues that have active blockers
697
+ (if .status == "in_progress" and (.dependency_count // 0) > 0 then true else false end) as $blocked_resume |
698
+ $item + { group: $group, days_ago: $days_ago, blocked_resume: $blocked_resume }
699
+ ] |
700
+
701
+ # Group order mapping
702
+ def group_order:
703
+ if . == "RESUME" then 0
704
+ elif . == "UNBLOCK_CHAINS" then 1
705
+ elif . == "READY_WORK" then 2
706
+ elif . == "BLOCKED" then 3
707
+ elif . == "BACKLOG" then 4
708
+ else 5 end;
709
+
710
+ # Sort by group order, then by score descending within group
711
+ sort_by([(.group | group_order), -.score]) |
712
+
713
+ # Group and format
714
+ group_by(.group) |
715
+ sort_by(.[0].group | group_order) |
716
+ [.[] |
717
+ # Group header
718
+ (.[0].group | if . == "UNBLOCK_CHAINS" then "UNBLOCK CHAINS"
719
+ elif . == "READY_WORK" then "READY WORK"
720
+ else . end) as $header |
721
+ "GROUP:" + $header,
722
+ (to_entries[] |
723
+ .value as $item |
724
+ (.key + 1 | tostring) as $rank |
725
+ # Staleness flag
726
+ (if $item.days_ago >= 7 then " [stale " + ($item.days_ago | tostring) + "d]" else "" end) as $stale |
727
+ # Status + days
728
+ ("[" + ($item.status // "open") + " " + ($item.days_ago | tostring) + "d]") as $status_tag |
729
+ # Unblocks annotation
730
+ (if ($item.dependents // [] | length) > 0 then
731
+ " -> Unblocks: " + ($item.dependents | join(", "))
732
+ else "" end) as $unblocks |
733
+ # Blocked-resume warning
734
+ (if $item.blocked_resume then " !! BLOCKED by dependencies" else "" end) as $blocked_warn |
735
+ "ENTRY:" + $item.group + ":" +
736
+ $rank + ". [" + ($item.score | tostring) + "] " +
737
+ $item.id + " (" + ($item.priority // "-") + " " + ($item.type // "-") + ") -- " +
738
+ ($item.title // "-") + " " + $status_tag + $stale + $blocked_warn + $unblocks
739
+ ),
740
+ ""
741
+ ] | .[]
742
+ ' | while IFS= read -r line; do
743
+ case "$line" in
744
+ GROUP:RESUME)
745
+ printf '%s%s=== RESUME ===%s\n' "$C_BOLD" "$C_GREEN" "$C_RESET"
746
+ ;;
747
+ GROUP:UNBLOCK\ CHAINS)
748
+ printf '%s%s=== UNBLOCK CHAINS ===%s\n' "$C_BOLD" "$C_CYAN" "$C_RESET"
749
+ ;;
750
+ GROUP:READY\ WORK)
751
+ printf '%s%s=== READY WORK ===%s\n' "$C_BOLD" "$C_YELLOW" "$C_RESET"
752
+ ;;
753
+ GROUP:BLOCKED)
754
+ printf '%s%s=== BLOCKED ===%s\n' "$C_BOLD" "$C_RED" "$C_RESET"
755
+ ;;
756
+ GROUP:BACKLOG)
757
+ printf '%s%s=== BACKLOG ===%s\n' "$C_BOLD" "$C_DIM" "$C_RESET"
758
+ ;;
759
+ ENTRY:RESUME:*)
760
+ printf ' %s%s%s\n' "$C_GREEN" "${line#ENTRY:RESUME:}" "$C_RESET"
761
+ ;;
762
+ ENTRY:UNBLOCK_CHAINS:*)
763
+ printf ' %s%s%s\n' "$C_CYAN" "${line#ENTRY:UNBLOCK_CHAINS:}" "$C_RESET"
764
+ ;;
765
+ ENTRY:READY_WORK:*)
766
+ printf ' %s%s%s\n' "$C_YELLOW" "${line#ENTRY:READY_WORK:}" "$C_RESET"
767
+ ;;
768
+ ENTRY:BLOCKED:*)
769
+ printf ' %s%s%s\n' "$C_RED" "${line#ENTRY:BLOCKED:}" "$C_RESET"
770
+ ;;
771
+ ENTRY:BACKLOG:*)
772
+ printf ' %s%s%s\n' "$C_DIM" "${line#ENTRY:BACKLOG:}" "$C_RESET"
773
+ ;;
774
+ "")
775
+ # blank line between groups
776
+ ;;
777
+ *)
778
+ printf '%s\n' "$line"
779
+ ;;
780
+ esac
781
+ done
782
+ fi