@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 +13 -14
- package/README_Ko-KR.md +13 -14
- package/assets/lithermes-plugin/core_contexts.py +2 -2
- package/assets/lithermes-plugin/diagnostics.py +47 -9
- package/assets/lithermes-plugin/payload-version.json +10 -10
- package/assets/lithermes-plugin/plugin.yaml +1 -1
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/complete-contract.md +2 -2
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/production.md +1 -1
- package/assets/lithermes-plugin/skills/readme-studio/SKILL.md +1 -1
- package/assets/lithermes-plugin/skills/readme-studio/references/production-interview.md +2 -0
- package/assets/lithermes-plugin/skills/visual-qa/references/complete-contract.md +2 -2
- package/package.json +1 -1
- package/readme-assets/badge-version.svg +1 -1
- package/src/lib/check.js +76 -3
- package/src/lib/config.js +23 -0
- package/src/lib/install.js +2 -3
- package/src/lib/modelConfig.js +30 -9
- package/src/lib/modelConfigBoundary.js +5 -2
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.
|
|
2
|
-
<p align="center"><
|
|
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.
|
|
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.
|
|
35
|
-
<p align="center"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
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.
|
|
39
|
-
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
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.
|
|
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>
|
|
44
44
|
<a href="#install">Install</a>
|
|
45
|
-
<a href="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
46
|
-
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
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>
|
|
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.
|
|
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.
|
|
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.
|
|
2
|
-
<p align="center"><
|
|
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.
|
|
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.
|
|
35
|
-
<p align="center"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
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.
|
|
39
|
-
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
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.
|
|
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>
|
|
44
44
|
<a href="#설치">설치</a>
|
|
45
|
-
<a href="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
46
|
-
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
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>
|
|
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.
|
|
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.
|
|
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
|
|
110
|
-
"
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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-
|
|
2
|
+
"syncedAt": "2026-09-24T15:44:37.289Z",
|
|
3
3
|
"source": "bundled-payload",
|
|
4
|
-
"sourceHash": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
1972
|
+
"sha256": "b35b71a8751a90ac8ce40251b8a2d18860068db96e7bd4eedda88fd7b548b362"
|
|
1973
1973
|
},
|
|
1974
1974
|
{
|
|
1975
1975
|
"path": "skills/visual-qa/schemas/evidence-manifest-v1alpha1.schema.json",
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.
|
|
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: ${
|
|
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,
|
package/src/lib/install.js
CHANGED
|
@@ -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
|
|
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)`
|
package/src/lib/modelConfig.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
242
|
-
|
|
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
|
-
|
|
389
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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,
|