Skip to content

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:

  1. Polls the GitLab group for recently-merged MRs
  2. Matches changed file paths against the feature registry (features/registry.yaml)
  3. Marks affected feature documentation as stale
  4. Optionally triggers DocUpdateGenerator to create an update MR (when MINH_AUTO_DOC_UPDATE_ENABLED is true)
  5. Optionally runs an LLM-based confidence review and auto-merges the doc MR if it passes all gates (when MINH_DOC_AUTO_MERGE_ENABLED is 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:

  1. SkipMessageParseError exceptions (triggered by unknown message types such as rate_limit_event) are logged and skipped so the remaining stream is still consumed.
  2. RetryProcessError, CLIConnectionError, and CLI error messages re-raised as plain Exception by the SDK (matched by "exit code" or "Command failed" in the message) trigger up to max_retries retries (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:

  1. Diff size check — The MR diff is fetched via glab mr diff. If it exceeds MINH_DOC_AUTO_MERGE_MAX_DIFF_LINES (default 300), the MR is left for human review.
  2. 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.
  3. 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": "..."}).
  4. 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:

  1. Factual plausibility — flags hallucinated endpoints, function names, or config keys
  2. Style preservation — heading structure, tone, and formatting conventions
  3. YAML front matter integrity — all required fields present, last_updated correct
  4. No agent dialogue leakage — no LLM reasoning traces or tool call artifacts
  5. Completeness — content is not truncated mid-sentence or mid-section
  6. 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=False and reason set 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 merge command fails, verdict.merged is False and 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 merge failed
  • 👀 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:

  1. MERGE on path only: MERGE (d:Document {path: $path}) — the MERGE pattern targets only the path property, without including the :Team label or repository property. This ensures that a single canonical Document node is matched or created per path, regardless of label or repository metadata.
  2. SET label and properties separately: SET d:Team, d.repository = ... — the :Team label and repository property 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 with doc_type = 'guide' and is_stale = false.
  • Existing relationships (ON MATCH): doc_type is updated to 'guide', but is_stale is preserved via coalesce(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.

See also

  • Shared MR review utilities: src/utils/mr_review_utils.py (used by both DocUpdateGenerator and SelfMaintenanceService)
  • 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