From 42eef18771ce527df5699fbecbb4e01ca4131858 Mon Sep 17 00:00:00 2001 From: Jer Miller Date: Sun, 19 Apr 2026 11:23:53 -0600 Subject: [PATCH] talent: retire legacy skills observer (Lode B) Deletes talent/skills.{md,py} and tests/test_skills_hook.py. The new apps/skills/talent pair (skill_observer + skill_editor) plus apps/skills/call.py now owns the full skills pipeline end to end. tests/test_journal_skill.py is unrelated (tests the journal SKILL.md symlinks) and is preserved. Part of Lode B of the skills-observer-editor refactor. --- talent/skills.md | 32 --- talent/skills.py | 547 -------------------------------------- tests/test_skills_hook.py | 503 ----------------------------------- 3 files changed, 1082 deletions(-) delete mode 100644 talent/skills.md delete mode 100644 talent/skills.py delete mode 100644 tests/test_skills_hook.py diff --git a/talent/skills.md b/talent/skills.md deleted file mode 100644 index 79754d306..000000000 --- a/talent/skills.md +++ /dev/null @@ -1,32 +0,0 @@ -{ - "type": "generate", - "title": "Skill Observer", - "description": "Detects recurring activity patterns and generates structured skill documents describing what the owner does, how, and why.", - "hook": {"pre": "skills", "post": "skills"}, - "schedule": "activity", - "activities": ["*"], - "priority": 90, - "output": "json", - "load": {"transcripts": false, "percepts": false, "talents": false} -} - -You are analyzing recurring activity patterns to identify and document the owner's skills. - -$skill_instruction - -$pattern_context - -$previous_outputs - -Return JSON with these fields: -- `skill_name` (string) -- `slug` (string, must match the canonical slug provided in context) -- `category` (string) -- `description` (string, 1-2 sentences) -- `how` (string, one paragraph) -- `why` (string, one paragraph) -- `tools` (list of strings) -- `collaborators` (list of strings) -- `confidence` (float from 0.0 to 1.0) - -Stay grounded in the supplied evidence. Do not fabricate tools, collaborators, or behaviors that are not supported by the observations and prior outputs. diff --git a/talent/skills.py b/talent/skills.py deleted file mode 100644 index 71d03fd94..000000000 --- a/talent/skills.py +++ /dev/null @@ -1,547 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -import json -import logging -import re -from datetime import datetime, timezone -from pathlib import Path - -from think.activities import get_activity_output_path -from think.entities.core import atomic_write -from think.identity import update_identity_section -from think.utils import get_journal - -logger = logging.getLogger(__name__) - -STOPWORDS = frozenset( - { - "a", - "an", - "the", - "and", - "or", - "but", - "in", - "on", - "at", - "to", - "for", - "of", - "with", - "by", - "from", - "as", - "is", - "was", - "are", - "were", - "be", - "been", - "being", - "have", - "has", - "had", - "do", - "does", - "did", - "will", - "would", - "could", - "should", - "may", - "might", - "shall", - "can", - "need", - "not", - "no", - "nor", - "so", - "if", - "then", - "than", - "too", - "very", - "just", - "about", - "above", - "after", - "before", - "between", - "into", - "through", - "during", - "out", - "off", - "over", - "under", - "again", - "further", - "once", - "here", - "there", - "when", - "where", - "why", - "how", - "all", - "each", - "every", - "both", - "few", - "more", - "most", - "other", - "some", - "such", - "only", - "own", - "same", - "also", - "that", - "this", - "these", - "those", - "what", - "which", - "who", - "whom", - "its", - "his", - "her", - "their", - "our", - "your", - "any", - "it", - "he", - "she", - "they", - "we", - "you", - "me", - "him", - "them", - "us", - "my", - "up", - } -) -AGENT_OUTPUT_KEYS = ["decisions", "followups", "meetings", "messaging"] -MATCH_THRESHOLD = 0.3 - - -def _skills_dir(facet: str) -> Path: - path = Path(get_journal()) / "facets" / facet / "skills" - path.mkdir(parents=True, exist_ok=True) - return path - - -def _patterns_path(facet: str) -> Path: - return _skills_dir(facet) / "patterns.jsonl" - - -def _load_patterns(facet: str) -> list[dict]: - path = _patterns_path(facet) - if not path.exists(): - return [] - - patterns = [] - with open(path, "r", encoding="utf-8") as f: - for lineno, line in enumerate(f, start=1): - line = line.strip() - if not line: - continue - try: - patterns.append(json.loads(line)) - except json.JSONDecodeError: - logger.warning("skills: malformed patterns.jsonl line %s", lineno) - return patterns - - -def _write_patterns(facet: str, patterns: list[dict]) -> None: - content = "" - if patterns: - content = "\n".join( - json.dumps(pattern, ensure_ascii=False) for pattern in patterns - ) - content += "\n" - atomic_write(_patterns_path(facet), content) - - -def _extract_keywords(text: str) -> list[str]: - keywords = set() - for token in re.split(r"[^a-z0-9]+", text.lower()): - if len(token) < 3 or token in STOPWORDS or token.isdigit(): - continue - keywords.add(token) - return sorted(keywords) - - -def _normalize_entities(entities: list) -> list[str]: - normalized = set() - for entity in entities: - value = str(entity).strip().lower() - if value: - normalized.add(value) - return sorted(normalized) - - -def _slugify(text: str) -> str: - text = re.sub(r"[^a-z0-9]+", "-", text.lower()) - text = re.sub(r"-+", "-", text) - return text.strip("-") - - -def _make_slug(activity_type: str, entities: list[str], keywords: list[str]) -> str: - import hashlib - - tokens = [_slugify(activity_type)] - tokens.extend(_slugify(entity) for entity in entities[:2]) - tokens.extend(_slugify(keyword) for keyword in keywords[:3]) - slug = "-".join(token for token in tokens if token) - slug = _slugify(slug) - if not entities and not keywords: - base = _slugify(activity_type) or "activity" - slug = f"{base}-pattern" - elif not slug: - base = _slugify(activity_type) or "activity" - slug = f"{base}-pattern" - if len(slug) > 80: - digest = hashlib.md5(slug.encode("utf-8")).hexdigest()[:8] - slug = f"{slug[:70].rstrip('-')}-{digest}" - return slug - - -def _jaccard(a: set, b: set) -> float: - union = a | b - if not union: - return 0.0 - return len(a & b) / len(union) - - -def _pattern_score( - incoming_entities: set, incoming_keywords: set, pattern: dict -) -> float | None: - scores = [] - pattern_entities = set(pattern.get("entities", [])) - pattern_keywords = set(pattern.get("keywords", [])) - - if incoming_entities and pattern_entities: - scores.append(_jaccard(incoming_entities, pattern_entities)) - if incoming_keywords and pattern_keywords: - scores.append(_jaccard(incoming_keywords, pattern_keywords)) - if not scores: - return None - - combined = sum(scores) / len(scores) - if combined >= MATCH_THRESHOLD: - return combined - return None - - -def _find_best_match( - activity_type: str, entities: set, keywords: set, patterns: list[dict] -) -> dict | None: - best_match = None - best_score = -1.0 - - for pattern in patterns: - if pattern.get("activity_type") != activity_type: - continue - score = _pattern_score(entities, keywords, pattern) - if score is None: - continue - if score > best_score: - best_score = score - best_match = pattern - - return best_match - - -def _make_observation( - day: str, - activity_id: str, - description: str, - entities: list[str], - keywords: list[str], -) -> dict: - return { - "day": day, - "activity_id": activity_id, - "description": description, - "entities": entities, - "keywords": keywords, - } - - -def _format_pattern_context(pattern: dict) -> str: - observations = pattern.get("observations", []) - first_seen = observations[0]["day"] if observations else "" - last_seen = observations[-1]["day"] if observations else "" - lines = [ - "## Pattern Context", - f"Canonical slug: {pattern.get('id', '')}", - f"Activity type: {pattern.get('activity_type', '')}", - f"Observation count: {len(observations)}", - f"First seen: {first_seen}", - f"Last seen: {last_seen}", - f"Merged entities: {', '.join(pattern.get('entities', [])) or '[none]'}", - f"Merged keywords: {', '.join(pattern.get('keywords', [])) or '[none]'}", - "", - "Observations:", - ] - - for obs in observations: - description = obs.get("description", "") or "[no description]" - entities = ", ".join(obs.get("entities", [])) or "[none]" - lines.append(f"- {obs.get('day', '')}: {description}") - lines.append(f" entities: {entities}") - - return "\n".join(lines) - - -def _load_previous_outputs(facet: str, observations: list[dict]) -> str: - if not observations: - return "No prior agent outputs available." - - lines = ["## Prior Agent Outputs"] - found_any = False - - for obs in observations[-3:]: - obs_lines = [] - for key in AGENT_OUTPUT_KEYS: - path = get_activity_output_path(facet, obs["day"], obs["activity_id"], key) - try: - content = path.read_text(encoding="utf-8").strip() - except (FileNotFoundError, OSError): - continue - if not content: - continue - found_any = True - obs_lines.append(f"### {key} ({path.name})") - obs_lines.append(content[:2000]) - obs_lines.append("") - - if obs_lines: - lines.append(f"Observation {obs['day']} / {obs['activity_id']}") - lines.append(obs.get("description", "") or "[no description]") - lines.append("") - lines.extend(obs_lines) - - if not found_any: - return "No prior agent outputs available." - - return "\n".join(lines).rstrip() - - -def _read_agency_observations() -> str: - """Read the current ## observations section from agency.md.""" - try: - path = Path(get_journal()) / "identity" / "agency.md" - text = path.read_text(encoding="utf-8") - except (FileNotFoundError, OSError): - return "" - lines = text.split("\n") - start = None - for i, line in enumerate(lines): - if line == "## observations": - start = i + 1 - elif start is not None and line.startswith("## "): - return "\n".join(lines[start:i]).strip() - if start is not None: - return "\n".join(lines[start:]).strip() - return "" - - -def _skill_instruction(mode: str) -> str: - if mode == "comparison": - return ( - "Compare these two activity observations. Identify whether they reflect " - "the same recurring skill or capability. Draft a provisional skill " - "profile based on the evidence so far. Be conservative — only claim " - "what both observations support." - ) - if mode == "refresh": - return ( - "Update this skill profile with new evidence from the latest " - "observation. Preserve the core skill identity. Incorporate new " - "details about tools, collaborators, or techniques observed. The " - "slug must remain unchanged." - ) - return ( - "Synthesize a complete skill profile from these recurring activity " - "observations. You have 3+ observations of this pattern. Produce a " - "thorough, grounded skill document that captures what the owner does, " - "how they do it, and why." - ) - - -def pre_process(context: dict) -> dict | None: - facet = context.get("facet") - day = context.get("day") - activity = context.get("activity") - if not facet or not day or not activity: - return None - - activity_type = activity.get("activity") - activity_id = activity.get("id") - if not activity_type or not activity_id: - return None - - description = activity.get("description", "") - entities = _normalize_entities(activity.get("active_entities", [])) - keywords = _extract_keywords(description) - patterns = _load_patterns(facet) - match = _find_best_match(activity_type, set(entities), set(keywords), patterns) - now_iso = datetime.now(timezone.utc).isoformat() - obs = _make_observation(day, activity_id, description, entities, keywords) - - if match is None: - if not entities and not keywords: - return {"skip_reason": "no signal to seed pattern"} - pattern = { - "id": _make_slug(activity_type, entities, keywords), - "activity_type": activity_type, - "keywords": keywords, - "entities": entities, - "observations": [obs], - "created_at": now_iso, - "updated_at": now_iso, - "skill_generated": False, - } - patterns.append(pattern) - _write_patterns(facet, patterns) - return {"skip_reason": "first observation, seeded pattern"} - - match.setdefault("observations", []).append(obs) - match["entities"] = sorted(set(match.get("entities", [])) | set(entities)) - match["keywords"] = sorted(set(match.get("keywords", [])) | set(keywords)) - match["updated_at"] = now_iso - _write_patterns(facet, patterns) - - observation_count = len(match["observations"]) - if observation_count == 2: - mode = "comparison" - elif not match.get("skill_generated", False): - mode = "generate" - else: - mode = "refresh" - - return { - "template_vars": { - "skill_instruction": _skill_instruction(mode), - "pattern_context": _format_pattern_context(match), - "previous_outputs": _load_previous_outputs( - facet, match["observations"][:-1] - ), - }, - "meta": { - "pattern_id": match["id"], - "facet": facet, - "mode": mode, - }, - } - - -def post_process(result: str, context: dict) -> str | None: - try: - data = json.loads(result) - except json.JSONDecodeError: - logger.warning("skills: could not parse result as JSON") - return None - - if not isinstance(data, dict): - logger.warning("skills: expected JSON object result") - return None - - meta = context.get("meta") or {} - pattern_id = meta.get("pattern_id") - facet = meta.get("facet") - mode = meta.get("mode") - if not pattern_id or not facet or not mode: - logger.warning("skills: missing required post-process metadata") - return None - - if mode == "comparison": - return None - - patterns = _load_patterns(facet) - pattern = next((item for item in patterns if item.get("id") == pattern_id), None) - if pattern is None: - logger.warning("skills: pattern %s not found for facet %s", pattern_id, facet) - return None - - was_new = not pattern.get("skill_generated", False) - pattern["skill_generated"] = True - pattern["updated_at"] = datetime.now(timezone.utc).isoformat() - _write_patterns(facet, patterns) - - observations = pattern.get("observations", []) - first_seen = observations[0]["day"] if observations else "" - last_seen = observations[-1]["day"] if observations else "" - collaborators = sorted( - {str(item) for item in data.get("collaborators", []) if item} - ) - tools = sorted({str(item) for item in data.get("tools", []) if item}) - - frontmatter = [ - "---", - f'name: "{str(data.get("skill_name", pattern_id)).replace(chr(34), chr(39))}"', - f'slug: "{pattern_id}"', - f'category: "{str(data.get("category", "")).replace(chr(34), chr(39))}"', - f"confidence: {data.get('confidence', 0.0)}", - f"observations: {len(observations)}", - f'first_seen: "{first_seen}"', - f'last_seen: "{last_seen}"', - "collaborators:", - ] - if collaborators: - frontmatter.extend( - f' - "{item.replace(chr(34), chr(39))}"' for item in collaborators - ) - else: - frontmatter.append(" []") - frontmatter.append("tools:") - if tools: - frontmatter.extend(f' - "{item.replace(chr(34), chr(39))}"' for item in tools) - else: - frontmatter.append(" []") - frontmatter.append("---") - frontmatter.append("") - frontmatter.append("## Description") - frontmatter.append("") - frontmatter.append(str(data.get("description", "")).strip()) - frontmatter.append("") - frontmatter.append("## How") - frontmatter.append("") - frontmatter.append(str(data.get("how", "")).strip()) - frontmatter.append("") - frontmatter.append("## Why") - frontmatter.append("") - frontmatter.append(str(data.get("why", "")).strip()) - frontmatter.append("") - - skill_path = _skills_dir(facet) / f"{pattern_id}.md" - atomic_write(skill_path, "\n".join(frontmatter)) - - if was_new: - skill_name = data.get("skill_name", pattern_id) - new_line = f"- Noticed recurring skill: {skill_name} — observed {len(observations)} times in {facet}" - existing = _read_agency_observations() - if existing and existing.strip() != "[watching and learning]": - content = existing.rstrip("\n") + "\n" + new_line - else: - content = new_line - update_identity_section( - "agency.md", - "observations", - content, - actor="agency-observations-tender", - reason="agency observations refresh", - ) - - return None diff --git a/tests/test_skills_hook.py b/tests/test_skills_hook.py deleted file mode 100644 index 78cc8c13b..000000000 --- a/tests/test_skills_hook.py +++ /dev/null @@ -1,503 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -import json -import os -from pathlib import Path -from unittest.mock import patch - -from talent.skills import ( - MATCH_THRESHOLD, - _extract_keywords, - _find_best_match, - _jaccard, - _load_patterns, - _make_slug, - _normalize_entities, - _pattern_score, - _write_patterns, - post_process, - pre_process, -) - -os.environ.setdefault("_SOLSTONE_JOURNAL_OVERRIDE", "tests/fixtures/journal") - - -_DEFAULT_ENTITIES = ["Alice", "Bob"] - - -def _make_context( - facet="test-facet", - day="20260410", - activity_type="meeting", - activity_id="act-001", - description="Weekly standup with engineering team", - entities=None, -): - return { - "facet": facet, - "day": day, - "activity": { - "activity": activity_type, - "id": activity_id, - "description": description, - "active_entities": _DEFAULT_ENTITIES if entities is None else entities, - }, - } - - -SAMPLE_LLM_RESULT = json.dumps( - { - "skill_name": "Engineering Standup Facilitation", - "slug": "meeting-alice-bob-standup-engineering", - "category": "communication", - "description": "Facilitates regular engineering standup meetings.", - "how": "Runs structured standups covering blockers and progress.", - "why": "Keeps the engineering team aligned and unblocked.", - "tools": ["Zoom", "Jira"], - "collaborators": ["Alice", "Bob"], - "confidence": 0.85, - } -) - - -class TestKeywordExtraction: - def test_basic_extraction(self): - result = _extract_keywords("Weekly standup with engineering team") - assert "weekly" in result - assert "standup" in result - assert "engineering" in result - assert "team" in result - - def test_stopword_removal(self): - result = _extract_keywords("the quick brown fox") - assert "the" not in result - - def test_short_words_removed(self): - assert _extract_keywords("a an is") == [] - - def test_numeric_removed(self): - result = _extract_keywords("meeting 123 items") - assert "123" not in result - - def test_empty_string(self): - assert _extract_keywords("") == [] - - -class TestNormalizeEntities: - def test_basic(self): - assert _normalize_entities(["Alice", "Bob"]) == ["alice", "bob"] - - def test_dedup(self): - assert _normalize_entities(["Alice", "alice"]) == ["alice"] - - def test_non_string(self): - assert _normalize_entities([123]) == ["123"] - - -class TestJaccard: - def test_identical(self): - assert _jaccard({1, 2, 3}, {1, 2, 3}) == 1.0 - - def test_disjoint(self): - assert _jaccard({1, 2}, {3, 4}) == 0.0 - - def test_partial(self): - assert _jaccard({1, 2, 3}, {2, 3, 4}) == 0.5 - - def test_empty(self): - assert _jaccard(set(), set()) == 0.0 - - -class TestMakeSlug: - def test_basic(self): - assert _make_slug("meeting", ["alice"], ["standup"]) == "meeting-alice-standup" - - def test_special_chars(self): - slug = _make_slug("Team Meeting!", ["Alice Smith"], ["Q2/Planning"]) - assert slug == "team-meeting-alice-smith-q2-planning" - - def test_truncation(self): - slug = _make_slug( - "meeting", - ["a" * 40, "b" * 40], - ["c" * 40, "d" * 40, "e" * 40], - ) - assert len(slug) <= 80 - - def test_empty_fallback(self): - assert _make_slug("meeting", [], []) == "meeting-pattern" - - -class TestPatternScore: - def test_matching_entities(self): - pattern = { - "activity_type": "meeting", - "entities": ["alice", "bob"], - "keywords": ["engineering", "standup"], - } - score = _pattern_score( - {"alice", "carol"}, - {"engineering", "weekly"}, - pattern, - ) - assert score is not None - assert score >= MATCH_THRESHOLD - - def test_no_overlap(self): - pattern = { - "activity_type": "meeting", - "entities": ["alice"], - "keywords": ["standup"], - } - assert _pattern_score({"carol"}, {"planning"}, pattern) is None - - def test_exact_type_required(self): - patterns = [ - { - "id": "meeting-alice", - "activity_type": "meeting", - "entities": ["alice"], - "keywords": ["standup"], - } - ] - match = _find_best_match("email", {"alice"}, {"standup"}, patterns) - assert match is None - - def test_entity_only_signal(self): - pattern = {"activity_type": "meeting", "entities": ["alice"], "keywords": []} - score = _pattern_score({"alice"}, set(), pattern) - assert score == 1.0 - - -class TestPreProcess: - def test_novel_activity_seeds_pattern_and_skips(self, tmp_path): - with patch("talent.skills.get_journal", return_value=str(tmp_path)): - result = pre_process(_make_context()) - patterns = _load_patterns("test-facet") - - assert result == {"skip_reason": "first observation, seeded pattern"} - assert len(patterns) == 1 - assert patterns[0]["activity_type"] == "meeting" - assert len(patterns[0]["observations"]) == 1 - - def test_second_observation_returns_template_vars(self, tmp_path): - with patch("talent.skills.get_journal", return_value=str(tmp_path)): - pre_process(_make_context()) - result = pre_process( - _make_context( - day="20260411", - activity_id="act-002", - description="Weekly engineering standup with Alice and Bob", - ) - ) - - assert "template_vars" in result - assert "meta" in result - assert result["meta"]["mode"] == "comparison" - assert { - "skill_instruction", - "pattern_context", - "previous_outputs", - } <= set(result["template_vars"]) - - def test_third_observation_triggers_generation(self, tmp_path): - with patch("talent.skills.get_journal", return_value=str(tmp_path)): - pre_process(_make_context()) - pre_process( - _make_context( - day="20260411", - activity_id="act-002", - description="Weekly engineering standup with Alice and Bob", - ) - ) - result = pre_process( - _make_context( - day="20260412", - activity_id="act-003", - description="Weekly engineering standup and blocker review", - ) - ) - - assert result["meta"]["mode"] == "generate" - - def test_no_signal_activity_skips(self, tmp_path): - with patch("talent.skills.get_journal", return_value=str(tmp_path)): - result = pre_process(_make_context(entities=[], description="")) - patterns = _load_patterns("test-facet") - - assert result == {"skip_reason": "no signal to seed pattern"} - assert len(patterns) == 0 - - def test_entities_and_keywords_merged(self, tmp_path): - with patch("talent.skills.get_journal", return_value=str(tmp_path)): - pre_process( - _make_context( - entities=["Alice", "Bob"], - description="Weekly standup with engineering team", - ) - ) - pre_process( - _make_context( - day="20260411", - activity_id="act-002", - entities=["Alice", "Carol"], - description="Weekly standup with product team", - ) - ) - patterns = _load_patterns("test-facet") - - assert patterns[0]["entities"] == ["alice", "bob", "carol"] - assert "engineering" in patterns[0]["keywords"] - assert "product" in patterns[0]["keywords"] - - -class TestPostProcess: - def _seed_pattern(self, tmp_path, skill_generated=False): - pattern = { - "id": "meeting-alice-bob-standup-engineering", - "activity_type": "meeting", - "keywords": ["engineering", "standup", "weekly"], - "entities": ["alice", "bob"], - "observations": [ - { - "day": "20260410", - "activity_id": "act-001", - "description": "Weekly standup with engineering team", - "entities": ["alice", "bob"], - "keywords": ["engineering", "standup", "weekly"], - }, - { - "day": "20260411", - "activity_id": "act-002", - "description": "Weekly engineering standup with Alice and Bob", - "entities": ["alice", "bob"], - "keywords": ["engineering", "standup", "weekly"], - }, - { - "day": "20260412", - "activity_id": "act-003", - "description": "Weekly engineering standup and blocker review", - "entities": ["alice", "bob"], - "keywords": [ - "blocker", - "engineering", - "review", - "standup", - "weekly", - ], - }, - ], - "created_at": "2026-04-10T00:00:00+00:00", - "updated_at": "2026-04-12T00:00:00+00:00", - "skill_generated": skill_generated, - } - with patch("talent.skills.get_journal", return_value=str(tmp_path)): - _write_patterns("test-facet", [pattern]) - - def test_comparison_mode_is_noop(self, tmp_path): - self._seed_pattern(tmp_path) - with patch("talent.skills.get_journal", return_value=str(tmp_path)): - result = post_process( - SAMPLE_LLM_RESULT, - { - "meta": { - "pattern_id": "meeting-alice-bob-standup-engineering", - "facet": "test-facet", - "mode": "comparison", - } - }, - ) - - assert result is None - skill_path = ( - Path(tmp_path) - / "facets" - / "test-facet" - / "skills" - / "meeting-alice-bob-standup-engineering.md" - ) - assert not skill_path.exists() - - def test_generates_skill_document(self, tmp_path): - self._seed_pattern(tmp_path) - with ( - patch("talent.skills.get_journal", return_value=str(tmp_path)), - patch("talent.skills.update_identity_section"), - ): - post_process( - SAMPLE_LLM_RESULT, - { - "meta": { - "pattern_id": "meeting-alice-bob-standup-engineering", - "facet": "test-facet", - "mode": "generate", - } - }, - ) - - skill_path = ( - Path(tmp_path) - / "facets" - / "test-facet" - / "skills" - / "meeting-alice-bob-standup-engineering.md" - ) - content = skill_path.read_text(encoding="utf-8") - assert content.startswith("---\n") - assert 'slug: "meeting-alice-bob-standup-engineering"' in content - assert "## Description" in content - assert "## How" in content - assert "## Why" in content - - def test_writes_patterns_atomically(self, tmp_path): - self._seed_pattern(tmp_path) - with ( - patch("talent.skills.get_journal", return_value=str(tmp_path)), - patch("talent.skills.update_identity_section"), - ): - post_process( - SAMPLE_LLM_RESULT, - { - "meta": { - "pattern_id": "meeting-alice-bob-standup-engineering", - "facet": "test-facet", - "mode": "generate", - } - }, - ) - patterns = _load_patterns("test-facet") - - assert patterns[0]["skill_generated"] is True - assert patterns[0]["updated_at"] != "2026-04-12T00:00:00+00:00" - - def test_agency_notification(self, tmp_path): - self._seed_pattern(tmp_path) - with ( - patch("talent.skills.get_journal", return_value=str(tmp_path)), - patch("talent.skills.update_identity_section") as mock_update, - patch("talent.skills._read_agency_observations", return_value=""), - ): - post_process( - SAMPLE_LLM_RESULT, - { - "meta": { - "pattern_id": "meeting-alice-bob-standup-engineering", - "facet": "test-facet", - "mode": "generate", - } - }, - ) - - mock_update.assert_called_once() - args = mock_update.call_args.args - assert args[0] == "agency.md" - assert args[1] == "observations" - assert "Engineering Standup Facilitation" in args[2] - - def test_agency_notification_appends_to_existing(self, tmp_path): - self._seed_pattern(tmp_path) - with ( - patch("talent.skills.get_journal", return_value=str(tmp_path)), - patch("talent.skills.update_identity_section") as mock_update, - patch( - "talent.skills._read_agency_observations", - return_value="- Existing observation about something", - ), - ): - post_process( - SAMPLE_LLM_RESULT, - { - "meta": { - "pattern_id": "meeting-alice-bob-standup-engineering", - "facet": "test-facet", - "mode": "generate", - } - }, - ) - - args = mock_update.call_args.args - content = args[2] - assert "Existing observation" in content - assert "Engineering Standup Facilitation" in content - - def test_refresh_does_not_notify_agency(self, tmp_path): - self._seed_pattern(tmp_path, skill_generated=True) - with ( - patch("talent.skills.get_journal", return_value=str(tmp_path)), - patch("talent.skills.update_identity_section") as mock_update, - ): - post_process( - SAMPLE_LLM_RESULT, - { - "meta": { - "pattern_id": "meeting-alice-bob-standup-engineering", - "facet": "test-facet", - "mode": "refresh", - } - }, - ) - - mock_update.assert_not_called() - skill_path = ( - Path(tmp_path) - / "facets" - / "test-facet" - / "skills" - / "meeting-alice-bob-standup-engineering.md" - ) - assert skill_path.exists() - - def test_malformed_json_result(self, tmp_path): - self._seed_pattern(tmp_path) - with patch("talent.skills.get_journal", return_value=str(tmp_path)): - result = post_process( - "not valid json", - { - "meta": { - "pattern_id": "meeting-alice-bob-standup-engineering", - "facet": "test-facet", - "mode": "generate", - } - }, - ) - assert result is None - - def test_missing_pattern_id(self, tmp_path): - self._seed_pattern(tmp_path) - with patch("talent.skills.get_journal", return_value=str(tmp_path)): - result = post_process( - SAMPLE_LLM_RESULT, - { - "meta": { - "pattern_id": "missing-pattern", - "facet": "test-facet", - "mode": "generate", - } - }, - ) - assert result is None - - -class TestLoadPatterns: - def test_empty_file(self, tmp_path): - path = tmp_path / "facets" / "test-facet" / "skills" - path.mkdir(parents=True) - (path / "patterns.jsonl").write_text("", encoding="utf-8") - with patch("talent.skills.get_journal", return_value=str(tmp_path)): - assert _load_patterns("test-facet") == [] - - def test_missing_file(self, tmp_path): - with patch("talent.skills.get_journal", return_value=str(tmp_path)): - assert _load_patterns("test-facet") == [] - - def test_malformed_lines_skipped(self, tmp_path): - path = tmp_path / "facets" / "test-facet" / "skills" - path.mkdir(parents=True) - (path / "patterns.jsonl").write_text( - '{"id": "one", "activity_type": "meeting"}\nnot-json\n{"id": "two", "activity_type": "meeting"}\n', - encoding="utf-8", - ) - with patch("talent.skills.get_journal", return_value=str(tmp_path)): - patterns = _load_patterns("test-facet") - assert [pattern["id"] for pattern in patterns] == ["one", "two"] -- 2.51.2