From 55f37b9444c7479fa7e9018ee0508090cc7b2e97 Mon Sep 17 00:00:00 2001 From: Gros Frumos Date: Thu, 19 Mar 2026 19:06:18 +0200 Subject: [PATCH] kin: KIN-DOCS-003-backend_dev --- agents/prompts/knowledge_synthesizer.md | 93 +++++++++++++++++++++++++ agents/specialists.yaml | 15 ++++ core/context_builder.py | 3 + core/phases.py | 39 +++++++++-- tests/test_kin_docs_002_regression.py | 8 +-- tests/test_phases.py | 35 ++++++++-- 6 files changed, 180 insertions(+), 13 deletions(-) create mode 100644 agents/prompts/knowledge_synthesizer.md diff --git a/agents/prompts/knowledge_synthesizer.md b/agents/prompts/knowledge_synthesizer.md new file mode 100644 index 0000000..28e824b --- /dev/null +++ b/agents/prompts/knowledge_synthesizer.md @@ -0,0 +1,93 @@ +You are a Knowledge Synthesizer for the Kin multi-agent orchestrator. + +Your job: aggregate and synthesize outputs from multiple parallel research agents into a unified, confidence-rated knowledge base ready for the Architect. + +## Input + +You receive: +- PROJECT: id, name, path, tech stack +- TASK: id, title, brief with `phases_context` keyed by researcher role name (e.g. `business_analyst`, `market_researcher`) +- DECISIONS: known architectural decisions and conventions +- PREVIOUS STEP OUTPUT: latest approved researcher outputs (if phases_context is absent) + +## Working Mode + +1. Read `brief.phases_context` — each key is a researcher role, value is their output summary +2. Identify consensus findings: claims supported by ≥2 researcher roles +3. Identify conflicts: directly contradictory findings between roles; name the conflict explicitly +4. Assign confidence rating to each conclusion: `high` (≥2 supporting roles), `medium` (1 role), `low` (inferred) +5. Prioritize actions: order by impact and confidence, not by which role reported them first + +## Focus On + +- Synthesize, do not repeat raw data — extract conclusions, not summaries of summaries +- Make conflicts explicit: name both positions and which roles hold them; do not paper over disagreements +- Confidence must be grounded in cross-role agreement, not assertion strength +- Unresolved conflicts = honest knowledge gaps, not speculation — flag them as open questions +- `phases_context_used` must list every input role to prove completeness + +## Quality Checks + +- Every item in `confidence_rated_conclusions` cites at least one `supporting_roles` entry +- `unified_findings` contains no raw data dumps — only synthesized insights +- `unresolved_conflicts` is not silently omitted when contradictions exist +- `prioritized_actions` are concrete and directed at the Architect, not vague recommendations +- All input roles from `phases_context` appear in `phases_context_used` + +## Return Format + +Return ONLY valid JSON (no markdown, no explanation): + +```json +{ + "status": "done", + "unified_findings": [ + "Synthesized insight that multiple researchers converge on" + ], + "confidence_rated_conclusions": [ + { + "conclusion": "...", + "confidence": "high | medium | low", + "supporting_roles": ["business_analyst", "market_researcher"], + "rationale": "Why this confidence level" + } + ], + "unresolved_conflicts": [ + { + "topic": "...", + "positions": { + "role_a": "their position", + "role_b": "their position" + }, + "recommendation": "What the Architect should decide" + } + ], + "prioritized_actions": [ + "Action 1 for Architect — grounded in high-confidence findings", + "Action 2 for Architect" + ], + "phases_context_used": ["business_analyst", "market_researcher"] +} +``` + +Valid values for `status`: `"done"`, `"blocked"`. + +If status is "blocked", include `"blocked_reason": "..."`. + +## Constraints + +- Do NOT repeat raw researcher outputs — synthesize only +- Do NOT assign `confidence: high` to findings supported by only one role +- Do NOT omit `unresolved_conflicts` when contradictions exist between roles +- Do NOT write implementation code or architectural decisions — that is the Architect's job +- Do NOT speculate beyond what researchers reported; unknown = unresolved + +## Blocked Protocol + +If you cannot perform the task (phases_context is missing or empty, ambiguous requirements, task outside your scope), return this JSON **instead of** the normal output: + +```json +{"status": "blocked", "reason": "", "blocked_at": ""} +``` + +Use current datetime for `blocked_at`. Do NOT guess or partially complete — return blocked immediately. diff --git a/agents/specialists.yaml b/agents/specialists.yaml index 6ec24b4..cd3af24 100644 --- a/agents/specialists.yaml +++ b/agents/specialists.yaml @@ -227,6 +227,21 @@ specialists: context_rules: decisions: all + knowledge_synthesizer: + name: "Knowledge Synthesizer" + model: sonnet + tools: [Read, Grep, Glob] + description: "Aggregates multi-agent research outputs into unified, confidence-rated knowledge base for the Architect" + permissions: read_only + context_rules: + decisions: all + output_schema: + unified_findings: "array of strings" + confidence_rated_conclusions: "array of { conclusion, confidence: high|medium|low, supporting_roles, rationale }" + unresolved_conflicts: "array of { topic, positions: { role: position }, recommendation }" + prioritized_actions: "array of strings" + phases_context_used: "array of role names" + research_head: name: "Research Department Head" model: opus diff --git a/core/context_builder.py b/core/context_builder.py index f53d2f5..8d99668 100644 --- a/core/context_builder.py +++ b/core/context_builder.py @@ -75,6 +75,9 @@ def build_context( ctx["modules"] = models.get_modules(conn, project_id) ctx["decisions"] = models.get_decisions(conn, project_id) + elif role == "knowledge_synthesizer": + ctx["decisions"] = models.get_decisions(conn, project_id) + elif role == "debugger": ctx["decisions"] = models.get_decisions( conn, project_id, types=["gotcha", "workaround"], diff --git a/core/phases.py b/core/phases.py index 1e08bac..5c37f91 100644 --- a/core/phases.py +++ b/core/phases.py @@ -10,7 +10,7 @@ import sqlite3 from core import models -# Canonical order of research roles (architect always last) +# Canonical order of research roles (knowledge_synthesizer and architect always last) RESEARCH_ROLES = [ "business_analyst", "market_researcher", @@ -18,6 +18,7 @@ RESEARCH_ROLES = [ "tech_researcher", "ux_designer", "marketer", + "knowledge_synthesizer", "architect", ] @@ -29,17 +30,18 @@ ROLE_LABELS = { "tech_researcher": "Tech Researcher", "ux_designer": "UX Designer", "marketer": "Marketer", + "knowledge_synthesizer": "Knowledge Synthesizer", "architect": "Architect", } def validate_roles(roles: list[str]) -> list[str]: - """Filter unknown roles, remove duplicates, strip 'architect' (auto-added later).""" + """Filter unknown roles, remove duplicates, strip auto-managed roles (architect, knowledge_synthesizer).""" seen: set[str] = set() result = [] for r in roles: r = r.strip().lower() - if r == "architect": + if r in ("architect", "knowledge_synthesizer"): continue if r in RESEARCH_ROLES and r not in seen: seen.add(r) @@ -48,9 +50,18 @@ def validate_roles(roles: list[str]) -> list[str]: def build_phase_order(selected_roles: list[str]) -> list[str]: - """Return roles in canonical RESEARCH_ROLES order, append architect if any selected.""" - ordered = [r for r in RESEARCH_ROLES if r in selected_roles and r != "architect"] + """Return roles in canonical RESEARCH_ROLES order. + + Auto-inserts knowledge_synthesizer before architect when ≥2 researchers selected. + Architect always appended last when any researcher is selected. + """ + ordered = [ + r for r in RESEARCH_ROLES + if r in selected_roles and r not in ("architect", "knowledge_synthesizer") + ] if ordered: + if len(ordered) >= 2: + ordered.append("knowledge_synthesizer") ordered.append("architect") return ordered @@ -114,6 +125,24 @@ def activate_phase(conn: sqlite3.Connection, phase_id: int) -> dict: "phase_order": phase["phase_order"], "workflow": "research", } + + # knowledge_synthesizer: collect approved researcher outputs into phases_context + if phase["role"] == "knowledge_synthesizer": + all_phases = models.list_phases(conn, phase["project_id"]) + phases_context = {} + for p in all_phases: + if p["status"] == "approved" and p.get("task_id"): + row = conn.execute( + """SELECT output_summary FROM agent_logs + WHERE task_id = ? AND success = 1 + ORDER BY created_at DESC LIMIT 1""", + (p["task_id"],), + ).fetchone() + if row and row["output_summary"]: + phases_context[p["role"]] = row["output_summary"] + if phases_context: + brief["phases_context"] = phases_context + task = models.create_task( conn, task_id, phase["project_id"], title=f"[Research] {ROLE_LABELS.get(phase['role'], phase['role'])}", diff --git a/tests/test_kin_docs_002_regression.py b/tests/test_kin_docs_002_regression.py index dafd7d1..7f8124a 100644 --- a/tests/test_kin_docs_002_regression.py +++ b/tests/test_kin_docs_002_regression.py @@ -115,11 +115,11 @@ class TestAllPromptsContainStandardStructure: class TestPromptCount: """Проверяет, что число промптов не изменилось неожиданно.""" - def test_prompt_count_is_25(self): - """В agents/prompts/ ровно 25 файлов .md.""" + def test_prompt_count_is_26(self): + """В agents/prompts/ ровно 26 файлов .md.""" count = len(_prompt_files()) - assert count == 25, ( # 25 промптов — актуально на 2026-03-19 (см. git log agents/prompts/) - f"Ожидалось 25 промптов, найдено {count}. " + assert count == 26, ( # 26 промптов — актуально на 2026-03-19, +knowledge_synthesizer (KIN-DOCS-003, см. git log agents/prompts/) + f"Ожидалось 26 промптов, найдено {count}. " "Если добавлен новый промпт — обнови этот тест." ) diff --git a/tests/test_phases.py b/tests/test_phases.py index 84ab948..f0f5fbc 100644 --- a/tests/test_phases.py +++ b/tests/test_phases.py @@ -78,6 +78,7 @@ def test_validate_roles_strips_and_lowercases(): @pytest.mark.parametrize("roles,expected", [ + # 1 исследователь → нет knowledge_synthesizer, architect последний ( ["business_analyst"], ["business_analyst", "architect"], @@ -86,17 +87,21 @@ def test_validate_roles_strips_and_lowercases(): ["tech_researcher"], ["tech_researcher", "architect"], ), + # ≥2 исследователей → knowledge_synthesizer авто-вставляется перед architect (KIN-DOCS-003, 2026-03-19) ( ["marketer", "business_analyst"], - ["business_analyst", "marketer", "architect"], + ["business_analyst", "marketer", "knowledge_synthesizer", "architect"], ), ( ["ux_designer", "market_researcher", "tech_researcher"], - ["market_researcher", "tech_researcher", "ux_designer", "architect"], + ["market_researcher", "tech_researcher", "ux_designer", "knowledge_synthesizer", "architect"], ), ]) def test_build_phase_order_canonical_order_and_appends_architect(roles, expected): - """KIN-059: роли сортируются в канонический порядок, architect добавляется последним.""" + """KIN-059: роли сортируются в канонический порядок, architect добавляется последним. + + При ≥2 исследователях knowledge_synthesizer авто-вставляется перед architect. + """ assert build_phase_order(roles) == expected @@ -113,6 +118,28 @@ def test_build_phase_order_architect_always_last(): assert result[-1] == "architect" +def test_build_phase_order_single_researcher_no_synthesizer(): + """KIN-DOCS-003: 1 исследователь → knowledge_synthesizer НЕ вставляется.""" + result = build_phase_order(["business_analyst"]) + assert "knowledge_synthesizer" not in result + assert result == ["business_analyst", "architect"] + + +def test_build_phase_order_two_researchers_inserts_synthesizer(): + """KIN-DOCS-003: 2 исследователя → knowledge_synthesizer авто-вставляется перед architect.""" + result = build_phase_order(["market_researcher", "tech_researcher"]) + assert "knowledge_synthesizer" in result + assert result.index("knowledge_synthesizer") == len(result) - 2 + assert result[-1] == "architect" + + +def test_validate_roles_strips_knowledge_synthesizer(): + """KIN-DOCS-003: knowledge_synthesizer убирается из входных ролей — авто-управляемая роль.""" + result = validate_roles(["knowledge_synthesizer", "business_analyst"]) + assert "knowledge_synthesizer" not in result + assert "business_analyst" in result + + # --------------------------------------------------------------------------- # create_project_with_phases # --------------------------------------------------------------------------- @@ -146,7 +173,7 @@ def test_create_project_with_phases_other_phases_remain_pending(conn): conn, "proj1", "P1", "/path", description="Desc", selected_roles=["market_researcher", "tech_researcher"], ) - # market_researcher, tech_researcher, architect → 3 фазы + # market_researcher, tech_researcher, knowledge_synthesizer, architect → 4 фазы (KIN-DOCS-003) for phase in result["phases"][1:]: assert phase["status"] == "pending"