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.
Files changed (155) hide show
  1. package/.forge/hooks/check-tdd.js +79 -5
  2. package/.forge/hooks/forge-native-hook.js +194 -8
  3. package/AGENTS.md +2 -1
  4. package/CHANGELOG.md +28 -0
  5. package/QUICKSTART.md +6 -2
  6. package/README.md +3 -1
  7. package/bin/forge-cmd.js +1 -1
  8. package/bin/forge.js +95 -19
  9. package/docs/guides/SETUP.md +4 -1
  10. package/docs/guides/SUPPORT.md +5 -0
  11. package/docs/reference/COMMANDS.md +12 -1
  12. package/docs/reference/shepherd.md +114 -3
  13. package/lib/activation/ensure-forge-home.js +135 -0
  14. package/lib/adapters/beads-kernel-compat.js +67 -0
  15. package/lib/adapters/greptile-review-adapter.js +1 -1
  16. package/lib/adapters/pr-state-adapter.js +103 -8
  17. package/lib/adoption-profiles.js +17 -4
  18. package/lib/agents-config.js +5 -0
  19. package/lib/beads-detect.js +60 -0
  20. package/lib/commands/_aliases.js +248 -0
  21. package/lib/commands/_issue.js +65 -41
  22. package/lib/commands/_manifest.js +3 -1
  23. package/lib/commands/_registry.js +14 -0
  24. package/lib/commands/_resolve-command-opts.js +36 -60
  25. package/lib/commands/claim.js +2 -4
  26. package/lib/commands/gate.js +19 -2
  27. package/lib/commands/hooks.js +294 -12
  28. package/lib/commands/init.js +26 -20
  29. package/lib/commands/memory.js +81 -0
  30. package/lib/commands/migrate.js +0 -161
  31. package/lib/commands/plan.js +71 -123
  32. package/lib/commands/pr.js +88 -0
  33. package/lib/commands/prime.js +8 -1
  34. package/lib/commands/push.js +66 -0
  35. package/lib/commands/recall.js +67 -12
  36. package/lib/commands/recap.js +18 -4
  37. package/lib/commands/release.js +15 -3
  38. package/lib/commands/remember.js +86 -20
  39. package/lib/commands/serve.js +5 -2
  40. package/lib/commands/setup.js +135 -73
  41. package/lib/commands/shepherd.js +102 -4
  42. package/lib/commands/ship.js +40 -4
  43. package/lib/commands/skill.js +275 -0
  44. package/lib/commands/status.js +37 -32
  45. package/lib/commands/test.js +32 -0
  46. package/lib/commands/worktree.js +87 -5
  47. package/lib/core/runtime-graph.js +122 -17
  48. package/lib/forge-issues.js +13 -464
  49. package/lib/gate-events.js +54 -55
  50. package/lib/global-flags.js +30 -0
  51. package/lib/grounding/context-events.js +230 -0
  52. package/lib/grounding/read-first.js +112 -0
  53. package/lib/harness-capability-matrix.js +2 -2
  54. package/lib/hook-renderer.js +144 -12
  55. package/lib/issue-backend.js +42 -3
  56. package/lib/kernel/backing-issue.js +7 -1
  57. package/lib/kernel/broker.js +28 -0
  58. package/lib/kernel/migrations.js +30 -2
  59. package/lib/kernel/owned-kernel.js +43 -0
  60. package/lib/kernel/schema.js +35 -0
  61. package/lib/kernel/sqlite-driver.js +187 -1
  62. package/lib/memory-recall.js +151 -0
  63. package/lib/orientation.js +303 -6
  64. package/lib/pr-bundle.js +6 -2
  65. package/lib/pr-monitor/auto-actions.js +175 -0
  66. package/lib/pr-monitor/digest.js +206 -0
  67. package/lib/pr-monitor/reconcile-executor.js +659 -0
  68. package/lib/pr-monitor/reconcile-tick.js +138 -0
  69. package/lib/pr-monitor/reconcile.js +0 -0
  70. package/lib/pr-monitor/render-sticky.js +57 -8
  71. package/lib/pr-monitor/shepherd-lease.js +243 -0
  72. package/lib/pr-monitor/upsert-sticky.js +169 -0
  73. package/lib/pr-monitor/watch-lifecycle.js +1 -1
  74. package/lib/pr-pull.js +113 -17
  75. package/lib/project-memory.js +8 -0
  76. package/lib/release-readiness.js +17 -1
  77. package/lib/rules-sync.js +4 -0
  78. package/lib/runtime-health.js +15 -46
  79. package/lib/skill-eval.js +750 -0
  80. package/lib/status/identity.js +46 -0
  81. package/lib/status/presenter.js +0 -35
  82. package/lib/status/snapshot.js +11 -16
  83. package/lib/upgrade-safety.js +52 -1
  84. package/lib/using-forge.js +315 -0
  85. package/lib/workflow/enforce-stage.js +64 -7
  86. package/lib/workflow/state-manager.js +23 -23
  87. package/package.json +2 -2
  88. package/rules/using-forge.md +24 -0
  89. package/scripts/forge-team/index.sh +0 -5
  90. package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
  91. package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
  92. package/scripts/pr-auto-actions.js +93 -0
  93. package/scripts/pr-verdict-label.js +50 -0
  94. package/scripts/test.js +8 -1
  95. package/skills/claim-safety/SKILL.md +4 -0
  96. package/skills/claim-safety/evals/scorecard.json +41 -0
  97. package/skills/coverage.json +83 -0
  98. package/skills/dev/SKILL.md +4 -0
  99. package/skills/dev/evals/scorecard.json +41 -0
  100. package/skills/gates/SKILL.md +80 -0
  101. package/skills/gates/evals/evals.json +38 -0
  102. package/skills/gates/evals/scorecard.json +41 -0
  103. package/skills/hermes-forge/SKILL.md +1 -0
  104. package/skills/hermes-forge/evals/scorecard.json +41 -0
  105. package/skills/issue-basics/SKILL.md +1 -0
  106. package/skills/issue-basics/evals/scorecard.json +41 -0
  107. package/skills/kernel/SKILL.md +38 -0
  108. package/skills/kernel/evals/scorecard.json +41 -0
  109. package/skills/memory/SKILL.md +16 -1
  110. package/skills/memory/evals/scorecard.json +41 -0
  111. package/skills/parallel-deep-research/SKILL.md +1 -0
  112. package/skills/parallel-deep-research/evals/scorecard.json +41 -0
  113. package/skills/plan/SKILL.md +6 -0
  114. package/skills/plan/evals/scorecard.json +41 -0
  115. package/skills/portability/SKILL.md +47 -0
  116. package/skills/portability/evals/evals.json +34 -0
  117. package/skills/portability/evals/scorecard.json +41 -0
  118. package/skills/research/SKILL.md +1 -0
  119. package/skills/research/evals/scorecard.json +41 -0
  120. package/skills/review/SKILL.md +6 -0
  121. package/skills/review/evals/scorecard.json +41 -0
  122. package/skills/rollback/SKILL.md +1 -0
  123. package/skills/rollback/evals/scorecard.json +41 -0
  124. package/skills/setup/SKILL.md +91 -0
  125. package/skills/setup/evals/evals.json +42 -0
  126. package/skills/setup/evals/scorecard.json +41 -0
  127. package/skills/shepherd/SKILL.md +76 -36
  128. package/skills/shepherd/evals/evals.json +21 -9
  129. package/skills/shepherd/evals/scorecard.json +41 -0
  130. package/skills/ship/SKILL.md +6 -0
  131. package/skills/ship/evals/scorecard.json +41 -0
  132. package/skills/smith/SKILL.md +8 -0
  133. package/skills/smith/evals/scorecard.json +41 -0
  134. package/skills/sonarcloud/SKILL.md +1 -0
  135. package/skills/sonarcloud/evals/scorecard.json +41 -0
  136. package/skills/sonarcloud-analysis/SKILL.md +1 -0
  137. package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
  138. package/skills/status/SKILL.md +3 -0
  139. package/skills/status/evals/scorecard.json +41 -0
  140. package/skills/triage-ready/SKILL.md +2 -0
  141. package/skills/triage-ready/evals/scorecard.json +41 -0
  142. package/skills/using-forge/SKILL.md +104 -0
  143. package/skills/using-forge/evals/scorecard.json +41 -0
  144. package/skills/validate/SKILL.md +4 -0
  145. package/skills/validate/evals/scorecard.json +41 -0
  146. package/skills/verify/SKILL.md +4 -0
  147. package/skills/verify/evals/scorecard.json +41 -0
  148. package/skills/worktree/SKILL.md +87 -0
  149. package/skills/worktree/evals/evals.json +38 -0
  150. package/skills/worktree/evals/scorecard.json +41 -0
  151. package/lib/adapters/beads-issue-adapter.js +0 -127
  152. package/lib/commands/board.js +0 -64
  153. package/lib/status/beads-snapshot.js +0 -145
  154. package/scripts/forge-team/lib/dashboard.sh +0 -316
  155. 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