vibego 0.2.58__py3-none-any.whl → 1.0.10__py3-none-any.whl
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.
- bot.py +1346 -1136
- logging_setup.py +25 -18
- master.py +812 -506
- project_repository.py +42 -40
- scripts/__init__.py +1 -2
- scripts/bump_version.sh +57 -55
- scripts/log_writer.py +19 -16
- scripts/master_healthcheck.py +38 -138
- scripts/models/claudecode.sh +4 -4
- scripts/models/codex.sh +1 -1
- scripts/models/common.sh +24 -6
- scripts/models/gemini.sh +2 -2
- scripts/publish.sh +50 -50
- scripts/requirements.txt +1 -0
- scripts/run_bot.sh +41 -17
- scripts/session_pointer_watch.py +265 -0
- scripts/start.sh +147 -120
- scripts/start_tmux_codex.sh +33 -8
- scripts/stop_all.sh +21 -21
- scripts/stop_bot.sh +31 -10
- scripts/test_deps_check.sh +32 -28
- tasks/__init__.py +1 -1
- tasks/commands.py +4 -4
- tasks/constants.py +1 -1
- tasks/fsm.py +9 -9
- tasks/models.py +7 -7
- tasks/service.py +56 -101
- vibego-1.0.10.dist-info/METADATA +226 -0
- {vibego-0.2.58.dist-info → vibego-1.0.10.dist-info}/RECORD +38 -36
- vibego-1.0.10.dist-info/licenses/LICENSE +201 -0
- vibego_cli/__init__.py +5 -4
- vibego_cli/__main__.py +1 -2
- vibego_cli/config.py +9 -9
- vibego_cli/deps.py +8 -9
- vibego_cli/main.py +63 -63
- vibego-0.2.58.dist-info/METADATA +0 -197
- {vibego-0.2.58.dist-info → vibego-1.0.10.dist-info}/WHEEL +0 -0
- {vibego-0.2.58.dist-info → vibego-1.0.10.dist-info}/entry_points.txt +0 -0
- {vibego-0.2.58.dist-info → vibego-1.0.10.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Capture the session JSONL path generated after a worker launch and lock it.
|
|
3
|
+
|
|
4
|
+
This helper watches the Codex/Claude session directory after the tmux worker
|
|
5
|
+
starts. The first rollout file that matches the configured working directory is
|
|
6
|
+
recorded into both the pointer file (used by the worker for streaming) and a
|
|
7
|
+
dedicated lock file so subsequent bindings never drift to another CLI session.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from fnmatch import fnmatch
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from threading import Event, Lock
|
|
21
|
+
from typing import Iterable, Optional
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
from watchdog.events import FileSystemEvent, FileSystemEventHandler
|
|
25
|
+
from watchdog.observers import Observer
|
|
26
|
+
except Exception: # pragma: no cover - watchdog is an optional dependency fallback
|
|
27
|
+
Observer = None
|
|
28
|
+
FileSystemEventHandler = object # type: ignore[assignment]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
log = logging.getLogger("session_pointer_watch")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _resolve(path: str) -> Path:
|
|
35
|
+
return Path(path).expanduser().resolve()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _read_session_cwd(path: Path) -> Optional[str]:
|
|
39
|
+
"""Read first JSON line and return payload.cwd when available."""
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
with path.open("r", encoding="utf-8", errors="ignore") as fh:
|
|
43
|
+
first_line = fh.readline()
|
|
44
|
+
except OSError:
|
|
45
|
+
return None
|
|
46
|
+
if not first_line:
|
|
47
|
+
return None
|
|
48
|
+
try:
|
|
49
|
+
data = json.loads(first_line)
|
|
50
|
+
except json.JSONDecodeError:
|
|
51
|
+
return None
|
|
52
|
+
payload = data.get("payload")
|
|
53
|
+
if isinstance(payload, dict):
|
|
54
|
+
raw = payload.get("cwd")
|
|
55
|
+
if isinstance(raw, str):
|
|
56
|
+
return raw
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _iter_candidate_files(roots: Iterable[Path], glob: str) -> Iterable[Path]:
|
|
61
|
+
for root in roots:
|
|
62
|
+
if not root.exists():
|
|
63
|
+
continue
|
|
64
|
+
try:
|
|
65
|
+
real_root = root.resolve()
|
|
66
|
+
except OSError:
|
|
67
|
+
real_root = root
|
|
68
|
+
yield from real_root.glob(f"**/{glob}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class _RolloutCapture(FileSystemEventHandler):
|
|
72
|
+
"""Handle filesystem events and pick the first rollout that matches criteria."""
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
*,
|
|
77
|
+
pattern: str,
|
|
78
|
+
baseline: set[str],
|
|
79
|
+
start_wall: float,
|
|
80
|
+
start_monotonic: float,
|
|
81
|
+
target_cwd: Optional[str],
|
|
82
|
+
timeout: float,
|
|
83
|
+
poll_interval: float,
|
|
84
|
+
) -> None:
|
|
85
|
+
self._pattern = pattern
|
|
86
|
+
self._baseline = baseline
|
|
87
|
+
self._start_wall = start_wall
|
|
88
|
+
self._target_cwd = target_cwd
|
|
89
|
+
self._deadline = start_monotonic + timeout
|
|
90
|
+
self._poll_interval = poll_interval
|
|
91
|
+
self._chosen: Optional[Path] = None
|
|
92
|
+
self._event = Event()
|
|
93
|
+
self._lock = Lock()
|
|
94
|
+
|
|
95
|
+
def _consider(self, candidate: Path) -> None:
|
|
96
|
+
if candidate.is_dir():
|
|
97
|
+
return
|
|
98
|
+
name = candidate.name
|
|
99
|
+
if not fnmatch(name, self._pattern):
|
|
100
|
+
return
|
|
101
|
+
try:
|
|
102
|
+
real_path = candidate.resolve()
|
|
103
|
+
except OSError:
|
|
104
|
+
real_path = candidate
|
|
105
|
+
real_key = str(real_path)
|
|
106
|
+
if real_key in self._baseline:
|
|
107
|
+
return
|
|
108
|
+
try:
|
|
109
|
+
stat = real_path.stat()
|
|
110
|
+
except OSError:
|
|
111
|
+
return
|
|
112
|
+
if stat.st_mtime + 0.01 < self._start_wall:
|
|
113
|
+
# Ignore historical files.
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
if self._target_cwd:
|
|
117
|
+
# Wait until the JSON header is flushed and matches our CWD.
|
|
118
|
+
deadline = time.monotonic() + self._poll_interval * 10
|
|
119
|
+
while time.monotonic() < deadline:
|
|
120
|
+
cwd = _read_session_cwd(real_path)
|
|
121
|
+
if cwd is None:
|
|
122
|
+
time.sleep(self._poll_interval)
|
|
123
|
+
continue
|
|
124
|
+
if cwd == self._target_cwd:
|
|
125
|
+
break
|
|
126
|
+
log.debug("Skip rollout with mismatched cwd=%s", cwd)
|
|
127
|
+
return
|
|
128
|
+
with self._lock:
|
|
129
|
+
if self._chosen is None:
|
|
130
|
+
self._chosen = real_path
|
|
131
|
+
self._event.set()
|
|
132
|
+
|
|
133
|
+
# The following methods are only called when watchdog is available.
|
|
134
|
+
def on_created(self, event: FileSystemEvent) -> None: # type: ignore[override]
|
|
135
|
+
if getattr(event, "is_directory", False):
|
|
136
|
+
return
|
|
137
|
+
self._consider(Path(event.src_path))
|
|
138
|
+
|
|
139
|
+
def on_moved(self, event: FileSystemEvent) -> None: # type: ignore[override]
|
|
140
|
+
if getattr(event, "is_directory", False):
|
|
141
|
+
return
|
|
142
|
+
self._consider(Path(event.dest_path))
|
|
143
|
+
|
|
144
|
+
def poll_until_found(self, roots: Iterable[Path]) -> Optional[Path]:
|
|
145
|
+
while time.monotonic() < self._deadline:
|
|
146
|
+
remaining = self._deadline - time.monotonic()
|
|
147
|
+
if remaining <= 0:
|
|
148
|
+
break
|
|
149
|
+
wait_time = min(self._poll_interval, remaining)
|
|
150
|
+
if self._event.wait(timeout=wait_time):
|
|
151
|
+
break
|
|
152
|
+
for candidate in _iter_candidate_files(roots, self._pattern):
|
|
153
|
+
self._consider(candidate)
|
|
154
|
+
if self._event.is_set():
|
|
155
|
+
break
|
|
156
|
+
if self._event.is_set():
|
|
157
|
+
break
|
|
158
|
+
return self._chosen
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _write_pointer(pointer: Path, session_path: Path) -> None:
|
|
162
|
+
pointer.parent.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
pointer.write_text(str(session_path), encoding="utf-8")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _write_lock(lock_file: Path, *, session_path: Path, tmux_session: str, project: str, workdir: str, method: str) -> None:
|
|
167
|
+
payload = {
|
|
168
|
+
"session_path": str(session_path),
|
|
169
|
+
"captured_at": datetime.now(timezone.utc).isoformat(),
|
|
170
|
+
"tmux_session": tmux_session,
|
|
171
|
+
"project": project,
|
|
172
|
+
"workdir": workdir,
|
|
173
|
+
"method": method,
|
|
174
|
+
}
|
|
175
|
+
lock_file.parent.mkdir(parents=True, exist_ok=True)
|
|
176
|
+
lock_file.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def main(argv: Optional[list[str]] = None) -> int:
|
|
180
|
+
parser = argparse.ArgumentParser(description="Capture newly created Codex/Claude rollout JSONL and write pointer+lock files.")
|
|
181
|
+
parser.add_argument("--pointer", required=True, help="Path to current_session.txt pointer file")
|
|
182
|
+
parser.add_argument("--lock", required=True, help="Path to persistent lock metadata JSON")
|
|
183
|
+
parser.add_argument("--session-root", required=False, help="Primary sessions root directory")
|
|
184
|
+
parser.add_argument("--additional-root", action="append", default=[], help="Extra directories to monitor for rollouts")
|
|
185
|
+
parser.add_argument("--glob", default="rollout-*.jsonl", help="Glob pattern for rollout files")
|
|
186
|
+
parser.add_argument("--workdir", default="", help="Model working directory, used to filter sessions")
|
|
187
|
+
parser.add_argument("--tmux-session", default="", help="tmux session name for diagnostics")
|
|
188
|
+
parser.add_argument("--project", default="", help="Project slug for diagnostics")
|
|
189
|
+
parser.add_argument("--timeout", type=float, default=180.0, help="Maximum seconds to wait for a new rollout")
|
|
190
|
+
parser.add_argument("--poll", type=float, default=0.5, help="Polling interval when watchdog is unavailable")
|
|
191
|
+
args = parser.parse_args(argv)
|
|
192
|
+
|
|
193
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
|
194
|
+
|
|
195
|
+
pointer_path = _resolve(args.pointer)
|
|
196
|
+
lock_path = _resolve(args.lock)
|
|
197
|
+
roots: list[Path] = []
|
|
198
|
+
if args.session_root:
|
|
199
|
+
roots.append(_resolve(args.session_root))
|
|
200
|
+
pointer_parent = pointer_path.parent
|
|
201
|
+
if pointer_parent not in roots:
|
|
202
|
+
roots.append(pointer_parent)
|
|
203
|
+
for raw in args.additional_root:
|
|
204
|
+
resolved = _resolve(raw)
|
|
205
|
+
if resolved not in roots:
|
|
206
|
+
roots.append(resolved)
|
|
207
|
+
|
|
208
|
+
baseline = {str(path.resolve()) for path in _iter_candidate_files(roots, args.glob)}
|
|
209
|
+
start_wall = time.time()
|
|
210
|
+
start_monotonic = time.monotonic()
|
|
211
|
+
|
|
212
|
+
capture = _RolloutCapture(
|
|
213
|
+
pattern=args.glob,
|
|
214
|
+
baseline=baseline,
|
|
215
|
+
start_wall=start_wall,
|
|
216
|
+
start_monotonic=start_monotonic,
|
|
217
|
+
target_cwd=args.workdir or None,
|
|
218
|
+
timeout=args.timeout,
|
|
219
|
+
poll_interval=max(args.poll, 0.1),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
observer: Optional[Observer] = None
|
|
223
|
+
method = "watchdog"
|
|
224
|
+
if Observer is not None:
|
|
225
|
+
observer = Observer()
|
|
226
|
+
for root in roots:
|
|
227
|
+
if root.exists():
|
|
228
|
+
observer.schedule(capture, str(root), recursive=True)
|
|
229
|
+
observer.start()
|
|
230
|
+
else: # pragma: no cover - watchdog is optional
|
|
231
|
+
log.warning("watchdog not available, falling back to polling")
|
|
232
|
+
method = "polling"
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
session_path = capture.poll_until_found(roots)
|
|
236
|
+
finally:
|
|
237
|
+
if observer is not None:
|
|
238
|
+
observer.stop()
|
|
239
|
+
observer.join(timeout=5)
|
|
240
|
+
|
|
241
|
+
if session_path is None:
|
|
242
|
+
log.error(
|
|
243
|
+
"Failed to detect rollout file within timeout %.1fs (tmux session=%s, project=%s)",
|
|
244
|
+
args.timeout,
|
|
245
|
+
args.tmux_session or "-",
|
|
246
|
+
args.project or "-",
|
|
247
|
+
)
|
|
248
|
+
return 1
|
|
249
|
+
|
|
250
|
+
_write_pointer(pointer_path, session_path)
|
|
251
|
+
_write_lock(
|
|
252
|
+
lock_path,
|
|
253
|
+
session_path=session_path,
|
|
254
|
+
tmux_session=args.tmux_session,
|
|
255
|
+
project=args.project,
|
|
256
|
+
workdir=args.workdir,
|
|
257
|
+
method=method,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
log.info("Recorded session pointer -> %s", session_path)
|
|
261
|
+
return 0
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
if __name__ == "__main__":
|
|
265
|
+
sys.exit(main())
|