Merge Monitor Pipeline¶
Detects recently-merged MRs affecting tracked features and marks documentation as stale, triggering automatic doc update MRs.
Overview¶
The Merge Monitor runs on a configurable interval (default 30 minutes) and:
- Polls the GitLab group for recently-merged MRs
- Matches changed file paths against the feature registry (
features/registry.yaml) - Marks affected feature documentation as stale
- Optionally triggers
DocUpdateGeneratorto create an update MR (whenMINH_AUTO_DOC_UPDATE_ENABLEDis true) - Optionally runs an LLM-based confidence review and auto-merges the doc MR if it passes all gates (when
MINH_DOC_AUTO_MERGE_ENABLEDis true)
The last-checked timestamp is persisted in Redis (key merge_monitor:last_checked, TTL 2 hours) so the monitor resumes correctly across restarts. If Redis is unavailable, it falls back to an in-memory timestamp or a default lookback of 35 minutes.
Key Components¶
| Component | Location | Role |
|---|---|---|
MergeMonitorService |
src/services/merge_monitor_service.py |
Orchestrates the detection cycle |
DocUpdateGenerator |
src/services/doc_update_generator.py |
LLM-powered doc update + MR creation + optional auto-merge |
AgentDocUpdater |
src/core/agent_doc_updater.py |
Multi-turn Agent SDK doc updates and stub generation |
FeatureManager |
src/services/feature_manager.py |
Loads feature registry, matches code paths |
mr_review_utils |
src/utils/mr_review_utils.py |
Shared helpers for fetching diffs, waiting for CI, merging MRs, and parsing reviewer verdicts |
agent_sdk_config |
src/core/agent_sdk_config.py |
Shared Agent SDK configuration: auth env, CLI path resolution, and stderr capture |
agent_sdk_utils |
src/core/agent_sdk_utils.py |
Resilience wrappers for Agent SDK queries (skip + retry) |
Doc Generation Modes¶
AgentDocUpdater exposes two public methods:
| Method | Purpose |
|---|---|
generate_update() |
Updates an existing document using the UPDATE_SYSTEM_PROMPT |
generate_stub() |
Creates an initial documentation stub for a feature that has no docs yet, using the STUB_SYSTEM_PROMPT |
Both methods share a common internal runner (_run_agent_query) that executes a multi-turn Agent SDK query (model claude-opus-4-6, max 10 turns) with MCP tools for codebase exploration.
DocUpdateGenerator delegates to these methods when MINH_AGENT_SDK_ENABLED is true. If the Agent SDK call fails, it falls back to a single-turn LLM call automatically.
Prompt Guidelines¶
Both update and stub prompts now include additional quality instructions for the LLM:
- Usage examples: Where applicable, include brief usage examples or invocation snippets for tools, APIs, or commands described in the documentation.
- Error cases: Note important error cases, failure modes, or prerequisites (e.g., required config, missing dependencies).
- Cross-references (updates only): Add a "See also" section with links to related documentation when known.
- Coverage consistency (updates only): Ensure all topics mentioned in the document overview or introduction are covered in the body; add a placeholder noting expansion is needed if a topic cannot be documented from the available code changes.
Validation Gates¶
Before accepting LLM-generated content, DocUpdateGenerator runs validate_generated_content to check that front matter and structural integrity are preserved. Updates that fail validation are skipped with a warning.
After a successful commit, DocUpdateGenerator runs a non-blocking MkDocs nav validation (if an mkdocs_validator is configured) and reports any orphaned or missing files as warnings.
Diff Handling¶
Large MR diffs are truncated to approximately 12 KB (~3 000 tokens) before being passed to the LLM to stay within context limits.
Stub Generation¶
When a feature lacks existing documentation, generate_stub() produces a starting-point document that includes:
- YAML front matter (
title,status: draft,last_updated) - Placeholder sections (Overview, Architecture, Usage, Configuration) as appropriate
<!-- TODO: expand -->markers for sections needing human input- Brief usage examples or invocation snippets where applicable
- Notes on important error cases, failure modes, or prerequisites
Output Normalization¶
Both _extract_agent_markdown (Agent SDK path) and _extract_markdown (single-turn fallback) normalize output to always end with a trailing newline character. Empty strings are returned unchanged.
Agent SDK Resilience¶
The Agent SDK query path (_run_agent_query) incorporates two layers of resilience via resilient_query in src/core/agent_sdk_utils.py:
- Skip —
MessageParseErrorexceptions (triggered by unknown message types such asrate_limit_event) are logged and skipped so the remaining stream is still consumed. - Retry —
ProcessError,CLIConnectionError, and CLI error messages re-raised as plainExceptionby the SDK (matched by"exit code"or"Command failed"in the message) trigger up tomax_retriesretries (default 1) with exponential backoff (default base 2.0 s). This masks intermittent CLI subprocess crashes.
If all retries are exhausted, the last error is re-raised and the caller (DocUpdateGenerator) falls back to a single-turn LLM call.
Stderr Capture¶
All Agent SDK consumers — including AgentDocUpdater — now pass a stderr callback via build_stderr_callback() from src/core/agent_sdk_config.py. Without this callback, the Agent SDK discards Claude CLI stderr output and only surfaces a generic "Check stderr output for details" message on exit-code-1 crashes. The callback logs each stderr line at WARNING level under a label-specific logger (e.g., core.agent_sdk_config.stderr.doc-updater).
Usage example:
from core.agent_sdk_config import build_agent_env, build_stderr_callback, resolve_cli_path
options = ClaudeAgentOptions(
...,
env=build_agent_env(),
cli_path=resolve_cli_path(),
stderr=build_stderr_callback("doc-updater"),
)
MCP Stdio Safety¶
The MCP tool servers used by Agent SDK queries (mcp_doc_tools_server.py, mcp_conversation_tools_server.py, mcp_maintenance_tools_server.py) redirect all Python logging and structlog output to stderr before any other imports. This prevents library code that writes to stdout from corrupting the JSON-RPC protocol, which would otherwise cause the Claude CLI subprocess to crash with exit-code-1.
Doc Auto-Merge Pipeline¶
When MINH_DOC_AUTO_MERGE_ENABLED is true, DocUpdateGenerator runs a post-creation confidence review on each doc-update MR via the _review_and_merge_doc_mr method. The pipeline proceeds through four sequential gates:
- Diff size check — The MR diff is fetched via
glab mr diff. If it exceedsMINH_DOC_AUTO_MERGE_MAX_DIFF_LINES(default 300), the MR is left for human review. - CI gate — Polls the MR's CI pipeline (typically
mkdocs build --strict) at a configurable interval until it passes, fails, or times out. If CI does not pass, auto-merge is skipped. - LLM reviewer agent — A dedicated reviewer agent (model
claude-sonnet-4-20250514, max 5 turns) evaluates the diff against six criteria and outputs a JSON verdict ({"approved": true/false, "reason": "..."}). - Merge or comment — If approved, the MR is squash-merged with source branch removal. If rejected, a review comment is posted on the MR explaining why, and the MR is left for human review.
Reviewer Criteria¶
The DOC_REVIEWER_SYSTEM_PROMPT instructs the reviewer agent to evaluate:
- Factual plausibility — flags hallucinated endpoints, function names, or config keys
- Style preservation — heading structure, tone, and formatting conventions
- YAML front matter integrity — all required fields present,
last_updatedcorrect - No agent dialogue leakage — no LLM reasoning traces or tool call artifacts
- Completeness — content is not truncated mid-sentence or mid-section
- No sensitive information — no secrets, tokens, or internal URLs
The reviewer operates read-only with no MCP tools and follows a "when in doubt, reject" policy.
ReviewVerdict¶
The shared ReviewVerdict dataclass (from utils/mr_review_utils) carries the outcome:
| Field | Type | Description |
|---|---|---|
approved |
bool |
Whether the reviewer approved the MR (default False) |
reason |
str |
Brief explanation from the reviewer |
merged |
bool |
Whether the MR was actually merged (default False) |
Error Handling¶
- If any step in the review pipeline raises an exception, the verdict is returned with
approved=Falseandreasonset to the error message. The MR remains open for human review. - If the reviewer output cannot be parsed as JSON, the verdict defaults to rejected with reason
"Could not parse reviewer output". - If the reviewer approves but the
glab mr mergecommand fails,verdict.mergedisFalseand a warning is logged.
Slack Notifications¶
MergeMonitorService appends the review verdict to the Slack notification for each auto-created doc MR:
- 🚀 Auto-merged — reviewer approved and merge succeeded
- ⚠️ Approved but merge failed — reviewer approved but
glab mr mergefailed - 👀 Left for human review — reviewer rejected or auto-merge was skipped
System Prompts¶
| Constant | Description |
|---|---|
UPDATE_SYSTEM_PROMPT |
Used for updating existing documentation |
STUB_SYSTEM_PROMPT |
Used for creating new documentation stubs |
DOC_REVIEWER_SYSTEM_PROMPT |
Used by the doc auto-merge reviewer agent |
AGENT_SYSTEM_PROMPT |
Backward-compatible alias for UPDATE_SYSTEM_PROMPT |
Registry Parsing Resilience¶
FeatureManager.load_features_from_yaml() defensively validates each entry in the features list before attempting to construct a FeatureDefinition. Entries that are None (e.g., a dangling - in YAML) or non-dict types (e.g., bare strings or integers) are skipped with a warning log rather than raising an exception. This prevents a single malformed entry in features/registry.yaml from blocking the entire feature-loading pipeline.
For example, given a registry file with a trailing dangling list marker:
features:
- name: Valid Feature
description: A real feature
status: active
-
- name: Another Valid
description: Also real
The loader logs a warning for the null entry at index 1 and returns the two valid FeatureDefinition objects. The same handling applies to scalar entries (strings, integers) that appear where a feature mapping is expected.
Graph Sync Behavior¶
When FeatureManager.sync_feature_to_graph() creates relationships, it uses a two-step MERGE/SET pattern to avoid Document.path uniqueness constraint violations:
- MERGE on path only:
MERGE (d:Document {path: $path})— the MERGE pattern targets only thepathproperty, without including the:Teamlabel orrepositoryproperty. This ensures that a single canonicalDocumentnode is matched or created per path, regardless of label or repository metadata. - SET label and properties separately:
SET d:Team, d.repository = ...— the:Teamlabel andrepositoryproperty are applied after the MERGE, so they do not affect node identity or trigger constraint conflicts when the same path appears across different contexts.
For DOCUMENTED_IN relationships, idempotent merge semantics are preserved:
- New relationships (
ON CREATE): initialized withdoc_type = 'guide'andis_stale = false. - Existing relationships (
ON MATCH):doc_typeis updated to'guide', butis_staleis preserved viacoalesce(r.is_stale, false), preventing accidental reset of staleness state during re-sync.
For IMPLEMENTED_IN relationships, the same MERGE/SET separation applies: the Document node is merged by path alone, then the :Team label and repository are set afterward.
Configuration¶
| Env Variable | Default | Description |
|---|---|---|
MINH_MERGE_MONITOR_ENABLED |
true |
Enable/disable the scheduler |
MINH_MERGE_MONITOR_INTERVAL |
30 |
Minutes between monitor cycles |
MINH_AUTO_DOC_UPDATE_ENABLED |
false |
When true, automatically creates doc-update MRs for newly-stale features |
MINH_DOC_AUTO_MERGE_ENABLED |
false |
When true, runs the LLM reviewer agent on doc-update MRs and auto-merges if approved |
MINH_DOC_AUTO_MERGE_MAX_DIFF_LINES |
300 |
Maximum diff lines eligible for auto-merge; larger diffs are left for human review |
MINH_DOC_AUTO_MERGE_CI_POLL_INTERVAL |
30 |
Seconds between CI status polls during auto-merge |
MINH_DOC_AUTO_MERGE_CI_POLL_TIMEOUT |
600 |
Maximum seconds to wait for CI to pass before skipping auto-merge |
MINH_AGENT_SDK_ENABLED |
false |
When true, uses multi-turn Agent SDK for both updates and stub generation |
MINH_AGENT_SDK_AUTH |
api |
Auth mode for Agent SDK: api (uses ANTHROPIC_API_KEY) or subscription (uses claude login credentials) |
MINH_AGENT_SDK_CLI_PATH |
— | Override the Claude CLI path for subscription auth mode (falls back to which claude) |
MINH_DEFAULT_REVIEWERS |
— | Comma-separated GitLab usernames to assign as reviewers on doc-update MRs |
Internal Import Convention¶
As of the latest refactoring, internal Python imports within the src/ tree use package-relative paths without the src. prefix (e.g., from core.repo_registry import get_repo_config rather than from src.core.repo_registry import ...). This applies to all core modules including agent_doc_updater.py. The sys.path manipulation previously used in some modules (such as content_generator.py) has been removed in favor of proper package configuration.
Related¶
- Feature Registry:
features/registry.yaml(repo root) - Safe GitLab Writer
See also¶
- Shared MR review utilities:
src/utils/mr_review_utils.py(used by bothDocUpdateGeneratorandSelfMaintenanceService) - Agent SDK config helpers:
src/core/agent_sdk_config.py(auth env, CLI path, stderr capture) - Agent SDK resilience wrappers:
src/core/agent_sdk_utils.py(skip + retry logic)
Feature Repositories¶
minh→ project_id:the-smithy1/agents/minh
Code Paths to Explore¶
src/services/feature_manager.pyinminhsrc/services/doc_update_generator.pyinminhsrc/services/merge_monitor_service.pyinminh- Keep stdout clean on a stdio protocol channel
- A resilience ladder for streaming agent calls
- Capture the subprocess stderr you were told to check
- A gate ladder for auto-merging generated changes