diff --git a/packages/hermes-tlon-adapter/README.md b/packages/hermes-tlon-adapter/README.md index ff08d73a31..396b7070bd 100644 --- a/packages/hermes-tlon-adapter/README.md +++ b/packages/hermes-tlon-adapter/README.md @@ -293,6 +293,7 @@ The seven keys above are the full "dashboard edit works" set. Everything else Tl ``` *Harness*: **Hermes** +*Harness Version*: **0.17.0 (2026.6.19)** *Adapter Version*: **0.1.0** *Tlon Skill*: **0.3.2** *Fingerprint*: **fp1:3f9a2c1b8d02** @@ -300,6 +301,7 @@ The seven keys above are the full "dashboard edit works" set. Everything else Tl ``` - **Harness** — always `Hermes`; identifies which bot framework is running this node at a glance. +- **Harness Version** — the running Hermes Agent's own version, read from its `__version__` and `__release_date__` constants (the same source its `/version` command uses), with the installed distribution's metadata as a fallback. Reads `unknown` when the host reports neither, so the row is never dropped and the reply stays line-for-line comparable with OpenClaw's. - **Adapter Version** — semver from this package's `package.json`, bumped at releases. - **Tlon Skill** — version of the packaged `@tloncorp/tlon-skill` CLI (first line of `tlon --version`). - **Fingerprint** — sha256 over the runtime files (non-test `*.py`, `plugin.yaml`, `prompts/`), so copied or hand-patched installs are still identifiable. To match a fingerprint to a commit, recompute it at a candidate checkout: `python3 -c "import version; print(version.content_fingerprint())"` from this directory. diff --git a/packages/hermes-tlon-adapter/adapter.py b/packages/hermes-tlon-adapter/adapter.py index 885ff03b4e..342287e9ab 100644 --- a/packages/hermes-tlon-adapter/adapter.py +++ b/packages/hermes-tlon-adapter/adapter.py @@ -1052,6 +1052,7 @@ async def connect(self, *, is_reconnect: bool = False) -> bool: source=source, fingerprint=fingerprint, cli_version=cli_version, + harness_version=self._harness_version(), markdown=False, ).replace("\n", " | "), ) @@ -2272,6 +2273,7 @@ async def _version_reply(self) -> str: source=await git_source(), fingerprint=content_fingerprint(), cli_version=await self._cli_version(), + harness_version=self._harness_version(), ) async def _cli_version(self) -> str: diff --git a/packages/hermes-tlon-adapter/prompts/shared/owner-listen.md b/packages/hermes-tlon-adapter/prompts/shared/owner-listen.md index e127ed345f..e77eb9d836 100644 --- a/packages/hermes-tlon-adapter/prompts/shared/owner-listen.md +++ b/packages/hermes-tlon-adapter/prompts/shared/owner-listen.md @@ -5,7 +5,7 @@ The platform adapter handles these owner-only chat commands deterministically, s - `/owner-listen [on|off|status|list] [|<~host/group>]`, `/owner-listen all [on|off]`, `/owner-listen default [owned|all]` — owner-listen lets the owner be heard without a mention. Channels hosted by the bot or owner are on by default; any channel or whole group can be opted in or out, and `default all` extends the default to every monitored channel. - `/channel-access [open|restricted|status|list] []` — open lets anyone in that channel address the bot with a mention; restricted (default) limits it to authorized or approved ships. - `/pending`, `/allow `, `/reject `, `/ban `, `/unban ~ship`, `/banned` — access is deny-by-default. Unknown ships that DM the bot, mention it in restricted channels, or invite it to a group queue for owner approval (approving a group invite joins it). The owner gets a DM with an approval card; the buttons send these same commands. -- `/tlon version` — reports the running adapter version, source commit, content fingerprint, and `tlon` CLI version. (`/tlon-version` is a legacy alias for the same output.) +- `/tlon version` — reports the running Hermes harness version, adapter version, source commit, content fingerprint, and `tlon` CLI version. (`/tlon-version` is a legacy alias for the same output.) - `/tlon status storage` — reports image-upload storage debug info: node URL, whether it looks hosted, the `TLON_HOSTING` override, storage service, S3 credentials, `%genuine` reachability, and the resolved upload path. - `/tlon status binary` — identifies the exact `tlon` CLI the adapter invokes: version, a content hash (so two builds of one version are distinguishable), size, and build time. - `/tlon status telemetry`, `/tlon status telemetry test` — reports telemetry status (whether it is enabled and why, the PostHog identity events are sent under, and any delivery failures); `test` sends a test event and confirms whether PostHog accepted it. diff --git a/packages/hermes-tlon-adapter/test_adapter_owner_listen.py b/packages/hermes-tlon-adapter/test_adapter_owner_listen.py index d81861880e..b9c6c1a1a8 100644 --- a/packages/hermes-tlon-adapter/test_adapter_owner_listen.py +++ b/packages/hermes-tlon-adapter/test_adapter_owner_listen.py @@ -1167,6 +1167,11 @@ def test_migrate_command_from_non_owner_is_not_intercepted(self): def test_version_command_replies_with_field_lines(self): adapter = self.make_adapter({}) adapter._cli = FakeCLI() + # A value nothing else in the tree can produce, so the row can only be + # right if the reply is actually wired to the resolver. A regex on + # "some nonempty string" passes even when the wiring is dropped and + # every bot silently reports `unknown`. + adapter._harness_version_cache = "harness-sentinel-9.9.9" events = self.dispatches(adapter, channel_event("/tlon-version")) @@ -1174,13 +1179,14 @@ def test_version_command_replies_with_field_lines(self): self.assertEqual(len(adapter._cli.messages), 1) self.assertEqual(adapter._cli.messages[0][0], "chat/~pen/general") lines = adapter._cli.messages[0][1].splitlines() - self.assertEqual(len(lines), 5) + self.assertEqual(len(lines), 6) self.assertEqual(lines[0], "*Harness*: **Hermes**") - # exact version is covered in test_version; here we pin field + format - self.assertRegex(lines[1], r"^\*Adapter Version\*: \*\*.+\*\*$") - self.assertEqual(lines[2], "*Tlon Skill*: **0.3.2**") - self.assertRegex(lines[3], r"^\*Fingerprint\*: \*\*fp1:[0-9a-f]{12}\*\*$") - self.assertTrue(lines[4].startswith("*Source*: **")) + self.assertEqual(lines[1], "*Harness Version*: **harness-sentinel-9.9.9**") + # exact versions are covered in test_version; here we pin field + format + self.assertRegex(lines[2], r"^\*Adapter Version\*: \*\*.+\*\*$") + self.assertEqual(lines[3], "*Tlon Skill*: **0.3.2**") + self.assertRegex(lines[4], r"^\*Fingerprint\*: \*\*fp1:[0-9a-f]{12}\*\*$") + self.assertTrue(lines[5].startswith("*Source*: **")) self.assertIn(("--version",), adapter._cli.commands) def test_version_command_works_from_dm_and_with_mention(self): @@ -1223,13 +1229,15 @@ def test_version_command_reports_cli_failure(self): def test_tlon_version_subcommand(self): adapter = self.make_adapter({}) adapter._cli = FakeCLI() + adapter._harness_version_cache = "harness-sentinel-9.9.9" events = self.dispatches(adapter, channel_event("/tlon version")) self.assertEqual(events, []) lines = adapter._cli.messages[0][1].splitlines() self.assertEqual(lines[0], "*Harness*: **Hermes**") - self.assertRegex(lines[1], r"^\*Adapter Version\*: \*\*.+\*\*$") + self.assertEqual(lines[1], "*Harness Version*: **harness-sentinel-9.9.9**") + self.assertRegex(lines[2], r"^\*Adapter Version\*: \*\*.+\*\*$") def test_tlon_status_telemetry_subcommand(self): adapter = self.make_adapter({}) diff --git a/packages/hermes-tlon-adapter/test_version.py b/packages/hermes-tlon-adapter/test_version.py index ee91ec814f..3f769d9abf 100644 --- a/packages/hermes-tlon-adapter/test_version.py +++ b/packages/hermes-tlon-adapter/test_version.py @@ -117,11 +117,13 @@ def test_field_per_line_format(self): source="main @ abc1234 (clean)", fingerprint="fp1:0123456789ab", cli_version="0.3.2", + harness_version="0.17.0 (2026.6.19)", ) self.assertEqual( reply.splitlines(), [ "*Harness*: **Hermes**", + "*Harness Version*: **0.17.0 (2026.6.19)**", "*Adapter Version*: **0.1.0**", "*Tlon Skill*: **0.3.2**", "*Fingerprint*: **fp1:0123456789ab**", @@ -129,6 +131,24 @@ def test_field_per_line_format(self): ], ) + def test_absent_harness_version_renders_unknown(self): + # The row is never dropped: OpenClaw always emits it, and the README + # promises the two harnesses answer with the same field-per-line + # summary. A host that reports nothing degrades the value, not the + # shape. + for absent in (None, "", " "): + with self.subTest(harness_version=absent): + reply = version.format_version_reply( + adapter_version="0.1.0", + source=None, + fingerprint="fp1:0123456789ab", + cli_version="0.3.2", + harness_version=absent, + ) + self.assertEqual( + reply.splitlines()[1], "*Harness Version*: **unknown**" + ) + def test_plain_keys_for_logs(self): reply = version.format_version_reply( adapter_version="0.1.0", diff --git a/packages/hermes-tlon-adapter/version.py b/packages/hermes-tlon-adapter/version.py index 605e7737bb..f7d99e7977 100644 --- a/packages/hermes-tlon-adapter/version.py +++ b/packages/hermes-tlon-adapter/version.py @@ -125,11 +125,17 @@ def format_version_reply( source: Optional[str], fingerprint: str, cli_version: str, + harness_version: Optional[str] = None, markdown: bool = True, ) -> str: """Field-per-line version summary. For the chat reply keys are italicized and values bolded (Tlon markdown) for scannability; pass ``markdown=False`` - for plain log output.""" + for plain log output. + + ``harness_version`` is the running Hermes Agent's own version, taken by the + caller (the adapter resolves and caches it). It renders as ``unknown`` when + the host reports nothing, matching OpenClaw: the row always appears, so the + two harnesses' replies stay line-for-line comparable.""" def row(label: str, value: str) -> str: return f"*{label}*: **{value}**" if markdown else f"{label}: {value}" @@ -137,6 +143,7 @@ def row(label: str, value: str) -> str: return "\n".join( [ row("Harness", "Hermes"), + row("Harness Version", (harness_version or "").strip() or "unknown"), row("Adapter Version", adapter_version), row("Tlon Skill", cli_version), row("Fingerprint", fingerprint),