@bitseek/hermes-webui 0.1.0-beta.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 (233) hide show
  1. package/README.md +213 -0
  2. package/bin/hermes-webui.mjs +588 -0
  3. package/package.json +25 -0
  4. package/scripts/sync-vendor.mjs +74 -0
  5. package/templates/launchd/com.bitseek.hermes-webui.plist +21 -0
  6. package/templates/systemd/hermes-webui.service +13 -0
  7. package/templates/windows/hermes-webui-task.ps1 +3 -0
  8. package/vendor/agent-frontend-shell/.bitseek-source.json +6 -0
  9. package/vendor/agent-frontend-shell/.dockerignore +7 -0
  10. package/vendor/agent-frontend-shell/.env.docker.example +89 -0
  11. package/vendor/agent-frontend-shell/.env.example +34 -0
  12. package/vendor/agent-frontend-shell/.github/FUNDING.yml +3 -0
  13. package/vendor/agent-frontend-shell/.github/workflows/browser-smoke.yml +42 -0
  14. package/vendor/agent-frontend-shell/.github/workflows/docker-smoke.yml +233 -0
  15. package/vendor/agent-frontend-shell/.github/workflows/native-windows-startup.yml +132 -0
  16. package/vendor/agent-frontend-shell/.github/workflows/release.yml +57 -0
  17. package/vendor/agent-frontend-shell/.github/workflows/tests.yml +88 -0
  18. package/vendor/agent-frontend-shell/.vscode/launch.json +59 -0
  19. package/vendor/agent-frontend-shell/.vscode/settings.json +13 -0
  20. package/vendor/agent-frontend-shell/AGENTS.md +80 -0
  21. package/vendor/agent-frontend-shell/ARCHITECTURE.md +1658 -0
  22. package/vendor/agent-frontend-shell/BUGS.md +52 -0
  23. package/vendor/agent-frontend-shell/CHANGELOG.md +7295 -0
  24. package/vendor/agent-frontend-shell/CONTRIBUTING.md +205 -0
  25. package/vendor/agent-frontend-shell/CONTRIBUTORS.md +107 -0
  26. package/vendor/agent-frontend-shell/DESIGN.md +173 -0
  27. package/vendor/agent-frontend-shell/Dockerfile +91 -0
  28. package/vendor/agent-frontend-shell/LICENSE +21 -0
  29. package/vendor/agent-frontend-shell/README-CUSTOM.md +76 -0
  30. package/vendor/agent-frontend-shell/README.md +705 -0
  31. package/vendor/agent-frontend-shell/ROADMAP.md +351 -0
  32. package/vendor/agent-frontend-shell/SPRINTS.md +147 -0
  33. package/vendor/agent-frontend-shell/TESTING.md +1932 -0
  34. package/vendor/agent-frontend-shell/THEMES.md +170 -0
  35. package/vendor/agent-frontend-shell/api/__init__.py +1 -0
  36. package/vendor/agent-frontend-shell/api/agent_health.py +392 -0
  37. package/vendor/agent-frontend-shell/api/agent_sessions.py +782 -0
  38. package/vendor/agent-frontend-shell/api/auth.py +592 -0
  39. package/vendor/agent-frontend-shell/api/background.py +87 -0
  40. package/vendor/agent-frontend-shell/api/clarify.py +238 -0
  41. package/vendor/agent-frontend-shell/api/commands.py +124 -0
  42. package/vendor/agent-frontend-shell/api/compression_anchor.py +134 -0
  43. package/vendor/agent-frontend-shell/api/config.py +5178 -0
  44. package/vendor/agent-frontend-shell/api/dashboard_probe.py +255 -0
  45. package/vendor/agent-frontend-shell/api/extensions.py +253 -0
  46. package/vendor/agent-frontend-shell/api/gateway_chat.py +435 -0
  47. package/vendor/agent-frontend-shell/api/gateway_watcher.py +230 -0
  48. package/vendor/agent-frontend-shell/api/goals.py +608 -0
  49. package/vendor/agent-frontend-shell/api/helpers.py +474 -0
  50. package/vendor/agent-frontend-shell/api/kanban_bridge.py +1255 -0
  51. package/vendor/agent-frontend-shell/api/metering.py +194 -0
  52. package/vendor/agent-frontend-shell/api/models.py +4210 -0
  53. package/vendor/agent-frontend-shell/api/oauth.py +770 -0
  54. package/vendor/agent-frontend-shell/api/onboarding.py +1046 -0
  55. package/vendor/agent-frontend-shell/api/passkeys.py +365 -0
  56. package/vendor/agent-frontend-shell/api/profiles.py +1499 -0
  57. package/vendor/agent-frontend-shell/api/providers.py +2175 -0
  58. package/vendor/agent-frontend-shell/api/request_diagnostics.py +160 -0
  59. package/vendor/agent-frontend-shell/api/rollback.py +320 -0
  60. package/vendor/agent-frontend-shell/api/routes.py +13990 -0
  61. package/vendor/agent-frontend-shell/api/run_journal.py +284 -0
  62. package/vendor/agent-frontend-shell/api/runner_client.py +156 -0
  63. package/vendor/agent-frontend-shell/api/runtime_adapter.py +431 -0
  64. package/vendor/agent-frontend-shell/api/session_discoverability.py +640 -0
  65. package/vendor/agent-frontend-shell/api/session_events.py +45 -0
  66. package/vendor/agent-frontend-shell/api/session_lifecycle.py +208 -0
  67. package/vendor/agent-frontend-shell/api/session_ops.py +207 -0
  68. package/vendor/agent-frontend-shell/api/session_recovery.py +655 -0
  69. package/vendor/agent-frontend-shell/api/skill_usage.py +32 -0
  70. package/vendor/agent-frontend-shell/api/startup.py +128 -0
  71. package/vendor/agent-frontend-shell/api/state_sync.py +187 -0
  72. package/vendor/agent-frontend-shell/api/streaming.py +7048 -0
  73. package/vendor/agent-frontend-shell/api/system_health.py +167 -0
  74. package/vendor/agent-frontend-shell/api/terminal.py +410 -0
  75. package/vendor/agent-frontend-shell/api/turn_journal.py +214 -0
  76. package/vendor/agent-frontend-shell/api/updates.py +1261 -0
  77. package/vendor/agent-frontend-shell/api/upload.py +322 -0
  78. package/vendor/agent-frontend-shell/api/usage.py +26 -0
  79. package/vendor/agent-frontend-shell/api/workspace.py +867 -0
  80. package/vendor/agent-frontend-shell/api/workspace_git.py +1261 -0
  81. package/vendor/agent-frontend-shell/api/worktrees.py +357 -0
  82. package/vendor/agent-frontend-shell/bootstrap.py +492 -0
  83. package/vendor/agent-frontend-shell/ctl.sh +427 -0
  84. package/vendor/agent-frontend-shell/docker-compose.custom.yml +26 -0
  85. package/vendor/agent-frontend-shell/docker-compose.three-container.yml +168 -0
  86. package/vendor/agent-frontend-shell/docker-compose.two-container.yml +147 -0
  87. package/vendor/agent-frontend-shell/docker-compose.yml +57 -0
  88. package/vendor/agent-frontend-shell/docker_init.bash +459 -0
  89. package/vendor/agent-frontend-shell/docs/CONTRACTS.md +207 -0
  90. package/vendor/agent-frontend-shell/docs/EXTENSIONS.md +212 -0
  91. package/vendor/agent-frontend-shell/docs/ISSUES.md +23 -0
  92. package/vendor/agent-frontend-shell/docs/UIUX-GUIDE.md +196 -0
  93. package/vendor/agent-frontend-shell/docs/advanced-chat-setup.md +83 -0
  94. package/vendor/agent-frontend-shell/docs/docker.md +337 -0
  95. package/vendor/agent-frontend-shell/docs/onboarding-agent-checklist.md +207 -0
  96. package/vendor/agent-frontend-shell/docs/onboarding.md +202 -0
  97. package/vendor/agent-frontend-shell/docs/remote-access.md +75 -0
  98. package/vendor/agent-frontend-shell/docs/rfcs/README.md +53 -0
  99. package/vendor/agent-frontend-shell/docs/rfcs/agent-source-boundary.md +70 -0
  100. package/vendor/agent-frontend-shell/docs/rfcs/canonical-session-resolution.md +124 -0
  101. package/vendor/agent-frontend-shell/docs/rfcs/hermes-run-adapter-contract.md +1079 -0
  102. package/vendor/agent-frontend-shell/docs/rfcs/turn-journal.md +195 -0
  103. package/vendor/agent-frontend-shell/docs/rfcs/webui-run-state-consistency-contract.md +157 -0
  104. package/vendor/agent-frontend-shell/docs/supervisor.md +280 -0
  105. package/vendor/agent-frontend-shell/docs/troubleshooting.md +132 -0
  106. package/vendor/agent-frontend-shell/docs/ui-ux/index.html +863 -0
  107. package/vendor/agent-frontend-shell/docs/ui-ux/two-stage-proposal.html +768 -0
  108. package/vendor/agent-frontend-shell/docs/why-hermes.md +489 -0
  109. package/vendor/agent-frontend-shell/docs/workspace-git.md +92 -0
  110. package/vendor/agent-frontend-shell/docs/wsl-autostart.md +126 -0
  111. package/vendor/agent-frontend-shell/eslint.runtime-guard.config.mjs +35 -0
  112. package/vendor/agent-frontend-shell/extensions/bitseek-design-system.md +330 -0
  113. package/vendor/agent-frontend-shell/extensions/branding/assets/apple-touch-icon.png +0 -0
  114. package/vendor/agent-frontend-shell/extensions/branding/assets/empty-logo.svg +739 -0
  115. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-192.png +0 -0
  116. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-32.png +0 -0
  117. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.png +0 -0
  118. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.svg +745 -0
  119. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.ico +0 -0
  120. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.svg +745 -0
  121. package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v2.svg +751 -0
  122. package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v3.svg +739 -0
  123. package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon.svg +745 -0
  124. package/vendor/agent-frontend-shell/extensions/branding/branding.js +112 -0
  125. package/vendor/agent-frontend-shell/extensions/branding/config.json +14 -0
  126. package/vendor/agent-frontend-shell/extensions/branding/manifest.json +53 -0
  127. package/vendor/agent-frontend-shell/extensions/index.js +67 -0
  128. package/vendor/agent-frontend-shell/extensions/loader/hermes-loader.js +77 -0
  129. package/vendor/agent-frontend-shell/extensions/manifest.json +16 -0
  130. package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.css +333 -0
  131. package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.js +487 -0
  132. package/vendor/agent-frontend-shell/extensions/pages/manifest.json +6 -0
  133. package/vendor/agent-frontend-shell/extensions/pages/registry.css +56 -0
  134. package/vendor/agent-frontend-shell/extensions/pages/registry.js +302 -0
  135. package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.css +93 -0
  136. package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.js +98 -0
  137. package/vendor/agent-frontend-shell/install.sh +63 -0
  138. package/vendor/agent-frontend-shell/mcp_server.py +567 -0
  139. package/vendor/agent-frontend-shell/package.json +12 -0
  140. package/vendor/agent-frontend-shell/pyproject.toml +56 -0
  141. package/vendor/agent-frontend-shell/pytest.ini +3 -0
  142. package/vendor/agent-frontend-shell/requirements.txt +5 -0
  143. package/vendor/agent-frontend-shell/server.py +624 -0
  144. package/vendor/agent-frontend-shell/start.ps1 +210 -0
  145. package/vendor/agent-frontend-shell/start.sh +65 -0
  146. package/vendor/agent-frontend-shell/static/apple-touch-icon.png +0 -0
  147. package/vendor/agent-frontend-shell/static/boot.js +1990 -0
  148. package/vendor/agent-frontend-shell/static/commands.js +1402 -0
  149. package/vendor/agent-frontend-shell/static/favicon-192.png +0 -0
  150. package/vendor/agent-frontend-shell/static/favicon-32.png +0 -0
  151. package/vendor/agent-frontend-shell/static/favicon-512.png +0 -0
  152. package/vendor/agent-frontend-shell/static/favicon-512.svg +18 -0
  153. package/vendor/agent-frontend-shell/static/favicon.ico +0 -0
  154. package/vendor/agent-frontend-shell/static/favicon.svg +20 -0
  155. package/vendor/agent-frontend-shell/static/i18n.js +15389 -0
  156. package/vendor/agent-frontend-shell/static/icons.js +92 -0
  157. package/vendor/agent-frontend-shell/static/index.html +1506 -0
  158. package/vendor/agent-frontend-shell/static/login.js +177 -0
  159. package/vendor/agent-frontend-shell/static/manifest.json +53 -0
  160. package/vendor/agent-frontend-shell/static/messages.js +3521 -0
  161. package/vendor/agent-frontend-shell/static/onboarding.js +800 -0
  162. package/vendor/agent-frontend-shell/static/panels.js +7995 -0
  163. package/vendor/agent-frontend-shell/static/pwa-startup.js +83 -0
  164. package/vendor/agent-frontend-shell/static/sessions.js +5165 -0
  165. package/vendor/agent-frontend-shell/static/style.css +4774 -0
  166. package/vendor/agent-frontend-shell/static/sw.js +173 -0
  167. package/vendor/agent-frontend-shell/static/terminal.js +632 -0
  168. package/vendor/agent-frontend-shell/static/ui.js +8997 -0
  169. package/vendor/agent-frontend-shell/static/vendor/js-yaml/4.1.0/js-yaml.min.js +2 -0
  170. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.ttf +0 -0
  171. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff +0 -0
  172. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  173. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  174. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  175. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  176. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  177. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  178. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  179. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  180. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  181. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  182. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  183. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  184. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  185. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.ttf +0 -0
  186. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff +0 -0
  187. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff2 +0 -0
  188. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  189. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  190. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  191. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.ttf +0 -0
  192. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff +0 -0
  193. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff2 +0 -0
  194. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.ttf +0 -0
  195. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff +0 -0
  196. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff2 +0 -0
  197. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  198. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  199. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  200. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.ttf +0 -0
  201. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff +0 -0
  202. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff2 +0 -0
  203. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  204. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  205. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  206. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  207. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  208. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  209. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  210. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  211. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  212. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.ttf +0 -0
  213. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff +0 -0
  214. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff2 +0 -0
  215. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.ttf +0 -0
  216. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff +0 -0
  217. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  218. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.ttf +0 -0
  219. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff +0 -0
  220. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  221. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.ttf +0 -0
  222. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff +0 -0
  223. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  224. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.ttf +0 -0
  225. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff +0 -0
  226. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  227. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  228. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  229. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  230. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.css +1 -0
  231. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.js +1 -0
  232. package/vendor/agent-frontend-shell/static/vendor/smd.min.js +29 -0
  233. package/vendor/agent-frontend-shell/static/workspace.js +680 -0
@@ -0,0 +1,357 @@
1
+ """Helpers for WebUI-managed Hermes Agent git worktrees."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ import time
7
+ from contextlib import redirect_stderr, redirect_stdout
8
+ from io import StringIO
9
+ from pathlib import Path
10
+
11
+ import logging
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def _run_git(args: list[str], cwd: str | Path, timeout: float = 2) -> subprocess.CompletedProcess:
17
+ return subprocess.run(
18
+ ["git", *args],
19
+ cwd=str(cwd),
20
+ text=True,
21
+ capture_output=True,
22
+ timeout=timeout,
23
+ check=False,
24
+ )
25
+
26
+
27
+ def _resolve_path(path: str | Path | None) -> Path | None:
28
+ if not path:
29
+ return None
30
+ try:
31
+ return Path(path).expanduser().resolve(strict=False)
32
+ except (OSError, RuntimeError):
33
+ return Path(path).expanduser()
34
+
35
+
36
+ def _worktree_list_cwd(worktree_path: Path, repo_root: str | Path | None) -> Path | None:
37
+ repo = _resolve_path(repo_root)
38
+ if repo and repo.is_dir():
39
+ return repo
40
+ if worktree_path.is_dir():
41
+ return worktree_path
42
+ return None
43
+
44
+
45
+ def _parse_worktree_list_porcelain(output: str) -> set[str]:
46
+ paths: set[str] = set()
47
+ for line in str(output or "").splitlines():
48
+ if not line.startswith("worktree "):
49
+ continue
50
+ path = line[len("worktree "):].strip()
51
+ if not path:
52
+ continue
53
+ resolved = _resolve_path(path)
54
+ paths.add(str(resolved or Path(path).expanduser()))
55
+ return paths
56
+
57
+
58
+ def _worktree_listed(worktree_path: Path, repo_root: str | Path | None) -> bool:
59
+ """Return whether git currently lists the worktree.
60
+
61
+ False is a safe fallback for probe failures, not definitive orphan proof.
62
+ Future cleanup UI must combine this with the rest of the status payload.
63
+ """
64
+ cwd = _worktree_list_cwd(worktree_path, repo_root)
65
+ if cwd is None:
66
+ return False
67
+ try:
68
+ result = _run_git(["worktree", "list", "--porcelain"], cwd)
69
+ except (OSError, subprocess.TimeoutExpired):
70
+ return False
71
+ if result.returncode != 0:
72
+ return False
73
+ return str(worktree_path) in _parse_worktree_list_porcelain(result.stdout)
74
+
75
+
76
+ def _status_porcelain(worktree_path: Path) -> tuple[bool, int]:
77
+ try:
78
+ result = _run_git(
79
+ ["status", "--porcelain", "--untracked-files=normal"],
80
+ worktree_path,
81
+ )
82
+ except (OSError, subprocess.TimeoutExpired):
83
+ return False, 0
84
+ if result.returncode != 0:
85
+ return False, 0
86
+ lines = [line for line in result.stdout.splitlines() if line]
87
+ return bool(lines), sum(1 for line in lines if line.startswith("??"))
88
+
89
+
90
+ def _ahead_behind(worktree_path: Path) -> dict:
91
+ payload = {
92
+ "ahead": 0,
93
+ "behind": 0,
94
+ "available": False,
95
+ "upstream": None,
96
+ }
97
+ try:
98
+ upstream = _run_git(
99
+ ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"],
100
+ worktree_path,
101
+ )
102
+ except (OSError, subprocess.TimeoutExpired):
103
+ return payload
104
+ if upstream.returncode != 0:
105
+ return payload
106
+ upstream_ref = upstream.stdout.strip()
107
+ if not upstream_ref:
108
+ return payload
109
+ payload["upstream"] = upstream_ref
110
+ try:
111
+ counts = _run_git(
112
+ ["rev-list", "--left-right", "--count", "HEAD...@{u}"],
113
+ worktree_path,
114
+ )
115
+ except (OSError, subprocess.TimeoutExpired):
116
+ return payload
117
+ if counts.returncode != 0:
118
+ return payload
119
+ parts = counts.stdout.strip().split()
120
+ if len(parts) != 2:
121
+ return payload
122
+ try:
123
+ payload["ahead"] = max(0, int(parts[0]))
124
+ payload["behind"] = max(0, int(parts[1]))
125
+ payload["available"] = True
126
+ except ValueError:
127
+ pass
128
+ return payload
129
+
130
+
131
+ def _locked_by_stream(session) -> bool:
132
+ stream_id = getattr(session, "active_stream_id", None)
133
+ if not stream_id:
134
+ return False
135
+ try:
136
+ from api.config import STREAMS, STREAMS_LOCK
137
+
138
+ with STREAMS_LOCK:
139
+ return stream_id in STREAMS
140
+ except Exception:
141
+ return False
142
+
143
+
144
+ def _locked_by_terminal(session_id: str, worktree_path: Path) -> bool:
145
+ try:
146
+ from api.terminal import get_terminal
147
+
148
+ term = get_terminal(session_id)
149
+ except Exception:
150
+ return False
151
+ if not term:
152
+ return False
153
+ try:
154
+ if not term.is_alive():
155
+ return False
156
+ terminal_workspace = _resolve_path(getattr(term, "workspace", None))
157
+ return terminal_workspace == worktree_path
158
+ except Exception:
159
+ return False
160
+
161
+
162
+ def worktree_status_for_session(session) -> dict:
163
+ """Return a read-only worktree status snapshot for a WebUI session."""
164
+ raw_path = getattr(session, "worktree_path", None)
165
+ if not raw_path:
166
+ raise ValueError("Session is not worktree-backed")
167
+
168
+ worktree_path = _resolve_path(raw_path)
169
+ if worktree_path is None:
170
+ raise ValueError("Session is not worktree-backed")
171
+
172
+ exists = worktree_path.is_dir()
173
+ status = {
174
+ "path": str(worktree_path),
175
+ "exists": bool(exists),
176
+ "dirty": False,
177
+ "untracked_count": 0,
178
+ "ahead_behind": {
179
+ "ahead": 0,
180
+ "behind": 0,
181
+ "available": False,
182
+ "upstream": None,
183
+ },
184
+ "locked_by_stream": _locked_by_stream(session),
185
+ "locked_by_terminal": _locked_by_terminal(
186
+ getattr(session, "session_id", ""),
187
+ worktree_path,
188
+ ),
189
+ "listed": _worktree_listed(
190
+ worktree_path,
191
+ getattr(session, "worktree_repo_root", None),
192
+ ),
193
+ }
194
+ if not exists:
195
+ return status
196
+
197
+ dirty, untracked_count = _status_porcelain(worktree_path)
198
+ status["dirty"] = dirty
199
+ status["untracked_count"] = untracked_count
200
+ status["ahead_behind"] = _ahead_behind(worktree_path)
201
+ return status
202
+
203
+
204
+ def remove_worktree_for_session(session, *, force: bool = False) -> dict:
205
+ """Remove a session's git worktree from disk.
206
+
207
+ Returns status dict with keys: ok, removed_path, warnings.
208
+ Raises ValueError for terminal blockers (locked by stream/terminal,
209
+ dirty with force=False).
210
+ """
211
+ raw_path = getattr(session, "worktree_path", None)
212
+ if not raw_path:
213
+ raise ValueError("Session is not worktree-backed")
214
+
215
+ worktree_path = _resolve_path(raw_path)
216
+ if worktree_path is None:
217
+ raise ValueError("Session is not worktree-backed")
218
+
219
+ # Read current status before removal
220
+ status = worktree_status_for_session(session)
221
+
222
+ if not status["exists"]:
223
+ return {
224
+ "ok": True,
225
+ "removed_path": str(worktree_path),
226
+ "warnings": ["Worktree directory no longer exists on disk."],
227
+ }
228
+
229
+ warnings = []
230
+
231
+ # Guard: locked by stream
232
+ if status["locked_by_stream"]:
233
+ raise ValueError("Worktree is locked by an active streaming session")
234
+
235
+ # Guard: locked by terminal
236
+ if status["locked_by_terminal"]:
237
+ raise ValueError("Worktree is locked by an active terminal session")
238
+
239
+ # Guard: local changes and unpushed commits without explicit force.
240
+ if status["dirty"] and not force:
241
+ raise ValueError(
242
+ "Worktree has uncommitted changes. Use force=true to override."
243
+ )
244
+ if status["untracked_count"] > 0:
245
+ if force:
246
+ warnings.append(
247
+ f"{status['untracked_count']} untracked file(s) will be removed."
248
+ )
249
+ else:
250
+ raise ValueError(
251
+ f"Worktree has {status['untracked_count']} untracked file(s). "
252
+ "Use force=true to override."
253
+ )
254
+ ahead = int((status.get("ahead_behind") or {}).get("ahead") or 0)
255
+ if ahead > 0:
256
+ if force:
257
+ warnings.append(f"{ahead} unpushed commit(s) will be removed.")
258
+ else:
259
+ raise ValueError(
260
+ f"Worktree has {ahead} unpushed commit(s). "
261
+ "Use force=true to override."
262
+ )
263
+
264
+ # Remove the worktree — must run from the repo root, not the worktree dir
265
+ repo_root = getattr(session, "worktree_repo_root", None)
266
+ if not repo_root:
267
+ raise ValueError("Session missing worktree_repo_root")
268
+ try:
269
+ remove_args = ["worktree", "remove"]
270
+ if force:
271
+ remove_args.append("--force")
272
+ remove_args.append(str(worktree_path))
273
+ result = _run_git(remove_args, str(repo_root), timeout=10)
274
+ except (OSError, subprocess.TimeoutExpired) as exc:
275
+ raise ValueError(f"Failed to remove worktree: {exc}") from exc
276
+
277
+ if result.returncode != 0:
278
+ stderr = (result.stderr or "").strip().split("\n")[-1]
279
+ raise ValueError(
280
+ f"git worktree remove failed: {stderr or result.stdout.strip()}"
281
+ )
282
+
283
+ # Prune in case the worktree dir was already gone
284
+ try:
285
+ _run_git(
286
+ ["worktree", "prune"],
287
+ str(repo_root),
288
+ timeout=5,
289
+ )
290
+ except Exception:
291
+ pass
292
+
293
+ return {
294
+ "ok": True,
295
+ "removed_path": str(worktree_path),
296
+ "warnings": warnings or None,
297
+ }
298
+
299
+
300
+ def find_git_repo_root(workspace: str | Path) -> Path:
301
+ """Return the enclosing git repo root for *workspace*.
302
+
303
+ Use git itself instead of checking ``workspace/.git`` so nested workspaces
304
+ and linked git worktrees are both handled correctly.
305
+ """
306
+ ws = Path(workspace).expanduser().resolve()
307
+ if not ws.is_dir():
308
+ raise ValueError("Workspace path does not exist or is not a directory")
309
+ try:
310
+ result = subprocess.run(
311
+ ["git", "rev-parse", "--show-toplevel"],
312
+ cwd=ws,
313
+ text=True,
314
+ capture_output=True,
315
+ timeout=5,
316
+ check=False,
317
+ )
318
+ except (OSError, subprocess.TimeoutExpired) as exc:
319
+ raise ValueError("Workspace is not inside a git repository") from exc
320
+ if result.returncode != 0:
321
+ raise ValueError("Workspace is not inside a git repository")
322
+ root = result.stdout.strip()
323
+ if not root:
324
+ raise ValueError("Workspace is not inside a git repository")
325
+ return Path(root).expanduser().resolve()
326
+
327
+
328
+ def _setup_agent_worktree(repo_root: str) -> dict:
329
+ try:
330
+ import api.config # noqa: F401 # ensure Hermes Agent dir is on sys.path
331
+ from cli import _setup_worktree
332
+ except Exception as exc:
333
+ raise RuntimeError("Hermes Agent worktree helper is unavailable") from exc
334
+ output = StringIO()
335
+ with redirect_stdout(output), redirect_stderr(output):
336
+ info = _setup_worktree(repo_root)
337
+ emitted = output.getvalue().strip()
338
+ if emitted:
339
+ logger.debug("Hermes Agent worktree helper output: %s", emitted)
340
+ if not info:
341
+ raise RuntimeError("Hermes Agent failed to create a git worktree")
342
+ return info
343
+
344
+
345
+ def create_worktree_for_workspace(workspace: str | Path) -> dict:
346
+ repo_root = find_git_repo_root(workspace)
347
+ info = _setup_agent_worktree(str(repo_root))
348
+ path = info.get("path")
349
+ branch = info.get("branch")
350
+ if not path or not branch:
351
+ raise RuntimeError("Hermes Agent returned incomplete worktree metadata")
352
+ return {
353
+ "path": str(Path(path).expanduser().resolve()),
354
+ "branch": str(branch),
355
+ "repo_root": str(Path(info.get("repo_root") or repo_root).expanduser().resolve()),
356
+ "created_at": time.time(),
357
+ }