@litfamily/lithermes 1.0.23 → 1.0.24

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,14 +1,14 @@
1
- <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.23/readme-assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.23/readme-assets/cover-motion.webp" width="100%" alt="LitFamily motion cover: five armored robots power on one by one, the LitHermes robot wakes with glowing eyes and a lit frame, then LITFAMILY and KEEP THE WORK LIT. light up." /></picture></p>
1
+ <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.24/readme-assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.24/readme-assets/cover-motion.webp" width="100%" alt="LitFamily motion cover: five armored robots power on one by one, the LitHermes robot wakes with glowing eyes and a lit frame, then LITFAMILY and KEEP THE WORK LIT. light up." /></picture></p>
2
2
 
3
3
  <p align="center">
4
- <a href="#install"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.23/readme-assets/badge-version.svg" alt="1.0.23" /></a>
5
- <a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.23/readme-assets/badge-license.svg" alt="MIT license" /></a>
4
+ <a href="#install"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.24/readme-assets/badge-version.svg" alt="1.0.24" /></a>
5
+ <a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.24/readme-assets/badge-license.svg" alt="MIT license" /></a>
6
6
  </p>
7
7
 
8
8
  <p align="center">
9
- <a href="https://github.com/wjgoarxiv/lithermes/blob/main/docs/guide.md"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.23/readme-assets/lucide-book-open.svg" width="16" alt="" /> Guide</a> &nbsp;
10
- <a href="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.23/readme-assets/ignition-film.mp4"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.23/readme-assets/lucide-play.svg" width="16" alt="" /> Ignition film</a> &nbsp;
11
- <a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.23/readme-assets/lucide-shield-check.svg" width="16" alt="" /> MIT</a>
9
+ <a href="https://github.com/wjgoarxiv/lithermes/blob/main/docs/guide.md"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.24/readme-assets/lucide-book-open.svg" width="16" alt="" /> Guide</a> &nbsp;
10
+ <a href="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.24/readme-assets/ignition-film.mp4"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.24/readme-assets/lucide-play.svg" width="16" alt="" /> Ignition film</a> &nbsp;
11
+ <a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.24/readme-assets/lucide-shield-check.svg" width="16" alt="" /> MIT</a>
12
12
  </p>
13
13
 
14
14
  # LitHermes
@@ -17,7 +17,7 @@
17
17
 
18
18
  A plugin for **Hermes Agent**. Put `lit` in front of a request, and Hermes plans the task, does it, checks what it did and leaves a note that the next session can pick up.
19
19
 
20
- **[README and docs on GitHub](https://github.com/wjgoarxiv/lithermes#readme)** · [한국어](https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.23/README_Ko-KR.md)
20
+ **[README and docs on GitHub](https://github.com/wjgoarxiv/lithermes#readme)** · [한국어](https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.24/README_Ko-KR.md)
21
21
 
22
22
  ## Install
23
23
 
package/README_Ko-KR.md CHANGED
@@ -1,14 +1,14 @@
1
- <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.23/readme-assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.23/readme-assets/cover-motion.webp" width="100%" alt="LitFamily 모션 커버: 다섯 로봇 패널이 차례로 켜지고, LitHermes 로봇의 눈과 테두리가 빛난 뒤 LITFAMILY와 KEEP THE WORK LIT. 문구가 밝아지는 영상" /></picture></p>
1
+ <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.24/readme-assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.24/readme-assets/cover-motion.webp" width="100%" alt="LitFamily 모션 커버: 다섯 로봇 패널이 차례로 켜지고, LitHermes 로봇의 눈과 테두리가 빛난 뒤 LITFAMILY와 KEEP THE WORK LIT. 문구가 밝아지는 영상" /></picture></p>
2
2
 
3
3
  <p align="center">
4
- <a href="#설치"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.23/readme-assets/badge-version.svg" alt="1.0.23" /></a>
5
- <a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.23/readme-assets/badge-license.svg" alt="MIT license" /></a>
4
+ <a href="#설치"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.24/readme-assets/badge-version.svg" alt="1.0.24" /></a>
5
+ <a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.24/readme-assets/badge-license.svg" alt="MIT license" /></a>
6
6
  </p>
7
7
 
8
8
  <p align="center">
9
- <a href="https://github.com/wjgoarxiv/lithermes/blob/main/docs/guide.ko.md"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.23/readme-assets/lucide-book-open.svg" width="16" alt="" /> 안내</a> &nbsp;
10
- <a href="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.23/readme-assets/ignition-film.mp4"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.23/readme-assets/lucide-play.svg" width="16" alt="" /> Ignition 영상</a> &nbsp;
11
- <a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.23/readme-assets/lucide-shield-check.svg" width="16" alt="" /> MIT</a>
9
+ <a href="https://github.com/wjgoarxiv/lithermes/blob/main/docs/guide.ko.md"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.24/readme-assets/lucide-book-open.svg" width="16" alt="" /> 안내</a> &nbsp;
10
+ <a href="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.24/readme-assets/ignition-film.mp4"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.24/readme-assets/lucide-play.svg" width="16" alt="" /> Ignition 영상</a> &nbsp;
11
+ <a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.24/readme-assets/lucide-shield-check.svg" width="16" alt="" /> MIT</a>
12
12
  </p>
13
13
 
14
14
  # LitHermes
@@ -17,7 +17,7 @@
17
17
 
18
18
  **Hermes Agent**용 플러그인입니다. 요청 앞에 `lit`을 붙이면 Hermes가 작업을 계획하고, 해내고, 한 일을 확인한 뒤, 다음 세션이 이어받을 수 있는 기록을 남깁니다.
19
19
 
20
- **[GitHub의 README와 문서](https://github.com/wjgoarxiv/lithermes/blob/main/README_Ko-KR.md)** · [English](https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.23/README.md)
20
+ **[GitHub의 README와 문서](https://github.com/wjgoarxiv/lithermes/blob/main/README_Ko-KR.md)** · [English](https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.24/README.md)
21
21
 
22
22
  ## 설치
23
23
 
@@ -174,6 +174,18 @@ CLI/plugin name, not the npm package name.
174
174
  Native `/goal` is user-managed and unobserved; users may manage it themselves.
175
175
  LitHermes has no cache-coherent host controller/post-judge API and performs no automatic update, clear, or resume. Durable `goal_*` state is authoritative for
176
176
  criteria, evidence, and completion.
177
+ - The `transform_tool_result` hook is the one post-execution hook whose return
178
+ value Hermes keeps. After a `terminal` or `execute_code` call it looks for DOCX,
179
+ PPTX and Markdown files modified while the call ran, in a bounded walk of the
180
+ call's working directories and the folders of paths named in the command, code
181
+ or output (depth 4, 4,000 entries per folder, 6 files, 16 MiB per Office file;
182
+ hidden, dependency and internal `plans/` / `evidence/` / `.hermes/` paths are
183
+ skipped). Office text goes through the bundled extractor and the LitHumanizer
184
+ detector. A block-tier finding adds `LIT_HUMANIZER_SCRIPT_DELIVERABLE_BLOCK`
185
+ with the file, rule ids and matched words to that result's `error` field, so the
186
+ model reads it before its next step. It instructs a rebuild and cannot refuse
187
+ the write: the file already exists, and the command's exit code is left as it
188
+ ran. Each file version is judged once per session.
177
189
  - The `post_api_request` observer consumes Hermes' normalized numeric `usage`
178
190
  receipt only (`input_tokens`, `output_tokens`, `prompt_tokens`,
179
191
  `total_tokens`, `reasoning_tokens`, `cache_read_tokens`, and
@@ -434,7 +434,7 @@ def _setup_lithermes_cli(parser) -> None:
434
434
  "contract schema: lithermes_llm_contract/v1 headings include #contract.activation, "
435
435
  "#contract.inputs, #contract.outputs, #contract.evidence, and #contract.hard_stops\n"
436
436
  "hooks: on_session_start, pre_llm_call, pre_tool_call, pre_command, post_tool_call, "
437
- "post_api_request, subagent_stop, transform_llm_output, on_session_finalize, on_session_reset\n"
437
+ "transform_tool_result, post_api_request, subagent_stop, transform_llm_output, on_session_finalize, on_session_reset\n"
438
438
  "bounded work: schema 3 via /lit-loop init|status|resume|cancel|complete and lithermes_work_progress\n"
439
439
  "wikify knowledge: hermes lithermes knowledge status|query|capture|save|review\n"
440
440
  "run `hermes lithermes status` for versions + full surface, or `doctor` for health checks"
@@ -604,6 +604,10 @@ def _post_tool_call(**kwargs: Any) -> None:
604
604
  return core.post_tool_call(**kwargs)
605
605
 
606
606
 
607
+ def _transform_tool_result(**kwargs: Any) -> str | None:
608
+ return deliverable_hedge_guard.transform_tool_result(**kwargs)
609
+
610
+
607
611
  def _on_session_start(**kwargs: Any) -> None:
608
612
  return core.on_session_start(**kwargs)
609
613
 
@@ -643,6 +647,10 @@ def register(ctx) -> None:
643
647
  # it only buffers the mutated paths; pre_llm_call renders them into the
644
648
  # event-shaped skill route on the next turn.
645
649
  ctx.register_hook("post_tool_call", _post_tool_call)
650
+ # The one post-execution hook whose return Hermes keeps: a deliverable built by
651
+ # a terminal/execute_code script is checked and, on a block-tier finding, the
652
+ # tool result the model reads next carries the rebuild instruction.
653
+ ctx.register_hook("transform_tool_result", _transform_tool_result)
646
654
  # Hermes exposes normalized provider usage here. The callback records only
647
655
  # bounded numeric cache counters and hashed labels; raw request/response
648
656
  # compatibility fields are intentionally ignored.
@@ -3,6 +3,13 @@
3
3
  ``pre_tool_call`` can deny ``write_file`` and ``patch`` before persistence. The
4
4
  ``post_tool_call`` observer cannot undo a completed Office/PDF export, so those
5
5
  checks are advisory and ask for a source edit and rebuild.
6
+
7
+ A deliverable built by a script never passes through ``write_file``. For
8
+ ``terminal`` and ``execute_code`` calls, ``transform_tool_result`` (the one
9
+ post-execution hook whose return value Hermes keeps) looks for DOCX, PPTX and
10
+ Markdown files modified while the call ran and, on a block-tier finding,
11
+ rewrites the tool result the model reads next. The file is already on disk, so
12
+ this instructs a rebuild; it cannot refuse the write.
6
13
  """
7
14
 
8
15
  from __future__ import annotations
@@ -14,6 +21,8 @@ import re
14
21
  import stat
15
22
  import subprocess
16
23
  import sys
24
+ import time
25
+ from collections import deque
17
26
  from dataclasses import dataclass
18
27
  from pathlib import Path
19
28
  from typing import Iterable
@@ -32,6 +41,18 @@ DETECT_TIMEOUT_SECONDS = 3
32
41
  TEXT_SUFFIXES = frozenset({".adoc", ".csv", ".htm", ".html", ".md", ".mdx", ".rst", ".svg", ".tex", ".txt"})
33
42
  OFFICE_SUFFIXES = frozenset({".docx", ".pptx", ".pdf"})
34
43
  READER_FACING_SUFFIXES = TEXT_SUFFIXES | OFFICE_SUFFIXES
44
+ SCRIPT_TOOLS = frozenset({"terminal", "execute_code"})
45
+ SCRIPT_DELIVERABLE_SUFFIXES = frozenset({".docx", ".md", ".pptx"})
46
+ MAX_SCAN_ROOTS = 6
47
+ MAX_SCAN_DEPTH = 4
48
+ MAX_SCAN_ENTRIES_PER_ROOT = 4000
49
+ MAX_SCAN_FILES = 6
50
+ MAX_SCRIPT_OFFICE_BYTES = 16 * 1024 * 1024
51
+ MAX_SCRIPT_AGGREGATE_BYTES = 32 * 1024 * 1024
52
+ MAX_SCRIPT_TEXT_CHARS = 65_536
53
+ MTIME_SLACK_SECONDS = 2.0
54
+ _SKIP_SCAN_DIRS = frozenset({"node_modules", "__pycache__", "site-packages", "venv"})
55
+ _PATH_TOKEN = re.compile(r"[^\s'\"`=<>|;&(),]+\.(?:docx|pptx|md|py|mjs|js|sh)\b", re.I)
35
56
 
36
57
  _QUOTED_SPAN = re.compile(
37
58
  r'"([^"\n]{1,2000})"|“([^”\n]{1,2000})”|'
@@ -58,12 +79,16 @@ _READ_BUDGET_EXCEEDED = _ReadBudgetExceeded()
58
79
  _ROOT_TURNS: dict[str, str] = {}
59
80
  _USER_QUOTES: dict[str, tuple[str, ...]] = {}
60
81
  _PENDING_ADVISORIES: dict[str, list[str]] = {}
82
+ # File versions (device, inode, mtime, size) a script check already judged, per
83
+ # session, so a later quick command does not repeat the same verdict.
84
+ _SCRIPT_CHECKED: dict[str, list[tuple[int, int, int, int]]] = {}
61
85
 
62
86
 
63
87
  def reset_state() -> None:
64
88
  _ROOT_TURNS.clear()
65
89
  _USER_QUOTES.clear()
66
90
  _PENDING_ADVISORIES.clear()
91
+ _SCRIPT_CHECKED.clear()
67
92
 
68
93
 
69
94
  def release_session(session_id: object) -> None:
@@ -72,6 +97,7 @@ def release_session(session_id: object) -> None:
72
97
  _ROOT_TURNS.pop(key, None)
73
98
  _USER_QUOTES.pop(key, None)
74
99
  _PENDING_ADVISORIES.pop(key, None)
100
+ _SCRIPT_CHECKED.pop(key, None)
75
101
 
76
102
 
77
103
  def _bounded_set(mapping: dict[str, object], key: str, value: object) -> None:
@@ -167,12 +193,13 @@ def _read_stable_file_result(
167
193
  path_text: str,
168
194
  workspace: Path,
169
195
  byte_limit: int,
196
+ max_file_bytes: int = MAX_FILE_BYTES,
170
197
  ) -> _StableFile | _ReadBudgetExceeded | None:
171
198
  relative = _relative_artifact_path(path_text, workspace)
172
199
  directory_flags = _directory_flags()
173
200
  if relative is None or directory_flags is None or byte_limit < 0:
174
201
  return None
175
- read_limit = min(MAX_FILE_BYTES, byte_limit)
202
+ read_limit = min(max_file_bytes, byte_limit)
176
203
  root_parts = tuple(part for part in workspace.parts if part != os.path.sep)
177
204
  directory_fds: list[int] = []
178
205
  edges: list[tuple[int, str, tuple[int, int], bool]] = []
@@ -211,7 +238,7 @@ def _read_stable_file_result(
211
238
  dir_fd=parent_fd,
212
239
  )
213
240
  initial = os.fstat(artifact_fd)
214
- if not stat.S_ISREG(initial.st_mode) or initial.st_size > MAX_FILE_BYTES:
241
+ if not stat.S_ISREG(initial.st_mode) or initial.st_size > max_file_bytes:
215
242
  return None
216
243
  if initial.st_size > read_limit:
217
244
  return _READ_BUDGET_EXCEEDED
@@ -581,3 +608,163 @@ def observe_completed_paths(
581
608
  "LIT_HUMANIZER_POSTWRITE_ADVISORY: the completed Office/PDF export has "
582
609
  f"{blocks} block-tier and {warnings} warning-tier finding(s). Edit the source and rebuild; Hermes cannot undo a completed export.",
583
610
  )
611
+
612
+
613
+ def _script_scan_roots(payload: dict, parsed: object, task_id: str, text: str) -> list[Path]:
614
+ """Directories a script call could have written into, most specific first."""
615
+ named: list[object] = [parsed.get("cwd") if isinstance(parsed, dict) else None, payload.get("workdir")]
616
+ terminal = sys.modules.get("tools.terminal_tool")
617
+ session_cwd = getattr(terminal, "get_session_cwd", None)
618
+ if callable(session_cwd) and task_id:
619
+ try:
620
+ named.append(session_cwd(task_id))
621
+ except Exception: # noqa: BLE001 - a host helper failure only drops one candidate
622
+ pass
623
+ named.append(os.getcwd())
624
+ base = next((Path(str(item)).expanduser() for item in named if isinstance(item, str) and item and os.path.isdir(item)), Path.cwd())
625
+ for token in _PATH_TOKEN.findall(text)[:32]:
626
+ candidate = Path(token).expanduser()
627
+ named.insert(len(named) - 1, os.fspath((candidate if candidate.is_absolute() else base / candidate).parent))
628
+ roots: list[Path] = []
629
+ for item in named:
630
+ if not isinstance(item, str) or not item:
631
+ continue
632
+ try:
633
+ root = Path(item).expanduser().resolve(strict=True)
634
+ except (OSError, RuntimeError):
635
+ continue
636
+ if root.is_dir() and root not in roots:
637
+ roots.append(root)
638
+ if len(roots) >= MAX_SCAN_ROOTS:
639
+ break
640
+ return roots
641
+
642
+
643
+ def _recent_deliverables(
644
+ roots: list[Path], since: float, checked: list[tuple[int, int, int, int]]
645
+ ) -> list[tuple[Path, Path, tuple[int, int, int, int]]]:
646
+ """Bounded breadth-first walk for deliverables modified at or after ``since``."""
647
+ found: list[tuple[Path, Path, tuple[int, int, int, int]]] = []
648
+ seen: set[tuple[int, int]] = set()
649
+ for root in roots:
650
+ queue: deque[tuple[Path, int]] = deque([(root, 0)])
651
+ entries = 0
652
+ while queue and entries < MAX_SCAN_ENTRIES_PER_ROOT:
653
+ directory, depth = queue.popleft()
654
+ try:
655
+ with os.scandir(directory) as iterator:
656
+ for entry in iterator:
657
+ entries += 1
658
+ if entries > MAX_SCAN_ENTRIES_PER_ROOT:
659
+ break
660
+ name = entry.name
661
+ if entry.is_dir(follow_symlinks=False):
662
+ if (
663
+ depth < MAX_SCAN_DEPTH
664
+ and not name.startswith(".")
665
+ and name not in _SKIP_SCAN_DIRS
666
+ and not _INTERNAL_FILE.search(name)
667
+ ):
668
+ queue.append((Path(entry.path), depth + 1))
669
+ continue
670
+ if os.path.splitext(name)[1].lower() not in SCRIPT_DELIVERABLE_SUFFIXES:
671
+ continue
672
+ info = entry.stat(follow_symlinks=False)
673
+ identity = (info.st_dev, info.st_ino)
674
+ version = (info.st_dev, info.st_ino, info.st_mtime_ns, info.st_size)
675
+ if (
676
+ not stat.S_ISREG(info.st_mode)
677
+ or info.st_mtime < since
678
+ or identity in seen
679
+ or version in checked
680
+ ):
681
+ continue
682
+ relative = Path(entry.path).relative_to(root)
683
+ if _relative_artifact_path(relative.as_posix(), root) is None:
684
+ continue
685
+ seen.add(identity)
686
+ found.append((root, relative, version))
687
+ if len(found) >= MAX_SCAN_FILES:
688
+ return found
689
+ except OSError:
690
+ continue
691
+ return found
692
+
693
+
694
+ def _script_block_message(hits: list[tuple[str, list[dict]]]) -> str:
695
+ parts = []
696
+ for name, blocks in hits:
697
+ refs = ", ".join(
698
+ f"{item['rule']}:{item['line']} \"{' '.join(item['match'].split())[:60]}\"" for item in blocks[:5]
699
+ )
700
+ extra = f" (+{len(blocks) - 5})" if len(blocks) > 5 else ""
701
+ parts.append(f"{name} ({refs}{extra})")
702
+ return (
703
+ "LIT_HUMANIZER_SCRIPT_DELIVERABLE_BLOCK: this call wrote reader-facing file(s) with block-tier "
704
+ f"LitHumanizer findings: {'; '.join(parts)}. The file is already on disk and is not ready to present. "
705
+ "Rewrite those sentences in the build source, then rebuild: state a real limit once as a fact beside "
706
+ "the number it affects, and say what the document is not for in your chat reply, not in the file."
707
+ )
708
+
709
+
710
+ def transform_tool_result(
711
+ tool_name: object = None,
712
+ args: object = None,
713
+ result: object = None,
714
+ session_id: object = None,
715
+ task_id: object = None,
716
+ duration_ms: object = 0,
717
+ **_: object,
718
+ ) -> str | None:
719
+ """Check deliverables a terminal/execute_code call just built; rewrite its result on a block hit."""
720
+ if str(tool_name or "") not in SCRIPT_TOOLS or not isinstance(result, str):
721
+ return None
722
+ key = str(session_id or "") or str(task_id or "")
723
+ payload = args if isinstance(args, dict) else {}
724
+ try:
725
+ parsed = json.loads(result)
726
+ except (TypeError, ValueError):
727
+ parsed = None
728
+ elapsed = duration_ms / 1000 if isinstance(duration_ms, (int, float)) and duration_ms > 0 else 0.0
729
+ since = time.time() - elapsed - MTIME_SLACK_SECONDS
730
+ output = parsed.get("output") if isinstance(parsed, dict) else result
731
+ text = "\n".join(
732
+ str(value)[:MAX_SCRIPT_TEXT_CHARS]
733
+ for value in (payload.get("command"), payload.get("code"), output)
734
+ if isinstance(value, str)
735
+ )
736
+ roots = _script_scan_roots(payload, parsed, str(task_id or ""), text)
737
+ hits: list[tuple[str, list[dict]]] = []
738
+ remaining = MAX_SCRIPT_AGGREGATE_BYTES
739
+ checked = _SCRIPT_CHECKED.get(key, [])
740
+ for root, relative, version in _recent_deliverables(roots, since, checked):
741
+ suffix = relative.suffix.lower()
742
+ cap = MAX_SCRIPT_OFFICE_BYTES if suffix in OFFICE_SUFFIXES else MAX_FILE_BYTES
743
+ read = _read_stable_file_result(relative.as_posix(), root, remaining, max_file_bytes=cap)
744
+ if not isinstance(read, _StableFile):
745
+ _fail_open(key)
746
+ continue
747
+ remaining -= read.byte_count
748
+ try:
749
+ content = read.data.decode("utf-8") if suffix == ".md" else _extract_office(read.data, suffix)
750
+ except (OSError, RuntimeError, ValueError, subprocess.TimeoutExpired, ImportError):
751
+ _fail_open(key)
752
+ continue
753
+ findings = _run_detector(_mask_exact_user_quotes(content, key), relative.as_posix())
754
+ if findings is None:
755
+ _fail_open(key)
756
+ continue
757
+ checked = [*checked, version][-64:]
758
+ if key:
759
+ _bounded_set(_SCRIPT_CHECKED, key, checked)
760
+ blocks = [item for item in findings if item["severity"] == "block"]
761
+ if blocks:
762
+ hits.append((relative.as_posix(), blocks))
763
+ if not hits:
764
+ return None
765
+ message = _script_block_message(hits)
766
+ if isinstance(parsed, dict):
767
+ previous = parsed.get("error")
768
+ parsed["error"] = f"{previous}\n{message}" if previous else message
769
+ return json.dumps(parsed, ensure_ascii=False)
770
+ return f"{result}\n\n{message}"
@@ -43,6 +43,7 @@ HOOKS = (
43
43
  "pre_tool_call",
44
44
  "pre_command",
45
45
  "post_tool_call",
46
+ "transform_tool_result",
46
47
  "post_api_request",
47
48
  "subagent_stop",
48
49
  "transform_llm_output",
@@ -125,6 +125,17 @@ def _inline_code_as_spaces(line: str) -> str:
125
125
  return re.sub(r"(`+)[^`]*?\1", lambda match: " " * len(match.group(0)), line)
126
126
 
127
127
 
128
+ _QUOTE_PREFIX = re.compile(r"^\s{0,3}>\s?")
129
+ _BOLD_LABEL_QUOTE = re.compile(r"^\s{0,3}>\s?(?:\*\*|__)[^*_\n]{1,24}(?:\*\*|__)")
130
+
131
+
132
+ def _callout_text(line: str) -> str:
133
+ """A quote opening with a bold label is the author's own callout; blank the marker and read it."""
134
+ if not _BOLD_LABEL_QUOTE.match(line):
135
+ return ""
136
+ return _inline_code_as_spaces(_QUOTE_PREFIX.sub(lambda prefix: " " * len(prefix.group(0)), line, count=1))
137
+
138
+
128
139
  def _source_rows(text: str) -> list[dict]:
129
140
  fence: tuple[str, int] | None = None
130
141
  caption_pending = 0
@@ -148,7 +159,10 @@ def _source_rows(text: str) -> list[dict]:
148
159
  if re.match(r"^\s{0,3}>", line):
149
160
  caption_pending = 0
150
161
  figure_active = False
151
- rows.append({"line": line, "number": number, "candidate": "", "skipped": True, "caption": False, "caption_attribution": False})
162
+ rows.append({
163
+ "line": line, "number": number, "candidate": "", "callout": _callout_text(line),
164
+ "skipped": True, "caption": False, "caption_attribution": False,
165
+ })
152
166
  continue
153
167
  candidate = _inline_code_as_spaces(line)
154
168
  caption = _is_caption(line)
@@ -228,11 +242,13 @@ def _split_sentences(span: dict) -> list[dict]:
228
242
 
229
243
  def _rule_segments(rows: list[dict], rule: dict) -> list[dict]:
230
244
  skip_captions = "table_figure_caption" in rule["context"]
245
+ read_callouts = "blockquote_callout" in rule["context"]
231
246
  if rule["scope"] == "line":
232
247
  return [
233
- {"text": row["candidate"], "line": row["number"]}
248
+ {"text": row["callout"] if row.get("callout") and read_callouts else row["candidate"], "line": row["number"]}
234
249
  for row in rows
235
- if not row["skipped"] and not (skip_captions and (row["caption"] or row["caption_attribution"]))
250
+ if (read_callouts and row.get("callout"))
251
+ or (not row["skipped"] and not (skip_captions and (row["caption"] or row["caption_attribution"])))
236
252
  ]
237
253
  segments: list[dict] = []
238
254
  group: list[dict] = []
@@ -246,6 +262,10 @@ def _rule_segments(rows: list[dict], rule: dict) -> list[dict]:
246
262
 
247
263
  for row in rows:
248
264
  exempt = skip_captions and (row["caption"] or row["caption_attribution"])
265
+ if read_callouts and row.get("callout"):
266
+ flush()
267
+ segments.append({"text": row["callout"], "line": row["number"]})
268
+ continue
249
269
  if row["skipped"] or exempt or (rule["scope"] == "sentence" and not row["candidate"].strip()):
250
270
  flush()
251
271
  else:
@@ -269,9 +289,9 @@ def scan_text(text: str, rules: Iterable[dict] | None = None, file: str = "<stdi
269
289
  selected = list(rules) if rules is not None else load_rules()
270
290
  rows = _source_rows(str(text or ""))
271
291
  findings = []
272
- cache: dict[tuple[str, bool], list[dict]] = {}
292
+ cache: dict[tuple[str, bool, bool], list[dict]] = {}
273
293
  for rule in selected:
274
- key = (rule["scope"], "table_figure_caption" in rule["context"])
294
+ key = (rule["scope"], "table_figure_caption" in rule["context"], "blockquote_callout" in rule["context"])
275
295
  if key not in cache:
276
296
  # Scope is the only rule-specific segmentation property. A shallow
277
297
  # rule copy shares its context only when caption handling differs.
@@ -1,7 +1,7 @@
1
1
  {
2
- "syncedAt": "2026-10-15T09:46:03.900Z",
2
+ "syncedAt": "2026-10-17T05:27:18.436Z",
3
3
  "source": "bundled-payload",
4
- "sourceHash": "164f684b1b783534bf9b71f657f9e9d86b321e018c4a1ac02e7abe3cf604edd6",
4
+ "sourceHash": "8c064731aa1b50d96ed0bd56d63fdf9f6035e990123543583024446fd0627c08",
5
5
  "files": [
6
6
  {
7
7
  "path": "NOTICE.md",
@@ -9,11 +9,11 @@
9
9
  },
10
10
  {
11
11
  "path": "README.md",
12
- "sha256": "03256264acbaba328e51b82a527fe511db7f4ff907d0a74762bdaf0baadc8e20"
12
+ "sha256": "2749b24d24a2af75fc54f48d59916549fb876ea823a22a2881817cdb6542cbcf"
13
13
  },
14
14
  {
15
15
  "path": "__init__.py",
16
- "sha256": "76cf72655600b82544e9047fab1f5f4030927803c0cfbc08fca311204f37d32c"
16
+ "sha256": "4793a2c3b1384528d59c725d7f33a6fd81995c6120906311888a5f17b5eec941"
17
17
  },
18
18
  {
19
19
  "path": "auto_handoff.py",
@@ -85,11 +85,11 @@
85
85
  },
86
86
  {
87
87
  "path": "deliverable_hedge_guard.py",
88
- "sha256": "804cf1253064e94ac19db45df03a5eb2ed68ef968fe1658c720d56159bf29048"
88
+ "sha256": "084eb4381d8be3055eada034bf0c3c31339b9c766cbf3b19fd85042e2e254039"
89
89
  },
90
90
  {
91
91
  "path": "diagnostics.py",
92
- "sha256": "88d7b76438c82755fb9456882994ab4efd70c889705c04095e5a12d1759425fb"
92
+ "sha256": "51200cd2a2d9782c16b09aaca32773ebbd46b501739094af185c8ea87009f696"
93
93
  },
94
94
  {
95
95
  "path": "handoff.py",
@@ -97,7 +97,7 @@
97
97
  },
98
98
  {
99
99
  "path": "humanizer_detector.py",
100
- "sha256": "bca68ebaad84ab2fec5a41a91f293abe9e194011669ef03386611c8412561e97"
100
+ "sha256": "48e4f3a02326521769ac193b858eecbb90773cb765aa8d5794ffaa3c1dee2d62"
101
101
  },
102
102
  {
103
103
  "path": "humanizer_ko_metrics.py",
@@ -181,7 +181,7 @@
181
181
  },
182
182
  {
183
183
  "path": "plugin.yaml",
184
- "sha256": "92d263e3abf96c4533db6814cfb6a104f3a8822e3de205106eaa4edff1c76338"
184
+ "sha256": "660275e3e5e46e75d21d4fc6a4a715f6a8cd95b4cc7cfc3c799a295d299d844d"
185
185
  },
186
186
  {
187
187
  "path": "provider_metrics.py",
@@ -1285,7 +1285,7 @@
1285
1285
  },
1286
1286
  {
1287
1287
  "path": "skills/frontend-ui-ux/references/complete-contract.md",
1288
- "sha256": "f872ab0d5e2c5a99c40fec7630df67c45c23a927b562fd35943e404204e756aa"
1288
+ "sha256": "aee6777260bf314b911a54c8ae8f005f3111d6dafd9fafdc2e8e5fe76534e118"
1289
1289
  },
1290
1290
  {
1291
1291
  "path": "skills/frontend-ui-ux/references/composition.md",
@@ -3277,7 +3277,7 @@
3277
3277
  },
3278
3278
  {
3279
3279
  "path": "skills/lit-humanizer/SKILL.md",
3280
- "sha256": "1df63514da5f9295578c3fc7ff475ae911da2310ed43ca3e794759dc4414b66a"
3280
+ "sha256": "47c562a344c89d16be5271f86874af556991dd5c9a26f45e0800eda5a08f5381"
3281
3281
  },
3282
3282
  {
3283
3283
  "path": "skills/lit-humanizer/assets/report-skeleton.md",
@@ -3513,11 +3513,11 @@
3513
3513
  },
3514
3514
  {
3515
3515
  "path": "skills/lit-humanizer/references/taxonomy.md",
3516
- "sha256": "d0958c702eb531f82612ff48d16134afbfd6c65b40bc80ebc4bce778a02b4238"
3516
+ "sha256": "7bdda99ad8f2e1358b70f96e017d5a70b879251b4681f8752babaf7598030239"
3517
3517
  },
3518
3518
  {
3519
3519
  "path": "skills/lit-humanizer/rules.json",
3520
- "sha256": "806e8caaeea25b34fa7a2caf8957a4af6fe95e557d5db71fa6d6e90fcea7091c"
3520
+ "sha256": "61f494bbbd9dee707b96b50308bf9c045d6b268218a442b3022b1c29194bb95c"
3521
3521
  },
3522
3522
  {
3523
3523
  "path": "skills/lit-humanizer/scripts/detect.py",
@@ -4405,7 +4405,7 @@
4405
4405
  },
4406
4406
  {
4407
4407
  "path": "skills/visual-qa/references/complete-contract.md",
4408
- "sha256": "daa71196ff16fc545fdaad07367cdcd425c9e5cd3cbcb10e20f28ba10f1c3be3"
4408
+ "sha256": "cceb7ffbac840f63517aa2a47918a2d02b063b383c763942724f64881667e40c"
4409
4409
  },
4410
4410
  {
4411
4411
  "path": "skills/visual-qa/schemas/evidence-manifest-v1alpha1.schema.json",
@@ -1,5 +1,5 @@
1
1
  name: lithermes
2
- version: 1.0.23
2
+ version: 1.0.24
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
@@ -10,6 +10,7 @@ hooks:
10
10
  - pre_tool_call
11
11
  - pre_command
12
12
  - post_tool_call
13
+ - transform_tool_result
13
14
  - post_api_request
14
15
  - subagent_stop
15
16
  - transform_llm_output
@@ -33,6 +34,7 @@ provides_hooks:
33
34
  - pre_tool_call
34
35
  - pre_command
35
36
  - post_tool_call
37
+ - transform_tool_result
36
38
  - post_api_request
37
39
  - subagent_stop
38
40
  - transform_llm_output
@@ -445,7 +445,7 @@ isolated Hermes home. Use `--hermes-home`; never `--home`.
445
445
 
446
446
  ```text
447
447
  $ node packages/lithermes-installer/bin/lithermes.js install --yes --offline --no-hud --hermes-home "$ISOLATED"
448
- Installed LitHermes 1.0.23
448
+ Installed LitHermes 1.0.24
449
449
  plugin: $ISOLATED/plugins/lithermes
450
450
  model config: updated
451
451
 
@@ -462,7 +462,7 @@ installed payload: PASS
462
462
  enabled config: PASS
463
463
 
464
464
  $ hermes lithermes doctor
465
- [OK] plugin.yaml readable (version 1.0.23)
465
+ [OK] plugin.yaml readable (version 1.0.24)
466
466
  [OK] skills bundled: 36
467
467
  [OK] litgoal durable runtime importable
468
468
  ```
@@ -21,7 +21,7 @@ identity:
21
21
  invocation: "lithermes:<frontmatter.name>"
22
22
  surfaces:
23
23
  manifest: plugin.yaml
24
- python_entrypoints: ["__init__.py:register", "core.py:pre_llm_call", "deliverable_hedge_guard.py:pre_tool_call"]
24
+ python_entrypoints: ["__init__.py:register", "core.py:pre_llm_call", "deliverable_hedge_guard.py:pre_tool_call", "deliverable_hedge_guard.py:transform_tool_result"]
25
25
  bundled_rule: rules/bundled-rules/lit-humanizer.md
26
26
  detector: skills/lit-humanizer/scripts/detect.py
27
27
  state:
@@ -60,7 +60,7 @@ after_payload_edit: "npm --prefix packages/lithermes-installer run sync-plugin -
60
60
 
61
61
  For a file edit, the Hermes `pre_tool_call` guard blocks block-tier findings in added text passed to `write_file` or `patch` for Markdown, HTML, text, TeX, CSV, SVG, reStructuredText, and AsciiDoc files. Warn-tier findings arrive as advisory context. Fenced code, inline code, exact quoted user spans, and internal `plans/`, `evidence/`, `HANDOFF*`, ledger, `.lit*/`, and `.hermes/` paths are excluded. The guard never scans unchanged file text. Use `write_file` or `patch` for prose edits when pre-save checking is required.
62
62
 
63
- DOCX/PPTX/PDF checks happen after a supported file-write event or when a script-like tool reports an explicit output path in its result. Hermes' `post_tool_call` is observer-only, so a finding cannot undo a completed export: revise the source and rebuild the file. DOCX/PPTX extraction uses Python's standard-library ZIP/XML support. PDF extraction uses host `pdftotext` when present; if it is unavailable or fails, the scan fails open and reports that boundary. Script-created files without a reported output path are outside automatic post-write inspection; run the detector explicitly after such a write.
63
+ DOCX/PPTX/PDF checks happen after a supported file-write event or when a script-like tool reports an explicit output path in its result. Hermes' `post_tool_call` is observer-only, so a finding cannot undo a completed export: revise the source and rebuild the file. DOCX/PPTX extraction uses Python's standard-library ZIP/XML support. PDF extraction uses host `pdftotext` when present; if it is unavailable or fails, the scan fails open and reports that boundary. A DOCX, PPTX or Markdown file that a `terminal` or `execute_code` call builds is checked when that call returns: Hermes' `transform_tool_result` hook finds files modified while the call ran (the reported or session working directory, `workdir`, and folders of paths named in the command, code or output; bounded depth, file count and size, internal paths skipped) and, on a block-tier finding, puts `LIT_HUMANIZER_SCRIPT_DELIVERABLE_BLOCK` with the rule ids in that tool result's `error` field. The command's exit code stays as it ran, and the file stays on disk: edit the build source and rebuild before presenting it. A file written outside those folders, deeper than the scan, or by a background process after the call returned is not seen; run the detector on it explicitly.
64
64
 
65
65
  ## #contract.language_and_scope
66
66
 
@@ -17,7 +17,7 @@ This original category comes from the 26 real lines in `examples/detector-valida
17
17
  - ko-bracket-status-tag and en-bracket-status-tag — ko/en, block. Replace bracket keys with a sentence describing the status.
18
18
  - ko-standalone-scope-disclaimer and en-standalone-scope-disclaimer — ko/en, block for stock disclaimers. Preserve a user-requested legal, medical or safety notice and place it once where it applies.
19
19
  - ko-status-negation (also catches 쓰지, 않습니다/않았습니다 endings and bold-wrapped endings), ko-mi-status, en-epistemic-negation and en-not-performed — warnings. A negative result or unperformed action may be the finding. Write the actor, scope and verb plainly; never turn unknown into false or an estimate into a fact.
20
- - ko-negative-pledge and en-negative-pledge — ko/en, block. A line that tells the reader what the document, a table or a number is not for (`동일 조건의 독립 성능 비교로 쓰지 않음`, `This is not intended as a head-to-head benchmark`) is a pledge to the reader. Move it to the chat reply, or restate the real limit as a data fact beside the number ("샘플은 한 공정 조건만 포함한다"). Operating rules, method lines and legal or financial notices ("원금을 보장하지 않습니다", "not a substitute for professional advice") are not pledges and pass; so does a negative finding such as 수율을 개선하지 않았다.
20
+ - ko-negative-pledge and en-negative-pledge — ko/en, block. A line that tells the reader what the document, a table or a number is not for (`동일 조건의 독립 성능 비교로 쓰지 않음`, `This is not intended as a head-to-head benchmark`) is a pledge to the reader. Move it to the chat reply, or restate the real limit as a data fact beside the number ("샘플은 한 공정 조건만 포함한다"). Operating rules, method lines and legal or financial notices ("원금을 보장하지 않습니다", "not a substitute for professional advice") are not pledges and pass; so does a negative finding such as 수율을 개선하지 않았다. The same refusal covers the phrasings a model reaches for next (`이 표는 순위가 아닙니다`, `숫자끼리 서로 비교할 수 없습니다`, `This table is not a ranking`), while a plain instruction such as `Do not compare these values directly; convert to SI first` or a unit-mismatch rule is an operating rule and passes. A quote line is skipped unless it opens with a bold label (`> **주의:** …`); that is the author's own callout and is read like any other line.
21
21
  - ko-negative-pledge-repeat — ko, block, whole-text scope. Two or more sentences ending in 해석하지/주장하지/대표하지 + 않음, or in a claim noun + 이/가 + 아님, make a disclaimer list. One such sentence stays with the warning above.
22
22
  - ko-plain-meta-label-warn and en-plain-caveat-label — warnings for boundary headings. They may be useful in internal or technical packets.
23
23
  - en-knowledge-cutoff, en-ai-self-disclaimer, en-hope-helps and ko-chatbot-signoff — block. Remove the model wrapper or generic sign-off, then retain the answer.
@@ -884,14 +884,15 @@
884
884
  "id": "ko-negative-pledge",
885
885
  "lang": "ko",
886
886
  "severity": "block",
887
- "pattern": "(?<![가-힣])(?:이|본|해당|위|아래)\\s+(?:자료|문서|보고서|슬라이드|덱|표|그림|그래프|차트|수치|결과|비교|분석|내용|예시)(?:은|는)\\s[^.!?\\n]{0,40}?(?:(?:해석하|의미하|주장하|대표하)(?:지\\s*(?:[*_]{1,2}\\s*)?(?:않(?:음|는다|습니다)|말\\s*것|마세요|마십시오)|(?:으)?면\\s*(?:[*_]{1,2}\\s*)?안\\s*(?:됨|된다|됩니다))|(?:(?:비교|결론|벤치마크|순위|대표값|일반화)(?:이|가)|목적이|위한\\s*것이)\\s*(?:[*_]{1,2}\\s*)?(?:아님|아니다|아닙니다)(?![가-힣]))|(?:비교|결론|벤치마크|순위|대표값|일반화)(?:으로|로)\\s*(?:[*_]{1,2}\\s*)?(?:쓰|사용하|해석하|읽|간주하|인용하)(?:지\\s*(?:[*_]{1,2}\\s*)?(?:않(?:음|는다|습니다)|말\\s*것|마세요|마십시오)|(?:으)?면\\s*(?:[*_]{1,2}\\s*)?안\\s*(?:됨|된다|됩니다))|(?:공개|배포|대외|외부|홍보|발표|보고|비교|인용|공식)용\\s*(?:자료|문서|수치|표|그래프)(?:이|가)\\s*(?:[*_]{1,2}\\s*)?(?:아님|아니다|아닙니다)(?![가-힣])",
887
+ "pattern": "(?<![가-힣])(?:이|본|해당|위|아래)\\s+(?:자료|문서|보고서|슬라이드|덱|표|그림|그래프|차트|수치|결과|비교|분석|내용|예시)(?:은|는)\\s[^.!?\\n]{0,40}?(?:(?:해석하|의미하|주장하|대표하)(?:지\\s*(?:[*_]{1,2}\\s*)?(?:않(?:음|는다|습니다)|말\\s*것|마세요|마십시오|말아\\s*주(?:세요|십시오))|(?:으)?면\\s*(?:[*_]{1,2}\\s*)?안\\s*(?:됨|된다|됩니다)(?![가-힣]))|(?:(?:비교|결론|벤치마크|순위|대표값|일반화)(?:이|가)|목적이|(?:위한|려는)\\s*것이)\\s*(?:[*_]{1,2}\\s*)?(?:아님|아니다|아닙니다)(?![가-힣])|순위표(?:이|가)\\s*(?:[*_]{1,2}\\s*)?(?:아님|아니다|아닙니다|아니고)(?![가-힣]))|(?:비교|결론|벤치마크|순위|대표값|일반화)(?:처럼|으로|로)\\s*(?:[*_]{1,2}\\s*)?(?:쓰|사용하|해석하|읽|간주하|인용하)(?:지\\s*(?:[*_]{1,2}\\s*)?(?:않(?:음|는다|습니다)|말\\s*것|마세요|마십시오|말아\\s*주(?:세요|십시오))|(?:으)?면\\s*(?:[*_]{1,2}\\s*)?안\\s*(?:됨|된다|됩니다)(?![가-힣]))|(?:공개|배포|대외|외부|홍보|발표|보고|비교|인용|공식)용\\s*(?:자료|문서|수치|표|그래프)(?:이|가)\\s*(?:[*_]{1,2}\\s*)?(?:아님|아니다|아닙니다)(?![가-힣])|(?<![가-힣])(?:숫자|수치)(?:들)?(?:은|는|이|가|을|를|끼리|도)(?![가-힣])(?:(?!면\\s)[^.!?\\n]){0,40}?(?<![와과]\\s)(?<![와과])비교할\\s*수\\s*(?:는\\s*)?없(?:다|습니다|음|나)(?![가-힣])|(?<![가-힣])(?:숫자|수치)(?:들)?끼리\\s*(?:서로\\s*|직접\\s*)?비교(?:해서|하여|해)\\s[^.!?\\n]{0,30}?[\"”’']\\s*(?:는|라는)\\s*결론[^.!?\\n]{0,8}?(?:수\\s*없(?:다|습니다)(?![가-힣])|(?:으)?면\\s*(?:[*_]{1,2}\\s*)?안\\s*(?:됨|된다|됩니다)(?![가-힣]))|(?:(?<![가-힣])(?:숫자|수치)(?:들)?|(?<![가-힣])(?:이|위|아래|본)\\s+(?:표|값|자료|그래프|차트))[^.!?\\n]{0,40}?(?:빠르|느리)다고\\s*판단하(?:지\\s*(?:[*_]{1,2}\\s*)?(?:말\\s*것|마세요|마십시오|말아\\s*주(?:세요|십시오))|(?:으)?면\\s*(?:[*_]{1,2}\\s*)?안\\s*(?:됨|된다|됩니다)(?![가-힣]))|(?:빠르|느리)다[\"”’']?\\s*(?:는|라는)\\s*식으로\\s*읽(?:지\\s*(?:[*_]{1,2}\\s*)?(?:말\\s*것|마세요|마십시오|말아\\s*주(?:세요|십시오))|(?:으)?면\\s*(?:[*_]{1,2}\\s*)?안\\s*(?:됨|된다|됩니다)(?![가-힣]))|만으로\\s[^.!?\\n]{0,40}?(?<![가-힣])순위를\\s*정하[^.!?\\n]{0,25}?(?:수\\s*없(?:다|습니다|음)(?![가-힣])|(?:해|하)서는\\s*안\\s*(?:됨|된다|됩니다)(?![가-힣]))|(?<![가-힣])순위[^.!?\\n]{0,20}?근거로\\s*(?:[*_]{1,2}\\s*)?사용할\\s*수\\s*(?:는\\s*)?없(?:다|습니다|음)(?![가-힣])|할\\s*수\\s*없는\\s*것\\s{0,3}:\\s{0,3}(?:[*_]{1,2}\\s{0,3})?[^.!?\\n]{0,30}?(?<![가-힣])순위를?\\s*매기거나|(?<![가-힣])표의\\s+(?:나열\\s+)?순서(?:는|가)\\s[^.!?\\n]{0,20}?(?<![가-힣])순위(?:(?:를|을)\\s*뜻하지\\s*(?:[*_]{1,2}\\s*)?않(?:음|는다|습니다)|(?:가|이)\\s*(?:[*_]{1,2}\\s*)?(?:아님|아니다|아닙니다)(?![가-힣]))",
888
888
  "flags": "m",
889
889
  "scope": "line",
890
890
  "context": [
891
891
  "fenced_code",
892
- "quoted_user_text"
892
+ "quoted_user_text",
893
+ "blockquote_callout"
893
894
  ],
894
- "rationale": "A line that tells the reader what the document, a table or a number is not for is a pledge to the reader, not a finding, so the write is refused and the pledge goes to the chat reply or turns into a data fact. The pattern recognises three shapes: a self-reference such as 이 표 or 본 자료 followed by a negated claim verb or a claim noun plus 아님; a claim noun such as 비교 or 순위 followed by (으)로 and a negated use verb, which has no subject at all; and a purpose-용 noun such as 공개용 수치 followed by 아님. Use verbs after a self-reference, 근거, 평가, 증거 and 추천 are deliberately absent, as are the legal and financial notice verbs 대체하, 보장하 and 권유, because operating rules, method lines and standard notices end that way.",
895
+ "rationale": "A line that tells the reader what the document, a table or a number is not for is a pledge to the reader, not a finding, so the write is refused and the pledge goes to the chat reply or turns into a data fact. The pattern recognises these shapes: a self-reference such as 이 표 or 본 자료 followed by a negated claim verb, a claim noun plus 아님 (순위표 also takes 아니고), or 위한/-려는 것이 아님; a claim noun such as 비교 or 순위 followed by 처럼/(으)로 and a negated use or reading verb, including 말아 주세요; a purpose-용 noun such as 공개용 수치 followed by 아님; 숫자 or 수치 said to be incomparable (서로 비교할 수 없다) or compared among themselves toward a quoted verdict; a speed judgment or a quoted verdict forbidden for these numbers; ranking refused for this data (…만으로 순위를 정할 수 없다, 할 수 없는 것: 순위를 매기거나); and a table's row order said not to be a ranking. Use verbs after a self-reference, 근거, 평가, 증거 and 추천 stay out, and so do inference limits (결론 내릴 수 없다), operating rules (…근거로 사용하지 마십시오), conditional method facts (…면 비교할 수 없다), type-system rules (숫자 문자열을 그대로 비교하지 마십시오), reported speech and explainers (…면 안 된다는 반론), order notices with no table anchor, and the legal and financial notice verbs 대체하, 보장하 and 권유, because operating rules, method lines, manuals and standard notices end that way. A quote line that opens with a bold label (> **주의:** …) is read as the author's own callout; any other quote line is skipped.",
895
896
  "fixture_ids": [
896
897
  "ko-negative-pledge:positive",
897
898
  "ko-negative-pledge:negative"
@@ -906,7 +907,8 @@
906
907
  "scope": "document",
907
908
  "context": [
908
909
  "fenced_code",
909
- "quoted_user_text"
910
+ "quoted_user_text",
911
+ "blockquote_callout"
910
912
  ],
911
913
  "rationale": "Two sentences in one text that each end in 해석하지, 주장하지 or 대표하지 plus 않음, or in a claim noun plus 이/가 아님, turn a document into a list of disclaimers. The verbs 의미하, 간주하 and 인용하 and the use verbs stay out because repeated definitions, data-processing rules and operating rules end that way. A single such sentence is left to the warning-tier ko-status-negation.",
912
914
  "fixture_ids": [
@@ -918,14 +920,15 @@
918
920
  "id": "en-negative-pledge",
919
921
  "lang": "en",
920
922
  "severity": "block",
921
- "pattern": "\\b(?:this|these)\\s+(?:deck|report|document|analysis|table|chart|figure|figures|numbers|results|slide|comparison)\\s+(?:(?:is|are)\\s+not\\s+(?:intended|meant|to\\s+be\\s+(?:read|used|taken|interpreted))(?:\\s+to\\s+be\\s+(?:read|used|taken|interpreted))?\\s+as\\b(?!\\s*(?:a\\s+substitute|a\\s+replacement|(?:legal|investment|medical|professional)\\s+advice))\\s+(?:an?\\s+|the\\s+)?(?:[A-Za-z0-9-]+\\s+){0,4}?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)|(?:should|must)\\s+not\\s+be\\s+(?:read|used|taken|interpreted)\\s+as\\s+(?:an?\\s+|the\\s+)?(?:[A-Za-z0-9-]+\\s+){0,4}?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)|makes?\\s+no\\s+claims?\\s+(?:about|that|regarding|as\\s+to)\\b)|\\b(?:this|these)\\s+(?:is|are)\\s+not\\s+(?:intended|meant)\\s+as\\b(?!\\s*(?:a\\s+substitute|a\\s+replacement|(?:legal|investment|medical|professional)\\s+advice))\\s+(?:an?\\s+|the\\s+)?(?:[A-Za-z0-9-]+\\s+){0,4}?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)|\\bthe\\s+(?:deck|report|document|analysis)\\s+makes?\\s+no\\s+claims?\\s+(?:about|that|regarding|as\\s+to)\\b|(?:^|\\s[-*]\\s)\\s*(?:[-*]\\s+|\\d+\\.\\s+)?(?:[*_]{1,2})?not\\s+(?:intended|meant|to\\s+be\\s+read|to\\s+be\\s+used)\\s+as\\b(?!\\s*(?:a\\s+substitute|a\\s+replacement|(?:legal|investment|medical|professional)\\s+advice))\\s+(?:an?\\s+|the\\s+)?(?:[A-Za-z0-9-]+\\s+){0,4}?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)|\\b(?:this|these)\\s+(?:table|chart|figure|figures|numbers|results|comparison|analysis)\\s+(?:does|do)\\s+not\\s+constitute\\s+(?:an?\\s+)?[^.!?]{0,60}?\\b(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)",
923
+ "pattern": "\\b(?:this|these)\\s+(?:deck|report|document|analysis|table|chart|figure|figures|numbers|results|slide|comparison)\\s+(?:(?:is|are)\\s+not\\s+(?:intended|meant|to\\s+be\\s+(?:read|used|taken|interpreted))(?:\\s+to\\s+be\\s+(?:read|used|taken|interpreted))?\\s+as\\b(?!\\s*(?:a\\s+substitute|a\\s+replacement|(?:legal|investment|medical|professional)\\s+advice))\\s+(?:an?\\s+|the\\s+)?(?:[A-Za-z0-9-]+\\s+){0,4}?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)|(?:should|must)\\s+not\\s+be\\s+(?:read|used|taken|interpreted)\\s+as\\s+(?:an?\\s+|the\\s+)?(?:[A-Za-z0-9-]+\\s+){0,4}?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)|makes?\\s+no\\s+claims?\\s+(?:about|that|regarding|as\\s+to)\\b)|\\b(?:this|these)\\s+(?:is|are)\\s+not\\s+(?:intended|meant)\\s+as\\b(?!\\s*(?:a\\s+substitute|a\\s+replacement|(?:legal|investment|medical|professional)\\s+advice))\\s+(?:an?\\s+|the\\s+)?(?:[A-Za-z0-9-]+\\s+){0,4}?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)|\\bthe\\s+(?:deck|report|document|analysis)\\s+makes?\\s+no\\s+claims?\\s+(?:about|that|regarding|as\\s+to)\\b|(?:^|\\s[-*]\\s)\\s*(?:[-*]\\s+|\\d+\\.\\s+)?(?:[*_]{1,2})?not\\s+(?:intended|meant|to\\s+be\\s+read|to\\s+be\\s+used)\\s+as\\b(?!\\s*(?:a\\s+substitute|a\\s+replacement|(?:legal|investment|medical|professional)\\s+advice))\\s+(?:an?\\s+|the\\s+)?(?:[A-Za-z0-9-]+\\s+){0,4}?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)|\\b(?:this|these)\\s+(?:table|chart|figure|figures|numbers|results|comparison|analysis)\\s+(?:does|do)\\s+not\\s+constitute\\s+(?:an?\\s+)?[^.!?]{0,60}?\\b(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)|\\bthey\\s+are\\s+not\\s+(?:an?\\s+)?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)(?=\\s*(?:[.!?*_]|$))|\\b(?:do\\s+not|don[’']t)\\s+compare\\s+(?:these|those)\\s+(?:numbers|figures|results|values|scores|latencies)(?:\\s+(?:with|to)\\s+(?:each\\s+other|one\\s+another)\\b|(?=\\s*(?:[.!?*_]|$)))|\\b(?:these|those|the)\\s+(?:numbers|figures)\\s+are\\s+not\\s+comparable(?:\\s+(?:with|to)\\s+(?:each\\s+other|one\\s+another)\\b|(?=\\s*(?:[.!?*_]|$)))|\\b(?:this|the|these|our)\\s+(?:table|chart|figure|graph|list|report|document|deck|slide|page|summary|overview|analysis)s?\\s+(?:is|are)\\s+not\\s+(?:(?:meant|intended)\\s+)?(?:(?:to\\s+be\\s+)?(?:an?\\s+)?(?:ranking|benchmark|comparison|leaderboard|endorsement)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)(?=\\s*(?:[.!?*_]|$))|to\\s+(?:rank|compare|benchmark)\\b)|\\b(?:do\\s+not|don[’']t)\\s+read\\s+(?:this|the|these|our)\\s+(?:table|chart|figure|graph|list|report|document|deck|slide|page|summary|overview|analysis)s?\\s+as\\s+(?:an?\\s+)?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)",
922
924
  "flags": "i",
923
925
  "scope": "sentence",
924
926
  "context": [
925
927
  "fenced_code",
926
- "quoted_user_text"
928
+ "quoted_user_text",
929
+ "blockquote_callout"
927
930
  ],
928
- "rationale": "A sentence that says the deck, report, table or figures are not meant to be read as a comparison, benchmark, ranking or evaluation is a pledge to the reader, not a finding. The pattern needs a comparison noun, so the data-fact sentence about a sample that does not represent a population and investment-advice notices pass. A match followed by a substitute, replacement or professional advice is dropped, which keeps the standard legal notice. A sentence-initial 'Not to be read as' list item is covered too.",
931
+ "rationale": "A sentence that says the deck, report, table or figures are not meant to be read as a comparison, benchmark, ranking or evaluation is a pledge to the reader, not a finding. The pattern needs a comparison noun, so the data-fact sentence about a sample that does not represent a population and investment-advice notices pass. A match followed by a substitute, replacement or professional advice is dropped, which keeps the standard legal notice. A sentence-initial 'Not to be read as' list item is covered, and so are the short closers 'They are not a ranking.', 'Do not compare these numbers (with each other).' and 'The numbers are not comparable.', and a deliverable subject (this table, the report, our deck) that is not a ranking, is not meant to rank or compare, or is not to be read as a ranking. A refusal that continues into a description or an instruction (compare these values directly; …) is left alone, and a bare 'this' is not a subject. A bold-label quote callout is read as the author's own line.",
929
932
  "fixture_ids": [
930
933
  "en-negative-pledge:positive",
931
934
  "en-negative-pledge:negative"
@@ -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.23
488
+ Installed LitHermes 1.0.24
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.23)
505
+ [OK] plugin.yaml readable (version 1.0.24)
506
506
  [OK] skills bundled: 36
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.23",
3
+ "version": "1.0.24",
4
4
  "description": "Hermes Agent plugin that plans, builds, checks and hands off your work when you add one word, lit, to a request.",
5
5
  "keywords": [
6
6
  "hermes-agent",
@@ -1 +1 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.23"><title>release: 1.0.23</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.23</text></g></svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.24"><title>release: 1.0.24</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.24</text></g></svg>