0.2.4: искать воркера во всех профилях ~/.claude* (#5) #6
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "foreman-supervisor",
|
||||
"version": "0.2.3",
|
||||
"description": "Semantic supervision for our own Claude agents. Wraps thruwire/foreman (MIT) — a runtime that watches a coding agent with a bounded observation (job, git status, diff, output tails, repository instructions), scores ten independent yes/no questions, and lets a deterministic Python policy decide continue / stop / retry / verify / finish / escalate. This plugin replaces both of upstream's paid dependencies: the worker is `claude -p` (Worker protocol, steer() returns False so the policy degrades to stop/retry, per upstream's own docs/workers.md), and the judge is the free-model gateway (free-worker.sh) instead of TypeSafe Jev, so a supervised run costs no paid tokens. Adds two things measured here rather than assumed: the observation carries commits since the run's base commit (our agents commit, which empties `git diff` exactly when the worker does the right thing), and warning dimensions stay muted until the factory has produced evidence (a small judge escalated a healthy run at 25 seconds, before the worker touched a file). Ships foreman-calibrate.py, which replays thresholds over the runs' own JSONL timelines — the calibration upstream says it has not done. v0.2.0 adds bin/foreman-attach.py: supervision of a background worker someone ELSE already started (worker.sh / `claude agents`), which upstream cannot do because its runtime launches the worker itself. The observation is rebuilt from traces the worker already leaves — its state.json, the commits in its worktree, the tail of its transcript — so nothing is injected into the worker. Measured here on a live worker: warning dimensions read stuck 0.80 / off-track 0.80 at 362 s and 0.05 / 0.10 at 766 s on the same run with no commit in between, which is why warmup and confirmation matter more than threshold tuning. Also records that upstream's policy gates STEER_WORKER on `codex_backend == \"app-server\"` — the name of another backend rather than whether the worker accepts direction — so a steerable third-party worker can only ever be killed; the adapter works around it by printing the steering message for the lead to deliver. v0.2.3 resolves the worker's tree from the LIVE session process (/proc/<pid>/cwd, skipping the bg-pty-host wrapper whose cwd is where the session was launched) instead of trusting state.json's starting cwd: measured on a live worker whose state.json said /home/a/pro/siege-lines while the session worked in /data/siege-arena364, so the judge read commits from the wrong checkout and printed \"no commits\" at the moment the worker was opening a PR.",
|
||||
"version": "0.2.4",
|
||||
"description": "Semantic supervision for our own Claude agents. Wraps thruwire/foreman (MIT) — a runtime that watches a coding agent with a bounded observation (job, git status, diff, output tails, repository instructions), scores ten independent yes/no questions, and lets a deterministic Python policy decide continue / stop / retry / verify / finish / escalate. This plugin replaces both of upstream's paid dependencies: the worker is `claude -p` (Worker protocol, steer() returns False so the policy degrades to stop/retry, per upstream's own docs/workers.md), and the judge is the free-model gateway (free-worker.sh) instead of TypeSafe Jev, so a supervised run costs no paid tokens. Adds two things measured here rather than assumed: the observation carries commits since the run's base commit (our agents commit, which empties `git diff` exactly when the worker does the right thing), and warning dimensions stay muted until the factory has produced evidence (a small judge escalated a healthy run at 25 seconds, before the worker touched a file). Ships foreman-calibrate.py, which replays thresholds over the runs' own JSONL timelines — the calibration upstream says it has not done. v0.2.0 adds bin/foreman-attach.py: supervision of a background worker someone ELSE already started (worker.sh / `claude agents`), which upstream cannot do because its runtime launches the worker itself. The observation is rebuilt from traces the worker already leaves — its state.json, the commits in its worktree, the tail of its transcript — so nothing is injected into the worker. Measured here on a live worker: warning dimensions read stuck 0.80 / off-track 0.80 at 362 s and 0.05 / 0.10 at 766 s on the same run with no commit in between, which is why warmup and confirmation matter more than threshold tuning. Also records that upstream's policy gates STEER_WORKER on `codex_backend == \"app-server\"` — the name of another backend rather than whether the worker accepts direction — so a steerable third-party worker can only ever be killed; the adapter works around it by printing the steering message for the lead to deliver. v0.2.3 resolves the worker's tree from the LIVE session process (/proc/<pid>/cwd, skipping the bg-pty-host wrapper whose cwd is where the session was launched) instead of trusting state.json's starting cwd: measured on a live worker whose state.json said /home/a/pro/siege-lines while the session worked in /data/siege-arena364, so the judge read commits from the wrong checkout and printed \"no commits\" at the moment the worker was opening a PR. v0.2.4 finds a worker's state.json across ALL ~/.claude* profiles, own profile first: each profile keeps its own jobs directory, and a live worker resumed from a neighbouring profile was refused with \"worker not found\" while it was working.",
|
||||
"author": {
|
||||
"name": "oleks",
|
||||
"email": "plugins@oleks.space"
|
||||
|
||||
+18
-3
@@ -36,6 +36,21 @@ PLUGIN = Path.home() / ".claude" / "plugins" / "cache" / "kotkan-fleet" / "forem
|
||||
JOBS = Path.home() / ".claude" / "jobs"
|
||||
|
||||
|
||||
def job_state_path(short_id: str) -> Path | None:
|
||||
"""state.json воркера — в jobs ТОГО профиля, из которого его подняли.
|
||||
|
||||
kotkan/claude-plugin-foreman-supervisor#5: у каждого профиля (`~/.claude`, `~/.claude-anton`,
|
||||
`~/.claude-kotkan`) свой каталог `jobs`. Замер 20.09: реанимированная сессия арены легла в
|
||||
`~/.claude-anton/jobs/4d81cb97`, и присмотр отказывался её видеть — «воркер с таким id не
|
||||
найден» при живом, работающем воркере. Свой профиль проверяется первым.
|
||||
"""
|
||||
for jobs in [JOBS, *sorted(Path.home().glob(".claude*/jobs"))]:
|
||||
p = jobs / short_id / "state.json"
|
||||
if p.exists():
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def _plugin_bin() -> Path:
|
||||
"""Самая свежая установленная версия плагина — путь с версией внутри."""
|
||||
if not PLUGIN.exists():
|
||||
@@ -59,9 +74,9 @@ from foreman.observation import FactoryObservation # noqa: E402
|
||||
|
||||
|
||||
def read_state(short_id: str) -> dict:
|
||||
p = JOBS / short_id / "state.json"
|
||||
if not p.exists():
|
||||
sys.exit("нет %s — воркер с таким id не найден" % p)
|
||||
p = job_state_path(short_id)
|
||||
if p is None:
|
||||
sys.exit("нет jobs/%s/state.json ни в одном профиле ~/.claude* — воркер не найден" % short_id)
|
||||
return json.loads(p.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
|
||||
@@ -78,3 +78,25 @@ def test_unreadable_proc_does_not_crash(monkeypatch, tmp_path: Path) -> None:
|
||||
monkeypatch.setattr(attach.os, "readlink", boom)
|
||||
stale = tmp_path / "из-state"
|
||||
assert attach.worker_repo({"sessionId": SID, "cwd": str(stale)}) == stale
|
||||
|
||||
|
||||
def test_job_found_in_another_profile(monkeypatch, tmp_path: Path) -> None:
|
||||
"""Воркер, поднятый из другого профиля (~/.claude-anton), должен находиться.
|
||||
|
||||
kotkan/claude-plugin-foreman-supervisor#5: у каждого профиля свой jobs; присмотр отказывал
|
||||
живому воркеру «id не найден», потому что смотрел только в ~/.claude/jobs.
|
||||
"""
|
||||
other = tmp_path / ".claude-anton" / "jobs" / "4d81cb97"
|
||||
other.mkdir(parents=True)
|
||||
(other / "state.json").write_text('{"state": "working"}', encoding="utf-8")
|
||||
monkeypatch.setattr(attach.Path, "home", staticmethod(lambda: tmp_path))
|
||||
monkeypatch.setattr(attach, "JOBS", tmp_path / ".claude" / "jobs")
|
||||
assert attach.job_state_path("4d81cb97") == other / "state.json"
|
||||
|
||||
|
||||
def test_unknown_id_is_none(monkeypatch, tmp_path: Path) -> None:
|
||||
"""Несуществующий воркер — None, а не первый попавшийся путь."""
|
||||
(tmp_path / ".claude" / "jobs").mkdir(parents=True)
|
||||
monkeypatch.setattr(attach.Path, "home", staticmethod(lambda: tmp_path))
|
||||
monkeypatch.setattr(attach, "JOBS", tmp_path / ".claude" / "jobs")
|
||||
assert attach.job_state_path("нетакого") is None
|
||||
|
||||
Reference in New Issue
Block a user