forge-workflow 0.0.4 → 0.0.6

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 (248) hide show
  1. package/.claude/commands/dev.md +345 -340
  2. package/.claude/commands/plan.md +566 -521
  3. package/.claude/commands/premerge.md +186 -176
  4. package/.claude/commands/research.md +42 -42
  5. package/.claude/commands/review.md +448 -442
  6. package/.claude/commands/rollback.md +721 -721
  7. package/.claude/commands/ship.md +212 -164
  8. package/.claude/commands/sonarcloud.md +152 -152
  9. package/.claude/commands/status.md +90 -48
  10. package/.claude/commands/validate.md +288 -282
  11. package/.claude/commands/verify.md +269 -221
  12. package/.claude/rules/greptile-review-process.md +285 -285
  13. package/.claude/rules/workflow.md +121 -105
  14. package/.claude/scripts/greptile-resolve.sh +558 -526
  15. package/.claude/scripts/load-env.sh +32 -32
  16. package/.cline/workflows/dev.md +342 -337
  17. package/.cline/workflows/plan.md +563 -518
  18. package/.cline/workflows/premerge.md +183 -173
  19. package/.cline/workflows/research.md +39 -39
  20. package/.cline/workflows/review.md +445 -439
  21. package/.cline/workflows/rollback.md +718 -718
  22. package/.cline/workflows/ship.md +209 -161
  23. package/.cline/workflows/sonarcloud.md +146 -146
  24. package/.cline/workflows/status.md +87 -45
  25. package/.cline/workflows/validate.md +285 -279
  26. package/.cline/workflows/verify.md +266 -218
  27. package/.codex/config.toml +11 -11
  28. package/.codex/skills/dev/SKILL.md +345 -340
  29. package/.codex/skills/plan/SKILL.md +566 -521
  30. package/.codex/skills/premerge/SKILL.md +186 -176
  31. package/.codex/skills/research/SKILL.md +42 -42
  32. package/.codex/skills/review/SKILL.md +448 -442
  33. package/.codex/skills/rollback/SKILL.md +721 -721
  34. package/.codex/skills/ship/SKILL.md +212 -164
  35. package/.codex/skills/sonarcloud/SKILL.md +149 -149
  36. package/.codex/skills/status/SKILL.md +90 -48
  37. package/.codex/skills/validate/SKILL.md +288 -282
  38. package/.codex/skills/verify/SKILL.md +269 -221
  39. package/.cursor/commands/dev.md +342 -337
  40. package/.cursor/commands/plan.md +563 -518
  41. package/.cursor/commands/premerge.md +183 -173
  42. package/.cursor/commands/research.md +39 -39
  43. package/.cursor/commands/review.md +445 -439
  44. package/.cursor/commands/rollback.md +718 -718
  45. package/.cursor/commands/ship.md +209 -161
  46. package/.cursor/commands/sonarcloud.md +146 -146
  47. package/.cursor/commands/status.md +87 -45
  48. package/.cursor/commands/validate.md +285 -279
  49. package/.cursor/commands/verify.md +266 -218
  50. package/.cursor/rules/permissions-guidance.mdc +37 -37
  51. package/.forge/hooks/check-tdd.js +240 -240
  52. package/.github/PLUGIN_TEMPLATE.json +32 -32
  53. package/.github/prompts/dev.prompt.md +347 -342
  54. package/.github/prompts/plan.prompt.md +568 -523
  55. package/.github/prompts/premerge.prompt.md +188 -178
  56. package/.github/prompts/research.prompt.md +44 -44
  57. package/.github/prompts/review.prompt.md +450 -444
  58. package/.github/prompts/rollback.prompt.md +723 -723
  59. package/.github/prompts/ship.prompt.md +214 -166
  60. package/.github/prompts/sonarcloud.prompt.md +151 -151
  61. package/.github/prompts/status.prompt.md +92 -50
  62. package/.github/prompts/validate.prompt.md +290 -284
  63. package/.github/prompts/verify.prompt.md +271 -223
  64. package/.github/workflows/beads-to-github.yml +56 -0
  65. package/.github/workflows/github-to-beads.yml +97 -0
  66. package/.kilocode/workflows/dev.md +346 -341
  67. package/.kilocode/workflows/plan.md +567 -522
  68. package/.kilocode/workflows/premerge.md +187 -177
  69. package/.kilocode/workflows/research.md +43 -43
  70. package/.kilocode/workflows/review.md +449 -443
  71. package/.kilocode/workflows/rollback.md +722 -722
  72. package/.kilocode/workflows/ship.md +213 -165
  73. package/.kilocode/workflows/sonarcloud.md +150 -150
  74. package/.kilocode/workflows/status.md +91 -49
  75. package/.kilocode/workflows/validate.md +289 -283
  76. package/.kilocode/workflows/verify.md +270 -222
  77. package/.mcp.json.example +12 -12
  78. package/.opencode/commands/dev.md +345 -340
  79. package/.opencode/commands/plan.md +566 -521
  80. package/.opencode/commands/premerge.md +186 -176
  81. package/.opencode/commands/research.md +42 -42
  82. package/.opencode/commands/review.md +448 -442
  83. package/.opencode/commands/rollback.md +721 -721
  84. package/.opencode/commands/ship.md +212 -164
  85. package/.opencode/commands/sonarcloud.md +149 -149
  86. package/.opencode/commands/status.md +90 -48
  87. package/.opencode/commands/validate.md +288 -282
  88. package/.opencode/commands/verify.md +269 -221
  89. package/.roo/commands/dev.md +346 -341
  90. package/.roo/commands/plan.md +567 -522
  91. package/.roo/commands/premerge.md +187 -177
  92. package/.roo/commands/research.md +43 -43
  93. package/.roo/commands/review.md +449 -443
  94. package/.roo/commands/rollback.md +722 -722
  95. package/.roo/commands/ship.md +213 -165
  96. package/.roo/commands/sonarcloud.md +150 -150
  97. package/.roo/commands/status.md +91 -49
  98. package/.roo/commands/validate.md +289 -283
  99. package/.roo/commands/verify.md +270 -222
  100. package/AGENTS.md +272 -175
  101. package/CLAUDE.md +110 -100
  102. package/README.md +429 -416
  103. package/bin/forge-cmd.js +317 -313
  104. package/bin/forge-preflight.js +322 -309
  105. package/bin/forge.js +4765 -4303
  106. package/docs/AGENT_INSTALL_PROMPT.md +342 -342
  107. package/docs/BEADS_GITHUB_SYNC.md +251 -251
  108. package/docs/ENHANCED_ONBOARDING.md +612 -602
  109. package/docs/EXAMPLES.md +482 -482
  110. package/docs/GREPTILE_SETUP.md +400 -400
  111. package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
  112. package/docs/ROADMAP.md +359 -359
  113. package/docs/SETUP.md +663 -631
  114. package/docs/TOOLCHAIN.md +653 -630
  115. package/docs/VALIDATION.md +363 -363
  116. package/install.sh +40 -1056
  117. package/lefthook.yml +50 -39
  118. package/lib/agents/README.md +198 -198
  119. package/lib/agents/claude.plugin.json +28 -28
  120. package/lib/agents/cline.plugin.json +22 -22
  121. package/lib/agents/codex.plugin.json +19 -19
  122. package/lib/agents/copilot.plugin.json +24 -24
  123. package/lib/agents/cursor.plugin.json +25 -25
  124. package/lib/agents/kilocode.plugin.json +22 -22
  125. package/lib/agents/opencode.plugin.json +20 -20
  126. package/lib/agents/roo.plugin.json +23 -23
  127. package/lib/agents-config.js +2112 -2112
  128. package/lib/beads-health-check.js +143 -0
  129. package/lib/beads-setup.js +341 -0
  130. package/lib/beads-sync-scaffold.js +260 -0
  131. package/lib/commands/_registry.js +134 -0
  132. package/lib/commands/clean.js +181 -0
  133. package/lib/commands/dev.js +571 -513
  134. package/lib/commands/plan.js +692 -692
  135. package/lib/commands/push.js +196 -0
  136. package/lib/commands/recommend.js +119 -119
  137. package/lib/commands/ship.js +377 -377
  138. package/lib/commands/status.js +378 -378
  139. package/lib/commands/sync.js +55 -0
  140. package/lib/commands/team.js +37 -0
  141. package/lib/commands/test.js +207 -0
  142. package/lib/commands/validate.js +602 -602
  143. package/lib/commands/worktree.js +310 -0
  144. package/lib/context-merge.js +359 -359
  145. package/lib/dep-guard/analyzer.js +294 -294
  146. package/lib/dep-guard/behavior-detector.js +98 -98
  147. package/lib/dep-guard/contract-detector.js +162 -162
  148. package/lib/dep-guard/import-detector.js +498 -498
  149. package/lib/dep-guard/path-utils.js +13 -13
  150. package/lib/dep-guard/rubric.js +120 -120
  151. package/lib/dep-guard/task-parser.js +318 -318
  152. package/lib/detect-agent.js +191 -191
  153. package/lib/detect-worktree.js +47 -47
  154. package/lib/docs-command.js +51 -0
  155. package/lib/docs-copy.js +50 -0
  156. package/lib/file-hash.js +26 -26
  157. package/lib/freshness-token.js +148 -0
  158. package/lib/greptile-match.js +80 -0
  159. package/lib/husky-migration.js +450 -0
  160. package/lib/lefthook-check.js +65 -0
  161. package/lib/pat-setup.js +207 -0
  162. package/lib/plugin-catalog.js +350 -350
  163. package/lib/plugin-manager.js +166 -166
  164. package/lib/plugin-recommender.js +141 -141
  165. package/lib/project-discovery.js +491 -491
  166. package/lib/reset.js +309 -0
  167. package/lib/setup-action-log.js +139 -139
  168. package/lib/setup-summary-renderer.js +106 -106
  169. package/lib/setup-utils.js +96 -0
  170. package/lib/setup.js +192 -192
  171. package/lib/smart-merge.js +64 -0
  172. package/lib/symlink-utils.js +81 -0
  173. package/lib/task-ownership.js +117 -0
  174. package/lib/workflow-profiles.js +197 -197
  175. package/package.json +131 -128
  176. package/scripts/beads-context.sh +426 -0
  177. package/scripts/beads-context.test.js +567 -0
  178. package/scripts/behavioral-judge.sh +378 -0
  179. package/scripts/benchmark.js +85 -0
  180. package/scripts/branch-protection.js +183 -0
  181. package/scripts/check-agents.js +172 -0
  182. package/scripts/check-forge-token.js +98 -0
  183. package/scripts/commitlint.js +42 -0
  184. package/scripts/conflict-detect.sh +323 -0
  185. package/scripts/dep-guard-analyze.js +71 -0
  186. package/scripts/dep-guard.sh +789 -0
  187. package/scripts/eval_win.py +249 -0
  188. package/scripts/file-index.sh +493 -0
  189. package/scripts/forge-team/index.sh +86 -0
  190. package/scripts/forge-team/lib/agent-prompt.sh +52 -0
  191. package/scripts/forge-team/lib/claim.sh +256 -0
  192. package/scripts/forge-team/lib/dashboard.sh +341 -0
  193. package/scripts/forge-team/lib/epic.sh +332 -0
  194. package/scripts/forge-team/lib/hooks.sh +253 -0
  195. package/scripts/forge-team/lib/identity.sh +235 -0
  196. package/scripts/forge-team/lib/sync-github.sh +317 -0
  197. package/scripts/forge-team/lib/verify.sh +284 -0
  198. package/scripts/forge-team/lib/workload.sh +296 -0
  199. package/scripts/forge-team/tests/agent-prompt.test.sh +72 -0
  200. package/scripts/forge-team/tests/claim.test.sh +179 -0
  201. package/scripts/forge-team/tests/dashboard.test.sh +170 -0
  202. package/scripts/forge-team/tests/dispatcher.test.sh +79 -0
  203. package/scripts/forge-team/tests/epic.test.sh +176 -0
  204. package/scripts/forge-team/tests/hooks.test.sh +239 -0
  205. package/scripts/forge-team/tests/identity.test.sh +176 -0
  206. package/scripts/forge-team/tests/integration.test.sh +371 -0
  207. package/scripts/forge-team/tests/sync-github.test.sh +209 -0
  208. package/scripts/forge-team/tests/verify.test.sh +314 -0
  209. package/scripts/forge-team/tests/workflow-integration.test.sh +43 -0
  210. package/scripts/forge-team/tests/workload.test.sh +209 -0
  211. package/scripts/github-beads-sync/comment.mjs +64 -0
  212. package/scripts/github-beads-sync/config.mjs +148 -0
  213. package/scripts/github-beads-sync/github-api.mjs +131 -0
  214. package/scripts/github-beads-sync/index.mjs +332 -0
  215. package/scripts/github-beads-sync/label-mapper.mjs +54 -0
  216. package/scripts/github-beads-sync/mapping.mjs +78 -0
  217. package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
  218. package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
  219. package/scripts/github-beads-sync/run-bd.mjs +159 -0
  220. package/scripts/github-beads-sync/sanitize.mjs +121 -0
  221. package/scripts/github-beads-sync.config.json +26 -0
  222. package/scripts/improve-command.js +375 -0
  223. package/scripts/lib/eval-runner.js +268 -0
  224. package/scripts/lib/eval-schema.js +135 -0
  225. package/scripts/lib/eval-storage.js +78 -0
  226. package/scripts/lib/grading.js +203 -0
  227. package/scripts/lib/jsonl-lock.sh +48 -0
  228. package/scripts/lib/sanitize.sh +116 -0
  229. package/scripts/lib/transcript-parser.js +63 -0
  230. package/scripts/lint.js +47 -0
  231. package/scripts/migrate-to-bun-test.js +412 -0
  232. package/scripts/pr-coordinator.sh +706 -0
  233. package/scripts/run-command-eval.js +236 -0
  234. package/scripts/smart-status.sh +809 -0
  235. package/scripts/sync-commands.js +571 -0
  236. package/scripts/sync-utils.sh +455 -0
  237. package/scripts/test-dashboard.js +123 -0
  238. package/scripts/test.js +46 -0
  239. package/scripts/validate.sh +94 -0
  240. package/skills/parallel-deep-research/SKILL.md +108 -108
  241. package/skills/parallel-deep-research/evals/README.md +27 -27
  242. package/skills/parallel-deep-research/evals/evals.json +62 -62
  243. package/skills/sonarcloud-analysis/SKILL.md +171 -171
  244. package/skills/sonarcloud-analysis/evals/README.md +27 -27
  245. package/skills/sonarcloud-analysis/evals/evals.json +50 -50
  246. package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
  247. package/.cursor/hooks/state/continual-learning-index.json +0 -19
  248. package/.cursor/hooks/state/continual-learning.json +0 -8
@@ -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
@@ -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
+ # Override TEAM_MAP_ROOT so tests don't touch real .beads/
11
+ export TEAM_MAP_ROOT="$TEST_TMP"
12
+
13
+ # ── Create mock gh ───────────────────────────────────────────────────────
14
+ mock_dir="$(mktemp -d)"
15
+ cat > "$mock_dir/gh" << 'MOCK'
16
+ #!/usr/bin/env bash
17
+ if [[ "$1" == "api" && "$2" == "user" ]]; then
18
+ echo "testuser"
19
+ exit 0
20
+ fi
21
+ echo "unknown" >&2; exit 1
22
+ MOCK
23
+ chmod +x "$mock_dir/gh"
24
+
25
+ # Create a failing mock gh
26
+ cat > "$mock_dir/gh-fail" << 'MOCK'
27
+ #!/usr/bin/env bash
28
+ echo "not logged in" >&2
29
+ exit 1
30
+ MOCK
31
+ chmod +x "$mock_dir/gh-fail"
32
+
33
+ # Source the library under test
34
+ source "$SCRIPT_DIR/lib/identity.sh"
35
+
36
+ PASS=0
37
+ FAIL=0
38
+
39
+ assert_eq() {
40
+ local label="$1" expected="$2" actual="$3"
41
+ if [[ "$actual" == "$expected" ]]; then
42
+ PASS=$((PASS + 1)); echo " PASS: $label"
43
+ else
44
+ FAIL=$((FAIL + 1)); echo " FAIL: $label (expected '$expected', got '$actual')"
45
+ fi
46
+ }
47
+
48
+ assert_contains() {
49
+ local label="$1" expected="$2" actual="$3"
50
+ if [[ "$actual" == *"$expected"* ]]; then
51
+ PASS=$((PASS + 1)); echo " PASS: $label"
52
+ else
53
+ FAIL=$((FAIL + 1)); echo " FAIL: $label (expected to contain '$expected')"
54
+ fi
55
+ }
56
+
57
+ assert_exit() {
58
+ local label="$1" expected="$2" actual="$3"
59
+ if [[ "$actual" -eq "$expected" ]]; then
60
+ PASS=$((PASS + 1)); echo " PASS: $label"
61
+ else
62
+ FAIL=$((FAIL + 1)); echo " FAIL: $label (expected exit $expected, got $actual)"
63
+ fi
64
+ }
65
+
66
+ # ── Test 1: get_github_user with mock gh returning "testuser" ────────────
67
+ echo "── Test 1: get_github_user with mock gh ──"
68
+ unset _GITHUB_USER_CACHE
69
+ export GH_CMD="$mock_dir/gh"
70
+ result="$(get_github_user)"
71
+ rc=$?
72
+ assert_eq "returns testuser" "testuser" "$result"
73
+ assert_exit "exit code 0" 0 "$rc"
74
+
75
+ # ── Test 2: get_github_user with mock gh failing ────────────────────────
76
+ echo ""
77
+ echo "── Test 2: get_github_user with failing gh ──"
78
+ unset _GITHUB_USER_CACHE
79
+ export GH_CMD="$mock_dir/gh-fail"
80
+ result="$(get_github_user 2>/dev/null)" || true
81
+ rc=0
82
+ get_github_user >/dev/null 2>/dev/null || rc=$?
83
+ assert_exit "returns exit 1 on failure" 1 "$rc"
84
+
85
+ # ── Test 3: team_map_add creates JSONL entry ────────────────────────────
86
+ echo ""
87
+ echo "── Test 3: team_map_add creates parseable JSONL ──"
88
+ export GH_CMD="$mock_dir/gh"
89
+ unset _GITHUB_USER_CACHE
90
+ team_map_add "testuser" "Test User"
91
+ mapfile="$TEAM_MAP_ROOT/.beads/team-map.jsonl"
92
+ # Entry should be parseable JSON
93
+ last_line="$(tail -1 "$mapfile")"
94
+ parsed="$(echo "$last_line" | jq -r '.github')"
95
+ assert_eq "github field is testuser" "testuser" "$parsed"
96
+ display="$(echo "$last_line" | jq -r '.display_name')"
97
+ assert_eq "display_name is Test User" "Test User" "$display"
98
+ is_bot_val="$(echo "$last_line" | jq -r '.is_bot')"
99
+ assert_eq "is_bot is false" "false" "$is_bot_val"
100
+ updated="$(echo "$last_line" | jq -r '.updated_at')"
101
+ assert_contains "updated_at has timestamp" "20" "$updated"
102
+
103
+ # ── Test 4: team_map_read with LWW resolution ──────────────────────────
104
+ echo ""
105
+ echo "── Test 4: team_map_read with LWW resolution ──"
106
+ # Add a second entry for same user with different display name
107
+ sleep 1 # ensure different timestamp
108
+ team_map_add "testuser" "Updated User"
109
+ # Add another user
110
+ team_map_add "otheruser" "Other"
111
+ result="$(team_map_read)"
112
+ # Should have 2 unique users, with testuser having "Updated User"
113
+ count="$(echo "$result" | jq 'length')"
114
+ assert_eq "2 unique users" "2" "$count"
115
+ testuser_display="$(echo "$result" | jq -r '.[] | select(.github=="testuser") | .display_name')"
116
+ assert_eq "LWW: last entry wins" "Updated User" "$testuser_display"
117
+
118
+ # ── Test 5: team_map_get "testuser" ─────────────────────────────────────
119
+ echo ""
120
+ echo "── Test 5: team_map_get testuser ──"
121
+ result="$(team_map_get "testuser")"
122
+ gh_val="$(echo "$result" | jq -r '.github')"
123
+ assert_eq "returns correct github" "testuser" "$gh_val"
124
+
125
+ # ── Test 6: team_map_get "nonexistent" ──────────────────────────────────
126
+ echo ""
127
+ echo "── Test 6: team_map_get nonexistent ──"
128
+ result="$(team_map_get "nonexistent")"
129
+ assert_eq "returns null" "null" "$result"
130
+
131
+ # ── Test 7: is_bot "dependabot[bot]" ────────────────────────────────────
132
+ echo ""
133
+ echo "── Test 7: is_bot dependabot[bot] ──"
134
+ is_bot "dependabot[bot]"
135
+ rc=$?
136
+ assert_exit "bot detected" 0 "$rc"
137
+
138
+ # ── Test 8: is_bot "harshanandak" ───────────────────────────────────────
139
+ echo ""
140
+ echo "── Test 8: is_bot harshanandak ──"
141
+ rc=0
142
+ is_bot "harshanandak" || rc=$?
143
+ assert_exit "not a bot" 1 "$rc"
144
+
145
+ # ── Test 9: auto_detect_identity with mock gh ───────────────────────────
146
+ echo ""
147
+ echo "── Test 9: auto_detect_identity ──"
148
+ # Clean state
149
+ rm -rf "$TEAM_MAP_ROOT/.beads"
150
+ unset _GITHUB_USER_CACHE
151
+ export GH_CMD="$mock_dir/gh"
152
+ auto_detect_identity
153
+ rc=$?
154
+ assert_exit "silent success" 0 "$rc"
155
+ # Verify entry was added
156
+ result="$(team_map_get "testuser")"
157
+ gh_val="$(echo "$result" | jq -r '.github')"
158
+ assert_eq "entry created in JSONL" "testuser" "$gh_val"
159
+
160
+ # ── Test 10: Invalid username with special chars ────────────────────────
161
+ echo ""
162
+ echo "── Test 10: Invalid username rejected ──"
163
+ rc=0
164
+ team_map_add "bad user!" "Bad" 2>/dev/null || rc=$?
165
+ assert_exit "rejects invalid username" 1 "$rc"
166
+ rc=0
167
+ team_map_add "-startdash" "Bad" 2>/dev/null || rc=$?
168
+ assert_exit "rejects leading hyphen" 1 "$rc"
169
+ rc=0
170
+ team_map_add "has spaces" "Bad" 2>/dev/null || rc=$?
171
+ assert_exit "rejects spaces" 1 "$rc"
172
+
173
+ # ── Results ──────────────────────────────────────────────────────────────
174
+ echo ""
175
+ echo "Results: $PASS passed, $FAIL failed"
176
+ [[ $FAIL -eq 0 ]] && exit 0 || exit 1