beth-copilot 1.0.18 → 1.1.0

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 (291) hide show
  1. package/CHANGELOG.md +41 -28
  2. package/README.md +87 -247
  3. package/bin/cli.js +158 -358
  4. package/dist/__tests__/smoke.test.d.ts +8 -0
  5. package/dist/__tests__/smoke.test.d.ts.map +1 -0
  6. package/dist/__tests__/smoke.test.js +49 -0
  7. package/dist/__tests__/smoke.test.js.map +1 -0
  8. package/dist/cli/commands/beads.e2e.test.d.ts +13 -0
  9. package/dist/cli/commands/beads.e2e.test.d.ts.map +1 -0
  10. package/dist/cli/commands/beads.e2e.test.js +526 -0
  11. package/dist/cli/commands/beads.e2e.test.js.map +1 -0
  12. package/dist/cli/commands/cli-edge-cases.e2e.test.d.ts +32 -0
  13. package/dist/cli/commands/cli-edge-cases.e2e.test.d.ts.map +1 -0
  14. package/dist/cli/commands/cli-edge-cases.e2e.test.js +162 -0
  15. package/dist/cli/commands/cli-edge-cases.e2e.test.js.map +1 -0
  16. package/dist/cli/commands/close.d.ts +89 -0
  17. package/dist/cli/commands/close.d.ts.map +1 -0
  18. package/dist/cli/commands/close.e2e.test.d.ts +27 -0
  19. package/dist/cli/commands/close.e2e.test.d.ts.map +1 -0
  20. package/dist/cli/commands/close.e2e.test.js +252 -0
  21. package/dist/cli/commands/close.e2e.test.js.map +1 -0
  22. package/dist/cli/commands/close.js +309 -0
  23. package/dist/cli/commands/close.js.map +1 -0
  24. package/dist/cli/commands/close.test.d.ts +15 -0
  25. package/dist/cli/commands/close.test.d.ts.map +1 -0
  26. package/dist/cli/commands/close.test.js +634 -0
  27. package/dist/cli/commands/close.test.js.map +1 -0
  28. package/dist/cli/commands/doctor.d.ts +23 -0
  29. package/dist/cli/commands/doctor.d.ts.map +1 -1
  30. package/dist/cli/commands/doctor.js +93 -0
  31. package/dist/cli/commands/doctor.js.map +1 -1
  32. package/dist/cli/commands/doctor.test.js +209 -0
  33. package/dist/cli/commands/doctor.test.js.map +1 -1
  34. package/dist/cli/commands/framework-isolation.test.d.ts +30 -0
  35. package/dist/cli/commands/framework-isolation.test.d.ts.map +1 -0
  36. package/dist/cli/commands/framework-isolation.test.js +119 -0
  37. package/dist/cli/commands/framework-isolation.test.js.map +1 -0
  38. package/dist/cli/commands/help.e2e.test.js +4 -4
  39. package/dist/cli/commands/help.e2e.test.js.map +1 -1
  40. package/dist/cli/commands/init-logic.e2e.test.d.ts +37 -0
  41. package/dist/cli/commands/init-logic.e2e.test.d.ts.map +1 -0
  42. package/dist/cli/commands/init-logic.e2e.test.js +305 -0
  43. package/dist/cli/commands/init-logic.e2e.test.js.map +1 -0
  44. package/dist/cli/commands/land.d.ts +142 -0
  45. package/dist/cli/commands/land.d.ts.map +1 -0
  46. package/dist/cli/commands/land.js +647 -0
  47. package/dist/cli/commands/land.js.map +1 -0
  48. package/dist/cli/commands/land.test.d.ts +20 -0
  49. package/dist/cli/commands/land.test.d.ts.map +1 -0
  50. package/dist/cli/commands/land.test.js +622 -0
  51. package/dist/cli/commands/land.test.js.map +1 -0
  52. package/dist/cli/commands/mcp.e2e.test.js +22 -29
  53. package/dist/cli/commands/mcp.e2e.test.js.map +1 -1
  54. package/dist/cli/commands/pipeline.e2e.test.js +20 -20
  55. package/dist/cli/commands/pipeline.e2e.test.js.map +1 -1
  56. package/dist/cli/commands/pre-push-guard.d.ts +84 -0
  57. package/dist/cli/commands/pre-push-guard.d.ts.map +1 -0
  58. package/dist/cli/commands/pre-push-guard.e2e.test.d.ts +24 -0
  59. package/dist/cli/commands/pre-push-guard.e2e.test.d.ts.map +1 -0
  60. package/dist/cli/commands/pre-push-guard.e2e.test.js +171 -0
  61. package/dist/cli/commands/pre-push-guard.e2e.test.js.map +1 -0
  62. package/dist/cli/commands/pre-push-guard.js +257 -0
  63. package/dist/cli/commands/pre-push-guard.js.map +1 -0
  64. package/dist/cli/commands/pre-push-guard.test.d.ts +15 -0
  65. package/dist/cli/commands/pre-push-guard.test.d.ts.map +1 -0
  66. package/dist/cli/commands/pre-push-guard.test.js +397 -0
  67. package/dist/cli/commands/pre-push-guard.test.js.map +1 -0
  68. package/dist/cli/commands/quickstart-expanded.e2e.test.d.ts +23 -0
  69. package/dist/cli/commands/quickstart-expanded.e2e.test.d.ts.map +1 -0
  70. package/dist/cli/commands/quickstart-expanded.e2e.test.js +179 -0
  71. package/dist/cli/commands/quickstart-expanded.e2e.test.js.map +1 -0
  72. package/dist/cli/commands/quickstart.d.ts.map +1 -1
  73. package/dist/cli/commands/quickstart.js +7 -23
  74. package/dist/cli/commands/quickstart.js.map +1 -1
  75. package/dist/cli/commands/quickstart.test.js +40 -67
  76. package/dist/cli/commands/quickstart.test.js.map +1 -1
  77. package/dist/core/agents/suite.test.js +4 -2
  78. package/dist/core/agents/suite.test.js.map +1 -1
  79. package/dist/core/agents/tools.test.js +5 -1
  80. package/dist/core/agents/tools.test.js.map +1 -1
  81. package/dist/index.d.ts +3 -10
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +5 -10
  84. package/dist/index.js.map +1 -1
  85. package/package.json +15 -9
  86. package/sbom.json +2011 -819
  87. package/templates/.github/agents/beth.agent.md +220 -66
  88. package/templates/.github/agents/developer.agent.md +53 -90
  89. package/templates/.github/agents/product-manager.agent.md +15 -68
  90. package/templates/.github/agents/researcher.agent.md +20 -71
  91. package/templates/.github/agents/security-reviewer.agent.md +29 -81
  92. package/templates/.github/agents/tester.agent.md +40 -69
  93. package/templates/.github/agents/ux-designer.agent.md +20 -74
  94. package/templates/.github/copilot-instructions.md +217 -225
  95. package/templates/AGENTS.md +108 -20
  96. package/templates/mcp.json.example +0 -3
  97. package/dist/cli/commands/client-config.d.ts +0 -31
  98. package/dist/cli/commands/client-config.d.ts.map +0 -1
  99. package/dist/cli/commands/client-config.e2e.test.d.ts +0 -15
  100. package/dist/cli/commands/client-config.e2e.test.d.ts.map +0 -1
  101. package/dist/cli/commands/client-config.e2e.test.js +0 -556
  102. package/dist/cli/commands/client-config.e2e.test.js.map +0 -1
  103. package/dist/cli/commands/client-config.js +0 -73
  104. package/dist/cli/commands/client-config.js.map +0 -1
  105. package/dist/cli/commands/client-config.test.d.ts +0 -6
  106. package/dist/cli/commands/client-config.test.d.ts.map +0 -1
  107. package/dist/cli/commands/client-config.test.js +0 -133
  108. package/dist/cli/commands/client-config.test.js.map +0 -1
  109. package/dist/cli/commands/init-quickstart.e2e.test.d.ts +0 -11
  110. package/dist/cli/commands/init-quickstart.e2e.test.d.ts.map +0 -1
  111. package/dist/cli/commands/init-quickstart.e2e.test.js +0 -221
  112. package/dist/cli/commands/init-quickstart.e2e.test.js.map +0 -1
  113. package/dist/core/context.d.ts +0 -171
  114. package/dist/core/context.d.ts.map +0 -1
  115. package/dist/core/context.js +0 -353
  116. package/dist/core/context.js.map +0 -1
  117. package/dist/core/context.test.d.ts +0 -8
  118. package/dist/core/context.test.d.ts.map +0 -1
  119. package/dist/core/context.test.js +0 -253
  120. package/dist/core/context.test.js.map +0 -1
  121. package/dist/core/handoffs.d.ts +0 -151
  122. package/dist/core/handoffs.d.ts.map +0 -1
  123. package/dist/core/handoffs.js +0 -220
  124. package/dist/core/handoffs.js.map +0 -1
  125. package/dist/core/handoffs.test.d.ts +0 -8
  126. package/dist/core/handoffs.test.d.ts.map +0 -1
  127. package/dist/core/handoffs.test.js +0 -231
  128. package/dist/core/handoffs.test.js.map +0 -1
  129. package/dist/core/orchestrator.d.ts +0 -246
  130. package/dist/core/orchestrator.d.ts.map +0 -1
  131. package/dist/core/orchestrator.js +0 -514
  132. package/dist/core/orchestrator.js.map +0 -1
  133. package/dist/core/orchestrator.test.d.ts +0 -8
  134. package/dist/core/orchestrator.test.d.ts.map +0 -1
  135. package/dist/core/orchestrator.test.js +0 -517
  136. package/dist/core/orchestrator.test.js.map +0 -1
  137. package/dist/core/router.d.ts +0 -102
  138. package/dist/core/router.d.ts.map +0 -1
  139. package/dist/core/router.js +0 -178
  140. package/dist/core/router.js.map +0 -1
  141. package/dist/core/router.test.d.ts +0 -8
  142. package/dist/core/router.test.d.ts.map +0 -1
  143. package/dist/core/router.test.js +0 -215
  144. package/dist/core/router.test.js.map +0 -1
  145. package/dist/init.test.js +0 -288
  146. package/dist/providers/azure.d.ts +0 -147
  147. package/dist/providers/azure.d.ts.map +0 -1
  148. package/dist/providers/azure.js +0 -491
  149. package/dist/providers/azure.js.map +0 -1
  150. package/dist/providers/azure.test.d.ts +0 -11
  151. package/dist/providers/azure.test.d.ts.map +0 -1
  152. package/dist/providers/azure.test.js +0 -330
  153. package/dist/providers/azure.test.js.map +0 -1
  154. package/dist/providers/config.d.ts +0 -87
  155. package/dist/providers/config.d.ts.map +0 -1
  156. package/dist/providers/config.js +0 -193
  157. package/dist/providers/config.js.map +0 -1
  158. package/dist/providers/config.test.d.ts +0 -7
  159. package/dist/providers/config.test.d.ts.map +0 -1
  160. package/dist/providers/config.test.js +0 -370
  161. package/dist/providers/config.test.js.map +0 -1
  162. package/dist/providers/index.d.ts +0 -18
  163. package/dist/providers/index.d.ts.map +0 -1
  164. package/dist/providers/index.js +0 -14
  165. package/dist/providers/index.js.map +0 -1
  166. package/dist/providers/interface.d.ts +0 -191
  167. package/dist/providers/interface.d.ts.map +0 -1
  168. package/dist/providers/interface.js +0 -94
  169. package/dist/providers/interface.js.map +0 -1
  170. package/dist/providers/retry.d.ts +0 -128
  171. package/dist/providers/retry.d.ts.map +0 -1
  172. package/dist/providers/retry.js +0 -205
  173. package/dist/providers/retry.js.map +0 -1
  174. package/dist/providers/retry.test.d.ts +0 -7
  175. package/dist/providers/retry.test.d.ts.map +0 -1
  176. package/dist/providers/retry.test.js +0 -439
  177. package/dist/providers/retry.test.js.map +0 -1
  178. package/dist/providers/streaming.d.ts +0 -157
  179. package/dist/providers/streaming.d.ts.map +0 -1
  180. package/dist/providers/streaming.js +0 -233
  181. package/dist/providers/streaming.js.map +0 -1
  182. package/dist/providers/streaming.test.d.ts +0 -7
  183. package/dist/providers/streaming.test.d.ts.map +0 -1
  184. package/dist/providers/streaming.test.js +0 -372
  185. package/dist/providers/streaming.test.js.map +0 -1
  186. package/dist/providers/types.d.ts +0 -209
  187. package/dist/providers/types.d.ts.map +0 -1
  188. package/dist/providers/types.js +0 -53
  189. package/dist/providers/types.js.map +0 -1
  190. package/dist/providers/types.test.d.ts +0 -7
  191. package/dist/providers/types.test.d.ts.map +0 -1
  192. package/dist/providers/types.test.js +0 -141
  193. package/dist/providers/types.test.js.map +0 -1
  194. package/dist/tools/cli/beads.d.ts +0 -27
  195. package/dist/tools/cli/beads.d.ts.map +0 -1
  196. package/dist/tools/cli/beads.js +0 -172
  197. package/dist/tools/cli/beads.js.map +0 -1
  198. package/dist/tools/cli/beads.test.d.ts +0 -8
  199. package/dist/tools/cli/beads.test.d.ts.map +0 -1
  200. package/dist/tools/cli/beads.test.js +0 -264
  201. package/dist/tools/cli/beads.test.js.map +0 -1
  202. package/dist/tools/cli/editFile.d.ts +0 -17
  203. package/dist/tools/cli/editFile.d.ts.map +0 -1
  204. package/dist/tools/cli/editFile.js +0 -125
  205. package/dist/tools/cli/editFile.js.map +0 -1
  206. package/dist/tools/cli/editFile.test.d.ts +0 -8
  207. package/dist/tools/cli/editFile.test.d.ts.map +0 -1
  208. package/dist/tools/cli/editFile.test.js +0 -177
  209. package/dist/tools/cli/editFile.test.js.map +0 -1
  210. package/dist/tools/cli/readFile.d.ts +0 -25
  211. package/dist/tools/cli/readFile.d.ts.map +0 -1
  212. package/dist/tools/cli/readFile.js +0 -118
  213. package/dist/tools/cli/readFile.js.map +0 -1
  214. package/dist/tools/cli/readFile.test.d.ts +0 -8
  215. package/dist/tools/cli/readFile.test.d.ts.map +0 -1
  216. package/dist/tools/cli/readFile.test.js +0 -194
  217. package/dist/tools/cli/readFile.test.js.map +0 -1
  218. package/dist/tools/cli/search.d.ts +0 -16
  219. package/dist/tools/cli/search.d.ts.map +0 -1
  220. package/dist/tools/cli/search.js +0 -261
  221. package/dist/tools/cli/search.js.map +0 -1
  222. package/dist/tools/cli/search.test.d.ts +0 -8
  223. package/dist/tools/cli/search.test.d.ts.map +0 -1
  224. package/dist/tools/cli/search.test.js +0 -172
  225. package/dist/tools/cli/search.test.js.map +0 -1
  226. package/dist/tools/cli/subagent.d.ts +0 -43
  227. package/dist/tools/cli/subagent.d.ts.map +0 -1
  228. package/dist/tools/cli/subagent.js +0 -99
  229. package/dist/tools/cli/subagent.js.map +0 -1
  230. package/dist/tools/cli/subagent.test.d.ts +0 -8
  231. package/dist/tools/cli/subagent.test.d.ts.map +0 -1
  232. package/dist/tools/cli/subagent.test.js +0 -190
  233. package/dist/tools/cli/subagent.test.js.map +0 -1
  234. package/dist/tools/cli/terminal.d.ts +0 -19
  235. package/dist/tools/cli/terminal.d.ts.map +0 -1
  236. package/dist/tools/cli/terminal.js +0 -164
  237. package/dist/tools/cli/terminal.js.map +0 -1
  238. package/dist/tools/cli/terminal.test.d.ts +0 -8
  239. package/dist/tools/cli/terminal.test.d.ts.map +0 -1
  240. package/dist/tools/cli/terminal.test.js +0 -161
  241. package/dist/tools/cli/terminal.test.js.map +0 -1
  242. package/dist/tools/index.d.ts +0 -25
  243. package/dist/tools/index.d.ts.map +0 -1
  244. package/dist/tools/index.js +0 -41
  245. package/dist/tools/index.js.map +0 -1
  246. package/dist/tools/interface.d.ts +0 -64
  247. package/dist/tools/interface.d.ts.map +0 -1
  248. package/dist/tools/interface.js +0 -37
  249. package/dist/tools/interface.js.map +0 -1
  250. package/dist/tools/interface.test.d.ts +0 -7
  251. package/dist/tools/interface.test.d.ts.map +0 -1
  252. package/dist/tools/interface.test.js +0 -179
  253. package/dist/tools/interface.test.js.map +0 -1
  254. package/dist/tools/mcp/bridge.d.ts +0 -48
  255. package/dist/tools/mcp/bridge.d.ts.map +0 -1
  256. package/dist/tools/mcp/bridge.js +0 -128
  257. package/dist/tools/mcp/bridge.js.map +0 -1
  258. package/dist/tools/mcp/bridge.test.d.ts +0 -8
  259. package/dist/tools/mcp/bridge.test.d.ts.map +0 -1
  260. package/dist/tools/mcp/bridge.test.js +0 -300
  261. package/dist/tools/mcp/bridge.test.js.map +0 -1
  262. package/dist/tools/mcp/client.d.ts +0 -135
  263. package/dist/tools/mcp/client.d.ts.map +0 -1
  264. package/dist/tools/mcp/client.js +0 -263
  265. package/dist/tools/mcp/client.js.map +0 -1
  266. package/dist/tools/mcp/client.test.d.ts +0 -8
  267. package/dist/tools/mcp/client.test.d.ts.map +0 -1
  268. package/dist/tools/mcp/client.test.js +0 -390
  269. package/dist/tools/mcp/client.test.js.map +0 -1
  270. package/dist/tools/registry.d.ts +0 -82
  271. package/dist/tools/registry.d.ts.map +0 -1
  272. package/dist/tools/registry.js +0 -99
  273. package/dist/tools/registry.js.map +0 -1
  274. package/dist/tools/registry.test.d.ts +0 -7
  275. package/dist/tools/registry.test.d.ts.map +0 -1
  276. package/dist/tools/registry.test.js +0 -199
  277. package/dist/tools/registry.test.js.map +0 -1
  278. package/dist/tools/suite.test.d.ts +0 -11
  279. package/dist/tools/suite.test.d.ts.map +0 -1
  280. package/dist/tools/suite.test.js +0 -119
  281. package/dist/tools/suite.test.js.map +0 -1
  282. package/dist/tools/types.d.ts +0 -75
  283. package/dist/tools/types.d.ts.map +0 -1
  284. package/dist/tools/types.js +0 -30
  285. package/dist/tools/types.js.map +0 -1
  286. package/dist/tools/types.test.d.ts +0 -7
  287. package/dist/tools/types.test.d.ts.map +0 -1
  288. package/dist/tools/types.test.js +0 -178
  289. package/dist/tools/types.test.js.map +0 -1
  290. package/templates/.vscode/mcp.json +0 -20
  291. package/templates/CLAUDE.md +0 -129
@@ -0,0 +1,171 @@
1
+ /**
2
+ * E2E tests for pre-push-guard command.
3
+ *
4
+ * Runs the actual `beth-copilot pre-push-guard` binary and validates output/behavior.
5
+ *
6
+ * beth-ywg.2: pre-push-guard has 56 unit tests but zero E2E — this fills the gap.
7
+ *
8
+ * Repro steps:
9
+ * 1. Build: npm run build
10
+ * 2. Run: npx vitest run src/cli/commands/pre-push-guard.e2e.test.ts
11
+ * OR: npx vitest run --config vitest.e2e.config.ts
12
+ *
13
+ * Test cases:
14
+ * - Running on an epic branch → exit 0, no errors
15
+ * - Running with BETH_SKIP_PUSH_GUARD=1 → exit 0, bypass
16
+ * - Running on a protected branch (main) → exit 1, error message
17
+ * - Running on unrecognized branch name → exit 0 with warning
18
+ * - Running outside git repo → graceful handling
19
+ * - Output contains expected formatting
20
+ *
21
+ * Expected outcomes documented inline per test case.
22
+ */
23
+ import { describe, it, beforeEach, afterEach } from 'node:test';
24
+ import assert from 'node:assert';
25
+ import { execSync, spawnSync } from 'child_process';
26
+ import { resolve, join } from 'path';
27
+ import { mkdirSync, rmSync, existsSync, mkdtempSync } from 'fs';
28
+ import { tmpdir } from 'os';
29
+ const CLI_PATH = resolve(join(import.meta.dirname, '..', '..', '..', 'bin', 'cli.js'));
30
+ /**
31
+ * Run pre-push-guard via the CLI binary.
32
+ * Accepts optional env overrides and optional stdin for git ref simulation.
33
+ */
34
+ function runGuard(options = {}) {
35
+ const env = { ...process.env, NO_COLOR: '1', ...options.env };
36
+ const result = spawnSync('node', [CLI_PATH, 'pre-push-guard'], {
37
+ cwd: options.cwd || process.cwd(),
38
+ encoding: 'utf-8',
39
+ env,
40
+ input: options.stdin || '',
41
+ timeout: 15000,
42
+ });
43
+ return {
44
+ stdout: result.stdout || '',
45
+ stderr: result.stderr || '',
46
+ code: result.status ?? 1,
47
+ };
48
+ }
49
+ function createGitRepoOnBranch(branchName, prefix) {
50
+ const tmpDir = mkdtempSync(join(tmpdir(), prefix));
51
+ execSync('git init', { cwd: tmpDir, stdio: 'ignore' });
52
+ execSync('git config user.name "Test"', { cwd: tmpDir, stdio: 'ignore' });
53
+ execSync('git config user.email "test@test.com"', { cwd: tmpDir, stdio: 'ignore' });
54
+ execSync(`git checkout --orphan ${branchName}`, { cwd: tmpDir, stdio: 'ignore' });
55
+ execSync('git commit --allow-empty -m "init"', { cwd: tmpDir, stdio: 'ignore' });
56
+ return tmpDir;
57
+ }
58
+ describe('pre-push-guard command E2E', () => {
59
+ describe('valid epic branch repo', () => {
60
+ let tmpDir;
61
+ beforeEach(() => {
62
+ tmpDir = createGitRepoOnBranch('epic/beth-ywg', 'beth-guard-epic-');
63
+ });
64
+ afterEach(() => {
65
+ if (existsSync(tmpDir)) {
66
+ rmSync(tmpDir, { recursive: true, force: true });
67
+ }
68
+ });
69
+ // Expected: the guard should pass on a valid epic branch.
70
+ it('should exit 0 on the current epic branch', () => {
71
+ const result = runGuard({ cwd: tmpDir });
72
+ assert.strictEqual(result.code, 0, 'Should exit 0 on a valid epic branch');
73
+ });
74
+ it('should not produce error output on a valid branch', () => {
75
+ const result = runGuard({ cwd: tmpDir });
76
+ // Errors go to stderr — should be empty or just warnings
77
+ const hasBlockingError = result.stderr.includes('blocked') || result.stderr.includes('BLOCKED');
78
+ assert.ok(!hasBlockingError, 'Should not have blocking errors on epic branch');
79
+ });
80
+ });
81
+ describe('bypass with environment variable', () => {
82
+ // Expected: exit 0, skip all checks
83
+ it('should exit 0 when BETH_SKIP_PUSH_GUARD=1', () => {
84
+ const result = runGuard({ env: { BETH_SKIP_PUSH_GUARD: '1' } });
85
+ assert.strictEqual(result.code, 0, 'Should exit 0 when bypass env is set');
86
+ });
87
+ it('should indicate bypass in output', () => {
88
+ const result = runGuard({ env: { BETH_SKIP_PUSH_GUARD: '1' } });
89
+ const combined = result.stdout + result.stderr;
90
+ assert.ok(combined.includes('bypass') || combined.includes('skip') || combined.includes('SKIP') || result.code === 0, 'Should indicate bypass or just exit cleanly');
91
+ });
92
+ });
93
+ describe('outside git repo', () => {
94
+ let tmpDir;
95
+ beforeEach(() => {
96
+ tmpDir = mkdtempSync(join(tmpdir(), 'beth-guard-test-'));
97
+ mkdirSync(tmpDir, { recursive: true });
98
+ });
99
+ afterEach(() => {
100
+ if (existsSync(tmpDir)) {
101
+ rmSync(tmpDir, { recursive: true, force: true });
102
+ }
103
+ });
104
+ // Expected: graceful handling — no crash on missing git
105
+ it('should handle non-git directory gracefully', () => {
106
+ const result = runGuard({ cwd: tmpDir });
107
+ // Should not crash with unhandled exception
108
+ assert.ok(typeof result.code === 'number', 'Should exit with a numeric code, not crash');
109
+ });
110
+ });
111
+ describe('protected branch simulation', () => {
112
+ let tmpDir;
113
+ beforeEach(() => {
114
+ tmpDir = createGitRepoOnBranch('main', 'beth-guard-main-');
115
+ });
116
+ afterEach(() => {
117
+ if (existsSync(tmpDir)) {
118
+ rmSync(tmpDir, { recursive: true, force: true });
119
+ }
120
+ });
121
+ // Expected: exit 1, error about protected branch
122
+ it('should block push from main branch', () => {
123
+ const result = runGuard({ cwd: tmpDir });
124
+ assert.strictEqual(result.code, 1, 'Should exit 1 when on main branch');
125
+ const combined = result.stdout + result.stderr;
126
+ assert.ok(combined.includes('blocked') || combined.includes('main') || combined.includes('BLOCK'), 'Should mention main is blocked');
127
+ });
128
+ // Expected: exit 1, suggest using epic branch
129
+ it('should suggest using an epic branch', () => {
130
+ const result = runGuard({ cwd: tmpDir });
131
+ const combined = result.stdout + result.stderr;
132
+ assert.ok(combined.includes('epic') || combined.includes('PR'), 'Should suggest using an epic branch or PR');
133
+ });
134
+ });
135
+ describe('unrecognized branch naming', () => {
136
+ let tmpDir;
137
+ beforeEach(() => {
138
+ tmpDir = createGitRepoOnBranch('my-random-branch', 'beth-guard-weird-');
139
+ });
140
+ afterEach(() => {
141
+ if (existsSync(tmpDir)) {
142
+ rmSync(tmpDir, { recursive: true, force: true });
143
+ }
144
+ });
145
+ // Expected: exit 0 (warnings only, not blocking), with naming convention suggestion
146
+ it('should exit 0 but warn about non-epic branch naming', () => {
147
+ const result = runGuard({ cwd: tmpDir });
148
+ assert.strictEqual(result.code, 0, 'Should exit 0 for unrecognized but non-protected branch');
149
+ });
150
+ it('should produce a warning about naming convention', () => {
151
+ const result = runGuard({ cwd: tmpDir });
152
+ const combined = result.stdout + result.stderr;
153
+ assert.ok(combined.includes('convention') || combined.includes('warning') || combined.includes('Warning') || combined.includes('doesn\'t follow'), 'Should warn about naming convention');
154
+ });
155
+ });
156
+ describe('stdin ref parsing (simulated git push input)', () => {
157
+ // Git sends refs on stdin during pre-push. Test that parsing works end-to-end.
158
+ it('should process refs from stdin without crashing', () => {
159
+ const stdin = 'refs/heads/epic/beth-ywg abc123 refs/heads/epic/beth-ywg def456\n';
160
+ const result = runGuard({ stdin });
161
+ // Should not crash — the exit code depends on current branch
162
+ assert.ok(typeof result.code === 'number', 'Should handle stdin refs gracefully');
163
+ });
164
+ it('should block when stdin refs target main', () => {
165
+ const stdin = 'refs/heads/epic/beth-ywg abc123 refs/heads/main def456\n';
166
+ const result = runGuard({ stdin });
167
+ assert.strictEqual(result.code, 1, 'Should block push targeting main via stdin refs');
168
+ });
169
+ });
170
+ });
171
+ //# sourceMappingURL=pre-push-guard.e2e.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pre-push-guard.e2e.test.js","sourceRoot":"","sources":["../../../src/cli/commands/pre-push-guard.e2e.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Pre-Push Guard — Branch discipline enforcement
3
+ *
4
+ * Validates before push:
5
+ * - No direct pushes to main/master (BLOCKS)
6
+ * - Current branch follows epic/<id> convention (WARNING)
7
+ * - Open in-progress beads issues reported (WARNING)
8
+ * - Bypassed with BETH_SKIP_PUSH_GUARD=1 environment variable
9
+ */
10
+ import { execFileSync } from 'child_process';
11
+ const COLORS = {
12
+ reset: '\x1b[0m',
13
+ bright: '\x1b[1m',
14
+ red: '\x1b[31m',
15
+ yellow: '\x1b[33m',
16
+ cyan: '\x1b[36m',
17
+ };
18
+ /** Protected branch names that cannot receive direct pushes. */
19
+ const PROTECTED_BRANCHES = ['main', 'master'];
20
+ /** Epic branch naming convention: epic/<rig>-<hash> */
21
+ const EPIC_BRANCH_PATTERN = /^epic\/[a-z]+-[a-z0-9]+$/;
22
+ /** Release branches are also valid push targets. */
23
+ const RELEASE_BRANCH_PATTERN = /^release\/v?\d+/;
24
+ /**
25
+ * Parse pre-push stdin input into structured refs.
26
+ * Git sends: <local ref> <local SHA> <remote ref> <remote SHA>
27
+ * One line per ref being pushed.
28
+ */
29
+ export function parsePushRefs(stdin) {
30
+ return stdin
31
+ .trim()
32
+ .split('\n')
33
+ .filter((line) => line.trim().length > 0)
34
+ .map((line) => {
35
+ const parts = line.trim().split(/\s+/);
36
+ return {
37
+ localRef: parts[0] || '',
38
+ localSha: parts[1] || '',
39
+ remoteRef: parts[2] || '',
40
+ remoteSha: parts[3] || '',
41
+ };
42
+ });
43
+ }
44
+ /**
45
+ * Extract branch name from a Git ref.
46
+ * refs/heads/main → main
47
+ * refs/heads/epic/beth-abc123 → epic/beth-abc123
48
+ */
49
+ export function extractBranchName(ref) {
50
+ const prefix = 'refs/heads/';
51
+ if (ref.startsWith(prefix)) {
52
+ return ref.slice(prefix.length);
53
+ }
54
+ return ref;
55
+ }
56
+ /**
57
+ * Check if a branch name is protected (main, master).
58
+ */
59
+ export function isProtectedBranch(branch) {
60
+ return PROTECTED_BRANCHES.includes(branch);
61
+ }
62
+ /**
63
+ * Check if the branch follows the epic/<id> convention.
64
+ */
65
+ export function isEpicBranch(branch) {
66
+ return EPIC_BRANCH_PATTERN.test(branch);
67
+ }
68
+ /**
69
+ * Check if the branch is a release branch.
70
+ */
71
+ export function isReleaseBranch(branch) {
72
+ return RELEASE_BRANCH_PATTERN.test(branch);
73
+ }
74
+ /**
75
+ * Check if the branch follows any recognized naming convention.
76
+ */
77
+ export function isRecognizedBranch(branch) {
78
+ return isEpicBranch(branch) || isReleaseBranch(branch) || isProtectedBranch(branch);
79
+ }
80
+ /**
81
+ * Get the current Git branch name.
82
+ * Returns null if not in a git repo or in detached HEAD state.
83
+ */
84
+ export function getCurrentBranch() {
85
+ try {
86
+ const result = execFileSync('git', ['branch', '--show-current'], {
87
+ encoding: 'utf-8',
88
+ timeout: 5000,
89
+ stdio: ['pipe', 'pipe', 'pipe'],
90
+ }).trim();
91
+ return result || null;
92
+ }
93
+ catch {
94
+ return null;
95
+ }
96
+ }
97
+ /**
98
+ * Get beads issues with in_progress status.
99
+ * Returns empty array if bd is unavailable or errors.
100
+ */
101
+ export function getInProgressIssues() {
102
+ try {
103
+ const output = execFileSync('bd', ['list', '--json'], {
104
+ encoding: 'utf-8',
105
+ timeout: 10000,
106
+ stdio: ['pipe', 'pipe', 'pipe'],
107
+ });
108
+ const parsed = JSON.parse(output);
109
+ if (!Array.isArray(parsed))
110
+ return [];
111
+ return parsed
112
+ .filter((item) => typeof item === 'object' &&
113
+ item !== null &&
114
+ 'id' in item &&
115
+ 'title' in item &&
116
+ 'status' in item &&
117
+ item.status === 'in_progress')
118
+ .map((item) => ({ id: item.id, title: item.title }));
119
+ }
120
+ catch {
121
+ return [];
122
+ }
123
+ }
124
+ /**
125
+ * Run all pre-push guard checks.
126
+ *
127
+ * @param currentBranch - Current Git branch name
128
+ * @param refs - Optional parsed push refs from Git stdin (for remote ref validation)
129
+ * @param checkBeads - Whether to check for in-progress beads issues
130
+ * @returns GuardResult with allowed status and diagnostics
131
+ */
132
+ export function runGuard(currentBranch, refs, checkBeads = true) {
133
+ const errors = [];
134
+ const warnings = [];
135
+ // Check 1: No pushing to protected branches via remote refs
136
+ if (refs && refs.length > 0) {
137
+ for (const ref of refs) {
138
+ const targetBranch = extractBranchName(ref.remoteRef);
139
+ if (isProtectedBranch(targetBranch)) {
140
+ errors.push(`Direct push to '${targetBranch}' is blocked. Use a PR from your epic branch.`);
141
+ }
142
+ }
143
+ }
144
+ // Check 1b: Also block if current branch IS a protected branch
145
+ // (catches the common case without needing stdin refs)
146
+ if (currentBranch && isProtectedBranch(currentBranch)) {
147
+ const msg = `Pushing from '${currentBranch}' is blocked. Work on an epic branch.`;
148
+ if (!errors.some((e) => e.includes(currentBranch))) {
149
+ errors.push(msg);
150
+ }
151
+ }
152
+ // Check 2: Current branch should follow epic convention
153
+ if (currentBranch && !isRecognizedBranch(currentBranch)) {
154
+ warnings.push(`Branch '${currentBranch}' doesn't follow the epic/<id> convention. Consider renaming.`);
155
+ }
156
+ // Check 3: Warn about in-progress beads issues (soft check)
157
+ if (checkBeads) {
158
+ try {
159
+ const inProgress = getInProgressIssues();
160
+ if (inProgress.length > 0) {
161
+ warnings.push(`${inProgress.length} issue${inProgress.length === 1 ? '' : 's'} still in_progress:`);
162
+ for (const issue of inProgress) {
163
+ warnings.push(` ◐ ${issue.id}: ${issue.title}`);
164
+ }
165
+ warnings.push('Consider closing completed work before pushing.');
166
+ }
167
+ }
168
+ catch {
169
+ // bd not available — skip silently
170
+ }
171
+ }
172
+ return {
173
+ allowed: errors.length === 0,
174
+ errors,
175
+ warnings,
176
+ };
177
+ }
178
+ /**
179
+ * Main entry point for the pre-push guard command.
180
+ * Reads refs from stdin (if available) and runs all checks.
181
+ */
182
+ export async function prePushGuard(stdinInput) {
183
+ // Check bypass
184
+ if (process.env.BETH_SKIP_PUSH_GUARD === '1') {
185
+ console.error(`${COLORS.yellow}⚠ Pre-push guard bypassed (BETH_SKIP_PUSH_GUARD=1)${COLORS.reset}`);
186
+ return;
187
+ }
188
+ // Read stdin if not provided (Git pre-push pipes refs via stdin)
189
+ let stdin = stdinInput;
190
+ if (stdin === undefined) {
191
+ const fs = await import('fs');
192
+ try {
193
+ stdin = fs.readFileSync(0, 'utf-8');
194
+ }
195
+ catch {
196
+ stdin = '';
197
+ }
198
+ }
199
+ const refs = stdin.trim() ? parsePushRefs(stdin) : undefined;
200
+ const currentBranch = getCurrentBranch();
201
+ const result = runGuard(currentBranch, refs);
202
+ // Print warnings
203
+ for (const warning of result.warnings) {
204
+ console.error(`${COLORS.yellow}⚠ ${warning}${COLORS.reset}`);
205
+ }
206
+ // Print errors and exit non-zero to block push
207
+ if (!result.allowed) {
208
+ console.error('');
209
+ for (const error of result.errors) {
210
+ console.error(`${COLORS.red}✗ ${error}${COLORS.reset}`);
211
+ }
212
+ console.error(`\n${COLORS.yellow}Set BETH_SKIP_PUSH_GUARD=1 to bypass.${COLORS.reset}`);
213
+ process.exit(1);
214
+ }
215
+ }
216
+ /**
217
+ * Generate the shell script content to append to .beads/hooks/pre-push.
218
+ * Pure shell — no Node dependency at hook time for speed.
219
+ */
220
+ export function generateHookScript() {
221
+ return `
222
+ # --- BEGIN BETH GUARD ---
223
+ # Branch discipline enforcement — installed by beth-copilot
224
+ # Bypass: BETH_SKIP_PUSH_GUARD=1 git push
225
+ if [ "\$BETH_SKIP_PUSH_GUARD" = "1" ]; then
226
+ echo "⚠ Pre-push guard bypassed (BETH_SKIP_PUSH_GUARD=1)" >&2
227
+ else
228
+ _beth_branch=\$(git branch --show-current 2>/dev/null)
229
+
230
+ # Block pushes from protected branches
231
+ case "\$_beth_branch" in
232
+ main|master)
233
+ echo "✗ Pushing from '\$_beth_branch' is blocked. Work on an epic branch." >&2
234
+ echo " Set BETH_SKIP_PUSH_GUARD=1 to bypass." >&2
235
+ exit 1
236
+ ;;
237
+ esac
238
+
239
+ # Warn if not on an epic or release branch
240
+ case "\$_beth_branch" in
241
+ epic/*) ;;
242
+ release/*) ;;
243
+ "")
244
+ echo "⚠ Detached HEAD — no branch name. Proceeding anyway." >&2
245
+ ;;
246
+ *)
247
+ echo "⚠ Branch '\$_beth_branch' doesn't follow the epic/<id> convention." >&2
248
+ ;;
249
+ esac
250
+ fi
251
+ # --- END BETH GUARD ---
252
+ `;
253
+ }
254
+ /** Marker used to detect if bethguard is already installed in a hook file. */
255
+ export const BETH_GUARD_BEGIN = '# --- BEGIN BETH GUARD ---';
256
+ export const BETH_GUARD_END = '# --- END BETH GUARD ---';
257
+ //# sourceMappingURL=pre-push-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pre-push-guard.js","sourceRoot":"","sources":["../../../src/cli/commands/pre-push-guard.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Pre-Push Guard Tests
3
+ *
4
+ * Tests branch discipline enforcement:
5
+ * - Push ref parsing from Git stdin
6
+ * - Branch name extraction from refs
7
+ * - Protected branch detection
8
+ * - Epic branch convention validation
9
+ * - Release branch recognition
10
+ * - Guard logic: errors vs warnings
11
+ * - Hook script generation
12
+ * - Beads in-progress issue detection
13
+ */
14
+ export {};
15
+ //# sourceMappingURL=pre-push-guard.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pre-push-guard.test.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/pre-push-guard.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG"}