beth-copilot 1.1.0 → 2.0.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 (223) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/README.md +51 -62
  3. package/assets/beth-questioning.png +0 -0
  4. package/assets/yellowstone-beth.png +0 -0
  5. package/bin/cli.js +19 -410
  6. package/dist/__tests__/inject-skills.test.d.ts +9 -0
  7. package/dist/__tests__/inject-skills.test.d.ts.map +1 -0
  8. package/dist/__tests__/inject-skills.test.js +143 -0
  9. package/dist/__tests__/inject-skills.test.js.map +1 -0
  10. package/dist/__tests__/skills/disambiguation.test.d.ts +10 -0
  11. package/dist/__tests__/skills/disambiguation.test.d.ts.map +1 -0
  12. package/dist/__tests__/skills/disambiguation.test.js +192 -0
  13. package/dist/__tests__/skills/disambiguation.test.js.map +1 -0
  14. package/dist/__tests__/skills/hook-injection.test.d.ts +11 -0
  15. package/dist/__tests__/skills/hook-injection.test.d.ts.map +1 -0
  16. package/dist/__tests__/skills/hook-injection.test.js +173 -0
  17. package/dist/__tests__/skills/hook-injection.test.js.map +1 -0
  18. package/dist/__tests__/skills/mapping-completeness.test.d.ts +17 -0
  19. package/dist/__tests__/skills/mapping-completeness.test.d.ts.map +1 -0
  20. package/dist/__tests__/skills/mapping-completeness.test.js +281 -0
  21. package/dist/__tests__/skills/mapping-completeness.test.js.map +1 -0
  22. package/dist/__tests__/skills/pipeline-integration.test.d.ts +18 -0
  23. package/dist/__tests__/skills/pipeline-integration.test.d.ts.map +1 -0
  24. package/dist/__tests__/skills/pipeline-integration.test.js +234 -0
  25. package/dist/__tests__/skills/pipeline-integration.test.js.map +1 -0
  26. package/dist/__tests__/skills/skill-routing.test.d.ts +15 -0
  27. package/dist/__tests__/skills/skill-routing.test.d.ts.map +1 -0
  28. package/dist/__tests__/skills/skill-routing.test.js +723 -0
  29. package/dist/__tests__/skills/skill-routing.test.js.map +1 -0
  30. package/dist/__tests__/skills/trigger-coverage.test.d.ts +24 -0
  31. package/dist/__tests__/skills/trigger-coverage.test.d.ts.map +1 -0
  32. package/dist/__tests__/skills/trigger-coverage.test.js +746 -0
  33. package/dist/__tests__/skills/trigger-coverage.test.js.map +1 -0
  34. package/dist/__tests__/smoke.test.js +13 -0
  35. package/dist/__tests__/smoke.test.js.map +1 -1
  36. package/dist/__tests__/verify-skills.test.d.ts +9 -0
  37. package/dist/__tests__/verify-skills.test.d.ts.map +1 -0
  38. package/dist/__tests__/verify-skills.test.js +78 -0
  39. package/dist/__tests__/verify-skills.test.js.map +1 -0
  40. package/dist/cli/commands/beads.e2e.test.d.ts +4 -2
  41. package/dist/cli/commands/beads.e2e.test.d.ts.map +1 -1
  42. package/dist/cli/commands/beads.e2e.test.js +97 -38
  43. package/dist/cli/commands/beads.e2e.test.js.map +1 -1
  44. package/dist/cli/commands/cli-edge-cases.e2e.test.js +1 -1
  45. package/dist/cli/commands/cli-edge-cases.e2e.test.js.map +1 -1
  46. package/dist/cli/commands/close.d.ts +11 -46
  47. package/dist/cli/commands/close.d.ts.map +1 -1
  48. package/dist/cli/commands/close.e2e.test.d.ts +4 -20
  49. package/dist/cli/commands/close.e2e.test.d.ts.map +1 -1
  50. package/dist/cli/commands/close.e2e.test.js +23 -204
  51. package/dist/cli/commands/close.e2e.test.js.map +1 -1
  52. package/dist/cli/commands/close.js +26 -240
  53. package/dist/cli/commands/close.js.map +1 -1
  54. package/dist/cli/commands/close.test.d.ts +7 -9
  55. package/dist/cli/commands/close.test.d.ts.map +1 -1
  56. package/dist/cli/commands/close.test.js +44 -424
  57. package/dist/cli/commands/close.test.js.map +1 -1
  58. package/dist/cli/commands/doctor.d.ts +5 -22
  59. package/dist/cli/commands/doctor.d.ts.map +1 -1
  60. package/dist/cli/commands/doctor.e2e.test.js +3 -59
  61. package/dist/cli/commands/doctor.e2e.test.js.map +1 -1
  62. package/dist/cli/commands/doctor.js +38 -111
  63. package/dist/cli/commands/doctor.js.map +1 -1
  64. package/dist/cli/commands/doctor.test.js +32 -234
  65. package/dist/cli/commands/doctor.test.js.map +1 -1
  66. package/dist/cli/commands/framework-isolation.test.d.ts +1 -1
  67. package/dist/cli/commands/framework-isolation.test.js +2 -3
  68. package/dist/cli/commands/framework-isolation.test.js.map +1 -1
  69. package/dist/cli/commands/help.e2e.test.js +1 -5
  70. package/dist/cli/commands/help.e2e.test.js.map +1 -1
  71. package/dist/cli/commands/init-logic.e2e.test.js +12 -2
  72. package/dist/cli/commands/init-logic.e2e.test.js.map +1 -1
  73. package/dist/cli/commands/init.test.js +4 -21
  74. package/dist/cli/commands/init.test.js.map +1 -1
  75. package/dist/cli/commands/land.d.ts +3 -15
  76. package/dist/cli/commands/land.d.ts.map +1 -1
  77. package/dist/cli/commands/land.js +13 -68
  78. package/dist/cli/commands/land.js.map +1 -1
  79. package/dist/cli/commands/land.test.d.ts +0 -1
  80. package/dist/cli/commands/land.test.d.ts.map +1 -1
  81. package/dist/cli/commands/land.test.js +2 -57
  82. package/dist/cli/commands/land.test.js.map +1 -1
  83. package/dist/cli/commands/mcp.e2e.test.js +3 -3
  84. package/dist/cli/commands/mcp.e2e.test.js.map +1 -1
  85. package/dist/cli/commands/pipeline.e2e.test.js +23 -26
  86. package/dist/cli/commands/pipeline.e2e.test.js.map +1 -1
  87. package/dist/cli/commands/pre-push-guard.d.ts +2 -12
  88. package/dist/cli/commands/pre-push-guard.d.ts.map +1 -1
  89. package/dist/cli/commands/pre-push-guard.e2e.test.js +1 -1
  90. package/dist/cli/commands/pre-push-guard.e2e.test.js.map +1 -1
  91. package/dist/cli/commands/pre-push-guard.js +2 -47
  92. package/dist/cli/commands/pre-push-guard.js.map +1 -1
  93. package/dist/cli/commands/pre-push-guard.test.d.ts +0 -1
  94. package/dist/cli/commands/pre-push-guard.test.d.ts.map +1 -1
  95. package/dist/cli/commands/pre-push-guard.test.js +15 -98
  96. package/dist/cli/commands/pre-push-guard.test.js.map +1 -1
  97. package/dist/cli/commands/quickstart-expanded.e2e.test.d.ts +1 -1
  98. package/dist/cli/commands/quickstart-expanded.e2e.test.js +3 -30
  99. package/dist/cli/commands/quickstart-expanded.e2e.test.js.map +1 -1
  100. package/dist/cli/commands/quickstart.d.ts +0 -1
  101. package/dist/cli/commands/quickstart.d.ts.map +1 -1
  102. package/dist/cli/commands/quickstart.js +2 -60
  103. package/dist/cli/commands/quickstart.js.map +1 -1
  104. package/dist/cli/commands/quickstart.test.js +10 -104
  105. package/dist/cli/commands/quickstart.test.js.map +1 -1
  106. package/dist/cli/commands/update.d.ts +35 -0
  107. package/dist/cli/commands/update.d.ts.map +1 -0
  108. package/dist/cli/commands/update.e2e.test.d.ts +24 -0
  109. package/dist/cli/commands/update.e2e.test.d.ts.map +1 -0
  110. package/dist/cli/commands/update.e2e.test.js +240 -0
  111. package/dist/cli/commands/update.e2e.test.js.map +1 -0
  112. package/dist/cli/commands/update.js +255 -0
  113. package/dist/cli/commands/update.js.map +1 -0
  114. package/dist/core/agents/frontmatter.test.js +1 -1
  115. package/dist/core/agents/frontmatter.test.js.map +1 -1
  116. package/dist/core/agents/handoffs.test.js +1 -1
  117. package/dist/core/agents/handoffs.test.js.map +1 -1
  118. package/dist/core/agents/loader.d.ts +4 -2
  119. package/dist/core/agents/loader.d.ts.map +1 -1
  120. package/dist/core/agents/loader.js +5 -3
  121. package/dist/core/agents/loader.js.map +1 -1
  122. package/dist/core/agents/loader.test.js +42 -4
  123. package/dist/core/agents/loader.test.js.map +1 -1
  124. package/dist/core/agents/suite.test.js +8 -7
  125. package/dist/core/agents/suite.test.js.map +1 -1
  126. package/dist/core/agents/tools.test.js +10 -8
  127. package/dist/core/agents/tools.test.js.map +1 -1
  128. package/dist/core/agents/types.test.js +1 -1
  129. package/dist/core/agents/types.test.js.map +1 -1
  130. package/dist/core/skills/loader.test.js +1 -1
  131. package/dist/core/skills/loader.test.js.map +1 -1
  132. package/dist/index.d.ts +0 -1
  133. package/dist/index.d.ts.map +1 -1
  134. package/dist/index.js +0 -2
  135. package/dist/index.js.map +1 -1
  136. package/dist/lib/pathValidation.d.ts +0 -5
  137. package/dist/lib/pathValidation.d.ts.map +1 -1
  138. package/dist/lib/pathValidation.js +0 -11
  139. package/dist/lib/pathValidation.js.map +1 -1
  140. package/dist/lib/pathValidation.test.js +2 -14
  141. package/dist/lib/pathValidation.test.js.map +1 -1
  142. package/package.json +3 -6
  143. package/sbom.json +259 -371
  144. package/templates/.github/agents/beth.agent.md +194 -122
  145. package/templates/.github/agents/developer.agent.md +30 -22
  146. package/templates/.github/agents/product-manager.agent.md +15 -6
  147. package/templates/.github/agents/researcher.agent.md +10 -7
  148. package/templates/.github/agents/security-reviewer.agent.md +16 -7
  149. package/templates/.github/agents/tester.agent.md +16 -8
  150. package/templates/.github/agents/ux-designer.agent.md +12 -9
  151. package/templates/.github/copilot-instructions.md +33 -4
  152. package/templates/.github/copilot-mcp-config.json +12 -0
  153. package/templates/.github/dependabot.yml +68 -0
  154. package/templates/.github/hooks/scripts/inject-skills.mjs +139 -0
  155. package/templates/.github/hooks/scripts/verify-skills.mjs +47 -0
  156. package/templates/.github/hooks/skill-enforcement.json +18 -0
  157. package/templates/.github/pull_request_template.md +48 -0
  158. package/templates/.github/skills/framer-components/SKILL.md +0 -0
  159. package/templates/.github/skills/prd/SKILL.md +0 -0
  160. package/templates/.github/skills/security-analysis/SKILL.md +798 -798
  161. package/templates/.github/skills/shadcn-ui/SKILL.md +561 -561
  162. package/templates/.github/skills/vercel-react-best-practices/AGENTS.md +0 -0
  163. package/templates/.github/skills/vercel-react-best-practices/SKILL.md +0 -0
  164. package/templates/.github/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +0 -0
  165. package/templates/.github/skills/vercel-react-best-practices/rules/advanced-use-latest.md +0 -0
  166. package/templates/.github/skills/vercel-react-best-practices/rules/async-api-routes.md +0 -0
  167. package/templates/.github/skills/vercel-react-best-practices/rules/async-defer-await.md +0 -0
  168. package/templates/.github/skills/vercel-react-best-practices/rules/async-dependencies.md +0 -0
  169. package/templates/.github/skills/vercel-react-best-practices/rules/async-parallel.md +0 -0
  170. package/templates/.github/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +0 -0
  171. package/templates/.github/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +0 -0
  172. package/templates/.github/skills/vercel-react-best-practices/rules/bundle-conditional.md +0 -0
  173. package/templates/.github/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +0 -0
  174. package/templates/.github/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +0 -0
  175. package/templates/.github/skills/vercel-react-best-practices/rules/bundle-preload.md +0 -0
  176. package/templates/.github/skills/vercel-react-best-practices/rules/client-event-listeners.md +0 -0
  177. package/templates/.github/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +0 -0
  178. package/templates/.github/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +0 -0
  179. package/templates/.github/skills/vercel-react-best-practices/rules/client-swr-dedup.md +0 -0
  180. package/templates/.github/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +0 -0
  181. package/templates/.github/skills/vercel-react-best-practices/rules/js-cache-function-results.md +0 -0
  182. package/templates/.github/skills/vercel-react-best-practices/rules/js-cache-property-access.md +0 -0
  183. package/templates/.github/skills/vercel-react-best-practices/rules/js-cache-storage.md +0 -0
  184. package/templates/.github/skills/vercel-react-best-practices/rules/js-combine-iterations.md +0 -0
  185. package/templates/.github/skills/vercel-react-best-practices/rules/js-early-exit.md +0 -0
  186. package/templates/.github/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +0 -0
  187. package/templates/.github/skills/vercel-react-best-practices/rules/js-index-maps.md +0 -0
  188. package/templates/.github/skills/vercel-react-best-practices/rules/js-length-check-first.md +0 -0
  189. package/templates/.github/skills/vercel-react-best-practices/rules/js-min-max-loop.md +0 -0
  190. package/templates/.github/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +0 -0
  191. package/templates/.github/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +0 -0
  192. package/templates/.github/skills/vercel-react-best-practices/rules/rendering-activity.md +0 -0
  193. package/templates/.github/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -0
  194. package/templates/.github/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +0 -0
  195. package/templates/.github/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +0 -0
  196. package/templates/.github/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +0 -0
  197. package/templates/.github/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +0 -0
  198. package/templates/.github/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +0 -0
  199. package/templates/.github/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +0 -0
  200. package/templates/.github/skills/vercel-react-best-practices/rules/rerender-dependencies.md +0 -0
  201. package/templates/.github/skills/vercel-react-best-practices/rules/rerender-derived-state.md +0 -0
  202. package/templates/.github/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +0 -0
  203. package/templates/.github/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +0 -0
  204. package/templates/.github/skills/vercel-react-best-practices/rules/rerender-memo.md +0 -0
  205. package/templates/.github/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +0 -0
  206. package/templates/.github/skills/vercel-react-best-practices/rules/rerender-transitions.md +0 -0
  207. package/templates/.github/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +0 -0
  208. package/templates/.github/skills/vercel-react-best-practices/rules/server-auth-actions.md +0 -0
  209. package/templates/.github/skills/vercel-react-best-practices/rules/server-cache-lru.md +0 -0
  210. package/templates/.github/skills/vercel-react-best-practices/rules/server-cache-react.md +0 -0
  211. package/templates/.github/skills/vercel-react-best-practices/rules/server-dedup-props.md +0 -0
  212. package/templates/.github/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +0 -0
  213. package/templates/.github/skills/vercel-react-best-practices/rules/server-serialization.md +0 -0
  214. package/templates/.github/skills/web-design-guidelines/SKILL.md +0 -0
  215. package/templates/.vscode/settings.json +16 -16
  216. package/templates/AGENTS.md +59 -98
  217. package/templates/Backlog.md +80 -80
  218. package/assets/beth-portrait-small.txt +0 -13
  219. package/assets/beth-portrait.txt +0 -60
  220. package/bin/beth-animation.sh +0 -155
  221. package/bin/lib/animation.js +0 -189
  222. package/bin/lib/pathValidation.js +0 -233
  223. package/bin/lib/pathValidation.test.js +0 -280
package/bin/cli.js CHANGED
@@ -2,10 +2,9 @@
2
2
 
3
3
  import { fileURLToPath } from 'url';
4
4
  import { dirname, join, relative } from 'path';
5
- import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
5
+ import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync, readFileSync, writeFileSync, unlinkSync, chmodSync } from 'fs';
6
6
  import { createRequire } from 'module';
7
7
  import { execSync, spawn } from 'child_process';
8
- import { validateBeadsPath, validateBinaryPath } from './lib/pathValidation.js';
9
8
 
10
9
  const require = createRequire(import.meta.url);
11
10
  const __filename = fileURLToPath(import.meta.url);
@@ -518,59 +517,6 @@ async function checkForUpdates() {
518
517
  }
519
518
  }
520
519
 
521
- function getBeadsPath() {
522
- // Check if bd is available in PATH
523
- try {
524
- logDebug('Checking if bd is in PATH...');
525
- execSync('bd --version', { stdio: 'ignore' });
526
- logDebug('Found bd in PATH');
527
- return 'bd';
528
- } catch {
529
- logDebug('bd not in PATH, checking common locations...');
530
- // Check common installation paths based on platform
531
- const homeDir = process.env.HOME || process.env.USERPROFILE || '';
532
- const isWindows = process.platform === 'win32';
533
-
534
- const commonPaths = isWindows ? [
535
- // Windows: npm global, Go bin, local apps
536
- join(process.env.APPDATA || '', 'npm', 'bd.cmd'),
537
- join(homeDir, 'AppData', 'Roaming', 'npm', 'bd.cmd'),
538
- join(homeDir, 'AppData', 'Local', 'Microsoft', 'WindowsApps', 'bd.exe'),
539
- join(homeDir, 'go', 'bin', 'bd.exe'),
540
- join(process.env.GOPATH || join(homeDir, 'go'), 'bin', 'bd.exe'),
541
- ] : [
542
- // Unix: homebrew, npm global, go bin, local bin
543
- '/opt/homebrew/bin/bd',
544
- '/usr/local/bin/bd',
545
- join(homeDir, '.local', 'bin', 'bd'),
546
- join(homeDir, 'bin', 'bd'),
547
- join(homeDir, '.npm-global', 'bin', 'bd'),
548
- join(homeDir, 'go', 'bin', 'bd'),
549
- join(process.env.GOPATH || join(homeDir, 'go'), 'bin', 'bd'),
550
- ];
551
-
552
- for (const bdPath of commonPaths) {
553
- logDebug(`Checking: ${bdPath}`);
554
- if (existsSync(bdPath)) {
555
- logDebug(`Found at: ${bdPath}`);
556
- return bdPath;
557
- }
558
- }
559
-
560
- logDebug('bd not found in any common location');
561
- return null;
562
- }
563
- }
564
-
565
- function isBeadsInstalled() {
566
- return getBeadsPath() !== null;
567
- }
568
-
569
- function isBeadsInitialized(cwd) {
570
- // Check if .beads directory exists in the project
571
- return existsSync(join(cwd, '.beads'));
572
- }
573
-
574
520
  async function promptYesNo(question) {
575
521
  const readline = await import('readline');
576
522
  const rl = readline.createInterface({
@@ -601,180 +547,6 @@ async function promptForInput(question) {
601
547
  });
602
548
  }
603
549
 
604
- /**
605
- * Installs the beads CLI globally via npm.
606
- *
607
- * SECURITY NOTE - shell:true usage:
608
- * - Required for cross-platform npm execution (npm.cmd on Windows, npm on Unix)
609
- * - Arguments are HARDCODED - no user input is passed to the shell
610
- * - Command injection risk: NONE (no dynamic/user-supplied values)
611
- *
612
- * Alternative considered: Using platform-specific binary names (npm.cmd vs npm)
613
- * would eliminate shell:true but adds complexity and edge cases for non-standard installs.
614
- *
615
- * @returns {Promise<boolean>} True if installation succeeded and was verified
616
- */
617
- async function installBeads() {
618
- const isWindows = process.platform === 'win32';
619
- const isMac = process.platform === 'darwin';
620
-
621
- log('\nInstalling beads CLI via npm...', COLORS.cyan);
622
- logInfo('npm install -g @beads/bd');
623
-
624
- // SECURITY: shell:true is required for cross-platform npm execution.
625
- // All arguments are hardcoded constants - no user input reaches the shell.
626
- return new Promise((resolve) => {
627
- const child = spawn('npm', ['install', '-g', '@beads/bd'], {
628
- stdio: 'inherit',
629
- shell: true
630
- });
631
-
632
- child.on('close', (code) => {
633
- if (code === 0) {
634
- // CRITICAL: Verify installation actually worked before claiming success
635
- // npm can exit 0 even when the package isn't properly installed
636
- const verifiedPath = getBeadsPath();
637
- if (verifiedPath) {
638
- logSuccess('beads CLI installed and verified!');
639
- resolve(true);
640
- } else {
641
- logWarning('npm reported success but beads CLI not found in PATH.');
642
- logInfo('This can happen if npm global bin is not in your PATH.');
643
- if (globalThis.VERBOSE) {
644
- showPathDiagnostics();
645
- } else {
646
- logInfo('Run with --verbose for PATH diagnostics.');
647
- }
648
- console.log('');
649
- showBeadsAlternatives(isWindows, isMac);
650
- resolve(false);
651
- }
652
- } else {
653
- logError('npm install failed.');
654
- console.log('');
655
- showBeadsAlternatives(isWindows, isMac);
656
- resolve(false);
657
- }
658
- });
659
-
660
- child.on('error', () => {
661
- logError('Failed to run npm.');
662
- logInfo('Make sure npm is installed and in your PATH.');
663
- resolve(false);
664
- });
665
- });
666
- }
667
-
668
- function showBeadsAlternatives(isWindows, isMac) {
669
- logInfo('Alternative installation methods:');
670
- if (isWindows) {
671
- logInfo(' PowerShell: irm https://raw.githubusercontent.com/steveyegge/beads/main/install.ps1 | iex');
672
- logInfo(' Go: go install github.com/steveyegge/beads/cmd/bd@latest');
673
- } else {
674
- if (isMac) {
675
- logInfo(' Homebrew: brew install beads');
676
- }
677
- logInfo(' Script: curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash');
678
- logInfo(' Go: go install github.com/steveyegge/beads/cmd/bd@latest');
679
- }
680
- logInfo('');
681
- logInfo('Learn more: https://github.com/steveyegge/beads');
682
- }
683
-
684
- /**
685
- * Initializes beads in the current project directory.
686
- *
687
- * SECURITY NOTE - shell:true usage:
688
- * - bdPath is validated via getBeadsPath() which only returns paths that:
689
- * 1. Pass execSync('bd --version') verification, OR
690
- * 2. Exist on disk (verified via existsSync) from a HARDCODED list of paths
691
- * - Arguments are HARDCODED ('init') - no user input is passed to the shell
692
- * - Command injection risk: LOW (bdPath is validated, no user input in args)
693
- *
694
- * The shell:true is used for PATH resolution consistency, though it could be
695
- * eliminated since we have an absolute path. Kept for consistency with other
696
- * spawn calls and to handle edge cases in shell script wrappers.
697
- *
698
- * @param {string} cwd - Current working directory (validated by caller)
699
- * @returns {Promise<boolean>} True if initialization succeeded
700
- */
701
- async function initializeBeads(cwd) {
702
- log('\nInitializing beads in project...', COLORS.cyan);
703
-
704
- const bdPath = getBeadsPath();
705
- if (!bdPath) {
706
- logWarning('Failed to initialize beads. Run manually: bd init');
707
- return false;
708
- }
709
-
710
- // SECURITY: bdPath is validated by getBeadsPath() (existsSync check).
711
- // Only 'init' argument is passed - no user input reaches the shell.
712
- return new Promise((resolve) => {
713
- const child = spawn(bdPath, ['init'], {
714
- stdio: 'inherit',
715
- shell: true,
716
- cwd
717
- });
718
-
719
- child.on('close', (code) => {
720
- if (code === 0) {
721
- logSuccess('beads initialized successfully!');
722
- resolve(true);
723
- } else {
724
- logWarning('Failed to initialize beads. Run manually: bd init');
725
- resolve(false);
726
- }
727
- });
728
-
729
- child.on('error', () => {
730
- logWarning('Failed to initialize beads. Run manually: bd init');
731
- resolve(false);
732
- });
733
- });
734
- }
735
-
736
- /**
737
- * Runs `bd doctor` to verify beads configuration health.
738
- *
739
- * SECURITY NOTE - shell:true usage:
740
- * - bdPath is validated via getBeadsPath() (same as initializeBeads)
741
- * - Arguments are HARDCODED ('doctor') - no user input is passed to the shell
742
- * - Command injection risk: LOW (bdPath is validated, no user input in args)
743
- *
744
- * @returns {Promise<boolean>} True if bd doctor passed
745
- */
746
- async function runBeadsDoctor() {
747
- log('\nRunning beads doctor to verify configuration...', COLORS.cyan);
748
-
749
- const bdPath = getBeadsPath();
750
- if (!bdPath) {
751
- logWarning('Cannot run beads doctor: bd not found.');
752
- return false;
753
- }
754
-
755
- return new Promise((resolve) => {
756
- const child = spawn(bdPath, ['doctor'], {
757
- stdio: 'inherit',
758
- shell: true,
759
- });
760
-
761
- child.on('close', (code) => {
762
- if (code === 0) {
763
- logSuccess('beads doctor passed!');
764
- resolve(true);
765
- } else {
766
- logWarning('beads doctor reported issues. Run "bd doctor" manually to investigate.');
767
- resolve(false);
768
- }
769
- });
770
-
771
- child.on('error', () => {
772
- logWarning('Failed to run beads doctor. Run "bd doctor" manually.');
773
- resolve(false);
774
- });
775
- });
776
- }
777
-
778
550
  const BETH_GUARD_BEGIN = '# --- BEGIN BETH GUARD ---';
779
551
  const BETH_GUARD_END = '# --- END BETH GUARD ---';
780
552
 
@@ -817,35 +589,6 @@ ${BETH_GUARD_END}
817
589
  `;
818
590
  }
819
591
 
820
- /**
821
- * Install the pre-push guard into .beads/hooks/pre-push.
822
- * Appends the guard section after the beads integration section.
823
- * Idempotent — skips if guard is already installed.
824
- *
825
- * @param {string} cwd - Project root directory
826
- */
827
- function installPrePushGuard(cwd) {
828
- const hookPath = join(cwd, '.beads', 'hooks', 'pre-push');
829
-
830
- if (!existsSync(hookPath)) {
831
- logWarning('Pre-push hook not found (.beads/hooks/pre-push). Skipping guard installation.');
832
- return;
833
- }
834
-
835
- const content = readFileSync(hookPath, 'utf-8');
836
-
837
- // Already installed?
838
- if (content.includes(BETH_GUARD_BEGIN)) {
839
- logSuccess('Pre-push branch guard already installed');
840
- return;
841
- }
842
-
843
- // Append guard after existing content
844
- const guardScript = generateGuardScript();
845
- writeFileSync(hookPath, content.trimEnd() + '\n' + guardScript, 'utf-8');
846
- logSuccess('Installed pre-push branch guard (blocks direct pushes to main)');
847
- }
848
-
849
592
  function showHelp() {
850
593
  showBethBannerStatic({ showQuickHelp: false });
851
594
  console.log(`${COLORS.bright}Beth${COLORS.reset} - AI Orchestrator for GitHub Copilot
@@ -853,18 +596,18 @@ function showHelp() {
853
596
  ${COLORS.bright}Usage:${COLORS.reset}
854
597
  npx beth-copilot init [options] Initialize Beth in current directory
855
598
  npx beth-copilot doctor Check system health and dependencies
856
- npx beth-copilot close <id> [opts] Close issue with dependency enforcement
857
599
  npx beth-copilot land [opts] Automated session completion (test, commit, push)
858
600
  npx beth-copilot pre-push-guard Run branch discipline checks (used by git hook)
859
- npx beth-copilot quickstart Run init + doctor + beads setup
601
+ npx beth-copilot update [options] Update project files to latest templates
602
+ npx beth-copilot quickstart Run init + doctor
860
603
  npx beth-copilot help Show this help message
861
604
 
862
605
  ${COLORS.bright}Options:${COLORS.reset}
863
606
  --force Overwrite existing files
864
607
  --skip-backlog Don't create Backlog.md
865
608
  --skip-mcp Don't create mcp.json.example
866
- --skip-beads Skip beads check (not recommended)
867
609
  --verbose Show detailed diagnostics on errors
610
+ --check-only Check for updates without modifying files
868
611
 
869
612
  ${COLORS.bright}Examples:${COLORS.reset}
870
613
  npx beth-copilot init Set up Beth in current project
@@ -939,7 +682,7 @@ function copyDirRecursive(src, dest, options = {}) {
939
682
  }
940
683
 
941
684
  async function init(options = {}) {
942
- const { force = false, skipBacklog = false, skipMcp = false, skipBeads = false } = options;
685
+ const { force = false, skipBacklog = false, skipMcp = false } = options;
943
686
  const cwd = process.cwd();
944
687
 
945
688
  // Check for updates
@@ -1053,142 +796,10 @@ ${COLORS.yellow}╔════════════════════
1053
796
  logWarning('No files were copied. Use --force to overwrite existing files.');
1054
797
  }
1055
798
 
1056
- // Check for beads CLI (REQUIRED for Beth)
1057
- if (!skipBeads) {
1058
- console.log('');
1059
- log('Checking beads (required for task tracking)...', COLORS.cyan);
1060
-
1061
- let bdPath = getBeadsPath();
1062
-
1063
- // Loop until beads is installed
1064
- while (!bdPath) {
1065
- logWarning('beads CLI is not installed.');
1066
- logInfo('Beth requires beads for task tracking. Agents use it to coordinate work.');
1067
- logInfo('Learn more: https://github.com/steveyegge/beads');
1068
- console.log('');
1069
-
1070
- const shouldInstallBeads = await promptYesNo('Install beads CLI now? (required)');
1071
- if (shouldInstallBeads) {
1072
- const installed = await installBeads();
1073
- if (installed) {
1074
- // Re-check for beads after installation
1075
- bdPath = getBeadsPath();
1076
- if (!bdPath) {
1077
- console.log('');
1078
- logWarning('beads installed but not found in common paths.');
1079
- logInfo('The installer may have placed it in a custom location.');
1080
- console.log('');
1081
- logInfo('Please try one of these options:');
1082
- logInfo(' 1. Open a NEW terminal and run: npx beth-copilot init');
1083
- logInfo(' 2. Add ~/.local/bin to your PATH and retry');
1084
- logInfo(' 3. Run: source ~/.bashrc (or ~/.zshrc) then retry');
1085
- console.log('');
1086
-
1087
- const retryCheck = await promptYesNo('Retry detection? (select No to enter path manually)');
1088
- if (retryCheck) {
1089
- bdPath = getBeadsPath();
1090
- continue;
1091
- }
1092
-
1093
- // Allow manual path entry
1094
- const customPath = await promptForInput('Enter full path to bd binary (or press Enter to retry installation):');
1095
- if (customPath) {
1096
- const validation = validateBeadsPath(customPath);
1097
- if (validation.valid) {
1098
- bdPath = validation.normalizedPath;
1099
- logSuccess(`Found beads at: ${bdPath}`);
1100
- } else {
1101
- logError(`Invalid path: ${validation.error}`);
1102
- }
1103
- }
1104
- }
1105
- } else {
1106
- console.log('');
1107
- logError('Installation script failed.');
1108
- logInfo('You can try installing manually:');
1109
- logInfo(' curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash');
1110
- console.log('');
1111
- }
1112
- } else {
1113
- console.log('');
1114
- logError('beads is REQUIRED for Beth to function.');
1115
- logInfo('Beth agents use beads to track tasks, dependencies, and coordinate work.');
1116
- logInfo('Without beads, the multi-agent workflow will not work correctly.');
1117
- console.log('');
1118
-
1119
- const tryAgain = await promptYesNo('Would you like to try installing beads?');
1120
- if (!tryAgain) {
1121
- logError('Cannot continue without beads. Exiting.');
1122
- logInfo('Install beads manually and run "npx beth-copilot init" again:');
1123
- logInfo(' npm install -g @beads/bd');
1124
- process.exit(1);
1125
- }
1126
- }
1127
- }
1128
-
1129
- // Show path info if not in standard PATH
1130
- if (bdPath && bdPath !== 'bd') {
1131
- logSuccess(`beads CLI found at: ${bdPath}`);
1132
- const isWindows = process.platform === 'win32';
1133
- if (isWindows) {
1134
- logInfo('Tip: Ensure npm global bin is in your PATH to use "bd" directly.');
1135
- } else {
1136
- logInfo('Tip: Add ~/.local/bin or npm global bin to your PATH to use "bd" directly.');
1137
- }
1138
- } else {
1139
- logSuccess('beads CLI is installed');
1140
- }
1141
-
1142
- // Initialize beads in the project if not already done
1143
- if (!isBeadsInitialized(cwd)) {
1144
- logInfo('beads not initialized in this project.');
1145
- let initialized = false;
1146
-
1147
- while (!initialized) {
1148
- const shouldInitBeads = await promptYesNo('Initialize beads now? (required)');
1149
- if (shouldInitBeads) {
1150
- initialized = await initializeBeads(cwd);
1151
- if (!initialized) {
1152
- logWarning('Initialization failed. Let\'s try again.');
1153
- }
1154
- } else {
1155
- logError('beads must be initialized for Beth to work correctly.');
1156
- logInfo('The .beads directory stores task tracking data used by all agents.');
1157
- console.log('');
1158
- }
1159
- }
1160
- } else {
1161
- logSuccess('beads is initialized in this project');
1162
- }
1163
- } else {
1164
- logWarning('Skipped beads check (--skip-beads). Beth may not function correctly.');
1165
- }
1166
-
1167
- // Run bd doctor to verify beads configuration
1168
- if (!skipBeads && getBeadsPath() && isBeadsInitialized(cwd)) {
1169
- await runBeadsDoctor();
1170
- }
1171
-
1172
- // Install pre-push guard hook
1173
- if (!skipBeads && isBeadsInitialized(cwd)) {
1174
- installPrePushGuard(cwd);
1175
- }
1176
-
1177
799
  // Final verification
1178
800
  console.log('');
1179
801
  log('Verifying installation...', COLORS.cyan);
1180
-
1181
- const finalBeadsOk = skipBeads || getBeadsPath();
1182
- const finalBeadsInit = skipBeads || isBeadsInitialized(cwd);
1183
-
1184
- if (finalBeadsOk && finalBeadsInit) {
1185
- logSuccess('All dependencies installed and configured!');
1186
- } else {
1187
- if (!finalBeadsOk) logError('beads CLI not found');
1188
- if (!finalBeadsInit) logError('beads not initialized in project');
1189
- logError('Setup incomplete. Please resolve issues above and run init again.');
1190
- process.exit(1);
1191
- }
802
+ logSuccess('All files installed and configured!');
1192
803
 
1193
804
  // Next steps
1194
805
  console.log(`
@@ -1207,15 +818,15 @@ ${COLORS.cyan}"They broke my wings and forgot I had claws."${COLORS.reset}
1207
818
  }
1208
819
 
1209
820
  // Input validation constants
1210
- const ALLOWED_COMMANDS = ['init', 'help', '--help', '-h', 'doctor', 'quickstart', 'close', 'pre-push-guard'];
1211
- const ALLOWED_FLAGS = ['--force', '--skip-backlog', '--skip-mcp', '--skip-beads', '--verbose', '--reason', '-r', '-f', '--skip-tests', '--skip-backup', '--message', '-m', '--dry-run'];
821
+ const ALLOWED_COMMANDS = ['init', 'help', '--help', '-h', 'doctor', 'quickstart', 'pre-push-guard', 'update', 'land'];
822
+ const ALLOWED_FLAGS = ['--force', '--skip-backlog', '--skip-mcp', '--verbose', '--reason', '-r', '-f', '--skip-tests', '--message', '-m', '--dry-run', '--check-only'];
1212
823
  const MAX_ARG_LENGTH = 50;
1213
824
 
1214
825
  // Validate and sanitize input
1215
826
  function validateArgs(args) {
1216
- // The 'close' and 'land' commands handle their own arg validation
827
+ // The 'land' and 'update' commands handle their own arg validation
1217
828
  const command = args[0]?.toLowerCase();
1218
- if (command === 'close' || command === 'land') return;
829
+ if (command === 'land' || command === 'update') return;
1219
830
 
1220
831
  for (const arg of args) {
1221
832
  // Prevent excessively long arguments (log injection, DoS)
@@ -1241,7 +852,6 @@ const options = {
1241
852
  force: args.includes('--force'),
1242
853
  skipBacklog: args.includes('--skip-backlog'),
1243
854
  skipMcp: args.includes('--skip-mcp'),
1244
- skipBeads: args.includes('--skip-beads'),
1245
855
  verbose: args.includes('--verbose'),
1246
856
  };
1247
857
 
@@ -1249,8 +859,8 @@ const options = {
1249
859
  globalThis.VERBOSE = options.verbose;
1250
860
 
1251
861
  // Validate unknown flags (exclude --help which is handled as a command)
1252
- // Skip for 'close' and 'land' commands which handle their own arg parsing
1253
- if (command !== 'close' && command !== 'land') {
862
+ // Skip for 'land' and 'update' commands which handle their own arg parsing
863
+ if (command !== 'land' && command !== 'update') {
1254
864
  const unknownFlags = args.filter(arg => arg.startsWith('--') && !ALLOWED_FLAGS.includes(arg) && arg !== '--help');
1255
865
  if (unknownFlags.length > 0) {
1256
866
  logError(`Unknown flag: ${unknownFlags[0].slice(0, MAX_ARG_LENGTH)}`);
@@ -1283,14 +893,6 @@ switch (command) {
1283
893
  await quickstart(options);
1284
894
  }
1285
895
  break;
1286
- case 'close':
1287
- {
1288
- const { close } = await loadTsCommand('close');
1289
- // Pass raw args after 'close' — the command handles its own parsing
1290
- const closeArgs = process.argv.slice(3);
1291
- await close(closeArgs);
1292
- }
1293
- break;
1294
896
  case 'land':
1295
897
  {
1296
898
  const { land } = await loadTsCommand('land');
@@ -1299,6 +901,13 @@ switch (command) {
1299
901
  await land(landArgs);
1300
902
  }
1301
903
  break;
904
+ case 'update':
905
+ {
906
+ const { update } = await loadTsCommand('update');
907
+ const updateArgs = process.argv.slice(3);
908
+ await update(updateArgs);
909
+ }
910
+ break;
1302
911
  case 'pre-push-guard':
1303
912
  {
1304
913
  const { prePushGuard } = await loadTsCommand('pre-push-guard');
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Unit tests for .github/hooks/scripts/inject-skills.mjs
3
+ *
4
+ * Tests the SubagentStart hook that deterministically injects skill context
5
+ * for each agent type. Verifies correct skill mapping, output format,
6
+ * and graceful handling of edge cases.
7
+ */
8
+ export {};
9
+ //# sourceMappingURL=inject-skills.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inject-skills.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/inject-skills.test.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Unit tests for .github/hooks/scripts/inject-skills.mjs
3
+ *
4
+ * Tests the SubagentStart hook that deterministically injects skill context
5
+ * for each agent type. Verifies correct skill mapping, output format,
6
+ * and graceful handling of edge cases.
7
+ */
8
+ import { describe, it, expect } from 'vitest';
9
+ import { execFileSync } from 'node:child_process';
10
+ import { join } from 'node:path';
11
+ const SCRIPT_PATH = join(process.cwd(), '.github/hooks/scripts/inject-skills.mjs');
12
+ const PROJECT_ROOT = process.cwd();
13
+ /** Helper: pipe JSON input to inject-skills.mjs and parse the JSON output */
14
+ function runHook(input) {
15
+ const result = execFileSync('node', [SCRIPT_PATH], {
16
+ input: JSON.stringify(input),
17
+ encoding: 'utf8',
18
+ cwd: PROJECT_ROOT,
19
+ timeout: 10000,
20
+ });
21
+ return JSON.parse(result);
22
+ }
23
+ /** Helper: extract additionalContext string from hook output */
24
+ function getContext(input) {
25
+ const output = runHook(input);
26
+ return output.hookSpecificOutput?.additionalContext ?? '';
27
+ }
28
+ // ─── Core behavior ─────────────────────────────────────────────────────────
29
+ describe('inject-skills.mjs: output structure', () => {
30
+ it('should always set continue: true', () => {
31
+ const output = runHook({ agent_type: 'developer', cwd: PROJECT_ROOT });
32
+ expect(output.continue).toBe(true);
33
+ });
34
+ it('should include hookSpecificOutput with SubagentStart event name', () => {
35
+ const output = runHook({ agent_type: 'developer', cwd: PROJECT_ROOT });
36
+ expect(output.hookSpecificOutput).toBeDefined();
37
+ expect(output.hookSpecificOutput.hookEventName).toBe('SubagentStart');
38
+ });
39
+ });
40
+ // ─── UX Designer (includes ui-ux-pro-max) ──────────────────────────────────
41
+ describe('inject-skills.mjs: ux-designer', () => {
42
+ const ctx = () => getContext({ agent_type: 'ux-designer', cwd: PROJECT_ROOT });
43
+ it('should inject web-design-guidelines into context', () => {
44
+ expect(ctx()).toContain('.github/skills/web-design-guidelines/SKILL.md');
45
+ expect(ctx()).toContain('Skills loaded into context');
46
+ });
47
+ it('should mandate readFile for framer-components', () => {
48
+ expect(ctx()).toContain('.github/skills/framer-components/SKILL.md');
49
+ expect(ctx()).toContain('Skills to load via readFile');
50
+ });
51
+ it('should mandate readFile for ui-ux-pro-max', () => {
52
+ expect(ctx()).toContain('.github/prompts/ui-ux-pro-max/PROMPT.md');
53
+ });
54
+ it('should include the NON-NEGOTIABLE header', () => {
55
+ expect(ctx()).toContain('SKILL ENFORCEMENT');
56
+ expect(ctx()).toContain('NON-NEGOTIABLE');
57
+ });
58
+ it('should identify the agent type in the context', () => {
59
+ expect(ctx()).toContain('You are `ux-designer`');
60
+ });
61
+ });
62
+ // ─── Developer ─────────────────────────────────────────────────────────────
63
+ describe('inject-skills.mjs: developer', () => {
64
+ const ctx = () => getContext({ agent_type: 'developer', cwd: PROJECT_ROOT });
65
+ it('should inject vercel-react-best-practices SKILL.md into context', () => {
66
+ expect(ctx()).toContain('.github/skills/vercel-react-best-practices/SKILL.md');
67
+ expect(ctx()).toContain('Skills loaded into context');
68
+ });
69
+ it('should mandate readFile for shadcn-ui', () => {
70
+ expect(ctx()).toContain('.github/skills/shadcn-ui/SKILL.md');
71
+ });
72
+ it('should mandate readFile for vercel-react-best-practices AGENTS.md', () => {
73
+ expect(ctx()).toContain('.github/skills/vercel-react-best-practices/AGENTS.md');
74
+ });
75
+ });
76
+ // ─── Product Manager ───────────────────────────────────────────────────────
77
+ describe('inject-skills.mjs: product-manager', () => {
78
+ const ctx = () => getContext({ agent_type: 'product-manager', cwd: PROJECT_ROOT });
79
+ it('should mandate readFile for prd skill', () => {
80
+ expect(ctx()).toContain('.github/skills/prd/SKILL.md');
81
+ });
82
+ it('should NOT have "Skills loaded into context" (no inject files)', () => {
83
+ expect(ctx()).not.toContain('Skills loaded into context');
84
+ });
85
+ });
86
+ // ─── Security Reviewer ─────────────────────────────────────────────────────
87
+ describe('inject-skills.mjs: security-reviewer', () => {
88
+ const ctx = () => getContext({ agent_type: 'security-reviewer', cwd: PROJECT_ROOT });
89
+ it('should mandate readFile for security-analysis', () => {
90
+ expect(ctx()).toContain('.github/skills/security-analysis/SKILL.md');
91
+ });
92
+ });
93
+ // ─── Tester ────────────────────────────────────────────────────────────────
94
+ describe('inject-skills.mjs: tester', () => {
95
+ const ctx = () => getContext({ agent_type: 'tester', cwd: PROJECT_ROOT });
96
+ it('should inject web-design-guidelines into context', () => {
97
+ expect(ctx()).toContain('.github/skills/web-design-guidelines/SKILL.md');
98
+ expect(ctx()).toContain('Skills loaded into context');
99
+ });
100
+ it('should NOT have readFile mandate (no readFile files)', () => {
101
+ expect(ctx()).not.toContain('Skills to load via readFile');
102
+ });
103
+ });
104
+ // ─── Researcher ────────────────────────────────────────────────────────────
105
+ describe('inject-skills.mjs: researcher', () => {
106
+ const ctx = () => getContext({ agent_type: 'researcher', cwd: PROJECT_ROOT });
107
+ it('should inject web-search skill into context', () => {
108
+ expect(ctx()).toContain('.github/skills/web-search/SKILL.md');
109
+ });
110
+ });
111
+ // ─── Edge cases ────────────────────────────────────────────────────────────
112
+ describe('inject-skills.mjs: edge cases', () => {
113
+ it('should pass through unknown agent types with continue: true', () => {
114
+ const output = runHook({ agent_type: 'unknown-agent', cwd: PROJECT_ROOT });
115
+ expect(output.continue).toBe(true);
116
+ expect(output).not.toHaveProperty('hookSpecificOutput');
117
+ });
118
+ it('should handle missing agent_type gracefully', () => {
119
+ const output = runHook({ cwd: PROJECT_ROOT });
120
+ expect(output.continue).toBe(true);
121
+ });
122
+ it('should handle malformed JSON input gracefully', () => {
123
+ const result = execFileSync('node', [SCRIPT_PATH], {
124
+ input: 'not json at all',
125
+ encoding: 'utf8',
126
+ cwd: PROJECT_ROOT,
127
+ timeout: 10000,
128
+ });
129
+ const output = JSON.parse(result);
130
+ expect(output.continue).toBe(true);
131
+ });
132
+ it('should handle empty input gracefully', () => {
133
+ const result = execFileSync('node', [SCRIPT_PATH], {
134
+ input: '',
135
+ encoding: 'utf8',
136
+ cwd: PROJECT_ROOT,
137
+ timeout: 10000,
138
+ });
139
+ const output = JSON.parse(result);
140
+ expect(output.continue).toBe(true);
141
+ });
142
+ });
143
+ //# sourceMappingURL=inject-skills.test.js.map