diff --git a/coworker/agent.py b/coworker/agent.py index 9ed546fa..0b30a1a3 100644 --- a/coworker/agent.py +++ b/coworker/agent.py @@ -25,7 +25,7 @@ from .environment import environment_context from .memory import MemoryStore, Scope, format_memories, memory_tools from .permissions import Mode, PermissionEngine -from .project import load_agents_md +from .project import load_agents_md, load_wiki_index from .roots import RootDir, normalize_roots, render_context from .providers import ProviderClient, ProviderRouter from .overrides import RiskOverrideStore @@ -280,6 +280,9 @@ def build_engine( conventions = load_agents_md(ws) if conventions: instructions = f"{instructions}\n\n{conventions}" + wiki = load_wiki_index(ws) + if wiki: + instructions = f"{instructions}\n\n{wiki}" if memory_store is not None: registry.register_all( diff --git a/coworker/project.py b/coworker/project.py index c4775819..02229af7 100644 --- a/coworker/project.py +++ b/coworker/project.py @@ -1,4 +1,4 @@ -"""Project context — AGENTS.md ingestion (root + global) into the system prompt.""" +"""Project context — AGENTS.md + knowledge-wiki ingestion into the system prompt.""" from __future__ import annotations @@ -38,3 +38,34 @@ def load_agents_md( for label, text in parts ] return "Project conventions:\n" + "\n\n".join(blocks) + + +# Knowledge-wiki index injection. Convention: a workspace that maintains a +# long-lived knowledge base keeps it as markdown pages under `wiki/` with an +# `INDEX.md` map-of-content. Injecting ONLY the index (progressive disclosure, +# same trick as the skills catalog) gives every session standing knowledge of +# what is known — pages load on demand via the normal file tools — at a context +# cost that stays flat as the wiki grows. +_WIKI_INDEX_CAP = 6_000 # chars; an index past this is a wiki smell, not a need + + +def load_wiki_index(workspace: str | Path) -> str: + """System-prompt block from `/wiki/INDEX.md`, if present.""" + index = Path(workspace).expanduser().resolve() / "wiki" / "INDEX.md" + if not index.is_file(): + return "" + try: + text = index.read_text(encoding="utf-8").strip() + except OSError: + return "" + if not text: + return "" + if len(text) > _WIKI_INDEX_CAP: + text = text[:_WIKI_INDEX_CAP] + "\n… (index truncated — trim it; an index is a map, not a page)" + return ( + "This workspace maintains a knowledge wiki (markdown pages under `wiki/`, " + "linked with [[wikilinks]]). Its index follows. Open pages on demand with " + "the file tools; when you learn something durable that changes a page's " + "claims, revise that page in place rather than appending duplicates.\n" + f"\n{text}\n" + ) diff --git a/tests/test_skills.py b/tests/test_skills.py index 203d163f..4465c512 100644 --- a/tests/test_skills.py +++ b/tests/test_skills.py @@ -101,3 +101,35 @@ def test_build_engine_code_has_agents_md_and_skills(tmp_path): assert engine.agent_name == "code" finally: engine.executor.close() + + +def test_build_engine_injects_wiki_index(tmp_path): + (tmp_path / "wiki").mkdir() + (tmp_path / "wiki" / "INDEX.md").write_text( + "# Home Wiki\n- [[adrian/heart-rate]] — baselines\n" + ) + engine = build_engine(agent=code_agent(), workspace=tmp_path, provider=_Stub()) + try: + sys_msg = engine.messages[0]["content"] + assert "knowledge wiki" in sys_msg + assert "[[adrian/heart-rate]]" in sys_msg + assert "revise that page in place" in sys_msg + finally: + engine.executor.close() + + +def test_no_wiki_no_injection(tmp_path): + engine = build_engine(agent=code_agent(), workspace=tmp_path, provider=_Stub()) + try: + assert "knowledge wiki" not in engine.messages[0]["content"] + finally: + engine.executor.close() + + +def test_wiki_index_truncated_when_huge(tmp_path): + (tmp_path / "wiki").mkdir() + (tmp_path / "wiki" / "INDEX.md").write_text("x" * 10_000) + from coworker.project import load_wiki_index + + block = load_wiki_index(tmp_path) + assert "index truncated" in block and len(block) < 7_000