feat(docs): documenter output is committed to the project repo, not just the knowledge store

Documenter docs only ever reached DOCS_BASE_PATH=/app/docs (a host-mounted, RAG-indexed knowledge store) and were never committed to the project's git repository — so the documenter's deliverable never landed in the repo. write_doc now also writes the doc into the agent's workspace clone under docs/<type>/<file> and commits it onto the task branch via GitService.commit, so it rides the existing PR into the repository on merge. Best-effort: a documenter without a cloned workspace or task branch still succeeds (logged, never fatal); the /app/docs knowledge store + RAG indexing are unchanged. Adds tests that the doc is written into the workspace and committed onto the task branch, and that it no-ops cleanly without a branch.
This commit is contained in:
Renn F
2026-06-22 15:50:46 +02:00
parent 2bbd1c2e70
commit 3a3cd69e1e
2 changed files with 150 additions and 8 deletions
+79 -3
View File
@@ -194,21 +194,97 @@ class DocsService(BaseService):
if existing_path:
# UPDATE existing doc instead of creating new
return await self._update_existing_doc(
result = await self._update_existing_doc(
agent_id=agent_id,
existing_path=existing_path,
req=req,
doc_type=doc_type,
)
else:
# 5. No similar doc found - create new
return await self._create_new_doc(
result = await self._create_new_doc(
agent_id=agent_id,
team=team,
req=req,
doc_type=doc_type,
)
# 6. Persist the doc into the project's repo and commit it onto the task
# branch, so the documenter's output actually lands in the repository via
# the open PR — not only in the /app/docs knowledge store. Best-effort: a
# documenter without a cloned workspace / task branch still succeeds.
await self._commit_doc_to_repo(agent_id, req, doc_type)
return result
async def _commit_doc_to_repo(
self, agent_id: str, req: WriteDocInput, doc_type: str
) -> None:
"""Write the doc into the project's workspace clone and commit it onto
the task branch, so it persists to the repository through the open PR.
Best-effort: any failure (no task branch yet, workspace not cloned, a git
hiccup) is logged and swallowed — it must never fail the doc write.
"""
from roboco.services.git import get_git_service
from roboco.services.project import get_project_service
try:
result = await self.session.execute(
select(TaskTable).where(TaskTable.id == req.task_id)
)
task = result.scalar_one_or_none()
if task is None or not task.branch_name or task.project_id is None:
return
project = await get_project_service(self.session).get(
UUID(str(task.project_id))
)
if project is None:
return
actor = self._agent_uuid(agent_id)
if actor is None:
return
git = get_git_service(self.session)
workspace = await git.get_workspace(project.slug, actor)
subfolder = TYPE_SUBFOLDERS[doc_type]
rel_path = (
f"docs/{subfolder}/{req.filename}"
if subfolder
else f"docs/{req.filename}"
)
await self._write_file(workspace / rel_path, req.content)
await git.commit(
branch_name=task.branch_name,
message=f"docs: {req.title}",
task_id=req.task_id,
files=[rel_path],
actor_agent_id=actor,
)
self.log.info(
"Documentation committed to project repo",
agent_id=agent_id,
task_id=str(req.task_id),
path=rel_path,
)
except Exception as exc:
self.log.warning(
"Could not commit documentation to repo (non-fatal)",
agent_id=agent_id,
task_id=str(req.task_id),
error=str(exc),
)
@staticmethod
def _agent_uuid(agent_id: str) -> UUID | None:
"""Resolve an agent slug or UUID string to its UUID, or None."""
from roboco.seeds.initial_data import AGENT_UUIDS
try:
return UUID(agent_id)
except ValueError:
raw = AGENT_UUIDS.get(agent_id)
return UUID(str(raw)) if raw is not None else None
async def _find_similar_doc(
self,
title: str,
+66
View File
@@ -802,3 +802,69 @@ async def test_list_docs_for_team_shallow_path_infers_readme(
):
docs = await svc._list_docs_for_team("backend")
assert any(d.doc_type == "readme" for d in docs)
@pytest.mark.asyncio
async def test_commit_doc_to_repo_writes_into_workspace_and_commits(
docs_setup: dict,
db_session: AsyncSession,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A doc write also commits the file into the project repo on the task branch."""
svc = docs_setup["svc"]
task_id = docs_setup["task_id"]
agent_uuid = docs_setup["agent_id"]
task = (
await db_session.execute(select(TaskTable).where(TaskTable.id == task_id))
).scalar_one()
task.branch_name = "feature/backend/ABC12345"
await db_session.flush()
mock_git = MagicMock()
mock_git.get_workspace = AsyncMock(return_value=tmp_path)
mock_git.commit = AsyncMock(return_value={"sha": "deadbeef"})
monkeypatch.setattr(
"roboco.services.git.get_git_service", lambda _session: mock_git
)
req = WriteDocInput(
task_id=task_id,
filename="guide.md",
doc_type="api",
title="API Guide",
content="# API Guide\n",
)
await svc._commit_doc_to_repo(str(agent_uuid), req, "api")
# The doc landed in the workspace repo under docs/...
assert (tmp_path / "docs" / "api" / "guide.md").read_text() == "# API Guide\n"
# ...and was committed onto the task branch.
mock_git.commit.assert_awaited_once()
kwargs = mock_git.commit.await_args.kwargs
assert kwargs["branch_name"] == "feature/backend/ABC12345"
assert kwargs["files"] == ["docs/api/guide.md"]
assert kwargs["task_id"] == task_id
@pytest.mark.asyncio
async def test_commit_doc_to_repo_skips_without_task_branch(
docs_setup: dict, monkeypatch: pytest.MonkeyPatch
) -> None:
"""No task branch yet → best-effort no-op (no git commit, no raise)."""
svc = docs_setup["svc"]
mock_git = MagicMock()
mock_git.commit = AsyncMock()
monkeypatch.setattr(
"roboco.services.git.get_git_service", lambda _session: mock_git
)
req = WriteDocInput(
task_id=docs_setup["task_id"],
filename="x.md",
doc_type="api",
title="X",
content="x",
)
await svc._commit_doc_to_repo(str(docs_setup["agent_id"]), req, "api")
mock_git.commit.assert_not_awaited()