@litfamily/lithermes 1.0.0 → 1.0.1
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 +2 -2
- package/README_Ko-KR.md +2 -2
- package/assets/lithermes-plugin/__init__.py +135 -15
- package/assets/lithermes-plugin/diagnostics.py +1 -0
- package/assets/lithermes-plugin/payload-version.json +7 -7
- package/assets/lithermes-plugin/plugin.yaml +2 -1
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/complete-contract.md +2 -2
- 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 +37 -4
- package/src/lib/hermesDiscovery.js +29 -8
- package/src/lib/install.js +9 -2
- package/src/lib/modelConfigBoundary.js +9 -0
- package/src/lib/patch.js +32 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<p align="center"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
1
|
+
<p align="center"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.1/readme-assets/cover.webp" width="100%" alt="LitHermes v5 armory cover" /></p>
|
|
2
2
|
|
|
3
3
|
```
|
|
4
4
|
▄▄▄▄
|
|
@@ -41,7 +41,7 @@ LitHermes connects planning, execution, review, and handoff inside **Hermes Agen
|
|
|
41
41
|
|
|
42
42
|
[한국어](./README_Ko-KR.md) · [npm](https://www.npmjs.com/package/@litfamily/lithermes) · [GitHub](https://github.com/wjgoarxiv/lithermes)
|
|
43
43
|
|
|
44
|
-
Source version: `@litfamily/lithermes@1.0.
|
|
44
|
+
Source version: `@litfamily/lithermes@1.0.1`. Install the scoped package from npm with the command in Quick start. A reviewed local archive remains available as an optional isolated trial.
|
|
45
45
|
|
|
46
46
|
Landing images load from the published package so they render on npmjs. Guide links still point at the source repository.
|
|
47
47
|
|
package/README_Ko-KR.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<p align="center"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
1
|
+
<p align="center"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.1/readme-assets/cover.webp" width="100%" alt="LitHermes v5 armory cover" /></p>
|
|
2
2
|
|
|
3
3
|
```
|
|
4
4
|
▄▄▄▄
|
|
@@ -44,7 +44,7 @@ LitHermes는 **Hermes Agent**에서 계획, 실행, 검토, 인계를 이어갑
|
|
|
44
44
|
|
|
45
45
|
[English](./README.md) · [npm](https://www.npmjs.com/package/@litfamily/lithermes) · [GitHub](https://github.com/wjgoarxiv/lithermes)
|
|
46
46
|
|
|
47
|
-
소스 기준 버전은 `@litfamily/lithermes@1.0.
|
|
47
|
+
소스 기준 버전은 `@litfamily/lithermes@1.0.1`입니다. 빠른 시작의 명령으로 scoped package를 설치합니다. 별도 체험이 필요하면 검토된 로컬 압축 파일을 선택할 수 있습니다.
|
|
48
48
|
|
|
49
49
|
랜딩 이미지는 게시된 패키지에서 불러와 npmjs에서도 보입니다. 안내 문서 링크는 소스 저장소를 가리킵니다.
|
|
50
50
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
from contextvars import ContextVar
|
|
4
5
|
import functools
|
|
5
6
|
import re
|
|
6
7
|
from pathlib import Path
|
|
@@ -21,6 +22,35 @@ from .litgoal import hook as litgoal_hook
|
|
|
21
22
|
from .litgoal import tools as litgoal_tools
|
|
22
23
|
|
|
23
24
|
|
|
25
|
+
_NATIVE_COMMAND_CONTEXT: ContextVar[dict[str, str] | None] = ContextVar(
|
|
26
|
+
"lithermes_native_command_context", default=None
|
|
27
|
+
)
|
|
28
|
+
_NATIVE_COMMANDS = frozenset(
|
|
29
|
+
{
|
|
30
|
+
"lit-plan",
|
|
31
|
+
"litwork-plan",
|
|
32
|
+
"lit",
|
|
33
|
+
"lit-loop",
|
|
34
|
+
"litwork-loop",
|
|
35
|
+
"litgoal",
|
|
36
|
+
"review-work",
|
|
37
|
+
"start-work",
|
|
38
|
+
"deep-interview",
|
|
39
|
+
"lit-recap",
|
|
40
|
+
"lit-handoff",
|
|
41
|
+
"lit-scientific-visualization",
|
|
42
|
+
"korean-ai-slop-remover",
|
|
43
|
+
*core.KOREAN_PROSE_COMMANDS,
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
_NATIVE_GATEWAY_NOTICE = (
|
|
47
|
+
"LitHermes could not start an agent turn on the Hermes gateway: native gateway "
|
|
48
|
+
"injection requires an existing session key and "
|
|
49
|
+
"`plugins.entries.lithermes.allow_gateway_injection: true`; LitHermes does not "
|
|
50
|
+
"enable that setting."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
24
54
|
PORTED_SKILLS = [
|
|
25
55
|
(
|
|
26
56
|
"lit-burnoff-file",
|
|
@@ -358,7 +388,7 @@ def _setup_lithermes_cli(parser) -> None:
|
|
|
358
388
|
f"skills: {len(PORTED_SKILLS)} lithermes:* skills — `hermes lithermes status` lists them\n"
|
|
359
389
|
"contract schema: lithermes_llm_contract/v1 headings include #contract.activation, "
|
|
360
390
|
"#contract.inputs, #contract.outputs, #contract.evidence, and #contract.hard_stops\n"
|
|
361
|
-
"hooks: on_session_start, pre_llm_call, pre_tool_call, post_tool_call, "
|
|
391
|
+
"hooks: on_session_start, pre_llm_call, pre_tool_call, pre_command, post_tool_call, "
|
|
362
392
|
"post_api_request, subagent_stop, transform_llm_output, on_session_finalize, on_session_reset\n"
|
|
363
393
|
"bounded work: schema 3 via /lit-loop init|status|resume|cancel|complete and lithermes_work_progress\n"
|
|
364
394
|
"wikify knowledge: hermes lithermes knowledge status|query|capture|save|review\n"
|
|
@@ -403,6 +433,91 @@ def _ignited(handler, route):
|
|
|
403
433
|
return wrapped
|
|
404
434
|
|
|
405
435
|
|
|
436
|
+
def _capture_pre_command(**kwargs: Any) -> None:
|
|
437
|
+
"""Remember the host route immediately before Hermes invokes a command.
|
|
438
|
+
|
|
439
|
+
Hermes 0.21 fires ``pre_command`` for both the CLI and gateway surfaces. The
|
|
440
|
+
command handler itself only receives raw arguments, so this small
|
|
441
|
+
context-local bridge carries the surface/session pair into the native
|
|
442
|
+
``PluginContext.inject_message`` adapter below. Older Hermes releases do not
|
|
443
|
+
expose ``inject_message`` and never register this hook.
|
|
444
|
+
"""
|
|
445
|
+
surface = str(kwargs.get("surface") or "").strip().lower()
|
|
446
|
+
if surface not in {"cli", "gateway"}:
|
|
447
|
+
return None
|
|
448
|
+
command = str(
|
|
449
|
+
kwargs.get("command")
|
|
450
|
+
or kwargs.get("alias_used")
|
|
451
|
+
or ""
|
|
452
|
+
).strip().lstrip("/").lower()
|
|
453
|
+
if command not in _NATIVE_COMMANDS:
|
|
454
|
+
return None
|
|
455
|
+
session_key = str(kwargs.get("session_key") or "").strip()
|
|
456
|
+
_NATIVE_COMMAND_CONTEXT.set(
|
|
457
|
+
{"surface": surface, "session_key": session_key}
|
|
458
|
+
)
|
|
459
|
+
return None
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _native_command_dispatch(ctx, handler):
|
|
463
|
+
"""Adapt structured LitHermes results to Hermes 0.21's native API.
|
|
464
|
+
|
|
465
|
+
Hermes 0.17/0.19 have no ``inject_message`` method, so registration keeps
|
|
466
|
+
returning the legacy structured payload for the installer source patch. On
|
|
467
|
+
0.21 the plugin API owns the queueing/turn lifecycle; returning the dict to
|
|
468
|
+
the host would otherwise print its Python representation to users.
|
|
469
|
+
"""
|
|
470
|
+
inject_message = getattr(ctx, "inject_message", None)
|
|
471
|
+
if not callable(inject_message):
|
|
472
|
+
return handler
|
|
473
|
+
|
|
474
|
+
@functools.wraps(handler)
|
|
475
|
+
def wrapped(user_args):
|
|
476
|
+
context = _NATIVE_COMMAND_CONTEXT.get() or {}
|
|
477
|
+
try:
|
|
478
|
+
result = handler(user_args)
|
|
479
|
+
if not isinstance(result, dict):
|
|
480
|
+
return result
|
|
481
|
+
|
|
482
|
+
display = result.get("display") or result.get("message")
|
|
483
|
+
agent_message = result.get("agent_message")
|
|
484
|
+
injected = False
|
|
485
|
+
if agent_message:
|
|
486
|
+
try:
|
|
487
|
+
injected = bool(
|
|
488
|
+
inject_message(
|
|
489
|
+
str(agent_message),
|
|
490
|
+
role="user",
|
|
491
|
+
session_key=context.get("session_key") or None,
|
|
492
|
+
)
|
|
493
|
+
)
|
|
494
|
+
except Exception:
|
|
495
|
+
# A host-side refusal must remain a visible, actionable
|
|
496
|
+
# result rather than leaking the structured dict repr.
|
|
497
|
+
injected = False
|
|
498
|
+
|
|
499
|
+
if context.get("surface") == "gateway" and not injected:
|
|
500
|
+
display_text = str(display) if display else ""
|
|
501
|
+
return (
|
|
502
|
+
f"{display_text}\n\n{_NATIVE_GATEWAY_NOTICE}"
|
|
503
|
+
if display_text
|
|
504
|
+
else _NATIVE_GATEWAY_NOTICE
|
|
505
|
+
)
|
|
506
|
+
if display is not None:
|
|
507
|
+
return str(display)
|
|
508
|
+
return ""
|
|
509
|
+
finally:
|
|
510
|
+
_NATIVE_COMMAND_CONTEXT.set(None)
|
|
511
|
+
|
|
512
|
+
return wrapped
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _command_handler(ctx, handler, route=None):
|
|
516
|
+
"""Build a command handler that works on both legacy and native Hermes."""
|
|
517
|
+
ignited = _ignited(handler, route) if route else handler
|
|
518
|
+
return _native_command_dispatch(ctx, ignited)
|
|
519
|
+
|
|
520
|
+
|
|
406
521
|
def _transform_llm_output(**kwargs: Any) -> str | None:
|
|
407
522
|
named = handoff.transform_llm_output(**kwargs)
|
|
408
523
|
if named is not None:
|
|
@@ -458,6 +573,11 @@ def register(ctx) -> None:
|
|
|
458
573
|
# The host-supplied session id authorizes progress before the model-facing
|
|
459
574
|
# tool handler runs. The tool itself has no resume/grant lifecycle bypass.
|
|
460
575
|
ctx.register_hook("pre_tool_call", _pre_tool_call)
|
|
576
|
+
# Hermes 0.21 gives native plugin commands a host-owned turn injection API.
|
|
577
|
+
# Register the route bridge only when that API exists; older Hermes releases
|
|
578
|
+
# continue through the installer-managed source patch below.
|
|
579
|
+
if callable(getattr(ctx, "inject_message", None)):
|
|
580
|
+
ctx.register_hook("pre_command", _capture_pre_command)
|
|
461
581
|
# Observer over completed mutations. Hermes ignores this hook's return value, so
|
|
462
582
|
# it only buffers the mutated paths; pre_llm_call renders them into the
|
|
463
583
|
# event-shaped skill route on the next turn.
|
|
@@ -489,80 +609,80 @@ def register(ctx) -> None:
|
|
|
489
609
|
|
|
490
610
|
ctx.register_command(
|
|
491
611
|
"lit-plan",
|
|
492
|
-
|
|
612
|
+
_command_handler(ctx, core.command_lit_plan, "lit-plan"),
|
|
493
613
|
description="Create a durable Litwork implementation plan",
|
|
494
614
|
args_hint='"what to build"',
|
|
495
615
|
)
|
|
496
616
|
ctx.register_command(
|
|
497
617
|
"litwork-plan",
|
|
498
|
-
|
|
618
|
+
_command_handler(ctx, core.command_lit_plan, "lit-plan"),
|
|
499
619
|
description="Alias for /lit-plan",
|
|
500
620
|
args_hint='"what to build"',
|
|
501
621
|
)
|
|
502
622
|
ctx.register_command(
|
|
503
623
|
"lit",
|
|
504
|
-
|
|
624
|
+
_command_handler(ctx, core.command_lit, "litwork"),
|
|
505
625
|
description="Start a Litwork run and execute the task immediately",
|
|
506
626
|
args_hint='"task"',
|
|
507
627
|
)
|
|
508
628
|
ctx.register_command(
|
|
509
629
|
"lit-loop",
|
|
510
|
-
|
|
630
|
+
_command_handler(ctx, core.command_lit_loop, "lit-loop"),
|
|
511
631
|
description="Start a Litwork run or operate its bounded lifecycle",
|
|
512
632
|
args_hint='"task" [--completion-promise TEXT] [--strategy reset|continue] | init <plan> --grant ACTION@ROOT[,ACTION@ROOT] [--worktree PATH] | status <work-id> [--worktree PATH] | resume <work-id> --revision N --boundary ID --grant ACTION@ROOT [--worktree PATH] | cancel|complete <work-id> --revision N [--worktree PATH]',
|
|
513
633
|
)
|
|
514
634
|
ctx.register_command(
|
|
515
635
|
"litwork-loop",
|
|
516
|
-
|
|
636
|
+
_command_handler(ctx, core.command_lit_loop, "lit-loop"),
|
|
517
637
|
description="Alias for /lit-loop",
|
|
518
638
|
args_hint='"task"',
|
|
519
639
|
)
|
|
520
640
|
ctx.register_command(
|
|
521
641
|
"litgoal",
|
|
522
|
-
|
|
642
|
+
_command_handler(ctx, core.command_litgoal, "litgoal"),
|
|
523
643
|
description="Open or inspect the LitHermes litgoal durable runtime",
|
|
524
644
|
args_hint='["objective"] [--worktree PATH]',
|
|
525
645
|
)
|
|
526
646
|
ctx.register_command(
|
|
527
647
|
"review-work",
|
|
528
|
-
|
|
648
|
+
_command_handler(ctx, core.command_review_work, "review-work"),
|
|
529
649
|
description="Run the LitHermes 5-lane review orchestrator on the current diff",
|
|
530
650
|
args_hint="[--base REF]",
|
|
531
651
|
)
|
|
532
652
|
ctx.register_command(
|
|
533
653
|
"start-work",
|
|
534
|
-
|
|
654
|
+
_command_handler(ctx, core.command_start_work, "start-work"),
|
|
535
655
|
description="Open or dry-run a LitHermes plan against a workspace",
|
|
536
656
|
args_hint="[plan-name] [--worktree PATH] [--dry-run]",
|
|
537
657
|
)
|
|
538
658
|
ctx.register_command(
|
|
539
659
|
"deep-interview",
|
|
540
|
-
|
|
660
|
+
_command_handler(ctx, core.command_deep_interview, "deep-interview"),
|
|
541
661
|
description="Run the LitHermes Socratic clarity gate before planning/execution",
|
|
542
662
|
args_hint="[--quick|--standard|--deep] <idea>",
|
|
543
663
|
)
|
|
544
664
|
ctx.register_command(
|
|
545
665
|
"lit-recap",
|
|
546
|
-
|
|
666
|
+
_command_handler(ctx, core.command_lit_recap, "lit-recap"),
|
|
547
667
|
description="Read-only Korean recap of finalized work (side-effect-free)",
|
|
548
668
|
args_hint="[--brief] [--en] [--worktree PATH]",
|
|
549
669
|
)
|
|
550
670
|
ctx.register_command(
|
|
551
671
|
"lit-handoff",
|
|
552
|
-
handoff.command_lit_handoff,
|
|
672
|
+
_command_handler(ctx, handoff.command_lit_handoff),
|
|
553
673
|
description="Create or update a verified LitHermes continuation handoff",
|
|
554
674
|
args_hint="[focus]",
|
|
555
675
|
)
|
|
556
676
|
ctx.register_command(
|
|
557
677
|
"lit-scientific-visualization",
|
|
558
|
-
scientific_visualization.command_lit_scientific_visualization,
|
|
678
|
+
_command_handler(ctx, scientific_visualization.command_lit_scientific_visualization),
|
|
559
679
|
description="Create a publication-quality scientific figure with the bundled source",
|
|
560
680
|
args_hint="[figure request or dataset paths]",
|
|
561
681
|
)
|
|
562
682
|
for name in core.KOREAN_PROSE_COMMANDS:
|
|
563
683
|
ctx.register_command(
|
|
564
684
|
name,
|
|
565
|
-
|
|
685
|
+
_command_handler(ctx, core.command_korean_prose_cleanup, "lit-korean"),
|
|
566
686
|
description="Side-effect-free Korean prose cleanup and naturalization",
|
|
567
687
|
args_hint="[Korean text]",
|
|
568
688
|
)
|
|
@@ -576,7 +696,7 @@ def register(ctx) -> None:
|
|
|
576
696
|
|
|
577
697
|
ctx.register_command(
|
|
578
698
|
"korean-ai-slop-remover",
|
|
579
|
-
|
|
699
|
+
_command_handler(ctx, korean_rename_redirect, "lit-korean"),
|
|
580
700
|
description="Redirect to /lit-korean for this release; removed in the next minor",
|
|
581
701
|
args_hint="[Korean text]",
|
|
582
702
|
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"syncedAt": "2026-09-
|
|
2
|
+
"syncedAt": "2026-09-17T12:28:42.389Z",
|
|
3
3
|
"source": "bundled-payload",
|
|
4
|
-
"sourceHash": "
|
|
4
|
+
"sourceHash": "20ca6086bec7f31f2f1bda43b4f99ec34d95fbc81fcdec9d98815897a69099bc",
|
|
5
5
|
"files": [
|
|
6
6
|
{
|
|
7
7
|
"path": "NOTICE.md",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
"path": "__init__.py",
|
|
16
|
-
"sha256": "
|
|
16
|
+
"sha256": "1cd11adf3eb20516e4891840db188324c3065e634b06007f36a9137485403c7a"
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
"path": "auto_update.py",
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
},
|
|
86
86
|
{
|
|
87
87
|
"path": "diagnostics.py",
|
|
88
|
-
"sha256": "
|
|
88
|
+
"sha256": "5ee1242f357dc164a6ab663baf3d658a13d53f437240e33b8bb1c5cffa4d9ed2"
|
|
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": "4af3d8b0f4ca8c613bba087fbf5d70400097f7bae73b0bc7936f077c88bb66ec"
|
|
161
161
|
},
|
|
162
162
|
{
|
|
163
163
|
"path": "provider_metrics.py",
|
|
@@ -1253,7 +1253,7 @@
|
|
|
1253
1253
|
},
|
|
1254
1254
|
{
|
|
1255
1255
|
"path": "skills/frontend-ui-ux/references/complete-contract.md",
|
|
1256
|
-
"sha256": "
|
|
1256
|
+
"sha256": "2728e0114c749ef101018ee217adf338e7a01030b2865fdec33946b0155b9e2d"
|
|
1257
1257
|
},
|
|
1258
1258
|
{
|
|
1259
1259
|
"path": "skills/frontend-ui-ux/references/composition.md",
|
|
@@ -1893,7 +1893,7 @@
|
|
|
1893
1893
|
},
|
|
1894
1894
|
{
|
|
1895
1895
|
"path": "skills/visual-qa/references/complete-contract.md",
|
|
1896
|
-
"sha256": "
|
|
1896
|
+
"sha256": "7858afb13f0e877e82e94039d290d8afd3bf938337cb3260364a12a6e1028924"
|
|
1897
1897
|
},
|
|
1898
1898
|
{
|
|
1899
1899
|
"path": "skills/visual-qa/schemas/evidence-manifest-v1alpha1.schema.json",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name: lithermes
|
|
2
|
-
version: 1.0.
|
|
2
|
+
version: 1.0.1
|
|
3
3
|
description: "Hermes-native workflow toolkit: litgoal durable runtime, 5-lane review orchestrator, Litwork commands, skills, and prompt steering."
|
|
4
4
|
author: "Hermes Agent"
|
|
5
5
|
kind: standalone
|
|
@@ -8,6 +8,7 @@ hooks:
|
|
|
8
8
|
- on_session_start
|
|
9
9
|
- pre_llm_call
|
|
10
10
|
- pre_tool_call
|
|
11
|
+
- pre_command
|
|
11
12
|
- post_tool_call
|
|
12
13
|
- post_api_request
|
|
13
14
|
- subagent_stop
|
|
@@ -420,7 +420,7 @@ isolated Hermes home. Use `--hermes-home`; never `--home`.
|
|
|
420
420
|
|
|
421
421
|
```text
|
|
422
422
|
$ node packages/lithermes-installer/bin/lithermes.js install --yes --offline --no-hud --hermes-home "$ISOLATED"
|
|
423
|
-
Installed LitHermes 1.0.
|
|
423
|
+
Installed LitHermes 1.0.1
|
|
424
424
|
plugin: $ISOLATED/plugins/lithermes
|
|
425
425
|
model config: updated
|
|
426
426
|
|
|
@@ -437,7 +437,7 @@ installed payload: PASS
|
|
|
437
437
|
enabled config: PASS
|
|
438
438
|
|
|
439
439
|
$ hermes lithermes doctor
|
|
440
|
-
[OK] plugin.yaml readable (version 1.0.
|
|
440
|
+
[OK] plugin.yaml readable (version 1.0.1)
|
|
441
441
|
[OK] skills bundled: 32
|
|
442
442
|
[OK] litgoal durable runtime importable
|
|
443
443
|
```
|
|
@@ -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.1
|
|
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.1)
|
|
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.1"><title>release: 1.0.1</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.1</text></g></svg>
|
package/src/lib/check.js
CHANGED
|
@@ -27,8 +27,25 @@ function scanPluginCommands(pluginPath) {
|
|
|
27
27
|
return requiredCommands.filter((cmd) => source.includes(`"${cmd}"`) || source.includes(`'${cmd}'`));
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
function nativePluginInjectionAvailable(repo) {
|
|
31
|
+
if (!repo) return false;
|
|
32
|
+
const file = path.join(repo, "hermes_cli", "plugins.py");
|
|
33
|
+
if (!fs.existsSync(file)) return false;
|
|
34
|
+
const source = fs.readFileSync(file, "utf8");
|
|
35
|
+
return source.includes("def inject_message(")
|
|
36
|
+
&& source.includes("session_key")
|
|
37
|
+
&& source.includes("allow_gateway_injection");
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
function checkGatewaySource(repo) {
|
|
31
41
|
if (!repo) return { ok: true, skipped: true };
|
|
42
|
+
if (nativePluginInjectionAvailable(repo)) {
|
|
43
|
+
return {
|
|
44
|
+
ok: true,
|
|
45
|
+
native: true,
|
|
46
|
+
reason: "Hermes PluginContext.inject_message handles native gateway dispatch",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
32
49
|
const file = path.join(repo, "gateway", "run.py");
|
|
33
50
|
if (!fs.existsSync(file)) return { ok: false, reason: "gateway/run.py missing" };
|
|
34
51
|
const source = fs.readFileSync(file, "utf8");
|
|
@@ -40,6 +57,13 @@ function checkGatewaySource(repo) {
|
|
|
40
57
|
|
|
41
58
|
function checkCliSource(repo) {
|
|
42
59
|
if (!repo) return { ok: true, skipped: true };
|
|
60
|
+
if (nativePluginInjectionAvailable(repo)) {
|
|
61
|
+
return {
|
|
62
|
+
ok: true,
|
|
63
|
+
native: true,
|
|
64
|
+
reason: "Hermes PluginContext.inject_message handles native CLI turns",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
43
67
|
const file = path.join(repo, "cli.py");
|
|
44
68
|
if (!fs.existsSync(file)) return { ok: false, reason: "cli.py missing" };
|
|
45
69
|
const source = fs.readFileSync(file, "utf8");
|
|
@@ -60,6 +84,12 @@ function checkTuiSource(repo) {
|
|
|
60
84
|
};
|
|
61
85
|
}
|
|
62
86
|
|
|
87
|
+
function formatDispatchStatus(result) {
|
|
88
|
+
if (result.skipped) return "SKIPPED";
|
|
89
|
+
if (result.native) return "PASS (native PluginContext.inject_message)";
|
|
90
|
+
return "PASS";
|
|
91
|
+
}
|
|
92
|
+
|
|
63
93
|
function enabledUserJsonIncludesPlugin(output, pluginName) {
|
|
64
94
|
try {
|
|
65
95
|
const entries = JSON.parse(String(output));
|
|
@@ -95,7 +125,9 @@ function checkLitHermes(flags = {}) {
|
|
|
95
125
|
if (flags["gateway-offline"]) {
|
|
96
126
|
const gateway = checkGatewaySource(hermesRepo);
|
|
97
127
|
if (!gateway.ok && !gateway.skipped) throw new LitHermesError(`gateway check FAIL: ${gateway.reason}`, 7);
|
|
98
|
-
const status = gateway.skipped
|
|
128
|
+
const status = gateway.skipped
|
|
129
|
+
? "SKIPPED (no Hermes repo provided)"
|
|
130
|
+
: formatDispatchStatus(gateway);
|
|
99
131
|
lines.push(`gateway /lit_loop ${status}`);
|
|
100
132
|
lines.push(`gateway /lit_plan ${status}`);
|
|
101
133
|
}
|
|
@@ -172,9 +204,9 @@ function doctorLitHermes(flags = {}) {
|
|
|
172
204
|
`model route safety: ${modelSafety.status}${modelSafety.code ? ` (${modelSafety.code}; ${modelSafety.reason})` : ` (${modelSafety.reason})`}`,
|
|
173
205
|
`runtime: ${runtime.status}${runtime.status === "hard" ? ` (${runtime.provider} ${runtime.apiMode})` : ` (${runtime.reason})`}`,
|
|
174
206
|
`global child route: ${formatManagedRoute(delegationRoute)}`,
|
|
175
|
-
`cli payload dispatch: ${cli.ok ? (cli
|
|
176
|
-
`tui payload dispatch: ${tui.ok ? (tui
|
|
177
|
-
`gateway underscore dispatch: ${gateway.ok ? (gateway
|
|
207
|
+
`cli payload dispatch: ${cli.ok ? formatDispatchStatus(cli) : "PATCH_AVAILABLE"}`,
|
|
208
|
+
`tui payload dispatch: ${tui.ok ? formatDispatchStatus(tui) : "PATCH_AVAILABLE"}`,
|
|
209
|
+
`gateway underscore dispatch: ${gateway.ok ? formatDispatchStatus(gateway) : "PATCH_AVAILABLE"}`,
|
|
178
210
|
];
|
|
179
211
|
return {
|
|
180
212
|
message: lines.join("\n"),
|
|
@@ -195,6 +227,7 @@ module.exports = {
|
|
|
195
227
|
loadedStatusMatchesOrigin,
|
|
196
228
|
requiredCommands,
|
|
197
229
|
requiredSkills,
|
|
230
|
+
nativePluginInjectionAvailable,
|
|
198
231
|
scanPluginCommands,
|
|
199
232
|
scanPluginSkills,
|
|
200
233
|
};
|
|
@@ -65,22 +65,43 @@ function detectHermesRuntimeRepo() {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
function readIfExists(filePath) {
|
|
69
|
+
try {
|
|
70
|
+
return fs.readFileSync(filePath, "utf8");
|
|
71
|
+
} catch {
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Hermes 0.21.0 split tools/delegate_tool.py: the concurrency knob and delegation-route
|
|
77
|
+
// helpers moved into tools/delegate_tool_config.py, and hermes_cli/runtime_provider.py
|
|
78
|
+
// moved from an if/elif chain to a _POOL_ENTRY_SIMPLE_MODES data table. Both layouts are
|
|
79
|
+
// checked so a 0.21.0 host is not misreported as lacking capabilities it genuinely has.
|
|
68
80
|
function inspectHermesHostCapabilities(hermesRepo) {
|
|
69
81
|
const unavailable = { concurrencyHard: false, delegationRouteHard: false, runtimeHard: false };
|
|
70
82
|
if (!hermesRepo) return unavailable;
|
|
71
83
|
try {
|
|
72
|
-
const
|
|
84
|
+
const legacyDelegation = readIfExists(path.join(hermesRepo, "tools", "delegate_tool.py"));
|
|
85
|
+
const configDelegation = readIfExists(path.join(hermesRepo, "tools", "delegate_tool_config.py"));
|
|
86
|
+
if (!legacyDelegation && !configDelegation) return unavailable;
|
|
87
|
+
const delegation = `${legacyDelegation}\n${configDelegation}`;
|
|
73
88
|
const runtime = fs.readFileSync(path.join(hermesRepo, "hermes_cli", "runtime_provider.py"), "utf8");
|
|
74
89
|
const runtimeStem = ["co", "dex"].join("");
|
|
75
90
|
return {
|
|
76
91
|
concurrencyHard: delegation.includes("_get_max_concurrent_children")
|
|
77
|
-
&& delegation.includes('cfg.get("max_concurrent_children")')
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
92
|
+
&& ((delegation.includes('cfg.get("max_concurrent_children")') && delegation.includes("max(1, int(val))"))
|
|
93
|
+
|| (delegation.includes('"max_concurrent_children", "DELEGATION_MAX_CONCURRENT_CHILDREN"')
|
|
94
|
+
&& delegation.includes("max(1, int(v))"))),
|
|
95
|
+
// The config-level model override moved from a single `configured_model` line
|
|
96
|
+
// (0.17/0.19) into a per-key dict comprehension in _resolve_delegation_credentials
|
|
97
|
+
// (0.21.0); both read the same cfg.get("model") value.
|
|
98
|
+
delegationRouteHard: delegation.includes('delegation_cfg.get("reasoning_effort")')
|
|
99
|
+
&& delegation.includes("effective_model = model or parent_agent.model")
|
|
100
|
+
&& (delegation.includes('configured_model = str(cfg.get("model")')
|
|
101
|
+
|| delegation.includes('str(cfg.get(k) or "").strip() or None for k in ("model"')),
|
|
102
|
+
runtimeHard: (runtime.includes(`provider == "openai-${runtimeStem}"`)
|
|
103
|
+
&& runtime.includes(`api_mode = "${runtimeStem}_responses"`))
|
|
104
|
+
|| runtime.includes(`"openai-${runtimeStem}": ("${runtimeStem}_responses"`),
|
|
84
105
|
};
|
|
85
106
|
} catch {
|
|
86
107
|
return unavailable;
|
package/src/lib/install.js
CHANGED
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
} = require("./config");
|
|
15
15
|
const { copyTree, removeEmptyDirs, sha256, writeFileAtomic } = require("./files");
|
|
16
16
|
const { assertStateParent, assertPluginParent, defaultHermesHome, defaultHermesRepo, detectHermesRuntimeRepo, detectHermesVersion, ensureHermesHome, inspectHermesHostCapabilities, LitHermesError } = require("./hermesDiscovery");
|
|
17
|
+
const { NOT_PROBED_HOST_VERSION } = require("./modelConfigBoundary");
|
|
17
18
|
const { patchInstalledHermes, rollbackPatches } = require("./patch");
|
|
18
19
|
const {
|
|
19
20
|
formatManagedRoute,
|
|
@@ -333,7 +334,7 @@ function installLitHermes(flags = {}) {
|
|
|
333
334
|
const modelPlan = planModelConfig(beforeConfig, {
|
|
334
335
|
...routeOptions(flags),
|
|
335
336
|
hostCapabilities: inspectHermesHostCapabilities(capabilityRepo),
|
|
336
|
-
hostVersion: dryRun ?
|
|
337
|
+
hostVersion: dryRun ? NOT_PROBED_HOST_VERSION : detectHermesVersion(),
|
|
337
338
|
reconfigure: Boolean(flags["reconfigure-model"]),
|
|
338
339
|
});
|
|
339
340
|
if (modelPlan.action === "stop") {
|
|
@@ -499,7 +500,13 @@ function installLitHermes(flags = {}) {
|
|
|
499
500
|
] : []),
|
|
500
501
|
...capabilityLines({ capabilities: effectiveCapabilities }),
|
|
501
502
|
];
|
|
502
|
-
if (patchResult)
|
|
503
|
+
if (patchResult) {
|
|
504
|
+
lines.push(
|
|
505
|
+
patchResult.native
|
|
506
|
+
? "patches: native PluginContext.inject_message dispatch (no source patch)"
|
|
507
|
+
: `patches: ${patchResult.changed.length ? patchResult.changed.join(", ") : "none needed"}`,
|
|
508
|
+
);
|
|
509
|
+
}
|
|
503
510
|
if (patchWarning) lines.push(`patches: skipped (${patchWarning})`);
|
|
504
511
|
const credentialWarning = modelPlan.request && effectiveModelAction === "write"
|
|
505
512
|
? missingCredentialWarning(modelPlan.request, process.env)
|
|
@@ -12,6 +12,10 @@ const VERIFIED_HOST_VERSION_TEXT = [...VERIFIED_HOST_VERSIONS].join(", ");
|
|
|
12
12
|
// Below this the managed schema did not exist, so forward compatibility must not become
|
|
13
13
|
// backward acceptance.
|
|
14
14
|
const MINIMUM_HOST_VERSION = "0.17.0";
|
|
15
|
+
// Sentinel `hostVersion` for a dry-run, which deliberately never executes the Hermes
|
|
16
|
+
// binary. Distinguishes "never probed" from a real host whose banner came back empty or
|
|
17
|
+
// malformed, which must still be reported as unsupported.
|
|
18
|
+
const NOT_PROBED_HOST_VERSION = "__lithermes_dry_run_not_probed__";
|
|
15
19
|
const CONFIG_VERSION = 30;
|
|
16
20
|
const CREDENTIAL_KEY = /(^|[_-])(api[_-]?key|client[_-]?secret|auth[_-]?token|(?:aws[_-]?)?access[_-]?key(?:[_-]?id)?|(?:ssh[_-]?)?private[_-]?key|token|password|passphrase|secret|credential|authorization)([_-]|$)/i;
|
|
17
21
|
const MAP_PATHS = ["model", "agent", "delegation", "compression"];
|
|
@@ -111,6 +115,7 @@ function compareVersions(left, right) {
|
|
|
111
115
|
* this matrix has seen it.
|
|
112
116
|
*/
|
|
113
117
|
function classifyHostVersion(raw) {
|
|
118
|
+
if (raw === NOT_PROBED_HOST_VERSION) return { version: "", status: "not-probed" };
|
|
114
119
|
const version = exactHostVersion(raw);
|
|
115
120
|
// A release is exactly major.minor.patch. Four-part builds such as 0.19.0.1 are not a
|
|
116
121
|
// published release line and were rejected before this gate accepted unseen versions.
|
|
@@ -121,6 +126,9 @@ function classifyHostVersion(raw) {
|
|
|
121
126
|
}
|
|
122
127
|
|
|
123
128
|
function hostVersionError(classified) {
|
|
129
|
+
if (classified.status === "not-probed") {
|
|
130
|
+
return "Hermes host version not probed (dry-run); re-run without --dry-run to detect the host and its capabilities";
|
|
131
|
+
}
|
|
124
132
|
if (classified.status === "malformed") {
|
|
125
133
|
return `unsupported Hermes host version; expected a release banner at or above ${MINIMUM_HOST_VERSION} (verified: ${VERIFIED_HOST_VERSION_TEXT})`;
|
|
126
134
|
}
|
|
@@ -197,6 +205,7 @@ function parseBoundary(text, options = {}) {
|
|
|
197
205
|
|
|
198
206
|
module.exports = {
|
|
199
207
|
MINIMUM_HOST_VERSION,
|
|
208
|
+
NOT_PROBED_HOST_VERSION,
|
|
200
209
|
OPENAI_PROVIDER,
|
|
201
210
|
RESPONSES_MODE,
|
|
202
211
|
classifyHostVersion,
|
package/src/lib/patch.js
CHANGED
|
@@ -7,6 +7,15 @@ function patchManifestPath(hermesHome) {
|
|
|
7
7
|
return path.join(hermesHome, "lithermes", "patch-manifest.json");
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
function nativePluginInjectionAvailable(repo) {
|
|
11
|
+
const file = path.join(repo, "hermes_cli", "plugins.py");
|
|
12
|
+
if (!fs.existsSync(file)) return false;
|
|
13
|
+
const source = fs.readFileSync(file, "utf8");
|
|
14
|
+
return source.includes("def inject_message(")
|
|
15
|
+
&& source.includes("session_key")
|
|
16
|
+
&& source.includes("allow_gateway_injection");
|
|
17
|
+
}
|
|
18
|
+
|
|
10
19
|
function patchTarget(repo, relative, marker, additions) {
|
|
11
20
|
const file = path.join(repo, relative);
|
|
12
21
|
if (!fs.existsSync(file)) {
|
|
@@ -95,6 +104,28 @@ function patchInstalledHermes({ hermesHome, hermesRepo }) {
|
|
|
95
104
|
if (!hermesRepo || !fs.existsSync(hermesRepo)) {
|
|
96
105
|
throw new LitHermesError("Cannot patch Hermes because --hermes-repo was not found. Pass --hermes-repo PATH or run doctor first.", 8);
|
|
97
106
|
}
|
|
107
|
+
if (nativePluginInjectionAvailable(hermesRepo)) {
|
|
108
|
+
fs.mkdirSync(path.dirname(patchManifestPath(hermesHome)), { recursive: true });
|
|
109
|
+
writeFileAtomic(
|
|
110
|
+
patchManifestPath(hermesHome),
|
|
111
|
+
JSON.stringify({
|
|
112
|
+
patchedAt: new Date().toISOString(),
|
|
113
|
+
nativeDispatch: {
|
|
114
|
+
cli: true,
|
|
115
|
+
gateway: true,
|
|
116
|
+
reason: "Hermes PluginContext.inject_message is available",
|
|
117
|
+
},
|
|
118
|
+
records: [],
|
|
119
|
+
}, null, 2),
|
|
120
|
+
"utf8",
|
|
121
|
+
);
|
|
122
|
+
return {
|
|
123
|
+
changed: [],
|
|
124
|
+
records: [],
|
|
125
|
+
native: true,
|
|
126
|
+
reason: "Hermes PluginContext.inject_message is available",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
98
129
|
const changed = [];
|
|
99
130
|
const records = [];
|
|
100
131
|
const cli = patchCliPayloadDispatch(hermesRepo);
|
|
@@ -146,6 +177,7 @@ function rollbackPatches({ hermesHome }) {
|
|
|
146
177
|
}
|
|
147
178
|
|
|
148
179
|
module.exports = {
|
|
180
|
+
nativePluginInjectionAvailable,
|
|
149
181
|
patchCliPayloadDispatch,
|
|
150
182
|
patchInstalledHermes,
|
|
151
183
|
patchManifestPath,
|