@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.
- package/README.md +213 -0
- package/bin/hermes-webui.mjs +588 -0
- package/package.json +25 -0
- package/scripts/sync-vendor.mjs +74 -0
- package/templates/launchd/com.bitseek.hermes-webui.plist +21 -0
- package/templates/systemd/hermes-webui.service +13 -0
- package/templates/windows/hermes-webui-task.ps1 +3 -0
- package/vendor/agent-frontend-shell/.bitseek-source.json +6 -0
- package/vendor/agent-frontend-shell/.dockerignore +7 -0
- package/vendor/agent-frontend-shell/.env.docker.example +89 -0
- package/vendor/agent-frontend-shell/.env.example +34 -0
- package/vendor/agent-frontend-shell/.github/FUNDING.yml +3 -0
- package/vendor/agent-frontend-shell/.github/workflows/browser-smoke.yml +42 -0
- package/vendor/agent-frontend-shell/.github/workflows/docker-smoke.yml +233 -0
- package/vendor/agent-frontend-shell/.github/workflows/native-windows-startup.yml +132 -0
- package/vendor/agent-frontend-shell/.github/workflows/release.yml +57 -0
- package/vendor/agent-frontend-shell/.github/workflows/tests.yml +88 -0
- package/vendor/agent-frontend-shell/.vscode/launch.json +59 -0
- package/vendor/agent-frontend-shell/.vscode/settings.json +13 -0
- package/vendor/agent-frontend-shell/AGENTS.md +80 -0
- package/vendor/agent-frontend-shell/ARCHITECTURE.md +1658 -0
- package/vendor/agent-frontend-shell/BUGS.md +52 -0
- package/vendor/agent-frontend-shell/CHANGELOG.md +7295 -0
- package/vendor/agent-frontend-shell/CONTRIBUTING.md +205 -0
- package/vendor/agent-frontend-shell/CONTRIBUTORS.md +107 -0
- package/vendor/agent-frontend-shell/DESIGN.md +173 -0
- package/vendor/agent-frontend-shell/Dockerfile +91 -0
- package/vendor/agent-frontend-shell/LICENSE +21 -0
- package/vendor/agent-frontend-shell/README-CUSTOM.md +76 -0
- package/vendor/agent-frontend-shell/README.md +705 -0
- package/vendor/agent-frontend-shell/ROADMAP.md +351 -0
- package/vendor/agent-frontend-shell/SPRINTS.md +147 -0
- package/vendor/agent-frontend-shell/TESTING.md +1932 -0
- package/vendor/agent-frontend-shell/THEMES.md +170 -0
- package/vendor/agent-frontend-shell/api/__init__.py +1 -0
- package/vendor/agent-frontend-shell/api/agent_health.py +392 -0
- package/vendor/agent-frontend-shell/api/agent_sessions.py +782 -0
- package/vendor/agent-frontend-shell/api/auth.py +592 -0
- package/vendor/agent-frontend-shell/api/background.py +87 -0
- package/vendor/agent-frontend-shell/api/clarify.py +238 -0
- package/vendor/agent-frontend-shell/api/commands.py +124 -0
- package/vendor/agent-frontend-shell/api/compression_anchor.py +134 -0
- package/vendor/agent-frontend-shell/api/config.py +5178 -0
- package/vendor/agent-frontend-shell/api/dashboard_probe.py +255 -0
- package/vendor/agent-frontend-shell/api/extensions.py +253 -0
- package/vendor/agent-frontend-shell/api/gateway_chat.py +435 -0
- package/vendor/agent-frontend-shell/api/gateway_watcher.py +230 -0
- package/vendor/agent-frontend-shell/api/goals.py +608 -0
- package/vendor/agent-frontend-shell/api/helpers.py +474 -0
- package/vendor/agent-frontend-shell/api/kanban_bridge.py +1255 -0
- package/vendor/agent-frontend-shell/api/metering.py +194 -0
- package/vendor/agent-frontend-shell/api/models.py +4210 -0
- package/vendor/agent-frontend-shell/api/oauth.py +770 -0
- package/vendor/agent-frontend-shell/api/onboarding.py +1046 -0
- package/vendor/agent-frontend-shell/api/passkeys.py +365 -0
- package/vendor/agent-frontend-shell/api/profiles.py +1499 -0
- package/vendor/agent-frontend-shell/api/providers.py +2175 -0
- package/vendor/agent-frontend-shell/api/request_diagnostics.py +160 -0
- package/vendor/agent-frontend-shell/api/rollback.py +320 -0
- package/vendor/agent-frontend-shell/api/routes.py +13990 -0
- package/vendor/agent-frontend-shell/api/run_journal.py +284 -0
- package/vendor/agent-frontend-shell/api/runner_client.py +156 -0
- package/vendor/agent-frontend-shell/api/runtime_adapter.py +431 -0
- package/vendor/agent-frontend-shell/api/session_discoverability.py +640 -0
- package/vendor/agent-frontend-shell/api/session_events.py +45 -0
- package/vendor/agent-frontend-shell/api/session_lifecycle.py +208 -0
- package/vendor/agent-frontend-shell/api/session_ops.py +207 -0
- package/vendor/agent-frontend-shell/api/session_recovery.py +655 -0
- package/vendor/agent-frontend-shell/api/skill_usage.py +32 -0
- package/vendor/agent-frontend-shell/api/startup.py +128 -0
- package/vendor/agent-frontend-shell/api/state_sync.py +187 -0
- package/vendor/agent-frontend-shell/api/streaming.py +7048 -0
- package/vendor/agent-frontend-shell/api/system_health.py +167 -0
- package/vendor/agent-frontend-shell/api/terminal.py +410 -0
- package/vendor/agent-frontend-shell/api/turn_journal.py +214 -0
- package/vendor/agent-frontend-shell/api/updates.py +1261 -0
- package/vendor/agent-frontend-shell/api/upload.py +322 -0
- package/vendor/agent-frontend-shell/api/usage.py +26 -0
- package/vendor/agent-frontend-shell/api/workspace.py +867 -0
- package/vendor/agent-frontend-shell/api/workspace_git.py +1261 -0
- package/vendor/agent-frontend-shell/api/worktrees.py +357 -0
- package/vendor/agent-frontend-shell/bootstrap.py +492 -0
- package/vendor/agent-frontend-shell/ctl.sh +427 -0
- package/vendor/agent-frontend-shell/docker-compose.custom.yml +26 -0
- package/vendor/agent-frontend-shell/docker-compose.three-container.yml +168 -0
- package/vendor/agent-frontend-shell/docker-compose.two-container.yml +147 -0
- package/vendor/agent-frontend-shell/docker-compose.yml +57 -0
- package/vendor/agent-frontend-shell/docker_init.bash +459 -0
- package/vendor/agent-frontend-shell/docs/CONTRACTS.md +207 -0
- package/vendor/agent-frontend-shell/docs/EXTENSIONS.md +212 -0
- package/vendor/agent-frontend-shell/docs/ISSUES.md +23 -0
- package/vendor/agent-frontend-shell/docs/UIUX-GUIDE.md +196 -0
- package/vendor/agent-frontend-shell/docs/advanced-chat-setup.md +83 -0
- package/vendor/agent-frontend-shell/docs/docker.md +337 -0
- package/vendor/agent-frontend-shell/docs/onboarding-agent-checklist.md +207 -0
- package/vendor/agent-frontend-shell/docs/onboarding.md +202 -0
- package/vendor/agent-frontend-shell/docs/remote-access.md +75 -0
- package/vendor/agent-frontend-shell/docs/rfcs/README.md +53 -0
- package/vendor/agent-frontend-shell/docs/rfcs/agent-source-boundary.md +70 -0
- package/vendor/agent-frontend-shell/docs/rfcs/canonical-session-resolution.md +124 -0
- package/vendor/agent-frontend-shell/docs/rfcs/hermes-run-adapter-contract.md +1079 -0
- package/vendor/agent-frontend-shell/docs/rfcs/turn-journal.md +195 -0
- package/vendor/agent-frontend-shell/docs/rfcs/webui-run-state-consistency-contract.md +157 -0
- package/vendor/agent-frontend-shell/docs/supervisor.md +280 -0
- package/vendor/agent-frontend-shell/docs/troubleshooting.md +132 -0
- package/vendor/agent-frontend-shell/docs/ui-ux/index.html +863 -0
- package/vendor/agent-frontend-shell/docs/ui-ux/two-stage-proposal.html +768 -0
- package/vendor/agent-frontend-shell/docs/why-hermes.md +489 -0
- package/vendor/agent-frontend-shell/docs/workspace-git.md +92 -0
- package/vendor/agent-frontend-shell/docs/wsl-autostart.md +126 -0
- package/vendor/agent-frontend-shell/eslint.runtime-guard.config.mjs +35 -0
- package/vendor/agent-frontend-shell/extensions/bitseek-design-system.md +330 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/apple-touch-icon.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/empty-logo.svg +739 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-192.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-32.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.svg +745 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.ico +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.svg +745 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v2.svg +751 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v3.svg +739 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon.svg +745 -0
- package/vendor/agent-frontend-shell/extensions/branding/branding.js +112 -0
- package/vendor/agent-frontend-shell/extensions/branding/config.json +14 -0
- package/vendor/agent-frontend-shell/extensions/branding/manifest.json +53 -0
- package/vendor/agent-frontend-shell/extensions/index.js +67 -0
- package/vendor/agent-frontend-shell/extensions/loader/hermes-loader.js +77 -0
- package/vendor/agent-frontend-shell/extensions/manifest.json +16 -0
- package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.css +333 -0
- package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.js +487 -0
- package/vendor/agent-frontend-shell/extensions/pages/manifest.json +6 -0
- package/vendor/agent-frontend-shell/extensions/pages/registry.css +56 -0
- package/vendor/agent-frontend-shell/extensions/pages/registry.js +302 -0
- package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.css +93 -0
- package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.js +98 -0
- package/vendor/agent-frontend-shell/install.sh +63 -0
- package/vendor/agent-frontend-shell/mcp_server.py +567 -0
- package/vendor/agent-frontend-shell/package.json +12 -0
- package/vendor/agent-frontend-shell/pyproject.toml +56 -0
- package/vendor/agent-frontend-shell/pytest.ini +3 -0
- package/vendor/agent-frontend-shell/requirements.txt +5 -0
- package/vendor/agent-frontend-shell/server.py +624 -0
- package/vendor/agent-frontend-shell/start.ps1 +210 -0
- package/vendor/agent-frontend-shell/start.sh +65 -0
- package/vendor/agent-frontend-shell/static/apple-touch-icon.png +0 -0
- package/vendor/agent-frontend-shell/static/boot.js +1990 -0
- package/vendor/agent-frontend-shell/static/commands.js +1402 -0
- package/vendor/agent-frontend-shell/static/favicon-192.png +0 -0
- package/vendor/agent-frontend-shell/static/favicon-32.png +0 -0
- package/vendor/agent-frontend-shell/static/favicon-512.png +0 -0
- package/vendor/agent-frontend-shell/static/favicon-512.svg +18 -0
- package/vendor/agent-frontend-shell/static/favicon.ico +0 -0
- package/vendor/agent-frontend-shell/static/favicon.svg +20 -0
- package/vendor/agent-frontend-shell/static/i18n.js +15389 -0
- package/vendor/agent-frontend-shell/static/icons.js +92 -0
- package/vendor/agent-frontend-shell/static/index.html +1506 -0
- package/vendor/agent-frontend-shell/static/login.js +177 -0
- package/vendor/agent-frontend-shell/static/manifest.json +53 -0
- package/vendor/agent-frontend-shell/static/messages.js +3521 -0
- package/vendor/agent-frontend-shell/static/onboarding.js +800 -0
- package/vendor/agent-frontend-shell/static/panels.js +7995 -0
- package/vendor/agent-frontend-shell/static/pwa-startup.js +83 -0
- package/vendor/agent-frontend-shell/static/sessions.js +5165 -0
- package/vendor/agent-frontend-shell/static/style.css +4774 -0
- package/vendor/agent-frontend-shell/static/sw.js +173 -0
- package/vendor/agent-frontend-shell/static/terminal.js +632 -0
- package/vendor/agent-frontend-shell/static/ui.js +8997 -0
- package/vendor/agent-frontend-shell/static/vendor/js-yaml/4.1.0/js-yaml.min.js +2 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.css +1 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.js +1 -0
- package/vendor/agent-frontend-shell/static/vendor/smd.min.js +29 -0
- 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
|
+
}
|