forge-workflow 0.0.4 → 0.0.6

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