"""Employee experience tracking, project reflections, and learned-skill distillation.""" from __future__ import annotations import re import json import hashlib from datetime import datetime, timezone from pathlib import Path from typing import Any from opc.core.config import validate_organization_id from opc.core.models import TaskStatus from opc.layer2_organization.work_item_identity import projection_id_for_task from opc.layer5_memory.preference import PreferenceManager from opc.layer5_memory.skill_library import Skill, SkillLibrary def _utc_now() -> str: return datetime.now(timezone.utc).replace(microsecond=0).isoformat() class EmployeeEvolutionManager: """Tracks employee outcomes, project reflections, and learned skills.""" LEARNED_SKILL_THRESHOLD = 2 EMPLOYEE_EXPERIENCE_SCHEMA_VERSION = 1 def __init__(self, opc_home) -> None: self.opc_home = Path(opc_home) self.preferences = PreferenceManager(self.opc_home) self.skills = SkillLibrary(self.opc_home) self.skills.load_all() def evolution_profile_path(self, project_id: str | None = None) -> Path: """Return the structured company-evolution state path. Employee evolution is runtime/company state, not user/project durable memory. Keeping it outside ``memory/*.md`` lets company mode retain experience scoring and learned playbooks without reintroducing hidden user preference writes. """ project = str(project_id or "").strip() if project: return self.opc_home / "projects" / project / "employee_evolution.json" return self.opc_home / "evolution" / "employees.json" def load_evolution_profile(self, project_id: str | None = None) -> dict[str, Any]: path = self.evolution_profile_path(project_id) if not path.exists(): return {} try: data = json.loads(path.read_text(encoding="utf-8")) return data if isinstance(data, dict) else {} except Exception: return {} def save_evolution_profile(self, profile: dict[str, Any], project_id: str | None = None) -> None: path = self.evolution_profile_path(project_id) path.parent.mkdir(parents=True, exist_ok=True) path.write_text( json.dumps(dict(profile or {}), ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8", ) def employee_experience_dir(self, organization_id: str) -> Path: org_id = validate_organization_id(organization_id) return self.opc_home / "company_state" / org_id / "employee_experience" def employee_experience_path(self, organization_id: str, employee_id: str) -> Path: safe_id = self._safe_employee_filename(employee_id) return self.employee_experience_dir(organization_id) / f"{safe_id}.json" def load_employee_experience(self, organization_id: str, employee_id: str) -> dict[str, Any]: path = self.employee_experience_path(organization_id, employee_id) if not path.exists(): return {} try: data = json.loads(path.read_text(encoding="utf-8")) return data if isinstance(data, dict) else {} except Exception: return {} def save_employee_experience(self, organization_id: str, employee_id: str, profile: dict[str, Any]) -> None: path = self.employee_experience_path(organization_id, employee_id) path.parent.mkdir(parents=True, exist_ok=True) path.write_text( json.dumps(dict(profile or {}), ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8", ) def get_employee_profile(self, employee_id: str, project_id: str | None = None) -> dict[str, Any]: global_profile = self.load_evolution_profile() project_profile = self.load_evolution_profile(project_id) if project_id else {} global_data = dict(global_profile.get("employees", {}).get(employee_id, {})) project_data = dict(project_profile.get("employees", {}).get(employee_id, {})) return self._deep_merge(global_data, project_data) def get_learned_skill_refs(self, employee_id: str, project_id: str | None = None) -> list[str]: profile = self.get_employee_profile(employee_id, project_id) refs = list(profile.get("learned_skill_refs", [])) unique: list[str] = [] for ref in refs: if ref and ref not in unique: unique.append(ref) return unique def build_employee_delta_context( self, employee_id: str, project_id: str | None = None, organization_id: str | None = None, ) -> str: profile = self.get_employee_profile(employee_id, project_id) delta = dict(profile.get("delta_profile", {})) experience_profile: dict[str, Any] = {} experience_delta: dict[str, Any] = {} if organization_id: experience_profile = self.load_employee_experience(organization_id, employee_id) experience_delta = dict(experience_profile.get("delta_profile", {}) or {}) if not delta and not experience_delta: return "" parts: list[str] = [] evolution_count = int(experience_profile.get("evolution_count", 0) or len(experience_profile.get("events", []) or [])) if evolution_count: parts.append(f"Self-evolution reviews: {evolution_count}") projects_reflected = int(profile.get("projects_reflected", 0)) if projects_reflected: parts.append(f"Reflected projects: {projects_reflected}") for title, key in ( ("Self-Evolved Strengths", "strengths"), ("Self-Evolved Adjustments", "adjustments"), ("Self-Evolved Watchouts", "avoid_next_time"), ("Self-Evolved Routing Notes", "routing_notes"), ): values = [str(item).strip() for item in list(experience_delta.get(key, []) or []) if str(item).strip()] if values: parts.append(f"## {title}\n" + "\n".join(f"- {item}" for item in values[:6])) for title, key in ( ("Working Patterns", "working_patterns"), ("Default Checklists", "default_checklists"), ("Reviewer Preferences", "reviewer_preferences"), ("Risk Watchouts", "risk_watchouts"), ("Tool Preferences", "tool_preferences"), ("Fit Domains", "fit_domains"), ("Avoid Domains", "avoid_domains"), ): values = [str(item).strip() for item in delta.get(key, []) if str(item).strip()] if values: parts.append(f"## {title}\n" + "\n".join(f"- {item}" for item in values[:6])) return "\n\n".join(parts) def get_experience_score( self, employee_id: str, role_id: str, domains: list[str] | None = None, project_id: str | None = None, organization_id: str | None = None, ) -> float: profile = self.get_employee_profile(employee_id, project_id) total_successes = int(profile.get("successes", 0)) total_partials = int(profile.get("partial_successes", 0)) total_failures = int(profile.get("failures", 0)) role_successes = int(profile.get("roles", {}).get(role_id, {}).get("successes", 0)) role_partials = int(profile.get("roles", {}).get(role_id, {}).get("partial_successes", 0)) role_failures = int(profile.get("roles", {}).get(role_id, {}).get("failures", 0)) learned_bonus = 2 * len(profile.get("learned_skill_refs", [])) reflection_bonus = min(4, int(profile.get("projects_reflected", 0))) domain_bonus = 0 for domain in domains or []: domain_record = profile.get("domains", {}).get(domain, {}) domain_bonus += int(domain_record.get("successes", 0)) domain_bonus += 0.5 * int(domain_record.get("partial_successes", 0)) domain_bonus -= 0.25 * int(domain_record.get("failures", 0)) score = ( total_successes + (0.5 * total_partials) - (0.25 * total_failures) + (2 * role_successes) + role_partials - (0.5 * role_failures) + domain_bonus + learned_bonus + reflection_bonus ) if organization_id: experience_profile = self.load_employee_experience(organization_id, employee_id) events = [item for item in list(experience_profile.get("events", []) or []) if isinstance(item, dict)] score += min(8.0, float(len(events))) for event in events[-8:]: if str(event.get("role_id", "") or "").strip() == role_id: score += 0.5 return float(max(0.0, score)) def apply_employee_evolution_patch( self, *, organization_id: str, patch: dict[str, Any], source: dict[str, Any] | None = None, allowed_employee_ids: set[str] | None = None, ) -> list[dict[str, Any]]: org_id = validate_organization_id(organization_id) source_payload = dict(source or {}) raw_events = list(patch.get("patches", []) or []) allowed = {str(item).strip() for item in (allowed_employee_ids or set()) if str(item).strip()} recorded: list[dict[str, Any]] = [] for raw_event in raw_events: if not isinstance(raw_event, dict): continue employee_id = str(raw_event.get("employee_id", "") or "").strip() if not employee_id or (allowed and employee_id not in allowed): continue event = self._normalize_self_evolution_event(raw_event, source_payload) if not event: continue profile = self.load_employee_experience(org_id, employee_id) if not profile: profile = { "schema_version": self.EMPLOYEE_EXPERIENCE_SCHEMA_VERSION, "kind": "company_employee_experience", "organization_id": org_id, "employee_id": employee_id, "events": [], "delta_profile": {}, "evolution_count": 0, } profile["schema_version"] = self.EMPLOYEE_EXPERIENCE_SCHEMA_VERSION profile["kind"] = "company_employee_experience" profile["organization_id"] = org_id profile["employee_id"] = employee_id events = [item for item in list(profile.get("events", []) or []) if isinstance(item, dict)] event_id = str(event.get("event_id", "") or "").strip() if event_id and any(str(item.get("event_id", "") or "").strip() == event_id for item in events): continue events.append(event) profile["events"] = events[-100:] profile["evolution_count"] = int(profile.get("evolution_count", 0) or 0) + 1 profile["updated_at"] = _utc_now() profile["delta_profile"] = self._build_self_evolution_delta(profile["events"]) self.save_employee_experience(org_id, employee_id, profile) recorded.append({ "employee_id": employee_id, "event_id": event_id, "status": "recorded", "path": str(self.employee_experience_path(org_id, employee_id)), }) return recorded def record_work_item_completion( self, task: Any, result_content: str, *, outcome: str = "success", feedback_summary: str = "", strengths: list[str] | None = None, weaknesses: list[str] | None = None, rationale: str = "", ) -> dict[str, Any]: assignment = dict(getattr(task, "metadata", {}).get("employee_assignment", {}) or {}) employee_id = str(assignment.get("employee_id", "")).strip() if not employee_id: return {} role_id = str( assignment.get("role_id") or getattr(task, "assigned_to", "") or getattr(task, "metadata", {}).get("work_item_role_id", "") ).strip() domains = list(assignment.get("domains") or getattr(task, "tags", []) or []) project_id = getattr(task, "project_id", None) or None summary = str(getattr(task, "metadata", {}).get("work_item_summary_for_downstream", "") or result_content).strip() pattern_key = self._pattern_key(role_id, domains) normalized_outcome = self._normalize_outcome(outcome) base_payload = { "employee_id": employee_id, "employee_name": assignment.get("name", ""), "role_id": role_id, "template_id": assignment.get("template_id", ""), "category": assignment.get("category", ""), } global_profile = self.load_evolution_profile() self._record_in_profile( global_profile, base_payload=base_payload, role_id=role_id, domains=domains, pattern_key=pattern_key, summary=summary, outcome=normalized_outcome, feedback_summary=feedback_summary, strengths=list(strengths or []), weaknesses=list(weaknesses or []), rationale=rationale, ) self.save_evolution_profile(global_profile) if project_id: project_profile = self.load_evolution_profile(project_id) self._record_in_profile( project_profile, base_payload=base_payload, role_id=role_id, domains=domains, pattern_key=pattern_key, summary=summary, outcome=normalized_outcome, feedback_summary=feedback_summary, strengths=list(strengths or []), weaknesses=list(weaknesses or []), rationale=rationale, ) self.save_evolution_profile(project_profile, project_id) return { "employee_id": employee_id, "project_id": project_id or "", "pattern_key": pattern_key, "outcome": normalized_outcome, } def record_project_reflections( self, delivery_task: Any, work_item_tasks: list[Any], partial: bool = False, *, feedback: dict[str, Any] | None = None, evaluation: dict[str, Any] | None = None, ) -> list[dict[str, Any]]: project_id = str(getattr(delivery_task, "project_id", "") or "").strip() delivery_task_id = str(getattr(delivery_task, "id", "") or "").strip() if not project_id or not delivery_task_id: return [] tasks = self._normalize_work_item_tasks(work_item_tasks, delivery_task) terminal = {TaskStatus.DONE, TaskStatus.FAILED, TaskStatus.CANCELLED} employee_groups: dict[str, list[Any]] = {} for task in tasks: if getattr(task, "status", None) not in terminal: continue assignment = dict(getattr(task, "metadata", {}).get("employee_assignment", {}) or {}) employee_id = str(assignment.get("employee_id", "")).strip() if not employee_id: continue employee_groups.setdefault(employee_id, []).append(task) results: list[dict[str, Any]] = [] for employee_id, employee_tasks in employee_groups.items(): reflection = self._build_project_reflection( delivery_task=delivery_task, employee_tasks=employee_tasks, project_id=project_id, partial=partial, feedback=feedback, evaluation=evaluation, ) reflection_path = self.evolution_profile_path(project_id) global_profile = self.load_evolution_profile() self._record_project_reflection_in_profile( global_profile, reflection=reflection, reflection_path=reflection_path, ) learned_skill_ref = self._maybe_promote_reflection_skill(global_profile, reflection, project_id=project_id) self.save_evolution_profile(global_profile) project_profile = self.load_evolution_profile(project_id) self._record_project_reflection_in_profile( project_profile, reflection=reflection, reflection_path=reflection_path, ) if learned_skill_ref: self._attach_learned_skill( project_profile, reflection["employee_id"], reflection["pattern_key"], learned_skill_ref, ) else: learned_skill_ref = self._maybe_promote_reflection_skill(project_profile, reflection, project_id=project_id) self.save_evolution_profile(project_profile, project_id) results.append({ "employee_id": employee_id, "reflection_path": str(reflection_path), "learned_skill_ref": learned_skill_ref, "status": "recorded", "reflection": reflection, }) return results def _record_in_profile( self, profile: dict[str, Any], *, base_payload: dict[str, Any], role_id: str, domains: list[str], pattern_key: str, summary: str, outcome: str, feedback_summary: str, strengths: list[str], weaknesses: list[str], rationale: str, ) -> None: employees = profile.setdefault("employees", {}) record = employees.setdefault(base_payload["employee_id"], {}) record.update({ "employee_name": base_payload.get("employee_name", ""), "role_id": role_id, "template_id": base_payload.get("template_id", ""), "category": base_payload.get("category", ""), "updated_at": _utc_now(), }) self._increment_outcome_counts(record, outcome) record["last_outcome"] = outcome if feedback_summary: record["last_feedback_summary"] = feedback_summary if rationale: record["last_feedback_rationale"] = rationale roles = record.setdefault("roles", {}) role_record = roles.setdefault(role_id, {"successes": 0, "partial_successes": 0, "failures": 0}) self._increment_outcome_counts(role_record, outcome) role_record["last_outcome"] = outcome domain_records = record.setdefault("domains", {}) for domain in domains: domain_record = domain_records.setdefault(domain, {"successes": 0, "partial_successes": 0, "failures": 0}) self._increment_outcome_counts(domain_record, outcome) domain_record["last_outcome"] = outcome patterns = record.setdefault("patterns", {}) pattern = patterns.setdefault(pattern_key, self._base_pattern_record(role_id, domains)) self._increment_outcome_counts(pattern, outcome) pattern["last_summary"] = summary pattern["last_outcome"] = outcome pattern["last_feedback_summary"] = feedback_summary pattern["last_feedback_rationale"] = rationale if outcome == "success": pattern["last_success_at"] = _utc_now() elif outcome == "partial_success": pattern["last_partial_success_at"] = _utc_now() else: pattern["last_failure_at"] = _utc_now() self._increment_counts(record, "working_pattern_counts", strengths) self._increment_counts(record, "risk_watchout_counts", weaknesses) self._increment_counts(pattern, "working_pattern_counts", strengths) self._increment_counts(pattern, "risk_watchout_counts", weaknesses) record["delta_profile"] = self._build_delta_profile(record) record.setdefault("learned_skill_refs", []) def _record_project_reflection_in_profile( self, profile: dict[str, Any], *, reflection: dict[str, Any], reflection_path: Path, ) -> None: employees = profile.setdefault("employees", {}) employee_id = str(reflection.get("employee_id", "")).strip() if not employee_id: return record = employees.setdefault(employee_id, {}) record.update({ "employee_name": reflection.get("employee_name", ""), "role_id": reflection.get("role_id", ""), "template_id": reflection.get("template_id", ""), "category": reflection.get("category", ""), "updated_at": _utc_now(), "last_reflection_at": _utc_now(), }) reflection_ids = record.setdefault("project_reflection_ids", []) if reflection["delivery_task_id"] in reflection_ids: return reflection_ids.append(reflection["delivery_task_id"]) reflection_paths = record.setdefault("reflection_paths", []) reflection_paths.append(str(reflection_path)) project_ids = record.setdefault("project_ids", []) if reflection["project_id"] not in project_ids: project_ids.append(reflection["project_id"]) record["projects_reflected"] = len(project_ids) patterns = record.setdefault("patterns", {}) pattern_key = str(reflection.get("pattern_key", "")) pattern = patterns.setdefault( pattern_key, self._base_pattern_record( str(reflection.get("role_id", "")).strip(), list(reflection.get("domains", [])), ), ) pattern["role_id"] = reflection.get("role_id", "") pattern["domains"] = list(reflection.get("domains", [])) pattern["reflection_count"] = int(pattern.get("reflection_count", 0)) + 1 pattern["latest_project_summary"] = str(reflection.get("project_summary", "")) pattern["last_reflection_at"] = _utc_now() project_reflection_paths = pattern.setdefault("reflection_paths", []) project_reflection_paths.append(str(reflection_path)) self._increment_counts(pattern, "working_pattern_counts", reflection.get("what_worked", [])) self._increment_counts(pattern, "checklist_counts", reflection.get("reusable_checklist", [])) self._increment_counts(pattern, "reviewer_preference_counts", reflection.get("reviewer_preferences", [])) self._increment_counts(pattern, "tool_preference_counts", reflection.get("tool_preferences", [])) self._increment_counts(pattern, "risk_watchout_counts", reflection.get("mistakes_to_avoid", [])) self._increment_counts(pattern, "fit_domain_counts", reflection.get("suitable_for", [])) self._increment_counts(pattern, "avoid_domain_counts", reflection.get("avoid_for", [])) self._increment_counts(record, "working_pattern_counts", reflection.get("what_worked", [])) self._increment_counts(record, "checklist_counts", reflection.get("reusable_checklist", [])) self._increment_counts(record, "reviewer_preference_counts", reflection.get("reviewer_preferences", [])) self._increment_counts(record, "tool_preference_counts", reflection.get("tool_preferences", [])) self._increment_counts(record, "risk_watchout_counts", reflection.get("mistakes_to_avoid", [])) self._increment_counts(record, "fit_domain_counts", reflection.get("suitable_for", [])) self._increment_counts(record, "avoid_domain_counts", reflection.get("avoid_for", [])) record["delta_profile"] = self._build_delta_profile(record) record.setdefault("learned_skill_refs", []) def _build_project_reflection( self, *, delivery_task: Any, employee_tasks: list[Any], project_id: str, partial: bool = False, feedback: dict[str, Any] | None = None, evaluation: dict[str, Any] | None = None, ) -> dict[str, Any]: first_assignment = dict(employee_tasks[0].metadata.get("employee_assignment", {}) or {}) employee_id = str(first_assignment.get("employee_id", "")).strip() role_id = str(first_assignment.get("role_id") or getattr(employee_tasks[0], "assigned_to", "") or "").strip() domains = self._collect_domains(employee_tasks, fallback=list(first_assignment.get("domains", []))) pattern_key = self._pattern_key(role_id, domains) task_summaries = self._collect_task_summaries(employee_tasks) artifacts = self._collect_items(employee_tasks, "artifacts") decisions = self._collect_items(employee_tasks, "decisions") risks = self._collect_items(employee_tasks, "risks") open_questions = self._collect_items(employee_tasks, "open_questions") preferred_agents = self._collect_preferred_agents(employee_tasks, first_assignment) employee_feedback = self._find_employee_feedback(employee_id, evaluation) historical_context = self.build_employee_delta_context(employee_id, project_id=project_id) feedback_summary = str((evaluation or {}).get("summary", "") or (feedback or {}).get("raw_feedback", "")).strip() what_worked = [ "Break work into explicit deliverables and leave concise handoff summaries.", "Preserve reviewer-friendly artifacts so downstream validation is faster.", ] if decisions: what_worked.append("Capture explicit implementation decisions that downstream reviewers can verify.") if artifacts: what_worked.append("Reference exact artifact paths or outputs in every handoff.") reusable_checklist = [ "State the objective and completion summary explicitly in the handoff.", "Leave reviewer-friendly artifacts for the next work item.", ] if decisions: reusable_checklist.append("Record key decisions that affect downstream execution.") if artifacts: reusable_checklist.append("List concrete artifact references for changed outputs.") if any(domain in {"coding", "api", "backend", "frontend", "devops"} for domain in domains): reusable_checklist.append("Include validation or test evidence before requesting review.") reviewer_preferences = [ "Make decisions, risks, and artifact references explicit for review.", ] if risks or open_questions: reviewer_preferences.append("Flag unresolved risks and open questions before requesting approval.") if artifacts: reviewer_preferences.append("Point reviewers to exact changed files or deliverables.") tool_preferences = [ f"Prefer external agent `{agent}` for similar `{role_id}` work." for agent in preferred_agents ] mistakes_to_avoid = list(risks[:4]) if open_questions: mistakes_to_avoid.extend(item for item in open_questions[:2] if item not in mistakes_to_avoid) if not mistakes_to_avoid: mistakes_to_avoid.append("Avoid handoffs that omit concrete artifacts, risks, or validation notes.") what_worked.extend(str(item).strip() for item in list(employee_feedback.get("strengths", [])) if str(item).strip()) mistakes_to_avoid.extend(str(item).strip() for item in list(employee_feedback.get("weaknesses", [])) if str(item).strip()) if feedback_summary: reviewer_preferences.append(f"Carry forward user feedback themes: {feedback_summary}") suitable_for = list(domains or [role_id]) avoid_for = self._infer_avoid_domains(mistakes_to_avoid) confidence = round(min(0.95, 0.55 + (0.08 * len(employee_tasks))), 2) failed_tasks = [ t for t in employee_tasks if getattr(t, "status", None) != TaskStatus.DONE ] failures: list[dict[str, str]] = [] if failed_tasks: for t in failed_tasks: reason = str(getattr(t, "metadata", {}).get("failure_reason", "")) failures.append({ "task": getattr(t, "title", ""), "status": str(getattr(t, "status", "")), "reason": reason, }) if reason: mistakes_to_avoid.append(reason) if partial: confidence = round(min(confidence, 0.4), 2) result: dict[str, Any] = { "employee_id": employee_id, "employee_name": first_assignment.get("name", ""), "template_id": first_assignment.get("template_id", ""), "category": first_assignment.get("category", ""), "project_id": project_id, "delivery_task_id": str(getattr(delivery_task, "id", "") or ""), "delivery_projection_id": projection_id_for_task(delivery_task), "role_id": role_id, "domains": domains, "pattern_key": pattern_key, "project_summary": " ".join(task_summaries[:3]), "what_worked": self._dedupe_preserve_order(what_worked), "mistakes_to_avoid": self._dedupe_preserve_order(mistakes_to_avoid), "reusable_checklist": self._dedupe_preserve_order(reusable_checklist), "reviewer_preferences": self._dedupe_preserve_order(reviewer_preferences), "tool_preferences": self._dedupe_preserve_order(tool_preferences), "suitable_for": self._dedupe_preserve_order(suitable_for), "avoid_for": self._dedupe_preserve_order(avoid_for), "confidence": confidence, "source_task_ids": [str(getattr(task, "id", "")) for task in employee_tasks], "created_at": _utc_now(), "employee_outcome": self._normalize_outcome(str(employee_feedback.get("outcome", "partial_success") or "partial_success")), "feedback_summary": feedback_summary, "feedback_rationale": str(employee_feedback.get("reason", "")).strip(), } if historical_context: result["historical_context"] = historical_context if feedback: result["user_feedback"] = { "label": str(feedback.get("label", "")).strip(), "raw_feedback": str(feedback.get("raw_feedback", "")).strip(), "scope": str(feedback.get("scope", "")).strip(), } if evaluation: result["runtime_feedback_evaluation"] = { "overall_outcome": str(evaluation.get("overall_outcome", "")).strip(), "summary": str(evaluation.get("summary", "")).strip(), "strengths": [str(item).strip() for item in list(evaluation.get("strengths", [])) if str(item).strip()][:6], "weaknesses": [str(item).strip() for item in list(evaluation.get("weaknesses", [])) if str(item).strip()][:6], } if failures: result["failures"] = failures result["partial"] = True return result def _maybe_promote_reflection_skill(self, profile: dict[str, Any], reflection: dict[str, Any], project_id: str | None = None) -> str: employees = profile.setdefault("employees", {}) employee_id = str(reflection.get("employee_id", "")).strip() record = employees.get(employee_id) if not record: return "" pattern_key = str(reflection.get("pattern_key", "")).strip() pattern = dict(record.get("patterns", {}).get(pattern_key, {})) if not pattern or pattern.get("learned_skill_ref"): return str(pattern.get("learned_skill_ref", "")) if int(pattern.get("reflection_count", 0)) < self.LEARNED_SKILL_THRESHOLD: return "" repeated_working = self._repeated_items(pattern.get("working_pattern_counts", {})) repeated_checklists = self._repeated_items(pattern.get("checklist_counts", {})) repeated_reviewer = self._repeated_items(pattern.get("reviewer_preference_counts", {})) repeated_tools = self._repeated_items(pattern.get("tool_preference_counts", {})) repeated_risks = self._repeated_items(pattern.get("risk_watchout_counts", {})) if not any((repeated_working, repeated_checklists, repeated_reviewer, repeated_tools, repeated_risks)): return "" employee_name = str(reflection.get("employee_name", "Employee")).strip() or "Employee" role_id = str(reflection.get("role_id", "")).strip() or record.get("role_id", "general") domains = list(reflection.get("domains", [])) or list(pattern.get("domains", [])) or [role_id] primary_domain = domains[0] if domains else role_id display_name = f"{employee_name} {role_id} {primary_domain} playbook" skill_name = self._normalize_skill_name(display_name) reflection_count = int(pattern.get("reflection_count", 0)) sections = [ f"# {display_name}", "", f"Learned playbook for **{employee_name}** in role `{role_id}` across {', '.join(domains)}.", f"Distilled from {reflection_count} project reflections.", ] if repeated_working: sections.extend(["", "## Successful Behaviors", *[f"- {item}" for item in repeated_working]]) if repeated_checklists: sections.extend(["", "## Default Checklist", *[f"- {item}" for item in repeated_checklists]]) if repeated_reviewer: sections.extend(["", "## Reviewer Preferences", *[f"- {item}" for item in repeated_reviewer]]) if repeated_tools: sections.extend(["", "## Tool Preferences", *[f"- {item}" for item in repeated_tools]]) if repeated_risks: sections.extend(["", "## Risk Watchouts", *[f"- {item}" for item in repeated_risks]]) avoid_for = self._rank_items(record.get("avoid_domain_counts", {}), limit=4) if avoid_for: sections.extend(["", "## Avoid For", *[f"- {item}" for item in avoid_for]]) skill = Skill( name=skill_name, description=( f"Learned playbook for `{role_id}` work ({', '.join(domains)}) by {employee_name}. " f"Use when assigning similar {role_id} tasks in these domains." ), metadata={ "employee_id": employee_id, "employee_name": employee_name, "role_id": role_id, "template_id": reflection.get("template_id", ""), "pattern_key": pattern_key, "built_from": "project_reflections", }, content="\n".join(sections).strip() + "\n", ) self.skills.save_skill(skill, project_id=project_id) self._attach_learned_skill(profile, employee_id, pattern_key, skill.name) return skill.name @staticmethod def _normalize_skill_name(raw: str) -> str: """Normalize to lowercase hyphen-case, matching skill-creator conventions.""" normalized = raw.strip().lower() normalized = re.sub(r"[^a-z0-9]+", "-", normalized) normalized = normalized.strip("-") normalized = re.sub(r"-{2,}", "-", normalized) return normalized[:64] def _attach_learned_skill( self, profile: dict[str, Any], employee_id: str, pattern_key: str, skill_name: str, ) -> None: employees = profile.setdefault("employees", {}) record = employees.setdefault(employee_id, {}) refs = record.setdefault("learned_skill_refs", []) if skill_name not in refs: refs.append(skill_name) patterns = record.setdefault("patterns", {}) pattern = patterns.setdefault(pattern_key, {}) pattern["learned_skill_ref"] = skill_name record["updated_at"] = _utc_now() record.setdefault("delta_profile", {}) def _reflection_path(self, project_id: str, employee_id: str, delivery_task_id: str) -> Path: return self.opc_home / "projects" / project_id / "employees" / employee_id / "reflections" / f"{delivery_task_id}.yaml" def _base_pattern_record(self, role_id: str, domains: list[str]) -> dict[str, Any]: return { "role_id": role_id, "domains": list(domains), "successes": 0, "partial_successes": 0, "failures": 0, "learned_skill_ref": "", "reflection_count": 0, } def _normalize_work_item_tasks(self, work_item_tasks: list[Any], delivery_task: Any) -> list[Any]: tasks_by_id: dict[str, Any] = {} for task in work_item_tasks: task_id = str(getattr(task, "id", "") or "").strip() if task_id: tasks_by_id[task_id] = task delivery_task_id = str(getattr(delivery_task, "id", "") or "").strip() if delivery_task_id: tasks_by_id[delivery_task_id] = delivery_task return list(tasks_by_id.values()) def _collect_domains(self, tasks: list[Any], fallback: list[str]) -> list[str]: collected: list[str] = [] for task in tasks: assignment = dict(getattr(task, "metadata", {}).get("employee_assignment", {}) or {}) for value in list(assignment.get("domains", [])) + list(getattr(task, "tags", []) or []): item = str(value).strip() if item and item not in collected: collected.append(item) for value in fallback: item = str(value).strip() if item and item not in collected: collected.append(item) return collected def _collect_task_summaries(self, tasks: list[Any]) -> list[str]: summaries: list[str] = [] for task in tasks: for candidate in ( str(getattr(task, "metadata", {}).get("work_item_summary_for_downstream", "") or "").strip(), str(getattr(task, "result", {}).get("content", "") or "").strip(), str(getattr(task, "title", "") or "").strip(), ): if candidate and candidate not in summaries: summaries.append(candidate) break return summaries def _collect_items(self, tasks: list[Any], key: str) -> list[str]: items: list[str] = [] for task in tasks: for value in list(getattr(task, "metadata", {}).get(key, []) or []): text = str(value).strip() if text and text not in items: items.append(text) return items def _collect_preferred_agents(self, tasks: list[Any], assignment: dict[str, Any]) -> list[str]: agents: list[str] = [] assignment_agent = str(assignment.get("preferred_external_agent", "") or "").strip() if assignment_agent: agents.append(assignment_agent) for task in tasks: value = str(getattr(task, "assigned_external_agent", "") or "").strip() if value and value not in agents: agents.append(value) return agents def _infer_avoid_domains(self, risks: list[str]) -> list[str]: avoid: list[str] = [] joined = " ".join(risks).lower() for token in ("security", "compliance", "billing", "finance", "production", "deployment"): if token in joined and token not in avoid: avoid.append(token) return avoid def _find_employee_feedback(self, employee_id: str, evaluation: dict[str, Any] | None) -> dict[str, Any]: if not evaluation: return {} for item in list(evaluation.get("employees", [])): if str(item.get("employee_id", "")).strip() == employee_id: return dict(item) return {} def _increment_outcome_counts(self, container: dict[str, Any], outcome: str) -> None: normalized = self._normalize_outcome(outcome) if normalized == "success": container["successes"] = int(container.get("successes", 0)) + 1 elif normalized == "failure": container["failures"] = int(container.get("failures", 0)) + 1 else: container["partial_successes"] = int(container.get("partial_successes", 0)) + 1 def _normalize_outcome(self, outcome: str) -> str: normalized = str(outcome or "").strip().lower() if normalized in {"success", "approved", "fully_approved", "complete_success"}: return "success" if normalized in {"failure", "failed", "rejected", "fully_rejected"}: return "failure" return "partial_success" def _increment_counts(self, container: dict[str, Any], key: str, items: list[str]) -> None: counts = container.setdefault(key, {}) for item in self._dedupe_preserve_order(items): counts[item] = int(counts.get(item, 0)) + 1 def _build_delta_profile(self, record: dict[str, Any]) -> dict[str, Any]: return { "working_patterns": self._rank_items(record.get("working_pattern_counts", {}), limit=5), "default_checklists": self._rank_items(record.get("checklist_counts", {}), limit=6), "reviewer_preferences": self._rank_items(record.get("reviewer_preference_counts", {}), limit=5), "risk_watchouts": self._rank_items(record.get("risk_watchout_counts", {}), limit=5), "tool_preferences": self._rank_items(record.get("tool_preference_counts", {}), limit=4), "fit_domains": self._rank_items(record.get("fit_domain_counts", {}), limit=4), "avoid_domains": self._rank_items(record.get("avoid_domain_counts", {}), limit=4), } def _normalize_self_evolution_event( self, event: dict[str, Any], source: dict[str, Any], ) -> dict[str, Any]: employee_id = str(event.get("employee_id", "") or "").strip() if not employee_id: return {} event_id = str(event.get("event_id", "") or "").strip() if not event_id: checkpoint_id = str(source.get("checkpoint_id", "") or "").strip() role_id = str(event.get("role_id", "") or "").strip() nonce = json.dumps(event, ensure_ascii=False, sort_keys=True) digest = hashlib.sha256(nonce.encode("utf-8")).hexdigest()[:12] event_id = self._safe_employee_filename(f"{checkpoint_id or _utc_now()}-{employee_id}-{role_id}-{digest}") return { "event_id": event_id, "employee_id": employee_id, "role_id": str(event.get("role_id", "") or "").strip(), "summary": str(event.get("summary", "") or "").strip(), "strengths": self._string_list(event.get("strengths", []), limit=8), "adjustments": self._string_list(event.get("adjustments", event.get("adjust", [])), limit=8), "avoid_next_time": self._string_list(event.get("avoid_next_time", event.get("avoid", [])), limit=8), "routing_notes": self._string_list(event.get("routing_notes", []), limit=8), "evidence_task_ids": self._string_list(event.get("evidence_task_ids", []), limit=12), "confidence": self._bounded_float(event.get("confidence", 0.7), default=0.7), "source": dict(source or {}), "created_at": _utc_now(), } def _build_self_evolution_delta(self, events: list[dict[str, Any]]) -> dict[str, Any]: counts: dict[str, dict[str, int]] = { "strengths": {}, "adjustments": {}, "avoid_next_time": {}, "routing_notes": {}, } for event in events: if not isinstance(event, dict): continue for key in counts: for item in self._string_list(event.get(key, []), limit=20): counts[key][item] = int(counts[key].get(item, 0)) + 1 return { "strengths": self._rank_items(counts["strengths"], limit=8), "adjustments": self._rank_items(counts["adjustments"], limit=8), "avoid_next_time": self._rank_items(counts["avoid_next_time"], limit=8), "routing_notes": self._rank_items(counts["routing_notes"], limit=8), } @staticmethod def _safe_employee_filename(value: str) -> str: safe = re.sub(r"[^A-Za-z0-9._-]+", "-", str(value or "").strip()).strip("-") return safe or "employee" @staticmethod def _bounded_float(value: Any, *, default: float = 0.0) -> float: try: number = float(value) except (TypeError, ValueError): number = default return max(0.0, min(1.0, number)) @staticmethod def _string_list(value: Any, *, limit: int = 8) -> list[str]: items = value if isinstance(value, list) else [value] result: list[str] = [] for item in items: text = str(item or "").strip() if text and text not in result: result.append(text) if len(result) >= limit: break return result def _rank_items(self, counts: dict[str, Any], *, limit: int = 6) -> list[str]: ranked = sorted( ((str(item).strip(), int(count)) for item, count in dict(counts).items() if str(item).strip()), key=lambda pair: (-pair[1], pair[0].lower()), ) return [item for item, _count in ranked[:limit]] def _repeated_items(self, counts: dict[str, Any], *, minimum: int = 2) -> list[str]: ranked = sorted( ((str(item).strip(), int(count)) for item, count in dict(counts).items() if int(count) >= minimum and str(item).strip()), key=lambda pair: (-pair[1], pair[0].lower()), ) return [item for item, _count in ranked[:6]] def _pattern_key(self, role_id: str, domains: list[str]) -> str: domain_key = ",".join(sorted({domain.strip().lower() for domain in domains if domain.strip()})) return f"{role_id}|{domain_key or 'general'}" def _dedupe_preserve_order(self, items: list[str]) -> list[str]: unique: list[str] = [] for item in items: text = str(item).strip() if text and text not in unique: unique.append(text) return unique def _deep_merge(self, base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]: merged = dict(base) for key, value in override.items(): if isinstance(value, dict) and isinstance(merged.get(key), dict): merged[key] = self._deep_merge(merged[key], value) elif isinstance(value, list) and isinstance(merged.get(key), list): existing = list(merged[key]) for item in value: if item not in existing: existing.append(item) merged[key] = existing else: merged[key] = value return merged