diff --git a/hermes_cli/curator.py b/hermes_cli/curator.py index 15b8048e4b..fb451c1d5c 100644 --- a/hermes_cli/curator.py +++ b/hermes_cli/curator.py @@ -246,7 +246,7 @@ def _cmd_list_unmanaged(args) -> int: return 0 print(f"unmanaged skills ({len(rows)}):") for r in sorted(rows, key=lambda x: x["name"]): - why = "created_by:null" if r.get("has_provenance_key") else "no marker" + why = f"created_by:{r.get('created_by') or 'null'}" if r.get("has_provenance_key") else "no marker" print( f" {r['name']:44s} activity={r.get('activity_count', 0):4d} " f"last_activity={_fmt_ts(r.get('last_activity_at')):14s} ({why})") diff --git a/tests/agent/test_learning_graph.py b/tests/agent/test_learning_graph.py index 8b68fc6f11..14ea7efceb 100644 --- a/tests/agent/test_learning_graph.py +++ b/tests/agent/test_learning_graph.py @@ -81,13 +81,22 @@ def test_full_payload_shape_and_edge_integrity(tmp_path): assert all("timestamp" in n for n in graph["nodes"]) -def test_learning_signal_predicate_includes_user_taught_skills(): - """created_by='learn' (foreground /learn, zero uses) is graph-worthy; an unmarked - unused skill is not; 'installed' alone is not a learning signal.""" - from agent.learning_graph import SkillNode, _has_learning_signal +def test_foreground_created_skill_is_in_journey_before_first_use(tmp_path): + """A skill created in the foreground (/learn, skill_manage) shows in the journey with zero + uses, while an unmarked never-used local skill (hand-written) stays out.""" + from tools import skill_usage - assert _has_learning_signal(SkillNode(name="a", category="x", created_by="learn")) - assert _has_learning_signal(SkillNode(name="b", category="x", created_by="agent")) - assert _has_learning_signal(SkillNode(name="c", category="x", use_count=3)) - assert not _has_learning_signal(SkillNode(name="d", category="x")) - assert not _has_learning_signal(SkillNode(name="e", category="x", created_by="installed")) + home = tmp_path / ".hermes" + for name in ("fresh-learn-skill", "hand-written"): + (home / "skills" / "demo" / name).mkdir(parents=True) + (home / "skills" / "demo" / name / "SKILL.md").write_text( + f"---\nname: {name}\ndescription: d.\n---\n\n# {name}\n", encoding="utf-8") + token = set_hermes_home_override(home) + try: + skill_usage.record_created("fresh-learn-skill", agent_created=False) + skill_nodes = {n["id"] for n in learning_graph.build_learning_graph()["nodes"] if n["kind"] == "skill"} + finally: + reset_hermes_home_override(token) + + assert "fresh-learn-skill" in skill_nodes + assert "hand-written" not in skill_nodes diff --git a/website/docs/user-guide/features/curator.md b/website/docs/user-guide/features/curator.md index 1827bc7ad6..11ffc76a4a 100644 --- a/website/docs/user-guide/features/curator.md +++ b/website/docs/user-guide/features/curator.md @@ -207,14 +207,17 @@ agent turns). The background fork runs with a write origin of `"background_revie `mark_agent_created()` call in `skill_manage`. Skills the foreground agent creates via `skill_manage(action="create")` during a -conversation are **not** marked as agent-created — they are considered -user-directed and the curator intentionally leaves them alone. +conversation (including `/learn`) are **not** marked as agent-created — they are +recorded as `created_by: learn`, which makes them show up in the +[learning journey](./memory.md#learning-journey-journey) right away but is not a +curator opt-in. They are considered user-directed and the curator intentionally +leaves them alone. :::warning Your hand-written skills are NOT curated If you manually created a `SKILL.md` or pointed Hermes at an external skill directory, that skill will have a `.usage.json` entry with `created_by: null` (or the field absent). The curator will not touch it. The same applies to -skills the foreground agent created at your request. +skills the foreground agent created at your request (`created_by: learn`). **To see which skills the curator actually manages**, run `hermes curator status`. If the agent-created count is 0, no skills are currently in the curator's @@ -245,8 +248,9 @@ for one of two reasons: - **pre-dates marker** — the record was written before `created_by` existed, so it carries no provenance signal at all. Authorship is genuinely unknowable from the record. -- **foreground-created** — a foreground `skill_manage(create)` left the marker - unset by design, since skills you ask for belong to you. +- **foreground-created** — a foreground `skill_manage(create)` recorded + `created_by: learn` (older records: unset) by design, since skills you ask for + belong to you. A large library can therefore look fully curated while most of it is untouchable. `adopt` closes that gap by **declaration**: diff --git a/website/docs/user-guide/features/memory.md b/website/docs/user-guide/features/memory.md index 6e252e6a1a..8eff471759 100644 --- a/website/docs/user-guide/features/memory.md +++ b/website/docs/user-guide/features/memory.md @@ -226,6 +226,8 @@ The learning journey is a timeline view of everything Hermes has learned — sav - **TUI** — `/journey` (aliases: `/learning`, `/memory-graph`) opens the timeline as an overlay. - **Desktop app** — `/journey` opens the Star Map / memory-graph panel, an interactive visual of the same nodes. +A skill appears on the timeline as soon as it has a learning signal: it was created in this profile (a `/learn` result or a foreground `skill_manage` create), created by the background review, or used at least once. Bundled skills and hand-written skills that have never been used stay out of the timeline. + Beyond viewing, the journey is also where you **prune and correct** what Hermes has learned: | Command | What it does |