@litfamily/lithermes 1.0.5 → 1.0.7

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 CHANGED
@@ -1,7 +1,7 @@
1
- <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/readme-assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/readme-assets/cover-motion.webp" width="100%" alt="LitHermes cover art. A lime signal moves through orbital paths above the HERMES AGENT wordmark; a still frame from this motion cover appears when reduced motion is preferred." /></picture></p>
2
- <p align="center"><a href="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/readme-assets/cover.webp">View the static family cover</a></p>
1
+ <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.7/readme-assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.7/readme-assets/cover-motion.webp" width="100%" alt="LitHermes cover art. A lime signal moves through orbital paths above the HERMES AGENT wordmark; a still frame from this motion cover appears when reduced motion is preferred." /></picture></p>
2
+ <p align="center"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.7/readme-assets/cover.webp" width="100%" alt="Five armored robots, one for each LitFamily product, beneath the words KEEP THE WORK LIT." /></p>
3
3
 
4
- <p align="center"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/readme-assets/ascii-readme.svg" width="480" alt="LIT ASCII B mark" /></p>
4
+ <p align="center"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.7/readme-assets/ascii-readme.svg" width="480" alt="LIT ASCII B mark" /></p>
5
5
 
6
6
  <details>
7
7
  <summary>Copy ASCII logo</summary>
@@ -31,19 +31,19 @@
31
31
 
32
32
  </details>
33
33
 
34
- <p align="center"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/readme-assets/lithermes-wordmark.svg" width="480" alt="LITHERMES display type" /></p>
35
- <p align="center"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/readme-assets/lithermes-clay-icon.png" width="160" alt="LitHermes clay mark" /></p>
34
+ <p align="center"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.7/readme-assets/lithermes-wordmark.svg" width="480" alt="LITHERMES display type" /></p>
35
+ <p align="center"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.7/readme-assets/lithermes-clay-icon.png" width="160" alt="LitHermes clay mark" /></p>
36
36
 
37
37
  <p align="center">
38
- <a href="#install"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/readme-assets/badge-version.svg" alt="1.0.5" /></a>
39
- <a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/readme-assets/badge-license.svg" alt="MIT license" /></a>
38
+ <a href="#install"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.7/readme-assets/badge-version.svg" alt="1.0.7" /></a>
39
+ <a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.7/readme-assets/badge-license.svg" alt="MIT license" /></a>
40
40
  </p>
41
41
 
42
42
  <p align="center">
43
- <a href="https://github.com/wjgoarxiv/lithermes/blob/main/docs/guide.md"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/readme-assets/lucide-book-open.svg" width="16" alt="" /> Docs</a> &nbsp;
43
+ <a href="https://github.com/wjgoarxiv/lithermes/blob/main/docs/guide.md"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.7/readme-assets/lucide-book-open.svg" width="16" alt="" /> Docs</a> &nbsp;
44
44
  <a href="#install">Install</a> &nbsp;
45
- <a href="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/readme-assets/ignition-film.mp4"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/readme-assets/lucide-play.svg" width="16" alt="" /> Ignition</a> &nbsp;
46
- <a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/readme-assets/lucide-shield-check.svg" width="16" alt="" /> MIT</a>
45
+ <a href="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.7/readme-assets/ignition-film.mp4"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.7/readme-assets/lucide-play.svg" width="16" alt="" /> Ignition</a> &nbsp;
46
+ <a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.7/readme-assets/lucide-shield-check.svg" width="16" alt="" /> MIT</a>
47
47
  </p>
48
48
 
49
49
  # LitHermes
@@ -52,11 +52,11 @@
52
52
 
53
53
  **What it is:** LitHermes connects planning, execution, review, and handoff inside **Hermes Agent**.
54
54
 
55
- [한국어](https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/README_Ko-KR.md) · [npm](https://www.npmjs.com/package/@litfamily/lithermes) · [GitHub](https://github.com/wjgoarxiv/lithermes)
55
+ [한국어](https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.7/README_Ko-KR.md) · [npm](https://www.npmjs.com/package/@litfamily/lithermes) · [GitHub](https://github.com/wjgoarxiv/lithermes)
56
56
 
57
57
  ## Install
58
58
 
59
- Source version: `@litfamily/lithermes@1.0.5`. Have Hermes Agent and Node.js 18+ installed, with write access to your Hermes home (normally `~/.hermes`).
59
+ Source version: `@litfamily/lithermes@1.0.7`. Have Hermes Agent and Node.js 18+ installed, with write access to your Hermes home (normally `~/.hermes`).
60
60
 
61
61
  The installer targets your Hermes home. For an isolated trial, set `HERMES_HOME` to a new empty directory before installation and use that same value when starting Hermes. Keep your existing home and settings. `--no-patch-installed-hermes` also prevents compatibility edits to the detected host installation outside that profile.
62
62
 
@@ -161,6 +161,7 @@ For Telegram gateway dispatch, use `/lit_loop` and `/lit_plan`. The complete com
161
161
  - Native `/goal` is user-managed and unobserved. Durable `goal_*` state is authoritative; there is no automatic update, clear, or resume of native `/goal`.
162
162
  - Hermes uses a global child model route. Per-task model overrides and named reviewer routes are unavailable. A configuration receipt does not prove an effective child execution.
163
163
  - The generated negative gate matrix uses an isolated HOME and `HERMES_HOME`; its receipt is safe to compare across concurrent sessions.
164
+ - Hermes resolves a bare skill name (not the `lithermes:<name>` explicit-load form) against Hermes `config.yaml`'s `skills.external_dirs` and local `<HERMES_HOME>/skills` before it reaches the LitHermes plugin. If one of those locations holds a same-named skill, `lithermes doctor` prints a `skill shadow check: WARNING` line naming the skill and the shadowing path; load the LitHermes copy explicitly as `lithermes:<name>`, or remove/rename the other copy. This warning never changes doctor's pass/fail exit status.
164
165
 
165
166
  ### Verify and uninstall
166
167
 
@@ -198,6 +199,4 @@ MIT License.
198
199
 
199
200
  ### LITFAMILY
200
201
 
201
- <p align="center"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/readme-assets/cover.webp" width="100%" alt="Five armored robots, one for each LitFamily product, beneath the words KEEP THE WORK LIT." /></p>
202
-
203
202
  Install each product separately in its supported agent harness.
package/README_Ko-KR.md CHANGED
@@ -1,7 +1,7 @@
1
- <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/readme-assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/readme-assets/cover-motion.webp" width="100%" alt="LitHermes 표지. 라임색 신호가 궤도를 지나 HERMES AGENT 워드마크로 이어집니다. 모션을 줄이도록 설정하면 이 모션 표지에서 추출한 정지 프레임을 표시합니다." /></picture></p>
2
- <p align="center"><a href="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/readme-assets/cover.webp">정지 패밀리 표지 보기</a></p>
1
+ <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.7/readme-assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.7/readme-assets/cover-motion.webp" width="100%" alt="LitHermes 표지. 라임색 신호가 궤도를 지나 HERMES AGENT 워드마크로 이어집니다. 모션을 줄이도록 설정하면 이 모션 표지에서 추출한 정지 프레임을 표시합니다." /></picture></p>
2
+ <p align="center"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.7/readme-assets/cover.webp" width="100%" alt="LitFamily 다섯 제품을 나타내는 장갑 로봇 패널과 KEEP THE WORK LIT. 문구" /></p>
3
3
 
4
- <p align="center"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/readme-assets/ascii-readme.svg" width="480" alt="LIT ASCII B 마크" /></p>
4
+ <p align="center"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.7/readme-assets/ascii-readme.svg" width="480" alt="LIT ASCII B 마크" /></p>
5
5
 
6
6
  <details>
7
7
  <summary>ASCII 로고 복사</summary>
@@ -31,19 +31,19 @@
31
31
 
32
32
  </details>
33
33
 
34
- <p align="center"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/readme-assets/lithermes-wordmark.svg" width="480" alt="LITHERMES 디스플레이 서체" /></p>
35
- <p align="center"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/readme-assets/lithermes-clay-icon.png" width="160" alt="LitHermes 클레이 마크" /></p>
34
+ <p align="center"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.7/readme-assets/lithermes-wordmark.svg" width="480" alt="LITHERMES 디스플레이 서체" /></p>
35
+ <p align="center"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.7/readme-assets/lithermes-clay-icon.png" width="160" alt="LitHermes 클레이 마크" /></p>
36
36
 
37
37
  <p align="center">
38
- <a href="#설치"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/readme-assets/badge-version.svg" alt="1.0.5" /></a>
39
- <a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/readme-assets/badge-license.svg" alt="MIT license" /></a>
38
+ <a href="#설치"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.7/readme-assets/badge-version.svg" alt="1.0.7" /></a>
39
+ <a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.7/readme-assets/badge-license.svg" alt="MIT license" /></a>
40
40
  </p>
41
41
 
42
42
  <p align="center">
43
- <a href="https://github.com/wjgoarxiv/lithermes/blob/main/docs/guide.ko.md"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/readme-assets/lucide-book-open.svg" width="16" alt="" /> 문서</a> &nbsp;
43
+ <a href="https://github.com/wjgoarxiv/lithermes/blob/main/docs/guide.ko.md"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.7/readme-assets/lucide-book-open.svg" width="16" alt="" /> 문서</a> &nbsp;
44
44
  <a href="#설치">설치</a> &nbsp;
45
- <a href="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/readme-assets/ignition-film.mp4"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/readme-assets/lucide-play.svg" width="16" alt="" /> Ignition</a> &nbsp;
46
- <a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/readme-assets/lucide-shield-check.svg" width="16" alt="" /> MIT</a>
45
+ <a href="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.7/readme-assets/ignition-film.mp4"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.7/readme-assets/lucide-play.svg" width="16" alt="" /> Ignition</a> &nbsp;
46
+ <a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.7/readme-assets/lucide-shield-check.svg" width="16" alt="" /> MIT</a>
47
47
  </p>
48
48
 
49
49
  # LitHermes
@@ -52,11 +52,11 @@
52
52
 
53
53
  **무엇인가요:** LitHermes는 **Hermes Agent**에서 계획, 실행, 검토, 인계를 이어갑니다.
54
54
 
55
- [English](https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/README.md) · [npm](https://www.npmjs.com/package/@litfamily/lithermes) · [GitHub](https://github.com/wjgoarxiv/lithermes)
55
+ [English](https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.7/README.md) · [npm](https://www.npmjs.com/package/@litfamily/lithermes) · [GitHub](https://github.com/wjgoarxiv/lithermes)
56
56
 
57
57
  ## 설치
58
58
 
59
- 소스 기준 버전은 `@litfamily/lithermes@1.0.5`입니다. Hermes Agent와 Node.js 18+가 필요하며, Hermes 홈(기본 `~/.hermes`)에 쓰기 권한이 있어야 합니다.
59
+ 소스 기준 버전은 `@litfamily/lithermes@1.0.7`입니다. Hermes Agent와 Node.js 18+가 필요하며, Hermes 홈(기본 `~/.hermes`)에 쓰기 권한이 있어야 합니다.
60
60
 
61
61
  설치기는 Hermes 홈을 대상으로 합니다. 별도 체험을 하려면 설치 전에 `HERMES_HOME`을 새 빈 디렉터리로 지정하고 Hermes를 시작할 때도 같은 값을 사용하세요. 기존 홈과 설정은 보관합니다. `--no-patch-installed-hermes`는 해당 프로필 밖에서 감지된 호스트 설치본에 호환성 패치를 적용하지 못하게 합니다.
62
62
 
@@ -161,6 +161,7 @@ Telegram gateway에서는 `/lit_loop`와 `/lit_plan` 별칭을 사용합니다.
161
161
  - native `/goal`은 사용자가 관리하며 plugin은 관찰하지 않습니다. durable `goal_*` 상태가 기준이며 native `/goal`을 자동으로 갱신·초기화·재개하지 않습니다.
162
162
  - Hermes 하위 모델은 공통 모델 경로를 사용합니다. 작업별 모델 재정의와 지정 reviewer 경로는 지원하지 않습니다. 설정 receipt가 실제 child 실행을 증명하지 않습니다.
163
163
  - 생성되는 negative gate matrix는 분리된 HOME과 `HERMES_HOME`을 사용하므로 동시 세션의 receipt를 비교할 수 있습니다.
164
+ - Hermes는 bare skill 이름(명시적 로드 형태인 `lithermes:<name>`이 아닌 경우)을 Hermes `config.yaml`의 `skills.external_dirs`와 로컬 `<HERMES_HOME>/skills`에서 먼저 찾은 뒤 LitHermes plugin으로 넘어갑니다. 이 위치 중 하나에 같은 이름의 skill이 있으면 `lithermes doctor`가 `skill shadow check: WARNING` 줄로 해당 skill과 가리는 경로를 알려줍니다. `lithermes:<name>`으로 명시적으로 불러오거나, 다른 위치의 사본을 제거·이름 변경하세요. 이 경고는 doctor의 성공/실패 종료 상태를 바꾸지 않습니다.
164
165
 
165
166
  ### 확인 및 삭제
166
167
 
@@ -198,6 +199,4 @@ MIT License.
198
199
 
199
200
  ### LITFAMILY
200
201
 
201
- <p align="center"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.5/readme-assets/cover.webp" width="100%" alt="LitFamily 다섯 제품을 나타내는 장갑 로봇 패널과 KEEP THE WORK LIT. 문구" /></p>
202
-
203
202
  각 제품은 지원하는 에이전트 환경에 따로 설치해 사용합니다.
@@ -106,8 +106,8 @@ _SKILL_ROUTE_CONTRACTS = {
106
106
  "implementation and rendered inspection. Record a compact direction and evolve the "
107
107
  "Design Contract with the work; do not stop at a contract or demand routine reapproval.",
108
108
  "Review-only and plan-only requests are read-only. Incidental UI words grant no write "
109
- "authority. Ask one material question if the target or direction conflicts; retain "
110
- "the answer on resume and continue once it resolves the conflict.",
109
+ "authority. Ask one material question per turn while target or direction conflicts or "
110
+ "named directions stay undecided (not defaults); retain answers, build once resolved.",
111
111
  "Every field you record must be one the shipped validator accepts. `design_contract_validation` "
112
112
  "answers `contract has unapproved keys [...]` and exits 1 on anything else; the reference "
113
113
  "documents name the real fields.",
@@ -84,6 +84,27 @@ _OPENAI_CATALOG_EFFORTS = {
84
84
  }
85
85
  _REVIEWER_ROUTE_NAME = "".join(chr(code) for code in (109, 111, 109, 117, 115))
86
86
  SUPPORTED_HERMES_HOST_VERSIONS = frozenset(("0.17.0", "0.19.0"))
87
+ _MINIMUM_HERMES_HOST_VERSION = (0, 17, 0)
88
+
89
+
90
+ def _classify_hermes_host_version(host_version: str) -> str:
91
+ if re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", host_version) is None:
92
+ return "unsupported"
93
+ version = tuple(int(part) for part in host_version.split("."))
94
+ if version < _MINIMUM_HERMES_HOST_VERSION:
95
+ return "unsupported"
96
+ if host_version in SUPPORTED_HERMES_HOST_VERSIONS:
97
+ return "verified"
98
+ return "beyond-verified"
99
+
100
+
101
+ def _beyond_verified_host_note(host_version: str) -> str | None:
102
+ if _classify_hermes_host_version(host_version) != "beyond-verified":
103
+ return None
104
+ return (
105
+ f"[NOTE] Hermes host {host_version} is beyond the verified matrix "
106
+ "(0.17.0, 0.19.0); diagnostics are read-only"
107
+ )
87
108
 
88
109
 
89
110
  @dataclass(frozen=True, slots=True)
@@ -94,6 +115,7 @@ class ModelCapabilities:
94
115
  child_effort: str
95
116
  concurrency: int
96
117
  async_batches: int
118
+ shared_async_cap: bool
97
119
  spawn_depth: int
98
120
  flat: bool
99
121
  approved: bool
@@ -203,7 +225,7 @@ def _cataloged_route(model: object, effort: object) -> bool:
203
225
 
204
226
 
205
227
  def _parse_capabilities(host_version: str) -> CapabilityResult:
206
- if host_version not in SUPPORTED_HERMES_HOST_VERSIONS:
228
+ if _classify_hermes_host_version(host_version) == "unsupported":
207
229
  return UnavailableCapabilities("unsupported Hermes host version")
208
230
  try:
209
231
  text = _host_config_path().read_text(encoding="utf-8")
@@ -221,7 +243,8 @@ def _parse_capabilities(host_version: str) -> CapabilityResult:
221
243
  return UnavailableCapabilities("malformed host config")
222
244
  if _has_credential_key(config):
223
245
  return UnavailableCapabilities("credential-risk host config")
224
- if config.get("_config_version") != 30:
246
+ schema_version = config.get("_config_version")
247
+ if type(schema_version) is not int or schema_version < 30:
225
248
  return UnavailableCapabilities("unknown host config schema")
226
249
  for key in _SAFE_CHILDREN:
227
250
  if key in config and not isinstance(config[key], dict):
@@ -256,7 +279,8 @@ def _parse_capabilities(host_version: str) -> CapabilityResult:
256
279
  child_effort = effort if child_effort_raw is None or child_effort_raw == "" else child_effort_raw
257
280
  child_provider = provider if child_provider_raw is None or child_provider_raw == "" else child_provider_raw
258
281
  concurrency_raw = delegation_config.get("max_concurrent_children", "")
259
- async_raw = delegation_config.get("max_async_children", 3)
282
+ shared_async_cap = schema_version >= 33
283
+ async_raw = 1 if shared_async_cap else delegation_config.get("max_async_children", 3)
260
284
  depth_raw = delegation_config.get("max_spawn_depth", 1)
261
285
  orchestrator_enabled = delegation_config.get("orchestrator_enabled", True)
262
286
  delegation_provider = delegation_config.get("provider") or provider
@@ -355,6 +379,7 @@ def _parse_capabilities(host_version: str) -> CapabilityResult:
355
379
  child_effort=child_effort,
356
380
  concurrency=concurrency,
357
381
  async_batches=async_batches,
382
+ shared_async_cap=shared_async_cap,
358
383
  spawn_depth=spawn_depth,
359
384
  flat=flat,
360
385
  approved=approved,
@@ -369,16 +394,22 @@ def _recursion_detail(spawn_depth: int, flat: bool) -> str:
369
394
  return f"hard (configured depth {spawn_depth}, flat via orchestrator kill switch)"
370
395
 
371
396
 
397
+ def _concurrency_detail(concurrency: int, async_batches: int, shared_async_cap: bool) -> str:
398
+ if shared_async_cap:
399
+ return f"per batch {concurrency}; background cap {concurrency}; potential children {concurrency}"
400
+ return f"per batch {concurrency}; async batches {async_batches}; potential children {concurrency * async_batches}"
401
+
402
+
372
403
  def _capability_status_lines(host_version: str) -> list[str]:
373
404
  result = _parse_capabilities(host_version)
374
405
  match result:
375
- case ModelCapabilities(parent_model=parent_model, parent_effort=parent_effort, child_model=child_model, child_effort=child_effort, concurrency=concurrency, async_batches=async_batches, spawn_depth=spawn_depth, flat=flat):
406
+ case ModelCapabilities(parent_model=parent_model, parent_effort=parent_effort, child_model=child_model, child_effort=child_effort, concurrency=concurrency, async_batches=async_batches, shared_async_cap=shared_async_cap, spawn_depth=spawn_depth, flat=flat):
376
407
  lines = [
377
408
  f"parent model: {parent_model}",
378
409
  f"parent effort: {parent_effort}",
379
410
  f"global child route: configured ({child_model}, effort {child_effort}, provider inherited)",
380
411
  "child execution proof: unavailable (delegate_task receipt required)",
381
- f"concurrency: hard (per batch {concurrency}; async batches {async_batches}; potential children {concurrency * async_batches})",
412
+ f"concurrency: hard ({_concurrency_detail(concurrency, async_batches, shared_async_cap)})",
382
413
  f"recursion: {_recursion_detail(spawn_depth, flat)}",
383
414
  "auto-compaction: unavailable (ratio-only compression; no exact 650K schema)",
384
415
  f"runtime: hard ({_OPENAI_PROVIDER} {_RESPONSES_MODE})",
@@ -408,7 +439,7 @@ def _capability_status_lines(host_version: str) -> list[str]:
408
439
  def status_report(host_version: str | None = None) -> str:
409
440
  version = host_version or hermes_host_version()
410
441
  skills = _skill_names()
411
- return "\n".join([
442
+ lines = [
412
443
  f"LitHermes plugin {plugin_version()} (Hermes host {version})",
413
444
  f"plugin dir: {PLUGIN_ROOT}",
414
445
  f"hooks ({len(HOOKS)}): {', '.join(HOOKS)}",
@@ -424,7 +455,11 @@ def status_report(host_version: str | None = None) -> str:
424
455
  f"litgoal evidence kinds: {', '.join(_evidence_kinds())}",
425
456
  f"scientific visualization: {_science.dependency_summary()}",
426
457
  *_capability_status_lines(version),
427
- ])
458
+ ]
459
+ host_note = _beyond_verified_host_note(version)
460
+ if host_note:
461
+ lines.insert(1, host_note)
462
+ return "\n".join(lines)
428
463
 
429
464
 
430
465
  def doctor_report(host_version: str | None = None) -> tuple[list[str], int]:
@@ -463,14 +498,17 @@ def doctor_report(host_version: str | None = None) -> tuple[list[str], int]:
463
498
  lines.append(f"[OK] Hermes host detected (v{version})")
464
499
  else:
465
500
  lines.append("[NOTE] Hermes host version unknown (running outside Hermes?)")
501
+ host_note = _beyond_verified_host_note(version)
502
+ if host_note:
503
+ lines.append(host_note)
466
504
  capabilities = _parse_capabilities(version)
467
505
  match capabilities:
468
- case ModelCapabilities(parent_model=parent_model, parent_effort=parent_effort, child_model=child_model, child_effort=child_effort, concurrency=concurrency, async_batches=async_batches, spawn_depth=spawn_depth, flat=flat):
506
+ case ModelCapabilities(parent_model=parent_model, parent_effort=parent_effort, child_model=child_model, child_effort=child_effort, concurrency=concurrency, async_batches=async_batches, shared_async_cap=shared_async_cap, spawn_depth=spawn_depth, flat=flat):
469
507
  lines.extend([
470
508
  f"[OK] parent model {parent_model} / effort {parent_effort}",
471
509
  f"[NOTE] global child route configured ({child_model}, effort {child_effort}, provider inherited)",
472
510
  "[NOTE] child execution proof unavailable (delegate_task receipt required)",
473
- f"[OK] concurrency hard (per batch {concurrency}; async batches {async_batches}; potential children {concurrency * async_batches})",
511
+ f"[OK] concurrency hard ({_concurrency_detail(concurrency, async_batches, shared_async_cap)})",
474
512
  f"[{'OK' if flat else 'WARN'}] recursion {_recursion_detail(spawn_depth, flat)}",
475
513
  "[OK] auto-compaction unavailable (ratio-only compression; no exact 650K schema)",
476
514
  f"[OK] runtime hard ({_OPENAI_PROVIDER} {_RESPONSES_MODE})",
@@ -1,7 +1,7 @@
1
1
  {
2
- "syncedAt": "2026-09-23T12:56:08.744Z",
2
+ "syncedAt": "2026-09-24T15:44:37.289Z",
3
3
  "source": "bundled-payload",
4
- "sourceHash": "e7ac8bd0623eb0ef78d7d97e76daa0bb3d5cd7aab909da0f0e2f3af19f08964b",
4
+ "sourceHash": "ab8de1105823a7d9d640d47cb0e65d2818dbb1080e5b6dc68898e6db21f5d831",
5
5
  "files": [
6
6
  {
7
7
  "path": "NOTICE.md",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  {
39
39
  "path": "core_contexts.py",
40
- "sha256": "0bb379b099bfedb9ce51141cae0f7c11a827fe35487249a9ca7fe6dfd345b257"
40
+ "sha256": "a60c32bd38a47ecac6bffc39a8549d2ea1b3b87733d666fbb1be4b5685f2fbbb"
41
41
  },
42
42
  {
43
43
  "path": "core_contract.py",
@@ -85,7 +85,7 @@
85
85
  },
86
86
  {
87
87
  "path": "diagnostics.py",
88
- "sha256": "8d8196c1c50a6dfa44a68f8681a22b3c84428776bc7b0af1847d80d30346f0d0"
88
+ "sha256": "f81d995ed89fda91716db19aaed536c45b961211d41039186637b5f454d7701c"
89
89
  },
90
90
  {
91
91
  "path": "handoff.py",
@@ -157,7 +157,7 @@
157
157
  },
158
158
  {
159
159
  "path": "plugin.yaml",
160
- "sha256": "6a3baf875b758fdf6d7973d5f9ef1a5727702452cd4b4a6f4a8268f130b32341"
160
+ "sha256": "7a95bad1937cb8d455e3c89d5d4ee300ea42e0fb1320357f024ff9cdbbb7aca3"
161
161
  },
162
162
  {
163
163
  "path": "provider_metrics.py",
@@ -1245,7 +1245,7 @@
1245
1245
  },
1246
1246
  {
1247
1247
  "path": "skills/frontend-ui-ux/references/complete-contract.md",
1248
- "sha256": "7aff009df5222d5cfbcdcc505f651104a7e8145c3b8477246c098a9f9b295f6f"
1248
+ "sha256": "8d753d41b63d666bd1cdd64f8f580faa8bab5bae52ddab5c32c9c710e63f0f59"
1249
1249
  },
1250
1250
  {
1251
1251
  "path": "skills/frontend-ui-ux/references/composition.md",
@@ -1289,7 +1289,7 @@
1289
1289
  },
1290
1290
  {
1291
1291
  "path": "skills/frontend-ui-ux/references/production.md",
1292
- "sha256": "af5d4483340c2410572488c3c6239b0624e83fb24eabda0cc49398d091e85312"
1292
+ "sha256": "f859bbca4a70621c4440feb4b288d6225e6ecdd206ef4e32baeea199526df2a3"
1293
1293
  },
1294
1294
  {
1295
1295
  "path": "skills/frontend-ui-ux/references/redesign-playbook.md",
@@ -1857,7 +1857,7 @@
1857
1857
  },
1858
1858
  {
1859
1859
  "path": "skills/readme-studio/SKILL.md",
1860
- "sha256": "40b9ed081c86db22386fd586903b6457248cbc1185b3a283c007f6bac0e98be4"
1860
+ "sha256": "010f567e56dbfb73da49acbaa2762737a7351cd0856123e86ac49b5b8d2b3533"
1861
1861
  },
1862
1862
  {
1863
1863
  "path": "skills/readme-studio/references/decoration-patterns.md",
@@ -1873,7 +1873,7 @@
1873
1873
  },
1874
1874
  {
1875
1875
  "path": "skills/readme-studio/references/production-interview.md",
1876
- "sha256": "be6169bff980aef9813e86e6670763d92166efbdbaaa9fa1cc98b733f00224e4"
1876
+ "sha256": "7ec8fcd79e3d55e8d17642696793f557717db0453ba7f0008eb3e80cc6e471a2"
1877
1877
  },
1878
1878
  {
1879
1879
  "path": "skills/readme-studio/references/type-and-images.md",
@@ -1969,7 +1969,7 @@
1969
1969
  },
1970
1970
  {
1971
1971
  "path": "skills/visual-qa/references/complete-contract.md",
1972
- "sha256": "c3008c41017bc548982cc424a3ec90bd338c2d5c2d49bebb5385a640531aabff"
1972
+ "sha256": "b35b71a8751a90ac8ce40251b8a2d18860068db96e7bd4eedda88fd7b548b362"
1973
1973
  },
1974
1974
  {
1975
1975
  "path": "skills/visual-qa/schemas/evidence-manifest-v1alpha1.schema.json",
@@ -1,5 +1,5 @@
1
1
  name: lithermes
2
- version: 1.0.5
2
+ version: 1.0.7
3
3
  description: "Hermes-native workflow toolkit: litgoal durable runtime, 5-lane review orchestrator, Litwork commands, skills, and prompt steering."
4
4
  author: "LitHermes contributors"
5
5
  kind: standalone
@@ -431,7 +431,7 @@ isolated Hermes home. Use `--hermes-home`; never `--home`.
431
431
 
432
432
  ```text
433
433
  $ node packages/lithermes-installer/bin/lithermes.js install --yes --offline --no-hud --hermes-home "$ISOLATED"
434
- Installed LitHermes 1.0.5
434
+ Installed LitHermes 1.0.7
435
435
  plugin: $ISOLATED/plugins/lithermes
436
436
  model config: updated
437
437
 
@@ -448,7 +448,7 @@ installed payload: PASS
448
448
  enabled config: PASS
449
449
 
450
450
  $ hermes lithermes doctor
451
- [OK] plugin.yaml readable (version 1.0.5)
451
+ [OK] plugin.yaml readable (version 1.0.7)
452
452
  [OK] skills bundled: 32
453
453
  [OK] litgoal durable runtime importable
454
454
  ```
@@ -4,7 +4,7 @@ Hermes loads this document through the selected frontend-ui-ux skill. Read the r
4
4
 
5
5
  Before editing, identify the actual page, stack, user task, preserved brand elements and actions/states. Keep this direction note small enough to revise. The evidence-capable v1beta2 contract remains the final inventory for verification, and evolves during the build. Older references that call this a freeze describe a versioned review snapshot, not an extra approval gate or a requirement to finish JSON before writing UI. Review/plan-only intent remains read-only even if other guidance says implement.
6
6
 
7
- There is no mandatory interview. Ask only when two plausible answers would materially change scope, architecture, permissions, accessibility, or visual direction. Ask one high-impact question at a time, wait for its answer, then resolve the next material uncertainty in a later turn; continue for as many rounds as materially necessary. Retain earlier answers across rounds in the conversation and task-local direction note, reread them on resume, and do not re-ask a resolved question. When the brief already bounds the work, state the default and proceed without pausing for an interview. Review, plan, and keyword-only requests remain read-only. A cancellation suspends new writes and captures partial status; resumption reuses confirmed assets and source.
7
+ There is no mandatory interview. Ask only when two plausible answers would materially change scope, architecture, permissions, accessibility, or visual direction. Ask one high-impact question at a time, wait for its answer, then resolve the next material uncertainty in a later turn; continue for as many rounds as materially necessary. Retain earlier answers across rounds in the conversation and task-local direction note, reread them on resume, and do not re-ask a resolved question. If a brief lists competing visual directions and leaves them undecided, that choice is still open: calling each option "acceptable" is not a choice or a delegation. Ask it in a separate round after higher-impact questions are answered; you may recommend one option in the question, but do not declare a default for it or start building until the user chooses. A brief that gives no visual direction at all is different: use the authored default direction without asking. Defaults cover only what the brief settles, what it explicitly hands to you, and details that do not materially change the result. When the brief already bounds the work, state the default and proceed without pausing for an interview. Review, plan, and keyword-only requests remain read-only. A cancellation suspends new writes and captures partial status; resumption reuses confirmed assets and source.
8
8
 
9
9
  When the brief does not specify another direction, use the authored tokens in `references/default-editorial-pixel.json` and make an original, task-specific pixel illustration the primary visual anchor. Render each cell at least 6 CSS px and preserve crisp integer-grid edges with nearest-neighbor scaling; do not smooth or blur the illustration. Build the surrounding editorial hierarchy with generous spacing, compact display lines, a terminal accent and deliberate asymmetry. A rounded abstract symbol or a generic gradient cannot stand in for the task-specific illustration. Keep pixel art decorative when the task is conveyed in real text; add meaningful alternative text when it conveys unique content. Never borrow a reference site's artwork, proprietary font or copy.
10
10
 
@@ -23,7 +23,7 @@ Use the authorized repository, applicable instructions, current README, real pac
23
23
  | Mode | Trigger | Contract |
24
24
  | --- | --- | --- |
25
25
  | production | Create or improve the README/cover | Inspect facts, state defaults for a bounded brief, create scoped files and inspect the resulting composition |
26
- | question | Two plausible choices materially change scope, architecture, permissions, accessibility or visual direction | Apply `references/production-interview.md`; ask one high-impact question at a time and retain answers across rounds |
26
+ | question | Two plausible choices materially change scope, architecture, permissions, accessibility or visual direction | Apply `references/production-interview.md`; ask one high-impact question at a time and retain answers across rounds; open competing directions: ask before choosing |
27
27
  | review/plan | Review-only or plan-only | Read-only findings or plan; no asset generation or file edits |
28
28
  | partial | A required tool, font, source or permission is absent | Name the affected deliverable and prerequisite; continue independent work |
29
29
 
@@ -6,6 +6,8 @@ There is no mandatory interview. Ask only when two plausible answers would mater
6
6
 
7
7
  When the brief already bounds the work, state the default and proceed. Use repository evidence and established identity for defaults; state assumptions that materially affect the result. Do not pause for a ceremonial approval of a bounded brief.
8
8
 
9
+ If a brief lists competing visual directions and leaves them undecided, that choice is still open: calling each option "acceptable" is not a choice or a delegation. Ask it in a separate round after higher-impact questions are answered; you may recommend one option in the question, but do not declare a default for it or start building until the user chooses. A brief that gives no visual direction at all is different: follow established identity or the default cover direction without asking. Defaults cover only what the brief settles, what it explicitly hands to you, and details that do not materially change the result.
10
+
9
11
  ## How to ask
10
12
 
11
13
  Ask one high-impact question at a time, in one turn. Present the distinct alternatives that change the deliverable and name their practical effect. Do not combine unrelated decisions into a questionnaire or ask the user to repeat the brief. Wait for the answer before asking about another unresolved material choice. Continue for as many rounds as materially necessary, and stop once remaining choices can safely follow stated defaults.
@@ -485,7 +485,7 @@ isolated Hermes home. Use `--hermes-home`; never `--home`.
485
485
 
486
486
  ```text
487
487
  $ node packages/lithermes-installer/bin/lithermes.js install --yes --offline --no-hud --hermes-home "$ISOLATED"
488
- Installed LitHermes 1.0.5
488
+ Installed LitHermes 1.0.7
489
489
  plugin: $ISOLATED/plugins/lithermes
490
490
  model config: updated
491
491
 
@@ -502,7 +502,7 @@ installed payload: PASS
502
502
  enabled config: PASS
503
503
 
504
504
  $ hermes lithermes doctor
505
- [OK] plugin.yaml readable (version 1.0.5)
505
+ [OK] plugin.yaml readable (version 1.0.7)
506
506
  [OK] skills bundled: 32
507
507
  [OK] litgoal durable runtime importable
508
508
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@litfamily/lithermes",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "npx/bunx installer for the LitHermes Hermes plugin",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1 +1 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.5"><title>release: 1.0.5</title><g shape-rendering="crispEdges"><rect width="49" height="20" fill="#080d14"/><rect x="49" width="97" height="20" fill="#ff6337"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="255" y="140" textLength="390" transform="scale(.1)">release</text><text x="965" y="140" textLength="870" transform="scale(.1)" fill="#080D14">1.0.5</text></g></svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.7"><title>release: 1.0.7</title><g shape-rendering="crispEdges"><rect width="49" height="20" fill="#080d14"/><rect x="49" width="97" height="20" fill="#ff6337"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="255" y="140" textLength="390" transform="scale(.1)">release</text><text x="965" y="140" textLength="870" transform="scale(.1)" fill="#080D14">1.0.7</text></g></svg>
package/src/lib/check.js CHANGED
@@ -1,8 +1,8 @@
1
1
  const fs = require("node:fs");
2
2
  const path = require("node:path");
3
3
  const { spawnSync } = require("node:child_process");
4
- const { configHasLitHermes, inspectHermesCapabilities, readConfig } = require("./config");
5
- const { detectHermesRuntimeRepo, detectHermesVersion, ensureHermesHome, inspectHermesHostCapabilities, LitHermesError } = require("./hermesDiscovery");
4
+ const { configHasLitHermes, formatConcurrencyCapability, inspectHermesCapabilities, readConfig, readExternalSkillDirsConfig } = require("./config");
5
+ const { detectHermesRuntimeRepo, detectHermesVersion, ensureHermesHome, expandHome, inspectHermesHostCapabilities, LitHermesError } = require("./hermesDiscovery");
6
6
  const { formatManagedRoute } = require("./modelRoutePolicy");
7
7
  const { inspectPythonCache } = require("./files");
8
8
  const { assetRoot, pluginDest } = require("./install");
@@ -112,6 +112,75 @@ function loadedStatusMatchesOrigin(output, expectedPluginPath) {
112
112
  }
113
113
  }
114
114
 
115
+ function isRegularSkillFile(file) {
116
+ try {
117
+ return fs.lstatSync(file).isFile();
118
+ } catch {
119
+ return false;
120
+ }
121
+ }
122
+
123
+ // Bounded, non-symlink-following scan for `<HERMES_HOME>/skills/**/<id>/SKILL.md`.
124
+ // Real local-skill trees are shallow, so a small depth/entry cap keeps this cheap
125
+ // while still catching a collision a couple of category folders deep.
126
+ function collectLocalSkillShadows(hermesHome, skillIds, { maxDepth = 3, maxEntries = 2000 } = {}) {
127
+ const remaining = new Set(skillIds);
128
+ const found = new Map();
129
+ let visited = 0;
130
+ function walk(dir, depth) {
131
+ if (!remaining.size || depth > maxDepth || visited >= maxEntries) return;
132
+ let entries;
133
+ try {
134
+ entries = fs.readdirSync(dir, { withFileTypes: true });
135
+ } catch {
136
+ return;
137
+ }
138
+ for (const entry of entries) {
139
+ if (!remaining.size || visited >= maxEntries) return;
140
+ visited += 1;
141
+ if (!entry.isDirectory()) continue;
142
+ const full = path.join(dir, entry.name);
143
+ if (remaining.has(entry.name)) {
144
+ const skillFile = path.join(full, "SKILL.md");
145
+ if (isRegularSkillFile(skillFile)) {
146
+ found.set(entry.name, skillFile);
147
+ remaining.delete(entry.name);
148
+ continue;
149
+ }
150
+ }
151
+ walk(full, depth + 1);
152
+ }
153
+ }
154
+ walk(path.join(hermesHome, "skills"), 0);
155
+ return found;
156
+ }
157
+
158
+ // Hermes resolves a bare skill name (not the `lithermes:<name>` explicit-load form)
159
+ // against `skills.external_dirs` and local `<HERMES_HOME>/skills`; a same-named entry
160
+ // there is picked instead of the LitHermes plugin copy. This is advisory: it never
161
+ // changes doctor's pass/fail status, only surfaces the collision.
162
+ function detectSkillShadows({ configText, hermesHome, skillIds = requiredSkills }) {
163
+ const externalDirs = readExternalSkillDirsConfig(configText)
164
+ .map((dir) => path.resolve(expandHome(dir)));
165
+ const localShadows = collectLocalSkillShadows(hermesHome, skillIds);
166
+ const warnings = [];
167
+ for (const id of skillIds) {
168
+ const externalDir = externalDirs.find((dir) => isRegularSkillFile(path.join(dir, id, "SKILL.md")));
169
+ if (externalDir) {
170
+ warnings.push({ id, path: path.join(externalDir, id, "SKILL.md"), source: "skills.external_dirs" });
171
+ continue;
172
+ }
173
+ const localPath = localShadows.get(id);
174
+ if (localPath) warnings.push({ id, path: localPath, source: "local skills" });
175
+ }
176
+ return warnings;
177
+ }
178
+
179
+ function formatSkillShadowWarning(shadow) {
180
+ return `WARNING: skill "${shadow.id}" bare name resolves to ${shadow.path} (${shadow.source}); `
181
+ + `use lithermes:${shadow.id} to load LitHermes's copy, or remove/rename the other copy.`;
182
+ }
183
+
115
184
  function checkLitHermes(flags = {}) {
116
185
  const { hermesHome, hermesRepo } = ensureHermesHome(flags);
117
186
  const pluginPath = pluginDest(hermesHome);
@@ -187,6 +256,7 @@ function doctorLitHermes(flags = {}) {
187
256
  const unsafeRecursion = recursion.status === "unavailable" && recursion.reason.startsWith("nested delegation");
188
257
  const unsafeModelRoute = modelSafety.status === "blocked";
189
258
  const runtime = capabilities.runtime;
259
+ const skillShadows = detectSkillShadows({ configText, hermesHome });
190
260
  const lines = [
191
261
  `plugin discovery: ${installedPass && configOk ? "PASS" : "PATCH_AVAILABLE"}`,
192
262
  `bundled source: ${bundledPass ? "PASS" : "FAIL"}`,
@@ -198,8 +268,10 @@ function doctorLitHermes(flags = {}) {
198
268
  `installed payload: ${installedPass ? "PASS" : "FAIL"}`,
199
269
  `enabled config: ${configOk ? "PASS" : "PARTIAL"}`,
200
270
  `loaded plugin: ${loaded.status} (${loaded.reason})`,
271
+ `skill shadow check: ${skillShadows.length ? "WARNING" : "PASS"}`,
272
+ ...skillShadows.map(formatSkillShadowWarning),
201
273
  `auto-compaction: ${auto.status}${auto.reason ? ` (${auto.reason})` : ""}`,
202
- `concurrency: ${concurrency.status}${concurrency.status === "hard" ? ` (per batch ${concurrency.limit}; async batches ${concurrency.asyncBatchLimit}; potential children ${concurrency.potentialChildCeiling})` : ` (${concurrency.reason})`}`,
274
+ `concurrency: ${formatConcurrencyCapability(concurrency)}`,
203
275
  `recursion: ${recursion.status}${recursion.status === "hard" ? recursion.flatBy === "depth" ? ` (depth ${recursion.limit}, flat)` : ` (configured depth ${recursion.limit}, flat via orchestrator kill switch)` : ` (${recursion.reason})`}`,
204
276
  `model route safety: ${modelSafety.status}${modelSafety.code ? ` (${modelSafety.code}; ${modelSafety.reason})` : ` (${modelSafety.reason})`}`,
205
277
  `runtime: ${runtime.status}${runtime.status === "hard" ? ` (${runtime.provider} ${runtime.apiMode})` : ` (${runtime.reason})`}`,
@@ -222,6 +294,7 @@ module.exports = {
222
294
  checkGatewaySource,
223
295
  checkLitHermes,
224
296
  checkTuiSource,
297
+ detectSkillShadows,
225
298
  doctorLitHermes,
226
299
  enabledUserJsonIncludesPlugin,
227
300
  loadedStatusMatchesOrigin,
package/src/lib/config.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const fs = require("node:fs");
2
2
  const path = require("node:path");
3
+ const { parseDocument } = require("yaml");
3
4
  const { writeFileAtomic } = require("./files");
4
5
  const modelConfig = require("./modelConfig");
5
6
 
@@ -205,6 +206,27 @@ function clearOutputStyleConfig(text) {
205
206
  return `${updated.replace(/\n*$/, "")}\n`;
206
207
  }
207
208
 
209
+ // Hermes `skills.external_dirs` (config.yaml key, default []): bare-name skill
210
+ // resolution searches these directories, so a same-named directory there can shadow
211
+ // a LitHermes plugin skill (which is only reachable unambiguously as `lithermes:<name>`).
212
+ // Malformed YAML fails closed to an empty list rather than throwing during doctor.
213
+ function readExternalSkillDirsConfig(text) {
214
+ const str = String(text || "");
215
+ if (!str.trim()) return [];
216
+ try {
217
+ const document = parseDocument(str, { strict: true, uniqueKeys: true });
218
+ if (document.errors.length) return [];
219
+ const node = document.getIn(["skills", "external_dirs"], true);
220
+ const value = node && typeof node.toJS === "function" ? node.toJS(document) : node;
221
+ if (!Array.isArray(value)) return [];
222
+ return value
223
+ .filter((entry) => typeof entry === "string" && entry.trim())
224
+ .map((entry) => entry.trim());
225
+ } catch {
226
+ return [];
227
+ }
228
+ }
229
+
208
230
  function writeConfig(hermesHome, text) {
209
231
  fs.mkdirSync(hermesHome, { recursive: true });
210
232
  writeFileAtomic(configPath(hermesHome), text, "utf8");
@@ -220,6 +242,7 @@ module.exports = {
220
242
  enableLitHermesConfig,
221
243
  readConfig,
222
244
  readDisplaySkinConfig,
245
+ readExternalSkillDirsConfig,
223
246
  readOutputStyleConfig,
224
247
  readTuiAgentsNudgeConfig,
225
248
  setDisplaySkinConfig,
@@ -5,6 +5,7 @@ const {
5
5
  configHasLitHermes,
6
6
  disableLitHermesConfig,
7
7
  enableLitHermesConfig,
8
+ formatConcurrencyCapability,
8
9
  inspectHermesCapabilities,
9
10
  planModelConfig,
10
11
  readConfig,
@@ -245,9 +246,7 @@ function capabilityLines(plan, { dryRun = false } = {}) {
245
246
  const auto = autoCompaction.status === "hard"
246
247
  ? `hard (${autoCompaction.limit} tokens)`
247
248
  : `unavailable (${autoCompaction.reason.replace(/^Hermes 0\.17\.0 exposes /, "")})`;
248
- const sync = concurrency.status === "hard"
249
- ? `hard (per batch ${concurrency.limit}; async batches ${concurrency.asyncBatchLimit}; potential children ${concurrency.potentialChildCeiling})`
250
- : `${concurrency.status} (${concurrency.reason})`;
249
+ const sync = formatConcurrencyCapability(concurrency);
251
250
  const nesting = recursion.status === "hard"
252
251
  ? recursion.flatBy === "depth"
253
252
  ? `hard (depth ${recursion.limit}, flat)`
@@ -1,7 +1,7 @@
1
1
  const fs = require("node:fs");
2
2
  const path = require("node:path");
3
3
  const { writeFileAtomic } = require("./files");
4
- const { OPENAI_PROVIDER, RESPONSES_MODE, classifyHostVersion,
4
+ const { OPENAI_PROVIDER, RESPONSES_MODE, VERIFIED_CONFIG_VERSION, classifyHostVersion,
5
5
  exactHostVersion, parseBoundary, sourceHash } = require("./modelConfigBoundary");
6
6
  const {
7
7
  ASTRA_MODEL,
@@ -138,6 +138,14 @@ function unavailableCapabilities(reason) {
138
138
  };
139
139
  }
140
140
 
141
+ function formatConcurrencyCapability(concurrency) {
142
+ if (concurrency.status !== "hard") return `${concurrency.status} (${concurrency.reason})`;
143
+ if (concurrency.sharedAsyncCap) {
144
+ return `hard (per batch ${concurrency.limit}; background cap ${concurrency.backgroundCap}; potential children ${concurrency.potentialChildCeiling})`;
145
+ }
146
+ return `hard (per batch ${concurrency.limit}; async batches ${concurrency.asyncBatchLimit}; potential children ${concurrency.potentialChildCeiling})`;
147
+ }
148
+
141
149
  function safeOriginalDispatchId(dispatch) {
142
150
  if (typeof dispatch !== "string" || !/^[A-Za-z0-9._:/-]{1,128}$/.test(dispatch)) return null;
143
151
  return dispatch;
@@ -199,9 +207,11 @@ function inspectHermesCapabilities(text, options = {}) {
199
207
  }
200
208
  const concurrency = boundary.document.getIn(["delegation", "max_concurrent_children"]);
201
209
  const asyncBatches = boundary.document.getIn(["delegation", "max_async_children"]);
210
+ const configVersion = boundary.document.get("_config_version");
211
+ const sharedAsyncCap = configVersion >= 33;
202
212
  const spawnDepth = boundary.document.getIn(["delegation", "max_spawn_depth"]);
203
213
  const orchestratorEnabled = boundary.document.getIn(["delegation", "orchestrator_enabled"]);
204
- const effectiveAsyncBatches = Number.isInteger(asyncBatches) ? asyncBatches : 3;
214
+ const effectiveAsyncBatches = sharedAsyncCap ? 1 : Number.isInteger(asyncBatches) ? asyncBatches : 3;
205
215
  const effectiveSpawnDepth = Number.isInteger(spawnDepth) ? spawnDepth : 1;
206
216
  const flatDelegation = effectiveSpawnDepth === 1 || orchestratorEnabled === false;
207
217
  const contextLength = boundary.document.getIn(["model", "context_length"]);
@@ -234,12 +244,15 @@ function inspectHermesCapabilities(text, options = {}) {
234
244
  concurrency: concurrency === 20
235
245
  ? {
236
246
  status: "hard",
237
- reason: "effective synchronous concurrency is configured to 20",
247
+ reason: sharedAsyncCap
248
+ ? "effective concurrency and background work share the configured cap of 20"
249
+ : "effective synchronous concurrency is configured to 20",
238
250
  observedSource: "delegation.max_concurrent_children",
239
251
  limit: 20,
240
252
  scope: "per-batch",
241
- asyncBatchLimit: effectiveAsyncBatches,
242
- potentialChildCeiling: 20 * effectiveAsyncBatches,
253
+ ...(sharedAsyncCap
254
+ ? { sharedAsyncCap: true, backgroundCap: 20, potentialChildCeiling: 20 }
255
+ : { asyncBatchLimit: effectiveAsyncBatches, potentialChildCeiling: 20 * effectiveAsyncBatches }),
243
256
  }
244
257
  : {
245
258
  status: "unavailable",
@@ -379,15 +392,22 @@ function planModelConfig(text, options = {}) {
379
392
  document.setIn(["delegation", "max_spawn_depth"], 1);
380
393
  document.setIn(["delegation", "orchestrator_enabled"], false);
381
394
  const plannedText = document.toString({ lineWidth: 0 });
395
+ const beyondVerifiedReasons = [];
396
+ if (classifyHostVersion(options.hostVersion).status === "beyond-verified") {
397
+ beyondVerifiedReasons.push(`Hermes ${request.hostVersion} is beyond the verified matrix; source and runtime markers verified, model schema written`);
398
+ }
399
+ const configVersion = document.get("_config_version");
400
+ if (configVersion > VERIFIED_CONFIG_VERSION) {
401
+ beyondVerifiedReasons.push(`config schema ${configVersion} is beyond the verified ceiling ${VERIFIED_CONFIG_VERSION}`);
402
+ }
382
403
  return {
383
404
  action: "write",
384
405
  backupAllowed: true,
385
406
  capabilities: inspectHermesCapabilities(plannedText, { ...options, ...route }),
386
407
  classification,
387
- reason:
388
- classifyHostVersion(options.hostVersion).status === "beyond-verified"
389
- ? `Hermes ${request.hostVersion} is beyond the verified matrix; source and runtime markers verified, model schema written`
390
- : `verified Hermes ${request.hostVersion} model schema`,
408
+ reason: beyondVerifiedReasons.length
409
+ ? beyondVerifiedReasons.join("; ")
410
+ : `verified Hermes ${request.hostVersion} model schema`,
391
411
  request,
392
412
  sourceHash: sourceHash(String(text || "")),
393
413
  text: plannedText,
@@ -429,6 +449,7 @@ function applyModelConfigPlan(hermesHome, plan) {
429
449
  module.exports = {
430
450
  applyModelConfigPlan,
431
451
  classifyModelConfig,
452
+ formatConcurrencyCapability,
432
453
  inspectHermesCapabilities,
433
454
  planModelConfig,
434
455
  revalidateModelConfigPlan,
@@ -17,6 +17,7 @@ const MINIMUM_HOST_VERSION = "0.17.0";
17
17
  // malformed, which must still be reported as unsupported.
18
18
  const NOT_PROBED_HOST_VERSION = "__lithermes_dry_run_not_probed__";
19
19
  const CONFIG_VERSION = 30;
20
+ const VERIFIED_CONFIG_VERSION = 45;
20
21
  const CREDENTIAL_KEY = /(^|[_-])(api[_-]?key|client[_-]?secret|auth[_-]?token|(?:aws[_-]?)?access[_-]?key(?:[_-]?id)?|(?:ssh[_-]?)?private[_-]?key|token|password|passphrase|secret|credential|authorization)([_-]|$)/i;
21
22
  const MAP_PATHS = ["model", "agent", "delegation", "compression"];
22
23
  const SCALAR_PATHS = [
@@ -165,8 +166,9 @@ function parseBoundary(text, options = {}) {
165
166
  return { error: "malformed host config" };
166
167
  }
167
168
  const credentialKey = findCredentialRiskKey(document);
168
- if (document.get("_config_version") !== CONFIG_VERSION) {
169
- return { error: `unknown host config schema; expected version ${CONFIG_VERSION}` };
169
+ const configVersion = document.get("_config_version");
170
+ if (!Number.isInteger(configVersion) || configVersion < CONFIG_VERSION) {
171
+ return { error: `unknown host config schema; expected integer version >= ${CONFIG_VERSION}` };
170
172
  }
171
173
  for (const key of MAP_PATHS) {
172
174
  const node = document.get(key, true);
@@ -208,6 +210,7 @@ module.exports = {
208
210
  NOT_PROBED_HOST_VERSION,
209
211
  OPENAI_PROVIDER,
210
212
  RESPONSES_MODE,
213
+ VERIFIED_CONFIG_VERSION,
211
214
  classifyHostVersion,
212
215
  exactHostVersion,
213
216
  parseBoundary,