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.
Files changed (164) hide show
  1. package/.claude/commands/dev.md +6 -1
  2. package/.claude/commands/plan.md +59 -14
  3. package/.claude/commands/premerge.md +10 -0
  4. package/.claude/commands/review.md +7 -1
  5. package/.claude/commands/ship.md +95 -47
  6. package/.claude/commands/status.md +42 -0
  7. package/.claude/commands/validate.md +7 -1
  8. package/.claude/commands/verify.md +52 -4
  9. package/.claude/rules/workflow.md +16 -0
  10. package/.claude/scripts/greptile-resolve.sh +32 -0
  11. package/.cline/workflows/dev.md +6 -1
  12. package/.cline/workflows/plan.md +59 -14
  13. package/.cline/workflows/premerge.md +10 -0
  14. package/.cline/workflows/review.md +7 -1
  15. package/.cline/workflows/ship.md +95 -47
  16. package/.cline/workflows/status.md +42 -0
  17. package/.cline/workflows/validate.md +7 -1
  18. package/.cline/workflows/verify.md +52 -4
  19. package/.codex/skills/dev/SKILL.md +6 -1
  20. package/.codex/skills/plan/SKILL.md +59 -14
  21. package/.codex/skills/premerge/SKILL.md +10 -0
  22. package/.codex/skills/review/SKILL.md +7 -1
  23. package/.codex/skills/ship/SKILL.md +95 -47
  24. package/.codex/skills/status/SKILL.md +42 -0
  25. package/.codex/skills/validate/SKILL.md +7 -1
  26. package/.codex/skills/verify/SKILL.md +52 -4
  27. package/.cursor/commands/dev.md +6 -1
  28. package/.cursor/commands/plan.md +59 -14
  29. package/.cursor/commands/premerge.md +10 -0
  30. package/.cursor/commands/review.md +7 -1
  31. package/.cursor/commands/ship.md +95 -47
  32. package/.cursor/commands/status.md +42 -0
  33. package/.cursor/commands/validate.md +7 -1
  34. package/.cursor/commands/verify.md +52 -4
  35. package/.cursorrules +149 -0
  36. package/.github/prompts/dev.prompt.md +6 -1
  37. package/.github/prompts/plan.prompt.md +59 -14
  38. package/.github/prompts/premerge.prompt.md +10 -0
  39. package/.github/prompts/review.prompt.md +7 -1
  40. package/.github/prompts/ship.prompt.md +95 -47
  41. package/.github/prompts/status.prompt.md +42 -0
  42. package/.github/prompts/validate.prompt.md +7 -1
  43. package/.github/prompts/verify.prompt.md +52 -4
  44. package/.kilocode/workflows/dev.md +6 -1
  45. package/.kilocode/workflows/plan.md +59 -14
  46. package/.kilocode/workflows/premerge.md +10 -0
  47. package/.kilocode/workflows/review.md +7 -1
  48. package/.kilocode/workflows/ship.md +95 -47
  49. package/.kilocode/workflows/status.md +42 -0
  50. package/.kilocode/workflows/validate.md +7 -1
  51. package/.kilocode/workflows/verify.md +52 -4
  52. package/.opencode/commands/dev.md +6 -1
  53. package/.opencode/commands/plan.md +59 -14
  54. package/.opencode/commands/premerge.md +10 -0
  55. package/.opencode/commands/review.md +7 -1
  56. package/.opencode/commands/ship.md +95 -47
  57. package/.opencode/commands/status.md +42 -0
  58. package/.opencode/commands/validate.md +7 -1
  59. package/.opencode/commands/verify.md +52 -4
  60. package/.roo/commands/dev.md +6 -1
  61. package/.roo/commands/plan.md +59 -14
  62. package/.roo/commands/premerge.md +10 -0
  63. package/.roo/commands/review.md +7 -1
  64. package/.roo/commands/ship.md +95 -47
  65. package/.roo/commands/status.md +42 -0
  66. package/.roo/commands/validate.md +7 -1
  67. package/.roo/commands/verify.md +52 -4
  68. package/AGENTS.md +97 -0
  69. package/CLAUDE.md +10 -0
  70. package/README.md +2 -2
  71. package/bin/forge-cmd.js +5 -1
  72. package/bin/forge-preflight.js +15 -2
  73. package/bin/forge.js +211 -9
  74. package/docs/ENHANCED_ONBOARDING.md +96 -86
  75. package/docs/ROADMAP.md +2 -2
  76. package/docs/TOOLCHAIN.md +23 -0
  77. package/docs/VALIDATION.md +1 -1
  78. package/lefthook.yml +11 -0
  79. package/lib/agents/README.md +46 -1
  80. package/lib/agents/cline.plugin.json +11 -4
  81. package/lib/agents/codex.plugin.json +2 -2
  82. package/lib/agents/copilot.plugin.json +5 -5
  83. package/lib/agents/cursor.plugin.json +1 -1
  84. package/lib/agents/kilocode.plugin.json +1 -1
  85. package/lib/agents/opencode.plugin.json +7 -4
  86. package/lib/agents/roo.plugin.json +10 -3
  87. package/lib/agents-config.js +129 -81
  88. package/lib/codex-skills.js +50 -0
  89. package/lib/commands/_registry.js +173 -0
  90. package/lib/commands/clean.js +181 -0
  91. package/lib/commands/commands-reset.js +147 -0
  92. package/lib/commands/dev.js +84 -0
  93. package/lib/commands/plan.js +18 -0
  94. package/lib/commands/push.js +196 -0
  95. package/lib/commands/recommend.js +1 -1
  96. package/lib/commands/setup.js +4295 -0
  97. package/lib/commands/ship.js +20 -0
  98. package/lib/commands/status.js +210 -44
  99. package/lib/commands/sync.js +71 -0
  100. package/lib/commands/team.js +37 -0
  101. package/lib/commands/test.js +207 -0
  102. package/lib/commands/validate.js +13 -0
  103. package/lib/commands/worktree.js +310 -0
  104. package/lib/detect-agent.js +38 -8
  105. package/lib/detection-utils.js +405 -0
  106. package/lib/docs-command.js +51 -0
  107. package/lib/docs-copy.js +50 -0
  108. package/lib/file-utils.js +260 -0
  109. package/lib/forge-context.js +42 -0
  110. package/lib/freshness-token.js +148 -0
  111. package/lib/frontmatter.js +79 -0
  112. package/lib/greptile-match.js +80 -0
  113. package/lib/husky-migration.js +113 -12
  114. package/lib/lefthook-check.js +27 -6
  115. package/lib/plugin-manager.js +225 -72
  116. package/lib/project-discovery.js +39 -5
  117. package/lib/reset.js +309 -0
  118. package/lib/runtime-health.js +305 -0
  119. package/lib/shell-utils.js +50 -0
  120. package/lib/task-ownership.js +117 -0
  121. package/lib/ui-utils.js +43 -0
  122. package/lib/validation-utils.js +163 -0
  123. package/lib/workflow/enforce-stage.js +179 -0
  124. package/lib/workflow/stages.js +201 -0
  125. package/lib/workflow/state.js +332 -0
  126. package/opencode.json +67 -0
  127. package/package.json +16 -6
  128. package/scripts/beads-context.sh +165 -22
  129. package/scripts/beads-context.test.js +5 -1
  130. package/scripts/check-agents.js +103 -0
  131. package/scripts/check-forge-token.js +98 -0
  132. package/scripts/conflict-detect.sh +2 -2
  133. package/scripts/dep-guard.sh +6 -28
  134. package/scripts/file-index.sh +117 -23
  135. package/scripts/forge-team/index.sh +86 -0
  136. package/scripts/forge-team/lib/agent-prompt.sh +52 -0
  137. package/scripts/forge-team/lib/claim.sh +256 -0
  138. package/scripts/forge-team/lib/dashboard.sh +341 -0
  139. package/scripts/forge-team/lib/epic.sh +332 -0
  140. package/scripts/forge-team/lib/hooks.sh +253 -0
  141. package/scripts/forge-team/lib/identity.sh +235 -0
  142. package/scripts/forge-team/lib/sync-github.sh +317 -0
  143. package/scripts/forge-team/lib/verify.sh +284 -0
  144. package/scripts/forge-team/lib/workload.sh +296 -0
  145. package/scripts/forge-team/tests/agent-prompt.test.sh +72 -0
  146. package/scripts/forge-team/tests/claim.test.sh +179 -0
  147. package/scripts/forge-team/tests/dashboard.test.sh +170 -0
  148. package/scripts/forge-team/tests/dispatcher.test.sh +79 -0
  149. package/scripts/forge-team/tests/epic.test.sh +176 -0
  150. package/scripts/forge-team/tests/hooks.test.sh +239 -0
  151. package/scripts/forge-team/tests/identity.test.sh +176 -0
  152. package/scripts/forge-team/tests/integration.test.sh +371 -0
  153. package/scripts/forge-team/tests/sync-github.test.sh +209 -0
  154. package/scripts/forge-team/tests/verify.test.sh +314 -0
  155. package/scripts/forge-team/tests/workflow-integration.test.sh +43 -0
  156. package/scripts/forge-team/tests/workload.test.sh +209 -0
  157. package/scripts/lib/eval-runner.js +39 -0
  158. package/scripts/lib/jsonl-lock.sh +48 -0
  159. package/scripts/lib/sanitize.sh +116 -0
  160. package/scripts/pr-coordinator.sh +756 -0
  161. package/scripts/smart-status.sh +58 -21
  162. package/scripts/sync-commands.js +49 -20
  163. package/scripts/sync-utils.sh +24 -29
  164. package/scripts/test.js +18 -1
@@ -0,0 +1,170 @@
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 bd ───────────────────────────────────────────────────────
11
+ mock_dir="$(mktemp -d)"
12
+
13
+ cat > "$mock_dir/bd" << 'MOCK'
14
+ #!/usr/bin/env bash
15
+ case "$1" in
16
+ list)
17
+ if echo "$*" | grep -q -- "--status=open,in_progress"; then
18
+ if [[ -n "${BD_MOCK_EMPTY:-}" ]]; then
19
+ # No output for empty test
20
+ :
21
+ elif [[ -n "${BD_MOCK_SINGLE:-}" ]]; then
22
+ echo "◐ forge-aaa · Feature A"
23
+ else
24
+ echo "◐ forge-aaa · Feature A"
25
+ echo "○ forge-bbb · Feature B"
26
+ echo "◐ forge-ccc · Feature C"
27
+ fi
28
+ fi
29
+ ;;
30
+ show)
31
+ case "$2" in
32
+ forge-aaa)
33
+ echo "◐ forge-aaa · Feature A [● P2 · IN_PROGRESS]"
34
+ echo "Owner: devone"
35
+ echo "Updated: 2026-03-27T10:00:00Z"
36
+ ;;
37
+ forge-bbb)
38
+ echo "○ forge-bbb · Feature B [● P2 · OPEN]"
39
+ echo "Owner: devtwo"
40
+ echo "Updated: 2026-03-24T10:00:00Z"
41
+ ;;
42
+ forge-ccc)
43
+ echo "◐ forge-ccc · Feature C [● P1 · IN_PROGRESS]"
44
+ echo "Owner: devone"
45
+ echo "Updated: 2026-03-27T09:00:00Z"
46
+ echo ""
47
+ echo "DEPENDS ON"
48
+ echo " → ◐ forge-aaa: Feature A"
49
+ ;;
50
+ esac
51
+ ;;
52
+ esac
53
+ MOCK
54
+ chmod +x "$mock_dir/bd"
55
+
56
+ export BD_CMD="$mock_dir/bd"
57
+
58
+ # Source the library under test
59
+ source "$SCRIPT_DIR/lib/dashboard.sh"
60
+
61
+ PASS=0
62
+ FAIL=0
63
+
64
+ assert_eq() {
65
+ local label="$1" expected="$2" actual="$3"
66
+ if [[ "$actual" == "$expected" ]]; then
67
+ PASS=$((PASS + 1)); echo " PASS: $label"
68
+ else
69
+ FAIL=$((FAIL + 1)); echo " FAIL: $label (expected '$expected', got '$actual')"
70
+ fi
71
+ }
72
+
73
+ assert_contains() {
74
+ local label="$1" expected="$2" actual="$3"
75
+ if [[ "$actual" == *"$expected"* ]]; then
76
+ PASS=$((PASS + 1)); echo " PASS: $label"
77
+ else
78
+ FAIL=$((FAIL + 1)); echo " FAIL: $label (expected to contain '$expected', got '$actual')"
79
+ fi
80
+ }
81
+
82
+ assert_not_contains() {
83
+ local label="$1" unexpected="$2" actual="$3"
84
+ if [[ "$actual" != *"$unexpected"* ]]; then
85
+ PASS=$((PASS + 1)); echo " PASS: $label"
86
+ else
87
+ FAIL=$((FAIL + 1)); echo " FAIL: $label (expected NOT to contain '$unexpected')"
88
+ fi
89
+ }
90
+
91
+ assert_exit() {
92
+ local label="$1" expected="$2" actual="$3"
93
+ if [[ "$actual" -eq "$expected" ]]; then
94
+ PASS=$((PASS + 1)); echo " PASS: $label"
95
+ else
96
+ FAIL=$((FAIL + 1)); echo " FAIL: $label (expected exit $expected, got $actual)"
97
+ fi
98
+ }
99
+
100
+ # ── Test 1: Full dashboard with multiple developers ──────────────────────
101
+ echo "── Test 1: Full dashboard with multiple developers ──"
102
+
103
+ # Override _now to a fixed time: 2026-03-27T12:00:00Z = 1774836000
104
+ export FORGE_NOW=1774836000
105
+
106
+ rc=0
107
+ output="$(cmd_dashboard 2>&1)" || rc=$?
108
+ assert_exit "dashboard exits 0" 0 "$rc"
109
+ assert_contains "shows Team Dashboard header" "Team Dashboard" "$output"
110
+ assert_contains "shows devone stats" "devone" "$output"
111
+ assert_contains "shows devtwo stats" "devtwo" "$output"
112
+ assert_contains "shows total summary" "Total:" "$output"
113
+
114
+ # ── Test 2: Stale issue flagged (forge-bbb >48h old) ────────────────────
115
+ echo ""
116
+ echo "── Test 2: Stale issue flagged ──"
117
+
118
+ # forge-bbb updated 2026-03-24T10:00:00Z, now is 2026-03-27T12:00:00Z => ~74h
119
+ assert_contains "stale section shown" "Stale assignments" "$output"
120
+ assert_contains "forge-bbb flagged as stale" "forge-bbb" "$output"
121
+ assert_contains "stale issue shows devtwo" "devtwo" "$output"
122
+
123
+ # ── Test 3: Blocked issue detected (forge-ccc depends on forge-aaa) ─────
124
+ echo ""
125
+ echo "── Test 3: Blocked issue detected ──"
126
+
127
+ assert_contains "blocked section shown" "Blocked issues" "$output"
128
+ assert_contains "forge-ccc flagged as blocked" "forge-ccc" "$output"
129
+ assert_contains "blocked shows dependency" "forge-aaa" "$output"
130
+
131
+ # ── Test 4: No issues → "No active issues" ──────────────────────────────
132
+ echo ""
133
+ echo "── Test 4: No issues → clear message ──"
134
+
135
+ export BD_MOCK_EMPTY=1
136
+ rc=0
137
+ output_empty="$(cmd_dashboard 2>&1)" || rc=$?
138
+ assert_exit "empty dashboard exits 0" 0 "$rc"
139
+ assert_contains "shows no active issues" "No active issues" "$output_empty"
140
+ unset BD_MOCK_EMPTY
141
+
142
+ # ── Test 5: Single developer → works correctly ──────────────────────────
143
+ echo ""
144
+ echo "── Test 5: Single developer ──"
145
+
146
+ export BD_MOCK_SINGLE=1
147
+ rc=0
148
+ output_single="$(cmd_dashboard 2>&1)" || rc=$?
149
+ assert_exit "single dev dashboard exits 0" 0 "$rc"
150
+ assert_contains "shows devone" "devone" "$output_single"
151
+ assert_not_contains "no devtwo in single mode" "devtwo" "$output_single"
152
+ unset BD_MOCK_SINGLE
153
+
154
+ # ── Test 6: --format=json → valid JSON ──────────────────────────────────
155
+ echo ""
156
+ echo "── Test 6: --format=json → valid JSON ──"
157
+
158
+ rc=0
159
+ output_json="$(cmd_dashboard --format=json 2>&1)" || rc=$?
160
+ assert_exit "json format exits 0" 0 "$rc"
161
+ # Check it starts with { and contains expected keys
162
+ assert_contains "json has developers key" "developers" "$output_json"
163
+ assert_contains "json has total key" "total" "$output_json"
164
+ assert_contains "json has stale key" "stale" "$output_json"
165
+ assert_contains "json has blocked key" "blocked" "$output_json"
166
+
167
+ # ── Results ──────────────────────────────────────────────────────────────
168
+ echo ""
169
+ echo "Results: $PASS passed, $FAIL failed"
170
+ [[ $FAIL -eq 0 ]] && exit 0 || exit 1
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env bash
2
+ # Tests for forge-team CLI dispatcher (index.sh)
3
+ # Exit codes: 0=all pass, 1=any failure
4
+
5
+ set -uo pipefail
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ DISPATCHER="$SCRIPT_DIR/../index.sh"
9
+
10
+ PASS=0
11
+ FAIL=0
12
+
13
+ assert_exit() {
14
+ local desc="$1" expected="$2"
15
+ shift 2
16
+ "$@" >/dev/null 2>&1
17
+ local actual=$?
18
+ if [[ "$actual" -eq "$expected" ]]; then
19
+ echo " PASS: $desc"
20
+ ((PASS++))
21
+ else
22
+ echo " FAIL: $desc (expected exit=$expected, got exit=$actual)"
23
+ ((FAIL++))
24
+ fi
25
+ }
26
+
27
+ assert_output_contains() {
28
+ local desc="$1" pattern="$2"
29
+ shift 2
30
+ local output
31
+ output=$("$@" 2>&1) || true
32
+ if echo "$output" | grep -q "$pattern"; then
33
+ echo " PASS: $desc"
34
+ ((PASS++))
35
+ else
36
+ echo " FAIL: $desc (expected output containing '$pattern')"
37
+ ((FAIL++))
38
+ fi
39
+ }
40
+
41
+ echo "=== forge-team dispatcher tests ==="
42
+
43
+ # Test 1: help exits 0 and shows usage
44
+ echo ""
45
+ echo "-- help subcommand --"
46
+ assert_exit "help exits 0" 0 bash "$DISPATCHER" help
47
+ assert_output_contains "help shows usage" "Usage: forge team" bash "$DISPATCHER" help
48
+
49
+ # Test 2: No args exits 1
50
+ echo ""
51
+ echo "-- no args --"
52
+ assert_exit "no args exits 1" 1 bash "$DISPATCHER"
53
+
54
+ # Test 3: Unknown subcommand exits 1 with error
55
+ echo ""
56
+ echo "-- unknown subcommand --"
57
+ assert_exit "unknown subcommand exits 1" 1 bash "$DISPATCHER" bogus
58
+ assert_output_contains "unknown subcommand shows error" "unknown subcommand" bash "$DISPATCHER" bogus
59
+
60
+ # Test 4: Each stub subcommand is reachable (exits 0)
61
+ echo ""
62
+ echo "-- stub subcommands reachable --"
63
+ for cmd in workload epic dashboard add verify sync claim; do
64
+ assert_exit "$cmd exits 0" 0 bash "$DISPATCHER" "$cmd"
65
+ assert_output_contains "$cmd outputs stub message" "not implemented" bash "$DISPATCHER" "$cmd"
66
+ done
67
+
68
+ # Test 5: Sources libs without error (script runs successfully)
69
+ echo ""
70
+ echo "-- sources libs gracefully --"
71
+ assert_exit "dispatcher sources libs without error" 0 bash "$DISPATCHER" help
72
+
73
+ echo ""
74
+ echo "=== Results: $PASS passed, $FAIL failed ==="
75
+
76
+ if [[ "$FAIL" -gt 0 ]]; then
77
+ exit 1
78
+ fi
79
+ exit 0
@@ -0,0 +1,176 @@
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 bd ───────────────────────────────────────────────────────
11
+ mock_dir="$(mktemp -d)"
12
+
13
+ cat > "$mock_dir/bd" << 'MOCK'
14
+ #!/usr/bin/env bash
15
+ case "$1 $2" in
16
+ "show forge-epic1")
17
+ cat << 'SHOW'
18
+ ◐ forge-epic1 · Test Epic [● P2 · IN_PROGRESS]
19
+ Owner: devone
20
+
21
+ BLOCKS
22
+ ← ✓ forge-child1: Child 1 (closed)
23
+ ← ◐ forge-child2: Child 2 (in_progress)
24
+ ← ○ forge-child3: Child 3 (open)
25
+ SHOW
26
+ ;;
27
+ "show forge-child1")
28
+ echo "✓ forge-child1 · Child 1 [● P2 · CLOSED]"
29
+ echo "Owner: devone"
30
+ ;;
31
+ "show forge-child2")
32
+ echo "◐ forge-child2 · Child 2 [● P2 · IN_PROGRESS]"
33
+ echo "Owner: devtwo"
34
+ ;;
35
+ "show forge-child3")
36
+ echo "○ forge-child3 · Child 3 [● P3 · OPEN]"
37
+ echo "Owner: devone"
38
+ ;;
39
+ "show forge-empty")
40
+ cat << 'SHOW'
41
+ ○ forge-empty · Empty Epic [● P2 · OPEN]
42
+ Owner: devone
43
+ SHOW
44
+ ;;
45
+ "show forge-blocked")
46
+ cat << 'SHOW'
47
+ ◐ forge-blocked · Blocked Epic [● P2 · IN_PROGRESS]
48
+ Owner: devone
49
+
50
+ BLOCKS
51
+ ← ✓ forge-bchild1: Bchild 1 (closed)
52
+ ← ◐ forge-bchild2: Bchild 2 (in_progress)
53
+ ← ○ forge-bchild3: Bchild 3 (open)
54
+ SHOW
55
+ ;;
56
+ "show forge-bchild1")
57
+ echo "✓ forge-bchild1 · Bchild 1 [● P2 · CLOSED]"
58
+ echo "Owner: devone"
59
+ ;;
60
+ "show forge-bchild2")
61
+ cat << 'SHOW'
62
+ ◐ forge-bchild2 · Bchild 2 [● P2 · IN_PROGRESS]
63
+ Owner: devtwo
64
+
65
+ BLOCKED_BY
66
+ → forge-bchild1: Bchild 1
67
+ SHOW
68
+ ;;
69
+ "show forge-bchild3")
70
+ echo "○ forge-bchild3 · Bchild 3 [● P3 · OPEN]"
71
+ echo "Owner: devone"
72
+ ;;
73
+ esac
74
+ MOCK
75
+ chmod +x "$mock_dir/bd"
76
+
77
+ export BD_CMD="$mock_dir/bd"
78
+
79
+ # Source the library under test
80
+ source "$SCRIPT_DIR/lib/epic.sh"
81
+
82
+ PASS=0
83
+ FAIL=0
84
+
85
+ assert_eq() {
86
+ local label="$1" expected="$2" actual="$3"
87
+ if [[ "$actual" == "$expected" ]]; then
88
+ PASS=$((PASS + 1)); echo " PASS: $label"
89
+ else
90
+ FAIL=$((FAIL + 1)); echo " FAIL: $label (expected '$expected', got '$actual')"
91
+ fi
92
+ }
93
+
94
+ assert_contains() {
95
+ local label="$1" expected="$2" actual="$3"
96
+ if [[ "$actual" == *"$expected"* ]]; then
97
+ PASS=$((PASS + 1)); echo " PASS: $label"
98
+ else
99
+ FAIL=$((FAIL + 1)); echo " FAIL: $label (expected to contain '$expected', got '$actual')"
100
+ fi
101
+ }
102
+
103
+ assert_exit() {
104
+ local label="$1" expected="$2" actual="$3"
105
+ if [[ "$actual" -eq "$expected" ]]; then
106
+ PASS=$((PASS + 1)); echo " PASS: $label"
107
+ else
108
+ FAIL=$((FAIL + 1)); echo " FAIL: $label (expected exit $expected, got $actual)"
109
+ fi
110
+ }
111
+
112
+ # ── Test 1: Epic with mixed status children → correct progress count ─────
113
+ echo "── Test 1: Epic with mixed status children ──"
114
+ rc=0
115
+ output="$(cmd_epic "forge-epic1" 2>&1)" || rc=$?
116
+ assert_exit "exits 0" 0 "$rc"
117
+ assert_contains "shows epic id" "forge-epic1" "$output"
118
+ assert_contains "progress shows 1/3" "1/3" "$output"
119
+ assert_contains "shows closed child marker" "forge-child1" "$output"
120
+ assert_contains "shows in_progress child" "forge-child2" "$output"
121
+ assert_contains "shows open child" "forge-child3" "$output"
122
+
123
+ # ── Test 2: Per-developer breakdown correct ──────────────────────────────
124
+ echo ""
125
+ echo "── Test 2: Per-developer breakdown correct ──"
126
+ assert_contains "devone breakdown" "devone" "$output"
127
+ assert_contains "devtwo breakdown" "devtwo" "$output"
128
+
129
+ # ── Test 3: Completion percentage correct ────────────────────────────────
130
+ echo ""
131
+ echo "── Test 3: Completion percentage correct ──"
132
+ assert_contains "shows 33%" "33%" "$output"
133
+
134
+ # ── Test 4: Empty epic → No child issues ─────────────────────────────────
135
+ echo ""
136
+ echo "── Test 4: Empty epic ──"
137
+ rc=0
138
+ output_empty="$(cmd_epic "forge-empty" 2>&1)" || rc=$?
139
+ assert_exit "exits 0 for empty epic" 0 "$rc"
140
+ assert_contains "shows no child issues" "No child issues" "$output_empty"
141
+
142
+ # ── Test 5: --format=json → valid JSON with all fields ───────────────────
143
+ echo ""
144
+ echo "── Test 5: --format=json ──"
145
+ rc=0
146
+ output_json="$(cmd_epic "forge-epic1" "--format=json" 2>&1)" || rc=$?
147
+ assert_exit "json exits 0" 0 "$rc"
148
+ # Validate it's actual JSON (check for opening brace and key fields)
149
+ assert_contains "json has epic_id" "epic_id" "$output_json"
150
+ assert_contains "json has total" "total" "$output_json"
151
+ assert_contains "json has done" "done" "$output_json"
152
+ assert_contains "json has percentage" "percentage" "$output_json"
153
+ assert_contains "json has children" "children" "$output_json"
154
+ assert_contains "json has by_developer" "by_developer" "$output_json"
155
+
156
+ # ── Test 6: Missing issue-id → error ────────────────────────────────────
157
+ echo ""
158
+ echo "── Test 6: Missing issue-id ──"
159
+ rc=0
160
+ output_missing="$(cmd_epic 2>&1)" || rc=$?
161
+ assert_exit "missing id returns 1" 1 "$rc"
162
+ assert_contains "error about usage" "Usage" "$output_missing"
163
+
164
+ # ── Test 7: Blocked children shown ───────────────────────────────────────
165
+ echo ""
166
+ echo "── Test 7: Blocked children shown ──"
167
+ rc=0
168
+ output_blocked="$(cmd_epic "forge-blocked" 2>&1)" || rc=$?
169
+ assert_exit "blocked epic exits 0" 0 "$rc"
170
+ assert_contains "blocked section present" "Blocked" "$output_blocked"
171
+ assert_contains "blocked child mentioned" "forge-bchild2" "$output_blocked"
172
+
173
+ # ── Results ──────────────────────────────────────────────────────────────
174
+ echo ""
175
+ echo "Results: $PASS passed, $FAIL failed"
176
+ [[ $FAIL -eq 0 ]] && exit 0 || exit 1
@@ -0,0 +1,239 @@
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 gh (authenticated) ──────────────────────────────────────
11
+ mock_dir="$(mktemp -d)"
12
+
13
+ cat > "$mock_dir/gh" << 'MOCK'
14
+ #!/usr/bin/env bash
15
+ case "$*" in
16
+ "auth status")
17
+ echo "Logged in to github.com as devone"
18
+ exit 0
19
+ ;;
20
+ "issue edit"*)
21
+ # Accept any issue edit (sync_issue_status)
22
+ exit 0
23
+ ;;
24
+ esac
25
+ MOCK
26
+ chmod +x "$mock_dir/gh"
27
+
28
+ # ── Create mock gh (not authenticated) ──────────────────────────────────
29
+ cat > "$mock_dir/gh-noauth" << 'MOCK'
30
+ #!/usr/bin/env bash
31
+ case "$*" in
32
+ "auth status")
33
+ echo "You are not logged in" >&2
34
+ exit 1
35
+ ;;
36
+ esac
37
+ MOCK
38
+ chmod +x "$mock_dir/gh-noauth"
39
+
40
+ # ── Create mock bd (with in_progress issues having github_issue state) ──
41
+ cat > "$mock_dir/bd" << 'MOCK'
42
+ #!/usr/bin/env bash
43
+ case "$1 $2" in
44
+ "list --status=in_progress")
45
+ echo "◐ forge-aaa · Feature A"
46
+ echo "◐ forge-bbb · Feature B"
47
+ ;;
48
+ "show forge-aaa")
49
+ echo "◐ forge-aaa · Feature A [● P2 · IN_PROGRESS]"
50
+ echo "Owner: devone"
51
+ echo "Updated: 2026-03-27T10:00:00Z"
52
+ echo ""
53
+ echo "STATE"
54
+ echo " github_issue:42"
55
+ ;;
56
+ "show forge-bbb")
57
+ echo "◐ forge-bbb · Feature B [● P2 · IN_PROGRESS]"
58
+ echo "Owner: devtwo"
59
+ echo "Updated: 2026-03-27T09:00:00Z"
60
+ echo ""
61
+ echo "STATE"
62
+ echo " github_issue:55"
63
+ ;;
64
+ esac
65
+ MOCK
66
+ chmod +x "$mock_dir/bd"
67
+
68
+ # ── Create mock bd (no in_progress issues) ──────────────────────────────
69
+ cat > "$mock_dir/bd-empty" << 'MOCK'
70
+ #!/usr/bin/env bash
71
+ case "$1 $2" in
72
+ "list --status=in_progress")
73
+ echo ""
74
+ ;;
75
+ esac
76
+ MOCK
77
+ chmod +x "$mock_dir/bd-empty"
78
+
79
+ # ── Create mock bd (issue without github_issue state) ───────────────────
80
+ cat > "$mock_dir/bd-no-gh" << 'MOCK'
81
+ #!/usr/bin/env bash
82
+ case "$1 $2" in
83
+ "list --status=in_progress")
84
+ echo "◐ forge-xxx · Feature X"
85
+ ;;
86
+ "show forge-xxx")
87
+ echo "◐ forge-xxx · Feature X [● P2 · IN_PROGRESS]"
88
+ echo "Owner: devone"
89
+ echo "Updated: 2026-03-27T10:00:00Z"
90
+ ;;
91
+ esac
92
+ MOCK
93
+ chmod +x "$mock_dir/bd-no-gh"
94
+
95
+ # ── Create .beads/config.yaml for auto-sync tests ──────────────────────
96
+ mkdir -p "$TEST_TMP/.beads"
97
+ cat > "$TEST_TMP/.beads/config.yaml" << 'YAML'
98
+ team:
99
+ auto-sync: true
100
+ YAML
101
+
102
+ export GH_CMD="$mock_dir/gh"
103
+ export BD_CMD="$mock_dir/bd"
104
+ export BEADS_ROOT="$TEST_TMP"
105
+
106
+ # Source the library under test
107
+ source "$SCRIPT_DIR/lib/hooks.sh"
108
+
109
+ PASS=0
110
+ FAIL=0
111
+
112
+ assert_eq() {
113
+ local label="$1" expected="$2" actual="$3"
114
+ if [[ "$actual" == "$expected" ]]; then
115
+ PASS=$((PASS + 1)); echo " PASS: $label"
116
+ else
117
+ FAIL=$((FAIL + 1)); echo " FAIL: $label (expected '$expected', got '$actual')"
118
+ fi
119
+ }
120
+
121
+ assert_contains() {
122
+ local label="$1" expected="$2" actual="$3"
123
+ if [[ "$actual" == *"$expected"* ]]; then
124
+ PASS=$((PASS + 1)); echo " PASS: $label"
125
+ else
126
+ FAIL=$((FAIL + 1)); echo " FAIL: $label (expected to contain '$expected', got '$actual')"
127
+ fi
128
+ }
129
+
130
+ assert_not_contains() {
131
+ local label="$1" unexpected="$2" actual="$3"
132
+ if [[ "$actual" != *"$unexpected"* ]]; then
133
+ PASS=$((PASS + 1)); echo " PASS: $label"
134
+ else
135
+ FAIL=$((FAIL + 1)); echo " FAIL: $label (expected NOT to contain '$unexpected', got '$actual')"
136
+ fi
137
+ }
138
+
139
+ assert_exit() {
140
+ local label="$1" expected="$2" actual="$3"
141
+ if [[ "$actual" -eq "$expected" ]]; then
142
+ PASS=$((PASS + 1)); echo " PASS: $label"
143
+ else
144
+ FAIL=$((FAIL + 1)); echo " FAIL: $label (expected exit $expected, got $actual)"
145
+ fi
146
+ }
147
+
148
+ # ── Test 1: forge_team_sync with mock gh + bd → syncs in_progress issues ──
149
+ echo "── Test 1: forge_team_sync syncs in_progress issues ──"
150
+ export GH_CMD="$mock_dir/gh"
151
+ export BD_CMD="$mock_dir/bd"
152
+ rc=0
153
+ output="$(forge_team_sync 2>&1)" || rc=$?
154
+ assert_exit "exits 0" 0 "$rc"
155
+ assert_contains "reports synced issues" "Synced 2 issues" "$output"
156
+
157
+ # ── Test 2: forge_team_sync with gh not authenticated → error, exit 1 ──
158
+ echo ""
159
+ echo "── Test 2: forge_team_sync with gh not authenticated → error, exit 1 ──"
160
+ export GH_CMD="$mock_dir/gh-noauth"
161
+ export BD_CMD="$mock_dir/bd"
162
+ rc=0
163
+ output="$(forge_team_sync 2>&1)" || rc=$?
164
+ assert_exit "exits 1" 1 "$rc"
165
+ assert_contains "error message" "FORGE_AGENT_7f3a:ERROR:" "$output"
166
+
167
+ # ── Test 3: forge_team_sync with auto-sync: false → skip, exit 0 ────────
168
+ echo ""
169
+ echo "── Test 3: forge_team_sync with auto-sync: false → skip, exit 0 ──"
170
+ export GH_CMD="$mock_dir/gh"
171
+ export BD_CMD="$mock_dir/bd"
172
+ cat > "$TEST_TMP/.beads/config.yaml" << 'YAML'
173
+ team:
174
+ auto-sync: false
175
+ YAML
176
+ rc=0
177
+ output="$(forge_team_sync 2>&1)" || rc=$?
178
+ assert_exit "exits 0" 0 "$rc"
179
+ assert_not_contains "does not sync" "Synced" "$output"
180
+ # Restore config
181
+ cat > "$TEST_TMP/.beads/config.yaml" << 'YAML'
182
+ team:
183
+ auto-sync: true
184
+ YAML
185
+
186
+ # ── Test 4: forge_team_sync with no in_progress issues → nothing to sync ──
187
+ echo ""
188
+ echo "── Test 4: forge_team_sync with no in_progress issues → nothing to sync ──"
189
+ export GH_CMD="$mock_dir/gh"
190
+ export BD_CMD="$mock_dir/bd-empty"
191
+ rc=0
192
+ output="$(forge_team_sync 2>&1)" || rc=$?
193
+ assert_exit "exits 0" 0 "$rc"
194
+ assert_contains "nothing to sync message" "Nothing to sync" "$output"
195
+
196
+ # ── Test 5: forge_team_sync --quiet → no output on success ──────────────
197
+ echo ""
198
+ echo "── Test 5: forge_team_sync --quiet → no output on success ──"
199
+ export GH_CMD="$mock_dir/gh"
200
+ export BD_CMD="$mock_dir/bd"
201
+ rc=0
202
+ output="$(forge_team_sync --quiet 2>&1)" || rc=$?
203
+ assert_exit "exits 0" 0 "$rc"
204
+ assert_eq "no output" "" "$output"
205
+
206
+ # ── Test 6: forge_team_sync_on_stage triggers correct status sync ────────
207
+ echo ""
208
+ echo "── Test 6: forge_team_sync_on_stage triggers correct status sync per stage ──"
209
+ export GH_CMD="$mock_dir/gh"
210
+ export BD_CMD="$mock_dir/bd"
211
+ # dev transition → in_progress
212
+ rc=0
213
+ output="$(forge_team_sync_on_stage forge-aaa plan dev 2>&1)" || rc=$?
214
+ assert_exit "dev transition exits 0" 0 "$rc"
215
+ # ship transition → in_progress (still active)
216
+ rc=0
217
+ output="$(forge_team_sync_on_stage forge-aaa dev ship 2>&1)" || rc=$?
218
+ assert_exit "ship transition exits 0" 0 "$rc"
219
+ # verify transition → skip (closed by /verify)
220
+ rc=0
221
+ output="$(forge_team_sync_on_stage forge-aaa ship verify 2>&1)" || rc=$?
222
+ assert_exit "verify transition exits 0" 0 "$rc"
223
+ assert_contains "verify skips sync" "skipped" "$output"
224
+
225
+ # ── Test 7: Sync failure doesn't block (non-fatal) ──────────────────────
226
+ echo ""
227
+ echo "── Test 7: Sync failure doesn't block (non-fatal warning) ──"
228
+ export GH_CMD="$mock_dir/gh"
229
+ export BD_CMD="$mock_dir/bd-no-gh"
230
+ rc=0
231
+ output="$(forge_team_sync 2>&1)" || rc=$?
232
+ # Should still exit 0 even with sync failures (non-blocking)
233
+ assert_exit "exits 0 despite sync failure" 0 "$rc"
234
+ assert_contains "warns about failure" "warn" "$output"
235
+
236
+ # ── Results ──────────────────────────────────────────────────────────────
237
+ echo ""
238
+ echo "Results: $PASS passed, $FAIL failed"
239
+ [[ $FAIL -eq 0 ]] && exit 0 || exit 1