@litfamily/lithermes 1.0.31 → 1.0.33
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 +7 -7
- package/README_Ko-KR.md +7 -7
- package/assets/lithermes-plugin/README.md +0 -12
- package/assets/lithermes-plugin/adhd_stack.py +43 -1
- package/assets/lithermes-plugin/auto_handoff.py +13 -2
- package/assets/lithermes-plugin/bounded_work.py +33 -3
- package/assets/lithermes-plugin/core.py +59 -18
- package/assets/lithermes-plugin/core_review.py +2 -2
- package/assets/lithermes-plugin/lit_mark.py +2 -7
- package/assets/lithermes-plugin/payload-version.json +43 -47
- package/assets/lithermes-plugin/plugin.yaml +1 -1
- package/assets/lithermes-plugin/rules/bundled-rules/baseline-discipline.md +7 -13
- package/assets/lithermes-plugin/rules/globmatch.py +1 -1
- package/assets/lithermes-plugin/skills/debugging/SKILL.md +1 -1
- package/assets/lithermes-plugin/skills/debugging/references/methodology/00-setup.md +1 -1
- package/assets/lithermes-plugin/skills/debugging/references/methodology/02-investigate.md +2 -2
- package/assets/lithermes-plugin/skills/debugging/references/methodology/09-cleanup.md +1 -1
- package/assets/lithermes-plugin/skills/debugging/references/runtimes/native-binary.md +1 -1
- package/assets/lithermes-plugin/skills/debugging/references/runtimes/rust.md +4 -4
- package/assets/lithermes-plugin/skills/debugging/references/tools/ghidra.md +1 -1
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/data/stacks/nuxtjs.csv +1 -1
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/manifest.json +2 -2
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/complete-contract.md +2 -2
- package/assets/lithermes-plugin/skills/lit-adhd/SKILL.md +10 -10
- package/assets/lithermes-plugin/skills/lit-adhd/references/brief-contract.md +12 -7
- package/assets/lithermes-plugin/skills/lit-adhd/scripts/lib/collect.mjs +81 -27
- package/assets/lithermes-plugin/skills/lit-adhd/scripts/lib/next-step.mjs +34 -6
- package/assets/lithermes-plugin/skills/lit-burnoff/SKILL.md +1 -1
- package/assets/lithermes-plugin/skills/lit-code/SKILL.md +4 -4
- package/assets/lithermes-plugin/skills/lit-code/references/rust/README.md +2 -2
- package/assets/lithermes-plugin/skills/lit-code/references/rust/async-tokio.md +4 -4
- package/assets/lithermes-plugin/skills/lit-code/references/rust/axum-stack.md +1 -1
- package/assets/lithermes-plugin/skills/lit-code/references/rust/clap-stack.md +1 -1
- package/assets/lithermes-plugin/skills/lit-code/references/rust/libraries.md +3 -3
- package/assets/lithermes-plugin/skills/lit-code/references/rust/proptest-insta.md +10 -10
- package/assets/lithermes-plugin/skills/lit-code/references/rust/zero-cost-safety.md +1 -1
- package/assets/lithermes-plugin/skills/lit-code/references/typescript/README.md +2 -2
- package/assets/lithermes-plugin/skills/lit-code/references/typescript/backend-hono.md +1 -1
- package/assets/lithermes-plugin/skills/lit-code/references/typescript/tsconfig-strict.md +1 -1
- package/assets/lithermes-plugin/skills/lit-code/scripts/typescript/check-no-excuse-rules.ts +2 -2
- package/assets/lithermes-plugin/skills/readme-studio/templates/remotion/package-lock.json +3 -3
- package/assets/lithermes-plugin/skills/readme-studio/templates/remotion/package.json +2 -1
- package/assets/lithermes-plugin/skills/rules/SKILL.md +1 -1
- package/assets/lithermes-plugin/skills/visual-qa/references/complete-contract.md +2 -2
- package/assets/{lithermes-plugin → npm-channel}/auto_update.py +43 -9
- package/package.json +1 -1
- package/readme-assets/badge-version.svg +1 -1
- package/src/lib/canonicalCorpus.js +1 -1
- package/src/lib/install.js +22 -0
- package/src/lib/skillPayload.js +10 -5
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
1
|
+
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.33/readme-assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.33/readme-assets/cover-motion.webp" width="100%" alt="LitFamily motion cover: seven armored robots power on one by one, the LitHermes robot wakes with glowing eyes and a lit frame, then LITFAMILY and KEEP THE WORK LIT. light up." /></picture></p>
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<a href="#install"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
5
|
-
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
4
|
+
<a href="#install"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.33/readme-assets/badge-version.svg" alt="1.0.33" /></a>
|
|
5
|
+
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.33/readme-assets/badge-license.svg" alt="MIT license" /></a>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
8
|
<p align="center">
|
|
9
|
-
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/docs/guide.md"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
10
|
-
<a href="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
11
|
-
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
9
|
+
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/docs/guide.md"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.33/readme-assets/lucide-book-open.svg" width="16" alt="" /> Guide</a>
|
|
10
|
+
<a href="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.33/readme-assets/ignition-film.mp4"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.33/readme-assets/lucide-play.svg" width="16" alt="" /> Ignition film</a>
|
|
11
|
+
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.33/readme-assets/lucide-shield-check.svg" width="16" alt="" /> MIT</a>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
14
|
# LitHermes
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
A plugin for **Hermes Agent**. Put `lit` in front of a request, and Hermes plans the task, does it, checks the result and writes down where things stand, so the next session can pick up where this one stopped.
|
|
19
19
|
|
|
20
|
-
**[README and docs on GitHub](https://github.com/wjgoarxiv/lithermes#readme)** · [한국어](https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
20
|
+
**[README and docs on GitHub](https://github.com/wjgoarxiv/lithermes#readme)** · [한국어](https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.33/README_Ko-KR.md)
|
|
21
21
|
|
|
22
22
|
## Install
|
|
23
23
|
|
package/README_Ko-KR.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
1
|
+
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.33/readme-assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.33/readme-assets/cover-motion.webp" width="100%" alt="LitFamily 모션 커버: 일곱 로봇 패널이 차례로 켜지고, LitHermes 로봇의 눈과 테두리가 빛난 뒤 LITFAMILY와 KEEP THE WORK LIT. 문구가 밝아지는 영상" /></picture></p>
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<a href="#설치"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
5
|
-
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
4
|
+
<a href="#설치"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.33/readme-assets/badge-version.svg" alt="1.0.33" /></a>
|
|
5
|
+
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.33/readme-assets/badge-license.svg" alt="MIT license" /></a>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
8
|
<p align="center">
|
|
9
|
-
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/docs/guide.ko.md"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
10
|
-
<a href="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
11
|
-
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
9
|
+
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/docs/guide.ko.md"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.33/readme-assets/lucide-book-open.svg" width="16" alt="" /> 안내</a>
|
|
10
|
+
<a href="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.33/readme-assets/ignition-film.mp4"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.33/readme-assets/lucide-play.svg" width="16" alt="" /> Ignition 영상</a>
|
|
11
|
+
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.33/readme-assets/lucide-shield-check.svg" width="16" alt="" /> MIT</a>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
14
|
# LitHermes
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
**Hermes Agent**용 플러그인입니다. 요청 앞에 `lit`을 붙이면 Hermes가 작업을 계획하고, 해내고, 결과를 확인한 뒤 어디까지 왔는지 적어 둡니다. 그래서 다음 세션이 그 기록을 읽고 멈춘 자리에서 이어 갈 수 있습니다.
|
|
19
19
|
|
|
20
|
-
**[GitHub의 README와 문서](https://github.com/wjgoarxiv/lithermes/blob/main/README_Ko-KR.md)** · [English](https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
20
|
+
**[GitHub의 README와 문서](https://github.com/wjgoarxiv/lithermes/blob/main/README_Ko-KR.md)** · [English](https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.33/README.md)
|
|
21
21
|
|
|
22
22
|
## 설치
|
|
23
23
|
|
|
@@ -111,18 +111,6 @@ CLI/plugin name, not the npm package name.
|
|
|
111
111
|
`lit-brainstorm` (after `lit`: `brainstorm`, `brainstorming`, `ideation`, `ideate`, `idea`, `ideas`),
|
|
112
112
|
`lit-burnoff`, `lit-burnoff-file`, `comment-checker`, `lsp`, `lsp-setup`, and
|
|
113
113
|
`rules`.
|
|
114
|
-
- On the first interactive top-level `pre_llm_call`, the payload runs one
|
|
115
|
-
foreground update barrier by default. It resolves the host-owned
|
|
116
|
-
`HERMES_HOME`, validates an exact stable registry version, and calls the
|
|
117
|
-
package's transaction bridge with `npx --yes --package @litfamily/lithermes@<version> -- lithermes
|
|
118
|
-
__auto-update --hermes-home PATH`. The bridge performs the real guarded
|
|
119
|
-
`install --yes --no-hud --no-style --no-patch-installed-hermes` child, separate locking, backup,
|
|
120
|
-
journal/receipt, rollback on interruption or timeout, and a truthful
|
|
121
|
-
post-install doctor. `--no-auto-update`, `LITHERMES_NO_AUTO_UPDATE`,
|
|
122
|
-
`NO_UPDATE_NOTIFIER`, and `LITHERMES_NO_UPDATE_CHECK` disable the relevant
|
|
123
|
-
path. Noninteractive, CI, JSON, dry-run, HUD/help/version/uninstall, and
|
|
124
|
-
delegate sessions remain no-op. The update bridge never receives npm
|
|
125
|
-
credentials and never uses the unsupported `--home` option.
|
|
126
114
|
- Optional automatic handoff (`auto_handoff.py`), off by default. The user
|
|
127
115
|
switches it on with `/lit-handoff auto on <percent>` (plain text reply, no
|
|
128
116
|
model call; `auto off` and `auto status` likewise) or with
|
|
@@ -13,7 +13,10 @@ step the last brief offered: the hook reads the next-step record the collector w
|
|
|
13
13
|
session (``.hermes/lithermes/adhd/next-<session>.json``) and returns a short context note for
|
|
14
14
|
the model. A plan saved under ``plans/`` during a lit-plan turn writes the same record
|
|
15
15
|
(``start-plan``), so ``go`` right after planning shows that plan's ``/start-work`` command. A record older than 30 minutes, or no record at all, reads as no next step on
|
|
16
|
-
record.
|
|
16
|
+
record. A ``continue-work`` record is re-checked against the run's state first: a run that is no
|
|
17
|
+
longer active or no longer bound to this session starts nothing, and a ``wait-lanes`` record
|
|
18
|
+
(other lanes were at work) starts nothing either. Hermes has no typed ``go`` command; ``go`` is
|
|
19
|
+
a word the hook consumes.
|
|
17
20
|
"""
|
|
18
21
|
from __future__ import annotations
|
|
19
22
|
|
|
@@ -214,6 +217,31 @@ def read_next_step(workspace: Path | str, session_id: Any) -> dict[str, Any] | N
|
|
|
214
217
|
return data if isinstance(data, dict) and isinstance(data.get("kind"), str) else None
|
|
215
218
|
|
|
216
219
|
|
|
220
|
+
_WORK_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,95}$")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _run_still_owned(workspace: Path | str, target: dict[str, Any], session_id: Any) -> bool:
|
|
224
|
+
"""True when the recorded bounded-work run is still active and bound to this session.
|
|
225
|
+
|
|
226
|
+
Reads the run's state file without taking its lock, so `go` never waits on a busy run.
|
|
227
|
+
A missing, unreadable or ownerless run does not hold. Never raises.
|
|
228
|
+
"""
|
|
229
|
+
work_id = str(target.get("workId") or "")
|
|
230
|
+
if not _WORK_ID.match(work_id):
|
|
231
|
+
return False
|
|
232
|
+
try:
|
|
233
|
+
path = Path(workspace) / ".hermes" / "lithermes" / "work" / work_id / "state.json"
|
|
234
|
+
if path.is_symlink() or not path.is_file() or path.stat().st_size > MAX_STATE_BYTES:
|
|
235
|
+
return False
|
|
236
|
+
state = json.loads(path.read_text(encoding="utf-8"))
|
|
237
|
+
except (OSError, ValueError):
|
|
238
|
+
return False
|
|
239
|
+
if not isinstance(state, dict):
|
|
240
|
+
return False
|
|
241
|
+
owner = state.get("active_session_id")
|
|
242
|
+
return state.get("status") == "active" and bool(owner) and owner == str(session_id or "")
|
|
243
|
+
|
|
244
|
+
|
|
217
245
|
def go_note(workspace: Path | str, session_id: Any, *, now: float | None = None) -> str:
|
|
218
246
|
"""The context note a consumed `go` injects: what to do next, or that nothing is on record."""
|
|
219
247
|
import time as _time
|
|
@@ -254,6 +282,20 @@ def go_note(workspace: Path | str, session_id: Any, *, now: float | None = None)
|
|
|
254
282
|
f"`/start-work {detail}`. Present that command for the user to run; do not run it, "
|
|
255
283
|
"and do not start anything else."
|
|
256
284
|
)
|
|
285
|
+
if record.get("kind") == "wait-lanes":
|
|
286
|
+
lanes = target.get("lanes")
|
|
287
|
+
count = f"{lanes} " if isinstance(lanes, int) and lanes > 0 else ""
|
|
288
|
+
return (
|
|
289
|
+
f"The user typed `go` while {count}other lane(s) of this repo were still at work when "
|
|
290
|
+
"the brief ran: start nothing. Say that `go` waits until those lanes finish, and offer "
|
|
291
|
+
"`lit adhd` for a fresh brief once they have."
|
|
292
|
+
)
|
|
293
|
+
if record.get("kind") == "continue-work" and not _run_still_owned(workspace, target, session_id):
|
|
294
|
+
return (
|
|
295
|
+
f"The user typed `go`, but the brief's step no longer holds: run {detail or 'unknown'} "
|
|
296
|
+
"is no longer active or no longer owned by this session. Say so and start nothing; "
|
|
297
|
+
"offer `lit adhd` for a fresh brief."
|
|
298
|
+
)
|
|
257
299
|
if record.get("kind") == "approve-boundary":
|
|
258
300
|
return (
|
|
259
301
|
"YOUR TURN\n"
|
|
@@ -350,14 +350,23 @@ def _host_comparison(setting: Setting) -> tuple[str, bool]:
|
|
|
350
350
|
|
|
351
351
|
# ---- the host's own compaction point ---------------------------------------
|
|
352
352
|
|
|
353
|
+
def _yaml_module() -> Any:
|
|
354
|
+
try:
|
|
355
|
+
return importlib.import_module("yaml")
|
|
356
|
+
except ImportError:
|
|
357
|
+
return None
|
|
358
|
+
|
|
359
|
+
|
|
353
360
|
def _host_config() -> dict[str, Any]:
|
|
361
|
+
yaml = _yaml_module()
|
|
362
|
+
if yaml is None:
|
|
363
|
+
return {"_lithermes_yaml": "missing"}
|
|
354
364
|
try:
|
|
355
365
|
path = get_hermes_home() / "config.yaml"
|
|
356
366
|
if path.stat().st_size > 1024 * 1024:
|
|
357
367
|
return {}
|
|
358
|
-
yaml = importlib.import_module("yaml")
|
|
359
368
|
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
360
|
-
except
|
|
369
|
+
except (OSError, ValueError, UnicodeDecodeError, yaml.YAMLError):
|
|
361
370
|
return {}
|
|
362
371
|
return data if isinstance(data, dict) else {}
|
|
363
372
|
|
|
@@ -459,6 +468,8 @@ def _context_window(model: str, base_url: str, provider: str, config: Mapping[st
|
|
|
459
468
|
def host_estimate() -> tuple[float | None, str]:
|
|
460
469
|
"""Estimate Hermes' compaction point for the configured model, or say why not."""
|
|
461
470
|
config = _host_config()
|
|
471
|
+
if config.get("_lithermes_yaml") == "missing":
|
|
472
|
+
return None, "PyYAML is not importable, so the compaction comparison is missing"
|
|
462
473
|
section = config.get("model") if isinstance(config.get("model"), Mapping) else {}
|
|
463
474
|
model = str(section.get("default") or "")
|
|
464
475
|
window = _context_window(model, str(section.get("base_url") or ""), str(section.get("provider") or ""), config)
|
|
@@ -16,6 +16,7 @@ import re
|
|
|
16
16
|
import secrets
|
|
17
17
|
import shlex
|
|
18
18
|
import tempfile
|
|
19
|
+
import time
|
|
19
20
|
from contextlib import contextmanager
|
|
20
21
|
from pathlib import Path
|
|
21
22
|
from typing import Any, Iterator
|
|
@@ -38,6 +39,7 @@ MAX_HISTORY = 64
|
|
|
38
39
|
MAX_LEDGER_EVENTS = 96
|
|
39
40
|
MAX_IDENTIFIER = 96
|
|
40
41
|
MAX_SESSION = 256
|
|
42
|
+
PROMPT_LOCK_WAIT_SECONDS = 2.0
|
|
41
43
|
TERMINAL_STATUSES = {"cancelled", "completed"}
|
|
42
44
|
PROGRESS_STATUSES = {"pending", "in_progress", "blocked", "pass", "fail"}
|
|
43
45
|
_FORBIDDEN_ACTION_TOKENS = {
|
|
@@ -202,15 +204,32 @@ def _atomic_json(path: Path, payload: dict[str, Any]) -> None:
|
|
|
202
204
|
pass
|
|
203
205
|
|
|
204
206
|
|
|
207
|
+
class BoundedWorkBusy(OSError):
|
|
208
|
+
"""The run lock stayed held past the caller's wait limit."""
|
|
209
|
+
|
|
210
|
+
|
|
205
211
|
@contextmanager
|
|
206
|
-
def _locked(run_dir: Path) -> Iterator[None]:
|
|
212
|
+
def _locked(run_dir: Path, wait_seconds: float | None = None) -> Iterator[None]:
|
|
207
213
|
run_dir.mkdir(parents=True, exist_ok=True)
|
|
208
214
|
lock_path = run_dir / ".lock"
|
|
209
215
|
with lock_path.open("a+", encoding="utf-8") as handle:
|
|
210
216
|
try:
|
|
211
217
|
import fcntl
|
|
212
218
|
|
|
213
|
-
|
|
219
|
+
if wait_seconds is None:
|
|
220
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
|
|
221
|
+
else:
|
|
222
|
+
# Prompt-path callers retry a non-blocking lock until the limit,
|
|
223
|
+
# so a stuck holder cannot stall the user's turn.
|
|
224
|
+
deadline = time.monotonic() + wait_seconds
|
|
225
|
+
while True:
|
|
226
|
+
try:
|
|
227
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
228
|
+
break
|
|
229
|
+
except BlockingIOError:
|
|
230
|
+
if time.monotonic() >= deadline:
|
|
231
|
+
raise BoundedWorkBusy(f"bounded work lock is busy: {run_dir}") from None
|
|
232
|
+
time.sleep(0.05)
|
|
214
233
|
yield
|
|
215
234
|
finally:
|
|
216
235
|
try:
|
|
@@ -430,12 +449,13 @@ def bind_bounded_work(
|
|
|
430
449
|
activation_token: str,
|
|
431
450
|
*,
|
|
432
451
|
session_id: str,
|
|
452
|
+
wait_seconds: float | None = None,
|
|
433
453
|
) -> dict[str, Any] | None:
|
|
434
454
|
root = _canonical_worktree(worktree)
|
|
435
455
|
if not session_id or len(session_id) > MAX_SESSION or len(activation_token) > 256:
|
|
436
456
|
return None
|
|
437
457
|
run_dir = _work_dir(root, work_id)
|
|
438
|
-
with _locked(run_dir):
|
|
458
|
+
with _locked(run_dir, wait_seconds):
|
|
439
459
|
state = _reconcile_locked(run_dir)
|
|
440
460
|
if state["status"] != "active":
|
|
441
461
|
return None
|
|
@@ -1018,6 +1038,16 @@ def consume_activation_message(message: str, *, session_id: str) -> str | None:
|
|
|
1018
1038
|
payload["work_id"],
|
|
1019
1039
|
payload["token"],
|
|
1020
1040
|
session_id=session_id,
|
|
1041
|
+
wait_seconds=PROMPT_LOCK_WAIT_SECONDS,
|
|
1042
|
+
)
|
|
1043
|
+
except BoundedWorkBusy:
|
|
1044
|
+
return "\n".join(
|
|
1045
|
+
[
|
|
1046
|
+
'<lithermes-bounded-work schema="3" status="busy">',
|
|
1047
|
+
"The bounded work run is busy: another LitHermes call holds its lock, so this activation was not bound.",
|
|
1048
|
+
"Tell the user the run is busy and to send it again in a moment. No authority is active for this turn.",
|
|
1049
|
+
"</lithermes-bounded-work>",
|
|
1050
|
+
]
|
|
1021
1051
|
)
|
|
1022
1052
|
except (OSError, ValueError):
|
|
1023
1053
|
return None
|
|
@@ -71,11 +71,6 @@ try:
|
|
|
71
71
|
except (ImportError, ModuleNotFoundError):
|
|
72
72
|
import rules as _rules
|
|
73
73
|
|
|
74
|
-
try:
|
|
75
|
-
from . import auto_update as _auto_update
|
|
76
|
-
except (ImportError, ModuleNotFoundError):
|
|
77
|
-
import auto_update as _auto_update
|
|
78
|
-
|
|
79
74
|
try:
|
|
80
75
|
from . import auto_handoff as _auto_handoff
|
|
81
76
|
except (ImportError, ModuleNotFoundError):
|
|
@@ -1656,6 +1651,60 @@ def browser_drive_tool_guard(
|
|
|
1656
1651
|
}
|
|
1657
1652
|
|
|
1658
1653
|
|
|
1654
|
+
NPM_CHANNEL_MARKER_NAME = ".lithermes-npm-channel"
|
|
1655
|
+
NPM_CHANNEL_MARKER_TEXT = "lithermes npm channel\n"
|
|
1656
|
+
_NPM_CHANNEL_MODULE: Any = False
|
|
1657
|
+
|
|
1658
|
+
|
|
1659
|
+
def _npm_channel_module() -> Any:
|
|
1660
|
+
"""Load the npm updater once. Reloading it would clear its per-session guard."""
|
|
1661
|
+
|
|
1662
|
+
global _NPM_CHANNEL_MODULE
|
|
1663
|
+
if _NPM_CHANNEL_MODULE is not False:
|
|
1664
|
+
return _NPM_CHANNEL_MODULE
|
|
1665
|
+
import importlib.util
|
|
1666
|
+
|
|
1667
|
+
here = Path(__file__).resolve().parent
|
|
1668
|
+
module_path = here / "auto_update.py"
|
|
1669
|
+
marker_path = here / NPM_CHANNEL_MARKER_NAME
|
|
1670
|
+
loaded = None
|
|
1671
|
+
try:
|
|
1672
|
+
if (
|
|
1673
|
+
not module_path.is_symlink()
|
|
1674
|
+
and not marker_path.is_symlink()
|
|
1675
|
+
and module_path.is_file()
|
|
1676
|
+
and marker_path.is_file()
|
|
1677
|
+
and marker_path.read_text(encoding="utf-8") == NPM_CHANNEL_MARKER_TEXT
|
|
1678
|
+
):
|
|
1679
|
+
spec = importlib.util.spec_from_file_location("_lithermes_npm_channel", module_path)
|
|
1680
|
+
if spec is not None and spec.loader is not None:
|
|
1681
|
+
module = importlib.util.module_from_spec(spec)
|
|
1682
|
+
sys.modules[spec.name] = module
|
|
1683
|
+
spec.loader.exec_module(module)
|
|
1684
|
+
loaded = module
|
|
1685
|
+
except Exception:
|
|
1686
|
+
loaded = None
|
|
1687
|
+
_NPM_CHANNEL_MODULE = loaded
|
|
1688
|
+
return loaded
|
|
1689
|
+
|
|
1690
|
+
|
|
1691
|
+
def _npm_channel_pre_llm(kwargs: dict[str, Any]) -> None:
|
|
1692
|
+
"""Run the npm-only updater when this directory carries both channel files.
|
|
1693
|
+
|
|
1694
|
+
A catalog copy never has the sibling module and the marker together, so it
|
|
1695
|
+
cannot reach the updater. A symlink is not the installer's file.
|
|
1696
|
+
"""
|
|
1697
|
+
|
|
1698
|
+
module = _npm_channel_module()
|
|
1699
|
+
if module is None:
|
|
1700
|
+
return
|
|
1701
|
+
try:
|
|
1702
|
+
module.pre_llm_call(**kwargs)
|
|
1703
|
+
except Exception as exc:
|
|
1704
|
+
if exc.__class__.__module__ == "_lithermes_npm_channel" and exc.__class__.__name__ == "UnknownStateError":
|
|
1705
|
+
raise
|
|
1706
|
+
|
|
1707
|
+
|
|
1659
1708
|
def pre_llm_call(**kwargs: Any) -> dict[str, str] | None:
|
|
1660
1709
|
# Hermes identifies delegate_task children with platform="subagent".
|
|
1661
1710
|
# Child goals may quote words such as "lit" or "lit-crucible" as inert task
|
|
@@ -1664,19 +1713,11 @@ def pre_llm_call(**kwargs: Any) -> dict[str, str] | None:
|
|
|
1664
1713
|
release_browser_drive_state(kwargs.get("session_id"))
|
|
1665
1714
|
return None
|
|
1666
1715
|
# A first interactive top-level turn is the only plugin lifecycle point
|
|
1667
|
-
# where
|
|
1668
|
-
#
|
|
1669
|
-
#
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
except _auto_update.UnknownStateError:
|
|
1673
|
-
# A failed restore is not an advisory miss: continuing would let the
|
|
1674
|
-
# model operate against a profile whose installed payload is unknown.
|
|
1675
|
-
raise
|
|
1676
|
-
except Exception:
|
|
1677
|
-
# Ordinary registry/installer failures remain fail-open for this turn;
|
|
1678
|
-
# the transaction receipt records the redacted reason.
|
|
1679
|
-
pass
|
|
1716
|
+
# where an npm install can safely foreground an exact update. The channel
|
|
1717
|
+
# loader no-ops unless this directory is that install. The updater owns
|
|
1718
|
+
# its recursion guard, timeout, transaction, rollback, and receipt, and it
|
|
1719
|
+
# never contributes text to the model-facing context.
|
|
1720
|
+
_npm_channel_pre_llm(kwargs)
|
|
1680
1721
|
user_message = str(kwargs.get("user_message") or "")
|
|
1681
1722
|
requested_reader_mode = reader_facing_mode_from_user_request(user_message)
|
|
1682
1723
|
reader_contract = reader_facing_contract_block(
|
|
@@ -61,7 +61,7 @@ def _decode_git_path(value: bytes) -> str:
|
|
|
61
61
|
def _changed_paths(workspace: Path, base: str) -> list[str]:
|
|
62
62
|
paths = {
|
|
63
63
|
_decode_git_path(value)
|
|
64
|
-
for value in _run_git_bytes(workspace, ["diff", "--name-only", "-z", base]).split(b"\0")
|
|
64
|
+
for value in _run_git_bytes(workspace, ["diff", "--name-only", "-z", "--", base]).split(b"\0")
|
|
65
65
|
if value
|
|
66
66
|
}
|
|
67
67
|
records = _run_git_bytes(
|
|
@@ -255,7 +255,7 @@ def command_review_work(raw_args: str) -> dict[str, str]:
|
|
|
255
255
|
if mode != "work":
|
|
256
256
|
return _plan_or_ask_review(mode, mode_line, target, workspace, reader_args)
|
|
257
257
|
changed_paths = _changed_paths(workspace, base)
|
|
258
|
-
diff = _run_git(workspace, ["diff", base])
|
|
258
|
+
diff = _run_git(workspace, ["diff", "--", base])
|
|
259
259
|
if len(diff) > 60000:
|
|
260
260
|
diff = diff[:60000] + "\n... [diff truncated at 60k chars — lanes should read full files as needed]"
|
|
261
261
|
run_cmd = detect_run_command(workspace)
|
|
@@ -260,10 +260,5 @@ def skin_logo(*, env=None, stream=None, json=False) -> str:
|
|
|
260
260
|
|
|
261
261
|
|
|
262
262
|
def configure_welcome_skin() -> None:
|
|
263
|
-
"""
|
|
264
|
-
|
|
265
|
-
from hermes_cli.skin_engine import get_active_skin, get_active_skin_name
|
|
266
|
-
except ImportError:
|
|
267
|
-
return
|
|
268
|
-
if get_active_skin_name().startswith("lithermes-"):
|
|
269
|
-
get_active_skin().banner_logo = skin_logo(json="--json" in sys.argv)
|
|
263
|
+
"""Leave the active skin as the host loaded it. The managed skin file carries its own logo."""
|
|
264
|
+
return None
|