fix(desktop): load property card guidance only on demand

This commit is contained in:
Teknium
2026-09-08 10:53:52 -07:00
parent ab6f4c5408
commit 78afbc3c37
7 changed files with 343 additions and 6 deletions

View File

@@ -667,12 +667,7 @@ PLATFORM_HINTS = {
"height live, width from the content's first measured span — lay content flush left with no centering wrappers "
"or it measures full-bleed. Widgets talk back: data-hermes-send=\"prompt\" on any clickable element (or "
"window.hermes.send(\"prompt\")) sends that prompt as a hidden user turn — answer it by updating the widget's "
"file, not with prose. Property/rental listings render as browsable cards: emit a ```listing fence "
"holding JSON — one object, or an array to compare several — with address (required), price, beds, "
"baths, size, note (why it is worth a look), facts[] (short specs), catches[] (risks to verify), "
"images[] (direct https photo URLs, in listing order — the first is the hero), and links[] "
"({label, url} detail pages, never a search-results URL). Use it for every property you present, "
"including follow-ups and re-rankings, so listings stay comparable."
"file, not with prose."
),
"sms": (
"You are communicating via SMS. Keep responses concise and use plain text only — no markdown, no "

View File

@@ -0,0 +1,76 @@
"""Offline prompt A/B and real optional catalog -> skill_view probe.
Run with the repository venv; tiktoken must be available. No model API calls.
"""
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
from types import SimpleNamespace
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
def main():
with tempfile.TemporaryDirectory(prefix="property-guidance-") as temporary:
os.environ["HERMES_HOME"] = temporary
os.environ["TERMINAL_CWD"] = temporary
os.chdir(temporary)
import tiktoken
from agent import prompt_builder
from agent.system_prompt import build_system_prompt
from tools.skills_hub_official import OptionalSkillSource
from tools.skills_tool import skill_view
agent = SimpleNamespace(
load_soul_identity=False, skip_context_files=True, valid_tool_names=[],
_task_completion_guidance=False, _tool_use_enforcement=False,
_environment_probe=False, _kanban_worker_guidance="", _memory_store=None,
_memory_manager=None, model="", provider="", platform="desktop",
pass_session_id=False, session_id="", _emit_status=lambda *args: None,
)
fixed_hint = prompt_builder.PLATFORM_HINTS["desktop"]
# The original clause is taken from the pinned base, never synthesized.
source = subprocess.check_output(
["git", "show", f"{sys.argv[1]}:agent/prompt_builder.py"], cwd=ROOT,
text=True, stdin=subprocess.DEVNULL,
)
import ast
tree = ast.parse(source)
mapping = next(n.value for n in tree.body if isinstance(n, ast.Assign)
and any(isinstance(t, ast.Name) and t.id == "PLATFORM_HINTS" for t in n.targets))
base_hint = next(ast.literal_eval(value) for key, value in zip(mapping.keys, mapping.values)
if isinstance(key, ast.Constant) and key.value == "desktop")
prompt_builder.PLATFORM_HINTS["desktop"] = base_hint
before = build_system_prompt(agent)
prompt_builder.PLATFORM_HINTS["desktop"] = fixed_hint
after = build_system_prompt(agent)
assert before.replace(base_hint, fixed_hint) == after
result = {"base": sys.argv[1], "tokenizers": {}}
for name in ("cl100k_base", "o200k_base"):
enc = tiktoken.get_encoding(name)
counts = [len(enc.encode(s)) for s in (base_hint, fixed_hint, before, after)]
result["tokenizers"][name] = dict(zip(
("hint_before", "hint_after", "prompt_before", "prompt_after"), counts))
optional = OptionalSkillSource()
matches = [m for m in optional.list_local() if "property" in m.tags and "rental" in m.tags]
assert matches
bundle = optional.fetch(matches[0].identifier)
assert bundle
dest = Path(temporary) / "skills" / bundle.name
dest.mkdir(parents=True)
for name, data in bundle.files.items():
(dest / name).write_bytes(data if isinstance(data, bytes) else data.encode("utf-8"))
loaded = json.loads(skill_view(bundle.name))
assert loaded["success"], loaded
example = loaded["content"].split("```listing\n", 1)[1].split("```", 1)[0]
assert json.loads(example)["address"]
result.update(identifier=matches[0].identifier, skill_view_success=True,
non_property_prompt_byte_parity=True, example=json.loads(example))
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,105 @@
---
name: property-listings
description: Present property and rental listings as desktop cards.
version: 0.1.0
author: Teknium (teknium1), Hermes Agent
license: MIT
platforms: [linux, macos, windows]
metadata:
hermes:
tags: [property, rental, real-estate, listings, desktop, cards]
category: productivity
related_skills: []
---
# Property Listings Skill
Present researched properties as browsable cards in the Hermes desktop transcript.
This is a presentation recipe, not a listing search service or an investment valuation.
## When to Use
- Presenting property or rental search results, comparing a shortlist, or re-ranking properties.
- Following up on a property already shown: keep using cards so the shortlist stays comparable.
- Outside the desktop app, use ordinary Markdown with source links instead; other clients need not render listing fences.
## Prerequisites
- A Hermes desktop conversation for native cards; the backend may be local or remote.
- Property details supplied by the user or verified through `web_search`, `web_extract`, or the browser tools available in this session.
- No additional API keys or dependencies are required for card formatting.
## How to Run
Install this optional skill through the Skills catalog, or use `terminal`:
```text
hermes skills install official/productivity/property-listings
```
Load it with `skill_view(name="property-listings")` when presenting listings.
Installing does not retrofit the running conversation's skill index; start a new
conversation for automatic discovery, or explicitly load the installed skill now.
## Quick Reference
Emit a fenced code block whose language is `listing` and whose body is valid JSON.
Use one object, an array of objects, or `{ "listings": [...] }` for a comparison.
| Field | Shape and meaning |
|---|---|
| `address` | Required nonempty street address or property headline. |
| `price` | Formatted string including currency and rental period, if applicable. |
| `beds`, `baths` | Positive numeric counts; omit unknown values. |
| `size` | Formatted area including units. |
| `note` | Why this property is worth a look. |
| `facts` | Array of short verified specs or amenities. |
| `catches` | Array of risks or questions to verify before a tour. |
| `images` | Direct HTTPS photo URLs in listing order; the first is the hero. |
| `links` | Array of `{ "label": "Source", "url": "https://..." }` detail-page links, not search-result URLs. |
## Procedure
1. Gather the address, price, specs, photos and canonical detail URL. Distinguish
verified facts from unknowns; do not invent prices, amenities, or photo URLs.
2. Deduplicate portal mirrors of the same property into one card, retaining useful
source links. Keep source dates and availability caveats in the surrounding prose.
3. Emit the `listing` fence for every property presented, including follow-ups and
re-rankings. Keep facts short and put unresolved concerns in `catches`.
4. Check the JSON before sending. This fictional format example illustrates all fields;
replace its values and example URLs with verified listing data:
```listing
{
"address": "12 Example Lane",
"price": "$2,400/mo",
"beds": 3,
"baths": 2.5,
"size": "1,600 sqft",
"note": "Fits the requested space and budget.",
"facts": ["12-month lease", "Covered parking"],
"catches": ["Verify pet policy and total move-in fees"],
"images": ["https://example.com/property/front.jpg", "https://example.com/property/kitchen.jpg"],
"links": [{"label": "Listing details", "url": "https://example.com/property/12"}]
}
```
## Pitfalls
- Cards are authored from gathered data, not fetched from a listing URL or embedded portal page.
- A sparse card needs only an address. Omit unknown fields rather than filling them with guesses.
- Use direct remote image URLs, not local paths, data URLs, or search-result pages.
Expired or blocked images disappear from the gallery; the text and links still matter.
- Keep a fence to at most 24 properties, 40 images per property, and 12 entries in
facts, catches and links. Text fields are truncated to 400 characters by the renderer.
- Malformed JSON or a card without identity falls back to a plain code block.
A valid card is not proof that the underlying listing is current or accurate.
## Verification
- Every presented property has an address and a verified source link; unknowns are explicit.
- Desktop displays the address, price, specs, facts, catches and links as a native card.
- Photos form a gallery; selecting a photo opens the lightbox. Three or more photos
use a hero-and-supporting-frames mosaic; additional photos remain browsable there.
- If the card fails to render, validate the fence language and JSON, then preserve a
readable Markdown fallback with the same facts and links.

View File

@@ -0,0 +1,38 @@
"""Property card recipes are optional, discoverable, and loadable on demand."""
import json
from pathlib import Path
from agent.prompt_builder import PLATFORM_HINTS
from tools.skills_hub_official import OptionalSkillSource
ROOT = Path(__file__).resolve().parents[2]
def test_property_recipe_is_not_paid_for_by_unrelated_desktop_sessions():
hint = PLATFORM_HINTS["desktop"]
assert "```listing" not in hint
assert "MEDIA:" in hint and "::preview" in hint
def test_optional_catalog_fetch_preserves_a_usable_property_recipe(tmp_path, monkeypatch):
source = OptionalSkillSource()
source._optional_dir = ROOT / "optional-skills"
matches = [m for m in source.list_local() if "property" in m.tags and "rental" in m.tags]
assert matches, "Property tasks must be discoverable in the optional catalog"
bundle = source.fetch(matches[0].identifier)
assert bundle is not None
from tools.skills_tool import skill_view
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
destination = tmp_path / "skills" / bundle.name
destination.mkdir(parents=True)
for name, data in bundle.files.items():
(destination / name).write_bytes(data if isinstance(data, bytes) else data.encode("utf-8"))
loaded = json.loads(skill_view(bundle.name))
assert loaded["success"], loaded
content = loaded["content"]
example = content.split("```listing\n", 1)[1].split("```", 1)[0]
listing = json.loads(example)
assert listing["address"] and listing["links"]
assert {"price", "beds", "baths", "size", "note", "facts", "catches", "images"} <= listing.keys()

View File

@@ -207,6 +207,7 @@ hermes skills uninstall <skill-name>
| [**decision-questionnaire**](/docs/user-guide/skills/optional/productivity/productivity-decision-questionnaire) | Turn an unanswerable decision into a questionnaire doc. |
| [**here-now**](/docs/user-guide/skills/optional/productivity/productivity-here-now) | Publish sites to &#123;slug&#125;.here.now and store files in Drives. |
| [**memento-flashcards**](/docs/user-guide/skills/optional/productivity/productivity-memento-flashcards) | Spaced-repetition flashcards: create, review, quiz, export. |
| [**property-listings**](/docs/user-guide/skills/optional/productivity/productivity-property-listings) | Present property and rental listings as desktop cards. |
| [**shop**](/docs/user-guide/skills/optional/productivity/productivity-shop) | Shop catalog search, checkout, order tracking, returns. |
| [**shopify**](/docs/user-guide/skills/optional/productivity/productivity-shopify) | Query Shopify Admin/Storefront GraphQL APIs via curl. |
| [**siyuan**](/docs/user-guide/skills/optional/productivity/productivity-siyuan) | Query and edit a SiYuan knowledge base via its API. |

View File

@@ -0,0 +1,121 @@
---
title: "Property Listings — Present property and rental listings as desktop cards"
sidebar_label: "Property Listings"
description: "Present property and rental listings as desktop cards"
---
{/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */}
# Property Listings
Present property and rental listings as desktop cards.
## Skill metadata
| | |
|---|---|
| Source | Optional — install with `hermes skills install official/productivity/property-listings` |
| Path | `optional-skills/productivity/property-listings` |
| Version | `0.1.0` |
| Author | Teknium (teknium1), Hermes Agent |
| License | MIT |
| Platforms | linux, macos, windows |
| Tags | `property`, `rental`, `real-estate`, `listings`, `desktop`, `cards` |
## Reference: full SKILL.md
:::info
The following is the complete skill definition that Hermes loads when this skill is triggered. This is what the agent sees as instructions when the skill is active.
:::
# Property Listings Skill
Present researched properties as browsable cards in the Hermes desktop transcript.
This is a presentation recipe, not a listing search service or an investment valuation.
## When to Use
- Presenting property or rental search results, comparing a shortlist, or re-ranking properties.
- Following up on a property already shown: keep using cards so the shortlist stays comparable.
- Outside the desktop app, use ordinary Markdown with source links instead; other clients need not render listing fences.
## Prerequisites
- A Hermes desktop conversation for native cards; the backend may be local or remote.
- Property details supplied by the user or verified through `web_search`, `web_extract`, or the browser tools available in this session.
- No additional API keys or dependencies are required for card formatting.
## How to Run
Install this optional skill through the Skills catalog, or use `terminal`:
```text
hermes skills install official/productivity/property-listings
```
Load it with `skill_view(name="property-listings")` when presenting listings.
Installing does not retrofit the running conversation's skill index; start a new
conversation for automatic discovery, or explicitly load the installed skill now.
## Quick Reference
Emit a fenced code block whose language is `listing` and whose body is valid JSON.
Use one object, an array of objects, or `{ "listings": [...] }` for a comparison.
| Field | Shape and meaning |
|---|---|
| `address` | Required nonempty street address or property headline. |
| `price` | Formatted string including currency and rental period, if applicable. |
| `beds`, `baths` | Positive numeric counts; omit unknown values. |
| `size` | Formatted area including units. |
| `note` | Why this property is worth a look. |
| `facts` | Array of short verified specs or amenities. |
| `catches` | Array of risks or questions to verify before a tour. |
| `images` | Direct HTTPS photo URLs in listing order; the first is the hero. |
| `links` | Array of `{ "label": "Source", "url": "https://..." }` detail-page links, not search-result URLs. |
## Procedure
1. Gather the address, price, specs, photos and canonical detail URL. Distinguish
verified facts from unknowns; do not invent prices, amenities, or photo URLs.
2. Deduplicate portal mirrors of the same property into one card, retaining useful
source links. Keep source dates and availability caveats in the surrounding prose.
3. Emit the `listing` fence for every property presented, including follow-ups and
re-rankings. Keep facts short and put unresolved concerns in `catches`.
4. Check the JSON before sending. This fictional format example illustrates all fields;
replace its values and example URLs with verified listing data:
```listing
{
"address": "12 Example Lane",
"price": "$2,400/mo",
"beds": 3,
"baths": 2.5,
"size": "1,600 sqft",
"note": "Fits the requested space and budget.",
"facts": ["12-month lease", "Covered parking"],
"catches": ["Verify pet policy and total move-in fees"],
"images": ["https://example.com/property/front.jpg", "https://example.com/property/kitchen.jpg"],
"links": [{"label": "Listing details", "url": "https://example.com/property/12"}]
}
```
## Pitfalls
- Cards are authored from gathered data, not fetched from a listing URL or embedded portal page.
- A sparse card needs only an address. Omit unknown fields rather than filling them with guesses.
- Use direct remote image URLs, not local paths, data URLs, or search-result pages.
Expired or blocked images disappear from the gallery; the text and links still matter.
- Keep a fence to at most 24 properties, 40 images per property, and 12 entries in
facts, catches and links. Text fields are truncated to 400 characters by the renderer.
- Malformed JSON or a card without identity falls back to a plain code block.
A valid card is not proof that the underlying listing is current or accurate.
## Verification
- Every presented property has an address and a verified source link; unknowns are explicit.
- Desktop displays the address, price, specs, facts, catches and links as a native card.
- Photos form a gallery; selecting a photo opens the lightbox. Three or more photos
use a hero-and-supporting-frames mosaic; additional photos remain browsable there.
- If the card fails to render, validate the fence language and JSON, then preserve a
readable Markdown fallback with the same facts and links.

View File

@@ -538,6 +538,7 @@ const sidebars: SidebarsConfig = {
'user-guide/skills/optional/productivity/productivity-decision-questionnaire',
'user-guide/skills/optional/productivity/productivity-here-now',
'user-guide/skills/optional/productivity/productivity-memento-flashcards',
'user-guide/skills/optional/productivity/productivity-property-listings',
'user-guide/skills/optional/productivity/productivity-shop',
'user-guide/skills/optional/productivity/productivity-shopify',
'user-guide/skills/optional/productivity/productivity-siyuan',