forge-workflow 0.1.0-beta.2 → 0.1.0-beta.4
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/.forge/hooks/check-tdd.js +79 -5
- package/.forge/hooks/forge-native-hook.js +194 -8
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +28 -0
- package/QUICKSTART.md +6 -2
- package/README.md +3 -1
- package/bin/forge-cmd.js +1 -1
- package/bin/forge.js +95 -19
- package/docs/guides/SETUP.md +4 -1
- package/docs/guides/SUPPORT.md +5 -0
- package/docs/reference/COMMANDS.md +12 -1
- package/docs/reference/shepherd.md +114 -3
- package/lib/activation/ensure-forge-home.js +135 -0
- package/lib/adapters/beads-kernel-compat.js +67 -0
- package/lib/adapters/greptile-review-adapter.js +1 -1
- package/lib/adapters/pr-state-adapter.js +103 -8
- package/lib/adoption-profiles.js +17 -4
- package/lib/agents-config.js +5 -0
- package/lib/beads-detect.js +60 -0
- package/lib/commands/_aliases.js +248 -0
- package/lib/commands/_issue.js +65 -41
- package/lib/commands/_manifest.js +3 -1
- package/lib/commands/_registry.js +14 -0
- package/lib/commands/_resolve-command-opts.js +36 -60
- package/lib/commands/claim.js +2 -4
- package/lib/commands/gate.js +19 -2
- package/lib/commands/hooks.js +294 -12
- package/lib/commands/init.js +26 -20
- package/lib/commands/memory.js +81 -0
- package/lib/commands/migrate.js +0 -161
- package/lib/commands/plan.js +71 -123
- package/lib/commands/pr.js +88 -0
- package/lib/commands/prime.js +8 -1
- package/lib/commands/push.js +66 -0
- package/lib/commands/recall.js +67 -12
- package/lib/commands/recap.js +18 -4
- package/lib/commands/release.js +15 -3
- package/lib/commands/remember.js +86 -20
- package/lib/commands/serve.js +5 -2
- package/lib/commands/setup.js +135 -73
- package/lib/commands/shepherd.js +102 -4
- package/lib/commands/ship.js +40 -4
- package/lib/commands/skill.js +275 -0
- package/lib/commands/status.js +37 -32
- package/lib/commands/test.js +32 -0
- package/lib/commands/worktree.js +87 -5
- package/lib/core/runtime-graph.js +122 -17
- package/lib/forge-issues.js +13 -464
- package/lib/gate-events.js +54 -55
- package/lib/global-flags.js +30 -0
- package/lib/grounding/context-events.js +230 -0
- package/lib/grounding/read-first.js +112 -0
- package/lib/harness-capability-matrix.js +2 -2
- package/lib/hook-renderer.js +144 -12
- package/lib/issue-backend.js +42 -3
- package/lib/kernel/backing-issue.js +7 -1
- package/lib/kernel/broker.js +28 -0
- package/lib/kernel/migrations.js +30 -2
- package/lib/kernel/owned-kernel.js +43 -0
- package/lib/kernel/schema.js +35 -0
- package/lib/kernel/sqlite-driver.js +187 -1
- package/lib/memory-recall.js +151 -0
- package/lib/orientation.js +303 -6
- package/lib/pr-bundle.js +6 -2
- package/lib/pr-monitor/auto-actions.js +175 -0
- package/lib/pr-monitor/digest.js +206 -0
- package/lib/pr-monitor/reconcile-executor.js +659 -0
- package/lib/pr-monitor/reconcile-tick.js +138 -0
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-sticky.js +57 -8
- package/lib/pr-monitor/shepherd-lease.js +243 -0
- package/lib/pr-monitor/upsert-sticky.js +169 -0
- package/lib/pr-monitor/watch-lifecycle.js +1 -1
- package/lib/pr-pull.js +113 -17
- package/lib/project-memory.js +8 -0
- package/lib/release-readiness.js +17 -1
- package/lib/rules-sync.js +4 -0
- package/lib/runtime-health.js +15 -46
- package/lib/skill-eval.js +750 -0
- package/lib/status/identity.js +46 -0
- package/lib/status/presenter.js +0 -35
- package/lib/status/snapshot.js +11 -16
- package/lib/upgrade-safety.js +52 -1
- package/lib/using-forge.js +315 -0
- package/lib/workflow/enforce-stage.js +64 -7
- package/lib/workflow/state-manager.js +23 -23
- package/package.json +2 -2
- package/rules/using-forge.md +24 -0
- package/scripts/forge-team/index.sh +0 -5
- package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
- package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
- package/scripts/pr-auto-actions.js +93 -0
- package/scripts/pr-verdict-label.js +50 -0
- package/scripts/test.js +8 -1
- package/skills/claim-safety/SKILL.md +4 -0
- package/skills/claim-safety/evals/scorecard.json +41 -0
- package/skills/coverage.json +83 -0
- package/skills/dev/SKILL.md +4 -0
- package/skills/dev/evals/scorecard.json +41 -0
- package/skills/gates/SKILL.md +80 -0
- package/skills/gates/evals/evals.json +38 -0
- package/skills/gates/evals/scorecard.json +41 -0
- package/skills/hermes-forge/SKILL.md +1 -0
- package/skills/hermes-forge/evals/scorecard.json +41 -0
- package/skills/issue-basics/SKILL.md +1 -0
- package/skills/issue-basics/evals/scorecard.json +41 -0
- package/skills/kernel/SKILL.md +38 -0
- package/skills/kernel/evals/scorecard.json +41 -0
- package/skills/memory/SKILL.md +16 -1
- package/skills/memory/evals/scorecard.json +41 -0
- package/skills/parallel-deep-research/SKILL.md +1 -0
- package/skills/parallel-deep-research/evals/scorecard.json +41 -0
- package/skills/plan/SKILL.md +6 -0
- package/skills/plan/evals/scorecard.json +41 -0
- package/skills/portability/SKILL.md +47 -0
- package/skills/portability/evals/evals.json +34 -0
- package/skills/portability/evals/scorecard.json +41 -0
- package/skills/research/SKILL.md +1 -0
- package/skills/research/evals/scorecard.json +41 -0
- package/skills/review/SKILL.md +6 -0
- package/skills/review/evals/scorecard.json +41 -0
- package/skills/rollback/SKILL.md +1 -0
- package/skills/rollback/evals/scorecard.json +41 -0
- package/skills/setup/SKILL.md +91 -0
- package/skills/setup/evals/evals.json +42 -0
- package/skills/setup/evals/scorecard.json +41 -0
- package/skills/shepherd/SKILL.md +76 -36
- package/skills/shepherd/evals/evals.json +21 -9
- package/skills/shepherd/evals/scorecard.json +41 -0
- package/skills/ship/SKILL.md +6 -0
- package/skills/ship/evals/scorecard.json +41 -0
- package/skills/smith/SKILL.md +8 -0
- package/skills/smith/evals/scorecard.json +41 -0
- package/skills/sonarcloud/SKILL.md +1 -0
- package/skills/sonarcloud/evals/scorecard.json +41 -0
- package/skills/sonarcloud-analysis/SKILL.md +1 -0
- package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
- package/skills/status/SKILL.md +3 -0
- package/skills/status/evals/scorecard.json +41 -0
- package/skills/triage-ready/SKILL.md +2 -0
- package/skills/triage-ready/evals/scorecard.json +41 -0
- package/skills/using-forge/SKILL.md +104 -0
- package/skills/using-forge/evals/scorecard.json +41 -0
- package/skills/validate/SKILL.md +4 -0
- package/skills/validate/evals/scorecard.json +41 -0
- package/skills/verify/SKILL.md +4 -0
- package/skills/verify/evals/scorecard.json +41 -0
- package/skills/worktree/SKILL.md +87 -0
- package/skills/worktree/evals/evals.json +38 -0
- package/skills/worktree/evals/scorecard.json +41 -0
- package/lib/adapters/beads-issue-adapter.js +0 -127
- package/lib/commands/board.js +0 -64
- package/lib/status/beads-snapshot.js +0 -145
- package/scripts/forge-team/lib/dashboard.sh +0 -316
- package/scripts/forge-team/tests/dashboard.test.sh +0 -155
|
@@ -1,316 +0,0 @@
|
|
|
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 the Forge issue backend and displays per-developer stats, stale
|
|
6
|
-
# assignments, and blocked issues.
|
|
7
|
-
|
|
8
|
-
# ── Dependencies ─────────────────────────────────────────────────────────
|
|
9
|
-
# FORGE_CMD — path to forge binary (default: forge)
|
|
10
|
-
FORGE_CMD="${FORGE_CMD:-forge}"
|
|
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
|
-
# _forge_active_issues_json — Merged JSON array of open + in_progress issues.
|
|
49
|
-
# The Kernel list filter does not accept comma-joined statuses, so query each
|
|
50
|
-
# status separately and concatenate the issue arrays.
|
|
51
|
-
_forge_active_issues_json() {
|
|
52
|
-
local open_json inprog_json
|
|
53
|
-
open_json="$("$FORGE_CMD" issue list --status=open --json 2>/dev/null)" || open_json=""
|
|
54
|
-
inprog_json="$("$FORGE_CMD" issue list --status=in_progress --json 2>/dev/null)" || inprog_json=""
|
|
55
|
-
jq -n \
|
|
56
|
-
--argjson a "${open_json:-null}" \
|
|
57
|
-
--argjson b "${inprog_json:-null}" \
|
|
58
|
-
'(((($a.data.issues) // []) + (($b.data.issues) // [])))' 2>/dev/null || echo "[]"
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
# _issue_detail_lines <issues-json>
|
|
62
|
-
# Emit one tab-delimited (jq @tsv) line per issue:
|
|
63
|
-
# id<TAB>title<TAB>STATUS<TAB>owner<TAB>updated_iso<TAB>blocked_by
|
|
64
|
-
# @tsv escapes any tab/newline/CR inside fields (issue titles are user-controlled
|
|
65
|
-
# and may contain '|', tabs, or newlines), so parsing stays field-aligned.
|
|
66
|
-
# STATUS is the upper-cased Kernel status; owner is assignee/claimed_by.
|
|
67
|
-
# A Kernel issue's `.dependencies` lists the issues it BLOCKS (outgoing edges),
|
|
68
|
-
# so blocked_by is computed by reverse scan: the issues (within this active set)
|
|
69
|
-
# whose `.dependencies` include this issue.
|
|
70
|
-
_issue_detail_lines() {
|
|
71
|
-
printf '%s' "$1" | jq -r '
|
|
72
|
-
. as $all
|
|
73
|
-
| $all[]
|
|
74
|
-
| . as $x
|
|
75
|
-
| [ $x.id,
|
|
76
|
-
($x.title // ""),
|
|
77
|
-
(($x.status // "open") | ascii_upcase),
|
|
78
|
-
($x.assignee // $x.claimed_by // ""),
|
|
79
|
-
(($x.updated_at // "") | sub("\\.[0-9]+Z$"; "Z")),
|
|
80
|
-
( [ $all[]
|
|
81
|
-
| select( (.dependencies // [])
|
|
82
|
-
| map(if type == "object" then (.id // .to // "") else . end)
|
|
83
|
-
| any(. == $x.id) )
|
|
84
|
-
| .id ]
|
|
85
|
-
| join(",") )
|
|
86
|
-
]
|
|
87
|
-
| @tsv' | tr -d '\r'
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
# ── Dashboard Command ────────────────────────────────────────────────────
|
|
91
|
-
|
|
92
|
-
cmd_dashboard() {
|
|
93
|
-
local format="text"
|
|
94
|
-
|
|
95
|
-
# Parse args
|
|
96
|
-
local arg
|
|
97
|
-
for arg in "$@"; do
|
|
98
|
-
case "$arg" in
|
|
99
|
-
--format=json) format="json" ;;
|
|
100
|
-
--format=*) echo "Error: unsupported format '${arg#--format=}'" >&2; return 1 ;;
|
|
101
|
-
esac
|
|
102
|
-
done
|
|
103
|
-
|
|
104
|
-
# 1. Get all open/in_progress issues as a merged JSON array
|
|
105
|
-
local issues_json
|
|
106
|
-
issues_json="$(_forge_active_issues_json)"
|
|
107
|
-
|
|
108
|
-
# Check for empty
|
|
109
|
-
local active_count
|
|
110
|
-
active_count="$(printf '%s' "$issues_json" | jq 'length' 2>/dev/null || echo 0)"
|
|
111
|
-
if [[ "$active_count" -eq 0 ]]; then
|
|
112
|
-
if [[ "$format" == "json" ]]; then
|
|
113
|
-
echo '{"developers":{},"total":0,"stale":[],"blocked":[],"message":"No active issues — team is clear"}'
|
|
114
|
-
else
|
|
115
|
-
echo "No active issues — team is clear"
|
|
116
|
-
fi
|
|
117
|
-
return 0
|
|
118
|
-
fi
|
|
119
|
-
|
|
120
|
-
# 2. Build per-issue detail lines: id|title|STATUS|owner|updated_iso|blocked_by
|
|
121
|
-
local issues=()
|
|
122
|
-
while IFS= read -r entry; do
|
|
123
|
-
[[ -z "$entry" ]] && continue
|
|
124
|
-
issues+=("$entry")
|
|
125
|
-
done <<< "$(_issue_detail_lines "$issues_json")"
|
|
126
|
-
|
|
127
|
-
# 3. Collect details for each issue
|
|
128
|
-
local now_epoch
|
|
129
|
-
now_epoch="$(_now)"
|
|
130
|
-
local stale_threshold=172800 # 48h in seconds
|
|
131
|
-
|
|
132
|
-
# Associative arrays for per-developer aggregation
|
|
133
|
-
declare -A dev_open dev_inprogress dev_blocked dev_stale
|
|
134
|
-
|
|
135
|
-
# Lists for stale and blocked sections
|
|
136
|
-
local stale_entries=()
|
|
137
|
-
local blocked_entries=()
|
|
138
|
-
local total_open=0 total_inprogress=0 total_blocked=0 total_stale=0
|
|
139
|
-
|
|
140
|
-
for entry in "${issues[@]}"; do
|
|
141
|
-
IFS=$'\t' read -r _id _title _status _owner _updated_iso _blocked_by <<< "$entry"
|
|
142
|
-
|
|
143
|
-
local _updated_epoch=0
|
|
144
|
-
if [[ -n "$_updated_iso" ]]; then
|
|
145
|
-
_updated_epoch="$(_iso_to_epoch "$_updated_iso")"
|
|
146
|
-
fi
|
|
147
|
-
|
|
148
|
-
# Default owner if empty
|
|
149
|
-
[[ -z "$_owner" ]] && _owner="unassigned"
|
|
150
|
-
|
|
151
|
-
# Initialize developer counters
|
|
152
|
-
[[ -z "${dev_open[$_owner]:-}" ]] && dev_open[$_owner]=0
|
|
153
|
-
[[ -z "${dev_inprogress[$_owner]:-}" ]] && dev_inprogress[$_owner]=0
|
|
154
|
-
[[ -z "${dev_blocked[$_owner]:-}" ]] && dev_blocked[$_owner]=0
|
|
155
|
-
[[ -z "${dev_stale[$_owner]:-}" ]] && dev_stale[$_owner]=0
|
|
156
|
-
|
|
157
|
-
# Count by status
|
|
158
|
-
case "$_status" in
|
|
159
|
-
OPEN)
|
|
160
|
-
dev_open[$_owner]=$(( ${dev_open[$_owner]} + 1 ))
|
|
161
|
-
total_open=$((total_open + 1))
|
|
162
|
-
;;
|
|
163
|
-
IN_PROGRESS)
|
|
164
|
-
dev_inprogress[$_owner]=$(( ${dev_inprogress[$_owner]} + 1 ))
|
|
165
|
-
total_inprogress=$((total_inprogress + 1))
|
|
166
|
-
;;
|
|
167
|
-
BLOCKED)
|
|
168
|
-
dev_blocked[$_owner]=$(( ${dev_blocked[$_owner]} + 1 ))
|
|
169
|
-
total_blocked=$((total_blocked + 1))
|
|
170
|
-
;;
|
|
171
|
-
esac
|
|
172
|
-
|
|
173
|
-
# Check for blocked (has DEPENDS ON with open deps)
|
|
174
|
-
if [[ -n "$_blocked_by" ]]; then
|
|
175
|
-
dev_blocked[$_owner]=$(( ${dev_blocked[$_owner]} + 1 ))
|
|
176
|
-
total_blocked=$((total_blocked + 1))
|
|
177
|
-
blocked_entries+=("$_id"$'\t'"$_owner"$'\t'"$_title"$'\t'"$_blocked_by")
|
|
178
|
-
fi
|
|
179
|
-
|
|
180
|
-
# Check for stale (>48h since last update)
|
|
181
|
-
if [[ "$_updated_epoch" -gt 0 ]]; then
|
|
182
|
-
local age=$(( now_epoch - _updated_epoch ))
|
|
183
|
-
if [[ $age -gt $stale_threshold ]]; then
|
|
184
|
-
local hours=$(( age / 3600 ))
|
|
185
|
-
dev_stale[$_owner]=$(( ${dev_stale[$_owner]} + 1 ))
|
|
186
|
-
total_stale=$((total_stale + 1))
|
|
187
|
-
stale_entries+=("$_id"$'\t'"$_owner"$'\t'"$_title"$'\t'"${hours}h since last update")
|
|
188
|
-
fi
|
|
189
|
-
fi
|
|
190
|
-
done
|
|
191
|
-
|
|
192
|
-
local total_issues=${#issues[@]}
|
|
193
|
-
|
|
194
|
-
# 4. Output
|
|
195
|
-
if [[ "$format" == "json" ]]; then
|
|
196
|
-
_dashboard_json "$total_issues"
|
|
197
|
-
else
|
|
198
|
-
_dashboard_text "$total_issues"
|
|
199
|
-
fi
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
# ── Text Output ──────────────────────────────────────────────────────────
|
|
203
|
-
|
|
204
|
-
_dashboard_text() {
|
|
205
|
-
local total_issues="$1"
|
|
206
|
-
|
|
207
|
-
echo "Team Dashboard"
|
|
208
|
-
echo "══════════════"
|
|
209
|
-
echo ""
|
|
210
|
-
|
|
211
|
-
# Header
|
|
212
|
-
printf "%-16s %4s %11s %7s %5s\n" "Developer" "Open" "In Progress" "Blocked" "Stale"
|
|
213
|
-
|
|
214
|
-
# Get sorted list of developers
|
|
215
|
-
local devs=()
|
|
216
|
-
local dev
|
|
217
|
-
for dev in "${!dev_open[@]}"; do
|
|
218
|
-
devs+=("$dev")
|
|
219
|
-
done
|
|
220
|
-
# Sort developers
|
|
221
|
-
IFS=$'\n' devs=($(sort <<< "${devs[*]}")); unset IFS
|
|
222
|
-
|
|
223
|
-
for dev in "${devs[@]}"; do
|
|
224
|
-
printf "%-16s %4d %11d %7d %5d\n" \
|
|
225
|
-
"$dev" \
|
|
226
|
-
"${dev_open[$dev]:-0}" \
|
|
227
|
-
"${dev_inprogress[$dev]:-0}" \
|
|
228
|
-
"${dev_blocked[$dev]:-0}" \
|
|
229
|
-
"${dev_stale[$dev]:-0}"
|
|
230
|
-
done
|
|
231
|
-
|
|
232
|
-
echo ""
|
|
233
|
-
|
|
234
|
-
# Build total summary parts
|
|
235
|
-
local parts=()
|
|
236
|
-
[[ $total_inprogress -gt 0 ]] && parts+=("$total_inprogress in progress")
|
|
237
|
-
[[ $total_blocked -gt 0 ]] && parts+=("$total_blocked blocked")
|
|
238
|
-
[[ $total_stale -gt 0 ]] && parts+=("$total_stale stale")
|
|
239
|
-
[[ $total_open -gt 0 ]] && parts+=("$total_open open")
|
|
240
|
-
|
|
241
|
-
local summary=""
|
|
242
|
-
if [[ ${#parts[@]} -gt 0 ]]; then
|
|
243
|
-
summary=" ($(IFS=', '; echo "${parts[*]}"))"
|
|
244
|
-
fi
|
|
245
|
-
echo "Total: $total_issues issues${summary}"
|
|
246
|
-
|
|
247
|
-
# Stale section
|
|
248
|
-
if [[ ${#stale_entries[@]} -gt 0 ]]; then
|
|
249
|
-
echo ""
|
|
250
|
-
echo "Stale assignments (>48h):"
|
|
251
|
-
local entry
|
|
252
|
-
for entry in "${stale_entries[@]}"; do
|
|
253
|
-
IFS=$'\t' read -r _id _owner _title _age <<< "$entry"
|
|
254
|
-
echo " ⚠ $_id [$_owner] — $_title ($_age)"
|
|
255
|
-
done
|
|
256
|
-
fi
|
|
257
|
-
|
|
258
|
-
# Blocked section
|
|
259
|
-
if [[ ${#blocked_entries[@]} -gt 0 ]]; then
|
|
260
|
-
echo ""
|
|
261
|
-
echo "Blocked issues:"
|
|
262
|
-
local entry
|
|
263
|
-
for entry in "${blocked_entries[@]}"; do
|
|
264
|
-
IFS=$'\t' read -r _id _owner _title _deps <<< "$entry"
|
|
265
|
-
echo " ⚠ $_id [$_owner] — $_title (blocked by $_deps)"
|
|
266
|
-
done
|
|
267
|
-
fi
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
# ── JSON Output ──────────────────────────────────────────────────────────
|
|
271
|
-
|
|
272
|
-
_dashboard_json() {
|
|
273
|
-
local total_issues="$1"
|
|
274
|
-
|
|
275
|
-
# Build developers JSON
|
|
276
|
-
local dev_json="{"
|
|
277
|
-
local first=1
|
|
278
|
-
local devs=()
|
|
279
|
-
local dev
|
|
280
|
-
for dev in "${!dev_open[@]}"; do
|
|
281
|
-
devs+=("$dev")
|
|
282
|
-
done
|
|
283
|
-
IFS=$'\n' devs=($(sort <<< "${devs[*]}")); unset IFS
|
|
284
|
-
|
|
285
|
-
for dev in "${devs[@]}"; do
|
|
286
|
-
[[ $first -eq 0 ]] && dev_json+=","
|
|
287
|
-
first=0
|
|
288
|
-
dev_json+="\"$dev\":{\"open\":${dev_open[$dev]:-0},\"in_progress\":${dev_inprogress[$dev]:-0},\"blocked\":${dev_blocked[$dev]:-0},\"stale\":${dev_stale[$dev]:-0}}"
|
|
289
|
-
done
|
|
290
|
-
dev_json+="}"
|
|
291
|
-
|
|
292
|
-
# Build stale JSON array
|
|
293
|
-
local stale_json="["
|
|
294
|
-
first=1
|
|
295
|
-
local entry
|
|
296
|
-
for entry in "${stale_entries[@]}"; do
|
|
297
|
-
[[ $first -eq 0 ]] && stale_json+=","
|
|
298
|
-
first=0
|
|
299
|
-
IFS=$'\t' read -r _id _owner _title _age <<< "$entry"
|
|
300
|
-
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}')"
|
|
301
|
-
done
|
|
302
|
-
stale_json+="]"
|
|
303
|
-
|
|
304
|
-
# Build blocked JSON array
|
|
305
|
-
local blocked_json="["
|
|
306
|
-
first=1
|
|
307
|
-
for entry in "${blocked_entries[@]}"; do
|
|
308
|
-
[[ $first -eq 0 ]] && blocked_json+=","
|
|
309
|
-
first=0
|
|
310
|
-
IFS=$'\t' read -r _id _owner _title _deps <<< "$entry"
|
|
311
|
-
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}')"
|
|
312
|
-
done
|
|
313
|
-
blocked_json+="]"
|
|
314
|
-
|
|
315
|
-
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}"
|
|
316
|
-
}
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
-
|
|
6
|
-
# ── Setup temp dir for test isolation ────────────────────────────────────
|
|
7
|
-
TEST_TMP="$(mktemp -d)"
|
|
8
|
-
trap 'rm -rf "$TEST_TMP"' EXIT
|
|
9
|
-
|
|
10
|
-
# ── Create mock forge ─────────────────────────────────────────────────────
|
|
11
|
-
# Emits Kernel-shaped JSON ({"data":{"issues":[...]}}) for `issue list`.
|
|
12
|
-
mock_dir="$(mktemp -d)"
|
|
13
|
-
|
|
14
|
-
cat > "$mock_dir/forge" << 'MOCK'
|
|
15
|
-
#!/usr/bin/env bash
|
|
16
|
-
args="$*"
|
|
17
|
-
emit() { printf '{"data":{"issues":%s}}\n' "$1"; }
|
|
18
|
-
|
|
19
|
-
# forge-aaa BLOCKS forge-ccc (kernel: blocker carries the dependency edge), so
|
|
20
|
-
# forge-ccc is the blocked one — its blocked_by resolves to forge-aaa via reverse scan.
|
|
21
|
-
aaa='{"id":"forge-aaa","title":"Feature A","status":"in_progress","assignee":"devone","updated_at":"2026-03-27T10:00:00Z","dependencies":["forge-ccc"]}'
|
|
22
|
-
bbb='{"id":"forge-bbb","title":"Feature B","status":"open","assignee":"devtwo","updated_at":"2026-03-24T10:00:00Z","dependencies":[]}'
|
|
23
|
-
ccc='{"id":"forge-ccc","title":"Feature C","status":"in_progress","assignee":"devone","updated_at":"2026-03-27T09:00:00Z","dependencies":[]}'
|
|
24
|
-
|
|
25
|
-
if [[ "$args" == *"issue list"* ]]; then
|
|
26
|
-
if [[ -n "${FORGE_MOCK_EMPTY:-}" ]]; then
|
|
27
|
-
emit "[]"
|
|
28
|
-
elif [[ -n "${FORGE_MOCK_SINGLE:-}" ]]; then
|
|
29
|
-
if [[ "$args" == *"--status=in_progress"* ]]; then emit "[$aaa]"; else emit "[]"; fi
|
|
30
|
-
else
|
|
31
|
-
if [[ "$args" == *"--status=open"* ]]; then emit "[$bbb]"
|
|
32
|
-
elif [[ "$args" == *"--status=in_progress"* ]]; then emit "[$aaa,$ccc]"
|
|
33
|
-
else emit "[]"; fi
|
|
34
|
-
fi
|
|
35
|
-
exit 0
|
|
36
|
-
fi
|
|
37
|
-
exit 0
|
|
38
|
-
MOCK
|
|
39
|
-
chmod +x "$mock_dir/forge"
|
|
40
|
-
|
|
41
|
-
export FORGE_CMD="$mock_dir/forge"
|
|
42
|
-
|
|
43
|
-
# Source the library under test
|
|
44
|
-
source "$SCRIPT_DIR/lib/dashboard.sh"
|
|
45
|
-
|
|
46
|
-
PASS=0
|
|
47
|
-
FAIL=0
|
|
48
|
-
|
|
49
|
-
assert_eq() {
|
|
50
|
-
local label="$1" expected="$2" actual="$3"
|
|
51
|
-
if [[ "$actual" == "$expected" ]]; then
|
|
52
|
-
PASS=$((PASS + 1)); echo " PASS: $label"
|
|
53
|
-
else
|
|
54
|
-
FAIL=$((FAIL + 1)); echo " FAIL: $label (expected '$expected', got '$actual')"
|
|
55
|
-
fi
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
assert_contains() {
|
|
59
|
-
local label="$1" expected="$2" actual="$3"
|
|
60
|
-
if [[ "$actual" == *"$expected"* ]]; then
|
|
61
|
-
PASS=$((PASS + 1)); echo " PASS: $label"
|
|
62
|
-
else
|
|
63
|
-
FAIL=$((FAIL + 1)); echo " FAIL: $label (expected to contain '$expected', got '$actual')"
|
|
64
|
-
fi
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
assert_not_contains() {
|
|
68
|
-
local label="$1" unexpected="$2" actual="$3"
|
|
69
|
-
if [[ "$actual" != *"$unexpected"* ]]; then
|
|
70
|
-
PASS=$((PASS + 1)); echo " PASS: $label"
|
|
71
|
-
else
|
|
72
|
-
FAIL=$((FAIL + 1)); echo " FAIL: $label (expected NOT to contain '$unexpected')"
|
|
73
|
-
fi
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
assert_exit() {
|
|
77
|
-
local label="$1" expected="$2" actual="$3"
|
|
78
|
-
if [[ "$actual" -eq "$expected" ]]; then
|
|
79
|
-
PASS=$((PASS + 1)); echo " PASS: $label"
|
|
80
|
-
else
|
|
81
|
-
FAIL=$((FAIL + 1)); echo " FAIL: $label (expected exit $expected, got $actual)"
|
|
82
|
-
fi
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
# ── Test 1: Full dashboard with multiple developers ──────────────────────
|
|
86
|
-
echo "── Test 1: Full dashboard with multiple developers ──"
|
|
87
|
-
|
|
88
|
-
# Override _now to a fixed time: 2026-03-27T12:00:00Z = 1774836000
|
|
89
|
-
export FORGE_NOW=1774836000
|
|
90
|
-
|
|
91
|
-
rc=0
|
|
92
|
-
output="$(cmd_dashboard 2>&1)" || rc=$?
|
|
93
|
-
assert_exit "dashboard exits 0" 0 "$rc"
|
|
94
|
-
assert_contains "shows Team Dashboard header" "Team Dashboard" "$output"
|
|
95
|
-
assert_contains "shows devone stats" "devone" "$output"
|
|
96
|
-
assert_contains "shows devtwo stats" "devtwo" "$output"
|
|
97
|
-
assert_contains "shows total summary" "Total:" "$output"
|
|
98
|
-
|
|
99
|
-
# ── Test 2: Stale issue flagged (forge-bbb >48h old) ────────────────────
|
|
100
|
-
echo ""
|
|
101
|
-
echo "── Test 2: Stale issue flagged ──"
|
|
102
|
-
|
|
103
|
-
# forge-bbb updated 2026-03-24T10:00:00Z, now is 2026-03-27T12:00:00Z => ~74h
|
|
104
|
-
assert_contains "stale section shown" "Stale assignments" "$output"
|
|
105
|
-
assert_contains "forge-bbb flagged as stale" "forge-bbb" "$output"
|
|
106
|
-
assert_contains "stale issue shows devtwo" "devtwo" "$output"
|
|
107
|
-
|
|
108
|
-
# ── Test 3: Blocked issue detected (forge-ccc depends on forge-aaa) ─────
|
|
109
|
-
echo ""
|
|
110
|
-
echo "── Test 3: Blocked issue detected ──"
|
|
111
|
-
|
|
112
|
-
assert_contains "blocked section shown" "Blocked issues" "$output"
|
|
113
|
-
assert_contains "forge-ccc flagged as blocked" "forge-ccc" "$output"
|
|
114
|
-
assert_contains "blocked shows dependency" "forge-aaa" "$output"
|
|
115
|
-
|
|
116
|
-
# ── Test 4: No issues → "No active issues" ──────────────────────────────
|
|
117
|
-
echo ""
|
|
118
|
-
echo "── Test 4: No issues → clear message ──"
|
|
119
|
-
|
|
120
|
-
export FORGE_MOCK_EMPTY=1
|
|
121
|
-
rc=0
|
|
122
|
-
output_empty="$(cmd_dashboard 2>&1)" || rc=$?
|
|
123
|
-
assert_exit "empty dashboard exits 0" 0 "$rc"
|
|
124
|
-
assert_contains "shows no active issues" "No active issues" "$output_empty"
|
|
125
|
-
unset FORGE_MOCK_EMPTY
|
|
126
|
-
|
|
127
|
-
# ── Test 5: Single developer → works correctly ──────────────────────────
|
|
128
|
-
echo ""
|
|
129
|
-
echo "── Test 5: Single developer ──"
|
|
130
|
-
|
|
131
|
-
export FORGE_MOCK_SINGLE=1
|
|
132
|
-
rc=0
|
|
133
|
-
output_single="$(cmd_dashboard 2>&1)" || rc=$?
|
|
134
|
-
assert_exit "single dev dashboard exits 0" 0 "$rc"
|
|
135
|
-
assert_contains "shows devone" "devone" "$output_single"
|
|
136
|
-
assert_not_contains "no devtwo in single mode" "devtwo" "$output_single"
|
|
137
|
-
unset FORGE_MOCK_SINGLE
|
|
138
|
-
|
|
139
|
-
# ── Test 6: --format=json → valid JSON ──────────────────────────────────
|
|
140
|
-
echo ""
|
|
141
|
-
echo "── Test 6: --format=json → valid JSON ──"
|
|
142
|
-
|
|
143
|
-
rc=0
|
|
144
|
-
output_json="$(cmd_dashboard --format=json 2>&1)" || rc=$?
|
|
145
|
-
assert_exit "json format exits 0" 0 "$rc"
|
|
146
|
-
# Check it starts with { and contains expected keys
|
|
147
|
-
assert_contains "json has developers key" "developers" "$output_json"
|
|
148
|
-
assert_contains "json has total key" "total" "$output_json"
|
|
149
|
-
assert_contains "json has stale key" "stale" "$output_json"
|
|
150
|
-
assert_contains "json has blocked key" "blocked" "$output_json"
|
|
151
|
-
|
|
152
|
-
# ── Results ──────────────────────────────────────────────────────────────
|
|
153
|
-
echo ""
|
|
154
|
-
echo "Results: $PASS passed, $FAIL failed"
|
|
155
|
-
[[ $FAIL -eq 0 ]] && exit 0 || exit 1
|