✅ fresh
last synced 2026-06-17T22:39:35.514075+00:00 · coverage 100% (
ai-techwriter)Validation by Beadloom
doc_sync— same source assync-check.
ai-techwriter
Deterministic, seam-isolated PR-triggered documentation-refresh harness.
Source
src/beadloom/ai_agents/ai_techwriter/
Overview
The AI tech-writer is a deterministic harness that keeps documentation in sync with code on every pull request. It orchestrates an external AI agent (Goose + a model) behind mockable seams so the whole loop is unit-testable without the agent, the model, or the network. Behaviour is the BDL-049/050 model, byte-unchanged by the BDL-051 / S2 move into the beadloom package: PR-trigger, --since merge-base, --target pr-branch, the {ok, flagged, infra} verdict, the loop-guard, and AI_TW_PAT authentication.
The loop:
- discover scope — parse
beadloom sync-check --jsoninto drift items; - build packet — per stale doc, assemble a context packet;
- invoke agent (seam) — Goose rewrites the doc via the shipped recipe;
- re-baseline —
beadloom sync-updateand iterate to a global fixpoint; - gate —
beadloom ci; - publish (seam) — commit onto the PR head branch + comment (pr-branch), or cut a branch + open a PR (branch-pr);
- emit a run-record.
Invocation
python -m beadloom.ai_agents.ai_techwriter --platform {github|gitlab} \
--target {pr-branch|branch-pr} --since <ref>
# console-script equivalent:
beadloom-ai-techwriter --platform github --target pr-branch --since <ref>The harness ships inside the installed beadloom package, so adopters invoke it directly (no vendoring). beadloom setup-ai-techwriter --platform {github|gitlab} scaffolds the CI workflow that calls it, the getting-started guide, and the operator artifacts (recipe.yaml + provision-runner.sh, copied from package data).
Modules
- runner.py — the deterministic orchestrator (the loop above) and the
{ok, flagged, infra}verdict classification (classify_verdict). Per-doc repair runs in a bounded session pool (HarnessConfig.max_parallel, default 3 — RAM-aware for the 8GB self-hosted VPS); each doc gets its own Goose session, and the per-session results are folded back in stale order so the aggregate / verdict is identical whether the pool ran them sequentially or concurrently. - backoff.py — per-session 429/5xx exponential back-off (
RateLimitError,retry_with_backoff) so concurrent sessions degrade gracefully against the rate-limited model endpoint instead of failing; thesleepseam is injected so the policy is deterministic and instant under test. - scope.py — drift discovery from
beadloom sync-check --json; when a--sincebaseline is given it also applies symbol-level narrowing (symbol_scope) before returning the stale set. - symbol_scope.py — symbol-level scope narrowing (BDL-052 S4). Narrows the stale set from "changed FILE → all its doc pairs" to "doc references a CHANGED symbol", killing the god-file fan-out (a one-symbol edit to
cli.pyno longer drifts every doc the file is linked to). For each stale pair it intersects the symbols whose body changed in the touched file (vs--since, via git hunks ∩ a Pythondef/classline-range map) with the symbols the doc references. The changed-symbol set unions BOTH diff sides — new-side edited/added defs AND old-side removed/renamed defs (so a doc naming a symbol that was deleted, whose name is gone from the new content, is still attributed and KEPT, never silently dropped); an empty intersection drops the pair from the agent run ANDsync-update-baselines it sosync-checkstill reaches 0 without a rewrite. Conservative by construction: any unavailable/ambiguous attribution (no--since, a non-symbol drift reason, a non-Python file, a change outside any symbol body) keeps the pair in scope — it never under-refreshes. Shared by the local flow and the CI agent (both reach it throughdiscover_scope). - packet.py — per-doc context-packet assembly.
- seams.py — the mockable seams: the agent runner (Goose) and the review publisher (GitHub/GitLab, pr-branch + branch-pr variants), plus their fakes.
- provider.py — Goose provider config (Qwen via an OpenAI-compatible endpoint) and
default_recipe_path()(the recipe shipped as package data, located viaimportlib.resources). - commands.py — patchable wrappers around
beadloom/gitsubprocess calls. - models.py — typed, immutable harness data structures.
- runs_store.py — append-only run-record store.
- cli.py / main.py — the thin Click entrypoint CI invokes.
Configuration
QWEN_API_KEY(required on the runner; never inlined) — the model key.QWEN_BASE_URL(optional) — workspace MaaS endpoint; falls back to the generic DashScope gateway.AI_TW_PAT— token used for the refresh commit + PR/MR comment so the push re-triggers the required status check.
Boundary
Part of the ai_agents domain (a leaf consumer). The harness consumes beadloom / bd via subprocess seams and must not be imported by the core (core-no-import-ai-agents / application-no-import-ai-agents rules).