forge-workflow 0.0.5 → 0.0.7
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.
- package/.claude/commands/dev.md +6 -1
- package/.claude/commands/plan.md +59 -14
- package/.claude/commands/premerge.md +10 -0
- package/.claude/commands/review.md +7 -1
- package/.claude/commands/ship.md +95 -47
- package/.claude/commands/status.md +42 -0
- package/.claude/commands/validate.md +7 -1
- package/.claude/commands/verify.md +52 -4
- package/.claude/rules/workflow.md +16 -0
- package/.claude/scripts/greptile-resolve.sh +32 -0
- package/.cline/workflows/dev.md +6 -1
- package/.cline/workflows/plan.md +59 -14
- package/.cline/workflows/premerge.md +10 -0
- package/.cline/workflows/review.md +7 -1
- package/.cline/workflows/ship.md +95 -47
- package/.cline/workflows/status.md +42 -0
- package/.cline/workflows/validate.md +7 -1
- package/.cline/workflows/verify.md +52 -4
- package/.codex/skills/dev/SKILL.md +6 -1
- package/.codex/skills/plan/SKILL.md +59 -14
- package/.codex/skills/premerge/SKILL.md +10 -0
- package/.codex/skills/review/SKILL.md +7 -1
- package/.codex/skills/ship/SKILL.md +95 -47
- package/.codex/skills/status/SKILL.md +42 -0
- package/.codex/skills/validate/SKILL.md +7 -1
- package/.codex/skills/verify/SKILL.md +52 -4
- package/.cursor/commands/dev.md +6 -1
- package/.cursor/commands/plan.md +59 -14
- package/.cursor/commands/premerge.md +10 -0
- package/.cursor/commands/review.md +7 -1
- package/.cursor/commands/ship.md +95 -47
- package/.cursor/commands/status.md +42 -0
- package/.cursor/commands/validate.md +7 -1
- package/.cursor/commands/verify.md +52 -4
- package/.cursorrules +149 -0
- package/.github/prompts/dev.prompt.md +6 -1
- package/.github/prompts/plan.prompt.md +59 -14
- package/.github/prompts/premerge.prompt.md +10 -0
- package/.github/prompts/review.prompt.md +7 -1
- package/.github/prompts/ship.prompt.md +95 -47
- package/.github/prompts/status.prompt.md +42 -0
- package/.github/prompts/validate.prompt.md +7 -1
- package/.github/prompts/verify.prompt.md +52 -4
- package/.kilocode/workflows/dev.md +6 -1
- package/.kilocode/workflows/plan.md +59 -14
- package/.kilocode/workflows/premerge.md +10 -0
- package/.kilocode/workflows/review.md +7 -1
- package/.kilocode/workflows/ship.md +95 -47
- package/.kilocode/workflows/status.md +42 -0
- package/.kilocode/workflows/validate.md +7 -1
- package/.kilocode/workflows/verify.md +52 -4
- package/.opencode/commands/dev.md +6 -1
- package/.opencode/commands/plan.md +59 -14
- package/.opencode/commands/premerge.md +10 -0
- package/.opencode/commands/review.md +7 -1
- package/.opencode/commands/ship.md +95 -47
- package/.opencode/commands/status.md +42 -0
- package/.opencode/commands/validate.md +7 -1
- package/.opencode/commands/verify.md +52 -4
- package/.roo/commands/dev.md +6 -1
- package/.roo/commands/plan.md +59 -14
- package/.roo/commands/premerge.md +10 -0
- package/.roo/commands/review.md +7 -1
- package/.roo/commands/ship.md +95 -47
- package/.roo/commands/status.md +42 -0
- package/.roo/commands/validate.md +7 -1
- package/.roo/commands/verify.md +52 -4
- package/AGENTS.md +97 -0
- package/CLAUDE.md +10 -0
- package/README.md +2 -2
- package/bin/forge-cmd.js +5 -1
- package/bin/forge-preflight.js +15 -2
- package/bin/forge.js +211 -9
- package/docs/ENHANCED_ONBOARDING.md +96 -86
- package/docs/ROADMAP.md +2 -2
- package/docs/TOOLCHAIN.md +23 -0
- package/docs/VALIDATION.md +1 -1
- package/lefthook.yml +11 -0
- package/lib/agents/README.md +46 -1
- package/lib/agents/cline.plugin.json +11 -4
- package/lib/agents/codex.plugin.json +2 -2
- package/lib/agents/copilot.plugin.json +5 -5
- package/lib/agents/cursor.plugin.json +1 -1
- package/lib/agents/kilocode.plugin.json +1 -1
- package/lib/agents/opencode.plugin.json +7 -4
- package/lib/agents/roo.plugin.json +10 -3
- package/lib/agents-config.js +129 -81
- package/lib/codex-skills.js +50 -0
- package/lib/commands/_registry.js +173 -0
- package/lib/commands/clean.js +181 -0
- package/lib/commands/commands-reset.js +147 -0
- package/lib/commands/dev.js +84 -0
- package/lib/commands/plan.js +18 -0
- package/lib/commands/push.js +196 -0
- package/lib/commands/recommend.js +1 -1
- package/lib/commands/setup.js +4295 -0
- package/lib/commands/ship.js +20 -0
- package/lib/commands/status.js +210 -44
- package/lib/commands/sync.js +71 -0
- package/lib/commands/team.js +37 -0
- package/lib/commands/test.js +207 -0
- package/lib/commands/validate.js +13 -0
- package/lib/commands/worktree.js +310 -0
- package/lib/detect-agent.js +38 -8
- package/lib/detection-utils.js +405 -0
- package/lib/docs-command.js +51 -0
- package/lib/docs-copy.js +50 -0
- package/lib/file-utils.js +260 -0
- package/lib/forge-context.js +42 -0
- package/lib/freshness-token.js +148 -0
- package/lib/frontmatter.js +79 -0
- package/lib/greptile-match.js +80 -0
- package/lib/husky-migration.js +113 -12
- package/lib/lefthook-check.js +27 -6
- package/lib/plugin-manager.js +225 -72
- package/lib/project-discovery.js +39 -5
- package/lib/reset.js +309 -0
- package/lib/runtime-health.js +305 -0
- package/lib/shell-utils.js +50 -0
- package/lib/task-ownership.js +117 -0
- package/lib/ui-utils.js +43 -0
- package/lib/validation-utils.js +163 -0
- package/lib/workflow/enforce-stage.js +179 -0
- package/lib/workflow/stages.js +201 -0
- package/lib/workflow/state.js +332 -0
- package/opencode.json +67 -0
- package/package.json +16 -6
- package/scripts/beads-context.sh +165 -22
- package/scripts/beads-context.test.js +5 -1
- package/scripts/check-agents.js +103 -0
- package/scripts/check-forge-token.js +98 -0
- package/scripts/conflict-detect.sh +2 -2
- package/scripts/dep-guard.sh +6 -28
- package/scripts/file-index.sh +117 -23
- package/scripts/forge-team/index.sh +86 -0
- package/scripts/forge-team/lib/agent-prompt.sh +52 -0
- package/scripts/forge-team/lib/claim.sh +256 -0
- package/scripts/forge-team/lib/dashboard.sh +341 -0
- package/scripts/forge-team/lib/epic.sh +332 -0
- package/scripts/forge-team/lib/hooks.sh +253 -0
- package/scripts/forge-team/lib/identity.sh +235 -0
- package/scripts/forge-team/lib/sync-github.sh +317 -0
- package/scripts/forge-team/lib/verify.sh +284 -0
- package/scripts/forge-team/lib/workload.sh +296 -0
- package/scripts/forge-team/tests/agent-prompt.test.sh +72 -0
- package/scripts/forge-team/tests/claim.test.sh +179 -0
- package/scripts/forge-team/tests/dashboard.test.sh +170 -0
- package/scripts/forge-team/tests/dispatcher.test.sh +79 -0
- package/scripts/forge-team/tests/epic.test.sh +176 -0
- package/scripts/forge-team/tests/hooks.test.sh +239 -0
- package/scripts/forge-team/tests/identity.test.sh +176 -0
- package/scripts/forge-team/tests/integration.test.sh +371 -0
- package/scripts/forge-team/tests/sync-github.test.sh +209 -0
- package/scripts/forge-team/tests/verify.test.sh +314 -0
- package/scripts/forge-team/tests/workflow-integration.test.sh +43 -0
- package/scripts/forge-team/tests/workload.test.sh +209 -0
- package/scripts/lib/eval-runner.js +39 -0
- package/scripts/lib/jsonl-lock.sh +48 -0
- package/scripts/lib/sanitize.sh +116 -0
- package/scripts/pr-coordinator.sh +756 -0
- package/scripts/smart-status.sh +58 -21
- package/scripts/sync-commands.js +49 -20
- package/scripts/sync-utils.sh +24 -29
- package/scripts/test.js +18 -1
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# dashboard.sh — Team health dashboard for forge-team.
|
|
3
|
+
#
|
|
4
|
+
# Provides cmd_dashboard [--format=json] which aggregates team issue data
|
|
5
|
+
# from Beads and displays per-developer stats, stale assignments, and
|
|
6
|
+
# blocked issues.
|
|
7
|
+
|
|
8
|
+
# ── Dependencies ─────────────────────────────────────────────────────────
|
|
9
|
+
# BD_CMD — path to bd binary (default: bd)
|
|
10
|
+
BD_CMD="${BD_CMD:-bd}"
|
|
11
|
+
|
|
12
|
+
# FORGE_NOW — override current epoch for testing (default: real time)
|
|
13
|
+
_now() {
|
|
14
|
+
if [[ -n "${FORGE_NOW:-}" ]]; then
|
|
15
|
+
echo "$FORGE_NOW"
|
|
16
|
+
else
|
|
17
|
+
date +%s
|
|
18
|
+
fi
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# Convert ISO8601 timestamp to epoch seconds
|
|
22
|
+
_iso_to_epoch() {
|
|
23
|
+
local ts="$1"
|
|
24
|
+
# Remove trailing Z and replace T with space for date parsing
|
|
25
|
+
local clean="${ts%Z}"
|
|
26
|
+
clean="${clean//T/ }"
|
|
27
|
+
# Use date -d for GNU date (Linux/Git Bash)
|
|
28
|
+
if date -d "$clean UTC" +%s 2>/dev/null; then
|
|
29
|
+
return
|
|
30
|
+
fi
|
|
31
|
+
# Fallback: manual parse YYYY-MM-DD HH:MM:SS
|
|
32
|
+
# This handles the common case in Git Bash on Windows
|
|
33
|
+
local year month day hour min sec
|
|
34
|
+
IFS='-: ' read -r year month day hour min sec <<< "$clean"
|
|
35
|
+
# Use python if available
|
|
36
|
+
if command -v python3 &>/dev/null; then
|
|
37
|
+
python3 -c "import calendar,datetime; print(int(calendar.timegm(datetime.datetime($year,$month,$day,$hour,$min,$sec).timetuple())))"
|
|
38
|
+
elif command -v python &>/dev/null; then
|
|
39
|
+
python -c "import calendar,datetime; print(int(calendar.timegm(datetime.datetime($year,$month,$day,$hour,$min,$sec).timetuple())))"
|
|
40
|
+
else
|
|
41
|
+
# Last resort: approximate with GNU date
|
|
42
|
+
echo 0
|
|
43
|
+
fi
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# ── Data Collection ──────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
# Parse bd list output into issue IDs and titles.
|
|
49
|
+
# Each line: "◐ forge-aaa · Feature A" or "○ forge-bbb · Feature B"
|
|
50
|
+
_parse_issue_list() {
|
|
51
|
+
local line
|
|
52
|
+
while IFS= read -r line; do
|
|
53
|
+
[[ -z "$line" ]] && continue
|
|
54
|
+
# Extract issue ID: between status icon and " · "
|
|
55
|
+
local id title
|
|
56
|
+
id="$(echo "$line" | sed -E 's/^[^ ]+ +([^ ]+) +·.*/\1/')"
|
|
57
|
+
title="$(echo "$line" | sed -E 's/^[^ ]+ +[^ ]+ +· +(.*)/\1/')"
|
|
58
|
+
echo "$id|$title"
|
|
59
|
+
done
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Get issue details: status, owner, updated timestamp, blocked-by list
|
|
63
|
+
# Output: id|title|status|owner|updated_epoch|blocked_by
|
|
64
|
+
_get_issue_details() {
|
|
65
|
+
local issue_id="$1"
|
|
66
|
+
local show_output
|
|
67
|
+
show_output="$($BD_CMD show "$issue_id" 2>/dev/null)" || return 1
|
|
68
|
+
|
|
69
|
+
local status="OPEN" owner="" updated="" blocked_by="" title=""
|
|
70
|
+
|
|
71
|
+
local line
|
|
72
|
+
local in_depends=0
|
|
73
|
+
while IFS= read -r line; do
|
|
74
|
+
# First line has status: "◐ forge-aaa · Feature A [● P2 · IN_PROGRESS]"
|
|
75
|
+
if [[ "$line" =~ \[.*·[[:space:]]*(OPEN|IN_PROGRESS|BLOCKED)\] ]]; then
|
|
76
|
+
status="${BASH_REMATCH[1]}"
|
|
77
|
+
# Extract title from first line
|
|
78
|
+
title="$(echo "$line" | sed -E 's/^[^ ]+ +[^ ]+ +· +(.+) +\[.*/\1/')"
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
# Owner line
|
|
82
|
+
if [[ "$line" =~ ^Owner:[[:space:]]*(.+) ]]; then
|
|
83
|
+
owner="${BASH_REMATCH[1]}"
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
# Updated line
|
|
87
|
+
if [[ "$line" =~ ^Updated:[[:space:]]*(.+) ]]; then
|
|
88
|
+
updated="${BASH_REMATCH[1]}"
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# DEPENDS ON section
|
|
92
|
+
if [[ "$line" == "DEPENDS ON" ]]; then
|
|
93
|
+
in_depends=1
|
|
94
|
+
continue
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
if [[ $in_depends -eq 1 && "$line" =~ →.*([a-z]+-[a-z]+):.* ]]; then
|
|
98
|
+
# Extract dependency issue ID
|
|
99
|
+
local dep_id
|
|
100
|
+
dep_id="$(echo "$line" | sed -E 's/.*→ +[^ ]+ +([a-z]+-[a-z0-9]+):.*/\1/')"
|
|
101
|
+
if [[ -n "$blocked_by" ]]; then
|
|
102
|
+
blocked_by="$blocked_by,$dep_id"
|
|
103
|
+
else
|
|
104
|
+
blocked_by="$dep_id"
|
|
105
|
+
fi
|
|
106
|
+
fi
|
|
107
|
+
done <<< "$show_output"
|
|
108
|
+
|
|
109
|
+
local updated_epoch=0
|
|
110
|
+
if [[ -n "$updated" ]]; then
|
|
111
|
+
updated_epoch="$(_iso_to_epoch "$updated")"
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
echo "$issue_id|$title|$status|$owner|$updated_epoch|$blocked_by"
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# ── Dashboard Command ────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
cmd_dashboard() {
|
|
120
|
+
local format="text"
|
|
121
|
+
|
|
122
|
+
# Parse args
|
|
123
|
+
local arg
|
|
124
|
+
for arg in "$@"; do
|
|
125
|
+
case "$arg" in
|
|
126
|
+
--format=json) format="json" ;;
|
|
127
|
+
--format=*) echo "Error: unsupported format '${arg#--format=}'" >&2; return 1 ;;
|
|
128
|
+
esac
|
|
129
|
+
done
|
|
130
|
+
|
|
131
|
+
# 1. Get all open/in_progress issues
|
|
132
|
+
local issue_list
|
|
133
|
+
issue_list="$($BD_CMD list --status=open,in_progress 2>/dev/null)" || issue_list=""
|
|
134
|
+
|
|
135
|
+
# Check for empty
|
|
136
|
+
if [[ -z "$issue_list" ]]; then
|
|
137
|
+
if [[ "$format" == "json" ]]; then
|
|
138
|
+
echo '{"developers":{},"total":0,"stale":[],"blocked":[],"message":"No active issues — team is clear"}'
|
|
139
|
+
else
|
|
140
|
+
echo "No active issues — team is clear"
|
|
141
|
+
fi
|
|
142
|
+
return 0
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
# 2. Parse issue IDs
|
|
146
|
+
local issues=()
|
|
147
|
+
while IFS= read -r entry; do
|
|
148
|
+
[[ -z "$entry" ]] && continue
|
|
149
|
+
issues+=("$entry")
|
|
150
|
+
done <<< "$(echo "$issue_list" | _parse_issue_list)"
|
|
151
|
+
|
|
152
|
+
# 3. Collect details for each issue
|
|
153
|
+
local now_epoch
|
|
154
|
+
now_epoch="$(_now)"
|
|
155
|
+
local stale_threshold=172800 # 48h in seconds
|
|
156
|
+
|
|
157
|
+
# Associative arrays for per-developer aggregation
|
|
158
|
+
declare -A dev_open dev_inprogress dev_blocked dev_stale
|
|
159
|
+
|
|
160
|
+
# Lists for stale and blocked sections
|
|
161
|
+
local stale_entries=()
|
|
162
|
+
local blocked_entries=()
|
|
163
|
+
local total_open=0 total_inprogress=0 total_blocked=0 total_stale=0
|
|
164
|
+
|
|
165
|
+
for entry in "${issues[@]}"; do
|
|
166
|
+
local issue_id="${entry%%|*}"
|
|
167
|
+
|
|
168
|
+
local details
|
|
169
|
+
details="$(_get_issue_details "$issue_id")" || continue
|
|
170
|
+
|
|
171
|
+
IFS='|' read -r _id _title _status _owner _updated_epoch _blocked_by <<< "$details"
|
|
172
|
+
|
|
173
|
+
# Default owner if empty
|
|
174
|
+
[[ -z "$_owner" ]] && _owner="unassigned"
|
|
175
|
+
|
|
176
|
+
# Initialize developer counters
|
|
177
|
+
[[ -z "${dev_open[$_owner]:-}" ]] && dev_open[$_owner]=0
|
|
178
|
+
[[ -z "${dev_inprogress[$_owner]:-}" ]] && dev_inprogress[$_owner]=0
|
|
179
|
+
[[ -z "${dev_blocked[$_owner]:-}" ]] && dev_blocked[$_owner]=0
|
|
180
|
+
[[ -z "${dev_stale[$_owner]:-}" ]] && dev_stale[$_owner]=0
|
|
181
|
+
|
|
182
|
+
# Count by status
|
|
183
|
+
case "$_status" in
|
|
184
|
+
OPEN)
|
|
185
|
+
dev_open[$_owner]=$(( ${dev_open[$_owner]} + 1 ))
|
|
186
|
+
total_open=$((total_open + 1))
|
|
187
|
+
;;
|
|
188
|
+
IN_PROGRESS)
|
|
189
|
+
dev_inprogress[$_owner]=$(( ${dev_inprogress[$_owner]} + 1 ))
|
|
190
|
+
total_inprogress=$((total_inprogress + 1))
|
|
191
|
+
;;
|
|
192
|
+
BLOCKED)
|
|
193
|
+
dev_blocked[$_owner]=$(( ${dev_blocked[$_owner]} + 1 ))
|
|
194
|
+
total_blocked=$((total_blocked + 1))
|
|
195
|
+
;;
|
|
196
|
+
esac
|
|
197
|
+
|
|
198
|
+
# Check for blocked (has DEPENDS ON with open deps)
|
|
199
|
+
if [[ -n "$_blocked_by" ]]; then
|
|
200
|
+
dev_blocked[$_owner]=$(( ${dev_blocked[$_owner]} + 1 ))
|
|
201
|
+
total_blocked=$((total_blocked + 1))
|
|
202
|
+
blocked_entries+=("$_id|$_owner|$_title|$_blocked_by")
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
# Check for stale (>48h since last update)
|
|
206
|
+
if [[ "$_updated_epoch" -gt 0 ]]; then
|
|
207
|
+
local age=$(( now_epoch - _updated_epoch ))
|
|
208
|
+
if [[ $age -gt $stale_threshold ]]; then
|
|
209
|
+
local hours=$(( age / 3600 ))
|
|
210
|
+
dev_stale[$_owner]=$(( ${dev_stale[$_owner]} + 1 ))
|
|
211
|
+
total_stale=$((total_stale + 1))
|
|
212
|
+
stale_entries+=("$_id|$_owner|$_title|${hours}h since last update")
|
|
213
|
+
fi
|
|
214
|
+
fi
|
|
215
|
+
done
|
|
216
|
+
|
|
217
|
+
local total_issues=${#issues[@]}
|
|
218
|
+
|
|
219
|
+
# 4. Output
|
|
220
|
+
if [[ "$format" == "json" ]]; then
|
|
221
|
+
_dashboard_json "$total_issues"
|
|
222
|
+
else
|
|
223
|
+
_dashboard_text "$total_issues"
|
|
224
|
+
fi
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
# ── Text Output ──────────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
_dashboard_text() {
|
|
230
|
+
local total_issues="$1"
|
|
231
|
+
|
|
232
|
+
echo "Team Dashboard"
|
|
233
|
+
echo "══════════════"
|
|
234
|
+
echo ""
|
|
235
|
+
|
|
236
|
+
# Header
|
|
237
|
+
printf "%-16s %4s %11s %7s %5s\n" "Developer" "Open" "In Progress" "Blocked" "Stale"
|
|
238
|
+
|
|
239
|
+
# Get sorted list of developers
|
|
240
|
+
local devs=()
|
|
241
|
+
local dev
|
|
242
|
+
for dev in "${!dev_open[@]}"; do
|
|
243
|
+
devs+=("$dev")
|
|
244
|
+
done
|
|
245
|
+
# Sort developers
|
|
246
|
+
IFS=$'\n' devs=($(sort <<< "${devs[*]}")); unset IFS
|
|
247
|
+
|
|
248
|
+
for dev in "${devs[@]}"; do
|
|
249
|
+
printf "%-16s %4d %11d %7d %5d\n" \
|
|
250
|
+
"$dev" \
|
|
251
|
+
"${dev_open[$dev]:-0}" \
|
|
252
|
+
"${dev_inprogress[$dev]:-0}" \
|
|
253
|
+
"${dev_blocked[$dev]:-0}" \
|
|
254
|
+
"${dev_stale[$dev]:-0}"
|
|
255
|
+
done
|
|
256
|
+
|
|
257
|
+
echo ""
|
|
258
|
+
|
|
259
|
+
# Build total summary parts
|
|
260
|
+
local parts=()
|
|
261
|
+
[[ $total_inprogress -gt 0 ]] && parts+=("$total_inprogress in progress")
|
|
262
|
+
[[ $total_blocked -gt 0 ]] && parts+=("$total_blocked blocked")
|
|
263
|
+
[[ $total_stale -gt 0 ]] && parts+=("$total_stale stale")
|
|
264
|
+
[[ $total_open -gt 0 ]] && parts+=("$total_open open")
|
|
265
|
+
|
|
266
|
+
local summary=""
|
|
267
|
+
if [[ ${#parts[@]} -gt 0 ]]; then
|
|
268
|
+
summary=" ($(IFS=', '; echo "${parts[*]}"))"
|
|
269
|
+
fi
|
|
270
|
+
echo "Total: $total_issues issues${summary}"
|
|
271
|
+
|
|
272
|
+
# Stale section
|
|
273
|
+
if [[ ${#stale_entries[@]} -gt 0 ]]; then
|
|
274
|
+
echo ""
|
|
275
|
+
echo "Stale assignments (>48h):"
|
|
276
|
+
local entry
|
|
277
|
+
for entry in "${stale_entries[@]}"; do
|
|
278
|
+
IFS='|' read -r _id _owner _title _age <<< "$entry"
|
|
279
|
+
echo " ⚠ $_id [$_owner] — $_title ($_age)"
|
|
280
|
+
done
|
|
281
|
+
fi
|
|
282
|
+
|
|
283
|
+
# Blocked section
|
|
284
|
+
if [[ ${#blocked_entries[@]} -gt 0 ]]; then
|
|
285
|
+
echo ""
|
|
286
|
+
echo "Blocked issues:"
|
|
287
|
+
local entry
|
|
288
|
+
for entry in "${blocked_entries[@]}"; do
|
|
289
|
+
IFS='|' read -r _id _owner _title _deps <<< "$entry"
|
|
290
|
+
echo " ⚠ $_id [$_owner] — $_title (blocked by $_deps)"
|
|
291
|
+
done
|
|
292
|
+
fi
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
# ── JSON Output ──────────────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
_dashboard_json() {
|
|
298
|
+
local total_issues="$1"
|
|
299
|
+
|
|
300
|
+
# Build developers JSON
|
|
301
|
+
local dev_json="{"
|
|
302
|
+
local first=1
|
|
303
|
+
local devs=()
|
|
304
|
+
local dev
|
|
305
|
+
for dev in "${!dev_open[@]}"; do
|
|
306
|
+
devs+=("$dev")
|
|
307
|
+
done
|
|
308
|
+
IFS=$'\n' devs=($(sort <<< "${devs[*]}")); unset IFS
|
|
309
|
+
|
|
310
|
+
for dev in "${devs[@]}"; do
|
|
311
|
+
[[ $first -eq 0 ]] && dev_json+=","
|
|
312
|
+
first=0
|
|
313
|
+
dev_json+="\"$dev\":{\"open\":${dev_open[$dev]:-0},\"in_progress\":${dev_inprogress[$dev]:-0},\"blocked\":${dev_blocked[$dev]:-0},\"stale\":${dev_stale[$dev]:-0}}"
|
|
314
|
+
done
|
|
315
|
+
dev_json+="}"
|
|
316
|
+
|
|
317
|
+
# Build stale JSON array
|
|
318
|
+
local stale_json="["
|
|
319
|
+
first=1
|
|
320
|
+
local entry
|
|
321
|
+
for entry in "${stale_entries[@]}"; do
|
|
322
|
+
[[ $first -eq 0 ]] && stale_json+=","
|
|
323
|
+
first=0
|
|
324
|
+
IFS='|' read -r _id _owner _title _age <<< "$entry"
|
|
325
|
+
stale_json+="$(jq -n -c --arg id "$_id" --arg owner "$_owner" --arg title "$_title" --arg age "$_age" '{id:$id,owner:$owner,title:$title,age:$age}')"
|
|
326
|
+
done
|
|
327
|
+
stale_json+="]"
|
|
328
|
+
|
|
329
|
+
# Build blocked JSON array
|
|
330
|
+
local blocked_json="["
|
|
331
|
+
first=1
|
|
332
|
+
for entry in "${blocked_entries[@]}"; do
|
|
333
|
+
[[ $first -eq 0 ]] && blocked_json+=","
|
|
334
|
+
first=0
|
|
335
|
+
IFS='|' read -r _id _owner _title _deps <<< "$entry"
|
|
336
|
+
blocked_json+="$(jq -n -c --arg id "$_id" --arg owner "$_owner" --arg title "$_title" --arg deps "$_deps" '{id:$id,owner:$owner,title:$title,blocked_by:$deps}')"
|
|
337
|
+
done
|
|
338
|
+
blocked_json+="]"
|
|
339
|
+
|
|
340
|
+
echo "{\"developers\":$dev_json,\"total\":$total_issues,\"in_progress\":$total_inprogress,\"blocked\":$total_blocked,\"stale\":$total_stale,\"stale_entries\":$stale_json,\"blocked_entries\":$blocked_json}"
|
|
341
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/forge-team/lib/epic.sh — Epic rollup view
|
|
3
|
+
#
|
|
4
|
+
# Shows epic progress with per-developer breakdown and blocked issue tracking.
|
|
5
|
+
#
|
|
6
|
+
# Functions:
|
|
7
|
+
# cmd_epic — Top-level dispatcher entry point
|
|
8
|
+
#
|
|
9
|
+
# Env overrides (for testing):
|
|
10
|
+
# BD_CMD — Path to bd binary
|
|
11
|
+
#
|
|
12
|
+
# This file does NOT set errexit/pipefail — callers manage their own shell options.
|
|
13
|
+
|
|
14
|
+
# Guard against double-sourcing
|
|
15
|
+
if [[ -n "${_EPIC_LIB_LOADED:-}" ]]; then
|
|
16
|
+
return 0 2>/dev/null || true
|
|
17
|
+
fi
|
|
18
|
+
_EPIC_LIB_LOADED=1
|
|
19
|
+
|
|
20
|
+
# ── Internal helpers ─────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
# _epic_get_bd — Returns the bd command path
|
|
23
|
+
_epic_get_bd() {
|
|
24
|
+
printf '%s' "${BD_CMD:-bd}"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# _epic_parse_blocks <bd-show-output>
|
|
28
|
+
# Extracts child issue IDs from the BLOCKS section.
|
|
29
|
+
# Each line in BLOCKS looks like: ← ✓ forge-child1: Child 1 (closed)
|
|
30
|
+
# Returns one ID per line.
|
|
31
|
+
_epic_parse_blocks() {
|
|
32
|
+
local bd_output="$1"
|
|
33
|
+
local in_blocks=0
|
|
34
|
+
|
|
35
|
+
while IFS= read -r line; do
|
|
36
|
+
if [[ "$line" == "BLOCKS" ]]; then
|
|
37
|
+
in_blocks=1
|
|
38
|
+
continue
|
|
39
|
+
fi
|
|
40
|
+
# Stop at next section header (non-indented, non-empty line after BLOCKS)
|
|
41
|
+
if [[ $in_blocks -eq 1 ]]; then
|
|
42
|
+
if [[ -z "$line" ]]; then
|
|
43
|
+
continue
|
|
44
|
+
fi
|
|
45
|
+
if [[ "$line" =~ ^[A-Z] ]]; then
|
|
46
|
+
break
|
|
47
|
+
fi
|
|
48
|
+
# Extract issue ID: matches forge-XXXX pattern after the arrow+status marker
|
|
49
|
+
local child_id
|
|
50
|
+
child_id="$(printf '%s' "$line" | sed -n 's/.*← [^ ]* \([a-z0-9-]*\):.*/\1/p')"
|
|
51
|
+
if [[ -n "$child_id" ]]; then
|
|
52
|
+
printf '%s\n' "$child_id"
|
|
53
|
+
fi
|
|
54
|
+
fi
|
|
55
|
+
done <<< "$bd_output"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# _epic_get_child_info <child-id>
|
|
59
|
+
# Returns: id|status|owner|title|blocked_by
|
|
60
|
+
_epic_get_child_info() {
|
|
61
|
+
local child_id="$1"
|
|
62
|
+
local bd_cmd
|
|
63
|
+
bd_cmd="$(_epic_get_bd)"
|
|
64
|
+
|
|
65
|
+
local child_output
|
|
66
|
+
child_output="$("$bd_cmd" show "$child_id" 2>/dev/null)" || return 1
|
|
67
|
+
|
|
68
|
+
local first_line
|
|
69
|
+
first_line="$(printf '%s' "$child_output" | head -1)"
|
|
70
|
+
|
|
71
|
+
# Parse status from the first character/marker
|
|
72
|
+
local status="open"
|
|
73
|
+
if [[ "$first_line" == *"CLOSED"* ]] || [[ "$first_line" == *"✓"* ]]; then
|
|
74
|
+
status="closed"
|
|
75
|
+
elif [[ "$first_line" == *"IN_PROGRESS"* ]] || [[ "$first_line" == *"◐"* ]]; then
|
|
76
|
+
status="in_progress"
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# Parse title — between · and [
|
|
80
|
+
local title
|
|
81
|
+
title="$(printf '%s' "$first_line" | sed -n 's/^[^ ]* [^ ]* · \(.*\) \[.*/\1/p')"
|
|
82
|
+
if [[ -z "$title" ]]; then
|
|
83
|
+
title="$child_id"
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
# Parse owner from "Owner: xxx" line
|
|
87
|
+
local owner=""
|
|
88
|
+
owner="$(printf '%s' "$child_output" | sed -n 's/^Owner: *//p' | head -1)"
|
|
89
|
+
|
|
90
|
+
# Check for BLOCKED_BY section
|
|
91
|
+
local blocked_by=""
|
|
92
|
+
blocked_by="$(printf '%s' "$child_output" | sed -n '/^BLOCKED_BY$/,/^[A-Z]/{ /^ /{ s/.*→ \([a-z0-9-]*\):.*/\1/p; }; }')"
|
|
93
|
+
|
|
94
|
+
printf '%s|%s|%s|%s|%s' "$child_id" "$status" "$owner" "$title" "$blocked_by"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# _epic_status_icon <status>
|
|
98
|
+
_epic_status_icon() {
|
|
99
|
+
case "$1" in
|
|
100
|
+
closed) printf '%s' "✓" ;;
|
|
101
|
+
in_progress) printf '%s' "◐" ;;
|
|
102
|
+
*) printf '%s' "○" ;;
|
|
103
|
+
esac
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# ── Public API ───────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
# cmd_epic <issue-id> [--format=json]
|
|
109
|
+
cmd_epic() {
|
|
110
|
+
local issue_id="${1:-}"
|
|
111
|
+
local format_flag="${2:-}"
|
|
112
|
+
|
|
113
|
+
if [[ -z "$issue_id" ]]; then
|
|
114
|
+
echo "ERROR: Usage: forge team epic <issue-id> [--format=json]" >&2
|
|
115
|
+
return 1
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
local bd_cmd
|
|
119
|
+
bd_cmd="$(_epic_get_bd)"
|
|
120
|
+
|
|
121
|
+
# 1. Get epic details
|
|
122
|
+
local epic_output
|
|
123
|
+
epic_output="$("$bd_cmd" show "$issue_id" 2>/dev/null)" || {
|
|
124
|
+
echo "ERROR: Could not fetch epic $issue_id" >&2
|
|
125
|
+
return 1
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# Parse epic title from first line
|
|
129
|
+
local epic_first_line
|
|
130
|
+
epic_first_line="$(printf '%s' "$epic_output" | head -1)"
|
|
131
|
+
local epic_title
|
|
132
|
+
epic_title="$(printf '%s' "$epic_first_line" | sed -n 's/^[^ ]* [^ ]* · \(.*\) \[.*/\1/p')"
|
|
133
|
+
if [[ -z "$epic_title" ]]; then
|
|
134
|
+
epic_title="$issue_id"
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
# 2. Parse BLOCKS section to find child issues
|
|
138
|
+
local children_ids
|
|
139
|
+
children_ids="$(_epic_parse_blocks "$epic_output")"
|
|
140
|
+
|
|
141
|
+
# Handle empty epic
|
|
142
|
+
if [[ -z "$children_ids" ]]; then
|
|
143
|
+
if [[ "$format_flag" == "--format=json" ]]; then
|
|
144
|
+
cat <<ENDJSON
|
|
145
|
+
{"epic_id":"$issue_id","title":"$epic_title","total":0,"done":0,"in_progress":0,"open_count":0,"percentage":0,"children":[],"by_developer":{},"blocked":[]}
|
|
146
|
+
ENDJSON
|
|
147
|
+
return 0
|
|
148
|
+
fi
|
|
149
|
+
echo "Epic: $issue_id — $epic_title"
|
|
150
|
+
echo "No child issues"
|
|
151
|
+
return 0
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
# 3. For each child: get status, assignee
|
|
155
|
+
local total=0
|
|
156
|
+
local done_count=0
|
|
157
|
+
local in_progress_count=0
|
|
158
|
+
local open_count=0
|
|
159
|
+
local blocked_list=""
|
|
160
|
+
|
|
161
|
+
# Arrays for child data (using indexed approach for bash 3 compat)
|
|
162
|
+
local child_lines=""
|
|
163
|
+
# Developer tracking: accumulate "dev:status" pairs
|
|
164
|
+
local dev_entries=""
|
|
165
|
+
|
|
166
|
+
while IFS= read -r child_id; do
|
|
167
|
+
[[ -z "$child_id" ]] && continue
|
|
168
|
+
|
|
169
|
+
local info
|
|
170
|
+
info="$(_epic_get_child_info "$child_id")" || continue
|
|
171
|
+
|
|
172
|
+
local c_id c_status c_owner c_title c_blocked
|
|
173
|
+
IFS='|' read -r c_id c_status c_owner c_title c_blocked <<< "$info"
|
|
174
|
+
|
|
175
|
+
total=$((total + 1))
|
|
176
|
+
|
|
177
|
+
case "$c_status" in
|
|
178
|
+
closed) done_count=$((done_count + 1)) ;;
|
|
179
|
+
in_progress) in_progress_count=$((in_progress_count + 1)) ;;
|
|
180
|
+
*) open_count=$((open_count + 1)) ;;
|
|
181
|
+
esac
|
|
182
|
+
|
|
183
|
+
# Track blocked children
|
|
184
|
+
if [[ -n "$c_blocked" ]]; then
|
|
185
|
+
if [[ -n "$blocked_list" ]]; then
|
|
186
|
+
blocked_list="$blocked_list"$'\n'"$c_id blocked by $c_blocked"
|
|
187
|
+
else
|
|
188
|
+
blocked_list="$c_id blocked by $c_blocked"
|
|
189
|
+
fi
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
# Build child line for display
|
|
193
|
+
local icon
|
|
194
|
+
icon="$(_epic_status_icon "$c_status")"
|
|
195
|
+
local display_owner="${c_owner:-unassigned}"
|
|
196
|
+
child_lines="${child_lines} $icon $c_id [$display_owner] $c_title ($c_status)"$'\n'
|
|
197
|
+
|
|
198
|
+
# Track developer entries
|
|
199
|
+
if [[ -n "$c_owner" ]]; then
|
|
200
|
+
dev_entries="${dev_entries}${c_owner}:${c_status}"$'\n'
|
|
201
|
+
fi
|
|
202
|
+
done <<< "$children_ids"
|
|
203
|
+
|
|
204
|
+
# 4. Calculate completion percentage
|
|
205
|
+
local percentage=0
|
|
206
|
+
if [[ $total -gt 0 ]]; then
|
|
207
|
+
percentage=$(( (done_count * 100) / total ))
|
|
208
|
+
fi
|
|
209
|
+
|
|
210
|
+
# 5. Build per-developer breakdown
|
|
211
|
+
# Collect unique developers
|
|
212
|
+
local devs=""
|
|
213
|
+
while IFS= read -r entry; do
|
|
214
|
+
[[ -z "$entry" ]] && continue
|
|
215
|
+
local dev="${entry%%:*}"
|
|
216
|
+
if [[ -z "$devs" ]] || ! printf '%s' "$devs" | grep -qxF "$dev"; then
|
|
217
|
+
if [[ -n "$devs" ]]; then
|
|
218
|
+
devs="$devs"$'\n'"$dev"
|
|
219
|
+
else
|
|
220
|
+
devs="$dev"
|
|
221
|
+
fi
|
|
222
|
+
fi
|
|
223
|
+
done <<< "$dev_entries"
|
|
224
|
+
|
|
225
|
+
# JSON output
|
|
226
|
+
if [[ "$format_flag" == "--format=json" ]]; then
|
|
227
|
+
local children_json="["
|
|
228
|
+
local first_child=1
|
|
229
|
+
while IFS= read -r child_id; do
|
|
230
|
+
[[ -z "$child_id" ]] && continue
|
|
231
|
+
local info
|
|
232
|
+
info="$(_epic_get_child_info "$child_id")" || continue
|
|
233
|
+
local c_id c_status c_owner c_title c_blocked
|
|
234
|
+
IFS='|' read -r c_id c_status c_owner c_title c_blocked <<< "$info"
|
|
235
|
+
|
|
236
|
+
if [[ $first_child -eq 0 ]]; then
|
|
237
|
+
children_json="$children_json,"
|
|
238
|
+
fi
|
|
239
|
+
first_child=0
|
|
240
|
+
children_json="$children_json{\"id\":\"$c_id\",\"status\":\"$c_status\",\"owner\":\"$c_owner\",\"title\":\"$c_title\",\"blocked_by\":\"$c_blocked\"}"
|
|
241
|
+
done <<< "$children_ids"
|
|
242
|
+
children_json="$children_json]"
|
|
243
|
+
|
|
244
|
+
local dev_json="{"
|
|
245
|
+
local first_dev=1
|
|
246
|
+
while IFS= read -r dev; do
|
|
247
|
+
[[ -z "$dev" ]] && continue
|
|
248
|
+
local d_done=0 d_in_progress=0 d_open=0 d_total=0
|
|
249
|
+
while IFS= read -r entry; do
|
|
250
|
+
[[ -z "$entry" ]] && continue
|
|
251
|
+
local e_dev="${entry%%:*}"
|
|
252
|
+
local e_status="${entry#*:}"
|
|
253
|
+
if [[ "$e_dev" == "$dev" ]]; then
|
|
254
|
+
d_total=$((d_total + 1))
|
|
255
|
+
case "$e_status" in
|
|
256
|
+
closed) d_done=$((d_done + 1)) ;;
|
|
257
|
+
in_progress) d_in_progress=$((d_in_progress + 1)) ;;
|
|
258
|
+
*) d_open=$((d_open + 1)) ;;
|
|
259
|
+
esac
|
|
260
|
+
fi
|
|
261
|
+
done <<< "$dev_entries"
|
|
262
|
+
|
|
263
|
+
if [[ $first_dev -eq 0 ]]; then
|
|
264
|
+
dev_json="$dev_json,"
|
|
265
|
+
fi
|
|
266
|
+
first_dev=0
|
|
267
|
+
dev_json="$dev_json\"$dev\":{\"total\":$d_total,\"done\":$d_done,\"in_progress\":$d_in_progress,\"open\":$d_open}"
|
|
268
|
+
done <<< "$devs"
|
|
269
|
+
dev_json="$dev_json}"
|
|
270
|
+
|
|
271
|
+
local blocked_json="["
|
|
272
|
+
if [[ -n "$blocked_list" ]]; then
|
|
273
|
+
local first_blocked=1
|
|
274
|
+
while IFS= read -r bline; do
|
|
275
|
+
[[ -z "$bline" ]] && continue
|
|
276
|
+
if [[ $first_blocked -eq 0 ]]; then
|
|
277
|
+
blocked_json="$blocked_json,"
|
|
278
|
+
fi
|
|
279
|
+
first_blocked=0
|
|
280
|
+
blocked_json="$blocked_json\"$bline\""
|
|
281
|
+
done <<< "$blocked_list"
|
|
282
|
+
fi
|
|
283
|
+
blocked_json="$blocked_json]"
|
|
284
|
+
|
|
285
|
+
printf '{"epic_id":"%s","title":"%s","total":%d,"done":%d,"in_progress":%d,"open_count":%d,"percentage":%d,"children":%s,"by_developer":%s,"blocked":%s}\n' \
|
|
286
|
+
"$issue_id" "$epic_title" "$total" "$done_count" "$in_progress_count" "$open_count" "$percentage" \
|
|
287
|
+
"$children_json" "$dev_json" "$blocked_json"
|
|
288
|
+
return 0
|
|
289
|
+
fi
|
|
290
|
+
|
|
291
|
+
# Text output
|
|
292
|
+
echo "Epic: $issue_id — $epic_title"
|
|
293
|
+
echo "Progress: $done_count/$total ($percentage%)"
|
|
294
|
+
printf '%s' "$child_lines"
|
|
295
|
+
|
|
296
|
+
echo ""
|
|
297
|
+
echo "By developer:"
|
|
298
|
+
while IFS= read -r dev; do
|
|
299
|
+
[[ -z "$dev" ]] && continue
|
|
300
|
+
local d_done=0 d_in_progress=0 d_open=0 d_total=0
|
|
301
|
+
while IFS= read -r entry; do
|
|
302
|
+
[[ -z "$entry" ]] && continue
|
|
303
|
+
local e_dev="${entry%%:*}"
|
|
304
|
+
local e_status="${entry#*:}"
|
|
305
|
+
if [[ "$e_dev" == "$dev" ]]; then
|
|
306
|
+
d_total=$((d_total + 1))
|
|
307
|
+
case "$e_status" in
|
|
308
|
+
closed) d_done=$((d_done + 1)) ;;
|
|
309
|
+
in_progress) d_in_progress=$((d_in_progress + 1)) ;;
|
|
310
|
+
*) d_open=$((d_open + 1)) ;;
|
|
311
|
+
esac
|
|
312
|
+
fi
|
|
313
|
+
done <<< "$dev_entries"
|
|
314
|
+
|
|
315
|
+
local dev_parts=""
|
|
316
|
+
dev_parts="$d_done/$d_total done"
|
|
317
|
+
[[ $d_in_progress -gt 0 ]] && dev_parts="$dev_parts, $d_in_progress in progress"
|
|
318
|
+
[[ $d_open -gt 0 ]] && dev_parts="$dev_parts, $d_open open"
|
|
319
|
+
echo " $dev: $dev_parts"
|
|
320
|
+
done <<< "$devs"
|
|
321
|
+
|
|
322
|
+
echo ""
|
|
323
|
+
if [[ -n "$blocked_list" ]]; then
|
|
324
|
+
echo "Blocked:"
|
|
325
|
+
while IFS= read -r bline; do
|
|
326
|
+
[[ -z "$bline" ]] && continue
|
|
327
|
+
echo " $bline"
|
|
328
|
+
done <<< "$blocked_list"
|
|
329
|
+
else
|
|
330
|
+
echo "Blocked: none"
|
|
331
|
+
fi
|
|
332
|
+
}
|