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,209 @@
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" "$mock_dir"' EXIT
9
+
10
+ # ── Create mock dir and log file ─────────────────────────────────────────
11
+ mock_dir="$(mktemp -d /c/tmp/forge-sync-test.XXXXXX)"
12
+ log_file="$mock_dir/calls.log"
13
+ touch "$log_file"
14
+
15
+ # ── Mock gh ──────────────────────────────────────────────────────────────
16
+ cat > "$mock_dir/gh" << 'MOCK'
17
+ #!/usr/bin/env bash
18
+ echo "$*" >> "$LOG_FILE"
19
+ case "$1 $2" in
20
+ "issue create") echo "https://github.com/test/repo/issues/42"; exit 0 ;;
21
+ "issue edit") echo ""; exit 0 ;;
22
+ "issue close") echo ""; exit 0 ;;
23
+ "issue comment") echo ""; exit 0 ;;
24
+ "api user") echo "testuser"; exit 0 ;;
25
+ esac
26
+ echo "unknown gh command: $*" >&2; exit 1
27
+ MOCK
28
+ chmod +x "$mock_dir/gh"
29
+
30
+ # ── Mock bd ──────────────────────────────────────────────────────────────
31
+ cat > "$mock_dir/bd" << 'MOCK'
32
+ #!/usr/bin/env bash
33
+ echo "$*" >> "$LOG_FILE"
34
+ case "$1" in
35
+ show)
36
+ echo "○ $2 · Test Issue [github_issue:42]"
37
+ echo "Title: Test Issue"
38
+ ;;
39
+ set-state) echo "State set" ;;
40
+ esac
41
+ MOCK
42
+ chmod +x "$mock_dir/bd"
43
+
44
+ # ── Mock bd that returns NO github_issue ─────────────────────────────────
45
+ cat > "$mock_dir/bd-no-gh" << 'MOCK'
46
+ #!/usr/bin/env bash
47
+ echo "$*" >> "$LOG_FILE"
48
+ case "$1" in
49
+ show)
50
+ echo "○ $2 · Test Issue"
51
+ echo "Title: Test Issue"
52
+ ;;
53
+ set-state) echo "State set" ;;
54
+ esac
55
+ MOCK
56
+ chmod +x "$mock_dir/bd-no-gh"
57
+
58
+ # Export env for mocks
59
+ export LOG_FILE="$log_file"
60
+ export GH_CMD="$mock_dir/gh"
61
+ export BD_CMD="$mock_dir/bd"
62
+ export TEAM_MAP_ROOT="$TEST_TMP"
63
+
64
+ # Source the library under test
65
+ source "$SCRIPT_DIR/lib/sync-github.sh"
66
+
67
+ PASS=0
68
+ FAIL=0
69
+
70
+ assert_eq() {
71
+ local label="$1" expected="$2" actual="$3"
72
+ if [[ "$actual" == "$expected" ]]; then
73
+ PASS=$((PASS + 1)); echo " PASS: $label"
74
+ else
75
+ FAIL=$((FAIL + 1)); echo " FAIL: $label (expected '$expected', got '$actual')"
76
+ fi
77
+ }
78
+
79
+ assert_contains() {
80
+ local label="$1" expected="$2" actual="$3"
81
+ if [[ "$actual" == *"$expected"* ]]; then
82
+ PASS=$((PASS + 1)); echo " PASS: $label"
83
+ else
84
+ FAIL=$((FAIL + 1)); echo " FAIL: $label (expected to contain '$expected', got '$actual')"
85
+ fi
86
+ }
87
+
88
+ assert_exit() {
89
+ local label="$1" expected="$2" actual="$3"
90
+ if [[ "$actual" -eq "$expected" ]]; then
91
+ PASS=$((PASS + 1)); echo " PASS: $label"
92
+ else
93
+ FAIL=$((FAIL + 1)); echo " FAIL: $label (expected exit $expected, got $actual)"
94
+ fi
95
+ }
96
+
97
+ # ── Test 1: sync_issue_create — creates GitHub issue, calls bd set-state ─
98
+ echo "── Test 1: sync_issue_create ──"
99
+ > "$log_file" # clear log
100
+ rc=0
101
+ sync_issue_create "beads-001" || rc=$?
102
+ assert_exit "exit code 0" 0 "$rc"
103
+ # Check gh was called with issue create
104
+ log_contents="$(cat "$log_file")"
105
+ assert_contains "gh issue create called" "issue create" "$log_contents"
106
+ assert_contains "title includes Test Issue" "Test Issue" "$log_contents"
107
+ assert_contains "body includes beads-001" "beads-001" "$log_contents"
108
+ # Check bd set-state was called with github_issue=42
109
+ assert_contains "bd set-state called" "set-state" "$log_contents"
110
+ assert_contains "github_issue=42 set" "github_issue=42" "$log_contents"
111
+
112
+ # ── Test 2: sync_issue_claim — updates GitHub assignee ───────────────────
113
+ echo ""
114
+ echo "── Test 2: sync_issue_claim ──"
115
+ > "$log_file"
116
+ unset _GITHUB_USER_CACHE
117
+ rc=0
118
+ sync_issue_claim "beads-001" || rc=$?
119
+ assert_exit "exit code 0" 0 "$rc"
120
+ log_contents="$(cat "$log_file")"
121
+ assert_contains "gh issue edit called" "issue edit" "$log_contents"
122
+ assert_contains "add-assignee used" "--add-assignee" "$log_contents"
123
+ assert_contains "assignee is testuser" "testuser" "$log_contents"
124
+
125
+ # ── Test 3: sync_issue_status — removes old labels, adds new one ─────────
126
+ echo ""
127
+ echo "── Test 3: sync_issue_status ──"
128
+ > "$log_file"
129
+ rc=0
130
+ sync_issue_status "beads-001" "in_progress" || rc=$?
131
+ assert_exit "exit code 0" 0 "$rc"
132
+ log_contents="$(cat "$log_file")"
133
+ assert_contains "gh issue edit called" "issue edit" "$log_contents"
134
+ assert_contains "removes status/open" "--remove-label status/open" "$log_contents"
135
+ assert_contains "adds status/in-progress" "--add-label status/in-progress" "$log_contents"
136
+
137
+ # ── Test 4: sync_issue_close — closes GitHub issue ───────────────────────
138
+ echo ""
139
+ echo "── Test 4: sync_issue_close ──"
140
+ > "$log_file"
141
+ rc=0
142
+ sync_issue_close "beads-001" || rc=$?
143
+ assert_exit "exit code 0" 0 "$rc"
144
+ log_contents="$(cat "$log_file")"
145
+ assert_contains "gh issue close called" "issue close" "$log_contents"
146
+ assert_contains "closes issue 42" " 42" "$log_contents"
147
+
148
+ # ── Test 5: sync_issue_deps — adds comment on GitHub issue ───────────────
149
+ echo ""
150
+ echo "── Test 5: sync_issue_deps ──"
151
+ > "$log_file"
152
+ rc=0
153
+ sync_issue_deps "beads-001" "beads-002" || rc=$?
154
+ assert_exit "exit code 0" 0 "$rc"
155
+ log_contents="$(cat "$log_file")"
156
+ assert_contains "gh issue comment called" "issue comment" "$log_contents"
157
+ assert_contains "Blocked by referenced" "Blocked by #42" "$log_contents"
158
+
159
+ # ── Test 6: _get_github_issue_number — extracts number from bd show ──────
160
+ echo ""
161
+ echo "── Test 6: _get_github_issue_number ──"
162
+ result="$(_get_github_issue_number "beads-001")"
163
+ assert_eq "extracts issue number 42" "42" "$result"
164
+
165
+ # ── Test 7: Missing GitHub issue number → returns error ──────────────────
166
+ echo ""
167
+ echo "── Test 7: Missing GitHub issue number ──"
168
+ export BD_CMD="$mock_dir/bd-no-gh"
169
+ rc=0
170
+ result="$(_get_github_issue_number "beads-999" 2>/dev/null)" || rc=$?
171
+ assert_exit "returns error when no github_issue" 1 "$rc"
172
+ # Restore normal bd mock
173
+ export BD_CMD="$mock_dir/bd"
174
+
175
+ # ── Test 8: Injection in issue title → sanitized before gh call ──────────
176
+ echo ""
177
+ echo "── Test 8: Injection in title sanitized ──"
178
+ # Create a bd mock that returns injection-laden title
179
+ cat > "$mock_dir/bd-inject" << 'MOCK'
180
+ #!/usr/bin/env bash
181
+ echo "$*" >> "$LOG_FILE"
182
+ case "$1" in
183
+ show)
184
+ echo '○ beads-inject · Evil $(rm -rf /) Issue [github_issue:99]'
185
+ echo 'Title: Evil $(rm -rf /) Issue'
186
+ ;;
187
+ set-state) echo "State set" ;;
188
+ esac
189
+ MOCK
190
+ chmod +x "$mock_dir/bd-inject"
191
+ export BD_CMD="$mock_dir/bd-inject"
192
+ > "$log_file"
193
+ rc=0
194
+ sync_issue_create "beads-inject" || rc=$?
195
+ assert_exit "exit code 0 with injection" 0 "$rc"
196
+ log_contents="$(cat "$log_file")"
197
+ # The $(...) pattern must NOT appear in the gh call
198
+ if [[ "$log_contents" == *'$(rm'* ]]; then
199
+ FAIL=$((FAIL + 1)); echo " FAIL: injection NOT sanitized — \$(rm found in gh call"
200
+ else
201
+ PASS=$((PASS + 1)); echo " PASS: injection sanitized — no \$(rm in gh call"
202
+ fi
203
+ # Restore normal bd mock
204
+ export BD_CMD="$mock_dir/bd"
205
+
206
+ # ── Results ──────────────────────────────────────────────────────────────
207
+ echo ""
208
+ echo "Results: $PASS passed, $FAIL failed"
209
+ [[ $FAIL -eq 0 ]] && exit 0 || exit 1
@@ -0,0 +1,314 @@
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
+ # Override TEAM_MAP_ROOT so tests don't touch real .beads/
11
+ export TEAM_MAP_ROOT="$TEST_TMP"
12
+
13
+ # ── Create mock directories ────────────────────────────────────────────
14
+ mock_dir="$(mktemp -d)"
15
+
16
+ # ── Mock gh (authenticated, returns user "testuser") ───────────────────
17
+ cat > "$mock_dir/gh" << 'MOCK'
18
+ #!/usr/bin/env bash
19
+ case "$1" in
20
+ auth)
21
+ if [[ "${2:-}" == "status" ]]; then
22
+ echo "github.com"
23
+ echo " ✓ Logged in to github.com account harshanandak"
24
+ exit 0
25
+ fi
26
+ ;;
27
+ api)
28
+ if [[ "${2:-}" == "user" ]]; then
29
+ echo "testuser"
30
+ exit 0
31
+ fi
32
+ ;;
33
+ issue)
34
+ if [[ "${2:-}" == "list" ]]; then
35
+ # Return JSON array of open issues
36
+ if [[ -n "${GH_MOCK_ISSUES:-}" ]]; then
37
+ echo "$GH_MOCK_ISSUES"
38
+ else
39
+ echo "[]"
40
+ fi
41
+ exit 0
42
+ fi
43
+ ;;
44
+ esac
45
+ echo "unknown gh call: $*" >&2
46
+ exit 1
47
+ MOCK
48
+ chmod +x "$mock_dir/gh"
49
+
50
+ # ── Mock gh (not authenticated) ────────────────────────────────────────
51
+ cat > "$mock_dir/gh-noauth" << 'MOCK'
52
+ #!/usr/bin/env bash
53
+ case "$1" in
54
+ auth)
55
+ if [[ "${2:-}" == "status" ]]; then
56
+ echo "You are not logged into any GitHub hosts." >&2
57
+ exit 1
58
+ fi
59
+ ;;
60
+ api)
61
+ echo "not logged in" >&2
62
+ exit 1
63
+ ;;
64
+ esac
65
+ exit 1
66
+ MOCK
67
+ chmod +x "$mock_dir/gh-noauth"
68
+
69
+ # ── Mock bd ────────────────────────────────────────────────────────────
70
+ cat > "$mock_dir/bd" << 'MOCK'
71
+ #!/usr/bin/env bash
72
+ case "$1" in
73
+ list)
74
+ if [[ -n "${BD_MOCK_OUTPUT:-}" ]]; then
75
+ echo "$BD_MOCK_OUTPUT"
76
+ fi
77
+ exit 0
78
+ ;;
79
+ show)
80
+ case "${2:-}" in
81
+ forge-aaa)
82
+ echo "◐ forge-aaa · Feature A [● P2 · IN_PROGRESS]"
83
+ echo "Owner: devone"
84
+ if [[ -z "${BD_MOCK_NO_GITHUB_AAA:-}" ]]; then
85
+ echo "State: github_issue:10"
86
+ fi
87
+ ;;
88
+ forge-bbb)
89
+ echo "○ forge-bbb · Feature B [● P2 · OPEN]"
90
+ echo "Owner: devtwo"
91
+ if [[ -z "${BD_MOCK_NO_GITHUB_BBB:-}" ]]; then
92
+ echo "State: github_issue:20"
93
+ fi
94
+ ;;
95
+ forge-ccc)
96
+ echo "◐ forge-ccc · Feature C [● P1 · IN_PROGRESS]"
97
+ echo "Owner: devone"
98
+ # forge-ccc never has github_issue (for orphan tests)
99
+ ;;
100
+ esac
101
+ exit 0
102
+ ;;
103
+ esac
104
+ exit 0
105
+ MOCK
106
+ chmod +x "$mock_dir/bd"
107
+
108
+ export GH_CMD="$mock_dir/gh"
109
+ export BD_CMD="$mock_dir/bd"
110
+
111
+ # Source the library under test
112
+ source "$SCRIPT_DIR/lib/agent-prompt.sh"
113
+ source "$SCRIPT_DIR/lib/identity.sh"
114
+ source "$SCRIPT_DIR/lib/verify.sh"
115
+
116
+ PASS=0
117
+ FAIL=0
118
+
119
+ assert_eq() {
120
+ local label="$1" expected="$2" actual="$3"
121
+ if [[ "$actual" == "$expected" ]]; then
122
+ PASS=$((PASS + 1)); echo " PASS: $label"
123
+ else
124
+ FAIL=$((FAIL + 1)); echo " FAIL: $label (expected '$expected', got '$actual')"
125
+ fi
126
+ }
127
+
128
+ assert_contains() {
129
+ local label="$1" expected="$2" actual="$3"
130
+ if [[ "$actual" == *"$expected"* ]]; then
131
+ PASS=$((PASS + 1)); echo " PASS: $label"
132
+ else
133
+ FAIL=$((FAIL + 1)); echo " FAIL: $label (expected to contain '$expected', got '$actual')"
134
+ fi
135
+ }
136
+
137
+ assert_not_contains() {
138
+ local label="$1" unexpected="$2" actual="$3"
139
+ if [[ "$actual" != *"$unexpected"* ]]; then
140
+ PASS=$((PASS + 1)); echo " PASS: $label"
141
+ else
142
+ FAIL=$((FAIL + 1)); echo " FAIL: $label (expected NOT to contain '$unexpected')"
143
+ fi
144
+ }
145
+
146
+ assert_exit() {
147
+ local label="$1" expected="$2" actual="$3"
148
+ if [[ "$actual" -eq "$expected" ]]; then
149
+ PASS=$((PASS + 1)); echo " PASS: $label"
150
+ else
151
+ FAIL=$((FAIL + 1)); echo " FAIL: $label (expected exit $expected, got $actual)"
152
+ fi
153
+ }
154
+
155
+ # ── Test 1: All clean → exit 0, all checks pass ─────────────────────────
156
+ echo "── Test 1: All clean → exit 0 ──"
157
+
158
+ # Setup: identity exists in team map
159
+ unset _GITHUB_USER_CACHE
160
+ export GH_CMD="$mock_dir/gh"
161
+ export BD_CMD="$mock_dir/bd"
162
+ team_map_add "testuser" "Test User"
163
+
164
+ # No beads issues, no github issues
165
+ export BD_MOCK_OUTPUT=""
166
+ export GH_MOCK_ISSUES="[]"
167
+
168
+ # Create empty mapping file
169
+ mkdir -p "$TEST_TMP/.github"
170
+ echo '{}' > "$TEST_TMP/.github/beads-mapping.json"
171
+
172
+ rc=0
173
+ output="$(cmd_verify 2>&1)" || rc=$?
174
+ assert_exit "all clean exits 0" 0 "$rc"
175
+ assert_contains "shows github cli check" "GitHub CLI" "$output"
176
+ assert_contains "shows identity check" "Identity" "$output"
177
+
178
+ # ── Test 2: gh not authenticated → exit 1, error message ────────────────
179
+ echo ""
180
+ echo "── Test 2: gh not authenticated → exit 1 ──"
181
+
182
+ unset _GITHUB_USER_CACHE
183
+ export GH_CMD="$mock_dir/gh-noauth"
184
+
185
+ rc=0
186
+ output="$(cmd_verify 2>&1)" || rc=$?
187
+ assert_exit "no auth exits 1" 1 "$rc"
188
+ assert_contains "error about auth" "not authenticated" "$output"
189
+ assert_contains "error has agent prefix" "FORGE_AGENT_7f3a:ERROR" "$output"
190
+
191
+ # ── Test 3: Orphan Beads issue (no github_issue state) → detected ───────
192
+ echo ""
193
+ echo "── Test 3: Orphan Beads issue detected ──"
194
+
195
+ unset _GITHUB_USER_CACHE
196
+ export GH_CMD="$mock_dir/gh"
197
+ export BD_MOCK_OUTPUT="◐ forge-ccc · Feature C"
198
+ export GH_MOCK_ISSUES="[]"
199
+
200
+ # forge-ccc has no github_issue state (see mock bd above)
201
+ rc=0
202
+ output="$(cmd_verify 2>&1)" || rc=$?
203
+ assert_exit "orphan beads exits 1" 1 "$rc"
204
+ assert_contains "orphan beads detected" "forge-ccc" "$output"
205
+ assert_contains "prompt for sync" "forge team sync-issue" "$output"
206
+
207
+ # ── Test 4: Orphan GitHub issue (not in mapping) → detected ─────────────
208
+ echo ""
209
+ echo "── Test 4: Orphan GitHub issue detected ──"
210
+
211
+ unset _GITHUB_USER_CACHE
212
+ export GH_CMD="$mock_dir/gh"
213
+ export BD_MOCK_OUTPUT=""
214
+ export GH_MOCK_ISSUES='[{"number":42,"title":"Some feature"},{"number":45,"title":"Another feature"}]'
215
+
216
+ # Mapping file has neither #42 nor #45
217
+ echo '{}' > "$TEST_TMP/.github/beads-mapping.json"
218
+
219
+ rc=0
220
+ output="$(cmd_verify 2>&1)" || rc=$?
221
+ assert_exit "orphan github exits 1" 1 "$rc"
222
+ assert_contains "orphan github #42 detected" "#42" "$output"
223
+ assert_contains "orphan github #45 detected" "#45" "$output"
224
+ assert_contains "prompt for import" "forge team import" "$output"
225
+
226
+ # ── Test 5: Assignee mismatch → info message ────────────────────────────
227
+ echo ""
228
+ echo "── Test 5: Assignee mismatch → info message ──"
229
+
230
+ unset _GITHUB_USER_CACHE
231
+ export GH_CMD="$mock_dir/gh"
232
+
233
+ # Setup: forge-aaa has github_issue:10, forge-bbb has github_issue:20
234
+ export BD_MOCK_OUTPUT="◐ forge-aaa · Feature A
235
+ ○ forge-bbb · Feature B"
236
+
237
+ # Create mapping with both issues
238
+ cat > "$TEST_TMP/.github/beads-mapping.json" << 'EOF'
239
+ {
240
+ "10": {"beads_id": "forge-aaa", "assignee": "devone"},
241
+ "20": {"beads_id": "forge-bbb", "assignee": "devtwo"},
242
+ "42": {"beads_id": "forge-ddd", "assignee": "devone"}
243
+ }
244
+ EOF
245
+
246
+ # Override gh to return mismatched assignees from github
247
+ cat > "$mock_dir/gh-mismatch" << 'MOCK'
248
+ #!/usr/bin/env bash
249
+ case "$1" in
250
+ auth)
251
+ if [[ "${2:-}" == "status" ]]; then
252
+ echo " ✓ Logged in to github.com account harshanandak"
253
+ exit 0
254
+ fi
255
+ ;;
256
+ api)
257
+ if [[ "${2:-}" == "user" ]]; then
258
+ echo "testuser"
259
+ exit 0
260
+ fi
261
+ ;;
262
+ issue)
263
+ if [[ "${2:-}" == "list" ]]; then
264
+ echo '[{"number":10,"title":"Feature A"},{"number":20,"title":"Feature B"},{"number":42,"title":"Feature D"}]'
265
+ exit 0
266
+ fi
267
+ # view --json assignees
268
+ if [[ "${2:-}" == "view" ]]; then
269
+ case "${3:-}" in
270
+ 10)
271
+ echo '{"assignees":[{"login":"devone"}]}'
272
+ ;;
273
+ 20)
274
+ echo '{"assignees":[{"login":"devthree"}]}'
275
+ ;;
276
+ 42)
277
+ echo '{"assignees":[{"login":"devone"}]}'
278
+ ;;
279
+ esac
280
+ exit 0
281
+ fi
282
+ ;;
283
+ esac
284
+ echo "unknown gh call: $*" >&2
285
+ exit 1
286
+ MOCK
287
+ chmod +x "$mock_dir/gh-mismatch"
288
+ export GH_CMD="$mock_dir/gh-mismatch"
289
+
290
+ rc=0
291
+ output="$(cmd_verify 2>&1)" || rc=$?
292
+ assert_exit "assignee mismatch exits 1" 1 "$rc"
293
+ assert_contains "mismatch info message" "mismatch" "$output"
294
+ assert_contains "mismatch shows forge-bbb" "forge-bbb" "$output"
295
+ assert_contains "mismatch shows devtwo vs devthree" "devtwo" "$output"
296
+
297
+ # ── Test 6: No issues at all (no beads, no github) → exit 0 ─────────────
298
+ echo ""
299
+ echo "── Test 6: No issues at all → exit 0 ──"
300
+
301
+ unset _GITHUB_USER_CACHE
302
+ export GH_CMD="$mock_dir/gh"
303
+ export BD_MOCK_OUTPUT=""
304
+ export GH_MOCK_ISSUES="[]"
305
+ echo '{}' > "$TEST_TMP/.github/beads-mapping.json"
306
+
307
+ rc=0
308
+ output="$(cmd_verify 2>&1)" || rc=$?
309
+ assert_exit "no issues exits 0" 0 "$rc"
310
+
311
+ # ── Results ──────────────────────────────────────────────────────────────
312
+ echo ""
313
+ echo "Results: $PASS passed, $FAIL failed"
314
+ [[ $FAIL -eq 0 ]] && exit 0 || exit 1
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
5
+ PASS=0
6
+ FAIL=0
7
+
8
+ assert_contains_file() {
9
+ local label="$1" pattern="$2" file="$3"
10
+ if grep -q "$pattern" "$file" 2>/dev/null; then
11
+ PASS=$((PASS + 1)); echo " PASS: $label"
12
+ else
13
+ FAIL=$((FAIL + 1)); echo " FAIL: $label (pattern '$pattern' not in $file)"
14
+ fi
15
+ }
16
+
17
+ echo "── /status integration ──"
18
+ STATUS="$SCRIPT_DIR/.claude/commands/status.md"
19
+ assert_contains_file "status calls workload --me" "forge-team.*workload.*--me" "$STATUS"
20
+ assert_contains_file "status calls dashboard" "forge-team.*dashboard" "$STATUS"
21
+
22
+ echo ""
23
+ echo "── /plan integration ──"
24
+ PLAN="$SCRIPT_DIR/.claude/commands/plan.md"
25
+ assert_contains_file "plan calls verify" "forge-team.*verify" "$PLAN"
26
+
27
+ echo ""
28
+ echo "── /ship integration ──"
29
+ SHIP="$SCRIPT_DIR/.claude/commands/ship.md"
30
+ assert_contains_file "ship calls sync" "forge-team.*sync" "$SHIP"
31
+ assert_contains_file "ship calls verify" "forge-team.*verify" "$SHIP"
32
+
33
+ echo ""
34
+ echo "── Sync check ──"
35
+ if node "$SCRIPT_DIR/scripts/sync-commands.js" --check 2>&1 | grep -qi 'error\|drift\|mismatch'; then
36
+ FAIL=$((FAIL + 1)); echo " FAIL: sync drift detected"
37
+ else
38
+ PASS=$((PASS + 1)); echo " PASS: all agent dirs in sync"
39
+ fi
40
+
41
+ echo ""
42
+ echo "Results: $PASS passed, $FAIL failed"
43
+ [[ $FAIL -eq 0 ]] && exit 0 || exit 1