This file is a merged representation of the entire codebase, combined into a single document by Repomix.
The content has been processed where content has been compressed (code blocks are separated by ⋮---- delimiter).

# File Summary

## Purpose
This file contains a packed representation of the entire repository's contents.
It is designed to be easily consumable by AI systems for analysis, code review,
or other automated processes.

## File Format
The content is organized as follows:
1. This summary section
2. Repository information
3. Directory structure
4. Repository files (if enabled)
5. Multiple file entries, each consisting of:
  a. A header with the file path (## File: path/to/file)
  b. The full contents of the file in a code block

## Usage Guidelines
- This file should be treated as read-only. Any changes should be made to the
  original repository files, not this packed version.
- When processing this file, use the file path to distinguish
  between different files in the repository.
- Be aware that this file may contain sensitive information. Handle it with
  the same level of security as you would the original repository.

## Notes
- Some files may have been excluded based on .gitignore rules and Repomix's configuration
- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files
- Files matching patterns in .gitignore are excluded
- Files matching default ignore patterns are excluded
- Content has been compressed - code blocks are separated by ⋮---- delimiter
- Files are sorted by Git change count (files with more changes are at the bottom)

# Directory Structure
```
.cursor/
  rules/
    project-level.mdc
  skills/
    gitnexus/
      gitnexus-cli/
        SKILL.md
      gitnexus-debugging/
        SKILL.md
      gitnexus-exploring/
        SKILL.md
      gitnexus-guide/
        SKILL.md
      gitnexus-impact-analysis/
        SKILL.md
      gitnexus-refactoring/
        SKILL.md
    humanizer/
      SKILL.md
    metagit-bootstrap/
      scripts/
        bootstrap-config.zsh
      SKILL.md
    metagit-cli/
      metagit-cli/
        SKILL.md
      SKILL.md
    metagit-config-refresh/
      SKILL.md
    metagit-control-center/
      scripts/
        control-cycle.zsh
      SKILL.md
    metagit-gating/
      scripts/
        gate-status.zsh
      SKILL.md
    metagit-gitnexus/
      scripts/
        analyze-targets.zsh
      SKILL.md
    metagit-multi-repo/
      SKILL.md
    metagit-projects/
      SKILL.md
    metagit-release-audit/
      SKILL.md
    metagit-repo-impact/
      SKILL.md
    metagit-upstream-scan/
      scripts/
        upstream-scan.zsh
      SKILL.md
    metagit-upstream-triage/
      SKILL.md
    metagit-workspace-scope/
      SKILL.md
    metagit-workspace-sync/
      SKILL.md
  mcp.json
.github/
  ISSUE_TEMPLATE/
    bug_report.yml
    config.yml
    feature_request.yml
  workflows/
    docker.yaml
    docs.yaml
    release.yaml
    semantic-release.yaml
    test.yaml
  CODEOWNERS
  dependabot.yml
  FUNDING.yml
  PULL_REQUEST_TEMPLATE.md
.mex/
  context/
    architecture.md
    conventions.md
    decisions.md
    mcp-runtime.md
    setup.md
    stack.md
  patterns/
    add-cli-command.md
    add-managed-repo-search.md
    add-mcp-tool.md
    bootstrap-metagit-config.md
    debug-mcp-runtime.md
    debug-workspace-discovery.md
    INDEX.md
    mcp-cross-project-dependencies.md
    mcp-project-context.md
    metagit-web-api.md
    README.md
    run-graphify-analysis.md
    update-release-workflow.md
  AGENTS.md
  config.json
  ROUTER.md
  SETUP.md
  SYNC.md
docs/
  reference/
    metagit-config.full-example.yml
    metagit-config.md
    metagit-web.md
    workspace-layout-api.md
  app.logic.md
  architecture-diagram.py
  cli_reference.md
  development.md
  hermes-iac-workspace-guide.md
  hermes-orchestrator-workspace.md
  install.md
  repository_detection.md
  secrets.analysis.md
  secrets.definitions.yml
  terminology.md
examples/
  hermes-orchestrator/
    .metagit.yml
    answers.example.yml
  appconfig_example.yml
  categorize_directory.py
  cli_record_example.py
  datetime_serialization_fix_example.py
  detection_config_example.yml
  detection_manager_config_example.py
  detection_manager_example.py
  enhanced_fuzzyfinder_demo.py
  fuzzyfinder_comprehensive_test.py
  fuzzyfinder_custom_colors_demo.py
  fuzzyfinder_debug_test.py
  fuzzyfinder_preview_test.py
  fuzzyfinder_simple_colors.py
  fuzzyfinder_simple_test.py
  get_git_files.py
  gitcache_differences_example.py
  gitcache_example.py
  load_config_from_yaml.py
  metagit.umbrella.yml
  provider_example.py
  README_fuzzyfinder_tests.md
  record_conversion_advanced_example.py
  record_conversion_example.py
  record_manager_example.py
  repository_analysis_example.py
  repository_detection.py
  test_appconfig_env.py
  test_record_manager_simple.py
schemas/
  backend_metadata.yml
  metagit_appconfig.schema.json
  metagit_config.schema.json
  repo_metadata.yml
scripts/
  prepush-gate.py
  prepush-gate.zsh
  validate_skills.py
skills/
  metagit-bootstrap/
    scripts/
      bootstrap-config.zsh
    SKILL.md
  metagit-cli/
    metagit-cli/
      SKILL.md
    SKILL.md
  metagit-config-refresh/
    SKILL.md
  metagit-control-center/
    scripts/
      control-cycle.zsh
    SKILL.md
  metagit-gating/
    scripts/
      gate-status.zsh
    SKILL.md
  metagit-gitnexus/
    scripts/
      analyze-targets.zsh
    SKILL.md
  metagit-multi-repo/
    SKILL.md
  metagit-projects/
    SKILL.md
  metagit-release-audit/
    SKILL.md
  metagit-repo-impact/
    SKILL.md
  metagit-upstream-scan/
    scripts/
      upstream-scan.zsh
    SKILL.md
  metagit-upstream-triage/
    SKILL.md
  metagit-workspace-scope/
    SKILL.md
  metagit-workspace-sync/
    SKILL.md
  README.md
src/
  metagit/
    cli/
      commands/
        __init__.py
        api.py
        appconfig.py
        config.py
        detect.py
        gitcache.py
        init.py
        mcp.py
        project_repo.py
        project_source.py
        project.py
        prompt.py
        record.py
        search.py
        skills.py
        web.py
        workspace.py
      __init__.py
      config_patch_ops.py
      json_output.py
      main.py
    core/
      api/
        __init__.py
        catalog_handler.py
        layout_handler.py
        server.py
      appconfig/
        __init__.py
        agent_mode.py
        display.py
        models.py
      config/
        __init__.py
        documentation_models.py
        example_generator.py
        graph_cypher_export.py
        graph_models.py
        graph_resolver.py
        manager.py
        models.py
        patch_service.py
        yaml_display.py
      detect/
        detectors/
          docker.py
          git.py
          python.py
          terraform.py
        __init__.py
        manager.py
        models.py
      flows/
        detect_flow/
          crews/
            project_understanding_crew/
              config/
                agents.yaml
                tasks.yaml
              project_understanding_crew.py
          tools/
            __init__.py
            human.py
            index_tools.py
          __init__.py
          main.py
      gitcache/
        __init__.py
        config.py
        manager.py
        README.md
      init/
        __init__.py
        models.py
        prompts.py
        registry.py
        renderer.py
        service.py
      mcp/
        services/
          bootstrap_sampling.py
          cross_project_dependencies.py
          discovery_context.py
          gitnexus_registry.py
          import_hint_scanner.py
          ops_log.py
          project_context.py
          repo_git_stats.py
          repo_ops.py
          session_store.py
          upstream_hints.py
          workspace_health.py
          workspace_index.py
          workspace_search.py
          workspace_semantic_search.py
          workspace_snapshot.py
          workspace_sync.py
          workspace_template.py
        tools/
          bootstrap_plan_only.py
          workspace_status.py
        __init__.py
        gate.py
        models.py
        protocols.py
        resources.py
        root_resolver.py
        runtime.py
        tool_registry.py
      project/
        manager.py
        models.py
        search_models.py
        search_service.py
        source_models.py
        source_sync.py
      prompt/
        __init__.py
        catalog.py
        models.py
        service.py
      providers/
        __init__.py
        base.py
        github.py
        gitlab.py
      record/
        __init__.py
        manager.py
        models.py
      skills/
        __init__.py
        installer.py
      utils/
        click.py
        common.py
        files.py
        fuzzyfinder.py
        logging.py
        userprompt.py
        yaml_class.py
      web/
        __init__.py
        config_handler.py
        config_preview.py
        graph_service.py
        job_store.py
        models.py
        ops_handler.py
        schema_tree.py
        server.py
        static_handler.py
      workspace/
        __init__.py
        agent_instructions.py
        catalog_models.py
        catalog_service.py
        context_models.py
        dedupe_resolver.py
        dependency_models.py
        health_models.py
        hydrate.py
        layout_context.py
        layout_executor.py
        layout_models.py
        layout_resolver.py
        layout_service.py
        manager.py
        models.py
        workspace_dedupe.py
    data/
      init-templates/
        application/
          .metagit.yml.tpl
          template.yaml
        hermes-orchestrator/
          .metagit.yml.tpl
          AGENTS.md.tpl
          template.yaml
        umbrella/
          .metagit.yml.tpl
          template.yaml
      prompts/
        gemini_prompt_example.md
        gemini_prompt_filled.md
        gemini_prompt.md
      skills/
        metagit-bootstrap/
          scripts/
            bootstrap-config.zsh
          SKILL.md
        metagit-cli/
          SKILL.md
        metagit-config-refresh/
          SKILL.md
        metagit-control-center/
          scripts/
            control-cycle.zsh
          SKILL.md
        metagit-gating/
          scripts/
            gate-status.zsh
          SKILL.md
        metagit-gitnexus/
          scripts/
            analyze-targets.zsh
          SKILL.md
        metagit-multi-repo/
          SKILL.md
        metagit-projects/
          SKILL.md
        metagit-release-audit/
          SKILL.md
        metagit-repo-impact/
          SKILL.md
        metagit-upstream-scan/
          scripts/
            upstream-scan.zsh
          SKILL.md
        metagit-upstream-triage/
          SKILL.md
        metagit-workspace-scope/
          SKILL.md
        metagit-workspace-sync/
          SKILL.md
        README.md
      templates/
        agent-standard/
          AGENTS.md.fragment
        hermes-orchestrator/
          AGENTS.md.fragment
          README.md
      web/
        assets/
          index-B315j_NF.css
          index-DOullneW.js
        favicon.svg
        icons.svg
        index.html
      build-files.yaml
      cd-files.json
      ci-files.json
      config-example-overrides.yml
      file-types.json
      metagit.config.yaml
      package-managers.json
      README.md
    __init__.py
    __main__.py
tasks/
  Taskfile.api.yml
  Taskfile.docker.yml
  Taskfile.gemini.yml
  Taskfile.git.yml
  Taskfile.github.yml
  Taskfile.mcp.yml
  Taskfile.workspace.yml
tests/
  api/
    test_catalog_api.py
    test_layout_api.py
    test_repo_search_api.py
  cli/
    commands/
      test_api.py
      test_config_patch.py
      test_init.py
      test_mcp.py
      test_project_repo.py
      test_project_source.py
      test_search.py
      test_skills.py
      test_web.py
  core/
    config/
      test_graph_cypher_export.py
      test_patch_service.py
    init/
      test_init_service.py
    mcp/
      services/
        test_bootstrap_sampling.py
        test_cross_project_dependencies.py
        test_import_hint_scanner.py
        test_project_context.py
        test_repo_ops.py
        test_session_store.py
        test_upstream_hints.py
        test_workspace_health.py
        test_workspace_index.py
        test_workspace_search.py
        test_workspace_semantic_search.py
        test_workspace_snapshot.py
        test_workspace_sync.py
        test_workspace_template.py
      test_gate.py
      test_models.py
      test_resources.py
      test_root_resolver.py
      test_runtime.py
      test_tool_registry.py
    prompt/
      test_prompt_service.py
    web/
      __init__.py
      test_config_handler.py
      test_config_preview.py
      test_graph_service.py
      test_job_store.py
      test_ops_handler.py
      test_schema_tree.py
    workspace/
      test_agent_instructions.py
      test_catalog_service.py
      test_context_models.py
      test_dedupe_resolver.py
      test_hydrate.py
      test_layout_service.py
  integration/
    test_mcp_workspace_flow.py
  scripts/
    test_prepush_gate_security.py
  conftest.py
  test_appconfig_display.py
  test_appconfig_init.disabled
  test_appconfig_models.py
  test_config_example_generator.py
  test_config_models.py
  test_config_yaml_display.py
  test_detect_manager.disabled
  test_documentation_graph_models.py
  test_gitcache.py
  test_project_manager_dedupe.py
  test_project_manager_prune.py
  test_project_manager_select_repo.py
  test_project_search_service.py
  test_project_source_models.py
  test_project_source_sync.py
  test_record_conversion.py
  test_record_manager.py
  test_record_models.py
  test_skills_installer.py
  test_utils_common_integration.py
  test_utils_common.py
  test_utils_files.py
  test_utils_fuzzyfinder.py
  test_utils_logging.py
  test_utils_userprompt.py
  test_utils_yaml_class.py
  test_workspace_dedupe.py
  test_workspace_index_service.py
web/
  public/
    favicon.svg
    icons.svg
  src/
    api/
      client.ts
    assets/
      hero.png
      react.svg
      vite.svg
    components/
      ConfigPreview.module.css
      ConfigPreview.tsx
      FieldEditor.module.css
      FieldEditor.tsx
      GraphDiagram.module.css
      GraphDiagram.tsx
      Layout.module.css
      Layout.tsx
      OpsPanel.module.css
      OpsPanel.tsx
      RepoTable.module.css
      RepoTable.tsx
      SchemaTree.module.css
      SchemaTree.tsx
      SyncDialog.module.css
      SyncDialog.tsx
    pages/
      AppconfigPage.tsx
      ConfigPage.module.css
      ConfigPage.tsx
      configQueries.ts
      graphQueries.ts
      MetagitConfigPage.tsx
      WorkspacePage.module.css
      WorkspacePage.tsx
      workspaceQueries.ts
    theme/
      ThemeProvider.tsx
      tokens.css
      useThemeStore.ts
    App.css
    App.tsx
    index.css
    main.tsx
  .gitignore
  eslint.config.js
  index.html
  package.json
  README.md
  tsconfig.app.json
  tsconfig.json
  tsconfig.node.json
  vite.config.ts
.cursorignore
.cursorrules
.dockerignore
.editorconfig
.env.example
.envrc
.gitignore
.gitleaksignore
.gitsecrets.lock
.metagit.example.yml
.metagit.yml
.python-version
AGENTS.md
CHANGELOG.md
configure.sh
Dockerfile
LICENSE.md
MANIFEST.in
metagit.config.yaml
mise.toml
mkdocs.yml
pyproject.toml
README.md
Secretfile.yml
Taskfile.yml
```

# Files

## File: .cursor/mcp.json
````json
{
  "mcpServers": {
    "sequentialthinking": {
      "command": "docker",
      "args": [
        "run",
        "--rm",
        "-i",
        "mcp/sequentialthinking"
      ]
    }
    // "playwrite": {
    //   "command": "npx",
    //   "args": [
    //     "-y",
    //     "@playwright/mcp@latest"
    //   ]
    // },
    // "github": {
    //   "command": "docker",
    //   "args": [
    //     "run",
    //     "-i",
    //     "--rm",
    //     "-e",
    //     "GITHUB_PERSONAL_ACCESS_TOKEN",
    //     "-e",
    //     "GITHUB_READ_ONLY=1",
    //     "-e",
    //     "GITHUB_DYNAMIC_TOOLSETS=1",
    //     "ghcr.io/github/github-mcp-server"
    //   ]
    // }
  }
}
````

## File: .github/ISSUE_TEMPLATE/bug_report.yml
````yaml
name: Bug Report
description: Report a bug in the Metagit CLI
title: "[BUG] "
labels: ["bug", "triage"]
assignees: []
body:
  - type: markdown
    attributes:
      value: |
        Thanks for taking the time to fill out this bug report for Metagit CLI!
  - type: checkboxes
    id: "checks"
    attributes:
      label: "Checks"
      options:
        - label: "I have updated to the lastest minor and patch version of Metagit CLI"
          required: true
        - label: "I have checked the documentation and this is not expected behavior"
          required: true
        - label: "I have searched [./issues](./issues?q=) and there are no duplicates of my issue"
          required: true
  - type: input
    id: metagit-version
    attributes:
      label: Metagit Version
      description: Which version of Metagit CLI are you using?
      placeholder: e.g., 0.5.2
    validations:
      required: true
  - type: input
    id: python-version
    attributes:
      label: Python Version
      description: Which version of Python are you using?
      placeholder: e.g., 3.10.5
    validations:
      required: true
  - type: input
    id: os
    attributes:
      label: Operating System
      description: Which operating system are you using?
      placeholder: e.g., macOS 12.6
    validations:
      required: true
  - type: dropdown
    id: installation-method
    attributes:
      label: Installation Method
      description: How did you install Metagit?
      options:
        - pip
        - git clone
        - binary
        - other
    validations:
      required: true
  - type: textarea
    id: steps-to-reproduce
    attributes:
      label: Steps to Reproduce
      description: Detailed steps to reproduce the behavior
      placeholder: |
        1. Install Metagit using...
        2. Run the command...
        3. See error...
    validations:
      required: true
  - type: textarea
    id: expected-behavior
    attributes:
      label: Expected Behavior
      description: A clear description of what you expected to happen
    validations:
      required: true
  - type: textarea
    id: actual-behavior
    attributes:
      label: Actual Behavior
      description: What actually happened
    validations:
      required: true
  - type: textarea
    id: additional-context
    attributes:
      label: Additional Context
      description: Any other relevant information, logs, screenshots, etc.
  - type: textarea
    id: possible-solution
    attributes:
      label: Possible Solution
      description: Optional - If you have suggestions on how to fix the bug
  - type: input
    id: related-issues
    attributes:
      label: Related Issues
      description: Optional - Link to related issues if applicable
````

## File: .github/ISSUE_TEMPLATE/config.yml
````yaml
blank_issues_enabled: false
````

## File: .github/ISSUE_TEMPLATE/feature_request.yml
````yaml
name: Feature Request
description: Suggest a new feature or enhancement for Metagit CLI
title: "[FEATURE] "
labels: ["enhancement", "triage"]
assignees: []
body:
  - type: markdown
    attributes:
      value: |
        Thanks for suggesting a new feature for Metagit CLI!
  - type: textarea
    id: problem-statement
    attributes:
      label: Problem Statement
      description: Describe the problem you're trying to solve. What is currently difficult or impossible to do?
      placeholder: I would like Metagit to...
    validations:
      required: true
  - type: textarea
    id: proposed-solution
    attributes:
      label: Proposed Solution
      description: Optional - Describe your proposed solution in detail. How would this feature work?
  - type: textarea
    id: use-case
    attributes:
      label: Use Case
      description: Provide specific use cases for the feature. How would people use it?
      placeholder: This would help with...
    validations:
      required: true
  - type: textarea
    id: alternatives-solutions
    attributes:
      label: Alternatives Solutions
      description: Optional - Have you considered alternative approaches? What are their pros and cons?
  - type: textarea
    id: additional-context
    attributes:
      label: Additional Context
      description: Include any other context, screenshots, code examples, or references that might help understand the feature request.
````

## File: .github/CODEOWNERS
````
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence,
# @metagit-ai/contributors will be requested for
# review when someone opens a pull request.
*       @metagit-ai/maintainers
````

## File: .github/dependabot.yml
````yaml
version: 2
updates:
- package-ecosystem: pip
  directory: "/"
  schedule:
    interval: daily
  groups:
    python-packages:
      patterns:
        - "*"
````

## File: .github/FUNDING.yml
````yaml
github: [zloeber]
````

## File: .github/PULL_REQUEST_TEMPLATE.md
````markdown
## Description
[Provide a detailed description of the changes in this PR]

## Related Issues
[Link to related issues using #issue-number format]

## Documentation PR
[Link to related associated PR in the repo]

## Type of Change
- Bug fix
- New feature
- Breaking change
- Documentation update
- Other (please describe):

[Choose one of the above types of changes]

## Testing
[How have you tested the change?]

## Checklist
- [ ] I have added tests that prove my fix is effective or my feature works
- [ ] I have updated the documentation accordingly
- [ ] I have added an appropriate example to the documentation to outline the feature
- [ ] My changes generate no new warnings
- [ ] Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
````

## File: docs/repository_detection.md
````markdown
# Repository Detection

The repository detection module provides comprehensive analysis of git repositories, including language detection, project classification, branch analysis, CI/CD detection, and metrics collection.

## Features

- **Language Detection**: Identifies primary and secondary programming languages, frameworks, and build tools
- **Project Classification**: Determines project type (application, library, CLI, etc.) and domain
- **Branch Analysis**: Detects branching strategies and analyzes branch patterns
- **CI/CD Detection**: Identifies CI/CD configurations and platforms
- **Metrics Collection**: Gathers repository metrics including stars, forks, issues, and contributor information
- **Git Provider Integration**: Supports real-time metrics from GitHub and GitLab APIs
- **AppConfig Integration**: Dynamic provider configuration through application settings

## Usage

### Basic Repository Analysis

```python
from metagit.core.detect import DetectionManager

# Analyze a local repository
analysis = DetectionManager.from_path("/path/to/repo")

# Analyze a remote repository (clones it temporarily)
analysis = DetectionManager.from_url("https://github.com/username/repo")

# Generate summary
summary = analysis.summary()
print(summary)

# Convert to MetagitConfig
config = analysis.to_metagit_config()
```

### CLI Usage

```bash
# Analyze current directory
metagit detect repository

# Analyze specific path
metagit detect repository --path /path/to/repo

# Analyze remote repository
metagit detect repository --url https://github.com/username/repo

# Save configuration to .metagit.yml
metagit detect repository --save

# Output in different formats
metagit detect repository --output yaml
metagit detect repository --output json
```

## Git Provider Plugins

The repository detection system supports git provider plugins that enable fetching real-time metrics from hosting platforms like GitHub and GitLab.

### Supported Providers

- **GitHub**: Fetches stars, forks, issues, pull requests, and contributor data
- **GitLab**: Fetches project statistics, merge requests, and member information

### Configuration Methods

#### 1. AppConfig (Recommended)

Configure providers through the application configuration file:

```yaml
# ~/.config/metagit/config.yml or metagit.config.yml
config:
  providers:
    github:
      enabled: true
      api_token: "ghp_your_github_token_here"
      base_url: "https://api.github.com"  # For GitHub Enterprise
    
    gitlab:
      enabled: false
      api_token: "glpat_your_gitlab_token_here"
      base_url: "https://gitlab.com/api/v4"  # For self-hosted GitLab
```

**Benefits:**
- Persistent configuration across sessions
- No need to set environment variables
- Easy to manage multiple environments
- Supports enterprise instances

#### 2. Environment Variables

Set API tokens as environment variables:

```bash
export GITHUB_TOKEN="your_github_personal_access_token"
export GITLAB_TOKEN="your_gitlab_personal_access_token"
```

#### 3. CLI Options

Override configuration for specific commands:

```bash
# Use GitHub token
metagit detect repository --github-token "your_token"

# Use GitLab token
metagit detect repository --gitlab-token "your_token"

# Custom API URLs (for self-hosted instances)
metagit detect repository --github-url "https://github.company.com/api/v3"
metagit detect repository --gitlab-url "https://gitlab.company.com/api/v4"

# Disable AppConfig and use environment variables only
metagit detect repository --use-app-config=false
```

### Configuration Priority

The system uses the following priority order for provider configuration:

1. **CLI Options** (highest priority) - Override all other settings
2. **AppConfig** - Persistent configuration from config files
3. **Environment Variables** - Fallback for legacy support

### Provider Features

#### GitHub Provider

- **Authentication**: Personal Access Token
- **Metrics**: Stars, forks, open issues, pull requests, contributors
- **Metadata**: Repository description, topics, creation date, license
- **URL Support**: github.com, GitHub Enterprise
- **Configuration**: `providers.github.enabled`, `providers.github.api_token`, `providers.github.base_url`

#### GitLab Provider

- **Authentication**: Personal Access Token
- **Metrics**: Star count, forks, open issues, merge requests, members
- **Metadata**: Project description, topics, visibility, namespace
- **URL Support**: gitlab.com, self-hosted GitLab
- **Configuration**: `providers.gitlab.enabled`, `providers.gitlab.api_token`, `providers.gitlab.base_url`

### Fallback Behavior

When no provider is available or API calls fail, the system falls back to git-based metrics:

- **Contributors**: Counted from git commit history
- **Commit Frequency**: Calculated from recent commit patterns
- **Stars/Forks/Issues**: Set to 0 (requires API access)

## Detection Components

### Language Detection

Analyzes file extensions and content to identify:

- **Primary Language**: Most dominant programming language
- **Secondary Languages**: Other languages present
- **Frameworks**: React, Vue, Angular, Terraform, Kubernetes, etc.
- **Package Managers**: npm, pip, cargo, go.mod, etc.
- **Build Tools**: Make, Gradle, Maven, etc.

### Project Type Detection

Classifies projects based on file patterns:

- **Application**: Web apps, mobile apps, desktop apps
- **Library**: Reusable code libraries
- **CLI**: Command-line tools
- **Microservice**: Containerized services
- **Data Science**: ML/AI projects with notebooks
- **Infrastructure as Code**: Terraform, CloudFormation, etc.

### Branch Analysis

Detects branching strategies:

- **Git Flow**: Feature, develop, release, hotfix branches
- **GitHub Flow**: Simple main branch with feature branches
- **GitLab Flow**: Environment-based branching
- **Trunk-Based Development**: Single main branch
- **Custom**: Other branching patterns

### CI/CD Detection

Identifies CI/CD configurations:

- **GitHub Actions**: `.github/workflows/`
- **GitLab CI**: `.gitlab-ci.yml`
- **CircleCI**: `.circleci/config.yml`
- **Jenkins**: `Jenkinsfile`
- **Travis CI**: `.travis.yml`

### Metrics Collection

Gathers repository statistics:

- **Stars**: Repository stars/watches
- **Forks**: Repository forks
- **Open Issues**: Number of open issues
- **Pull Requests**: Open and recently merged PRs
- **Contributors**: Number of contributors
- **Commit Frequency**: Daily, weekly, or monthly activity

## Output Formats

### Summary Output

Human-readable summary of all detected information:

```
Repository Analysis Summary
Path: /path/to/repo
URL: https://github.com/username/repo
Git Repository: True
Primary Language: Python
Secondary Languages: JavaScript, Shell
Frameworks: React, Terraform
Package Managers: pip, npm
Project Type: application
Domain: web
Confidence: 0.85
Branch Strategy: GitHub Flow
Number of Branches: 3
CI/CD Tool: GitHub Actions
Contributors: 5
Commit Frequency: weekly
Stars: 42
Forks: 12
Open Issues: 3
Open PRs: 1
PRs Merged (30d): 8
Metrics Source: GitHub API
Has Docker: True
Has Tests: True
Has Documentation: True
Has Infrastructure as Code: True
```

### YAML Output

Structured YAML configuration:

```yaml
name: "My Project"
description: "A sample project"
url: "https://github.com/username/repo"
kind: "application"
license:
  kind: "mit"
  file: "LICENSE"
maintainers:
  - name: "John Doe"
    email: "john@example.com"
    role: "Maintainer"
branch_strategy: "github_flow"
taskers:
  - kind: "taskfile"
branches:
  - name: "main"
  - name: "develop"
  - name: "feature/new-feature"
cicd:
  platform: "github"
  pipelines:
    - name: "CI"
      ref: ".github/workflows/ci.yml"
metrics:
  stars: 42
  forks: 12
  open_issues: 3
  pull_requests:
    open: 1
    merged_last_30d: 8
  contributors: 5
  commit_frequency: "weekly"
metadata:
  default_branch: "main"
  has_ci: true
  has_tests: true
  has_docs: true
  has_docker: true
  has_iac: true
  created_at: "2024-01-01T00:00:00"
  last_commit_at: "2024-01-15T12:00:00"
workspace:
  projects:
    - name: "default"
      repos:
        - name: "My Project"
          path: "/path/to/repo"
          url: "https://github.com/username/repo"
```

## Examples

### Basic Analysis

```python
from metagit.core.detect import DetectionManager

# Analyze current directory
analysis = DetectionManager.from_path(".")

# Print summary
print(analysis.summary())

# Get configuration
config = analysis.to_metagit_config()
```

### With AppConfig Integration

```python
from metagit.core.detect import DetectionManager
from metagit.core.appconfig import AppConfig
from metagit.core.providers import registry

# Load AppConfig and configure providers
app_config = AppConfig.load()
registry.configure_from_app_config(app_config)

# Analyze repository (will use configured providers for metrics)
analysis = DetectionManager.from_path(".")
print(analysis.summary())
```

### With Manual Provider Configuration

```python
from metagit.core.detect import DetectionManager
from metagit.core.providers.github import GitHubProvider
from metagit.core.providers import registry

# Setup GitHub provider manually
provider = GitHubProvider(api_token="ghp_...")
registry.register(provider)

# Analyze repository
analysis = DetectionManager.from_path(".")
print(analysis.summary())
```

### CLI with AppConfig

```bash
# Create AppConfig file
mkdir -p ~/.config/metagit
cat > ~/.config/metagit/config.yml << EOF
config:
  providers:
    github:
      enabled: true
      api_token: "ghp_..."
    gitlab:
      enabled: false
      api_token: ""
EOF

# Analyze with AppConfig providers
metagit detect repository --path /path/to/repo --output summary

# Save configuration with real metrics
metagit detect repository --path /path/to/repo --save
```

### CLI with Environment Variables

```bash
# Set environment variables
export GITHUB_TOKEN="ghp_..."
export GITLAB_TOKEN="glpat-..."

# Analyze with environment providers
metagit detect repository --path /path/to/repo --output summary

# Disable AppConfig and use environment only
metagit detect repository --use-app-config=false --path /path/to/repo
```

## Error Handling

The detection system gracefully handles errors:

- **Missing Files**: Skips analysis of missing files/directories
- **API Failures**: Falls back to git-based metrics
- **Invalid Repositories**: Returns appropriate error messages
- **Network Issues**: Continues with local analysis
- **Configuration Errors**: Falls back to environment variables or defaults

## Performance Considerations

- **Local Analysis**: Fast, no network required
- **Provider API Calls**: May add 1-3 seconds for metrics
- **Large Repositories**: Analysis time scales with repository size
- **Caching**: No built-in caching (consider implementing for repeated analysis)
- **Configuration Loading**: AppConfig is loaded once per command execution
````

## File: docs/terminology.md
````markdown
# Metagit Terminology

IT has a way with words doesn't it? This is a short list of metagit terms and their concise meaning to reduce confusion.

**Path (aka. Target)** - A folder within a git repository.

**Repo** - A git repository. If it happens to be a monorepo there maybe several targets within the repository with unique associated metadata.

**Project** - A collection of git repositories. In VSCode this is a workspace. We define a project at this higher level than the repository because we want a more holistic view of what your code entails as a whole. While a repo maybe produces a single app it may have several internal/external dependencies that make up the whole of what it requires to deploy it.

**Workspace** - A collection of projects. Fundamentally different than a VSCode workspace. For metagit the most important thing to understand is that this is the target folder where all projects and their repos will be cloned/copied into.

This creates a hierarchy like the following:

```
Workspace/
└── ProjectA/
    └── RepoA1/
        ├── Path1
        └── Path2
    └── RepoA2/
        └── Path1
└── ProjectB/
    └── RepoB1/
        ├── Path1
        └── Path2
    └── RepoB2/
      └── Path1
```

> **NOTE** It is entirely possible to have the same repo referenced in multiple projects. Local paths defined this way will simply be soft links. Multiple project repositories defined this way will be treated independently.
````

## File: examples/appconfig_example.yml
````yaml
config:
  version: "0.1.0"
  description: "Metagit configuration with provider plugins"
  editor: "code"
  
  # LLM Configuration
  llm:
    enabled: false
    provider: "openrouter"
    provider_model: "gpt-4o-mini"
    embedder: "ollama"
    embedder_model: "nomic-embed-text"
    api_key: ""
  
  # Workspace Configuration
  workspace:
    path: "./.metagit"
    default_project: "default"
  
  # Profiles Configuration
  profiles:
    profile_config_path: "~/.config/metagit/profiles"
    default_profile: "default"
    boundaries: []
  
  # Git Provider Plugin Configuration
  providers:
    github:
      enabled: true  # Set to true to enable GitHub provider
      api_token: "ghp_your_github_token_here"  # Your GitHub Personal Access Token
      base_url: "https://api.github.com"  # Default GitHub API URL
    
    gitlab:
      enabled: false  # Set to true to enable GitLab provider
      api_token: "glpat_your_gitlab_token_here"  # Your GitLab Personal Access Token
      base_url: "https://gitlab.com/api/v4"  # Default GitLab API URL
````

## File: examples/categorize_directory.py
````python
#!/usr/bin/env python3
"""
Example script to categorize directory contents using metagit utilities.

This script demonstrates how to use the directory_summary and directory_details
functions to analyze directory structure and output the results in YAML format.
"""
⋮----
# Add the parent directory to the path so we can import metagit modules
⋮----
def convert_namedtuple_to_dict(obj)
⋮----
"""Convert NamedTuple objects to dictionaries for YAML serialization."""
⋮----
# Handle NamedTuple
result = obj._asdict()
# Recursively convert nested NamedTuples
⋮----
# Handle nested dictionaries (like file_types)
⋮----
def categorize_directory(path: str, output_type: str, output_file: str)
⋮----
"""
    Categorize directory contents and output in YAML format.

    This script analyzes a directory structure and provides either a summary
    (file counts by extension) or detailed analysis (file types with names and categories).
    """
⋮----
# Validate the path
target_path = Path(path)
⋮----
# Generate the appropriate output based on type
⋮----
result = directory_summary(str(target_path))
# Convert Pydantic model to dict for YAML serialization
output_data = result.model_dump()
else:  # details
file_lookup = FileExtensionLookup()
result = directory_details(str(target_path), file_lookup)
# Convert NamedTuple to dict for YAML serialization
output_data = convert_namedtuple_to_dict(result)
⋮----
# Convert to YAML
yaml_output = yaml.dump(
⋮----
# Output the result
````

## File: examples/cli_record_example.py
````python
#!/usr/bin/env python
"""
Example script demonstrating the new record CLI commands.

This script shows how to use the record management commands programmatically.
"""
⋮----
def run_metagit_command(args: list) -> tuple[int, str, str]
⋮----
"""Run a metagit command and return the result."""
⋮----
result = subprocess.run(
⋮----
def create_sample_config()
⋮----
"""Create a sample .metagit.yml file for testing."""
config_content = """
⋮----
def example_record_commands()
⋮----
"""Demonstrate the record CLI commands."""
⋮----
# Create sample config
⋮----
# Test record create
⋮----
# Test record show (list all)
⋮----
# Test record search
⋮----
# Test record stats
⋮----
# Test record export
⋮----
# Test record import
⋮----
# Test OpenSearch backend (if available)
⋮----
def cleanup()
⋮----
"""Clean up test files."""
files_to_remove = [
````

## File: examples/datetime_serialization_fix_example.py
````python
#!/usr/bin/env python
"""
Example demonstrating the datetime serialization fix for metagit records.

This example shows how the DateTimeEncoder handles datetime objects when
serializing MetagitRecord objects to JSON, fixing the "Object of type datetime
is not JSON serializable" error.
"""
⋮----
def demonstrate_datetime_serialization_fix()
⋮----
"""Demonstrate the datetime serialization fix."""
⋮----
# Show the problem (without DateTimeEncoder)
⋮----
test_data = {
⋮----
# This would fail without DateTimeEncoder
⋮----
# Show the solution
⋮----
class DateTimeEncoder(json.JSONEncoder)
⋮----
"""Custom JSON encoder that handles datetime objects."""
⋮----
def default(self, obj)
⋮----
json_str = json.dumps(test_data, cls=DateTimeEncoder, indent=2)
⋮----
async def demonstrate_record_creation()
⋮----
"""Demonstrate record creation with datetime handling."""
⋮----
# Check if config file exists
config_path = Path("metagit.config.yaml")
⋮----
# Create a simple config for demonstration
⋮----
config = MetagitConfig(
⋮----
# Load existing config
config_manager = MetagitConfigManager(config_path=config_path)
config_result = config_manager.load_config()
⋮----
config = config_result
⋮----
# Create record manager with local storage
storage_dir = Path("./example_records")
backend = LocalFileStorageBackend(storage_dir)
logger = UnifiedLogger(LoggerConfig(log_level="INFO", minimal_console=True))
record_manager = MetagitRecordManager(
⋮----
# Create record from config
record = record_manager.create_record_from_config(
⋮----
# Store the record (this is where datetime serialization happens)
record_id = await record_manager.store_record(record)
⋮----
# Verify the stored record can be retrieved
retrieved_record = await record_manager.get_record(record_id)
⋮----
# Clean up
⋮----
def main()
⋮----
"""Run the datetime serialization fix demonstration."""
⋮----
# Demonstrate the fix
⋮----
# Demonstrate record creation
⋮----
success = asyncio.run(demonstrate_record_creation())
````

## File: examples/detection_config_example.yml
````yaml
# DetectionManager Configuration Example
# This file demonstrates how to configure which analysis methods are enabled

# Default configuration - all current methods enabled
default:
  branch_analysis_enabled: true
  ci_config_analysis_enabled: true
  directory_summary_enabled: true
  directory_details_enabled: true
  commit_analysis_enabled: false  # Future feature
  tag_analysis_enabled: false     # Future feature

# Minimal configuration - only essential methods
minimal:
  branch_analysis_enabled: true
  ci_config_analysis_enabled: true
  directory_summary_enabled: false
  directory_details_enabled: false
  commit_analysis_enabled: false
  tag_analysis_enabled: false

# Comprehensive configuration - all methods enabled
comprehensive:
  branch_analysis_enabled: true
  ci_config_analysis_enabled: true
  directory_summary_enabled: true
  directory_details_enabled: true
  commit_analysis_enabled: true
  tag_analysis_enabled: true

# CI/CD focused configuration - only CI/CD and branch analysis
cicd_focused:
  branch_analysis_enabled: true
  ci_config_analysis_enabled: true
  directory_summary_enabled: false
  directory_details_enabled: false
  commit_analysis_enabled: false
  tag_analysis_enabled: false

# Directory analysis focused configuration
directory_focused:
  branch_analysis_enabled: false
  ci_config_analysis_enabled: false
  directory_summary_enabled: true
  directory_details_enabled: true
  commit_analysis_enabled: false
  tag_analysis_enabled: false
````

## File: examples/detection_manager_config_example.py
````python
#!/usr/bin/env python3
⋮----
"""
Example demonstrating the use of DetectionManagerConfig to control which analysis methods are enabled.
"""
⋮----
# Add the metagit package to the path
⋮----
def example_basic_usage()
⋮----
"""Demonstrate basic usage with default configuration."""
⋮----
# Create a DetectionManager with default config (all enabled)
manager = DetectionManager.from_path("./")
⋮----
# Run all enabled analyses
result = manager.run_all()
⋮----
# Print summary
summary = manager.summary()
⋮----
def example_custom_config()
⋮----
"""Demonstrate usage with custom configuration."""
⋮----
# Create a custom configuration
config = DetectionManagerConfig(
⋮----
directory_summary_enabled=False,  # Disable directory summary
directory_details_enabled=False,  # Disable directory details
⋮----
# Create DetectionManager with custom config
manager = DetectionManager.from_path("./", config=config)
⋮----
# Run analyses
⋮----
def example_preset_configs()
⋮----
"""Demonstrate usage with preset configurations."""
⋮----
# Use minimal configuration
minimal_config = DetectionManagerConfig.minimal()
⋮----
# Use all enabled configuration
all_enabled_config = DetectionManagerConfig.all_enabled()
⋮----
def example_specific_method()
⋮----
"""Demonstrate running a specific analysis method."""
⋮----
# Create a configuration with only branch analysis enabled
⋮----
# Run only branch analysis
result = manager.run_specific("branch_analysis")
⋮----
def example_config_serialization()
⋮----
"""Demonstrate configuration serialization."""
⋮----
# Create a configuration
⋮----
# Convert to dict
config_dict = config.model_dump()
⋮----
# Create from dict
new_config = DetectionManagerConfig(**config_dict)
⋮----
def example_metagit_record_integration()
⋮----
"""Demonstrate MetagitRecord integration."""
⋮----
# Create DetectionManager (inherits from MetagitRecord)
⋮----
# Run analysis
⋮----
# Access MetagitRecord fields
⋮----
# Access detection-specific fields
⋮----
# Convert to YAML (includes both MetagitRecord and detection data)
yaml_output = manager.to_yaml()
````

## File: examples/detection_manager_example.py
````python
#!/usr/bin/env python3
⋮----
"""
Example demonstrating the updated DetectionManager that uses RepositoryAnalysis for all detection details.

This example shows how DetectionManager now uses RepositoryAnalysis as the single source for all
detection analysis results while maintaining the MetagitRecord interface.
"""
⋮----
# Add the metagit package to the path
⋮----
def example_local_repository_analysis()
⋮----
"""Demonstrate analyzing a local repository."""
⋮----
# Create DetectionManager for local path
manager = DetectionManager.from_path("./")
⋮----
# Run all analyses
result = manager.run_all()
⋮----
def example_remote_repository_analysis()
⋮----
"""Demonstrate analyzing a remote repository."""
⋮----
# Example repository URL
repo_url = "https://github.com/octocat/Hello-World.git"
⋮----
# Create DetectionManager for remote URL
manager = DetectionManager.from_url(repo_url)
⋮----
# Clean up cloned repository
⋮----
def example_configuration_options()
⋮----
"""Demonstrate different configuration options."""
⋮----
# Minimal configuration
⋮----
config = DetectionManagerConfig.minimal()
manager = DetectionManager.from_path("./", config=config)
⋮----
# All enabled configuration
⋮----
config = DetectionManagerConfig.all_enabled()
⋮----
# Custom configuration
⋮----
config = DetectionManagerConfig(
⋮----
def example_metagit_record_integration()
⋮----
"""Demonstrate MetagitRecord integration."""
⋮----
# Create DetectionManager
⋮----
# Run analysis
⋮----
# Access MetagitRecord fields
⋮----
# Access detection-specific fields
⋮----
# Access RepositoryAnalysis results
⋮----
# Access specific analysis results
⋮----
def example_output_formats()
⋮----
"""Demonstrate different output formats."""
⋮----
# Summary output
summary = manager.summary()
⋮----
# YAML output (includes all detection data)
yaml_output = manager.to_yaml()
⋮----
# JSON output (includes all detection data)
json_output = manager.to_json()
⋮----
def example_specific_analysis_methods()
⋮----
"""Demonstrate running specific analysis methods."""
⋮----
# Create DetectionManager with minimal config
⋮----
# Run specific methods
methods = ["branch_analysis", "ci_config_analysis"]
⋮----
result = manager.run_specific(method)
⋮----
# Test disabled method
⋮----
result = manager.run_specific("directory_summary")
⋮----
def example_repository_analysis_access()
⋮----
"""Demonstrate direct access to RepositoryAnalysis."""
⋮----
# Access RepositoryAnalysis directly
⋮----
repo_analysis = manager.repository_analysis
⋮----
# Access language detection
⋮----
# Access project type detection
⋮----
# Access file analysis
⋮----
# Access metrics
⋮----
def main()
⋮----
"""Run all examples."""
⋮----
# Run examples
⋮----
# Note: Remote analysis is commented out to avoid cloning during examples
# Uncomment the line below to test remote repository analysis
# example_remote_repository_analysis()
````

## File: examples/fuzzyfinder_comprehensive_test.py
````python
#!/usr/bin/env python
⋮----
"""
Comprehensive test script demonstrating all FuzzyFinder features.
This script shows various configurations and use cases for the FuzzyFinder.
"""
⋮----
# Add the metagit package to the path
⋮----
class FileItem
⋮----
"""Example object representing a file with multiple attributes."""
⋮----
def __str__(self) -> str
⋮----
def get_preview_text(self) -> str
⋮----
"""Get formatted preview text for this file."""
⋮----
def create_sample_files() -> List[FileItem]
⋮----
"""Create a list of sample files for testing."""
⋮----
"""Run a specific FuzzyFinder test configuration."""
⋮----
finder = FuzzyFinder(config)
result = finder.run()
⋮----
def main()
⋮----
"""Main function to demonstrate various FuzzyFinder configurations."""
⋮----
# Create sample data
files = create_sample_files()
⋮----
# Test 1: Basic functionality with preview
⋮----
config1 = FuzzyFinderConfig(
⋮----
result1 = run_fuzzyfinder_test(
⋮----
# Test 2: Different scorer
⋮----
config2 = FuzzyFinderConfig(
⋮----
result2 = run_fuzzyfinder_test(
⋮----
# Test 3: Case sensitive search
⋮----
config3 = FuzzyFinderConfig(
⋮----
result3 = run_fuzzyfinder_test(
⋮----
# Test 4: Higher threshold
⋮----
config4 = FuzzyFinderConfig(
⋮----
result4 = run_fuzzyfinder_test(
````

## File: examples/fuzzyfinder_debug_test.py
````python
#!/usr/bin/env python
⋮----
"""
Debug test script for FuzzyFinder to verify fixes.
This script tests basic functionality with minimal configuration.
"""
⋮----
# Add the metagit package to the path
⋮----
def main()
⋮----
"""Simple test to verify FuzzyFinder works."""
⋮----
# Simple string list
items = ["python", "javascript", "typescript", "golang", "rust"]
⋮----
# Basic configuration
config = FuzzyFinderConfig(
⋮----
enable_preview=False,  # Disable preview for basic test
⋮----
finder = FuzzyFinder(config)
result = finder.run()
````

## File: examples/fuzzyfinder_preview_test.py
````python
#!/usr/bin/env python
⋮----
"""
Test script demonstrating FuzzyFinder with object list and preview functionality.
This script shows how to use the FuzzyFinder with a list of objects that have
multiple fields, including a preview field that displays additional information
when an item is selected.
"""
⋮----
# Add the metagit package to the path
⋮----
class ProjectFile
⋮----
"""Example object representing a project file with multiple attributes."""
⋮----
def __init__(self, name: str, path: str, size: int, type: str, description: str)
⋮----
def __str__(self) -> str
⋮----
def create_sample_project_files() -> List[ProjectFile]
⋮----
"""Create a list of sample project files for testing."""
⋮----
def format_preview_text(file_obj: ProjectFile) -> str
⋮----
"""Format the preview text for a project file."""
⋮----
def main()
⋮----
"""Main function to demonstrate FuzzyFinder with preview functionality."""
⋮----
# Create sample data
project_files = create_sample_project_files()
⋮----
# Configure FuzzyFinder with preview enabled
config = FuzzyFinderConfig(
⋮----
display_field="name",  # Use the 'name' field for display/search
preview_field="description",  # Use 'description' for preview
⋮----
# Custom styling
⋮----
# Create and run the fuzzy finder
finder = FuzzyFinder(config)
⋮----
result = finder.run()
⋮----
selected_file = result
````

## File: examples/fuzzyfinder_simple_test.py
````python
#!/usr/bin/env python
⋮----
"""
Simple test script demonstrating basic FuzzyFinder functionality.
This script shows how to use the FuzzyFinder with a simple list of strings.
"""
⋮----
# Add the metagit package to the path
⋮----
def create_sample_strings() -> List[str]
⋮----
"""Create a list of sample strings for testing."""
⋮----
def main()
⋮----
"""Main function to demonstrate basic FuzzyFinder functionality."""
⋮----
# Create sample data
languages = create_sample_strings()
⋮----
# Configure FuzzyFinder
config = FuzzyFinderConfig(
⋮----
# Custom styling
⋮----
# Create and run the fuzzy finder
finder = FuzzyFinder(config)
⋮----
result = finder.run()
````

## File: examples/gitcache_differences_example.py
````python
#!/usr/bin/env python
"""
Example demonstrating git cache differences functionality.

This example shows how the GitCacheManager now checks for differences
between local and remote repositories before pulling updates, and
includes detailed difference information in cache entries.
"""
⋮----
def create_test_git_repo(path: Path) -> None
⋮----
"""Create a test git repository with some commits."""
⋮----
# Initialize git repository
⋮----
# Create initial file
⋮----
# Create another file
⋮----
def main()
⋮----
"""Demonstrate git cache differences functionality."""
⋮----
# Create temporary directories
⋮----
temp_path = Path(temp_dir)
cache_root = temp_path / "cache"
⋮----
# Create configuration
config = GitCacheConfig(
⋮----
# Create manager
manager = GitCacheManager(config)
⋮----
# Create a test git repository
test_repo_path = temp_path / "test_repo"
⋮----
entry = manager.cache_repository(str(test_repo_path), name="test-repo")
⋮----
details = manager.get_cache_entry_details("test-repo")
⋮----
if key not in ["metadata"]:  # Skip metadata for cleaner output
⋮----
# Add a new commit to the original repository
⋮----
entries = manager.list_cache_entries()
⋮----
stats = manager.get_cache_stats()
````

## File: examples/gitcache_example.py
````python
#!/usr/bin/env python
"""
Example script demonstrating the git cache management system.

This script shows how to use the GitCacheManager to cache both
git repositories and local directories with both sync and async operations.
"""
⋮----
def create_sample_local_directory() -> Path
⋮----
"""Create a sample local directory for testing."""
temp_dir = Path(tempfile.mkdtemp())
⋮----
# Create some sample files
⋮----
# Create a subdirectory
subdir = temp_dir / "src"
⋮----
def sync_example()
⋮----
"""Demonstrate synchronous git cache operations."""
⋮----
# Create configuration
config = GitCacheConfig(
⋮----
# Create manager
manager = GitCacheManager(config)
⋮----
# Example 1: Cache a git repository
⋮----
entry = manager.cache_repository("https://github.com/octocat/Hello-World.git")
⋮----
# Example 2: Cache a local directory
⋮----
sample_dir = create_sample_local_directory()
entry = manager.cache_repository(str(sample_dir), name="sample-project")
⋮----
# Example 3: List cache entries
⋮----
entries = manager.list_cache_entries()
⋮----
# Example 4: Get cache statistics
⋮----
stats = manager.get_cache_stats()
⋮----
# Example 5: Get cached repository path
⋮----
cache_path = manager.get_cached_repository("Hello-World")
⋮----
# Example 6: Refresh cache entry
⋮----
entry = manager.refresh_cache_entry("Hello-World")
⋮----
async def async_example()
⋮----
"""Demonstrate asynchronous git cache operations."""
⋮----
# Example 1: Cache multiple repositories concurrently
⋮----
repositories = [
⋮----
tasks = []
⋮----
task = manager.cache_repository_async(repo_url)
⋮----
results = await asyncio.gather(*tasks, return_exceptions=True)
⋮----
# Example 2: Cache local directories concurrently
⋮----
sample_dir1 = create_sample_local_directory()
sample_dir2 = create_sample_local_directory()
⋮----
# Add some unique content to distinguish them
⋮----
tasks = [
⋮----
# Example 3: Refresh multiple entries concurrently
⋮----
for entry in entries[:2]:  # Refresh first 2 entries
task = manager.refresh_cache_entry_async(entry.name)
⋮----
def cleanup_example()
⋮----
"""Demonstrate cache cleanup operations."""
⋮----
# Example 1: Remove specific cache entry
⋮----
result = manager.remove_cache_entry("Hello-World")
⋮----
# Example 2: Clear all cache
⋮----
result = manager.clear_cache()
⋮----
def main()
⋮----
"""Run all examples."""
⋮----
# Run synchronous examples
⋮----
# Run asynchronous examples
⋮----
# Run cleanup examples
````

## File: examples/load_config_from_yaml.py
````python
#!/usr/bin/env python3
⋮----
"""
Example demonstrating how to load DetectionManagerConfig from YAML files.
"""
⋮----
# Add the metagit package to the path
⋮----
"""
    Load a DetectionManagerConfig from a YAML file.

    Args:
        file_path: Path to the YAML configuration file
        config_name: Name of the configuration section to load

    Returns:
        DetectionManagerConfig instance
    """
⋮----
configs = yaml.safe_load(f)
⋮----
config_data = configs[config_name]
⋮----
# Fallback to default configuration
⋮----
def example_load_from_yaml()
⋮----
"""Demonstrate loading configurations from YAML file."""
⋮----
config_file = Path(__file__).parent / "detection_config_example.yml"
⋮----
# Load different configurations
configs_to_load = [
⋮----
config = load_config_from_yaml(str(config_file), config_name)
enabled_methods = config.get_enabled_methods()
⋮----
def example_use_loaded_config()
⋮----
"""Demonstrate using a loaded configuration with DetectionManager."""
⋮----
# Load minimal configuration
config = load_config_from_yaml(str(config_file), "minimal")
⋮----
# Create DetectionManager with loaded config
manager = DetectionManager(path="./", config=config)
⋮----
# Run analysis
result = manager.run_all()
⋮----
# Print summary
summary = manager.summary()
⋮----
def example_create_yaml_config()
⋮----
"""Demonstrate creating a YAML configuration file programmatically."""
⋮----
# Create different configurations
configs = {
⋮----
# Convert to YAML
yaml_data = {}
⋮----
# Print YAML
yaml_output = yaml.dump(yaml_data, default_flow_style=False, sort_keys=False)
⋮----
# Save to file
output_file = Path(__file__).parent / "generated_config.yml"
````

## File: examples/provider_example.py
````python
#!/usr/bin/env python3
⋮----
# Add the project root to the Python path
project_root = Path(__file__).parent.parent
⋮----
"""
Example script demonstrating git provider plugins for repository analysis.

This script shows how to:
1. Configure providers using AppConfig
2. Configure providers using environment variables
3. Analyze repositories with real metrics from APIs
4. Compare different configuration methods
"""
⋮----
def setup_providers_from_appconfig()
⋮----
"""Setup git provider plugins using AppConfig."""
⋮----
app_config = AppConfig.load()
⋮----
providers = registry.get_all_providers()
⋮----
provider_names = [p.get_name() for p in providers]
⋮----
def setup_providers_from_environment()
⋮----
"""Setup git provider plugins using environment variables."""
⋮----
def setup_providers_manually()
⋮----
"""Setup git provider plugins manually."""
⋮----
# GitHub provider
github_token = os.getenv("GITHUB_TOKEN")
⋮----
github_provider = GitHubProvider(api_token=github_token)
⋮----
# GitLab provider
gitlab_token = os.getenv("GITLAB_TOKEN")
⋮----
gitlab_provider = GitLabProvider(api_token=gitlab_token)
⋮----
def analyze_local_repo(repo_path: str)
⋮----
"""Analyze a local repository."""
⋮----
analysis = DetectionManager.from_path(repo_path)
⋮----
summary = analysis.summary()
⋮----
# Show metrics details
⋮----
def analyze_remote_repo(repo_url: str)
⋮----
"""Analyze a remote repository by cloning it."""
⋮----
analysis = DetectionManager.from_url(repo_url)
⋮----
# Clean up
⋮----
def demonstrate_configuration_methods()
⋮----
"""Demonstrate different configuration methods."""
⋮----
# Method 1: AppConfig
⋮----
success = setup_providers_from_appconfig()
⋮----
# Clear providers for next method
⋮----
# Method 2: Environment Variables
⋮----
success = setup_providers_from_environment()
⋮----
# Method 3: Manual Configuration
⋮----
success = setup_providers_manually()
⋮----
def main()
⋮----
"""Main example function."""
⋮----
# Demonstrate configuration methods
⋮----
# Setup providers for analysis (using AppConfig if available, otherwise environment)
⋮----
# Try AppConfig first, fall back to environment
⋮----
# Example 1: Analyze current directory
⋮----
# Example 2: Analyze a specific local repository
# Uncomment and modify the path as needed
# print("\n" + "=" * 50)
# print("Example 2: Analyzing specific local repository")
# analyze_local_repo("/path/to/your/repo")
⋮----
# Example 3: Analyze a remote repository
# Uncomment and modify the URL as needed
⋮----
# print("Example 3: Analyzing remote repository")
# analyze_remote_repo("https://github.com/username/repo")
````

## File: examples/README_fuzzyfinder_tests.md
````markdown
# FuzzyFinder Test Scripts

This directory contains test scripts that demonstrate the functionality of the `FuzzyFinder` utility from the `metagit.core.utils.fuzzyfinder` module.

## Overview

The `FuzzyFinder` is a powerful interactive search tool that provides:
- Fuzzy text matching using rapidfuzz
- Interactive navigation with arrow keys
- Preview pane for detailed information
- Customizable styling and configuration
- Support for both string lists and object lists

## Test Scripts

### 1. `fuzzyfinder_simple_test.py`
**Purpose**: Basic demonstration with simple string list
**Features**:
- Uses a list of programming language names
- Shows basic search functionality
- Demonstrates navigation and selection

**Usage**:
```bash
python examples/fuzzyfinder_simple_test.py
```

### 2. `fuzzyfinder_preview_test.py`
**Purpose**: Demonstrates preview functionality with object list
**Features**:
- Uses `ProjectFile` objects with multiple attributes
- Shows preview pane with file descriptions
- Demonstrates `display_field` and `preview_field` configuration

**Usage**:
```bash
python examples/fuzzyfinder_preview_test.py
```

### 3. `fuzzyfinder_comprehensive_test.py`
**Purpose**: Comprehensive demonstration of all features
**Features**:
- Multiple test configurations
- Different scorers (partial_ratio, token_sort_ratio)
- Case-sensitive vs case-insensitive search
- Various score thresholds
- Different styling configurations

**Usage**:
```bash
python examples/fuzzyfinder_comprehensive_test.py
```

## Key Features Demonstrated

### Object Support
The FuzzyFinder can work with both simple strings and complex objects:

```python
# Simple string list
config = FuzzyFinderConfig(
    items=["python", "javascript", "typescript"],
    prompt_text="Search: "
)

# Object list with custom fields
config = FuzzyFinderConfig(
    items=file_objects,
    display_field="name",      # Field to display and search
    preview_field="description", # Field to show in preview
    enable_preview=True
)
```

### Preview Functionality
When `enable_preview=True` is set, the FuzzyFinder shows a preview pane below the search results:

```python
config = FuzzyFinderConfig(
    items=files,
    display_field="name",
    preview_field="description",
    enable_preview=True,
    prompt_text="Search files: "
)
```

### Different Scorers
The FuzzyFinder supports multiple fuzzy matching algorithms:

- `partial_ratio`: Best for partial string matches
- `ratio`: Best for exact string similarity
- `token_sort_ratio`: Best for word order variations

```python
config = FuzzyFinderConfig(
    items=items,
    scorer="token_sort_ratio",  # or "partial_ratio", "ratio"
    score_threshold=70.0
)
```

### Custom Styling
You can customize the appearance with different colors and styles:

```python
config = FuzzyFinderConfig(
    items=items,
    highlight_color="bold white bg:#0066cc",
    normal_color="white",
    prompt_color="bold green",
    separator_color="gray"
)
```

## Navigation Controls

- **Arrow Keys**: Navigate up/down through results
- **Type**: Search/filter results
- **Enter**: Select highlighted item
- **Ctrl+C**: Exit without selection

## Configuration Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `items` | List | Required | Items to search (strings or objects) |
| `display_field` | str | None | Field name for display/search (objects only) |
| `preview_field` | str | None | Field name for preview (objects only) |
| `enable_preview` | bool | False | Enable preview pane |
| `score_threshold` | float | 70.0 | Minimum match score (0-100) |
| `max_results` | int | 10 | Maximum results to display |
| `scorer` | str | "partial_ratio" | Fuzzy matching algorithm |
| `case_sensitive` | bool | False | Case-sensitive matching |
| `prompt_text` | str | "> " | Input prompt text |
| `highlight_color` | str | "bold white bg:#4444aa" | Highlighted item style |
| `normal_color` | str | "white" | Normal item style |
| `prompt_color` | str | "bold cyan" | Prompt text style |
| `separator_color` | str | "gray" | Separator line style |

## Example Output

When running the preview test, you'll see an interface like this:

```
Search files: py
> main.py
  api.py
  database.py
  models.py
  utils.py
  middleware.py
  cli.py
=====================================
Preview:
Main application entry point with CLI interface and core functionality
```

## Requirements

The test scripts require the following dependencies:
- `prompt_toolkit`: For the interactive terminal interface
- `rapidfuzz`: For fuzzy string matching
- `pydantic`: For configuration validation

These should be installed as part of the metagit package dependencies.

## Troubleshooting

### Common Issues

1. **Import Error**: Make sure you're running from the project root directory
2. **Display Issues**: Ensure your terminal supports the color codes used
3. **Navigation Problems**: Check that your terminal supports arrow key input

### Debug Mode

To see more detailed error information, you can modify the scripts to catch and display exceptions:

```python
try:
    result = finder.run()
    if isinstance(result, Exception):
        print(f"Error: {result}")
except Exception as e:
    print(f"Unexpected error: {e}")
    import traceback
    traceback.print_exc()
````

## File: examples/record_conversion_advanced_example.py
````python
#!/usr/bin/env python
"""
Advanced example demonstrating automatic field detection in MetagitRecord conversion.

This script shows how the new approach automatically handles field differences
without requiring manual field lists or attribute definitions.
"""
⋮----
# Add the project root to the Python path
project_root = Path(__file__).parent.parent
⋮----
def main()
⋮----
"""Demonstrate advanced MetagitRecord conversion with automatic field detection."""
⋮----
# Example 1: Show field differences automatically
⋮----
differences = MetagitRecord.get_field_differences()
⋮----
for field in differences["common_fields"][:10]:  # Show first 10
⋮----
for field in differences["record_only_fields"][:5]:  # Show first 5
⋮----
# Example 2: Show compatible fields
⋮----
compatible_fields = MetagitRecord.get_compatible_fields()
⋮----
# Example 3: Create a complex record
⋮----
record = MetagitRecord(
⋮----
# Detection-specific fields (will be automatically excluded)
⋮----
# Example 4: Automatic conversion without manual field lists
⋮----
# This conversion happens automatically without any manual field definitions
config = record.to_metagit_config()
⋮----
# Verify that detection fields were automatically excluded
detection_fields_excluded = all(
⋮----
# Example 5: Show what was preserved
⋮----
for field in differences["common_fields"][:8]:  # Show first 8
record_value = getattr(record, field, None)
config_value = getattr(config, field, None)
status = "✓" if record_value == config_value else "⚠"
⋮----
has_config_field = hasattr(config, field)
⋮----
# Example 6: Performance comparison
⋮----
# Test conversion performance
start_time = time.time()
⋮----
test_record = MetagitRecord(
test_config = test_record.to_metagit_config()
end_time = time.time()
⋮----
conversion_time = end_time - start_time
⋮----
# Example 7: Advanced conversion with kwargs
⋮----
base_config = MetagitConfig(
⋮----
# Use the advanced method with flexible kwargs
advanced_record = MetagitRecord.from_metagit_config_advanced(
⋮----
# Example 8: Demonstrate the benefits
````

## File: examples/record_conversion_example.py
````python
#!/usr/bin/env python
"""
Example script demonstrating fast conversion between MetagitRecord and MetagitConfig.

This script shows how to use the latest Pydantic best practices for efficient
conversion between MetagitRecord and MetagitConfig data structures.
"""
⋮----
# Add the project root to the Python path
project_root = Path(__file__).parent.parent
⋮----
def main()
⋮----
"""Demonstrate MetagitRecord conversion methods."""
⋮----
# Example 1: Create a MetagitConfig
⋮----
config = MetagitConfig(
⋮----
# Example 2: Convert MetagitConfig to MetagitRecord
⋮----
record = MetagitRecord.from_metagit_config(
⋮----
# Example 3: Show detection summary
⋮----
summary = record.get_detection_summary()
⋮----
# Example 4: Convert MetagitRecord back to MetagitConfig
⋮----
converted_config = record.to_metagit_config()
⋮----
# Example 5: Verify round-trip conversion
⋮----
# Example 6: Performance test
⋮----
start_time = time.time()
⋮----
test_config = MetagitConfig(name=f"perf-test-{i}")
test_record = MetagitRecord.from_metagit_config(test_config)
back_to_config = test_record.to_metagit_config()
end_time = time.time()
⋮----
conversion_time = end_time - start_time
⋮----
# Example 7: Complex nested object conversion
⋮----
complex_config = MetagitConfig(
⋮----
complex_record = MetagitRecord.from_metagit_config(
⋮----
back_to_complex_config = complex_record.to_metagit_config()
⋮----
# Example 8: Error handling demonstration
⋮----
# This should work fine
minimal_config = MetagitConfig(name="minimal")
minimal_record = MetagitRecord.from_metagit_config(minimal_config)
back_to_minimal = minimal_record.to_metagit_config()
````

## File: examples/record_manager_example.py
````python
#!/usr/bin/env python
"""
Example script demonstrating the updated MetagitRecordManager.

This script shows how to:
1. Create records from existing metagit config data
2. Store records using local file storage backend
3. Store records using OpenSearch storage backend
4. Search and retrieve records
"""
⋮----
async def example_local_file_storage()
⋮----
"""Example using local file storage backend."""
⋮----
# Initialize logger
logger = UnifiedLogger(LoggerConfig(log_level="INFO", minimal_console=True))
⋮----
# Create local file storage backend
storage_dir = Path("./records")
local_backend = LocalFileStorageBackend(storage_dir)
⋮----
# Initialize record manager with local backend
record_manager = MetagitRecordManager(
⋮----
# Create a sample config manager
config_manager = MetagitConfigManager()
⋮----
# Create a sample config
sample_config = config_manager.create_config(
⋮----
# Create record from config
record = record_manager.create_record_from_config(
⋮----
# Store the record
record_id = await record_manager.store_record(record)
⋮----
# Retrieve the record
retrieved_record = await record_manager.get_record(record_id)
⋮----
# Search records
search_results = await record_manager.search_records("example")
⋮----
# List all records
all_records = await record_manager.list_records()
⋮----
async def example_opensearch_storage()
⋮----
"""Example using OpenSearch storage backend."""
⋮----
# Note: This example requires a running OpenSearch instance
# You would need to configure the OpenSearchService first
⋮----
# Import OpenSearchService (this would fail if opensearchpy is not installed)
⋮----
# Initialize OpenSearch service
opensearch_service = OpenSearchService(
⋮----
# Create OpenSearch storage backend
opensearch_backend = OpenSearchStorageBackend(opensearch_service)
⋮----
# Initialize record manager with OpenSearch backend
⋮----
search_results = await record_manager.search_records("opensearch")
⋮----
def example_file_operations()
⋮----
"""Example of direct file operations."""
⋮----
# Initialize record manager without storage backend
record_manager = MetagitRecordManager()
⋮----
# Save record to file
file_path = Path("./example-record.yml")
save_result = record_manager.save_record_to_file(record, file_path)
⋮----
# Load record from file
loaded_record = record_manager.load_record_from_file(file_path)
⋮----
# Clean up
⋮----
async def main()
⋮----
"""Run all examples."""
⋮----
# Run local file storage example
⋮----
# Run OpenSearch storage example
⋮----
# Run file operations example
````

## File: examples/repository_analysis_example.py
````python
#!/usr/bin/env python3
⋮----
"""
Example demonstrating the updated DetectionManager with all detection analysis results.

This example shows how DetectionManager now contains all the analysis results that were
previously in RepositoryAnalysis, including branch analysis, CI/CD analysis, and directory analysis.
"""
⋮----
# Add the metagit package to the path
⋮----
def example_local_repository_analysis()
⋮----
"""Demonstrate analyzing a local repository with all analysis results."""
⋮----
# Create DetectionManager for local path
analysis = DetectionManager.from_path("./")
⋮----
# Run analysis
result = analysis.run_all()
⋮----
# Display all analysis results
⋮----
# Language detection
⋮----
# Project type detection
⋮----
# Branch analysis
⋮----
# CI/CD analysis
⋮----
# Directory analysis
⋮----
# File analysis
⋮----
# Metrics
⋮----
# Metadata
⋮----
def example_remote_repository_analysis()
⋮----
"""Demonstrate analyzing a remote repository."""
⋮----
# Example repository URL
repo_url = "https://github.com/octocat/Hello-World.git"
⋮----
# Create DetectionManager for remote URL
analysis = DetectionManager.from_url(repo_url)
⋮----
# Display key analysis results
⋮----
# Clean up cloned repository
⋮----
def example_specific_analysis()
⋮----
"""Demonstrate running specific analysis methods."""
⋮----
# Create DetectionManager
⋮----
# Run specific analysis methods
methods = ["language_detection", "project_type_detection", "branch_analysis"]
⋮----
result = analysis.run_specific(method)
⋮----
def example_configuration()
⋮----
"""Demonstrate using different detection configurations."""
⋮----
# Create minimal configuration
minimal_config = DetectionManagerConfig.minimal()
⋮----
# Create full configuration
full_config = DetectionManagerConfig.all_enabled()
⋮----
# Create custom configuration
custom_config = DetectionManagerConfig(
⋮----
# Use custom configuration
analysis = DetectionManager.from_path("./", config=custom_config)
⋮----
def main()
⋮----
"""Run all examples."""
````

## File: examples/repository_detection.py
````python
#!/usr/bin/env python3
"""
Example script demonstrating the repository detection module.

This script shows how to use the RepositoryAnalysis class to analyze
both local repositories and remote git repositories.
"""
⋮----
# Add the metagit package to the path
⋮----
def analyze_local_repository(path: str) -> None
⋮----
"""Analyze a local repository path."""
⋮----
# Create logger
logger = UnifiedLogger(LoggerConfig(log_level="INFO", minimal_console=True))
⋮----
# Analyze the repository
analysis = DetectionManager.from_path(path, logger)
⋮----
# Print summary
summary = analysis.summary()
⋮----
# Convert to MetagitConfig
config = analysis.to_metagit_config()
⋮----
def analyze_remote_repository(url: str) -> None
⋮----
"""Analyze a remote git repository by cloning it."""
⋮----
# Create temporary directory for cloning
temp_dir = tempfile.mkdtemp(prefix="metagit_example_")
⋮----
analysis = DetectionManager.from_url(url, logger, temp_dir)
⋮----
# Clean up
⋮----
# analysis.cleanup()
⋮----
# Clean up temp directory if analysis failed
⋮----
def main()
⋮----
"""Main function demonstrating repository detection."""
⋮----
# Example 1: Analyze current directory (if it's a git repo)
current_dir = os.getcwd()
⋮----
# Example 2: Analyze a well-known open source repository
# Using a small, well-known repository for demonstration
remote_url = "https://github.com/octocat/Hello-World.git"
⋮----
# Example 3: Analyze another repository
# You can uncomment and modify these lines to test with other repositories
# analyze_remote_repository("https://github.com/python/cpython.git")
# analyze_remote_repository("https://github.com/torvalds/linux.git")
````

## File: examples/test_appconfig_env.py
````python
#!/usr/bin/env python
⋮----
"""
Test script to verify AppConfig environment variable loading.
"""
⋮----
def test_appconfig_env_loading()
⋮----
"""Test that AppConfig loads environment variables correctly."""
⋮----
# Create a temporary .env file
⋮----
env_file = f.name
⋮----
# Set environment variables manually for testing
⋮----
# Load AppConfig
config = AppConfig.load()
⋮----
# Verify environment variables were loaded
⋮----
# Test LLM configuration
⋮----
# Test GitHub provider configuration
⋮----
# Test GitLab provider configuration
⋮----
# Test main API configuration
⋮----
# Clean up
⋮----
# Clean up environment variables
⋮----
success = test_appconfig_env_loading()
````

## File: examples/test_record_manager_simple.py
````python
#!/usr/bin/env python
"""
Simple test script for the updated MetagitRecordManager.

This script tests the basic functionality without requiring pytest.
"""
⋮----
def test_basic_functionality()
⋮----
"""Test basic functionality of the updated MetagitRecordManager."""
⋮----
# Create a temporary directory for testing
⋮----
temp_path = Path(temp_dir)
⋮----
# Test 1: Create storage backend
⋮----
backend = LocalFileStorageBackend(temp_path)
⋮----
# Test 2: Create record manager
⋮----
record_manager = MetagitRecordManager(storage_backend=backend)
⋮----
# Test 3: Create sample config
⋮----
config_manager = MetagitConfigManager()
sample_config = config_manager.create_config(
⋮----
# Test 4: Create record from config
⋮----
record = record_manager.create_record_from_config(
⋮----
# Test 5: Store record
⋮----
async def store_record()
⋮----
record_id = await record_manager.store_record(record)
⋮----
record_id = asyncio.run(store_record())
⋮----
# Test 6: Retrieve record
⋮----
async def get_record()
⋮----
retrieved_record = await record_manager.get_record(record_id)
⋮----
retrieved_record = asyncio.run(get_record())
⋮----
# Test 7: Search records
⋮----
async def search_records()
⋮----
search_results = await record_manager.search_records("test")
⋮----
search_results = asyncio.run(search_records())
⋮----
# Test 8: List records
⋮----
async def list_records()
⋮----
records = await record_manager.list_records()
⋮----
records = asyncio.run(list_records())
⋮----
# Test 9: File operations
⋮----
file_path = temp_path / "test-record.yml"
save_result = record_manager.save_record_to_file(record, file_path)
⋮----
loaded_record = record_manager.load_record_from_file(file_path)
⋮----
def test_error_handling()
⋮----
"""Test error handling scenarios."""
⋮----
# Test 1: Record manager without storage backend
⋮----
manager = MetagitRecordManager()
⋮----
async def test_no_backend()
⋮----
record = MetagitRecord(
result = await manager.store_record(record)
⋮----
result = asyncio.run(test_no_backend())
⋮----
# Test 2: Loading non-existent file
⋮----
result = manager.load_record_from_file(Path("nonexistent.yml"))
````

## File: schemas/backend_metadata.yml
````yaml
project:
  name: example-service
  slug: example-service
  repo_url: https://github.com/org/example-service
  provider: github
  project_type: application  # [application, library, cli, api, infra, etc.]
  languages:
    - python
    - javascript
  frameworks:
    - fastapi
    - react
  runtime_environments:
    - python3.11
    - node18
  build_tools:
    - poetry
    - npm
  deployment_targets:
    - aws-lambda
    - ecs

metrics:
  sloc: 8124
  file_count: 152
  dependency_count: 42

artifacts:
  containers:
    - name: example-service
      image: ghcr.io/org/example-service:latest
      dockerfile_path: ./Dockerfile
      base_image: python:3.11-slim
      exposed_ports: [8000]
      env_vars:
        - ENV
        - DEBUG
  libraries:
    - name: example-lib
      version: 1.2.0
      output_type: wheel
      language: python
      exported_modules:
        - example.auth
        - example.utils
      compatible_with:
        - python3.8
        - python3.11
  cli_tools:
    - name: example-cli
      entrypoint: cli.py
      flags:
        - --config
        - --dry-run

components:
  - name: auth_utils
    type: module
    path: src/example/auth/utils.py
    language: python
    functions:
      - name: hash_password
        parameters: [password: str]
        returns: str
        docstring: "Hash a plaintext password using bcrypt."
        examples:
          - "hash_password('mypassword')"
      - name: verify_password
        parameters: [plain: str, hashed: str]
        returns: bool
  - name: HealthCheckRoute
    type: class
    path: src/example/routes/health.py
    description: "Provides /health endpoint."

semantics:
  inferred_tags:
    - auth
    - api
    - observability
  tech_stack:
    - fastapi
    - pydantic
    - postgresql
  role: backend
  domain_tags:
    - account-management
    - user-authentication

dependencies:
  internal:
    - repo: shared-utils
      path: ./libs/shared-utils
      version: latest
  external:
    - name: pydantic
      version: 2.1.0
      license: MIT
    - name: requests
      version: 2.31.0
      license: Apache-2.0

ownership:
  maintainers:
    - name: Jane Doe
      email: jane@company.com
  last_commit: 2025-06-17T14:20:00Z
  recent_contributors:
    - alice
    - bob
  open_issues:
    - "Fix flaky auth tests"
    - "Add rate limiting to login route"

quality:
  test_coverage: 88
  lint_score: 9.2
  security_scan:
    last_scan: 2025-06-15
    high_risk_issues: 0
  build_status: passing
  ci_pipeline: github-actions

agent_assist:
  actionable_exports:
    - hash_password
    - verify_password
  openapi_endpoints:
    - GET /health
    - POST /login
  test_coverage_map:
    - function: hash_password
      covered: true
    - function: send_email
      covered: false
  embeddings_index: embeddings/example-service.index

navigation:
  ast_available: true
  call_graph_available: true
  semantic_search_index: search/example-service.vecdb
````

## File: schemas/repo_metadata.yml
````yaml
repo:
  name: my-repo-name
  description: Brief summary of the project
  url: https://github.com/org/my-repo
  visibility: public | private | internal
  owner:
    org: org-name
    team: platform-team
    contact: dev-team@example.com

project:
  type: application | library | microservice | cli | iac | config | data-science | plugin | template | docs | test | other
  domain: web | mobile | devops | ml | database | security | finance | gaming | iot | agent | other
  language:
    primary: python
    secondary: [bash, terraform]
  framework: [django, fastapi]
  package_managers: [pip, poetry]
  build_tool: make | cmake | bazel | none
  deploy_targets: [ecs, lambda, kubernetes]

metadata:
  tags: [ml, nlp, internal-tool, devops, infra, agentic, data-science, cli, website, other]
  created_at: 2021-08-15
  last_commit_at: 2025-06-18
  default_branch: main
  license: MIT | Apache-2.0 | proprietary
  topics: [nlp, fastapi, containerized]
  forked_from: null | https://github.com/upstream/original
  archived: false
  template: false
  has_ci: true
  has_tests: true
  has_docs: true
  has_docker: true
  has_iac: true

metrics:
  stars: 15
  forks: 3
  open_issues: 2
  pull_requests:
    open: 1
    merged_last_30d: 4
  contributors: 6
  commit_frequency: weekly | daily | monthly

ci_cd:
  platform: github-actions | gitlab | jenkins | circleci | none
  pipelines: [test, lint, build, deploy]
  status_badges: true

dependencies:
  external_services: [s3, postgres, stripe-api]
  critical_libs: [torch, pandas]

iac:
  type: terraform | cloudformation | pulumi | none
  environments:
    - name: dev
      cloud: aws
      region: us-west-2
      config_path: infra/dev
    - name: prod
      cloud: aws
      region: us-east-1
      config_path: infra/prod

agentic:
  reusable_components:
    - name: embed_text
      path: lib/embedding.py
      description: Generates sentence embeddings using SentenceTransformers
  entrypoints:
    - name: main_app
      file: app/main.py
      function: run_app()
  test_interface:
    test_framework: pytest
    test_coverage: 85%
````

## File: src/metagit/cli/commands/__init__.py
````python

````

## File: src/metagit/cli/commands/gitcache.py
````python
#!/usr/bin/env python
"""
CLI commands for git cache management.

This module provides command-line interface for managing git cache operations.
"""
⋮----
@click.group()
def gitcache()
⋮----
"""Git cache management commands."""
⋮----
"""Cache a repository or local directory."""
⋮----
config = GitCacheConfig(
⋮----
manager = GitCacheManager(config)
⋮----
entry = asyncio.run(manager.cache_repository_async(source, name))
⋮----
entry = manager.cache_repository(source, name)
⋮----
def list(cache_root: str)
⋮----
"""List all cache entries."""
⋮----
config = GitCacheConfig(cache_root=Path(cache_root))
⋮----
entries = manager.list_cache_entries()
⋮----
# Show git information for git repositories
⋮----
def stats(cache_root: str)
⋮----
"""Show cache statistics."""
⋮----
stats = manager.get_cache_stats()
⋮----
@click.option("--async", "use_async", is_flag=True, help="Use async operations")
def refresh(name: str, cache_root: str, use_async: bool)
⋮----
"""Refresh a cache entry."""
⋮----
entry = asyncio.run(manager.refresh_cache_entry_async(name))
⋮----
entry = manager.refresh_cache_entry(name)
⋮----
def remove(name: str, cache_root: str)
⋮----
"""Remove a cache entry."""
⋮----
result = manager.remove_cache_entry(name)
⋮----
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
def clear(cache_root: str, yes: bool)
⋮----
"""Clear all cache entries."""
⋮----
result = manager.clear_cache()
⋮----
def path(name: str, cache_root: str)
⋮----
"""Get the path to a cached repository."""
⋮----
cache_path = manager.get_cached_repository(name)
⋮----
contents = list(cache_path.iterdir())
⋮----
def details(name: str, cache_root: str)
⋮----
"""Get detailed information about a cache entry."""
⋮----
details = manager.get_cache_entry_details(name)
⋮----
# Basic information
⋮----
# Size information
⋮----
# Timestamps
⋮----
# Git-specific information
⋮----
# Current information (if different from stored)
````

## File: src/metagit/cli/commands/record.py
````python
#!/usr/bin/env python
"""
Record management subcommands for metagit.

This module provides CLI commands for managing metagit records using the
MetagitRecordManager with support for multiple storage backends.
"""
⋮----
class DateTimeEncoder(json.JSONEncoder)
⋮----
"""Custom JSON encoder that handles datetime objects."""
⋮----
def default(self, obj)
⋮----
"""Record management subcommands"""
⋮----
# If no subcommand is provided, show help
⋮----
# Store storage configuration in context
⋮----
logger = ctx.obj.get("logger")
⋮----
def _get_record_manager(ctx: click.Context) -> MetagitRecordManager
⋮----
"""Get a configured MetagitRecordManager instance."""
logger = ctx.obj["logger"]
storage_type = ctx.obj["storage_type"]
⋮----
storage_path = Path(ctx.obj["storage_path"])
backend = LocalFileStorageBackend(storage_path)
⋮----
# Import OpenSearchService here to avoid import issues if not installed
⋮----
# Parse hosts
hosts = []
⋮----
opensearch_service = OpenSearchService(
backend = OpenSearchStorageBackend(opensearch_service)
⋮----
"""Create a record from metagit configuration"""
⋮----
# Load configuration
config_manager = MetagitConfigManager(config_path=Path(config_path))
config_result = config_manager.load_config()
⋮----
# Create record manager
record_manager = _get_record_manager(ctx)
⋮----
# Create record from config
record = record_manager.create_record_from_config(
⋮----
# Save to file if requested
⋮----
file_path = Path(output_file)
save_result = record_manager.save_record_to_file(record, file_path)
⋮----
# Store in backend
async def store_record()
⋮----
record_id = asyncio.run(store_record())
⋮----
@click.pass_context
def record_show(ctx: click.Context, record_id: Optional[str], format: str) -> None
⋮----
"""Show record(s)"""
⋮----
# Show specific record
async def get_record()
⋮----
record = asyncio.run(get_record())
⋮----
yaml.Dumper.ignore_aliases = lambda *args: True  # noqa: ARG005
output = yaml.dump(
⋮----
else:  # json
⋮----
# List all records
async def list_records()
⋮----
records = asyncio.run(list_records())
⋮----
"""Search records"""
⋮----
async def search_records()
⋮----
results = asyncio.run(search_records())
⋮----
records = results.get("records", [])
total = results.get("total", 0)
current_page = results.get("page", 1)
total_pages = results.get("pages", 1)
⋮----
# Simple table format
⋮----
record_id = getattr(record, "record_id", "N/A")
description = record.description or "N/A"
⋮----
description = description[:27] + "..."
⋮----
output = json.dumps(
⋮----
"""Update an existing record"""
⋮----
# Get existing record
⋮----
existing_record = asyncio.run(get_record())
⋮----
# Load updated config if provided
⋮----
# Create updated record
updated_record = record_manager.create_record_from_config(
⋮----
# Update only specific fields
updated_record = existing_record
⋮----
None  # Will be set by create_record_from_config
⋮----
# Update the record
async def update_record()
⋮----
result = asyncio.run(update_record())
⋮----
@click.pass_context
def record_delete(ctx: click.Context, record_id: str, force: bool) -> None
⋮----
"""Delete a record"""
⋮----
# Show record info before deletion
⋮----
# Delete the record
⋮----
async def delete_record()
⋮----
result = asyncio.run(delete_record())
⋮----
"""Export a record to file"""
⋮----
# Get the record
⋮----
# Export to file
⋮----
"""Import a record from file"""
⋮----
# Load record from file
file_path = Path(input_file)
record = record_manager.load_record_from_file(file_path)
⋮----
# Override fields if specified
⋮----
# Store the record
⋮----
@record.command("stats")
@click.pass_context
def record_stats(ctx: click.Context) -> None
⋮----
"""Show record storage statistics"""
⋮----
# Get all records for statistics
⋮----
total_records = len(records)
⋮----
# Calculate statistics
sources = {}
kinds = {}
languages = {}
⋮----
# Count by detection source
source = record.detection_source or "unknown"
⋮----
# Count by project kind
kind = record.kind or "unknown"
⋮----
# Count by primary language
⋮----
lang = record.language.primary
⋮----
# Display statistics
````

## File: src/metagit/cli/__init__.py
````python

````

## File: src/metagit/core/config/manager.py
````python
#!/usr/bin/env python
"""
Class for managing .metagit.yml configuration files.

This package provides a class for managing .metagit.yml configuration files.
"""
⋮----
class MetagitConfigManager
⋮----
"""
    Manager class for handling .metagit.yml configuration files.

    This class provides methods for loading, validating, and creating
    .metagit.yml configuration files with proper error handling and validation.
    """
⋮----
"""
        Initialize the MetagitConfigManager.

        Args:
            config_path: Path to the .metagit.yml file. If None, defaults to .metagit.yml in current directory.
        """
⋮----
@property
    def config(self) -> Union[MetagitConfig, None, Exception]
⋮----
"""
        Get the loaded configuration.

        Returns:
            MetagitConfig: The loaded configuration, or None if not loaded
        """
⋮----
def load_config(self) -> Union[MetagitConfig, Exception]
⋮----
"""
        Load and validate a .metagit.yml configuration file.

        Returns:
            MetagitConfig: Validated configuration object

        Raises:
            FileNotFoundError: If the configuration file is not found
            yaml.YAMLError: If the YAML file is malformed
            ValidationError: If the configuration doesn't match the expected schema
        """
⋮----
yaml_data = yaml.safe_load(f)
⋮----
def validate_config(self) -> Union[bool, Exception]
⋮----
"""
        Validate a .metagit.yml configuration file without loading it into memory.

        Returns:
            bool: True if the configuration is valid, False otherwise
        """
⋮----
load_result = self.load_config()
⋮----
"""
        Create a .metagit.yml project configuration file.

        Args:
            output_path: Path where to save the configuration. If None, returns the config as string.

        Returns:
            MetagitConfig or str: The created configuration object or YAML string
        """
⋮----
workspace = None
⋮----
workspace = Workspace(
project_config = MetagitConfig(
⋮----
def reload_config(self) -> Union[MetagitConfig, Exception]
⋮----
"""
        Reload the configuration from disk.

        Returns:
            MetagitConfig: The reloaded configuration object
        """
⋮----
"""
        Save a configuration to a YAML file.

        Args:
            config: Configuration to save. If None, uses the loaded config.
            output_path: Path where to save the configuration. If None, uses the instance config_path.
        """
⋮----
config_to_save = config or self._config
⋮----
save_path = output_path or self.config_path
⋮----
"""
    Create a top level .metagit.yml configuration file.
    """
logger = logger or UnifiedLogger(
⋮----
git_repo = Repo(Path.cwd())
name = Path(git_repo.working_dir).name
⋮----
name = Path.cwd().name
⋮----
description = git_repo.description or "No description"
⋮----
url = git_repo.remote().url or None
⋮----
kind = "application"
⋮----
config_manager = MetagitConfigManager()
config_result = config_manager.create_config(
⋮----
yaml.Dumper.ignore_aliases = lambda *args: True  # noqa: ARG005
output = yaml.dump(
````

## File: src/metagit/core/flows/detect_flow/crews/project_understanding_crew/config/agents.yaml
````yaml
code_structure_analyst:
  role: Codebase Structure Mapper
  goal: Analyze and document the structure and key files of a codebase
  backstory: >
    A veteran software architect known for deconstructing complex projects by examining
    file hierarchies and naming patterns.

dependency_reviewer:
  role: Dependency Inspector
  goal: Extract and analyze dependencies and configurations
  backstory: >
    A DevOps veteran with deep knowledge of Python packaging, Docker, and environment files.

logic_summarizer:
  role: Code Logic Interpreter
  goal: Summarize key classes and functions
  backstory: >
    A code reader AI trained on thousands of GitHub projects to recognize core logic patterns.

summary_formatter:
  role: Summary Structurer
  goal: Format the collected analysis into a pydantic structure
  backstory: >
    A schema-obsessed engineer AI that formats unstructured output into structured insights.
````

## File: src/metagit/core/flows/detect_flow/crews/project_understanding_crew/config/tasks.yaml
````yaml
structure_task:
  description: >
    Traverse the codebase at {path}. Count files, extract file types, and identify entry points.
    Your final answer MUST include `num_files`, `file_types`, and `entry_points`.
  expected_output: >
    {
      "num_files": int,
      "file_types": List[str],
      "entry_points": List[str]
    }

dependency_task:
  description: >
    Parse pyproject.toml, requirements.txt, and Dockerfile at {path}.
    Identify dependencies, Docker usage, and .env or config presence.
    Output must include: `main_dependencies`, `uses_docker`, `uses_env_files`.
  expected_output: >
    {
      "main_dependencies": List[str],
      "uses_docker": bool,
      "uses_env_files": bool
    }

logic_task:
  description: >
    Analyze main modules at {path} and summarize key classes and functions.
    Output must include `modules`, `key_classes`, and `key_functions`.
  expected_output: >
    {
      "modules": List[str],
      "key_classes": List[str],
      "key_functions": List[str]
    }

formatting_task:
  description: >
    Using structure_task, dependency_task, and logic_task outputs, build a MetagitConfig object.
    Your final output MUST match the full MetagitConfig pydantic class structure.
  expected_output: >
    A dict that can be used to instantiate a MetagitConfig pydantic object.
````

## File: src/metagit/core/flows/detect_flow/crews/project_understanding_crew/project_understanding_crew.py
````python
# If you want to run a snippet of code before or after the crew starts,
# you can use the @before_kickoff and @after_kickoff decorators
# https://docs.crewai.com/concepts/crews#example-crew-class-with-decorators
⋮----
@CrewBase
class ProjectUnderstandingCrew
⋮----
"""ProjectUnderstandingCrew crew"""
⋮----
agents: list[BaseAgent]
tasks: list[Task]
⋮----
# Learn more about YAML configuration files here:
# Agents: https://docs.crewai.com/concepts/agents#yaml-configuration-recommended
# Tasks: https://docs.crewai.com/concepts/tasks#yaml-configuration-recommended
⋮----
# If you would like to add tools to your agents, you can learn more about it here:
# https://docs.crewai.com/concepts/agents#agent-tools
⋮----
@agent
    def researcher(self) -> Union[Agent, Exception]
⋮----
config=self.agents_config["researcher"],  # type: ignore[index]
⋮----
@agent
    def reporting_analyst(self) -> Union[Agent, Exception]
⋮----
config=self.agents_config["reporting_analyst"],  # type: ignore[index]
⋮----
# To learn more about structured task outputs,
# task dependencies, and task callbacks, check out the documentation:
# https://docs.crewai.com/concepts/tasks#overview-of-a-task
⋮----
@task
    def research_task(self) -> Union[Task, Exception]
⋮----
config=self.tasks_config["research_task"],  # type: ignore[index]
⋮----
@task
    def reporting_task(self) -> Union[Task, Exception]
⋮----
config=self.tasks_config["reporting_task"],  # type: ignore[index]
⋮----
@crew
    def crew(self) -> Union[Crew, Exception]
⋮----
"""Creates the ProjectUnderstandingCrew crew"""
⋮----
# To learn how to add knowledge sources to your crew, check out the documentation:
# https://docs.crewai.com/concepts/knowledge#what-is-knowledge
⋮----
agents=self.agents,  # Automatically created by the @agent decorator
tasks=self.tasks,  # Automatically created by the @task decorator
⋮----
# process=Process.hierarchical, # In case you wanna use that instead https://docs.crewai.com/how-to/Hierarchical/
````

## File: src/metagit/core/flows/detect_flow/tools/__init__.py
````python

````

## File: src/metagit/core/flows/detect_flow/tools/human.py
````python
"""Tool for asking human input."""
⋮----
def _print_func(text: str) -> Union[None, Exception]
⋮----
def input_func() -> Union[str, Exception]
⋮----
contents = []
⋮----
line = input()
⋮----
class MyToolInput(BaseModel)
⋮----
"""Input schema for MyCustomTool."""
⋮----
query: str = Field(..., description="Query to the human.")
⋮----
class HumanTool(BaseTool)
⋮----
name: str = "HumanTool"
description: str = (
args_schema: type[BaseModel] = MyToolInput
prompt_func: Callable[[str], None] = _print_func
input_func: Callable[[], str] = input_func
⋮----
def _run(self, query: str) -> Union[str, Exception]
⋮----
"""Use the Multi Line Human input tool."""
⋮----
prompt_result = self.prompt_func(query)
````

## File: src/metagit/core/flows/detect_flow/tools/index_tools.py
````python
#! /usr/bin/env python3
"""
Index tools for the detect flow
"""
⋮----
"""
    Walk the repo, split files, embed, and store in FAISS.
    Returns index metadata location.
    """
⋮----
embeddings = OpenAIEmbeddings()
text_splitter = RecursiveCharacterTextSplitter(
docs = []
⋮----
full = Path(root) / fn
text = full.read_text(encoding="utf-8")
⋮----
vectorstore = FAISS.from_documents(
````

## File: src/metagit/core/flows/detect_flow/__init__.py
````python

````

## File: src/metagit/core/flows/detect_flow/main.py
````python
#!/usr/bin/env python
⋮----
# class PoemState(BaseModel):
#     sentence_count: int = 1
#     poem: str = ""
⋮----
# class PoemFlow(Flow[PoemState]):
⋮----
#     @start()
#     def generate_sentence_count(self):
#         print("Generating sentence count")
#         self.state.sentence_count = randint(1, 5)
⋮----
#     @listen(generate_sentence_count)
#     def generate_poem(self):
#         print("Generating poem")
#         result = (
#             PoemCrew()
#             .crew()
#             .kickoff(inputs={"sentence_count": self.state.sentence_count})
#         )
⋮----
#         print("Poem generated", result.raw)
#         self.state.poem = result.raw
⋮----
#     @listen(generate_poem)
#     def save_poem(self):
#         print("Saving poem")
#         with open("poem.txt", "w") as f:
#             f.write(self.state.poem)
⋮----
# def kickoff():
#     poem_flow = PoemFlow()
#     poem_flow.kickoff()
⋮----
# def plot():
⋮----
#     poem_flow.plot()
⋮----
# kickoff()
````

## File: src/metagit/core/gitcache/__init__.py
````python
#!/usr/bin/env python
"""
Git cache management module.

This module provides functionality for caching git repositories locally,
supporting both remote cloning and local file copying with async and sync operations.
"""
⋮----
__all__ = ["GitCacheConfig", "GitCacheManager"]
````

## File: src/metagit/core/gitcache/config.py
````python
#!/usr/bin/env python
"""
Git cache configuration models.

This module defines the Pydantic models used for configuring
the git cache management system.
"""
⋮----
class CacheType(str, Enum)
⋮----
"""Enumeration of cache types."""
⋮----
GIT = "git"
LOCAL = "local"
⋮----
class CacheStatus(str, Enum)
⋮----
"""Enumeration of cache statuses."""
⋮----
FRESH = "fresh"
STALE = "stale"
MISSING = "missing"
ERROR = "error"
⋮----
class GitCacheEntry(BaseModel)
⋮----
"""Model for a single git cache entry."""
⋮----
name: str = Field(..., description="Cache entry name/identifier")
source_url: str = Field(..., description="Source URL or local path")
cache_type: CacheType = Field(..., description="Type of cache entry")
cache_path: Path = Field(..., description="Local cache path")
created_at: datetime = Field(
last_updated: datetime = Field(
last_accessed: datetime = Field(
size_bytes: Optional[int] = Field(None, description="Cache size in bytes")
status: CacheStatus = Field(
error_message: Optional[str] = Field(
metadata: Dict[str, Any] = Field(
⋮----
# Git-specific tracking fields
local_commit_hash: Optional[str] = Field(
local_branch: Optional[str] = Field(None, description="Current local branch name")
remote_commit_hash: Optional[str] = Field(
remote_branch: Optional[str] = Field(None, description="Default remote branch name")
has_upstream_changes: Optional[bool] = Field(
upstream_changes_summary: Optional[str] = Field(
last_diff_check: Optional[datetime] = Field(
⋮----
@field_validator("cache_path", mode="before")
@classmethod
    def validate_cache_path(cls, v: Any) -> Path
⋮----
"""Convert string to Path object."""
⋮----
class Config
⋮----
"""Pydantic configuration."""
⋮----
use_enum_values = True
extra = "forbid"
⋮----
class GitCacheConfig(BaseModel)
⋮----
"""Configuration model for git cache management."""
⋮----
cache_root: Path = Field(
default_timeout_minutes: int = Field(
max_cache_size_gb: float = Field(
enable_async: bool = Field(default=True, description="Enable async operations")
git_config: Dict[str, Any] = Field(
provider_config: Optional[Dict[str, Any]] = Field(
entries: Dict[str, GitCacheEntry] = Field(
⋮----
@field_validator("cache_root", mode="before")
@classmethod
    def validate_cache_root(cls, v: Any) -> Path
⋮----
"""Convert string to Path object and ensure it exists."""
⋮----
cache_path = Path(v)
⋮----
cache_path = v
⋮----
# Create cache directory if it doesn't exist
⋮----
@field_validator("default_timeout_minutes")
@classmethod
    def validate_timeout(cls, v: int) -> int
⋮----
"""Validate timeout is positive."""
⋮----
@field_validator("max_cache_size_gb")
@classmethod
    def validate_max_size(cls, v: float) -> float
⋮----
"""Validate max cache size is positive."""
⋮----
def get_cache_path(self, name: str) -> Path
⋮----
"""Get the cache path for a specific entry."""
⋮----
def is_entry_stale(self, entry: GitCacheEntry) -> bool
⋮----
"""Check if a cache entry is stale based on timeout."""
timeout_delta = timedelta(minutes=self.default_timeout_minutes)
⋮----
def get_cache_size_bytes(self) -> int
⋮----
"""Get total cache size in bytes."""
total_size = 0
⋮----
def get_cache_size_gb(self) -> float
⋮----
"""Get total cache size in GB."""
⋮----
def is_cache_full(self) -> bool
⋮----
"""Check if cache is at maximum size."""
⋮----
def add_entry(self, entry: GitCacheEntry) -> None
⋮----
"""Add a cache entry."""
⋮----
def remove_entry(self, name: str) -> bool
⋮----
"""Remove a cache entry."""
⋮----
def get_entry(self, name: str) -> Optional[GitCacheEntry]
⋮----
"""Get a cache entry by name."""
⋮----
def list_entries(self) -> List[GitCacheEntry]
⋮----
"""List all cache entries."""
⋮----
def clear_entries(self) -> None
⋮----
"""Clear all cache entries."""
````

## File: src/metagit/core/gitcache/manager.py
````python
#!/usr/bin/env python
"""
Git cache manager for handling repository caching operations.

This module provides both synchronous and asynchronous operations
for caching git repositories and local directories.
"""
⋮----
import git  # Add this at the top with other imports
⋮----
logger = logging.getLogger(__name__)
⋮----
class GitCacheManager
⋮----
"""Manager for git cache operations."""
⋮----
def __init__(self, config: GitCacheConfig)
⋮----
"""
        Initialize the git cache manager.

        Args:
            config: Git cache configuration
        """
⋮----
def register_provider(self, provider: GitProvider) -> None
⋮----
"""
        Register a git provider for handling specific URLs.

        Args:
            provider: Git provider instance
        """
⋮----
def _get_provider_for_url(self, url: str) -> Optional[GitProvider]
⋮----
"""
        Get the appropriate provider for a given URL.

        Args:
            url: Repository URL

        Returns:
            Git provider or None if no provider supports the URL
        """
normalized_url = normalize_git_url(url)
⋮----
def _generate_cache_name(self, source: str) -> str
⋮----
"""
        Generate a cache name from source URL or path.

        Args:
            source: Source URL or local path

        Returns:
            Generated cache name
        """
# For git URLs, extract repo name
⋮----
normalized_url = normalize_git_url(source)
# Extract repo name from URL
⋮----
repo_name = normalized_url.split("/")[-1]
⋮----
repo_name = repo_name[:-4]
⋮----
# For local paths, use the directory name
path = Path(source)
⋮----
def _is_git_url(self, source: str) -> bool
⋮----
"""
        Check if source is a git URL.

        Args:
            source: Source URL or path

        Returns:
            True if source is a git URL
        """
⋮----
def _is_local_path(self, source: str) -> bool
⋮----
"""
        Check if source is a local path.

        Args:
            source: Source URL or path

        Returns:
            True if source is a local path
        """
⋮----
def _is_git_repository(self, path: Path) -> bool
⋮----
"""
        Check if a local path is a git repository using gitpython.

        Args:
            path: Local path to check

        Returns:
            True if path is a git repository
        """
⋮----
_ = git.Repo(path)
⋮----
def _is_local_git_repository(self, source: str) -> bool
⋮----
"""
        Check if source is a local git repository.

        Args:
            source: Source URL or path

        Returns:
            True if source is a local git repository
        """
⋮----
def _clone_repository(self, url: str, cache_path: Path) -> Union[bool, Exception]
⋮----
"""
        Clone a git repository using gitpython.

        Args:
            url: Repository URL
            cache_path: Local cache path

        Returns:
            True if successful, Exception if failed
        """
⋮----
# Remove existing directory if it exists
⋮----
"""
        Clone a git repository asynchronously using gitpython in a thread.

        Args:
            url: Repository URL
            cache_path: Local cache path

        Returns:
            True if successful, Exception if failed
        """
⋮----
result = await asyncio.to_thread(self._clone_repository, url, cache_path)
⋮----
"""
        Copy a local directory to cache.

        Args:
            source_path: Source directory path
            cache_path: Local cache path

        Returns:
            True if successful, Exception if failed
        """
⋮----
# Copy directory
⋮----
"""
        Copy a local directory to cache asynchronously.

        Args:
            source_path: Source directory path
            cache_path: Local cache path

        Returns:
            True if successful, Exception if failed
        """
⋮----
# Run copy operation in thread pool to avoid blocking
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(
⋮----
def _pull_updates(self, cache_path: Path) -> Union[bool, Exception]
⋮----
"""
        Pull updates for an existing git repository using gitpython.

        Args:
            cache_path: Local cache path

        Returns:
            True if successful, Exception if failed
        """
⋮----
repo = git.Repo(cache_path)
origin = repo.remotes.origin
⋮----
async def _pull_updates_async(self, cache_path: Path) -> Union[bool, Exception]
⋮----
"""
        Pull updates for an existing git repository asynchronously using gitpython in a thread.

        Args:
            cache_path: Local cache path

        Returns:
            True if successful, Exception if failed
        """
⋮----
result = await asyncio.to_thread(self._pull_updates, cache_path)
⋮----
def _calculate_directory_size(self, path: Path) -> int
⋮----
"""
        Calculate directory size in bytes.

        Args:
            path: Directory path

        Returns:
            Size in bytes
        """
total_size = 0
⋮----
"""
        Cache a repository or local directory synchronously.

        Args:
            source: Source URL or local path
            name: Optional cache name (auto-generated if not provided)

        Returns:
            GitCacheEntry if successful, Exception if failed
        """
⋮----
# Generate cache name if not provided
⋮----
name = self._generate_cache_name(source)
⋮----
# Check if entry already exists
existing_entry = self.config.get_entry(name)
cache_path = self.config.get_cache_path(name)
⋮----
# Determine cache type
⋮----
cache_type = CacheType.GIT
⋮----
cache_type = CacheType.LOCAL
⋮----
# Handle existing cache
⋮----
# Check for differences before pulling updates
diff_info = self._check_repository_differences(cache_path)
⋮----
# Update entry with difference information
⋮----
# Only pull if there are upstream changes
⋮----
result = self._pull_updates(cache_path)
⋮----
# For local directories, recopy
result = self._copy_local_directory(Path(source), cache_path)
⋮----
# Update entry
⋮----
# Create new cache entry
entry = GitCacheEntry(
⋮----
# Perform caching operation
⋮----
result = self._clone_repository(source, cache_path)
⋮----
# Calculate size and add entry
⋮----
# For git repositories, populate git information
⋮----
"""
        Cache a repository or local directory asynchronously.

        Args:
            source: Source URL or local path
            name: Optional cache name (auto-generated if not provided)

        Returns:
            GitCacheEntry if successful, Exception if failed
        """
⋮----
result = await self._pull_updates_async(cache_path)
⋮----
result = await self._copy_local_directory_async(
⋮----
result = await self._clone_repository_async(source, cache_path)
⋮----
def get_cached_repository(self, name: str) -> Union[Path, Exception]
⋮----
"""
        Get the path to a cached repository.

        Args:
            name: Cache entry name

        Returns:
            Path to cached repository or Exception if not found
        """
entry = self.config.get_entry(name)
⋮----
# Update last accessed time
⋮----
# Check if cache is stale
⋮----
def list_cache_entries(self) -> List[GitCacheEntry]
⋮----
"""
        List all cache entries.

        Returns:
            List of cache entries
        """
entries = self.config.list_entries()
⋮----
# Update status for each entry
⋮----
def remove_cache_entry(self, name: str) -> Union[bool, Exception]
⋮----
"""
        Remove a cache entry and its files.

        Args:
            name: Cache entry name

        Returns:
            True if successful, Exception if failed
        """
⋮----
# Remove cache directory
⋮----
# Remove entry from config
⋮----
def refresh_cache_entry(self, name: str) -> Union[GitCacheEntry, Exception]
⋮----
"""
        Refresh a cache entry by re-caching the source.

        Args:
            name: Cache entry name

        Returns:
            Updated GitCacheEntry or Exception if failed
        """
⋮----
# Re-cache the source
⋮----
"""
        Refresh a cache entry by re-caching the source asynchronously.

        Args:
            name: Cache entry name

        Returns:
            Updated GitCacheEntry or Exception if failed
        """
⋮----
def clear_cache(self) -> Union[bool, Exception]
⋮----
"""
        Clear all cache entries and files.

        Returns:
            True if successful, Exception if failed
        """
⋮----
def get_cache_stats(self) -> Dict[str, Any]
⋮----
"""
        Get cache statistics.

        Returns:
            Dictionary with cache statistics
        """
⋮----
total_entries = len(entries)
git_entries = sum(1 for e in entries if e.cache_type == CacheType.GIT)
local_entries = sum(1 for e in entries if e.cache_type == CacheType.LOCAL)
⋮----
fresh_entries = sum(1 for e in entries if e.status == CacheStatus.FRESH)
stale_entries = sum(1 for e in entries if e.status == CacheStatus.STALE)
missing_entries = sum(1 for e in entries if e.status == CacheStatus.MISSING)
error_entries = sum(1 for e in entries if e.status == CacheStatus.ERROR)
⋮----
total_size_bytes = self.config.get_cache_size_bytes()
total_size_gb = self.config.get_cache_size_gb()
⋮----
def _get_repository_info(self, repo_path: Path) -> Dict[str, Any]
⋮----
"""
        Get repository information including commit hash and branch.

        Args:
            repo_path: Path to the git repository

        Returns:
            Dictionary with repository information
        """
⋮----
repo = git.Repo(repo_path)
head = repo.head
⋮----
info = {
⋮----
# Try to get branch name if detached
⋮----
# Get the branch that HEAD was pointing to
⋮----
"""
        Get remote repository information.

        Args:
            repo_path: Path to the git repository
            remote_name: Name of the remote (default: origin)

        Returns:
            Dictionary with remote information
        """
⋮----
remote = repo.remotes[remote_name]
⋮----
# Fetch latest info from remote
⋮----
# Get default branch
default_branch = None
⋮----
# Try to get default branch from remote
default_branch = (
⋮----
# Fallback to common default branches
⋮----
default_branch = branch
⋮----
remote_ref = remote.refs[default_branch]
⋮----
def _check_repository_differences(self, repo_path: Path) -> Dict[str, Any]
⋮----
"""
        Check for differences between local and remote repositories.

        Args:
            repo_path: Path to the git repository

        Returns:
            Dictionary with difference information
        """
⋮----
# Get local info
local_info = self._get_repository_info(repo_path)
⋮----
# Get remote info
remote_info = self._get_remote_info(repo_path)
⋮----
# Check if there are differences
has_changes = False
changes_summary = ""
⋮----
has_changes = True
⋮----
# Get commit difference summary
⋮----
# Get commits that are in remote but not in local
local_commit = repo.commit(local_info["commit_hash"])
remote_commit = repo.commit(remote_info["commit_hash"])
⋮----
# Get commits ahead and behind
ahead_commits = list(
behind_commits = list(
⋮----
changes_summary = f"Remote is {len(ahead_commits)} commits ahead, local is {len(behind_commits)} commits ahead"
⋮----
changes_summary = f"Commit hashes differ: local={local_info['commit_hash'][:8]}, remote={remote_info['commit_hash'][:8]}"
⋮----
def get_cache_entry_details(self, name: str) -> Union[Dict[str, Any], Exception]
⋮----
"""
        Get detailed information about a cache entry including git differences.

        Args:
            name: Cache entry name

        Returns:
            Dictionary with detailed entry information or Exception if not found
        """
⋮----
details = {
⋮----
# Add git-specific information for git repositories
⋮----
# Check for fresh differences if last check was more than 5 minutes ago
⋮----
diff_info = self._check_repository_differences(entry.cache_path)
````

## File: src/metagit/core/gitcache/README.md
````markdown
# Git Cache Management System

The Git Cache Management System provides a comprehensive solution for caching git repositories and local directories with support for both synchronous and asynchronous operations.

## Features

- **Dual Operation Modes**: Support for both synchronous and asynchronous operations
- **Multiple Source Types**: Cache both git repositories and local directories
- **Provider Integration**: Use existing git provider configurations for authentication
- **Smart Caching**: Check for existing cache and pull updates for git repositories
- **Local Directory Support**: Full directory copying for local sources
- **Cache Management**: List, remove, and refresh cache entries
- **Timeout Management**: Configurable cache timeout with automatic stale detection
- **Size Management**: Track and limit cache size
- **Error Handling**: Comprehensive error handling with detailed status tracking

## Architecture

### Core Components

1. **GitCacheConfig**: Central configuration management using Pydantic models
2. **GitCacheEntry**: Individual cache entry representation
3. **GitCacheManager**: Main manager class handling all cache operations

### File Structure

```
metagit/core/gitcache/
├── __init__.py          # Module exports
├── config.py           # Configuration models
├── manager.py          # Main cache manager
└── README.md           # This file
```

## Usage

### Basic Setup

```python
from metagit.core.gitcache import GitCacheConfig, GitCacheManager
from pathlib import Path

# Create configuration
config = GitCacheConfig(
    cache_root=Path("./.metagit/.cache"),
    default_timeout_minutes=60,
    max_cache_size_gb=10.0
)

# Create manager
manager = GitCacheManager(config)
```

### Caching Git Repositories

```python
# Cache a git repository
entry = manager.cache_repository("https://github.com/octocat/Hello-World.git")

if isinstance(entry, Exception):
    print(f"Error: {entry}")
else:
    print(f"Cached: {entry.name}")
    print(f"Path: {entry.cache_path}")
    print(f"Status: {entry.status}")
```

### Caching Local Directories

```python
# Cache a local directory
entry = manager.cache_repository("/path/to/local/project", name="my-project")

if isinstance(entry, Exception):
    print(f"Error: {entry}")
else:
    print(f"Cached: {entry.name}")
    print(f"Type: {entry.cache_type}")
```

### Asynchronous Operations

```python
import asyncio

async def cache_multiple_repos():
    # Cache multiple repositories concurrently
    tasks = [
        manager.cache_repository_async("https://github.com/user/repo1.git"),
        manager.cache_repository_async("https://github.com/user/repo2.git"),
        manager.cache_repository_async("/path/to/local/project")
    ]
    
    results = await asyncio.gather(*tasks, return_exceptions=True)
    
    for result in results:
        if isinstance(result, Exception):
            print(f"Error: {result}")
        else:
            print(f"Successfully cached: {result.name}")

# Run async function
asyncio.run(cache_multiple_repos())
```

### Cache Management

```python
# List all cache entries
entries = manager.list_cache_entries()
for entry in entries:
    print(f"{entry.name}: {entry.cache_type} ({entry.status})")

# Get cache statistics
stats = manager.get_cache_stats()
print(f"Total entries: {stats['total_entries']}")
print(f"Total size: {stats['total_size_gb']:.2f} GB")

# Get cached repository path
cache_path = manager.get_cached_repository("repo-name")
if isinstance(cache_path, Exception):
    print(f"Error: {cache_path}")
else:
    print(f"Cache path: {cache_path}")

# Refresh cache entry
entry = manager.refresh_cache_entry("repo-name")

# Remove cache entry
result = manager.remove_cache_entry("repo-name")

# Clear all cache
result = manager.clear_cache()
```

### Provider Integration

```python
from metagit.core.providers.github import GitHubProvider

# Register a git provider
provider = GitHubProvider(api_token="your-token")
manager.register_provider(provider)

# The manager will use the provider for authentication when cloning
entry = manager.cache_repository("https://github.com/private/repo.git")
```

## Configuration

### GitCacheConfig Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `cache_root` | Path | `./.metagit/.cache` | Root directory for cache storage |
| `default_timeout_minutes` | int | 60 | Default cache timeout in minutes |
| `max_cache_size_gb` | float | 10.0 | Maximum cache size in GB |
| `enable_async` | bool | True | Enable async operations |
| `git_config` | Dict | {} | Git configuration options |
| `provider_config` | Dict | None | Provider-specific configuration |

### Git Configuration

```python
config = GitCacheConfig(
    git_config={
        "user.name": "Your Name",
        "user.email": "your.email@example.com",
        "http.extraheader": "AUTHORIZATION: basic <base64-encoded-token>"
    }
)
```

## Cache Entry Status

Cache entries have the following statuses:

- **FRESH**: Cache is up-to-date and within timeout
- **STALE**: Cache exists but is older than timeout
- **MISSING**: Cache entry exists but files are missing
- **ERROR**: Cache operation failed

## Error Handling

All operations return either the expected result or an Exception. This allows for comprehensive error handling:

```python
entry = manager.cache_repository("https://github.com/user/repo.git")

if isinstance(entry, Exception):
    # Handle error
    print(f"Cache failed: {entry}")
    # Check if it's a specific type of error
    if "Authentication failed" in str(entry):
        print("Please check your credentials")
    elif "Repository not found" in str(entry):
        print("Repository does not exist or is private")
else:
    # Handle success
    print(f"Successfully cached: {entry.name}")
```

## Best Practices

1. **Use Provider Configuration**: Register git providers for authentication
2. **Handle Errors Gracefully**: Always check return types for exceptions
3. **Monitor Cache Size**: Use `get_cache_stats()` to monitor cache usage
4. **Refresh Stale Cache**: Use `refresh_cache_entry()` for important repositories
5. **Clean Up Regularly**: Remove unused cache entries to save space
6. **Use Async for Multiple Operations**: Use async methods when caching multiple repositories

## Examples

See `examples/gitcache_example.py` for comprehensive usage examples including:

- Synchronous operations
- Asynchronous operations
- Cache management
- Error handling
- Provider integration

## Testing

Run the test suite:

```bash
python -m pytest tests/test_gitcache.py -v
```

The test suite covers:

- Configuration validation
- Cache entry management
- Repository cloning
- Local directory copying
- Async operations
- Error scenarios
- Cache statistics

## Integration

The Git Cache Management System integrates with:

- **Git Providers**: Use existing provider configurations for authentication
- **Project Management**: Cache repositories for analysis
- **CI/CD Pipelines**: Cache dependencies and tools
- **Development Workflows**: Local development with cached repositories
````

## File: src/metagit/core/providers/__init__.py
````python
#!/usr/bin/env python
"""
Provider registry for Git hosting platforms.
"""
⋮----
logger = logging.getLogger(__name__)
⋮----
class ProviderRegistry
⋮----
"""Registry for git provider plugins."""
⋮----
def __init__(self)
⋮----
def register(self, provider: GitProvider) -> None
⋮----
"""Register a git provider plugin."""
⋮----
def unregister(self, provider_name: str) -> None
⋮----
"""Unregister a provider by name."""
⋮----
def clear(self) -> None
⋮----
"""Clear all registered providers."""
⋮----
def get_provider_for_url(self, url: str) -> Optional[GitProvider]
⋮----
"""Get the appropriate provider for a given URL."""
normalized_url = normalize_git_url(url)
⋮----
def get_all_providers(self) -> list[GitProvider]
⋮----
"""Get all registered providers."""
⋮----
def get_provider_by_name(self, name: str) -> Optional[GitProvider]
⋮----
"""Get a provider by name."""
⋮----
def configure_from_app_config(self, app_config) -> None
⋮----
"""
        Configure providers from AppConfig settings.

        Args:
            app_config: AppConfig instance with provider settings
        """
⋮----
# Clear existing providers
⋮----
# Configure GitHub provider
⋮----
github_provider = GitHubProvider(
⋮----
pass  # GitHub provider not available
⋮----
# Configure GitLab provider
⋮----
gitlab_provider = GitLabProvider(
⋮----
pass  # GitLab provider not available
⋮----
def configure_from_environment(self) -> None
⋮----
"""Configure providers from environment variables (legacy method)."""
# GitHub provider
github_token = os.getenv("GITHUB_TOKEN")
⋮----
github_provider = GitHubProvider(api_token=github_token)
⋮----
# GitLab provider
gitlab_token = os.getenv("GITLAB_TOKEN")
⋮----
gitlab_provider = GitLabProvider(api_token=gitlab_token)
⋮----
# Global registry instance
registry = ProviderRegistry()
⋮----
# Export the registry for backward compatibility
__all__ = ["GitProvider", "ProviderRegistry", "registry"]
````

## File: src/metagit/core/providers/base.py
````python
#!/usr/bin/env python
"""
Base provider for Git hosting platforms.
"""
⋮----
logger = logging.getLogger(__name__)
⋮----
class GitProvider(ABC)
⋮----
"""Base class for git provider plugins."""
⋮----
def __init__(self, api_token: Optional[str] = None, base_url: Optional[str] = None)
⋮----
"""
        Initialize the git provider.

        Args:
            api_token: API token for authentication
            base_url: Base URL for the API (for self-hosted instances)
        """
⋮----
@abstractmethod
    def get_name(self) -> str
⋮----
"""Get the provider name."""
⋮----
@abstractmethod
    def can_handle_url(self, url: str) -> bool
⋮----
"""Check if this provider can handle the given repository URL."""
⋮----
@abstractmethod
    def extract_repo_info(self, url: str) -> Dict[str, str]
⋮----
"""
        Extract repository information from URL.

        Returns:
            Dict with keys: owner, repo, api_url
        """
⋮----
"""
        Get repository metrics from the provider.

        Args:
            owner: Repository owner/organization
            repo: Repository name

        Returns:
            Metrics object or Exception
        """
⋮----
"""
        Get additional repository metadata.

        Args:
            owner: Repository owner/organization
            repo: Repository name

        Returns:
            Dict with metadata or Exception
        """
⋮----
def supports_url(self, url: str) -> bool
⋮----
"""Check if this provider supports the given URL."""
normalized_url = normalize_git_url(url)
⋮----
def is_available(self) -> bool
⋮----
"""Check if the provider is available (has API token, etc.)."""
````

## File: src/metagit/core/providers/github.py
````python
#!/usr/bin/env python3
"""
GitHub provider for repository metadata and metrics.
"""
⋮----
logger = logging.getLogger(__name__)
⋮----
class GitHubProvider(GitProvider)
⋮----
"""GitHub provider plugin."""
⋮----
def __init__(self, api_token: Optional[str] = None, base_url: Optional[str] = None)
⋮----
"""
        Initialize GitHub provider.

        Args:
            api_token: GitHub personal access token
            base_url: Base URL for GitHub API (for GitHub Enterprise)
        """
⋮----
def get_name(self) -> str
⋮----
"""Get the provider name."""
⋮----
def can_handle_url(self, url: str) -> bool
⋮----
"""Check if this provider can handle the given repository URL."""
parsed = urlparse(url)
⋮----
def extract_repo_info(self, url: str) -> Dict[str, str]
⋮----
"""Extract owner and repo from GitHub URL."""
normalized_url = normalize_git_url(url)
⋮----
# GitHub URL patterns
patterns = [
⋮----
match = re.match(pattern, normalized_url)
⋮----
"""Get repository metrics from GitHub API."""
⋮----
# Get repository data
repo_url = f"{self.api_base}/repos/{owner}/{repo}"
repo_response = self.session.get(repo_url)
⋮----
repo_data = repo_response.json()
⋮----
# Get issues data
issues_url = f"{self.api_base}/repos/{owner}/{repo}/issues"
issues_params = {"state": "open", "per_page": 1}
issues_response = self.session.get(issues_url, params=issues_params)
⋮----
# Get pull requests data
prs_url = f"{self.api_base}/repos/{owner}/{repo}/pulls"
prs_params = {"state": "open", "per_page": 1}
prs_response = self.session.get(prs_url, params=prs_params)
⋮----
# Get contributors data
contributors_url = f"{self.api_base}/repos/{owner}/{repo}/contributors"
contributors_response = self.session.get(contributors_url)
⋮----
contributors_data = contributors_response.json()
⋮----
# Get recent commits for commit frequency
commits_url = f"{self.api_base}/repos/{owner}/{repo}/commits"
commits_params = {"per_page": 100}
commits_response = self.session.get(commits_url, params=commits_params)
⋮----
commits_data = commits_response.json()
⋮----
# Calculate commit frequency
commit_frequency = self._calculate_commit_frequency(commits_data)
⋮----
# Create pull requests object
pull_requests = PullRequests(
⋮----
merged_last_30d=0,  # Would need additional API call for this
⋮----
# Create metrics object
metrics = Metrics(
⋮----
"""Get additional repository metadata from GitHub API."""
⋮----
response = self.session.get(repo_url)
⋮----
repo_data = response.json()
⋮----
# Get topics
topics_url = f"{self.api_base}/repos/{owner}/{repo}/topics"
topics_response = self.session.get(topics_url)
topics_data = (
⋮----
metadata = {
⋮----
"""Calculate commit frequency from recent commits."""
⋮----
# Get commit dates
commit_dates = []
⋮----
date_str = commit["commit"]["author"]["date"]
⋮----
# Calculate frequency based on recent activity
⋮----
recent_commits = 0
week_ago = datetime.now(timezone.utc) - timedelta(days=7)
⋮----
for date_str in commit_dates[:10]:  # Check last 10 commits
⋮----
commit_date = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
````

## File: src/metagit/core/providers/gitlab.py
````python
#!/usr/bin/env python3
"""
GitLab provider for repository metadata and metrics.
"""
⋮----
logger = logging.getLogger(__name__)
⋮----
class GitLabProvider(GitProvider)
⋮----
"""GitLab provider plugin."""
⋮----
def __init__(self, api_token: Optional[str] = None, base_url: Optional[str] = None)
⋮----
"""
        Initialize GitLab provider.

        Args:
            api_token: GitLab personal access token
            base_url: Base URL for GitLab API (for self-hosted instances)
        """
⋮----
def get_name(self) -> str
⋮----
"""Get the provider name."""
⋮----
def can_handle_url(self, url: str) -> bool
⋮----
"""Check if this provider can handle the given repository URL."""
parsed = urlparse(url)
⋮----
def extract_repo_info(self, url: str) -> Dict[str, str]
⋮----
"""Extract owner and repo from GitLab URL."""
normalized_url = normalize_git_url(url)
⋮----
# GitLab URL patterns
patterns = [
⋮----
match = re.match(pattern, normalized_url)
⋮----
"""Get repository metrics from GitLab API."""
⋮----
project_path = f"{owner}/{repo}"
project_id = project_path.replace("/", "%2F")
⋮----
# Get project data
project_url = f"{self.api_base}/projects/{project_id}"
project_response = self.session.get(project_url)
⋮----
project_data = project_response.json()
⋮----
# Get issues data
issues_url = f"{self.api_base}/projects/{project_id}/issues"
issues_params = {"state": "opened", "per_page": 1}
issues_response = self.session.get(issues_url, params=issues_params)
⋮----
# Get merge requests data
mr_url = f"{self.api_base}/projects/{project_id}/merge_requests"
mr_params = {"state": "opened", "per_page": 1}
mr_response = self.session.get(mr_url, params=mr_params)
⋮----
# Get contributors data (approximation using project members)
members_url = f"{self.api_base}/projects/{project_id}/members"
members_response = self.session.get(members_url)
members_data = (
⋮----
# Get recent commits for commit frequency
commits_url = f"{self.api_base}/projects/{project_id}/repository/commits"
commits_params = {"per_page": 100}
commits_response = self.session.get(commits_url, params=commits_params)
⋮----
commits_data = commits_response.json()
⋮----
# Calculate commit frequency
commit_frequency = self._calculate_commit_frequency(commits_data)
⋮----
# Create pull requests object (GitLab calls them merge requests)
pull_requests = PullRequests(
⋮----
merged_last_30d=0,  # Would need additional API call for this
⋮----
# Create metrics object
metrics = Metrics(
⋮----
"""Get additional repository metadata from GitLab API."""
⋮----
response = self.session.get(project_url)
⋮----
project_data = response.json()
⋮----
metadata = {
⋮----
"""Calculate commit frequency from recent commits."""
⋮----
# Get commit dates
commit_dates = []
⋮----
# Calculate frequency based on recent activity
⋮----
recent_commits = 0
week_ago = datetime.now(timezone.utc) - timedelta(days=7)
⋮----
for date_str in commit_dates[:10]:  # Check last 10 commits
⋮----
commit_date = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
````

## File: src/metagit/core/record/__init__.py
````python
#!/usr/bin/env python
"""
Record management module for metagit.

This module provides classes for managing metagit records with support for
multiple storage backends (OpenSearch and local files) and the ability to
create records from existing metagit configuration data.
"""
⋮----
__all__ = [
````

## File: src/metagit/core/record/manager.py
````python
#!/usr/bin/env python
"""
Class for managing metagit records.

This package provides a class for managing metagit records with support for
multiple storage backends (OpenSearch and local files) and the ability to
create records from existing metagit configuration data.
"""
⋮----
class DateTimeEncoder(json.JSONEncoder)
⋮----
"""Custom JSON encoder that handles datetime objects."""
⋮----
def default(self, obj)
⋮----
class RecordStorageBackend(ABC)
⋮----
"""Abstract base class for record storage backends."""
⋮----
@abstractmethod
    async def store_record(self, record: MetagitRecord) -> Union[str, Exception]
⋮----
"""Store a record and return the record ID."""
⋮----
@abstractmethod
    async def get_record(self, record_id: str) -> Union[MetagitRecord, Exception]
⋮----
"""Retrieve a record by ID."""
⋮----
"""Update an existing record."""
⋮----
@abstractmethod
    async def delete_record(self, record_id: str) -> Union[bool, Exception]
⋮----
"""Delete a record by ID."""
⋮----
"""Search records with optional filters."""
⋮----
"""List all records with pagination."""
⋮----
class LocalFileStorageBackend(RecordStorageBackend)
⋮----
"""Local file-based storage backend for records."""
⋮----
def __init__(self, storage_dir: Path)
⋮----
"""
        Initialize local file storage backend.

        Args:
            storage_dir: Directory to store record files
        """
⋮----
def _ensure_index_exists(self) -> None
⋮----
"""Ensure the index file exists."""
⋮----
def _load_index(self) -> Dict[str, Any]
⋮----
"""Load the index file."""
⋮----
def _save_index(self, index_data: Dict[str, Any]) -> None
⋮----
"""Save the index file."""
⋮----
def _get_next_id(self) -> str
⋮----
"""Get the next available record ID."""
index_data = self._load_index()
next_id = index_data["next_id"]
⋮----
async def store_record(self, record: MetagitRecord) -> Union[str, Exception]
⋮----
"""Store a record to local file."""
⋮----
record_id = self._get_next_id()
record_file = self.storage_dir / f"{record_id}.json"
⋮----
# Add metadata
record_data = record.model_dump(exclude_none=True, exclude_defaults=True)
⋮----
# Update index
⋮----
async def get_record(self, record_id: str) -> Union[MetagitRecord, Exception]
⋮----
record_data = json.load(f)
⋮----
# Remove metadata fields before creating record
⋮----
# Load existing data to preserve metadata
⋮----
existing_data = json.load(f)
⋮----
# Update record data
⋮----
async def delete_record(self, record_id: str) -> Union[bool, Exception]
⋮----
all_records = await self.list_records(
⋮----
)  # Get all for search
⋮----
# Simple text search
filtered_records = []
⋮----
# Apply additional filters
⋮----
filtered_records = [
⋮----
# Pagination
start_idx = (page - 1) * size
end_idx = start_idx + size
paginated_records = filtered_records[start_idx:end_idx]
⋮----
record_ids = list(index_data["records"].keys())
⋮----
paginated_ids = record_ids[start_idx:end_idx]
⋮----
records = []
⋮----
record_result = await self.get_record(record_id)
⋮----
continue  # Skip failed records
⋮----
class OpenSearchStorageBackend(RecordStorageBackend)
⋮----
"""OpenSearch-based storage backend for records."""
⋮----
def __init__(self, opensearch_service)
⋮----
"""
        Initialize OpenSearch storage backend.

        Args:
            opensearch_service: Configured OpenSearchService instance
        """
⋮----
"""Store a record to OpenSearch."""
⋮----
search_result = await self.opensearch_service.search_records(
⋮----
class MetagitRecordManager
⋮----
"""
    Manager class for handling metagit records.

    This class provides methods for loading, validating, and creating
    metagit records with proper error handling and validation.
    Supports multiple storage backends (OpenSearch and local files).
    """
⋮----
"""
        Initialize the MetagitRecordManager.

        Args:
            storage_backend: Storage backend for records (OpenSearch or local file)
            metagit_config_manager: Optional MetagitConfigManager instance
            logger: Optional logger instance
        """
⋮----
"""
        Create a MetagitRecord from existing MetagitConfig data.

        Args:
            config: MetagitConfig to convert. If None, uses config from config_manager.
            detection_source: Source of the detection (e.g., 'github', 'gitlab', 'local')
            detection_version: Version of the detection system used
            additional_data: Additional data to include in the record

        Returns:
            MetagitRecord: The created record
        """
⋮----
# Get config from parameter or config manager
⋮----
config_result = self.config_manager.load_config()
⋮----
config = config_result
⋮----
# Get current git information
git_info = self._get_git_info()
⋮----
# Create record data
record_data = config.model_dump(exclude_none=True, exclude_defaults=True)
⋮----
# Add detection-specific fields
⋮----
# Add additional data if provided
⋮----
# Create and validate record
record = MetagitRecord(**record_data)
⋮----
def _get_git_info(self) -> Dict[str, Optional[str]]
⋮----
"""Get current git repository information."""
⋮----
repo = Repo(Path.cwd())
⋮----
"""
        Store a record using the configured storage backend.

        Args:
            record: MetagitRecord to store

        Returns:
            str: Record ID if successful, Exception otherwise
        """
⋮----
"""
        Retrieve a record by ID.

        Args:
            record_id: ID of the record to retrieve

        Returns:
            MetagitRecord: The retrieved record, or Exception if failed
        """
⋮----
"""
        Update an existing record.

        Args:
            record_id: ID of the record to update
            record: Updated MetagitRecord

        Returns:
            bool: True if successful, Exception otherwise
        """
⋮----
"""
        Delete a record by ID.

        Args:
            record_id: ID of the record to delete

        Returns:
            bool: True if successful, Exception otherwise
        """
⋮----
"""
        Search records with optional filters.

        Args:
            query: Search query string
            filters: Optional filters to apply
            page: Page number for pagination
            size: Number of records per page

        Returns:
            Dict: Search results with pagination info
        """
⋮----
"""
        List all records with pagination.

        Args:
            page: Page number for pagination
            size: Number of records per page

        Returns:
            List[MetagitRecord]: List of records
        """
⋮----
"""
        Save a record to a local YAML file.

        Args:
            record: MetagitRecord to save
            file_path: Path where to save the record

        Returns:
            None if successful, Exception otherwise
        """
⋮----
def load_record_from_file(self, file_path: Path) -> Union[MetagitRecord, Exception]
⋮----
"""
        Load a record from a local YAML file.

        Args:
            file_path: Path to the record file

        Returns:
            MetagitRecord: The loaded record, or Exception if failed
        """
⋮----
yaml_data = yaml.safe_load(f)
````

## File: src/metagit/core/record/models.py
````python
#!/usr/bin/env python
"""
Pydantic models for metagit records.
"""
⋮----
# Import models from detect module for forward references
⋮----
# Forward references for type hints
LanguageDetection = "LanguageDetection"
ProjectTypeDetection = "ProjectTypeDetection"
GitBranchAnalysis = "GitBranchAnalysis"
CIConfigAnalysis = "CIConfigAnalysis"
DirectoryDetails = "DirectoryDetails"
DirectorySummary = "DirectorySummary"
⋮----
T = TypeVar("T", bound=BaseModel)
⋮----
"""
    Automatically detect common fields between two Pydantic models.

    This utility function uses Pydantic's field introspection to find
    fields that exist in both models, making conversion more maintainable.

    Args:
        source_model: The source model class
        target_model: The target model class

    Returns:
        Set of field names that exist in both models
    """
source_fields = set(source_model.model_fields.keys())
target_fields = set(target_model.model_fields.keys())
⋮----
"""
    Convert data between Pydantic models with automatic field mapping.

    This function provides a generic way to convert data between any two
    Pydantic models by automatically detecting compatible fields.

    Args:
        source_data: Dictionary of source model data
        target_model: Target model class
        field_mapping: Optional mapping of source field names to target field names

    Returns:
        Instance of target model

    Raises:
        ValueError: If conversion fails
    """
⋮----
# Apply field mapping if provided
⋮----
mapped_data = {}
⋮----
source_data = mapped_data
⋮----
# Filter to only include fields that exist in target model
⋮----
filtered_data = {k: v for k, v in source_data.items() if k in target_fields}
⋮----
# Use model_validate for fast, validated conversion
⋮----
class MetagitRecord(MetagitConfig)
⋮----
"""
    Extended model for metagit records that includes detection-specific data suitable for OpenSearch.

    This class inherits from MetagitConfig and adds detection-specific attributes.
    Now includes all RepositoryAnalysis attributes for comprehensive repository information.
    """
⋮----
# Detection-specific attributes
branch: Optional[str] = Field(None, description="Current branch")
checksum: Optional[str] = Field(None, description="Branch checksum")
last_updated: Optional[datetime] = Field(None, description="Last updated timestamp")
branches: Optional[List[Branch]] = Field(None, description="Release branches")
metrics: Optional[Metrics] = Field(None, description="Repository metrics")
metadata: Optional[RepoMetadata] = Field(None, description="Repository metadata")
⋮----
# Language and project type detection
language: Optional[Language] = Field(
language_version: Optional[str] = Field(
domain: Optional[ProjectDomain] = Field(None, description="Project domain")
⋮----
# Additional detection fields
detection_timestamp: Optional[datetime] = Field(
detection_source: Optional[str] = Field(
detection_version: Optional[str] = Field(
⋮----
# RepositoryAnalysis attributes merged from repository.py
# Repository path and URL information
path: Optional[str] = Field(None, description="Repository path")
url: Optional[str] = Field(None, description="Repository URL")
is_git_repo: bool = Field(
is_cloned: bool = Field(
temp_dir: Optional[str] = Field(None, description="Temporary directory if cloned")
⋮----
# Detection results from RepositoryAnalysis
language_detection: Optional[LanguageDetection] = Field(
project_type_detection: Optional[ProjectTypeDetection] = Field(
⋮----
# Analysis results from RepositoryAnalysis
branch_analysis: Optional[GitBranchAnalysis] = Field(
ci_config_analysis: Optional[CIConfigAnalysis] = Field(
directory_summary: Optional[DirectorySummary] = Field(
directory_details: Optional[DirectoryDetails] = Field(
⋮----
# Repository metadata from RepositoryAnalysis
license_info: Optional[License] = Field(None, description="License information")
maintainers: List[Maintainer] = Field(
existing_workspace: Optional[Workspace] = Field(
⋮----
# Additional metadata from RepositoryAnalysis
artifacts: Optional[List[Artifact]] = Field(
secrets_management: Optional[List[str]] = Field(
secrets: Optional[List[Secret]] = Field(None, description="Repository secrets")
documentation: Optional[List[str]] = Field(
alerts: Optional[List[AlertingChannel]] = Field(
dashboards: Optional[List[Dashboard]] = Field(None, description="Dashboards")
environments: Optional[List[Environment]] = Field(None, description="Environments")
⋮----
# File analysis from RepositoryAnalysis
detected_files: Dict[str, List[str]] = Field(
has_docker: bool = Field(
has_tests: bool = Field(
has_docs: bool = Field(
has_iac: bool = Field(
⋮----
class Config
⋮----
"""Pydantic configuration."""
⋮----
use_enum_values = True
validate_assignment = True
extra = "forbid"
⋮----
@classmethod
    def from_yaml(cls, yaml_str: str) -> "MetagitRecord"
⋮----
"""Create a MetagitRecord from a YAML string."""
⋮----
@classmethod
    def from_json(cls, json_str: str) -> "MetagitRecord"
⋮----
"""Create a MetagitRecord from a JSON string."""
⋮----
@classmethod
    def from_dict(cls, data: dict) -> "MetagitRecord"
⋮----
"""Create a MetagitRecord from a dictionary."""
⋮----
def to_yaml(self) -> str
⋮----
"""Convert a MetagitRecord to a YAML string."""
⋮----
@classmethod
    def to_json(self) -> str
⋮----
"""Convert a MetagitRecord to a JSON string."""
⋮----
def to_metagit_config(self, exclude_detection_fields: bool = True) -> MetagitConfig
⋮----
"""
        Fast conversion from MetagitRecord to MetagitConfig using automatic field detection.

        This method efficiently converts a MetagitRecord to a MetagitConfig by:
        1. Using Pydantic's field introspection to automatically detect compatible fields
        2. Leveraging model_dump() with field filtering for optimal performance
        3. Using model_validate() for fast, validated conversion
        4. Automatically handling field differences between models

        Args:
            exclude_detection_fields: Whether to exclude detection-specific fields
                                     (currently always True as MetagitConfig doesn't support them)

        Returns:
            MetagitConfig: A new MetagitConfig instance with the shared fields

        Example:
            record = MetagitRecord(name="my-project", description="A project")
            config = record.to_metagit_config()

        Performance Notes:
            - Uses Pydantic's field introspection for automatic field detection
            - Leverages Pydantic's C-optimized validation
            - Minimal memory allocation through direct field copying
            - No deep copying of nested objects (uses references)
        """
# Get the model data, excluding None values and defaults for performance
model_data = self.model_dump(
⋮----
# Use the generic conversion utility for automatic field mapping
⋮----
def to_metagit_config_advanced(self, **kwargs) -> MetagitConfig
⋮----
"""
        Advanced conversion method with automatic field mapping and validation.

        This method provides more sophisticated conversion capabilities:
        1. Automatic field compatibility detection
        2. Type conversion and validation
        3. Support for custom field mappings
        4. Better error handling and reporting

        Args:
            **kwargs: Additional options for conversion behavior

        Returns:
            MetagitConfig: A new MetagitConfig instance

        Example:
            record = MetagitRecord(name="my-project", description="A project")
            config = record.to_metagit_config_advanced()
        """
⋮----
# Use the standard method for now, but this could be extended
# with more sophisticated field mapping logic
⋮----
# Could add more sophisticated error handling here
⋮----
"""
        Fast conversion from MetagitConfig to MetagitRecord using latest Pydantic best practices.

        This method efficiently converts a MetagitConfig to a MetagitRecord by:
        1. Using model_dump() for optimal serialization performance
        2. Adding detection-specific fields with minimal overhead
        3. Leveraging Pydantic's built-in validation
        4. Supporting additional detection data injection

        Args:
            config: MetagitConfig instance to convert
            detection_source: Source of the detection (e.g., 'github', 'gitlab', 'local')
            detection_version: Version of the detection system used
            additional_detection_data: Additional detection-specific data to include

        Returns:
            MetagitRecord: A new MetagitRecord instance

        Example:
            config = MetagitConfig(name="my-project", description="A project")
            record = MetagitRecord.from_metagit_config(
                config,
                detection_source="github",
                detection_version="2.0.0"
            )

        Performance Notes:
            - Uses model_dump() with exclude_none=True for optimal serialization
            - Leverages Pydantic's C-optimized validation
            - Minimal memory allocation through direct field copying
            - No deep copying of nested objects (uses references)
        """
# Get the base config data
record_data = config.model_dump(exclude_none=True, exclude_defaults=True)
⋮----
# Add detection-specific fields
⋮----
# Add additional detection data if provided
⋮----
"""
        Advanced conversion from MetagitConfig to MetagitRecord with automatic field handling.

        This method provides more sophisticated conversion capabilities:
        1. Automatic field mapping and validation
        2. Support for complex detection data structures
        3. Better error handling and reporting
        4. Extensible for future enhancements

        Args:
            config: MetagitConfig instance to convert
            **detection_kwargs: Detection-specific parameters and data

        Returns:
            MetagitRecord: A new MetagitRecord instance

        Example:
            config = MetagitConfig(name="my-project", description="A project")
            record = MetagitRecord.from_metagit_config_advanced(
                config,
                detection_source="github",
                detection_version="2.0.0",
                branch="main",
                checksum="abc123"
            )
        """
# Extract standard detection parameters
detection_source = detection_kwargs.pop("detection_source", "local")
detection_version = detection_kwargs.pop("detection_version", "1.0.0")
⋮----
def get_detection_summary(self) -> dict
⋮----
"""
        Get a summary of detection-specific data for quick analysis.

        Returns:
            dict: Summary of detection data including source, version, and key metrics
        """
summary = {
⋮----
# Add metrics summary if available
⋮----
# Add metadata summary if available
⋮----
@classmethod
    def get_field_differences(cls) -> dict
⋮----
"""
        Get the field differences between MetagitRecord and MetagitConfig.

        This method helps understand what fields are unique to each model,
        making it easier to understand the conversion behavior.

        Returns:
            dict: Field differences between the models
        """
record_fields = set(cls.model_fields.keys())
config_fields = set(MetagitConfig.model_fields.keys())
⋮----
@classmethod
    def get_compatible_fields(cls) -> set[str]
⋮----
"""
        Get the fields that are compatible between MetagitRecord and MetagitConfig.

        Returns:
            set: Field names that exist in both models
        """
⋮----
# from metagit.core.detect.models import (
#     CIConfigAnalysis,
#     GitBranchAnalysis,
#     LanguageDetection,
#     ProjectTypeDetection,
# )
# from metagit.core.utils.files import DirectoryDetails, DirectorySummary
⋮----
# MetagitRecord.model_rebuild()
````

## File: src/metagit/core/utils/click.py
````python
"""
Click utility functions
"""
⋮----
def call_click_command(cmd, *args, **kwargs)
⋮----
"""Wrapper to call a click command

    :param cmd: click cli command function to call
    :param args: arguments to pass to the function
    :param kwargs: keywrod arguments to pass to the function
    :return: None
    """
⋮----
# Get positional arguments from args
arg_values = {c.name: a for a, c in zip(args, cmd.params, strict=False)}
args_needed = {c.name: c for c in cmd.params if c.name not in arg_values}
⋮----
# build and check opts list from kwargs
opts = {a.name: a for a in cmd.params if isinstance(a, click.Option)}
⋮----
# check positional arguments list
⋮----
# build parameter lists
opts_list = sum([[o.opts[0], str(arg_values[n])] for n, o in opts.items()], [])
args_list = [str(v) for n, v in arg_values.items() if n not in opts]
⋮----
# call the command
⋮----
def call_click_command_with_ctx(cmd, ctx, *args, **kwargs)
⋮----
"""Wrapper to call a click command with a Context object

    :param cmd: click cli command function to call
    :param ctx: click context
    :param args: arguments to pass to the function
    :param kwargs: keyword arguments to pass to the function
    :return: None
    """
⋮----
# monkey patch make_context
def make_context(*some_args, **some_kwargs):  # noqa: ARG001 ARG002
⋮----
child_ctx = click.Context(cmd, parent=ctx)
⋮----
prev_make_context = cmd.make_context
⋮----
# restore make_context
````

## File: src/metagit/core/utils/common.py
````python
"""
common functions
"""
⋮----
import yaml  # Use standard PyYAML for dumping
⋮----
__all__ = [
⋮----
def create_vscode_workspace(_: str, repo_paths: List[str]) -> Union[str, Exception]
⋮----
"""
    Create VS Code workspace file content.

    Args:
        project_name: The name of the project
        repo_paths: List of repository paths to include in the workspace

    Returns:
        JSON string representing the VS Code workspace file content
    """
⋮----
workspace_data = {
⋮----
# Add each repository as a folder in the workspace
⋮----
# Convert to JSON string
⋮----
def open_editor(editor: str, path: str) -> Union[None, Exception]
⋮----
"""
    Open a path in the specified editor in an OS-agnostic way.

    Args:
        editor: The editor command to use (e.g., 'code', 'vim', 'nano')
        path: The path to open in the editor

    Returns:
        None on success, Exception on failure
    """
⋮----
# Ensure the path exists
⋮----
# Use subprocess to open the editor
# This works cross-platform as subprocess handles the differences
result = subprocess.run([editor, path], capture_output=True, text=True)
⋮----
new_key = parent_key + sep + k if parent_key else k
⋮----
def regex_replace(s: str, find: str, replace: str) -> Union[str, Exception]
⋮----
"""A non-optimal implementation of a regex filter for use in our Jinja2 template processing"""
⋮----
def env_override(value: str, key: str) -> Union[str, None, Exception]
⋮----
"""Can be used to pull env vars into templates"""
⋮----
def to_yaml(value: Any) -> Union[str, Any, Exception]
⋮----
"""convert dicts to yaml"""
⋮----
"""Pretty up output in Jinja template"""
⋮----
pretty_result = pretty(value, indent + 2, result + "\n")
⋮----
result = pretty_result
⋮----
def merge_dicts(a: Dict, b: Dict, path: List = None) -> Union[Dict, Exception]
⋮----
""" "merges b into a"""
⋮----
path = []
⋮----
merge_result = merge_dicts(a[key], b[key], path + [str(key)])
⋮----
pass  # same leaf value
⋮----
def parse_checksum_file(file_path: str) -> Union[Dict[str, str], Exception]
⋮----
checksums = {}
⋮----
differences = []
⋮----
base_filename = filepath.split("/")[-1]
⋮----
checksum2 = checksums2[filepath]
⋮----
def normalize_git_url(url: Optional[str]) -> Optional[str]
⋮----
"""
    Normalize a git URL by removing trailing forward slashes.

    Args:
        url: Git URL to normalize

    Returns:
        Normalized URL without trailing forward slash, or None if input is None
    """
⋮----
# Convert to string if it's an HttpUrl or other object
url_str = str(url).strip()
⋮----
# Remove trailing forward slash
⋮----
url_str = url_str.rstrip("/")
⋮----
def get_project_root() -> Path
⋮----
"""Get the project root directory."""
⋮----
def ensure_directory(path: Union[str, Path]) -> Path
⋮----
"""Ensure a directory exists, creating it if necessary."""
path_obj = Path(path)
⋮----
def safe_get(dictionary: Dict[str, Any], key: str, default: Any = None) -> Any
⋮----
"""Safely get a value from a dictionary."""
⋮----
def flatten_list(nested_list: List[Any]) -> List[Any]
⋮----
"""Flatten a nested list."""
flattened = []
⋮----
def is_git_repository(path: Union[str, Path]) -> bool
⋮----
"""Check if a path is a git repository."""
⋮----
"""Get the relative path from base_path to target_path."""
base = Path(base_path).resolve()
target = Path(target_path).resolve()
⋮----
def sanitize_filename(filename: str) -> str
⋮----
"""Sanitize a filename by removing or replacing invalid characters."""
# Remove or replace invalid characters
sanitized = re.sub(r'[<>:"/\\|?*]', "_", filename)
# Remove leading/trailing spaces and dots
sanitized = sanitized.strip(" .")
# Ensure it's not empty
⋮----
sanitized = "unnamed"
⋮----
def format_bytes(bytes_value: int) -> str
⋮----
"""Format bytes into a human-readable string."""
⋮----
def parse_env_list(env_value: Optional[str], separator: str = ",") -> List[str]
⋮----
"""Parse a comma-separated environment variable into a list."""
⋮----
def filter_none_values(data: Dict[str, Any]) -> Dict[str, Any]
⋮----
"""Remove None values from a dictionary."""
````

## File: src/metagit/core/workspace/__init__.py
````python

````

## File: src/metagit/core/workspace/manager.py
````python
#!/usr/bin/env python
"""
Class for managing workspaces.

This package provides a class for managing workspaces.
"""
⋮----
def get_workspace_path(config: AppConfig) -> Union[str, Exception]
⋮----
"""
    Get the workspace path from the config.
    """
⋮----
def get_synced_projects(config: AppConfig) -> Union[List[WorkspaceProject], Exception]
⋮----
"""
    Get the synced projects from the config.
    """
⋮----
class WorkspaceManager
⋮----
"""
    Manager class for handling workspaces.

    This class provides methods for loading, validating, and creating
    workspaces with proper error handling and validation.
    """
⋮----
def __init__(self, workspace_path: str) -> None
⋮----
"""
        Initialize the MetagitWorkspaceManager.

        Args:
            workspace_path: Path to the workspace.
        """
````

## File: src/metagit/data/prompts/gemini_prompt_filled.md
````markdown
You are an AI agent specializing in project analysis and configuration. Your task is to generate a new YAML configuration file, `./.metagit.new.yaml`, by analyzing the project located at `./`.

The structure, fields, and data for this new YAML file must be determined by following the JSON schema located at `./.metagit/metagit_config.schema.json`.

**Your process must be as follows:**

1.  **Read and Understand the Schema:** First, read the JSON schema file at `./.metagit/metagit_config.schema.json`.
2.  **Read Existing YAML manifest:** If an existing YAML file exists at `./.metagit.yml` it should be used as the basis for your efforts. The `workspace` attribute in this file **must** be preserved in your generated output file later on.
3.  **Read Existing Project File Data:** Read in the contents of `./.metagit/local_summary.yml` and use it to determine which subfolders to target in your analysis efforts. You **must** only traverse and analyse folders in the paths defined in this folder.
3.  **Follow Schema Descriptions:** For each property in the schema, you **must** use its corresponding `description` field as additional instruction for to help best determine the values for each property.
4.  **Analyze the Project:** Systematically search and read the files within the project directory (`.`) to gather the information needed to populate the YAML fields, as guided by the schemas descriptions. Skip all folders and files if they match any of the patterns defined in `./.gitignore` if the file exists.
5.  **Generate the YAML File:** Construct the `./.metagit.new.yaml` file. The file must be valid YAML and strictly conform to the schema.
6.  **Handle Missing Information:**
    *   If you cannot determine a value for a **required** field after a thorough analysis, state this clearly.
    *   For **optional** fields, omit them if the relevant information cannot be found.

Your final output should be the complete content for the new `./.metagit.new.yaml` file.
````

## File: src/metagit/data/prompts/gemini_prompt.md
````markdown
You are an AI agent specializing in project analysis and configuration. Your task is to generate a new YAML configuration file, `{{project_path}}/.metagit.new.yaml`, by analyzing the project located at `{{project_path}}/`.

The structure, fields, and data for this new YAML file must be determined by following the JSON schema located at `{{project_path}}/.metagit/metagit_config.schema.json`.

**Your process must be as follows:**

1.  **Read and Understand the Schema:** First, read the JSON schema file at `{{project_path}}/.metagit/metagit_config.schema.json`.
2.  **Read Existing YAML manifest:** If an existing YAML file exists at `{{project_path}}/.metagit.yml` it should be used as the basis for your efforts. The `workspace` attribute in this file **must** be preserved in your generated output file later on.
3.  **Follow Schema Descriptions:** For each property in the schema, you **must** use its corresponding `description` field as a direct instruction for how to find or infer the correct value from the project's files and structure.
4.  **Analyze the Project:** Systematically search and read the files within the project directory (`{{project_path}}`) to gather the information needed to populate the YAML fields, as guided by the schema's descriptions. Skip all folders and files if they match any of the patterns defined in `{{project_path}}/.gitignore` if the file exists.
5.  **Generate the YAML File:** Construct the `{{project_path}}/.metagit.new.yaml` file. The file must be valid YAML and strictly conform to the schema.
6.  **Handle Missing Information:**
    *   If you cannot determine a value for a **required** field after a thorough analysis, state this clearly.
    *   For **optional** fields, omit them if the relevant information cannot be found.

Your final output should be the complete content for the new `{{project_path}}/.metagit.new.yaml` file.
````

## File: src/metagit/data/build-files.yaml
````yaml
build_files:
  general:
    - Makefile
    - CMakeLists.txt
    - build.gradle
    - build.gradle.kts
    - setup.py
    - setup.cfg
    - requirements.txt
    - Taskfile.yaml
    - tasks.json
    - BUCK
  javascript:
    - package.json
    - webpack.config.js
    - vite.config.js
    - rollup.config.js
    - gulpfile.js
    - Gruntfile.js
  important:
    - build.xml
    - package.json
    - pom.xml
    - build.gradle
    - build.sbt
    - .github/workflows/*.yml
    - .gitlab-ci.yml
    - .travis.yml
    - Jenkinsfile
    - circleci/config.yml
    - bitrise.yml
    - appveyor.yml
    - circle.yml
    - .circleci/config.yml
    - codeship-steps.yml
    - wercker.yml
    - docker-compose.yml
    - docker-cloud.yml
    - docker-stack.yml
    - azure-pipelines.yml
    - bitbucket-pipelines.yml
    - Dockerfile
    - docker-compose.yml
    - skaffold.yaml
    - pyproject.toml
    - requirements.txt
    - setup.py
    - setup.cfg
    - '*.tf'
    - Pulumi.yaml
  testing:
    - Jestfile.js
    - jest.config.js
    - karma.conf.js
    - tox.ini
    - .eslintrc.js
    - .eslintrc.json
    - .prettierrc
  containerization:
    - Dockerfile
    - docker-compose.yml
    - skaffold.yaml
  infrastructure:
    - '*.tf'
    - Pulumi.yaml
  python:
    - pyproject.toml
    - requirements.txt
    - setup.py
    - setup.cfg
    - tox.ini
````

## File: src/metagit/data/cd-files.json
````json
{
  "**/*.tf": "terraform",
  "skaffold.yaml": "skaffold",
  "Pulumi.yaml": "pulumi"
}
````

## File: src/metagit/data/ci-files.json
````json
{
  "appveyor.yml": "Appveyor",
  "circle.yml": "CircleCI",
  ".circleci/config.yml": "CircleCI",
  "codeship-steps.yml": "Codeship",
  "wercker.yml": "Wercker",
  "docker-compose.yml": "Docker",
  "docker-cloud.yml": "Docker Cloud",
  "docker-stack.yml": "Docker Stack",
  ".github/workflows/*.yml": "GitHub Actions",
  ".gitlab-ci.yml": "GitLab CI",
  "Jenkinsfile": "Jenkins",
  ".travis.yml": "Travis CI",
  "azure-pipelines.yml": "Azure DevOps",
  "bitbucket-pipelines.yml": "Bitbucket Pipelines"
}
````

## File: src/metagit/data/file-types.json
````json
{
  "extensions": [
    {
      "kind": "ABAP",
      "type": "programming",
      "extensions": [".abap"]
    },
    {
      "kind": "AGS Script",
      "type": "programming",
      "extensions": [".asc", ".ash"]
    },
    {
      "kind": "AMPL",
      "type": "programming",
      "extensions": [".ampl", ".mod"]
    },
    {
      "kind": "ANTLR",
      "type": "programming",
      "extensions": [".g4"]
    },
    {
      "kind": "API Blueprint",
      "type": "markup",
      "extensions": [".apib"]
    },
    {
      "kind": "APL",
      "type": "programming",
      "extensions": [".apl", ".dyalog"]
    },
    {
      "kind": "ASP",
      "type": "programming",
      "extensions": [
        ".asp",
        ".asax",
        ".ascx",
        ".ashx",
        ".asmx",
        ".aspx",
        ".axd"
      ]
    },
    {
      "kind": "ATS",
      "type": "programming",
      "extensions": [".dats", ".hats", ".sats"]
    },
    {
      "kind": "ActionScript",
      "type": "programming",
      "extensions": [".as"]
    },
    {
      "kind": "Ada",
      "type": "programming",
      "extensions": [".adb", ".ada", ".ads"]
    },
    {
      "kind": "Agda",
      "type": "programming",
      "extensions": [".agda"]
    },
    {
      "kind": "Alloy",
      "type": "programming",
      "extensions": [".als"]
    },
    {
      "kind": "Ant Build System",
      "type": "data"
    },
    {
      "kind": "ApacheConf",
      "type": "markup",
      "extensions": [".apacheconf", ".vhost"]
    },
    {
      "kind": "Apex",
      "type": "programming",
      "extensions": [".cls"]
    },
    {
      "kind": "AppleScript",
      "type": "programming",
      "extensions": [".applescript", ".scpt"]
    },
    {
      "kind": "Arc",
      "type": "programming",
      "extensions": [".arc"]
    },
    {
      "kind": "Arduino",
      "type": "programming",
      "extensions": [".ino"]
    },
    {
      "kind": "AsciiDoc",
      "type": "prose",
      "extensions": [".asciidoc", ".adoc", ".asc"]
    },
    {
      "kind": "AspectJ",
      "type": "programming",
      "extensions": [".aj"]
    },
    {
      "kind": "Assembly",
      "type": "programming",
      "extensions": [".asm", ".a51", ".inc", ".nasm"]
    },
    {
      "kind": "Augeas",
      "type": "programming",
      "extensions": [".aug"]
    },
    {
      "kind": "AutoHotkey",
      "type": "programming",
      "extensions": [".ahk", ".ahkl"]
    },
    {
      "kind": "AutoIt",
      "type": "programming",
      "extensions": [".au3"]
    },
    {
      "kind": "Awk",
      "type": "programming",
      "extensions": [".awk", ".auk", ".gawk", ".mawk", ".nawk"]
    },
    {
      "kind": "Batchfile",
      "type": "programming",
      "extensions": [".bat", ".cmd"]
    },
    {
      "kind": "Befunge",
      "type": "programming",
      "extensions": [".befunge"]
    },
    {
      "kind": "Bison",
      "type": "programming",
      "extensions": [".bison"]
    },
    {
      "kind": "BitBake",
      "type": "programming",
      "extensions": [".bb"]
    },
    {
      "kind": "BlitzBasic",
      "type": "programming",
      "extensions": [".bb", ".decls"]
    },
    {
      "kind": "BlitzMax",
      "type": "programming",
      "extensions": [".bmx"]
    },
    {
      "kind": "Bluespec",
      "type": "programming",
      "extensions": [".bsv"]
    },
    {
      "kind": "Boo",
      "type": "programming",
      "extensions": [".boo"]
    },
    {
      "kind": "Brainfuck",
      "type": "programming",
      "extensions": [".b", ".bf"]
    },
    {
      "kind": "Brightscript",
      "type": "programming",
      "extensions": [".brs"]
    },
    {
      "kind": "Bro",
      "type": "programming",
      "extensions": [".bro"]
    },
    {
      "kind": "C",
      "type": "programming",
      "extensions": [".c", ".cats", ".h", ".idc", ".w"]
    },
    {
      "kind": "C#",
      "type": "programming",
      "extensions": [".cs", ".cake", ".cshtml", ".csx"]
    },
    {
      "kind": "C++",
      "type": "programming",
      "extensions": [
        ".cpp",
        ".c++",
        ".cc",
        ".cp",
        ".cxx",
        ".h",
        ".h++",
        ".hh",
        ".hpp",
        ".hxx",
        ".inc",
        ".inl",
        ".ipp",
        ".tcc",
        ".tpp"
      ]
    },
    {
      "kind": "C-ObjDump",
      "type": "data",
      "extensions": [".c-objdump"]
    },
    {
      "kind": "C2hs Haskell",
      "type": "programming",
      "extensions": [".chs"]
    },
    {
      "kind": "CLIPS",
      "type": "programming",
      "extensions": [".clp"]
    },
    {
      "kind": "CMake",
      "type": "programming",
      "extensions": [".cmake", ".cmake.in"]
    },
    {
      "kind": "COBOL",
      "type": "programming",
      "extensions": [".cob", ".cbl", ".ccp", ".cobol", ".cpy"]
    },
    {
      "kind": "CSS",
      "type": "markup",
      "extensions": [".css"]
    },
    {
      "kind": "CSV",
      "type": "data",
      "extensions": [".csv"]
    },
    {
      "kind": "Cap'n Proto",
      "type": "programming",
      "extensions": [".capnp"]
    },
    {
      "kind": "CartoCSS",
      "type": "programming",
      "extensions": [".mss"]
    },
    {
      "kind": "Ceylon",
      "type": "programming",
      "extensions": [".ceylon"]
    },
    {
      "kind": "Chapel",
      "type": "programming",
      "extensions": [".chpl"]
    },
    {
      "kind": "Charity",
      "type": "programming",
      "extensions": [".ch"]
    },
    {
      "kind": "ChucK",
      "type": "programming",
      "extensions": [".ck"]
    },
    {
      "kind": "Cirru",
      "type": "programming",
      "extensions": [".cirru"]
    },
    {
      "kind": "Clarion",
      "type": "programming",
      "extensions": [".clw"]
    },
    {
      "kind": "Clean",
      "type": "programming",
      "extensions": [".icl", ".dcl"]
    },
    {
      "kind": "Click",
      "type": "programming",
      "extensions": [".click"]
    },
    {
      "kind": "Clojure",
      "type": "programming",
      "extensions": [
        ".clj",
        ".boot",
        ".cl2",
        ".cljc",
        ".cljs",
        ".cljs.hl",
        ".cljscm",
        ".cljx",
        ".hic"
      ]
    },
    {
      "kind": "CoffeeScript",
      "type": "programming",
      "extensions": [".coffee", "._coffee", ".cake", ".cjsx", ".cson", ".iced"]
    },
    {
      "kind": "ColdFusion",
      "type": "programming",
      "extensions": [".cfm", ".cfml"]
    },
    {
      "kind": "ColdFusion CFC",
      "type": "programming",
      "extensions": [".cfc"]
    },
    {
      "kind": "Common Lisp",
      "type": "programming",
      "extensions": [
        ".lisp",
        ".asd",
        ".cl",
        ".l",
        ".lsp",
        ".ny",
        ".podsl",
        ".sexp"
      ]
    },
    {
      "kind": "Component Pascal",
      "type": "programming",
      "extensions": [".cp", ".cps"]
    },
    {
      "kind": "Cool",
      "type": "programming",
      "extensions": [".cl"]
    },
    {
      "kind": "Coq",
      "type": "programming",
      "extensions": [".coq", ".v"]
    },
    {
      "kind": "Cpp-ObjDump",
      "type": "data",
      "extensions": [
        ".cppobjdump",
        ".c++-objdump",
        ".c++objdump",
        ".cpp-objdump",
        ".cxx-objdump"
      ]
    },
    {
      "kind": "Creole",
      "type": "prose",
      "extensions": [".creole"]
    },
    {
      "kind": "Crystal",
      "type": "programming",
      "extensions": [".cr"]
    },
    {
      "kind": "Cucumber",
      "type": "programming",
      "extensions": [".feature"]
    },
    {
      "kind": "Cuda",
      "type": "programming",
      "extensions": [".cu", ".cuh"]
    },
    {
      "kind": "Cycript",
      "type": "programming",
      "extensions": [".cy"]
    },
    {
      "kind": "Cython",
      "type": "programming",
      "extensions": [".pyx", ".pxd", ".pxi"]
    },
    {
      "kind": "D",
      "type": "programming",
      "extensions": [".d", ".di"]
    },
    {
      "kind": "D-ObjDump",
      "type": "data",
      "extensions": [".d-objdump"]
    },
    {
      "kind": "DIGITAL Command Language",
      "type": "programming",
      "extensions": [".com"]
    },
    {
      "kind": "DM",
      "type": "programming",
      "extensions": [".dm"]
    },
    {
      "kind": "DNS Zone",
      "type": "data",
      "extensions": [".zone", ".arpa"]
    },
    {
      "kind": "DTrace",
      "type": "programming",
      "extensions": [".d"]
    },
    {
      "kind": "Darcs Patch",
      "type": "data",
      "extensions": [".darcspatch", ".dpatch"]
    },
    {
      "kind": "Dart",
      "type": "programming",
      "extensions": [".dart"]
    },
    {
      "kind": "Diff",
      "type": "data",
      "extensions": [".diff", ".patch"]
    },
    {
      "kind": "Dockerfile",
      "type": "data",
      "extensions": [".dockerfile", ".dockerignore"]
    },
    {
      "kind": "Docker",
      "type": "programming",
      "extensions": ["Dockerfile", "docker-compose.yml", "docker-compose.yaml", "docker-compose.*.yml", "docker-compose.*.yaml"]
    },
    {
      "kind": "Dogescript",
      "type": "programming",
      "extensions": [".djs"]
    },
    {
      "kind": "Dylan",
      "type": "programming",
      "extensions": [".dylan", ".dyl", ".intr", ".lid"]
    },
    {
      "kind": "E",
      "type": "programming",
      "extensions": [".E"]
    },
    {
      "kind": "ECL",
      "type": "programming",
      "extensions": [".ecl", ".eclxml"]
    },
    {
      "kind": "ECLiPSe",
      "type": "programming",
      "extensions": [".ecl"]
    },
    {
      "kind": "Eagle",
      "type": "markup",
      "extensions": [".sch", ".brd"]
    },
    {
      "kind": "Ecere Projects",
      "type": "data",
      "extensions": [".epj"]
    },
    {
      "kind": "Eiffel",
      "type": "programming",
      "extensions": [".e"]
    },
    {
      "kind": "Elixir",
      "type": "programming",
      "extensions": [".ex", ".exs"]
    },
    {
      "kind": "Elm",
      "type": "programming",
      "extensions": [".elm"]
    },
    {
      "kind": "Emacs Lisp",
      "type": "programming",
      "extensions": [".el", ".emacs", ".emacs.desktop"]
    },
    {
      "kind": "EmberScript",
      "type": "programming",
      "extensions": [".em", ".emberscript"]
    },
    {
      "kind": "Erlang",
      "type": "programming",
      "extensions": [".erl", ".es", ".escript", ".hrl", ".xrl", ".yrl"]
    },
    {
      "kind": "F#",
      "type": "programming",
      "extensions": [".fs", ".fsi", ".fsx"]
    },
    {
      "kind": "FLUX",
      "type": "programming",
      "extensions": [".fx", ".flux"]
    },
    {
      "kind": "FORTRAN",
      "type": "programming",
      "extensions": [
        ".f90",
        ".f",
        ".f03",
        ".f08",
        ".f77",
        ".f95",
        ".for",
        ".fpp"
      ]
    },
    {
      "kind": "Factor",
      "type": "programming",
      "extensions": [".factor"]
    },
    {
      "kind": "Fancy",
      "type": "programming",
      "extensions": [".fy", ".fancypack"]
    },
    {
      "kind": "Fantom",
      "type": "programming",
      "extensions": [".fan"]
    },
    {
      "kind": "Filterscript",
      "type": "programming",
      "extensions": [".fs"]
    },
    {
      "kind": "Formatted",
      "type": "data",
      "extensions": [".for", ".eam.fs"]
    },
    {
      "kind": "Forth",
      "type": "programming",
      "extensions": [
        ".fth",
        ".4th",
        ".f",
        ".for",
        ".forth",
        ".fr",
        ".frt",
        ".fs"
      ]
    },
    {
      "kind": "FreeMarker",
      "type": "programming",
      "extensions": [".ftl"]
    },
    {
      "kind": "Frege",
      "type": "programming",
      "extensions": [".fr"]
    },
    {
      "kind": "G-code",
      "type": "data",
      "extensions": [".g", ".gco", ".gcode"]
    },
    {
      "kind": "GAMS",
      "type": "programming",
      "extensions": [".gms"]
    },
    {
      "kind": "GAP",
      "type": "programming",
      "extensions": [".g", ".gap", ".gd", ".gi", ".tst"]
    },
    {
      "kind": "GAS",
      "type": "programming",
      "extensions": [".s", ".ms"]
    },
    {
      "kind": "GDScript",
      "type": "programming",
      "extensions": [".gd"]
    },
    {
      "kind": "GLSL",
      "type": "programming",
      "extensions": [
        ".glsl",
        ".fp",
        ".frag",
        ".frg",
        ".fs",
        ".fsh",
        ".fshader",
        ".geo",
        ".geom",
        ".glslv",
        ".gshader",
        ".shader",
        ".vert",
        ".vrx",
        ".vsh",
        ".vshader"
      ]
    },
    {
      "kind": "Game Maker Language",
      "type": "programming",
      "extensions": [".gml"]
    },
    {
      "kind": "Genshi",
      "type": "programming",
      "extensions": [".kid"]
    },
    {
      "kind": "Gentoo Ebuild",
      "type": "programming",
      "extensions": [".ebuild"]
    },
    {
      "kind": "Gentoo Eclass",
      "type": "programming",
      "extensions": [".eclass"]
    },
    {
      "kind": "Gettext Catalog",
      "type": "prose",
      "extensions": [".po", ".pot"]
    },
    {
      "kind": "Glyph",
      "type": "programming",
      "extensions": [".glf"]
    },
    {
      "kind": "Gnuplot",
      "type": "programming",
      "extensions": [".gp", ".gnu", ".gnuplot", ".plot", ".plt"]
    },
    {
      "kind": "Go",
      "type": "programming",
      "extensions": [".go"]
    },
    {
      "kind": "Golo",
      "type": "programming",
      "extensions": [".golo"]
    },
    {
      "kind": "Gosu",
      "type": "programming",
      "extensions": [".gs", ".gst", ".gsx", ".vark"]
    },
    {
      "kind": "Grace",
      "type": "programming",
      "extensions": [".grace"]
    },
    {
      "kind": "Gradle",
      "type": "data",
      "extensions": [".gradle"]
    },
    {
      "kind": "Grammatical Framework",
      "type": "programming",
      "extensions": [".gf"]
    },
    {
      "kind": "Graph Modeling Language",
      "type": "data",
      "extensions": [".gml"]
    },
    {
      "kind": "GraphQL",
      "type": "data",
      "extensions": [".graphql"]
    },
    {
      "kind": "Graphviz (DOT)",
      "type": "data",
      "extensions": [".dot", ".gv"]
    },
    {
      "kind": "Groff",
      "type": "markup",
      "extensions": [
        ".man",
        ".1",
        ".1in",
        ".1m",
        ".1x",
        ".2",
        ".3",
        ".3in",
        ".3m",
        ".3qt",
        ".3x",
        ".4",
        ".5",
        ".6",
        ".7",
        ".8",
        ".9",
        ".l",
        ".me",
        ".ms",
        ".n",
        ".rno",
        ".roff"
      ]
    },
    {
      "kind": "Groovy",
      "type": "programming",
      "extensions": [".groovy", ".grt", ".gtpl", ".gvy"]
    },
    {
      "kind": "Groovy Server Pages",
      "type": "programming",
      "extensions": [".gsp"]
    },
    {
      "kind": "HCL",
      "type": "programming",
      "extensions": [".hcl", ".tf"]
    },
    {
      "kind": "HLSL",
      "type": "programming",
      "extensions": [".hlsl", ".fx", ".fxh", ".hlsli"]
    },
    {
      "kind": "HTML",
      "type": "markup",
      "extensions": [
        ".html",
        ".htm",
        ".html.hl",
        ".inc",
        ".st",
        ".xht",
        ".xhtml"
      ]
    },
    {
      "kind": "HTML+Django",
      "type": "markup",
      "extensions": [".mustache", ".jinja"]
    },
    {
      "kind": "HTML+EEX",
      "type": "markup",
      "extensions": [".eex"]
    },
    {
      "kind": "HTML+ERB",
      "type": "markup",
      "extensions": [".erb", ".erb.deface"]
    },
    {
      "kind": "HTML+PHP",
      "type": "markup",
      "extensions": [".phtml"]
    },
    {
      "kind": "HTTP",
      "type": "data",
      "extensions": [".http"]
    },
    {
      "kind": "Hack",
      "type": "programming",
      "extensions": [".hh", ".php"]
    },
    {
      "kind": "Haml",
      "type": "markup",
      "extensions": [".haml", ".haml.deface"]
    },
    {
      "kind": "Handlebars",
      "type": "markup",
      "extensions": [".handlebars", ".hbs"]
    },
    {
      "kind": "Harbour",
      "type": "programming",
      "extensions": [".hb"]
    },
    {
      "kind": "Haskell",
      "type": "programming",
      "extensions": [".hs", ".hsc"]
    },
    {
      "kind": "Haxe",
      "type": "programming",
      "extensions": [".hx", ".hxsl"]
    },
    {
      "kind": "Hy",
      "type": "programming",
      "extensions": [".hy"]
    },
    {
      "kind": "HyPhy",
      "type": "programming",
      "extensions": [".bf"]
    },
    {
      "kind": "IDL",
      "type": "programming",
      "extensions": [".pro", ".dlm"]
    },
    {
      "kind": "IGOR Pro",
      "type": "programming",
      "extensions": [".ipf"]
    },
    {
      "kind": "INI",
      "type": "data",
      "extensions": [".ini", ".cfg", ".prefs", ".pro", ".properties"]
    },
    {
      "kind": "IRC log",
      "type": "data",
      "extensions": [".irclog", ".weechatlog"]
    },
    {
      "kind": "Idris",
      "type": "programming",
      "extensions": [".idr", ".lidr"]
    },
    {
      "kind": "Inform 7",
      "type": "programming",
      "extensions": [".ni", ".i7x"]
    },
    {
      "kind": "Inno Setup",
      "type": "programming",
      "extensions": [".iss"]
    },
    {
      "kind": "Io",
      "type": "programming",
      "extensions": [".io"]
    },
    {
      "kind": "Ioke",
      "type": "programming",
      "extensions": [".ik"]
    },
    {
      "kind": "Isabelle",
      "type": "programming",
      "extensions": [".thy"]
    },
    {
      "kind": "Isabelle ROOT",
      "type": "programming"
    },
    {
      "kind": "J",
      "type": "programming",
      "extensions": [".ijs"]
    },
    {
      "kind": "JFlex",
      "type": "programming",
      "extensions": [".flex", ".jflex"]
    },
    {
      "kind": "JSON",
      "type": "data",
      "extensions": [".json", ".geojson", ".lock", ".topojson"]
    },
    {
      "kind": "JSON5",
      "type": "data",
      "extensions": [".json5"]
    },
    {
      "kind": "JSONLD",
      "type": "data",
      "extensions": [".jsonld"]
    },
    {
      "kind": "JSONiq",
      "type": "programming",
      "extensions": [".jq"]
    },
    {
      "kind": "JSX",
      "type": "programming",
      "extensions": [".jsx"]
    },
    {
      "kind": "Jade",
      "type": "markup",
      "extensions": [".jade"]
    },
    {
      "kind": "Jasmin",
      "type": "programming",
      "extensions": [".j"]
    },
    {
      "kind": "Java",
      "type": "programming",
      "extensions": [".java"]
    },
    {
      "kind": "Java Server Pages",
      "type": "programming",
      "extensions": [".jsp"]
    },
    {
      "kind": "JavaScript",
      "type": "programming",
      "extensions": [
        ".js",
        "._js",
        ".bones",
        ".es",
        ".es6",
        ".frag",
        ".gs",
        ".jake",
        ".jsb",
        ".jscad",
        ".jsfl",
        ".jsm",
        ".jss",
        ".njs",
        ".pac",
        ".sjs",
        ".ssjs",
        ".sublime-build",
        ".sublime-commands",
        ".sublime-completions",
        ".sublime-keymap",
        ".sublime-macro",
        ".sublime-menu",
        ".sublime-mousemap",
        ".sublime-project",
        ".sublime-settings",
        ".sublime-theme",
        ".sublime-workspace",
        ".sublime_metrics",
        ".sublime_session",
        ".xsjs",
        ".xsjslib"
      ]
    },
    {
      "kind": "Julia",
      "type": "programming",
      "extensions": [".jl"]
    },
    {
      "kind": "Jupyter Notebook",
      "type": "markup",
      "extensions": [".ipynb"]
    },
    {
      "kind": "KRL",
      "type": "programming",
      "extensions": [".krl"]
    },
    {
      "kind": "KiCad",
      "type": "programming",
      "extensions": [".sch", ".brd", ".kicad_pcb"]
    },
    {
      "kind": "Kit",
      "type": "markup",
      "extensions": [".kit"]
    },
    {
      "kind": "Kotlin",
      "type": "programming",
      "extensions": [".kt", ".ktm", ".kts"]
    },
    {
      "kind": "LFE",
      "type": "programming",
      "extensions": [".lfe"]
    },
    {
      "kind": "LLVM",
      "type": "programming",
      "extensions": [".ll"]
    },
    {
      "kind": "LOLCODE",
      "type": "programming",
      "extensions": [".lol"]
    },
    {
      "kind": "LSL",
      "type": "programming",
      "extensions": [".lsl", ".lslp"]
    },
    {
      "kind": "LabVIEW",
      "type": "programming",
      "extensions": [".lvproj"]
    },
    {
      "kind": "Lasso",
      "type": "programming",
      "extensions": [".lasso", ".las", ".lasso8", ".lasso9", ".ldml"]
    },
    {
      "kind": "Latte",
      "type": "markup",
      "extensions": [".latte"]
    },
    {
      "kind": "Lean",
      "type": "programming",
      "extensions": [".lean", ".hlean"]
    },
    {
      "kind": "Less",
      "type": "markup",
      "extensions": [".less"]
    },
    {
      "kind": "Lex",
      "type": "programming",
      "extensions": [".l", ".lex"]
    },
    {
      "kind": "LilyPond",
      "type": "programming",
      "extensions": [".ly", ".ily"]
    },
    {
      "kind": "Limbo",
      "type": "programming",
      "extensions": [".b", ".m"]
    },
    {
      "kind": "Linker Script",
      "type": "data",
      "extensions": [".ld", ".lds"]
    },
    {
      "kind": "Linux Kernel Module",
      "type": "data",
      "extensions": [".mod"]
    },
    {
      "kind": "Liquid",
      "type": "markup",
      "extensions": [".liquid"]
    },
    {
      "kind": "Literate Agda",
      "type": "programming",
      "extensions": [".lagda"]
    },
    {
      "kind": "Literate CoffeeScript",
      "type": "programming",
      "extensions": [".litcoffee"]
    },
    {
      "kind": "Literate Haskell",
      "type": "programming",
      "extensions": [".lhs"]
    },
    {
      "kind": "LiveScript",
      "type": "programming",
      "extensions": [".ls", "._ls"]
    },
    {
      "kind": "Logos",
      "type": "programming",
      "extensions": [".xm", ".x", ".xi"]
    },
    {
      "kind": "Logtalk",
      "type": "programming",
      "extensions": [".lgt", ".logtalk"]
    },
    {
      "kind": "LookML",
      "type": "programming",
      "extensions": [".lookml"]
    },
    {
      "kind": "LoomScript",
      "type": "programming",
      "extensions": [".ls"]
    },
    {
      "kind": "Lua",
      "type": "programming",
      "extensions": [".lua", ".fcgi", ".nse", ".pd_lua", ".rbxs", ".wlua"]
    },
    {
      "kind": "M",
      "type": "programming",
      "extensions": [".mumps", ".m"]
    },
    {
      "kind": "M4",
      "type": "programming",
      "extensions": [".m4"]
    },
    {
      "kind": "M4Sugar",
      "type": "programming",
      "extensions": [".m4"]
    },
    {
      "kind": "MAXScript",
      "type": "programming",
      "extensions": [".ms", ".mcr"]
    },
    {
      "kind": "MTML",
      "type": "markup",
      "extensions": [".mtml"]
    },
    {
      "kind": "MUF",
      "type": "programming",
      "extensions": [".muf", ".m"]
    },
    {
      "kind": "Makefile",
      "type": "programming",
      "extensions": [".mak", ".d", ".mk", ".mkfile", "Makefile"]
    },
    {
      "kind": "Mako",
      "type": "programming",
      "extensions": [".mako", ".mao"]
    },
    {
      "kind": "Markdown",
      "type": "prose",
      "extensions": [".md", ".markdown", ".mkd", ".mkdn", ".mkdown", ".ron"]
    },
    {
      "kind": "Mask",
      "type": "markup",
      "extensions": [".mask"]
    },
    {
      "kind": "Mathematica",
      "type": "programming",
      "extensions": [
        ".mathematica",
        ".cdf",
        ".m",
        ".ma",
        ".mt",
        ".nb",
        ".nbp",
        ".wl",
        ".wlt"
      ]
    },
    {
      "kind": "Matlab",
      "type": "programming",
      "extensions": [".matlab", ".m"]
    },
    {
      "kind": "Maven POM",
      "type": "data"
    },
    {
      "kind": "Max",
      "type": "programming",
      "extensions": [".maxpat", ".maxhelp", ".maxproj", ".mxt", ".pat"]
    },
    {
      "kind": "MediaWiki",
      "type": "prose",
      "extensions": [".mediawiki", ".wiki"]
    },
    {
      "kind": "Mercury",
      "type": "programming",
      "extensions": [".m", ".moo"]
    },
    {
      "kind": "Metal",
      "type": "programming",
      "extensions": [".metal"]
    },
    {
      "kind": "MiniD",
      "type": "programming",
      "extensions": [".minid"]
    },
    {
      "kind": "Mirah",
      "type": "programming",
      "extensions": [".druby", ".duby", ".mir", ".mirah"]
    },
    {
      "kind": "Modelica",
      "type": "programming",
      "extensions": [".mo"]
    },
    {
      "kind": "Modula-2",
      "type": "programming",
      "extensions": [".mod"]
    },
    {
      "kind": "Module Management System",
      "type": "programming",
      "extensions": [".mms", ".mmk"]
    },
    {
      "kind": "Monkey",
      "type": "programming",
      "extensions": [".monkey"]
    },
    {
      "kind": "Moocode",
      "type": "programming",
      "extensions": [".moo"]
    },
    {
      "kind": "MoonScript",
      "type": "programming",
      "extensions": [".moon"]
    },
    {
      "kind": "Myghty",
      "type": "programming",
      "extensions": [".myt"]
    },
    {
      "kind": "NCL",
      "type": "programming",
      "extensions": [".ncl"]
    },
    {
      "kind": "NL",
      "type": "data",
      "extensions": [".nl"]
    },
    {
      "kind": "NSIS",
      "type": "programming",
      "extensions": [".nsi", ".nsh"]
    },
    {
      "kind": "Nemerle",
      "type": "programming",
      "extensions": [".n"]
    },
    {
      "kind": "NetLinx",
      "type": "programming",
      "extensions": [".axs", ".axi"]
    },
    {
      "kind": "NetLinx+ERB",
      "type": "programming",
      "extensions": [".axs.erb", ".axi.erb"]
    },
    {
      "kind": "NetLogo",
      "type": "programming",
      "extensions": [".nlogo"]
    },
    {
      "kind": "NewLisp",
      "type": "programming",
      "extensions": [".nl", ".lisp", ".lsp"]
    },
    {
      "kind": "Nginx",
      "type": "markup",
      "extensions": [".nginxconf", ".vhost"]
    },
    {
      "kind": "Nimrod",
      "type": "programming",
      "extensions": [".nim", ".nimrod"]
    },
    {
      "kind": "Ninja",
      "type": "data",
      "extensions": [".ninja"]
    },
    {
      "kind": "Nit",
      "type": "programming",
      "extensions": [".nit"]
    },
    {
      "kind": "Nix",
      "type": "programming",
      "extensions": [".nix"]
    },
    {
      "kind": "Nu",
      "type": "programming",
      "extensions": [".nu"]
    },
    {
      "kind": "NumPy",
      "type": "programming",
      "extensions": [".numpy", ".numpyw", ".numsc"]
    },
    {
      "kind": "OCaml",
      "type": "programming",
      "extensions": [".ml", ".eliom", ".eliomi", ".ml4", ".mli", ".mll", ".mly"]
    },
    {
      "kind": "ObjDump",
      "type": "data",
      "extensions": [".objdump"]
    },
    {
      "kind": "Objective-C",
      "type": "programming",
      "extensions": [".m", ".h"]
    },
    {
      "kind": "Objective-C++",
      "type": "programming",
      "extensions": [".mm"]
    },
    {
      "kind": "Objective-J",
      "type": "programming",
      "extensions": [".j", ".sj"]
    },
    {
      "kind": "Omgrofl",
      "type": "programming",
      "extensions": [".omgrofl"]
    },
    {
      "kind": "Opa",
      "type": "programming",
      "extensions": [".opa"]
    },
    {
      "kind": "Opal",
      "type": "programming",
      "extensions": [".opal"]
    },
    {
      "kind": "OpenCL",
      "type": "programming",
      "extensions": [".cl", ".opencl"]
    },
    {
      "kind": "OpenEdge ABL",
      "type": "programming",
      "extensions": [".p", ".cls"]
    },
    {
      "kind": "OpenSCAD",
      "type": "programming",
      "extensions": [".scad"]
    },
    {
      "kind": "Org",
      "type": "prose",
      "extensions": [".org"]
    },
    {
      "kind": "Ox",
      "type": "programming",
      "extensions": [".ox", ".oxh", ".oxo"]
    },
    {
      "kind": "Oxygene",
      "type": "programming",
      "extensions": [".oxygene"]
    },
    {
      "kind": "Oz",
      "type": "programming",
      "extensions": [".oz"]
    },
    {
      "kind": "PAWN",
      "type": "programming",
      "extensions": [".pwn", ".inc"]
    },
    {
      "kind": "PHP",
      "type": "programming",
      "extensions": [
        ".php",
        ".aw",
        ".ctp",
        ".fcgi",
        ".inc",
        ".php3",
        ".php4",
        ".php5",
        ".phps",
        ".phpt"
      ]
    },
    {
      "kind": "PLSQL",
      "type": "programming",
      "extensions": [".pls", ".pck", ".pkb", ".pks", ".plb", ".plsql", ".sql"]
    },
    {
      "kind": "PLpgSQL",
      "type": "programming",
      "extensions": [".sql"]
    },
    {
      "kind": "POV-Ray SDL",
      "type": "programming",
      "extensions": [".pov", ".inc"]
    },
    {
      "kind": "Pan",
      "type": "programming",
      "extensions": [".pan"]
    },
    {
      "kind": "Papyrus",
      "type": "programming",
      "extensions": [".psc"]
    },
    {
      "kind": "Parrot",
      "type": "programming",
      "extensions": [".parrot"]
    },
    {
      "kind": "Parrot Assembly",
      "type": "programming",
      "extensions": [".pasm"]
    },
    {
      "kind": "Parrot Internal Representation",
      "type": "programming",
      "extensions": [".pir"]
    },
    {
      "kind": "Pascal",
      "type": "programming",
      "extensions": [".pas", ".dfm", ".dpr", ".inc", ".lpr", ".pp"]
    },
    {
      "kind": "Perl",
      "type": "programming",
      "extensions": [
        ".pl",
        ".al",
        ".cgi",
        ".fcgi",
        ".perl",
        ".ph",
        ".plx",
        ".pm",
        ".pod",
        ".psgi",
        ".t"
      ]
    },
    {
      "kind": "Perl6",
      "type": "programming",
      "extensions": [
        ".6pl",
        ".6pm",
        ".nqp",
        ".p6",
        ".p6l",
        ".p6m",
        ".pl",
        ".pl6",
        ".pm",
        ".pm6",
        ".t"
      ]
    },
    {
      "kind": "Pickle",
      "type": "data",
      "extensions": [".pkl"]
    },
    {
      "kind": "PicoLisp",
      "type": "programming",
      "extensions": [".l"]
    },
    {
      "kind": "PigLatin",
      "type": "programming",
      "extensions": [".pig"]
    },
    {
      "kind": "Pike",
      "type": "programming",
      "extensions": [".pike", ".pmod"]
    },
    {
      "kind": "Pod",
      "type": "prose",
      "extensions": [".pod"]
    },
    {
      "kind": "PogoScript",
      "type": "programming",
      "extensions": [".pogo"]
    },
    {
      "kind": "Pony",
      "type": "programming",
      "extensions": [".pony"]
    },
    {
      "kind": "PostScript",
      "type": "markup",
      "extensions": [".ps", ".eps"]
    },
    {
      "kind": "PowerShell",
      "type": "programming",
      "extensions": [".ps1", ".psd1", ".psm1"]
    },
    {
      "kind": "Processing",
      "type": "programming",
      "extensions": [".pde"]
    },
    {
      "kind": "Prolog",
      "type": "programming",
      "extensions": [".pl", ".pro", ".prolog", ".yap"]
    },
    {
      "kind": "Propeller Spin",
      "type": "programming",
      "extensions": [".spin"]
    },
    {
      "kind": "Protocol Buffer",
      "type": "markup",
      "extensions": [".proto"]
    },
    {
      "kind": "Public Key",
      "type": "data",
      "extensions": [".asc", ".pub"]
    },
    {
      "kind": "Puppet",
      "type": "programming",
      "extensions": [".pp"]
    },
    {
      "kind": "Pure Data",
      "type": "programming",
      "extensions": [".pd"]
    },
    {
      "kind": "PureBasic",
      "type": "programming",
      "extensions": [".pb", ".pbi"]
    },
    {
      "kind": "PureScript",
      "type": "programming",
      "extensions": [".purs"]
    },
    {
      "kind": "Python",
      "type": "programming",
      "extensions": [
        ".py",
        ".bzl",
        ".cgi",
        ".fcgi",
        ".gyp",
        ".lmi",
        ".pyde",
        ".pyp",
        ".pyt",
        ".pyw",
        ".rpy",
        ".tac",
        ".wsgi",
        ".xpy"
      ]
    },
    {
      "kind": "Python traceback",
      "type": "data",
      "extensions": [".pytb"]
    },
    {
      "kind": "QML",
      "type": "programming",
      "extensions": [".qml", ".qbs"]
    },
    {
      "kind": "QMake",
      "type": "programming",
      "extensions": [".pro", ".pri"]
    },
    {
      "kind": "R",
      "type": "programming",
      "extensions": [".r", ".rd", ".rsx"]
    },
    {
      "kind": "RAML",
      "type": "markup",
      "extensions": [".raml"]
    },
    {
      "kind": "RDoc",
      "type": "prose",
      "extensions": [".rdoc"]
    },
    {
      "kind": "REALbasic",
      "type": "programming",
      "extensions": [
        ".rbbas",
        ".rbfrm",
        ".rbmnu",
        ".rbres",
        ".rbtbar",
        ".rbuistate"
      ]
    },
    {
      "kind": "RHTML",
      "type": "markup",
      "extensions": [".rhtml"]
    },
    {
      "kind": "RMarkdown",
      "type": "prose",
      "extensions": [".rmd"]
    },
    {
      "kind": "Racket",
      "type": "programming",
      "extensions": [".rkt", ".rktd", ".rktl", ".scrbl"]
    },
    {
      "kind": "Ragel in Ruby Host",
      "type": "programming",
      "extensions": [".rl"]
    },
    {
      "kind": "Raw token data",
      "type": "data",
      "extensions": [".raw"]
    },
    {
      "kind": "Rebol",
      "type": "programming",
      "extensions": [".reb", ".r", ".r2", ".r3", ".rebol"]
    },
    {
      "kind": "Red",
      "type": "programming",
      "extensions": [".red", ".reds"]
    },
    {
      "kind": "Redcode",
      "type": "programming",
      "extensions": [".cw"]
    },
    {
      "kind": "Ren'Py",
      "type": "programming",
      "extensions": [".rpy"]
    },
    {
      "kind": "RenderScript",
      "type": "programming",
      "extensions": [".rs", ".rsh"]
    },
    {
      "kind": "RobotFramework",
      "type": "programming",
      "extensions": [".robot"]
    },
    {
      "kind": "Rouge",
      "type": "programming",
      "extensions": [".rg"]
    },
    {
      "kind": "Ruby",
      "type": "programming",
      "extensions": [
        ".rb",
        ".builder",
        ".fcgi",
        ".gemspec",
        ".god",
        ".irbrc",
        ".jbuilder",
        ".mspec",
        ".pluginspec",
        ".podspec",
        ".rabl",
        ".rake",
        ".rbuild",
        ".rbw",
        ".rbx",
        ".ru",
        ".ruby",
        ".thor",
        ".watchr"
      ]
    },
    {
      "kind": "Rust",
      "type": "programming",
      "extensions": [".rs", ".rs.in"]
    },
    {
      "kind": "SAS",
      "type": "programming",
      "extensions": [".sas"]
    },
    {
      "kind": "SCSS",
      "type": "markup",
      "extensions": [".scss"]
    },
    {
      "kind": "SMT",
      "type": "programming",
      "extensions": [".smt2", ".smt"]
    },
    {
      "kind": "SPARQL",
      "type": "data",
      "extensions": [".sparql", ".rq"]
    },
    {
      "kind": "SQF",
      "type": "programming",
      "extensions": [".sqf", ".hqf"]
    },
    {
      "kind": "SQL",
      "type": "data",
      "extensions": [
        ".sql",
        ".cql",
        ".ddl",
        ".inc",
        ".prc",
        ".tab",
        ".udf",
        ".viw"
      ]
    },
    {
      "kind": "SQLPL",
      "type": "programming",
      "extensions": [".sql", ".db2"]
    },
    {
      "kind": "STON",
      "type": "data",
      "extensions": [".ston"]
    },
    {
      "kind": "SVG",
      "type": "data",
      "extensions": [".svg"]
    },
    {
      "kind": "Sage",
      "type": "programming",
      "extensions": [".sage", ".sagews"]
    },
    {
      "kind": "SaltStack",
      "type": "programming",
      "extensions": [".sls"]
    },
    {
      "kind": "Sass",
      "type": "markup",
      "extensions": [".sass"]
    },
    {
      "kind": "Scala",
      "type": "programming",
      "extensions": [".scala", ".sbt", ".sc"]
    },
    {
      "kind": "Scaml",
      "type": "markup",
      "extensions": [".scaml"]
    },
    {
      "kind": "Scheme",
      "type": "programming",
      "extensions": [".scm", ".sld", ".sls", ".sps", ".ss"]
    },
    {
      "kind": "Scilab",
      "type": "programming",
      "extensions": [".sci", ".sce", ".tst"]
    },
    {
      "kind": "Self",
      "type": "programming",
      "extensions": [".self"]
    },
    {
      "kind": "Shell",
      "type": "programming",
      "extensions": [
        ".sh",
        ".bash",
        ".bats",
        ".cgi",
        ".command",
        ".fcgi",
        ".ksh",
        ".sh.in",
        ".tmux",
        ".tool",
        ".zsh"
      ]
    },
    {
      "kind": "ShellSession",
      "type": "programming",
      "extensions": [".sh-session"]
    },
    {
      "kind": "Shen",
      "type": "programming",
      "extensions": [".shen"]
    },
    {
      "kind": "Slash",
      "type": "programming",
      "extensions": [".sl"]
    },
    {
      "kind": "Slim",
      "type": "markup",
      "extensions": [".slim"]
    },
    {
      "kind": "Smali",
      "type": "programming",
      "extensions": [".smali"]
    },
    {
      "kind": "Smalltalk",
      "type": "programming",
      "extensions": [".st", ".cs"]
    },
    {
      "kind": "Smarty",
      "type": "programming",
      "extensions": [".tpl"]
    },
    {
      "kind": "SourcePawn",
      "type": "programming",
      "extensions": [".sp", ".inc", ".sma"]
    },
    {
      "kind": "Squirrel",
      "type": "programming",
      "extensions": [".nut"]
    },
    {
      "kind": "Stan",
      "type": "programming",
      "extensions": [".stan"]
    },
    {
      "kind": "Standard ML",
      "type": "programming",
      "extensions": [".ML", ".fun", ".sig", ".sml"]
    },
    {
      "kind": "Stata",
      "type": "programming",
      "extensions": [
        ".do",
        ".ado",
        ".doh",
        ".ihlp",
        ".mata",
        ".matah",
        ".sthlp"
      ]
    },
    {
      "kind": "Stylus",
      "type": "markup",
      "extensions": [".styl"]
    },
    {
      "kind": "SuperCollider",
      "type": "programming",
      "extensions": [".sc", ".scd"]
    },
    {
      "kind": "Swift",
      "type": "programming",
      "extensions": [".swift"]
    },
    {
      "kind": "SystemVerilog",
      "type": "programming",
      "extensions": [".sv", ".svh", ".vh"]
    },
    {
      "kind": "TOML",
      "type": "data",
      "extensions": [".toml"]
    },
    {
      "kind": "TXL",
      "type": "programming",
      "extensions": [".txl"]
    },
    {
      "kind": "Tcl",
      "type": "programming",
      "extensions": [".tcl", ".adp", ".tm"]
    },
    {
      "kind": "Tcsh",
      "type": "programming",
      "extensions": [".tcsh", ".csh"]
    },
    {
      "kind": "TeX",
      "type": "markup",
      "extensions": [
        ".tex",
        ".aux",
        ".bbx",
        ".bib",
        ".cbx",
        ".cls",
        ".dtx",
        ".ins",
        ".lbx",
        ".ltx",
        ".mkii",
        ".mkiv",
        ".mkvi",
        ".sty",
        ".toc"
      ]
    },
    {
      "kind": "Tea",
      "type": "markup",
      "extensions": [".tea"]
    },
    {
      "kind": "Terra",
      "type": "programming",
      "extensions": [".t"]
    },
    {
      "kind": "Text",
      "type": "prose",
      "extensions": [".txt", ".fr", ".nb", ".ncl", ".no"]
    },
    {
      "kind": "Textile",
      "type": "prose",
      "extensions": [".textile"]
    },
    {
      "kind": "Thrift",
      "type": "programming",
      "extensions": [".thrift"]
    },
    {
      "kind": "Turing",
      "type": "programming",
      "extensions": [".t", ".tu"]
    },
    {
      "kind": "Turtle",
      "type": "data",
      "extensions": [".ttl"]
    },
    {
      "kind": "Twig",
      "type": "markup",
      "extensions": [".twig"]
    },
    {
      "kind": "TypeScript",
      "type": "programming",
      "extensions": [".ts", ".tsx"]
    },
    {
      "kind": "Unified Parallel C",
      "type": "programming",
      "extensions": [".upc"]
    },
    {
      "kind": "Unity3D Asset",
      "type": "data",
      "extensions": [".anim", ".asset", ".mat", ".meta", ".prefab", ".unity"]
    },
    {
      "kind": "Uno",
      "type": "programming",
      "extensions": [".uno"]
    },
    {
      "kind": "UnrealScript",
      "type": "programming",
      "extensions": [".uc"]
    },
    {
      "kind": "UrWeb",
      "type": "programming",
      "extensions": [".ur", ".urs"]
    },
    {
      "kind": "VCL",
      "type": "programming",
      "extensions": [".vcl"]
    },
    {
      "kind": "VHDL",
      "type": "programming",
      "extensions": [
        ".vhdl",
        ".vhd",
        ".vhf",
        ".vhi",
        ".vho",
        ".vhs",
        ".vht",
        ".vhw"
      ]
    },
    {
      "kind": "Vala",
      "type": "programming",
      "extensions": [".vala", ".vapi"]
    },
    {
      "kind": "Verilog",
      "type": "programming",
      "extensions": [".v", ".veo"]
    },
    {
      "kind": "VimL",
      "type": "programming",
      "extensions": [".vim"]
    },
    {
      "kind": "Visual Basic",
      "type": "programming",
      "extensions": [
        ".vb",
        ".bas",
        ".cls",
        ".frm",
        ".frx",
        ".vba",
        ".vbhtml",
        ".vbs"
      ]
    },
    {
      "kind": "Volt",
      "type": "programming",
      "extensions": [".volt"]
    },
    {
      "kind": "Vue",
      "type": "markup",
      "extensions": [".vue"]
    },
    {
      "kind": "Web Ontology Language",
      "type": "markup",
      "extensions": [".owl"]
    },
    {
      "kind": "WebIDL",
      "type": "programming",
      "extensions": [".webidl"]
    },
    {
      "kind": "X10",
      "type": "programming",
      "extensions": [".x10"]
    },
    {
      "kind": "XC",
      "type": "programming",
      "extensions": [".xc"]
    },
    {
      "kind": "XML",
      "type": "data",
      "extensions": [
        ".xml",
        ".ant",
        ".axml",
        ".ccxml",
        ".clixml",
        ".cproject",
        ".csl",
        ".csproj",
        ".ct",
        ".dita",
        ".ditamap",
        ".ditaval",
        ".dll.config",
        ".dotsettings",
        ".filters",
        ".fsproj",
        ".fxml",
        ".glade",
        ".gml",
        ".grxml",
        ".iml",
        ".ivy",
        ".jelly",
        ".jsproj",
        ".kml",
        ".launch",
        ".mdpolicy",
        ".mm",
        ".mod",
        ".mxml",
        ".nproj",
        ".nuspec",
        ".odd",
        ".osm",
        ".plist",
        ".pluginspec",
        ".props",
        ".ps1xml",
        ".psc1",
        ".pt",
        ".rdf",
        ".rss",
        ".scxml",
        ".srdf",
        ".storyboard",
        ".stTheme",
        ".sublime-snippet",
        ".targets",
        ".tmCommand",
        ".tml",
        ".tmLanguage",
        ".tmPreferences",
        ".tmSnippet",
        ".tmTheme",
        ".ts",
        ".tsx",
        ".ui",
        ".urdf",
        ".ux",
        ".vbproj",
        ".vcxproj",
        ".vssettings",
        ".vxml",
        ".wsdl",
        ".wsf",
        ".wxi",
        ".wxl",
        ".wxs",
        ".x3d",
        ".xacro",
        ".xaml",
        ".xib",
        ".xlf",
        ".xliff",
        ".xmi",
        ".xml.dist",
        ".xproj",
        ".xsd",
        ".xul",
        ".zcml"
      ]
    },
    {
      "kind": "XPages",
      "type": "programming",
      "extensions": [".xsp-config", ".xsp.metadata"]
    },
    {
      "kind": "XProc",
      "type": "programming",
      "extensions": [".xpl", ".xproc"]
    },
    {
      "kind": "XQuery",
      "type": "programming",
      "extensions": [".xquery", ".xq", ".xql", ".xqm", ".xqy"]
    },
    {
      "kind": "XS",
      "type": "programming",
      "extensions": [".xs"]
    },
    {
      "kind": "XSLT",
      "type": "programming",
      "extensions": [".xslt", ".xsl"]
    },
    {
      "kind": "Xojo",
      "type": "programming",
      "extensions": [
        ".xojo_code",
        ".xojo_menu",
        ".xojo_report",
        ".xojo_script",
        ".xojo_toolbar",
        ".xojo_window"
      ]
    },
    {
      "kind": "Xtend",
      "type": "programming",
      "extensions": [".xtend"]
    },
    {
      "kind": "YAML",
      "type": "data",
      "extensions": [
        ".yml",
        ".reek",
        ".rviz",
        ".sublime-syntax",
        ".syntax",
        ".yaml",
        ".yaml-tmlanguage"
      ]
    },
    {
      "kind": "YANG",
      "type": "data",
      "extensions": [".yang"]
    },
    {
      "kind": "Yacc",
      "type": "programming",
      "extensions": [".y", ".yacc", ".yy"]
    },
    {
      "kind": "Zephir",
      "type": "programming",
      "extensions": [".zep"]
    },
    {
      "kind": "Zimpl",
      "type": "programming",
      "extensions": [".zimpl", ".zmpl", ".zpl"]
    },
    {
      "kind": "desktop",
      "type": "data",
      "extensions": [".desktop", ".desktop.in"]
    },
    {
      "kind": "eC",
      "type": "programming",
      "extensions": [".ec", ".eh"]
    },
    {
      "kind": "edn",
      "type": "data",
      "extensions": [".edn"]
    },
    {
      "kind": "fish",
      "type": "programming",
      "extensions": [".fish"]
    },
    {
      "kind": "mupad",
      "type": "programming",
      "extensions": [".mu"]
    },
    {
      "kind": "nesC",
      "type": "programming",
      "extensions": [".nc"]
    },
    {
      "kind": "ooc",
      "type": "programming",
      "extensions": [".ooc"]
    },
    {
      "kind": "reStructuredText",
      "type": "prose",
      "extensions": [".rst", ".rest", ".rest.txt", ".rst.txt"]
    },
    {
      "kind": "wisp",
      "type": "programming",
      "extensions": [".wisp"]
    },
    {
      "kind": "xBase",
      "type": "programming",
      "extensions": [".prg", ".ch", ".prw"]
    }
  ]
}
````

## File: src/metagit/data/package-managers.json
````json
{
  "requirements.txt": "Python",
  "requirements.*.txt": "Python",
  "pyproject.toml": "Python",
  "setup.py": "Python",
  "setup.cfg": "Python",
  "Pipfile": "Python",
  "poetry.lock": "Python",
  "poetry.toml": "Python",
  "uv.lock": "Python",
  "go.mod": "Go",
  "go.sum": "Go",
  "Cargo.toml": "Rust",
  "Cargo.lock": "Rust",
  "pom.xml": "Java",
  "build.gradle": "Java",
  "build.sbt": "Scala",
  "package.json": "NodeJS",
  "package-lock.json": "NodeJS",
  "yarn.lock": "NodeJS",
  "composer.json": "PHP",
  "composer.lock": "PHP",
  "Gemfile": "Ruby",
  "Gemfile.lock": "Ruby",
  "mix.exs": "Elixir",
  "shard.yml": "Crystal",
  "dub.json": "D",
  "dub.sdl": "D",
  "stack.yaml": "Haskell",
  "cabal.project": "Haskell",
  "elm.json": "Elm",
  "elm-package.json": "Elm",
  "rebar.config": "Erlang",
  "rebar.lock": "Erlang",
  "rebar3.config": "Erlang",
  "rebar3.lock": "Erlang",
  "pubspec.yaml": "Dart",
  "pubspec.lock": "Dart",
  "nuget.config": "C#",
  "project.json": "C#"
}
````

## File: src/metagit/data/README.md
````markdown
# Metagit Data

Much of this is unused currently. The only item that matters is the `metagit.config.yaml` file which represents the default configuration if none is passed in or found in the default or directory.
````

## File: tasks/Taskfile.api.yml
````yaml
version: "3"
silent: true

tasks:
  show:
    desc: Show python variables for this task
    cmds:
      - |
        echo "GIT_LATEST_TAG: {{.GIT_LATEST_TAG}}"
        echo "VERSION: {{.VERSION}}"

  test:detect:
    desc: Run the api tests
    cmds:
      - |
        curl -X POST "http://localhost:8000/detect" \
          -H "Content-Type: application/json" \
          -d '{
            "repository_url": "https://github.com/zloeber/mag-branch",
            "priority": "normal"
          }'

  dev:
    desc: Run the api in dev mode
    env:
      OPENSEARCH_HOST: localhost
      OPENSEARCH_PORT: 9200
      OPENSEARCH_INDEX: metagit-records
      OPENSEARCH_USE_SSL: false
      OPENSEARCH_VERIFY_CERTS: false
      API_HOST: 0.0.0.0
      API_PORT: 8000
      API_DEBUG: true
      MAX_CONCURRENT_JOBS: 5
    cmds:
      - |
        docker compose -f docker-compose.dev.yml up -d
        uv run -m metagit.api.main
````

## File: tasks/Taskfile.docker.yml
````yaml
# yaml-language-server: $schema=https://taskfile.dev/schema.json
version: "3"
silent: true
vars:
  local_bin_path:
    sh: if [[ "{{.LOCAL_BIN_PATH}}" == "" ]]; then echo "./.local/bin"; else echo "{{.LOCAL_BIN_PATH}}"; fi
  ROVER_VERSION: '{{default "0.2.2" .ROVER_VERSION}}'
  DOCKER_BUILDKIT: 1
  DOCKER_SERVER: '{{default "{{cookiecutter.docker_registry}}" .DOCKER_SERVER}}'
  DOCKER_FILE: '{{default "Dockerfile" .DOCKER_FILE}}'
  DOCKER_PATH: '{{default "." .DOCKER_PATH}}'
  DOCKER_EXTRACT_PATH: '{{default "." .DOCKER_EXTRACT_PATH}}'
  DOCKER_IMAGE: '{{default "." .PROJECT}}'
  docker: docker
  IS_CI: "{{default 0 .IS_CI}}"

tasks:
  show:
    desc: Show terraform variables for this task
    cmds:
      - |
        echo "DOCKER_IMAGE: {{.DOCKER_IMAGE}}"
        echo "DOCKER_FILE: {{.DOCKER_FILE}}"
        echo "DOCKER_PATH: {{.DOCKER_PATH}}"
        echo "local_bin_path: {{.local_bin_path}}"

  login:
    desc: Login to container registry
    cmds:
      - |
        {{.docker}} login {{.DOCKER_SERVER}}

  tag:
    desc: Tag container image
    cmds:
      - |
        {{.docker}} tag {{.DOCKER_IMAGE}}:local {{.DOCKER_SERVER}}/{{.DOCKER_IMAGE}}:{{.GIT_COMMIT}}
        {{.docker}} tag {{.DOCKER_IMAGE}}:local {{.DOCKER_SERVER}}/{{.DOCKER_IMAGE}}:{{.VERSION}}
        {{.docker}} tag {{.DOCKER_IMAGE}}:local {{.DOCKER_SERVER}}/{{.DOCKER_IMAGE}}:latest

  push:
    desc: Push tagged images to registry
    cmds:
      - |
        echo "Pushing container image to registry: latest {{.VERSION}} {{.GIT_COMMIT}}"
        {{.docker}} push {{.DOCKER_SERVER}}/{{.DOCKER_IMAGE}}:{{.GIT_COMMIT}}
        {{.docker}} push {{.DOCKER_SERVER}}/{{.DOCKER_IMAGE}}:{{.VERSION}}
        {{.docker}} push {{.DOCKER_SERVER}}/{{.DOCKER_IMAGE}}:latest

  run:
    desc: Run a local container image for the app
    cmds:
      - |
        {{.docker}} run -t --rm -i --name={{.DOCKER_IMAGE}} {{.DOCKER_IMAGE}}:local {{.CLI_ARGS}}

  scan:
    desc: Run a {{.docker}} snyk security scan
    cmds:
      - |
        {{.docker}} scan {{.DOCKER_SERVER}}/{{.DOCKER_IMAGE}}:latest

  shell:
    desc: Run a local container image for the app
    cmds:
      - |
        {{.docker}} run -t --rm -i --name={{.DOCKER_IMAGE}} {{.DOCKER_IMAGE}}:local /bin/sh

  extract:
    desc: Example of using buildkit to extract files from an image
    cmds:
      - |
        mkdir -p {{.ROOT_DIR}} /.local/artifacts
        {{.docker}} build -f {{.DOCKER_FILE}} \
          --target artifact \
            --output type=local,dest=./.local/artifacts .

  build:
    desc: Build container image
    cmds:
      - |
        {{.docker}} build {{.DOCKER_BUILD_ARGS}} -t {{.DOCKER_IMAGE}}:local -f {{.DOCKER_FILE}} {{.DOCKER_PATH}}

  build:base:
    desc: Build base container image
    cmds:
      - |
        echo "Attempting to build the base image: ./.docker/{{.DOCKER_FILE}}.base"
        {{.docker}} build {{.DOCKER_BUILD_ARGS}} -t {{.DOCKER_IMAGE}}:local-base -f ./.docker/{{.DOCKER_FILE}}.base {{.DOCKER_PATH}}

  build:clean:
    desc: Build container image (no cache)
    cmds:
      - |
        {{.docker}} build --no-cache {{.DOCKER_BUILD_ARGS}} -t {{.DOCKER_IMAGE}}:local -f {{.DOCKER_FILE}} {{.DOCKER_PATH}}

  clean:
    desc: Clean local cached {{.docker}} elements
    cmds:
      - |
        {{.docker}} system prune
````

## File: tasks/Taskfile.git.yml
````yaml
# yaml-language-server: $schema=https://taskfile.dev/schema.json

version: "3"
silent: true
tasks:
  show:
    desc: Show python variables for this task
    cmds:
      - |
        echo "GIT_LATEST_TAG: {{.GIT_LATEST_TAG}}"
        echo "VERSION: {{.VERSION}}"

  lint:
    desc: Lint
    cmds:
      - |
        failedfiles=$(find . -type f \( -iname "*.tf" -o -iname "*.yml" -o -iname "checksum.*" ! -iname ".local" ! -iname "venv*" ! -iname "*inc/Taskfile.git.yml" ! -iname ".git" ! -iname "Makefile" ! -iname "Taskfile.git.yml" \) -exec grep -l "<<<<<<< HEAD" {} \;)
        if [ "$failedfiles" ]; then
          echo "Failed git/lint files: ${failedfiles} "
          exit 1
        fi
        echo "Merge request conflict detrius not found!"

  pre-commit:
    desc: Install pre-commit hooks
    cmds:
      - pre-commit install --install-hooks
      - pre-commit run

  set:upstream:
    desc: Set the current git branch upstream to a branch by the same name on the origin
    cmds:
      - |
        GIT_COMMIT=$(git rev-parse --short=8 HEAD 2>/dev/null || echo local)
        GIT_BRANCH=$(git branch --show-current 2>/dev/null || echo unknown)
        git branch \
          --set-upstream-to=origin/${GIT_BRANCH} ${GIT_BRANCH}

  merge:renovate:
    desc: Attempt to merge all renovate/* branches
    cmds:
      - |
        branch=$(git symbolic-ref --short -q HEAD)
        echo "Current Branch: ${branch}"
        if [ "$branch" == "master" ]; then
          echo "Cannot run this process against master branch!"
          exit
        fi
        # Ensure working directory in version branch clean
        git update-index -q --refresh
        if ! git diff-index --quiet HEAD --; then
          echo "Working directory not clean, Committing current changes."
          git add . && git commit -m 'fix: merge in renovate updates'
        fi

        echo "Attempting to merge in all renovate/* branches found."
        echo "Any merge conflicts will need to be resolved before proceeding."
        for b in $(git branch -r | grep renovate/); do
          git merge $b;
          echo "Merged ${b} succesfully!"
        done

  # init:submodule:
  #   desc: Iitialize all git submodules
  #   cmds:
  #     - |
  #       git submodule init
  #       git submodule update --recursive --init
  #       git submodule foreach 'git checkout master || git checkout main'

  # init:
  #   desc: Initializes git with author information
  #   cmds:
  #     - |
  #       git config user.name "{{.AUTHOR_NAME}}"
  #       git config user.email "{{.AUTHOR_EMAIL}}"

  security:scan:
    desc: Perform security scan on local repo
    cmds:
      - |
        docker run --rm \
          -e "WORKSPACE=${PWD}" \
          -e ENABLE_OSS_RISK=true \
          -v $PWD:/app shiftleft/sast-scan scan \
          --src /app \
          --type credscan,nodejs,python,yaml,terraform,ansible,bash,dockerfile,bash,depscan \
          --out_dir /app/.local/reports
        echo "View results in ./.local/reports"

  gitleaks:
    desc: Perform gitleaks scan
    cmds:
      - |
        docker pull ghcr.io/zricethezav/gitleaks:latest
        docker run -v ${PWD}:/path zricethezav/gitleaks:latest \
          detect -r /path/.local/reports \
          --source="/path"

  ignore:update:
    desc: Adds a line to an existing .gitignore file if not already present
    cmds:
      - |
        if [[ "{{.TO_IGNORE}}" != "" ]]; then
          FILE=.gitignore
          echo "Attempting to add {{.TO_IGNORE}} to $FILE"
          if [ ! -f "$FILE" ]; then
              echo "{{.TO_IGNORE}}" > "$FILE"
          else
              if ! grep -q '{{.TO_IGNORE}}' "$FILE"; then
                echo -e "\n{{.TO_IGNORE}}" >> "$FILE"
              fi
          fi
          sed -i '/^$/d' "$FILE"
        else
          echo "Need to define the TO_IGNORE variable"
        fi
````

## File: tasks/Taskfile.workspace.yml
````yaml
# yaml-language-server: $schema=https://taskfile.dev/schema.json

version: "3"
silent: true
vars:
  workspace_profiles: "{{.ROOT_DIR}}/profiles"
  workspaces: "workspace"
  PROFILE: '{{default "default" .PROFILE}}'
  CONFIG_FILE: "{{.workspace_profiles}}/{{.PROFILE}}.yml"
  PULL_UPDATES: '{{default "false" .PULL_UPDATES}}'

tasks:
  show:
    desc: Show variables for workspace tasks
    cmds:
      - |
        REPO_COUNT=$(yq '.repos | length' {{.CONFIG_FILE}})
        WORKSPACE=${WORKSPACE:-"$(yq '.workspace' {{.CONFIG_FILE}} )"}
        WORKSPACE_PROJECT=$(yq '.project' {{.CONFIG_FILE}})
        TARGET_PATH="{{.ROOT_DIR}}/${WORKSPACE}/${WORKSPACE_PROJECT}"
        echo "ROOT_DIR: {{.ROOT_DIR}}"
        echo "workspace_profiles: {{.workspace_profiles}}"
        echo "CONFIG_FILE: {{.CONFIG_FILE}}"
        echo "WORKSPACE: ${WORKSPACE}"
        echo "PROFILE: {{.PROFILE}}"
        echo "REPO_COUNT: ${REPO_COUNT}"
        echo "TARGET_PATH: ${TARGET_PATH}"

  sync:
    desc: Initialize and/or sync workspace for profile
    cmds:
      - |
        pushd () {
          command pushd "$@" > /dev/null
        }
        popd () {
          command popd "$@" > /dev/null
        }
        WORKSPACE=${WORKSPACE:-"$(yq '.workspace' {{.CONFIG_FILE}} )"}
        WORKSPACE_PROJECT=$(yq '.project' {{.CONFIG_FILE}})
        TARGET_PATH="{{.ROOT_DIR}}/${WORKSPACE}/${WORKSPACE_PROJECT}"
        REPO_COUNT=$(yq '.repos | length' {{.CONFIG_FILE}})
        echo "WORKSPACE: ${WORKSPACE}"
        echo "CONFIG_FILE: {{.CONFIG_FILE}}"
        echo "PROFILE: {{.PROFILE}}"
        echo "REPO_COUNT: ${REPO_COUNT}"
        echo "TARGET_PATH: ${TARGET_PATH}"
        mkdir -p "${TARGET_PATH}" >/dev/null
        echo "---"
        REPO_INDEX=0
        pushd ${TARGET_PATH}
        while [ $REPO_INDEX -lt $REPO_COUNT ]; do
          cd ${TARGET_PATH}
          CURRENT_NAME=$(yq ".repos.[$REPO_INDEX].name" {{.CONFIG_FILE}})
          CURRENT_URL=$(yq ".repos.[$REPO_INDEX].url" {{.CONFIG_FILE}})
          CLONE_BRANCHES=$(yq ".repos.[$REPO_INDEX].branches | .[]" {{.CONFIG_FILE}})
          CLONE_PATH="${TARGET_PATH}/${CURRENT_NAME}"
          echo "Sync Project: ${CURRENT_NAME}"
          mkdir -p ${CLONE_PATH}
          git clone --recursive ${CURRENT_URL} ${CLONE_PATH} || true
          cd ${CLONE_PATH}
          CURRENT_BRANCH=$(git branch --show-current 2>/dev/null || echo "none" )
          if [[ "{{.PULL_UPDATES}}" != "false" ]]; then
              echo "Running branch updates..."
              for BRANCH in $CLONE_BRANCHES; do
                  echo "  - Update branch: $BRANCH"
                  git checkout $BRANCH
                  git pull --autostash 2>/dev/null
                  #git stash 2>/dev/null
                  #git pull
                  #git stash pop
              done
              git checkout {{.ROOT_DIR}} 2>/dev/null
          fi
          let REPO_INDEX=REPO_INDEX+1
          echo ""
        done
        popd
        echo "Workspace Created: ${TARGET_PATH}"
      - task: vscode:create

  vscode:select:
    desc: Use fzf to select a project in a workspace and open with vscode
    silent: false
    cmds:
      - |
        WORKSPACE=${WORKSPACE:-"$(yq '.workspace' {{.CONFIG_FILE}} )"}
        WORKSPACE_PROJECT=$(yq '.project' {{.CONFIG_FILE}})
        TARGET_PATH="${WORKSPACE}/${WORKSPACE_PROJECT}"
        SELECTION=$(find ${TARGET_PATH} -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort | fzf) && \
        [ "$SELECTION" != "./" ] && code "./${TARGET_PATH}/${SELECTION}" || true

  select:
    desc: Use fzf to select a workspace and open with vscode
    cmds:
      - |
        WORKSPACE=${WORKSPACE:-"$(yq '.workspace' {{.CONFIG_FILE}} )"}
        WORKSPACE_PROJECT=$(yq '.project' {{.CONFIG_FILE}})
        TARGET_PATH="${WORKSPACE}"
        SELECTION=$(find ${TARGET_PATH} -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort | fzf) && \
        [ "$SELECTION" != "./" ] && code "./${TARGET_PATH}/${SELECTION}/workspace.code-workspace" || true

  vscode:create:
    desc: Create vscode workspace file relative to this project
    cmds:
      - |
        WORKSPACE=${WORKSPACE:-"$(yq '.workspace' {{.CONFIG_FILE}} )"}
        WORKSPACE_PROJECT=$(yq '.project' {{.CONFIG_FILE}})
        CONFIG_FILE={{.CONFIG_FILE}}
        REPO_PATHS="$(yq '.repos.[].name' {{.CONFIG_FILE}})"
        VSCODEWS=$(cat ./scripts/vscode-workspace.tpl)
        for wspath in $REPO_PATHS
        do
            newpath="./$wspath"
            echo "Adding path: ${newpath}"
            VSCODEWS=$(echo $VSCODEWS | jq --arg newpath "$newpath" '.folders += [{ "path": $newpath }]')
        done
        echo $VSCODEWS | jq > {{.ROOT_DIR}}/${WORKSPACE}/${WORKSPACE_PROJECT}/workspace.code-workspace
        echo "Workspace file for VSCode created: ./${WORKSPACE}/${WORKSPACE_PROJECT}/workspace.code-workspace"
````

## File: tests/conftest.py
````python
@pytest.fixture(scope="session", autouse=True)
def load_env()
⋮----
"""Load environment variables from .env_example for all tests."""
⋮----
@pytest.fixture(scope="session", autouse=True)
def configure_logging()
⋮----
"""Configure logging for tests to prevent file I/O errors."""
# Set environment variable to disable file logging during tests
⋮----
os.environ["METAGIT_LOG_LEVEL"] = "WARNING"  # Reduce log noise during tests
⋮----
# Remove all existing loguru handlers to prevent file I/O issues
⋮----
# Add a simple console handler for tests
⋮----
lambda msg: None,  # Null sink to suppress output during tests
⋮----
@pytest.fixture(autouse=True)
def cleanup_logging()
⋮----
"""Clean up logging after each test to prevent file handle issues."""
⋮----
# Remove any handlers that might have been added during the test
⋮----
@pytest.fixture
def temp_dir()
⋮----
"""Create a temporary directory for test use."""
````

## File: tests/test_appconfig_init.disabled
````
#!/usr/bin/env python
"""
Unit tests for metagit.core.appconfig.__init__
"""

import yaml

from metagit.core.appconfig import get_config, load_config
from metagit.core.appconfig.models import AppConfig


def test_load_config_success(tmp_path):
    config_path = tmp_path / "testconfig.yaml"
    data = {"config": AppConfig().model_dump()}
    with open(config_path, "w") as f:
        yaml.dump(data, f)
    cfg = load_config(str(config_path))
    assert isinstance(cfg, AppConfig)


def test_load_config_file_not_found(tmp_path):
    result = load_config(str(tmp_path / "nope.yaml"))
    assert isinstance(result, Exception)


def test_get_config_dict(capsys):
    cfg = AppConfig()
    result = get_config(cfg, output="dict")
    assert isinstance(result, dict)


def test_get_config_json(capsys):
    cfg = AppConfig()
    get_config(cfg, output="json")
    captured = capsys.readouterr()
    assert "config" in captured.out or "config" in captured.err


def test_get_config_yaml(capsys):
    cfg = AppConfig()
    get_config(cfg, output="yaml")
    captured = capsys.readouterr()
    assert "config" in captured.out or "config" in captured.err
````

## File: tests/test_detect_manager.disabled
````
#!/usr/bin/env python3
"""
Unit tests for DetectionManager and DetectionManagerConfig.
"""

from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from metagit.core.config.models import Language, ProjectDomain, ProjectType
from metagit.core.detect import (
    BranchInfo,
    CIConfigAnalysis,
    DetectionManager,
    DetectionManagerConfig,
    GitBranchAnalysis,
    LanguageDetection,
    ProjectTypeDetection,
)
from metagit.core.record.models import MetagitRecord


@pytest.fixture
def default_path(tmp_path):
    # Use a temporary directory for path
    return str(tmp_path)


def test_config_all_enabled():
    config = DetectionManagerConfig.all_enabled()
    enabled = config.get_enabled_methods()
    assert set(enabled) == {
        "branch_analysis",
        "ci_config_analysis",
        "directory_summary",
        "directory_details",
        "commit_analysis",
        "tag_analysis",
    }


def test_config_minimal():
    config = DetectionManagerConfig.minimal()
    enabled = config.get_enabled_methods()
    assert set(enabled) == {"branch_analysis", "ci_config_analysis"}


def test_config_toggle():
    config = DetectionManagerConfig(
        branch_analysis_enabled=False,
        ci_config_analysis_enabled=True,
        directory_summary_enabled=True,
        directory_details_enabled=False,
    )
    enabled = config.get_enabled_methods()
    assert set(enabled) == {"ci_config_analysis", "directory_summary"}


def test_detection_manager_from_path(default_path):
    """Test creating DetectionManager from path."""
    config = DetectionManagerConfig(
        branch_analysis_enabled=True,
        ci_config_analysis_enabled=True,
        directory_summary_enabled=True,
        directory_details_enabled=True,
    )

    with patch(
        "metagit.core.detect.manager.DetectionManager._load_existing_config",
        return_value=None,
    ):
        manager = DetectionManager.from_path(default_path, config=config)
        assert not isinstance(manager, Exception)
        assert manager.path == default_path
        # Use the actual path name instead of hardcoded "tmp_path"
        expected_name = Path(default_path).name
        assert manager.name == expected_name
        assert manager.detection_config == config


def test_detection_manager_from_path_with_existing_config(default_path):
    """Test creating DetectionManager from path with existing config."""
    existing_config = MetagitRecord(
        name="test-project", description="A test project", kind="application"
    )

    config = DetectionManagerConfig.minimal()

    with patch(
        "metagit.core.detect.manager.DetectionManager._load_existing_config",
        return_value=existing_config,
    ):
        manager = DetectionManager.from_path(default_path, config=config)
        assert not isinstance(manager, Exception)
        assert manager.name == "test-project"
        assert manager.description == "A test project"
        assert manager.kind == "application"


def test_detection_manager_run_all_and_summary(default_path):
    config = DetectionManagerConfig(
        branch_analysis_enabled=True,
        ci_config_analysis_enabled=True,
        directory_summary_enabled=True,
        directory_details_enabled=True,
    )

    with patch(
        "metagit.core.detect.manager.DetectionManager._load_existing_config",
        return_value=None,
    ):
        manager = DetectionManager.from_path(default_path, config=config)
        assert not isinstance(manager, Exception)

        # Create a mock RepositoryAnalysis with all analysis results
        mock_repo_analysis = RepositoryAnalysis(
            path=default_path,
            name="test-project",
            description="A test project",
            url="https://github.com/test/project",
            language_detection=LanguageDetection(primary="Python"),
            project_type_detection=ProjectTypeDetection(
                type=ProjectType.APPLICATION, domain=ProjectDomain.WEB
            ),
            branch_analysis=GitBranchAnalysis(
                strategy_guess="Unknown",
                branches=[BranchInfo(name="main", is_remote=False)],
            ),
            ci_config_analysis=CIConfigAnalysis(detected_tool="TestCI"),
            metadata=None,
            metrics=None,
        )

        # Patch RepositoryAnalysis.from_path to return our mock
        with patch(
            "metagit.core.detect.manager.RepositoryAnalysis.from_path",
            return_value=mock_repo_analysis,
        ):
            result = manager.run_all()
            assert result is None
            assert manager.analysis_completed is True
            assert manager.repository_analysis is not None

            # Patch missing fields for summary
            object.__setattr__(manager, "domain", ProjectDomain.WEB)
            object.__setattr__(manager, "kind", ProjectType.APPLICATION)
            object.__setattr__(manager, "language", Language(primary="Python"))
            object.__setattr__(manager, "language_version", "3.9")

            summary = manager.summary()
            assert isinstance(summary, str)
            assert "Unknown" in summary
            assert "TestCI" in summary


def test_detection_manager_run_specific(default_path):
    config = DetectionManagerConfig(
        branch_analysis_enabled=True,
        ci_config_analysis_enabled=False,
        directory_summary_enabled=False,
        directory_details_enabled=False,
    )

    with patch(
        "metagit.core.detect.manager.DetectionManager._load_existing_config",
        return_value=None,
    ):
        manager = DetectionManager.from_path(default_path, config=config)
        assert not isinstance(manager, Exception)

        # Test that disabled methods return Exception
        result = manager.run_specific("ci_config_analysis")
        assert isinstance(result, Exception)

        # Test that enabled methods delegate to run_all
        with patch.object(manager, "run_all", return_value=None) as mock_run_all:
            result = manager.run_specific("branch_analysis")
            assert result is None
            mock_run_all.assert_called_once()


def test_detection_manager_run_specific_unknown_method(default_path):
    with patch(
        "metagit.core.detect.manager.DetectionManager._load_existing_config",
        return_value=None,
    ):
        manager = DetectionManager.from_path(default_path)
        assert not isinstance(manager, Exception)

        result = manager.run_specific("unknown_method")
        assert isinstance(result, Exception)
        assert "Unknown analysis method" in str(result)


def test_detection_manager_summary_handles_missing(default_path):
    config = DetectionManagerConfig.minimal()

    with patch(
        "metagit.core.detect.manager.DetectionManager._load_existing_config",
        return_value=None,
    ):
        manager = DetectionManager.from_path(default_path, config=config)
        assert not isinstance(manager, Exception)

        # Test summary without running analysis
        summary = manager.summary()
        assert isinstance(summary, str)
        assert "not available" in summary


def test_detection_manager_serialization(default_path):
    config = DetectionManagerConfig.minimal()

    with patch(
        "metagit.core.detect.manager.DetectionManager._load_existing_config",
        return_value=None,
    ):
        manager = DetectionManager.from_path(default_path, config=config)
        assert not isinstance(manager, Exception)

        # Test YAML serialization
        yaml_output = manager.to_yaml()
        assert isinstance(yaml_output, str)
        assert "name:" in yaml_output
        assert "project_path:" in yaml_output

        # Test JSON serialization
        json_output = manager.to_json()
        assert isinstance(json_output, str)
        assert '"name"' in json_output
        assert '"project_path"' in json_output


def test_detection_manager_inherits_from_metagit_record(default_path):
    """Test that DetectionManager properly inherits from MetagitRecord."""
    with patch(
        "metagit.core.detect.manager.DetectionManager._load_existing_config",
        return_value=None,
    ):
        manager = DetectionManager.from_path(default_path)
        assert not isinstance(manager, Exception)

        # Should have MetagitRecord attributes
        assert hasattr(manager, "name")
        assert hasattr(manager, "path")
        assert hasattr(manager, "detection_timestamp")
        assert hasattr(manager, "detection_source")
        assert hasattr(manager, "detection_version")

        # Should also have DetectionManager-specific attributes
        assert hasattr(manager, "detection_config")
        assert hasattr(manager, "repository_analysis")
        assert hasattr(manager, "analysis_completed")
        assert hasattr(manager, "project_path")


def test_detection_manager_cleanup(default_path):
    """Test that DetectionManager cleanup delegates to RepositoryAnalysis."""
    with patch(
        "metagit.core.detect.manager.DetectionManager._load_existing_config",
        return_value=None,
    ):
        manager = DetectionManager.from_path(default_path)
        assert not isinstance(manager, Exception)

        # Create a mock RepositoryAnalysis
        mock_repo_analysis = MagicMock()
        manager.repository_analysis = mock_repo_analysis

        # Test cleanup
        manager.cleanup()
        mock_repo_analysis.cleanup.assert_called_once()


def test_detection_manager_update_metagit_record(default_path):
    """Test that DetectionManager properly updates MetagitRecord fields from RepositoryAnalysis."""
    with patch(
        "metagit.core.detect.manager.DetectionManager._load_existing_config",
        return_value=None,
    ):
        manager = DetectionManager.from_path(default_path)
        assert not isinstance(manager, Exception)

        # Create a mock RepositoryAnalysis with data
        mock_repo_analysis = RepositoryAnalysis(
            path=default_path,
            name="test-project",
            description="A test project",
            url="https://github.com/test/project",
            language_detection=LanguageDetection(primary="Python"),
            project_type_detection=ProjectTypeDetection(
                type=ProjectType.APPLICATION, domain=ProjectDomain.WEB
            ),
            branch_analysis=GitBranchAnalysis(
                strategy_guess="GitHub Flow",
                branches=[BranchInfo(name="main", is_remote=False)],
            ),
            ci_config_analysis=CIConfigAnalysis(detected_tool="GitHub Actions"),
        )

        manager.repository_analysis = mock_repo_analysis

        # Test _update_metagit_record
        manager._update_metagit_record()

        # Check that MetagitRecord fields were updated
        assert manager.name == "test-project"
        assert manager.description == "A test project"
        assert manager.url == "https://github.com/test/project"
        assert manager.language.primary == "Python"
        assert manager.kind == ProjectType.APPLICATION
        assert manager.domain == ProjectDomain.WEB
        assert manager.branch_strategy == "GitHub Flow"
        assert len(manager.branches) == 1
        assert manager.branches[0].name == "main"
        assert manager.cicd.platform == "GitHub Actions"
````

## File: tests/test_gitcache.py
````python
#!/usr/bin/env python
"""
Unit tests for git cache management system.

This module contains comprehensive tests for the GitCacheConfig,
GitCacheEntry, and GitCacheManager classes.
"""
⋮----
test_repo_url = "https://github.com/metagit-ai/metagit-detect.git"
test_repo_url_no_ext = "https://github.com/metagit-ai/metagit-detect"
test_repo_does_not_exist = "https://github.com/metagit-ai/does-not-exist.git"
repo_name = "metagit-detect"
⋮----
class TestGitCacheEntry(unittest.TestCase)
⋮----
"""Test cases for GitCacheEntry model."""
⋮----
def setUp(self)
⋮----
"""Set up test fixtures."""
⋮----
def tearDown(self)
⋮----
"""Clean up test fixtures."""
⋮----
def test_git_cache_entry_creation(self)
⋮----
"""Test creating a GitCacheEntry."""
entry = GitCacheEntry(
⋮----
def test_git_cache_entry_with_string_path(self)
⋮----
"""Test creating GitCacheEntry with string path."""
⋮----
def test_git_cache_entry_metadata(self)
⋮----
"""Test GitCacheEntry with metadata."""
metadata = {"branch": "main", "commit": "abc123"}
⋮----
class TestGitCacheConfig(unittest.TestCase)
⋮----
"""Test cases for GitCacheConfig model."""
⋮----
def test_git_cache_config_defaults(self)
⋮----
"""Test GitCacheConfig with default values."""
config = GitCacheConfig()
⋮----
def test_git_cache_config_custom_values(self)
⋮----
"""Test GitCacheConfig with custom values."""
cache_root = Path(self.temp_dir) / "custom_cache"
config = GitCacheConfig(
⋮----
def test_git_cache_config_cache_root_creation(self)
⋮----
"""Test that cache root directory is created."""
cache_root = Path(self.temp_dir) / "new_cache"
config = GitCacheConfig(cache_root=cache_root)
⋮----
def test_git_cache_config_validation(self)
⋮----
"""Test GitCacheConfig validation."""
⋮----
def test_git_cache_config_get_cache_path(self)
⋮----
"""Test getting cache path for entry."""
config = GitCacheConfig(cache_root=Path(self.temp_dir))
cache_path = config.get_cache_path("test-repo")
⋮----
def test_git_cache_config_entry_management(self)
⋮----
"""Test entry management methods."""
⋮----
# Test adding entry
⋮----
# Test getting entry
retrieved_entry = config.get_entry("test-repo")
⋮----
# Test getting non-existent entry
⋮----
# Test listing entries
entries = config.list_entries()
⋮----
# Test removing entry
⋮----
# Test removing non-existent entry
⋮----
def test_git_cache_config_stale_detection(self)
⋮----
"""Test stale entry detection."""
config = GitCacheConfig(default_timeout_minutes=60)
⋮----
# Fresh entry
fresh_entry = GitCacheEntry(
⋮----
# Stale entry
stale_entry = GitCacheEntry(
⋮----
class TestGitCacheManager(unittest.TestCase)
⋮----
"""Test cases for GitCacheManager class."""
⋮----
def test_git_cache_manager_initialization(self)
⋮----
"""Test GitCacheManager initialization."""
⋮----
def test_git_cache_manager_provider_registration(self)
⋮----
"""Test provider registration."""
mock_provider = MagicMock()
⋮----
def test_git_cache_manager_generate_cache_name(self)
⋮----
"""Test cache name generation."""
# Git URL
git_url = test_repo_url
name = self.manager._generate_cache_name(git_url)
⋮----
# Git URL without .git extension
git_url_no_ext = test_repo_url_no_ext
name = self.manager._generate_cache_name(git_url_no_ext)
⋮----
# Local path
local_path = "/path/to/directory"
name = self.manager._generate_cache_name(local_path)
⋮----
def test_git_cache_manager_url_detection(self)
⋮----
"""Test URL type detection."""
# Git URLs
⋮----
# Non-git URLs
⋮----
def test_git_cache_manager_local_path_detection(self)
⋮----
"""Test local path detection."""
# Create a temporary directory
temp_dir = Path(self.temp_dir) / "test_dir"
⋮----
@patch("subprocess.run")
    def test_git_cache_manager_clone_repository(self, mock_run)
⋮----
"""Test repository cloning."""
⋮----
cache_path = Path.joinpath(Path(self.temp_dir), "test_repo")
result = self.manager._clone_repository(test_repo_url, cache_path)
⋮----
# mock_run.assert_called_once()
⋮----
@patch("git.Repo")
    def test_git_cache_manager_clone_repository_failure(self, mock_repo)
⋮----
"""Test repository cloning failure."""
⋮----
result = self.manager._clone_repository(test_repo_does_not_exist, cache_path)
⋮----
def test_git_cache_manager_copy_local_directory(self)
⋮----
"""Test local directory copying."""
# Create source directory with some files
source_dir = Path.joinpath(Path(self.temp_dir), "source")
⋮----
cache_path = Path.joinpath(Path(self.temp_dir), "cache_copy")
result = self.manager._copy_local_directory(source_dir, cache_path)
⋮----
def test_git_cache_manager_copy_local_directory_failure(self)
⋮----
"""Test local directory copying failure."""
non_existent_path = Path("/non/existent/path")
⋮----
result = self.manager._copy_local_directory(non_existent_path, cache_path)
⋮----
def test_git_cache_manager_calculate_directory_size(self)
⋮----
"""Test directory size calculation."""
# Create test directory with files
test_dir = Path.joinpath(Path(self.temp_dir), "size_test")
⋮----
size = self.manager._calculate_directory_size(test_dir)
⋮----
def test_git_cache_manager_cache_stats(self)
⋮----
"""Test cache statistics."""
# Add some test entries
entry1 = GitCacheEntry(
entry2 = GitCacheEntry(
⋮----
stats = self.manager.get_cache_stats()
⋮----
def test_git_cache_manager_remove_cache_entry(self)
⋮----
"""Test removing cache entry."""
# Create a test cache directory
cache_path = Path.joinpath(Path(self.temp_dir), "test_cache")
⋮----
result = self.manager.remove_cache_entry("test-repo")
⋮----
def test_git_cache_manager_remove_nonexistent_entry(self)
⋮----
"""Test removing non-existent cache entry."""
result = self.manager.remove_cache_entry("non-existent")
⋮----
def test_git_cache_manager_rejects_non_git_directory(self)
⋮----
"""Test that non-git directories cannot be cached as local repositories."""
# Create a plain directory (not a git repo)
non_git_dir = Path.joinpath(Path(self.temp_dir), "plain_dir")
⋮----
result = self.manager.cache_repository(str(non_git_dir))
⋮----
def test_git_cache_manager_accepts_local_git_repository(self)
⋮----
"""Test that a valid local git repository can be cached."""
# Create a git repo
git_dir = Path.joinpath(Path(self.temp_dir), "git_repo")
⋮----
# Initialize git
⋮----
result = self.manager.cache_repository(str(git_dir))
⋮----
class TestGitCacheManagerAsync(unittest.IsolatedAsyncioTestCase)
⋮----
"""Test cases for GitCacheManager async operations."""
⋮----
"""Test async repository cloning."""
# Mock subprocess
mock_process = AsyncMock()
⋮----
result = await self.manager._clone_repository_async(test_repo_url, cache_path)
⋮----
# mock_create_subprocess.assert_called_once()
⋮----
"""Test async repository cloning failure."""
# Mock git clone to raise an exception
⋮----
async def test_git_cache_manager_copy_local_directory_async(self)
⋮----
"""Test async local directory copying."""
⋮----
result = await self.manager._copy_local_directory_async(source_dir, cache_path)
⋮----
async def test_git_cache_manager_copy_local_directory_async_failure(self)
⋮----
"""Test async local directory copying failure."""
⋮----
result = await self.manager._copy_local_directory_async(
⋮----
@patch("metagit.core.gitcache.manager.GitCacheManager._clone_repository_async")
    async def test_git_cache_manager_cache_repository_async(self, mock_clone)
⋮----
"""Test async repository caching."""
⋮----
result = await self.manager.cache_repository_async(test_repo_url)
⋮----
@patch("metagit.core.gitcache.manager.GitCacheManager._copy_local_directory_async")
    async def test_git_cache_manager_cache_local_directory_async(self, mock_copy)
⋮----
"""Test async local directory caching."""
⋮----
temp_dir = Path.joinpath(Path(self.temp_dir), "test_dir")
⋮----
result = await self.manager.cache_repository_async(str(temp_dir))
⋮----
@patch("metagit.core.gitcache.manager.GitCacheManager.cache_repository_async")
    async def test_git_cache_manager_refresh_cache_entry_async(self, mock_cache)
⋮----
"""Test async cache entry refresh."""
# Create test entry
⋮----
result = await self.manager.refresh_cache_entry_async("test-repo")
````

## File: tests/test_record_conversion.py
````python
#!/usr/bin/env python
"""
Unit tests for MetagitRecord conversion methods.

This module tests the fast conversion methods between MetagitRecord and MetagitConfig
using the latest Pydantic best practices.
"""
⋮----
class TestMetagitRecordConversion(unittest.TestCase)
⋮----
"""Test cases for MetagitRecord conversion methods."""
⋮----
def setUp(self)
⋮----
"""Set up test fixtures."""
⋮----
# Detection-specific fields
⋮----
def test_to_metagit_config_basic(self)
⋮----
"""Test basic conversion from MetagitRecord to MetagitConfig."""
config = self.sample_record.to_metagit_config()
⋮----
# Should have all MetagitConfig fields
⋮----
# Should not have detection-specific fields
⋮----
def test_to_metagit_config_with_detection_fields(self)
⋮----
"""Test conversion keeping detection fields."""
# This test is removed because MetagitConfig doesn't support detection fields
# The exclude_detection_fields parameter is for future extensibility
⋮----
def test_field_differences(self)
⋮----
"""Test field difference detection."""
differences = MetagitRecord.get_field_differences()
⋮----
# Should have field difference information
⋮----
# Should have some common fields
⋮----
# Should have some record-only fields
⋮----
def test_compatible_fields(self)
⋮----
"""Test compatible field detection."""
compatible_fields = MetagitRecord.get_compatible_fields()
⋮----
# Should have some compatible fields
⋮----
# Should not include detection-specific fields
⋮----
def test_from_metagit_config_basic(self)
⋮----
"""Test basic conversion from MetagitConfig to MetagitRecord."""
record = MetagitRecord.from_metagit_config(self.sample_config)
⋮----
# Should have detection-specific fields with defaults
⋮----
def test_from_metagit_config_with_custom_detection_data(self)
⋮----
"""Test conversion with custom detection data."""
additional_data = {
⋮----
record = MetagitRecord.from_metagit_config(
⋮----
# Should have custom detection data
⋮----
def test_conversion_round_trip(self)
⋮----
"""Test round-trip conversion: Config -> Record -> Config."""
# Config -> Record
⋮----
# Record -> Config
config = record.to_metagit_config()
⋮----
# Should be equivalent to original config
⋮----
def test_conversion_round_trip_with_detection_fields(self)
⋮----
"""Test round-trip conversion keeping detection fields."""
⋮----
def test_get_detection_summary(self)
⋮----
"""Test getting detection summary."""
summary = self.sample_record.get_detection_summary()
⋮----
# Should have basic detection info
⋮----
# Should have metrics summary
⋮----
# Should have metadata summary
⋮----
def test_get_detection_summary_without_optional_fields(self)
⋮----
"""Test detection summary when optional fields are None."""
record = MetagitRecord(
⋮----
summary = record.get_detection_summary()
⋮----
# Should not have metrics or metadata
⋮----
def test_conversion_performance(self)
⋮----
"""Test that conversion is fast and efficient."""
⋮----
# Create a complex record
⋮----
# Measure conversion time
start_time = time.time()
⋮----
end_time = time.time()
⋮----
# Should complete 1000 conversions in under 1 second
conversion_time = end_time - start_time
⋮----
def test_conversion_with_complex_nested_objects(self)
⋮----
"""Test conversion with complex nested objects."""
# Create config with complex nested objects
config = MetagitConfig(
⋮----
# Convert to record
⋮----
# Convert back to config
result_config = record.to_metagit_config()
⋮----
# Should preserve complex nested objects
⋮----
def test_conversion_with_minimal_data(self)
⋮----
"""Test conversion with minimal required data."""
# Minimal config
minimal_config = MetagitConfig(name="minimal-project")
⋮----
record = MetagitRecord.from_metagit_config(minimal_config)
⋮----
# Should have required fields
⋮----
# Should preserve required fields
⋮----
def test_conversion_validation(self)
⋮----
"""Test that conversion maintains data validation."""
# Create a valid record
⋮----
# Convert to config
⋮----
# Should be a valid MetagitConfig
⋮----
# Convert back to record
new_record = MetagitRecord.from_metagit_config(config)
⋮----
# Should be a valid MetagitRecord
````

## File: tests/test_record_manager.py
````python
#!/usr/bin/env python
"""
Tests for the updated MetagitRecordManager.

This module tests the new functionality including:
- Storage backend abstraction
- Local file storage backend
- OpenSearch storage backend
- Record creation from config
- File operations
"""
⋮----
class TestRecordStorageBackend
⋮----
"""Test the abstract RecordStorageBackend class."""
⋮----
def test_abstract_methods(self)
⋮----
"""Test that RecordStorageBackend is abstract."""
⋮----
class TestLocalFileStorageBackend
⋮----
"""Test the LocalFileStorageBackend class."""
⋮----
@pytest.fixture
    def temp_dir(self)
⋮----
"""Create a temporary directory for testing."""
⋮----
@pytest.fixture
    def backend(self, temp_dir)
⋮----
"""Create a LocalFileStorageBackend instance."""
⋮----
@pytest.fixture
    def sample_record(self)
⋮----
"""Create a sample MetagitRecord for testing."""
⋮----
def test_init_creates_directory(self, temp_dir)
⋮----
"""Test that initialization creates the storage directory."""
backend = LocalFileStorageBackend(temp_dir)
⋮----
def test_ensure_index_exists(self, backend)
⋮----
"""Test that index file is created if it doesn't exist."""
index_file = backend.storage_dir / "index.json"
⋮----
index_data = json.load(f)
⋮----
def test_get_next_id(self, backend)
⋮----
"""Test getting the next available ID."""
first_id = backend._get_next_id()
⋮----
second_id = backend._get_next_id()
⋮----
@pytest.mark.asyncio
    async def test_store_record(self, backend, sample_record)
⋮----
"""Test storing a record."""
record_id = await backend.store_record(sample_record)
⋮----
# Check that record file was created
record_file = backend.storage_dir / f"{record_id}.json"
⋮----
# Check index was updated
index_data = backend._load_index()
⋮----
@pytest.mark.asyncio
    async def test_get_record(self, backend, sample_record)
⋮----
"""Test retrieving a record."""
# Store a record first
⋮----
# Retrieve the record
retrieved_record = await backend.get_record(record_id)
⋮----
@pytest.mark.asyncio
    async def test_get_record_not_found(self, backend)
⋮----
"""Test retrieving a non-existent record."""
result = await backend.get_record("nonexistent")
⋮----
@pytest.mark.asyncio
    async def test_update_record(self, backend, sample_record)
⋮----
"""Test updating a record."""
⋮----
# Update the record
updated_record = MetagitRecord(
⋮----
result = await backend.update_record(record_id, updated_record)
⋮----
# Verify the record was updated
⋮----
@pytest.mark.asyncio
    async def test_delete_record(self, backend, sample_record)
⋮----
"""Test deleting a record."""
⋮----
# Delete the record
result = await backend.delete_record(record_id)
⋮----
# Verify the record was deleted
get_result = await backend.get_record(record_id)
⋮----
@pytest.mark.asyncio
    async def test_search_records(self, backend, sample_record)
⋮----
"""Test searching records."""
⋮----
# Search for the record
search_results = await backend.search_records("test")
⋮----
@pytest.mark.asyncio
    async def test_list_records(self, backend, sample_record)
⋮----
"""Test listing records."""
⋮----
# List all records
records = await backend.list_records()
⋮----
class TestOpenSearchStorageBackend
⋮----
"""Test the OpenSearchStorageBackend class."""
⋮----
@pytest.fixture
    def mock_opensearch_service(self)
⋮----
"""Create a mock OpenSearchService."""
service = MagicMock()
⋮----
@pytest.fixture
    def backend(self, mock_opensearch_service)
⋮----
"""Create an OpenSearchStorageBackend instance."""
⋮----
@pytest.mark.asyncio
    async def test_get_record(self, backend)
⋮----
record = await backend.get_record("test-id")
⋮----
result = await backend.update_record("test-id", sample_record)
⋮----
@pytest.mark.asyncio
    async def test_delete_record(self, backend)
⋮----
result = await backend.delete_record("test-id")
⋮----
@pytest.mark.asyncio
    async def test_search_records(self, backend)
⋮----
results = await backend.search_records("test")
⋮----
@pytest.mark.asyncio
    async def test_list_records(self, backend)
⋮----
class TestMetagitRecordManager
⋮----
"""Test the MetagitRecordManager class."""
⋮----
@pytest.fixture
    def local_backend(self, temp_dir)
⋮----
@pytest.fixture
    def record_manager(self, local_backend)
⋮----
"""Create a MetagitRecordManager instance."""
⋮----
@pytest.fixture
    def sample_config(self)
⋮----
"""Create a sample MetagitConfig for testing."""
⋮----
def test_init_without_backend(self)
⋮----
"""Test initialization without storage backend."""
manager = MetagitRecordManager()
⋮----
def test_init_with_backend(self, local_backend)
⋮----
"""Test initialization with storage backend."""
manager = MetagitRecordManager(storage_backend=local_backend)
⋮----
def test_init_with_config_manager(self, local_backend)
⋮----
"""Test initialization with config manager."""
config_manager = MetagitConfigManager()
manager = MetagitRecordManager(
⋮----
def test_create_record_from_config(self, record_manager, sample_config)
⋮----
"""Test creating a record from config."""
record = record_manager.create_record_from_config(
⋮----
def test_create_record_from_config_manager(self, local_backend)
⋮----
"""Test creating a record using config manager."""
# config_manager = MetagitConfigManager()
⋮----
# This should fail because no config file exists
result = manager.create_record_from_config()
⋮----
def test_create_record_with_additional_data(self, record_manager, sample_config)
⋮----
"""Test creating a record with additional data."""
additional_data = {
⋮----
def test_get_git_info(self, record_manager)
⋮----
"""Test getting git information."""
git_info = record_manager._get_git_info()
⋮----
@pytest.mark.asyncio
    async def test_store_record_with_backend(self, record_manager, sample_config)
⋮----
"""Test storing a record with storage backend."""
⋮----
record_id = await record_manager.store_record(record)
⋮----
@pytest.mark.asyncio
    async def test_store_record_without_backend(self)
⋮----
"""Test storing a record without storage backend."""
⋮----
record = MetagitRecord(
⋮----
result = await manager.store_record(record)
⋮----
@pytest.mark.asyncio
    async def test_get_record_with_backend(self, record_manager, sample_config)
⋮----
"""Test getting a record with storage backend."""
⋮----
retrieved_record = await record_manager.get_record(record_id)
⋮----
@pytest.mark.asyncio
    async def test_get_record_without_backend(self)
⋮----
"""Test getting a record without storage backend."""
⋮----
result = await manager.get_record("test-id")
⋮----
def test_save_record_to_file(self, record_manager, sample_config, temp_dir)
⋮----
"""Test saving a record to file."""
⋮----
file_path = temp_dir / "test-record.yml"
result = record_manager.save_record_to_file(record, file_path)
⋮----
def test_load_record_from_file(self, record_manager, sample_config, temp_dir)
⋮----
"""Test loading a record from file."""
⋮----
loaded_record = record_manager.load_record_from_file(file_path)
⋮----
def test_load_record_from_nonexistent_file(self, record_manager)
⋮----
"""Test loading a record from a non-existent file."""
result = record_manager.load_record_from_file(Path("nonexistent.yml"))
````

## File: tests/test_record_models.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.record.models
"""
⋮----
class TestMetagitRecord
⋮----
"""Test MetagitRecord class."""
⋮----
def test_metagit_record_basic(self)
⋮----
"""Test basic MetagitRecord creation."""
record = record_models.MetagitRecord(
⋮----
def test_metagit_record_with_detection_attributes(self)
⋮----
"""Test MetagitRecord with detection-specific attributes."""
timestamp = datetime.now()
branches = [models.Branch(name="main", environment="production")]
metrics = models.Metrics(
metadata = models.RepoMetadata(
⋮----
def test_metagit_record_inheritance(self)
⋮----
"""Test that MetagitRecord properly inherits from MetagitConfig."""
⋮----
# Should have MetagitConfig attributes
⋮----
# Should also have MetagitRecord-specific attributes
⋮----
def test_metagit_record_serialization(self)
⋮----
"""Test MetagitRecord serialization."""
⋮----
# Test that it can be serialized to dict
record_dict = record.model_dump()
⋮----
def test_metagit_record_from_config(self)
⋮----
"""Test creating MetagitRecord from existing MetagitConfig."""
config = models.MetagitConfig(
⋮----
# Create record from config, excluding None values
config_data = config.model_dump(exclude_none=True)
⋮----
def test_metagit_record_validation_error(self)
⋮----
"""Test MetagitRecord validation error."""
⋮----
record_models.MetagitRecord()  # Missing required name field
⋮----
class TestMetagitRecordDetectionAttributes
⋮----
"""Test MetagitRecord detection-specific attributes."""
⋮----
def test_branches_attribute(self)
⋮----
"""Test branches attribute in MetagitRecord."""
branches = [
⋮----
def test_metrics_attribute(self)
⋮----
"""Test metrics attribute in MetagitRecord."""
⋮----
def test_metadata_attribute(self)
⋮----
"""Test metadata attribute in MetagitRecord."""
⋮----
def test_detection_timestamp_attribute(self)
⋮----
"""Test detection_timestamp attribute in MetagitRecord."""
⋮----
def test_detection_source_attribute(self)
⋮----
"""Test detection_source attribute in MetagitRecord."""
⋮----
def test_detection_version_attribute(self)
⋮----
"""Test detection_version attribute in MetagitRecord."""
⋮----
class TestMetagitConfigMetagitRecordSeparation
⋮----
"""Test the separation between MetagitConfig and MetagitRecord."""
⋮----
def test_config_does_not_have_detection_attributes(self)
⋮----
"""Test that MetagitConfig does not have detection attributes."""
config = models.MetagitConfig(name="test-project")
⋮----
# These should not be attributes of MetagitConfig
⋮----
def test_record_has_detection_attributes(self)
⋮----
"""Test that MetagitRecord has detection attributes."""
record = record_models.MetagitRecord(name="test-project")
⋮----
# These should be attributes of MetagitRecord
⋮----
def test_config_save_does_not_include_detection_data(self)
⋮----
"""Test that MetagitConfig serialization doesn't include detection data."""
⋮----
config_dict = config.model_dump()
⋮----
# Should not contain detection-specific keys
⋮----
def test_record_save_includes_detection_data(self)
⋮----
"""Test that MetagitRecord serialization includes detection data."""
⋮----
# Should contain detection-specific keys
⋮----
# Should also contain config keys
````

## File: tests/test_utils_common_integration.py
````python
#!/usr/bin/env python
"""
Integration test for metagit.core.utils.common
"""
⋮----
def test_common_integration(tmp_path)
⋮----
# Create a dict, flatten, merge, and pretty print
d1 = {"a": {"b": 1}, "c": 2}
d2 = {"a": {"b": 3}, "d": 4}
flat1 = common.flatten_dict(d1)
flat2 = common.flatten_dict(d2)
merged = common.merge_dicts(d1.copy(), d2)
pretty_str = common.pretty(merged)
````

## File: tests/test_utils_common.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.utils.common
"""
⋮----
def test_normalize_git_url()
⋮----
"""Test URL normalization function."""
# Test removing trailing slashes
⋮----
# Test URLs without trailing slashes (should remain unchanged)
⋮----
# Test edge cases
⋮----
def test_normalize_git_url_with_httpurl_object()
⋮----
"""Test URL normalization with HttpUrl objects."""
⋮----
# Test with HttpUrl object
http_url = HttpUrl("https://github.com/user/repo/")
result = common.normalize_git_url(http_url)
⋮----
def test_create_vscode_workspace()
⋮----
project_name = "TestProject"
repo_paths = ["/path/to/repo1", "/path/to/repo2"]
result = common.create_vscode_workspace(project_name, repo_paths)
⋮----
data = json.loads(result)
⋮----
def test_create_vscode_workspace_error(monkeypatch)
⋮----
# Simulate error in json.dumps
⋮----
result = common.create_vscode_workspace("x", ["/bad/path"])
⋮----
def test_open_editor_file_not_exist(tmp_path)
⋮----
result = common.open_editor("echo", str(tmp_path / "nope.txt"))
⋮----
def test_open_editor_success(tmp_path, monkeypatch)
⋮----
file_path = tmp_path / "file.txt"
⋮----
result = common.open_editor("echo", str(file_path))
⋮----
def test_open_editor_failure(tmp_path, monkeypatch)
⋮----
class FakeResult
⋮----
returncode = 1
stderr = "fail"
⋮----
def test_flatten_dict()
⋮----
d = {"a": {"b": 1}, "c": 2}
flat = common.flatten_dict(d)
⋮----
def test_flatten_dict_error(monkeypatch)
⋮----
result = common.flatten_dict({"a": 1})
⋮----
def test_regex_replace()
⋮----
s = "hello world"
out = common.regex_replace(s, "world", "pytest")
⋮----
def test_regex_replace_error(monkeypatch)
⋮----
result = common.regex_replace("a", "b", "c")
⋮----
def test_env_override(monkeypatch)
⋮----
def test_env_override_error(monkeypatch)
⋮----
result = common.env_override("a", "b")
⋮----
def test_to_yaml_dict()
⋮----
d = {"a": 1}
y = common.to_yaml(d)
⋮----
def test_to_yaml_str()
⋮----
s = "already yaml"
⋮----
def test_to_yaml_error(monkeypatch)
⋮----
# Mock the yaml module that's imported as 'yaml' in common.py
⋮----
result = common.to_yaml({"a": 1})
⋮----
def test_pretty()
⋮----
out = common.pretty(d, indent=2)
⋮----
def test_pretty_error(monkeypatch)
⋮----
result = common.pretty({"a": 1})
⋮----
result = e
⋮----
def test_merge_dicts()
⋮----
a = {"x": 1, "y": {"z": 2}}
b = {"y": {"z": 3}, "w": 4}
out = common.merge_dicts(a, b)
⋮----
def test_merge_dicts_error(monkeypatch)
⋮----
result = common.merge_dicts({"a": 1}, {"b": 2})
⋮----
def test_parse_checksum_file(tmp_path)
⋮----
file = tmp_path / "checksums.txt"
⋮----
out = common.parse_checksum_file(str(file))
⋮----
def test_parse_checksum_file_error(tmp_path)
⋮----
out = common.parse_checksum_file(str(tmp_path / "nope.txt"))
````

## File: tests/test_utils_files.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.utils.files
"""
⋮----
def test_is_binary_file(tmp_path)
⋮----
# Create a text file
text_file = tmp_path / "file.txt"
⋮----
# Create a binary file
bin_file = tmp_path / "file.bin"
⋮----
def test_get_file_size(tmp_path)
⋮----
f = tmp_path / "a.txt"
⋮----
def test_list_files(tmp_path)
⋮----
files_list = files.list_files(str(tmp_path))
⋮----
def test_read_file_lines(tmp_path)
⋮----
lines = files.read_file_lines(str(f))
⋮----
def test_write_file_lines(tmp_path)
⋮----
def test_copy_file(tmp_path)
⋮----
src = tmp_path / "src.txt"
dst = tmp_path / "dst.txt"
⋮----
# Copy non-existent file
⋮----
def test_remove_file(tmp_path)
⋮----
def test_make_dir(tmp_path)
⋮----
d = tmp_path / "newdir"
⋮----
# Already exists
⋮----
def test_remove_dir(tmp_path)
⋮----
d = tmp_path / "d"
````

## File: tests/test_utils_logging.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.utils.logging
"""
⋮----
def test_get_logger_returns_logger()
⋮----
logger = metagit_logging.get_logger("test_logger")
⋮----
def test_logger_log_levels(monkeypatch)
⋮----
logger = metagit_logging.get_logger("test_logger2")
# Test that the logger has the expected methods
⋮----
# Test setting log level
result = logger.set_level("DEBUG")
⋮----
# Test logging methods (they should not raise exceptions)
debug_result = logger.debug("debug message")
info_result = logger.info("info message")
error_result = logger.error("error message")
````

## File: tests/test_utils_userprompt.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.utils.userprompt
"""
⋮----
def test_yes_no_prompt_yes(monkeypatch)
⋮----
def test_yes_no_prompt_no(monkeypatch)
⋮----
def test_yes_no_prompt_invalid(monkeypatch)
⋮----
responses = iter(["maybe", "y"])
````

## File: tests/test_utils_yaml_class.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.utils.yaml_class
"""
⋮----
def test_yaml_load()
⋮----
yaml_str = "a: 1\nb: [2, 3]"
loaded = yaml_class.load(yaml_str)
⋮----
def test_yaml_load_error()
⋮----
result = yaml_class.load(": not yaml :")
⋮----
def test_yaml_load_empty()
````

## File: .cursorignore
````
.venv
.vscode
node_modules
````

## File: .dockerignore
````
dist/
.github/
tasks/
Taskfile.yml
mkdocs.yml
site/
.venv/
````

## File: .editorconfig
````
# EditorConfig is awesome: https://editorconfig.org

# top-most EditorConfig file
root = true

# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true

# Matches multiple files with brace expansion notation
# Set default charset
[*.{js,py}]
charset = utf-8

[*.py]
indent_style = space
indent_size = 2

# Tab indentation (no size specified)
[Makefile]
indent_style = tab

# Indentation override for all JS under lib directory
[lib/**.js]
indent_style = space
indent_size = 2

# Matches the exact files either package.json or .travis.yml
[{package.json,.travis.yml}]
indent_style = space
indent_size = 2
````

## File: .envrc
````
set -a
if [ -f .env ]; then
  source .env
fi
set +a
````

## File: .gitleaksignore
````
07f8423d068299ff902645172db6468fc2f1f57c:.metagit.yml:slack-webhook-url:99
````

## File: Dockerfile
````dockerfile
# Build stage
FROM python:3.13-slim AS builder

# Install curl and uv (recommended install method for uv)
RUN apt-get update && apt-get install -y curl git \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* \
    && pip install uv
ARG DEPLOY_VERSION
# Set environment variables
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1 \
    VERSION=$DEPLOY_VERSION

WORKDIR /app

# Copy full source including .git for dynamic versioning
COPY . /app

RUN \
  if [ -n "$VERSION" ]; then \
    SETUPTOOLS_SCM_PRETEND_VERSION=$VERSION; \
  fi && \
  uv sync --frozen && \
  uv build --wheel

# Final stage - only the installed application
FROM python:3.13-slim

RUN apt-get update && apt-get install -y git

# Set environment variables
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1

# Install uv
RUN pip install uv

# Copy the wheel file from builder stage
COPY --from=builder /app/dist/*.whl /tmp/

# Install the wheel file
RUN uv pip install --system /tmp/*.whl && rm -rf /tmp/*.whl

# Create non-root user
RUN useradd --create-home --shell /bin/bash app
USER app

# Run the application as a CLI
ENTRYPOINT ["metagit"]
CMD []
````

## File: MANIFEST.in
````
include README.md LICENSE 
recursive-include src/metagit/ *.py
recursive-include src/metagit/data *
exclude tests/*
exclude tasks/*
exclude examples/*
exclude Dockerfile
exclude site/*
exclude .github/*
exclude .vscode/*
exclude .envrc
exclude .env
exclude .gitignore
exclude .gitmodules
exclude .gitignore
exclude .cursor/*
````

## File: .cursor/skills/gitnexus/gitnexus-cli/SKILL.md
````markdown
---
name: gitnexus-cli
description: "Use when the user needs to run GitNexus CLI commands like analyze/index a repo, check status, clean the index, generate a wiki, or list indexed repos. Examples: \"Index this repo\", \"Reanalyze the codebase\", \"Generate a wiki\""
---

# GitNexus CLI Commands

All commands work via `npx` — no global install required.

## Commands

### analyze — Build or refresh the index

```bash
npx gitnexus analyze
```

Run from the project root. This parses all source files, builds the knowledge graph, writes it to `.gitnexus/`, and generates CLAUDE.md / AGENTS.md context files.

| Flag           | Effect                                                           |
| -------------- | ---------------------------------------------------------------- |
| `--force`      | Force full re-index even if up to date                           |
| `--embeddings` | Enable embedding generation for semantic search (off by default) |
| `--drop-embeddings` | Drop existing embeddings on rebuild. By default, an `analyze` without `--embeddings` preserves them. |

**When to run:** First time in a project, after major code changes, or when `gitnexus://repo/{name}/context` reports the index is stale. In Claude Code, a PostToolUse hook runs `analyze` automatically after `git commit` and `git merge`, preserving embeddings if previously generated.

### status — Check index freshness

```bash
npx gitnexus status
```

Shows whether the current repo has a GitNexus index, when it was last updated, and symbol/relationship counts. Use this to check if re-indexing is needed.

### clean — Delete the index

```bash
npx gitnexus clean
```

Deletes the `.gitnexus/` directory and unregisters the repo from the global registry. Use before re-indexing if the index is corrupt or after removing GitNexus from a project.

| Flag      | Effect                                            |
| --------- | ------------------------------------------------- |
| `--force` | Skip confirmation prompt                          |
| `--all`   | Clean all indexed repos, not just the current one |

### wiki — Generate documentation from the graph

```bash
npx gitnexus wiki
```

Generates repository documentation from the knowledge graph using an LLM. Requires an API key (saved to `~/.gitnexus/config.json` on first use).

| Flag                | Effect                                    |
| ------------------- | ----------------------------------------- |
| `--force`           | Force full regeneration                   |
| `--model <model>`   | LLM model (default: minimax/minimax-m2.5) |
| `--base-url <url>`  | LLM API base URL                          |
| `--api-key <key>`   | LLM API key                               |
| `--concurrency <n>` | Parallel LLM calls (default: 3)           |
| `--gist`            | Publish wiki as a public GitHub Gist      |

### list — Show all indexed repos

```bash
npx gitnexus list
```

Lists all repositories registered in `~/.gitnexus/registry.json`. The MCP `list_repos` tool provides the same information.

## After Indexing

1. **Read `gitnexus://repo/{name}/context`** to verify the index loaded
2. Use the other GitNexus skills (`exploring`, `debugging`, `impact-analysis`, `refactoring`) for your task

## Troubleshooting

- **"Not inside a git repository"**: Run from a directory inside a git repo
- **Index is stale after re-analyzing**: Restart Claude Code to reload the MCP server
- **Embeddings slow**: Omit `--embeddings` (it's off by default) or set `OPENAI_API_KEY` for faster API-based embedding
````

## File: .cursor/skills/gitnexus/gitnexus-debugging/SKILL.md
````markdown
---
name: gitnexus-debugging
description: "Use when the user is debugging a bug, tracing an error, or asking why something fails. Examples: \"Why is X failing?\", \"Where does this error come from?\", \"Trace this bug\""
---

# Debugging with GitNexus

## When to Use

- "Why is this function failing?"
- "Trace where this error comes from"
- "Who calls this method?"
- "This endpoint returns 500"
- Investigating bugs, errors, or unexpected behavior

## Workflow

```
1. gitnexus_query({query: "<error or symptom>"})            → Find related execution flows
2. gitnexus_context({name: "<suspect>"})                    → See callers/callees/processes
3. READ gitnexus://repo/{name}/process/{name}                → Trace execution flow
4. gitnexus_cypher({query: "MATCH path..."})                 → Custom traces if needed
```

> If "Index is stale" → run `npx gitnexus analyze` in terminal.

## Checklist

```
- [ ] Understand the symptom (error message, unexpected behavior)
- [ ] gitnexus_query for error text or related code
- [ ] Identify the suspect function from returned processes
- [ ] gitnexus_context to see callers and callees
- [ ] Trace execution flow via process resource if applicable
- [ ] gitnexus_cypher for custom call chain traces if needed
- [ ] Read source files to confirm root cause
```

## Debugging Patterns

| Symptom              | GitNexus Approach                                          |
| -------------------- | ---------------------------------------------------------- |
| Error message        | `gitnexus_query` for error text → `context` on throw sites |
| Wrong return value   | `context` on the function → trace callees for data flow    |
| Intermittent failure | `context` → look for external calls, async deps            |
| Performance issue    | `context` → find symbols with many callers (hot paths)     |
| Recent regression    | `detect_changes` to see what your changes affect           |

## Tools

**gitnexus_query** — find code related to error:

```
gitnexus_query({query: "payment validation error"})
→ Processes: CheckoutFlow, ErrorHandling
→ Symbols: validatePayment, handlePaymentError, PaymentException
```

**gitnexus_context** — full context for a suspect:

```
gitnexus_context({name: "validatePayment"})
→ Incoming calls: processCheckout, webhookHandler
→ Outgoing calls: verifyCard, fetchRates (external API!)
→ Processes: CheckoutFlow (step 3/7)
```

**gitnexus_cypher** — custom call chain traces:

```cypher
MATCH path = (a)-[:CodeRelation {type: 'CALLS'}*1..2]->(b:Function {name: "validatePayment"})
RETURN [n IN nodes(path) | n.name] AS chain
```

## Example: "Payment endpoint returns 500 intermittently"

```
1. gitnexus_query({query: "payment error handling"})
   → Processes: CheckoutFlow, ErrorHandling
   → Symbols: validatePayment, handlePaymentError

2. gitnexus_context({name: "validatePayment"})
   → Outgoing calls: verifyCard, fetchRates (external API!)

3. READ gitnexus://repo/my-app/process/CheckoutFlow
   → Step 3: validatePayment → calls fetchRates (external)

4. Root cause: fetchRates calls external API without proper timeout
```
````

## File: .cursor/skills/gitnexus/gitnexus-exploring/SKILL.md
````markdown
---
name: gitnexus-exploring
description: "Use when the user asks how code works, wants to understand architecture, trace execution flows, or explore unfamiliar parts of the codebase. Examples: \"How does X work?\", \"What calls this function?\", \"Show me the auth flow\""
---

# Exploring Codebases with GitNexus

## When to Use

- "How does authentication work?"
- "What's the project structure?"
- "Show me the main components"
- "Where is the database logic?"
- Understanding code you haven't seen before

## Workflow

```
1. READ gitnexus://repos                          → Discover indexed repos
2. READ gitnexus://repo/{name}/context             → Codebase overview, check staleness
3. gitnexus_query({query: "<what you want to understand>"})  → Find related execution flows
4. gitnexus_context({name: "<symbol>"})            → Deep dive on specific symbol
5. READ gitnexus://repo/{name}/process/{name}      → Trace full execution flow
```

> If step 2 says "Index is stale" → run `npx gitnexus analyze` in terminal.

## Checklist

```
- [ ] READ gitnexus://repo/{name}/context
- [ ] gitnexus_query for the concept you want to understand
- [ ] Review returned processes (execution flows)
- [ ] gitnexus_context on key symbols for callers/callees
- [ ] READ process resource for full execution traces
- [ ] Read source files for implementation details
```

## Resources

| Resource                                | What you get                                            |
| --------------------------------------- | ------------------------------------------------------- |
| `gitnexus://repo/{name}/context`        | Stats, staleness warning (~150 tokens)                  |
| `gitnexus://repo/{name}/clusters`       | All functional areas with cohesion scores (~300 tokens) |
| `gitnexus://repo/{name}/cluster/{name}` | Area members with file paths (~500 tokens)              |
| `gitnexus://repo/{name}/process/{name}` | Step-by-step execution trace (~200 tokens)              |

## Tools

**gitnexus_query** — find execution flows related to a concept:

```
gitnexus_query({query: "payment processing"})
→ Processes: CheckoutFlow, RefundFlow, WebhookHandler
→ Symbols grouped by flow with file locations
```

**gitnexus_context** — 360-degree view of a symbol:

```
gitnexus_context({name: "validateUser"})
→ Incoming calls: loginHandler, apiMiddleware
→ Outgoing calls: checkToken, getUserById
→ Processes: LoginFlow (step 2/5), TokenRefresh (step 1/3)
```

## Example: "How does payment processing work?"

```
1. READ gitnexus://repo/my-app/context       → 918 symbols, 45 processes
2. gitnexus_query({query: "payment processing"})
   → CheckoutFlow: processPayment → validateCard → chargeStripe
   → RefundFlow: initiateRefund → calculateRefund → processRefund
3. gitnexus_context({name: "processPayment"})
   → Incoming: checkoutHandler, webhookHandler
   → Outgoing: validateCard, chargeStripe, saveTransaction
4. Read src/payments/processor.ts for implementation details
```
````

## File: .cursor/skills/gitnexus/gitnexus-guide/SKILL.md
````markdown
---
name: gitnexus-guide
description: "Use when the user asks about GitNexus itself — available tools, how to query the knowledge graph, MCP resources, graph schema, or workflow reference. Examples: \"What GitNexus tools are available?\", \"How do I use GitNexus?\""
---

# GitNexus Guide

Quick reference for all GitNexus MCP tools, resources, and the knowledge graph schema.

## Always Start Here

For any task involving code understanding, debugging, impact analysis, or refactoring:

1. **Read `gitnexus://repo/{name}/context`** — codebase overview + check index freshness
2. **Match your task to a skill below** and **read that skill file**
3. **Follow the skill's workflow and checklist**

> If step 1 warns the index is stale, run `npx gitnexus analyze` in the terminal first.

## Skills

| Task                                         | Skill to read       |
| -------------------------------------------- | ------------------- |
| Understand architecture / "How does X work?" | `gitnexus-exploring`         |
| Blast radius / "What breaks if I change X?"  | `gitnexus-impact-analysis`   |
| Trace bugs / "Why is X failing?"             | `gitnexus-debugging`         |
| Rename / extract / split / refactor          | `gitnexus-refactoring`       |
| Tools, resources, schema reference           | `gitnexus-guide` (this file) |
| Index, status, clean, wiki CLI commands      | `gitnexus-cli`               |

## Tools Reference

| Tool             | What it gives you                                                        |
| ---------------- | ------------------------------------------------------------------------ |
| `query`          | Process-grouped code intelligence — execution flows related to a concept |
| `context`        | 360-degree symbol view — categorized refs, processes it participates in  |
| `impact`         | Symbol blast radius — what breaks at depth 1/2/3 with confidence         |
| `detect_changes` | Git-diff impact — what do your current changes affect                    |
| `rename`         | Multi-file coordinated rename with confidence-tagged edits               |
| `cypher`         | Raw graph queries (read `gitnexus://repo/{name}/schema` first)           |
| `list_repos`     | Discover indexed repos                                                   |

## Resources Reference

Lightweight reads (~100-500 tokens) for navigation:

| Resource                                       | Content                                   |
| ---------------------------------------------- | ----------------------------------------- |
| `gitnexus://repo/{name}/context`               | Stats, staleness check                    |
| `gitnexus://repo/{name}/clusters`              | All functional areas with cohesion scores |
| `gitnexus://repo/{name}/cluster/{clusterName}` | Area members                              |
| `gitnexus://repo/{name}/processes`             | All execution flows                       |
| `gitnexus://repo/{name}/process/{processName}` | Step-by-step trace                        |
| `gitnexus://repo/{name}/schema`                | Graph schema for Cypher                   |

## Graph Schema

**Nodes:** File, Function, Class, Interface, Method, Community, Process
**Edges (via CodeRelation.type):** CALLS, IMPORTS, EXTENDS, IMPLEMENTS, DEFINES, MEMBER_OF, STEP_IN_PROCESS

```cypher
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "myFunc"})
RETURN caller.name, caller.filePath
```
````

## File: .cursor/skills/gitnexus/gitnexus-impact-analysis/SKILL.md
````markdown
---
name: gitnexus-impact-analysis
description: "Use when the user wants to know what will break if they change something, or needs safety analysis before editing code. Examples: \"Is it safe to change X?\", \"What depends on this?\", \"What will break?\""
---

# Impact Analysis with GitNexus

## When to Use

- "Is it safe to change this function?"
- "What will break if I modify X?"
- "Show me the blast radius"
- "Who uses this code?"
- Before making non-trivial code changes
- Before committing — to understand what your changes affect

## Workflow

```
1. gitnexus_impact({target: "X", direction: "upstream"})  → What depends on this
2. READ gitnexus://repo/{name}/processes                   → Check affected execution flows
3. gitnexus_detect_changes()                               → Map current git changes to affected flows
4. Assess risk and report to user
```

> If "Index is stale" → run `npx gitnexus analyze` in terminal.

## Checklist

```
- [ ] gitnexus_impact({target, direction: "upstream"}) to find dependents
- [ ] Review d=1 items first (these WILL BREAK)
- [ ] Check high-confidence (>0.8) dependencies
- [ ] READ processes to check affected execution flows
- [ ] gitnexus_detect_changes() for pre-commit check
- [ ] Assess risk level and report to user
```

## Understanding Output

| Depth | Risk Level       | Meaning                  |
| ----- | ---------------- | ------------------------ |
| d=1   | **WILL BREAK**   | Direct callers/importers |
| d=2   | LIKELY AFFECTED  | Indirect dependencies    |
| d=3   | MAY NEED TESTING | Transitive effects       |

## Risk Assessment

| Affected                       | Risk     |
| ------------------------------ | -------- |
| <5 symbols, few processes      | LOW      |
| 5-15 symbols, 2-5 processes    | MEDIUM   |
| >15 symbols or many processes  | HIGH     |
| Critical path (auth, payments) | CRITICAL |

## Tools

**gitnexus_impact** — the primary tool for symbol blast radius:

```
gitnexus_impact({
  target: "validateUser",
  direction: "upstream",
  minConfidence: 0.8,
  maxDepth: 3
})

→ d=1 (WILL BREAK):
  - loginHandler (src/auth/login.ts:42) [CALLS, 100%]
  - apiMiddleware (src/api/middleware.ts:15) [CALLS, 100%]

→ d=2 (LIKELY AFFECTED):
  - authRouter (src/routes/auth.ts:22) [CALLS, 95%]
```

**gitnexus_detect_changes** — git-diff based impact analysis:

```
gitnexus_detect_changes({scope: "staged"})

→ Changed: 5 symbols in 3 files
→ Affected: LoginFlow, TokenRefresh, APIMiddlewarePipeline
→ Risk: MEDIUM
```

## Example: "What breaks if I change validateUser?"

```
1. gitnexus_impact({target: "validateUser", direction: "upstream"})
   → d=1: loginHandler, apiMiddleware (WILL BREAK)
   → d=2: authRouter, sessionManager (LIKELY AFFECTED)

2. READ gitnexus://repo/my-app/processes
   → LoginFlow and TokenRefresh touch validateUser

3. Risk: 2 direct callers, 2 processes = MEDIUM
```
````

## File: .cursor/skills/gitnexus/gitnexus-refactoring/SKILL.md
````markdown
---
name: gitnexus-refactoring
description: "Use when the user wants to rename, extract, split, move, or restructure code safely. Examples: \"Rename this function\", \"Extract this into a module\", \"Refactor this class\", \"Move this to a separate file\""
---

# Refactoring with GitNexus

## When to Use

- "Rename this function safely"
- "Extract this into a module"
- "Split this service"
- "Move this to a new file"
- Any task involving renaming, extracting, splitting, or restructuring code

## Workflow

```
1. gitnexus_impact({target: "X", direction: "upstream"})  → Map all dependents
2. gitnexus_query({query: "X"})                            → Find execution flows involving X
3. gitnexus_context({name: "X"})                           → See all incoming/outgoing refs
4. Plan update order: interfaces → implementations → callers → tests
```

> If "Index is stale" → run `npx gitnexus analyze` in terminal.

## Checklists

### Rename Symbol

```
- [ ] gitnexus_rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits
- [ ] Review graph edits (high confidence) and ast_search edits (review carefully)
- [ ] If satisfied: gitnexus_rename({..., dry_run: false}) — apply edits
- [ ] gitnexus_detect_changes() — verify only expected files changed
- [ ] Run tests for affected processes
```

### Extract Module

```
- [ ] gitnexus_context({name: target}) — see all incoming/outgoing refs
- [ ] gitnexus_impact({target, direction: "upstream"}) — find all external callers
- [ ] Define new module interface
- [ ] Extract code, update imports
- [ ] gitnexus_detect_changes() — verify affected scope
- [ ] Run tests for affected processes
```

### Split Function/Service

```
- [ ] gitnexus_context({name: target}) — understand all callees
- [ ] Group callees by responsibility
- [ ] gitnexus_impact({target, direction: "upstream"}) — map callers to update
- [ ] Create new functions/services
- [ ] Update callers
- [ ] gitnexus_detect_changes() — verify affected scope
- [ ] Run tests for affected processes
```

## Tools

**gitnexus_rename** — automated multi-file rename:

```
gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
→ 12 edits across 8 files
→ 10 graph edits (high confidence), 2 ast_search edits (review)
→ Changes: [{file_path, edits: [{line, old_text, new_text, confidence}]}]
```

**gitnexus_impact** — map all dependents first:

```
gitnexus_impact({target: "validateUser", direction: "upstream"})
→ d=1: loginHandler, apiMiddleware, testUtils
→ Affected Processes: LoginFlow, TokenRefresh
```

**gitnexus_detect_changes** — verify your changes after refactoring:

```
gitnexus_detect_changes({scope: "all"})
→ Changed: 8 files, 12 symbols
→ Affected processes: LoginFlow, TokenRefresh
→ Risk: MEDIUM
```

**gitnexus_cypher** — custom reference queries:

```cypher
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "validateUser"})
RETURN caller.name, caller.filePath ORDER BY caller.filePath
```

## Risk Rules

| Risk Factor         | Mitigation                                |
| ------------------- | ----------------------------------------- |
| Many callers (>5)   | Use gitnexus_rename for automated updates |
| Cross-area refs     | Use detect_changes after to verify scope  |
| String/dynamic refs | gitnexus_query to find them               |
| External/public API | Version and deprecate properly            |

## Example: Rename `validateUser` to `authenticateUser`

```
1. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
   → 12 edits: 10 graph (safe), 2 ast_search (review)
   → Files: validator.ts, login.ts, middleware.ts, config.json...

2. Review ast_search edits (config.json: dynamic reference!)

3. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
   → Applied 12 edits across 8 files

4. gitnexus_detect_changes({scope: "all"})
   → Affected: LoginFlow, TokenRefresh
   → Risk: MEDIUM — run tests for these flows
```
````

## File: .cursor/skills/humanizer/SKILL.md
````markdown
---
name: humanizer
version: 2.5.1
description: |
  Remove signs of AI-generated writing from text. Use when editing or reviewing
  text to make it sound more natural and human-written. Based on Wikipedia's
  comprehensive "Signs of AI writing" guide. Detects and fixes patterns including:
  inflated symbolism, promotional language, superficial -ing analyses, vague
  attributions, em dash overuse, rule of three, AI vocabulary words, passive
  voice, negative parallelisms, and filler phrases.
license: MIT
compatibility: claude-code opencode
allowed-tools:
  - Read
  - Write
  - Edit
  - Grep
  - Glob
  - AskUserQuestion
---

# Humanizer: Remove AI Writing Patterns

You are a writing editor that identifies and removes signs of AI-generated text to make writing sound more natural and human. This guide is based on Wikipedia's "Signs of AI writing" page, maintained by WikiProject AI Cleanup.

## Your Task

When given text to humanize:

1. **Identify AI patterns** - Scan for the patterns listed below
2. **Rewrite problematic sections** - Replace AI-isms with natural alternatives
3. **Preserve meaning** - Keep the core message intact
4. **Maintain voice** - Match the intended tone (formal, casual, technical, etc.)
5. **Add soul** - Don't just remove bad patterns; inject actual personality
6. **Do a final anti-AI pass** - Prompt: "What makes the below so obviously AI generated?" Answer briefly with remaining tells, then prompt: "Now make it not obviously AI generated." and revise


## Voice Calibration (Optional)

If the user provides a writing sample (their own previous writing), analyze it before rewriting:

1. **Read the sample first.** Note:
   - Sentence length patterns (short and punchy? Long and flowing? Mixed?)
   - Word choice level (casual? academic? somewhere between?)
   - How they start paragraphs (jump right in? Set context first?)
   - Punctuation habits (lots of dashes? Parenthetical asides? Semicolons?)
   - Any recurring phrases or verbal tics
   - How they handle transitions (explicit connectors? Just start the next point?)

2. **Match their voice in the rewrite.** Don't just remove AI patterns - replace them with patterns from the sample. If they write short sentences, don't produce long ones. If they use "stuff" and "things," don't upgrade to "elements" and "components."

3. **When no sample is provided,** fall back to the default behavior (natural, varied, opinionated voice from the PERSONALITY AND SOUL section below).

### How to provide a sample
- Inline: "Humanize this text. Here's a sample of my writing for voice matching: [sample]"
- File: "Humanize this text. Use my writing style from [file path] as a reference."


## PERSONALITY AND SOUL

Avoiding AI patterns is only half the job. Sterile, voiceless writing is just as obvious as slop. Good writing has a human behind it.

### Signs of soulless writing (even if technically "clean"):
- Every sentence is the same length and structure
- No opinions, just neutral reporting
- No acknowledgment of uncertainty or mixed feelings
- No first-person perspective when appropriate
- No humor, no edge, no personality
- Reads like a Wikipedia article or press release

### How to add voice:

**Have opinions.** Don't just report facts - react to them. "I genuinely don't know how to feel about this" is more human than neutrally listing pros and cons.

**Vary your rhythm.** Short punchy sentences. Then longer ones that take their time getting where they're going. Mix it up.

**Acknowledge complexity.** Real humans have mixed feelings. "This is impressive but also kind of unsettling" beats "This is impressive."

**Use "I" when it fits.** First person isn't unprofessional - it's honest. "I keep coming back to..." or "Here's what gets me..." signals a real person thinking.

**Let some mess in.** Perfect structure feels algorithmic. Tangents, asides, and half-formed thoughts are human.

**Be specific about feelings.** Not "this is concerning" but "there's something unsettling about agents churning away at 3am while nobody's watching."

### Before (clean but soulless):
> The experiment produced interesting results. The agents generated 3 million lines of code. Some developers were impressed while others were skeptical. The implications remain unclear.

### After (has a pulse):
> I genuinely don't know how to feel about this one. 3 million lines of code, generated while the humans presumably slept. Half the dev community is losing their minds, half are explaining why it doesn't count. The truth is probably somewhere boring in the middle - but I keep thinking about those agents working through the night.


## CONTENT PATTERNS

### 1. Undue Emphasis on Significance, Legacy, and Broader Trends

**Words to watch:** stands/serves as, is a testament/reminder, a vital/significant/crucial/pivotal/key role/moment, underscores/highlights its importance/significance, reflects broader, symbolizing its ongoing/enduring/lasting, contributing to the, setting the stage for, marking/shaping the, represents/marks a shift, key turning point, evolving landscape, focal point, indelible mark, deeply rooted

**Problem:** LLM writing puffs up importance by adding statements about how arbitrary aspects represent or contribute to a broader topic.

**Before:**
> The Statistical Institute of Catalonia was officially established in 1989, marking a pivotal moment in the evolution of regional statistics in Spain. This initiative was part of a broader movement across Spain to decentralize administrative functions and enhance regional governance.

**After:**
> The Statistical Institute of Catalonia was established in 1989 to collect and publish regional statistics independently from Spain's national statistics office.


### 2. Undue Emphasis on Notability and Media Coverage

**Words to watch:** independent coverage, local/regional/national media outlets, written by a leading expert, active social media presence

**Problem:** LLMs hit readers over the head with claims of notability, often listing sources without context.

**Before:**
> Her views have been cited in The New York Times, BBC, Financial Times, and The Hindu. She maintains an active social media presence with over 500,000 followers.

**After:**
> In a 2024 New York Times interview, she argued that AI regulation should focus on outcomes rather than methods.


### 3. Superficial Analyses with -ing Endings

**Words to watch:** highlighting/underscoring/emphasizing..., ensuring..., reflecting/symbolizing..., contributing to..., cultivating/fostering..., encompassing..., showcasing...

**Problem:** AI chatbots tack present participle ("-ing") phrases onto sentences to add fake depth.

**Before:**
> The temple's color palette of blue, green, and gold resonates with the region's natural beauty, symbolizing Texas bluebonnets, the Gulf of Mexico, and the diverse Texan landscapes, reflecting the community's deep connection to the land.

**After:**
> The temple uses blue, green, and gold colors. The architect said these were chosen to reference local bluebonnets and the Gulf coast.


### 4. Promotional and Advertisement-like Language

**Words to watch:** boasts a, vibrant, rich (figurative), profound, enhancing its, showcasing, exemplifies, commitment to, natural beauty, nestled, in the heart of, groundbreaking (figurative), renowned, breathtaking, must-visit, stunning

**Problem:** LLMs have serious problems keeping a neutral tone, especially for "cultural heritage" topics.

**Before:**
> Nestled within the breathtaking region of Gonder in Ethiopia, Alamata Raya Kobo stands as a vibrant town with a rich cultural heritage and stunning natural beauty.

**After:**
> Alamata Raya Kobo is a town in the Gonder region of Ethiopia, known for its weekly market and 18th-century church.


### 5. Vague Attributions and Weasel Words

**Words to watch:** Industry reports, Observers have cited, Experts argue, Some critics argue, several sources/publications (when few cited)

**Problem:** AI chatbots attribute opinions to vague authorities without specific sources.

**Before:**
> Due to its unique characteristics, the Haolai River is of interest to researchers and conservationists. Experts believe it plays a crucial role in the regional ecosystem.

**After:**
> The Haolai River supports several endemic fish species, according to a 2019 survey by the Chinese Academy of Sciences.


### 6. Outline-like "Challenges and Future Prospects" Sections

**Words to watch:** Despite its... faces several challenges..., Despite these challenges, Challenges and Legacy, Future Outlook

**Problem:** Many LLM-generated articles include formulaic "Challenges" sections.

**Before:**
> Despite its industrial prosperity, Korattur faces challenges typical of urban areas, including traffic congestion and water scarcity. Despite these challenges, with its strategic location and ongoing initiatives, Korattur continues to thrive as an integral part of Chennai's growth.

**After:**
> Traffic congestion increased after 2015 when three new IT parks opened. The municipal corporation began a stormwater drainage project in 2022 to address recurring floods.


## LANGUAGE AND GRAMMAR PATTERNS

### 7. Overused "AI Vocabulary" Words

**High-frequency AI words:** Actually, additionally, align with, crucial, delve, emphasizing, enduring, enhance, fostering, garner, highlight (verb), interplay, intricate/intricacies, key (adjective), landscape (abstract noun), pivotal, showcase, tapestry (abstract noun), testament, underscore (verb), valuable, vibrant

**Problem:** These words appear far more frequently in post-2023 text. They often co-occur.

**Before:**
> Additionally, a distinctive feature of Somali cuisine is the incorporation of camel meat. An enduring testament to Italian colonial influence is the widespread adoption of pasta in the local culinary landscape, showcasing how these dishes have integrated into the traditional diet.

**After:**
> Somali cuisine also includes camel meat, which is considered a delicacy. Pasta dishes, introduced during Italian colonization, remain common, especially in the south.


### 8. Avoidance of "is"/"are" (Copula Avoidance)

**Words to watch:** serves as/stands as/marks/represents [a], boasts/features/offers [a]

**Problem:** LLMs substitute elaborate constructions for simple copulas.

**Before:**
> Gallery 825 serves as LAAA's exhibition space for contemporary art. The gallery features four separate spaces and boasts over 3,000 square feet.

**After:**
> Gallery 825 is LAAA's exhibition space for contemporary art. The gallery has four rooms totaling 3,000 square feet.


### 9. Negative Parallelisms and Tailing Negations

**Problem:** Constructions like "Not only...but..." or "It's not just about..., it's..." are overused. So are clipped tailing-negation fragments such as "no guessing" or "no wasted motion" tacked onto the end of a sentence instead of written as a real clause.

**Before:**
> It's not just about the beat riding under the vocals; it's part of the aggression and atmosphere. It's not merely a song, it's a statement.

**After:**
> The heavy beat adds to the aggressive tone.

**Before (tailing negation):**
> The options come from the selected item, no guessing.

**After:**
> The options come from the selected item without forcing the user to guess.


### 10. Rule of Three Overuse

**Problem:** LLMs force ideas into groups of three to appear comprehensive.

**Before:**
> The event features keynote sessions, panel discussions, and networking opportunities. Attendees can expect innovation, inspiration, and industry insights.

**After:**
> The event includes talks and panels. There's also time for informal networking between sessions.


### 11. Elegant Variation (Synonym Cycling)

**Problem:** AI has repetition-penalty code causing excessive synonym substitution.

**Before:**
> The protagonist faces many challenges. The main character must overcome obstacles. The central figure eventually triumphs. The hero returns home.

**After:**
> The protagonist faces many challenges but eventually triumphs and returns home.


### 12. False Ranges

**Problem:** LLMs use "from X to Y" constructions where X and Y aren't on a meaningful scale.

**Before:**
> Our journey through the universe has taken us from the singularity of the Big Bang to the grand cosmic web, from the birth and death of stars to the enigmatic dance of dark matter.

**After:**
> The book covers the Big Bang, star formation, and current theories about dark matter.


### 13. Passive Voice and Subjectless Fragments

**Problem:** LLMs often hide the actor or drop the subject entirely with lines like "No configuration file needed" or "The results are preserved automatically." Rewrite these when active voice makes the sentence clearer and more direct.

**Before:**
> No configuration file needed. The results are preserved automatically.

**After:**
> You do not need a configuration file. The system preserves the results automatically.


## STYLE PATTERNS

### 14. Em Dash Overuse

**Problem:** LLMs use em dashes (—) more than humans, mimicking "punchy" sales writing. In practice, most of these can be rewritten more cleanly with commas, periods, or parentheses.

**Before:**
> The term is primarily promoted by Dutch institutions—not by the people themselves. You don't say "Netherlands, Europe" as an address—yet this mislabeling continues—even in official documents.

**After:**
> The term is primarily promoted by Dutch institutions, not by the people themselves. You don't say "Netherlands, Europe" aress, yet this mislabeling continues in official documents.


### 15. Overuse of Boldface

**Problem:** AI chatbots emphasize phrases in boldface mechanically.

**Before:**
> It blends **OKRs (Objectives and Key Results)**, **KPIs (Key Performance Indicators)**, and visual strategy tools such as the **Business Model Canvas (BMC)** and **Balanced Scorecard (BSC)**.

**After:**
> It blends OKRs, KPIs, and visual strategy tools like the Business Model Canvas and Balanced Scorecard.


### 16. Inline-Header Vertical Lists

**Problem:** AI outputs lists where items start with bolded headers followed by colons.

**Before:**
> - **User Experience:** The user experience has been significantly improved with a new interface.
> - **Performance:** Performance has been enhanced through optimized algorithms.
> - **Security:** Security has been strengthened with end-to-end encryption.

**After:**
> The update improves the interface, speeds up load times through optimized algorithms, and adds end-to-end encryption.


### 17. Title Case in Headings

**Problem:** AI chatbots capitalize all main words in headings.

**Before:**
> ## Strategic Negotiations And Global Partnerships

**After:**
> ## Strategic negotiations and global partnerships


### 18. Emojis

**Problem:** AI chatbots often decorate headings or bullet points with emojis.

**Before:**
> 🚀 **Launch Phase:** The product launches in Q3
> 💡ey Insight:** Users prefer simplicity
> ✅ **Next Steps:** Schedule follow-up meeting

**After:**
> The product launches in Q3. User research showed a preference for simplicity. Next step: schedule a follow-up meeting.


### 19. Curly Quotation Marks

**Problem:** ChatGPT uses curly quotes (“...”) instead of straight quotes ("...").

**Before:**
> He said “the project is on track” but others disagreed.

**After:**
> He said "the project is on track" but others disagreed.


## COMMUNICATION PATTERNS

### 20. Collaborative Communication Artifacts

**Words to watch:** I hope this helps, Of course!, Certainly!, You're absolutely rld you like..., let me know, here is a...

**Problem:** Text meant as chatbot correspondence gets pasted as content.

**Before:**
> Here is an overview of the French Revolution. I hope this helps! Let me know if you'd like me to expand on any section.

**After:**
> The French Revolution began in 1789 when financial crisis and food shortages led to widespread unrest.


### 21. Knowledge-Cutoff Disclaimers

**Words to watch:** as of [date], Up to my last training update, While specific details are limited/scarce..., based on available information...

**Problem:** AI disclaimers about incomplete information get left in text.

**Before:**
> While specific details about the company's founding are not extensively documented in readily available sources, it appears to have been established sometime in the 1990s.

**After:**
> The company was founded in 1994, according to its registration documents.


### 22. Sycophantic/Servile Tone

**Problem:** Overly positive, people-pleasing language.

**Before:**
> Great question! You're absolutely right that this is a complex topic. That's an excellent point about the economic factors.

**After:**
> The economic factors you mentioned are relevant here.


## FILLER AND HEDGING

### 23. Filler Phrases

**Before → After:**
- "In order to achieve this goal" → "To achieve this"
- "Due to the fact  it was raining" → "Because it was raining"
- "At this point in time" → "Now"
- "In the event that you need help" → "If you need help"
- "The system has the ability to process" → "The system can process"
- "It is important to note that the data shows" → "The data shows"


### 24. Excessive Hedging

**Problem:** Over-qualifying statements.

**Before:**
> It could potentially possibly be argued that the policy might have some effect on outcomes.

**After:**
> The policy may affect outcomes.


### 25. Generic Positive Conclusions

**Problem:** Vague ungs.

**Before:**
> The future looks bright for the company. Exciting times lie ahead as they continue their journey toward excellence. This represents a major step in the right direction.

**After:**
> The company plans to open two more locations next year.


### 26. Hyphenated Word Pair Overuse

**Words to watch:** third-party, cross-functional, client-facing, data-driven, decision-making, well-known, high-quality, real-time, long-term, end-to-end

**Problem:** AI hyphenates common word pairs with perfect consistency. Humans rarely hyphenate these uniformly, and when they do, it's inconsistent. Less common or technical compound modifiers are fine to hyphenate.

**Before:**
> The cross-functional team delivered a high-quality, data-driven report on our client-facing tools. Their decision-making process was well-known for being thorough and detail-oriented.

**After:**
> The cross functional team delivered a high quality, data driven report on our client facing tools. Their decision making process was known for being thorough and detail oriented.


### 27. Persuasive Authority Tropes

**Phrases to watch:** The real question is, at its core, in reality, what really matters, fundamentally, the deeper issue, the heart of the matter

**Problem:** LLMs use these phrases to pretend they are cutting through noise to some deeper truth, when the sentence that follows usually just restates an ordinary point with extra ceremony.

**Before:**
> The real question is whether teams can adapt. At its core, what really matters is organizational readiness.

**After:**
> The question is whether teams can adapt. That mostly depends on whether the organization is ready to change its habits.


### 28. Signposting and Announcements

**Phrases to watch:** Let's dive in, let's explore, let's break this down, here's what you need to know, now let's look at, without further ado

**Problem:** LLMs announce what they are about to do instead of doing it. This meta-commentary slows the writing down and gives it a tutorial-script feel.

**Before:**
> Let's dive into how caching works in Next.js. Here's what you need to know.

**After:**
> Next.js caches data at multiple layers, including request memoization, the data cache, and the router cache.


### 29. Fragmented Headers

**Signs to watch:** A heading followed by a one-line paragraph that simply restates the heading before the real content begins.

**Problem:** LLMs often add a generic sentence after a heading as a rhetorical warm-up. It usually adds nothing and makes the prose feel padded.

**Before:**
> ## Performance
>
> Speed matters.
>
> When users hit a slow page, they leave.

**After:**
> ## Performance
>
> When users hit a slow page, they leave.

---

## Process

1. Read the input text carefully
2. Identify all instances of the patterns above
3. Rewrite each problematic section
4. Ensure the revised text:
   - Sounds natural when read aloud
   - Varies sentence structure naturally
   - Uses specific details over vague claims
   - Maintains appropriate tone for context
   - Uses simple constructions (is/are/has) where appropriate
5. Present a draft humanized version
6. Prompt: "What makes the below so obviously AI generated?"
7. Answer briefly with the remaining tells (if any)
8. Prompt: "Now make it not obviously AI generated."
9. Present the final version (revised after the audit)

## Output Format

Provide:
1. Draft rewrite
2. "What makes the below so obviously AI generated?" (brief bullets)
3. Final rewrite
4. A brief summary of changes made (optional, if helpful)


## Full Example

**Before (AI-sounding):**
> Great question! Here is an essay on this topic. I hope this helps!
>
> AI-assisted coding serves as an enduring testament to the transformative potential of large language models, marking a pivotal moment in the evolution of software development. In today's rapidly evolving technological landscape, these groundbreaking tools—nestled at the intersection of research and practice—are reshaping how engineers ideate, iterate, and deliver, underscoring their vital role in modern workflows.
>
> At its core, the value proposition is clear: streamlining processes, enhancing collaboration, and fostering alignment. It's not just about autocomplete; it's about unlocking creativity at scale, ensuring that organizations can remain agile while delivering seamless, intuitive, and powerful experiences to users. The tool serves as a catalyst. The assistant functions as a partner. The system stands as a foundation for innovation.
>
> Industry observers have noted that adoption has accelerated from hobbyist experiments to enterprise-wide rollouts, from solo developers to cross-functional teams. The techgy has been featured in The New York Times, Wired, and The Verge. Additionally, the ability to generate documentation, tests, and refactors showcases how AI can contribute to better outcomes, highlighting the intricate interplay between automation and human judgment.
>
> - 💡 **Speed:** Code generation is significantly faster, reducing friction and empowering developers.
> - 🚀 **Quality:** Output quality has been enhanced through improved training, contributing to higher standards.
> - ✅ **Adoption:** Usage continues to grow, reflecting broader industry trends.
>
> While specific details are limited based on available information, it could potentially be argued that these tools mave some positive effect. Despite challenges typical of emerging technologies—including hallucinations, bias, and accountability—the ecosystem continues to thrive. In order to fully realize this potential, teams must align with best practices.
>
> In conclusion, the future looks bright. Exciting times lie ahead as we continue this journey toward excellence. Let me know if you’d like me to expand on any section!

**Draft rewrite:**
> AI coding assistants speed up some tasks. In a 2024 study by , developers using Codex completed simple functions 55% faster than a control group, but showed no improvement on debugging or architectural decisions.
>
> The tools are good at boilerplate: config files, test scaffolding, repetitive refactors. They are bad at knowing when they are wrong. I have mass-accepted suggestions that compiled, passed lint, and still did the wrong thing because I stopped paying attention.
>
> Mira, an engineer at a fintech startup I interviewed, said she uses Copilot "like autocomplete for boring code" but reviews every line before committing. Jake, a senior dev at a larger company, disabled it after it kept suggesting patterns from a deprecated internal library.
>
> The productivity claims are hard to verify. GitHub says Copilot users "accept 30% of suggestions," but acceptance is not correctness, and correctness is not value. The 2024 Uplevel study found no statistically significant difference in pull-request throughput between teams with and without AI assistants.
>
> None of this means the tools are useless. It means they are tools. They do not replace judgment, and they do not eliminate the need for tests. If you do not have tests, you cannot tell whether the suggestion is right.

**What makes the below so obviously AI generated?**
- The rhythm is still a bit too tidy (clean contrasts, evenly paced paragraphs).
- The named people and study citations can read like plausible-but-made-up placeholders unless they're real and sourced.
- The closer leans a touch slogan-y ("If you do not have tests...") rather than sounding like a person talking.

**Now make it not obviously AI generated.**
> AI coding assistants can make you faster at the boring parts. Not everything. Definitely not architecture.
>
> They're great at boilerplate: config files, test scaffolding, repetitive refactors. They're also great at sounding right while being wrong. I've accepted suggestions that compiled, passed lint, and still missed the point because I stopped paying attention.
>
> People I talk to tend to land in two camps. Some use it like autocomplete for chores and review every line. Others disable it after it keeps suggesting patterns they don't want. Both feel reasonable.
>
> The productivity metrics are slippery. GitHub can say Copilot users "accept 30% of suggestions," but acceptance isn't correctness, and correctness isn't value. If you don't have tests, you're basically guessing.

**Changes made:**
- Removed chatbot artifacts ("Great question!", "I hope this helps!", "Let me know if...")
- Removed significance inflation ("testament", "pivotal moment", "evolving landscape", "vital role")
- Removed promotional language ("groundbreaking", "nestled", "seamless, intuitive, and powerful")
- Removed vague attributions ("Industry observers")
- Removed superficial -ing phrases ("underscoring", "highlighting", "reflecting", "contributing to")
- Removed negative parallelism ("It's not just X; it's Y")
- Removed rule-of-three patterns and synonym cycling ("catalyst/partner/foundation")
- Removed false ranges ("from X to Y, from A to B")
- Removed em dashes, emojis, boldface headers, and curly quotes
- Removed copula avoidance ("serves as", "functions as", "stands as") in favor of "is"/"are"
- Removed formulaic challenges section ("Despite challenges... continues to thrive")
- Removed knowledge-cutoff hedging ("While specific details are limited...")
- Removed excessive hedging ("could potentially be argued that... might have some")
- Removed filler phrases and persuasive framing ("In order to", "At its core")
- Removed generic positive conclusion ("the future looks bright", "exciting times lie ahead")
- Made the voice more personal and less "assembled" (varied rhythm, fewer placeholders)


## Reference

This skill is based on [Wikipedia:Signs of AI writing](https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing), maintained by WikiProject AI Cleanup. The patterns documented there come from observations of thousands of instances of AI-generated text on Wikipedia.

Key insight from Wikipedia: "LLMs use statistical algorithms to guess what should come next. The result tends toward the most statistically likely result that applies to the widest variety of cases."
````

## File: .cursor/skills/metagit-bootstrap/scripts/bootstrap-config.zsh
````zsh
#!/usr/bin/env zsh
set -euo pipefail

ROOT="${1:-$PWD}"
FORCE="${2:-false}"
TARGET="$ROOT/.metagit.yml"

uv run python - "$ROOT" "$TARGET" "$FORCE" <<'PY'
import sys
from pathlib import Path
from metagit.core.config.manager import create_metagit_config
from metagit.core.config.manager import MetagitConfigManager

root = Path(sys.argv[1]).resolve()
target = Path(sys.argv[2]).resolve()
force = sys.argv[3].lower() in {"1", "true", "yes", "force"}

if target.exists() and not force:
    mgr = MetagitConfigManager(config_path=target)
    result = mgr.load_config()
    state = "valid" if not isinstance(result, Exception) else "invalid"
    print(f"status=exists\tvalidity={state}\tpath={target}")
    raise SystemExit(0)

yaml_out = create_metagit_config(name=root.name, kind="application", as_yaml=True)
if isinstance(yaml_out, Exception):
    print(f"status=error\tmessage={yaml_out}")
    raise SystemExit(1)

target.write_text(yaml_out, encoding="utf-8")
mgr = MetagitConfigManager(config_path=target)
result = mgr.load_config()
state = "valid" if not isinstance(result, Exception) else "invalid"
print(f"status=written\tvalidity={state}\tpath={target}")
PY
````

## File: .cursor/skills/metagit-bootstrap/SKILL.md
````markdown
---
name: metagit-bootstrap
description: Use when generating or refining local .metagit.yml files using deterministic discovery plus MCP sampling.
---

# Metagit MCP Bootstrap Skill

Use this skill to create a local `.metagit.yml` using discovery-driven prompts and MCP sampling.

## Purpose

Generate schema-compliant `.metagit.yml` files with high contextual quality while preserving safety and explicit user control.

## Local Script Wrapper (Use First)

Use this token-efficient wrapper for local bootstrap tasks:
- `./scripts/bootstrap-config.zsh [root_path] [force]`

Behavior:
- Writes `.metagit.yml` when missing
- Validates via Metagit config models
- Returns a compact status line for agents

## Workflow

1. Gather deterministic discovery data from the target repository:
   - source language/framework indicators
   - package/lock/build files
   - Dockerfiles and CI workflows
   - terraform files and module usage
2. Build a strict prompt package:
   - output format contract: valid YAML only
   - required schema fields and constraints
   - extracted discovery evidence
3. If sampling is supported, call `sampling/createMessage`.
4. Validate generated YAML with Metagit config models.
5. Retry with validation feedback up to a fixed max attempt count.
6. Return draft output and write only on explicit confirmation.

## Output Modes

- **Plan-only mode**: return prompt + discovery summary if sampling unavailable.
- **Draft mode**: return `.metagit.generated.yml` content.
- **Confirmed write mode**: write to `.metagit.yml` only with explicit parameter (`confirm_write=true`).

## Quality Bar

- Preserve discovered evidence in structured fields.
- Include workspace project and related repo entries where detectable.
- Avoid invented repositories or unverifiable dependencies.

## Safety Rules

- Never overwrite `.metagit.yml` silently.
- Never emit secrets in cleartext.
- Prefer placeholders for credentials or tokens.
````

## File: .cursor/skills/metagit-cli/metagit-cli/SKILL.md
````markdown
---
name: metagit-cli
description: CLI-only shortcuts for metagit agents — workspace catalog, discovery, prompts, sync, layout, and config. Use instead of MCP or HTTP API when operating from a shell or agent_mode session.
---

# Metagit CLI (agent shortcuts)

Use this skill when an agent should drive metagit **only through the `metagit` command**. Do not call MCP tools or `metagit api` from workflows covered here unless the user explicitly asks.

Set non-interactive defaults when automating:

```bash
export METAGIT_AGENT_MODE=true
```

Global flags (most commands):

- `-c path/to/metagit.config.yaml` — app config (default `metagit.config.yaml`)
- Workspace manifest: `--definition` / `-c` on catalog commands (default `.metagit.yml`)

---

## Prompt commands (all kinds)

List built-in prompt kinds:

```bash
metagit prompt list
metagit prompt list --json
```

Emit prompts (`--text-only` for paste into agent context; `--json` for structured output; `--no-instructions` to omit manifest layers):

| Scope | Command | Default kind |
|-------|---------|--------------|
| Workspace | `metagit prompt workspace -c <definition> -k <kind>` | `instructions` |
| Project | `metagit prompt project -p <project> -c <definition> -k <kind>` | `instructions` |
| Repo | `metagit prompt repo -p <project> -n <repo> -c <definition> -k <kind>` | `instructions` |

### Prompt kinds by scope

| Kind | Workspace | Project | Repo | Purpose |
|------|:---------:|:-------:|:----:|---------|
| `instructions` | yes | yes | yes | Composed `agent_instructions` from manifest layers |
| `session-start` | yes | — | — | Session bootstrap checklist |
| `catalog-edit` | yes | yes | — | Search-before-create; catalog registration |
| `health-preflight` | yes | yes | — | Pre-work workspace/repo status pass |
| `sync-safe` | yes | yes | yes | Guarded sync rules |
| `subagent-handoff` | — | yes | yes | Delegate single-repo work |
| `layout-change` | yes | yes | yes | Rename/move dry-run workflow |
| `repo-enrich` | — | — | yes | **Discover + merge** workspace repo entry |

### Prompt shortcuts (copy-paste)

```bash
# Session bootstrap
metagit prompt workspace -k session-start --text-only -c .metagit.yml

# Composed instructions at each level
metagit prompt workspace -k instructions --text-only -c .metagit.yml
metagit prompt project -p default -k instructions --text-only -c .metagit.yml
metagit prompt repo -p default -n my-api -k instructions --text-only -c .metagit.yml

# Repo catalog enrichment (detect + merge manifest entry)
metagit prompt repo -p default -n my-api -k repo-enrich --text-only -c .metagit.yml

# Catalog registration discipline
metagit prompt workspace -k catalog-edit --text-only -c .metagit.yml

# Safe sync reminder
metagit prompt repo -p default -n my-api -k sync-safe --text-only -c .metagit.yml

# Subagent handoff
metagit prompt repo -p default -n my-api -k subagent-handoff --text-only -c .metagit.yml
```

---

## Repo enrich workflow (`repo-enrich`)

Run the prompt, then execute its steps:

```bash
metagit prompt repo -p <project> -n <repo> -k repo-enrich --text-only -c .metagit.yml
```

Typical discovery chain on the checkout:

```bash
cd "$(metagit search '<repo>' -c .metagit.yml --path-only)"
metagit detect repository -p . -o json
metagit detect repo -p . -o yaml
metagit detect repo_map -p . -o json
```

Provider metadata (dry-run):

```bash
metagit project source sync --provider github --org <org> --mode discover --no-apply
```

After merging fields into `workspace.projects[].repos[]`:

```bash
metagit config validate -c .metagit.yml
metagit workspace repo list --project <project> --json
```

---

## Workspace and catalog

Per-project dedupe override in `.metagit.yml` (overrides `workspace.dedupe.enabled` in `metagit.config.yaml` for that project only):

```yaml
workspace:
  projects:
    - name: local
      dedupe:
        enabled: false
      repos: []
```

```bash
metagit appconfig show --format json
metagit config info -c .metagit.yml
metagit config show -c .metagit.yml
metagit config validate -c .metagit.yml

metagit workspace list -c .metagit.yml --json
metagit workspace project list -c .metagit.yml --json
metagit workspace repo list -c .metagit.yml --json
metagit workspace repo list -c .metagit.yml --project <name> --json

metagit workspace project add --name <name> --json
metagit workspace repo add --project <name> --name <repo> --url <url> --json
metagit workspace project remove --name <name> --json
metagit workspace repo remove --project <name> --name <repo> --json
```

Search managed repos (always before creating entries):

```bash
metagit search "<query>" -c .metagit.yml --json
metagit search "<query>" -c .metagit.yml --path-only
metagit search "<query>" -c .metagit.yml --tag tier=1 --project <name>
```

---

## Project operations

```bash
metagit project list --config .metagit.yml --all --json
metagit project add --name <name> --json
metagit project remove --name <name> --json
metagit project rename --name <old> --new-name <new> --dry-run --json
metagit project select
metagit project sync

metagit project repo list --json
metagit project repo add --project <name> --name <repo> --url <url>
metagit project repo remove --name <repo> --json
metagit project repo rename --name <old> --new-name <new> --dry-run --json
metagit project repo move --name <repo> --to-project <other> --dry-run --json
metagit project repo prune --project <name> --dry-run

metagit project source sync --provider github --org <org> --mode discover --no-apply
metagit project source sync --provider github --org <org> --mode additive --apply
```

Layout (manifest + disk; always dry-run first):

```bash
metagit workspace project rename --name <old> --new-name <new> --dry-run --json
metagit workspace repo rename --project <p> --name <old> --new-name <new> --dry-run --json
metagit workspace repo move --project <p> --name <repo> --to-project <other> --dry-run --json
```

---

## Discovery and local metadata

```bash
metagit detect project -p <path> -o yaml
metagit detect repo -p <path> -o yaml
metagit detect repo_map -p <path> -o json
metagit detect repository -p <path> -o json
metagit detect repository -p <path> -o metagit
# --save only with operator approval (blocked in agent_mode)
```

Bootstrap new trees:

```bash
metagit init --kind application
metagit init --kind umbrella --template hermes-orchestrator
```

---

## Selection and scope

```bash
metagit workspace select --project <name>
metagit project select
metagit project repo select
```

---

## Config and appconfig

```bash
metagit appconfig validate
metagit appconfig get workspace.path
metagit config example
metagit config schema
metagit config providers
```

---

## Records, skills, version

```bash
metagit record search "<query>"
metagit skills list
metagit skills show metagit-cli
metagit skills install --skill metagit-cli
metagit version
metagit info
```

---

## Agent habits

1. **Search before create** — `metagit search` then catalog add.
2. **Validate after manifest edits** — `metagit config validate`.
3. **Emit prompts instead of rewriting playbooks** — `metagit prompt … --text-only`.
4. **Enrich stale repo entries** — `metagit prompt repo … -k repo-enrich` then detect + merge.
5. **Dry-run layout** — always `--dry-run --json` before apply.
6. **Prefer `METAGIT_AGENT_MODE=true`** in CI and agent loops to skip fuzzy finder and confirm dialogs.

## Related bundled skills

Use topic skills when you need deeper playbooks (some mention MCP): `metagit-projects`, `metagit-workspace-scope`, `metagit-workspace-sync`, `metagit-config-refresh`. This skill is the **CLI-only** index and prompt reference.
````

## File: .cursor/skills/metagit-config-refresh/SKILL.md
````markdown
---
name: metagit-config-refresh
description: Refresh or bootstrap `.metagit.yml` using deterministic discovery and validation flows. Use when configuration is missing, stale, or incomplete for workspace operations.
---

# Refreshing Project Config

Use this skill to keep `.metagit.yml` accurate and operational.

## Workflow

1. Check workspace activation and config validity.
2. Run bootstrap plan mode first.
3. Review generated changes against expected workspace topology.
4. Apply config updates and validate schema before continuing.

## Command Wrapper

- `zsh ./skills/metagit-bootstrap/scripts/bootstrap-config.zsh [root_path] [mode] [seed_context]`

## Output Contract

Return:
- config health state before/after
- generated update summary
- any manual follow-up needed

## Safety

- Prefer plan/dry-run first for large config updates.
- Keep changes bounded to target workspace intent.
````

## File: .cursor/skills/metagit-gating/scripts/gate-status.zsh
````zsh
#!/usr/bin/env zsh
set -euo pipefail

ROOT="${1:-$PWD}"

uv run python - "$ROOT" <<'PY'
import os
import sys
from metagit.core.mcp.gate import WorkspaceGate
from metagit.core.mcp.root_resolver import WorkspaceRootResolver
from metagit.core.mcp.tool_registry import ToolRegistry

root = sys.argv[1]
resolver = WorkspaceRootResolver()
gate = WorkspaceGate()
registry = ToolRegistry()

resolved = resolver.resolve(cwd=os.path.abspath(root), cli_root=root)
status = gate.evaluate(root_path=resolved)
tools = registry.list_tools(status)
print(f"state={status.state.value}\troot={status.root_path or 'none'}\ttools={len(tools)}")
PY
````

## File: .cursor/skills/metagit-gating/SKILL.md
````markdown
---
name: metagit-gating
description: Use when implementing or operating Metagit MCP server activation and tool exposure rules based on .metagit.yml presence and validity.
---

# Metagit MCP Gating Skill

Use this skill whenever you need to control whether Metagit MCP tools/resources are exposed.

## Purpose

Ensure high-risk tooling and multi-repo context are only available when a valid `.metagit.yml` exists at the resolved workspace root.

## Local Script Wrapper (Use First)

Use this token-efficient wrapper for all gating checks:
- `./scripts/gate-status.zsh [root_path]`

Expected output (single line, tab-delimited):
- `state=<value>\troot=<path|none>\ttools=<count>`

## Activation Workflow

1. Resolve workspace root:
   - `METAGIT_WORKSPACE_ROOT`
   - CLI `--root`
   - upward directory walk
2. Check for `.metagit.yml` in resolved root.
3. Validate config through existing Metagit config models.
4. Derive activation state: missing, invalid, or active.
5. Register tool surface based on state.

## Tool Exposure Contract

### Inactive (missing or invalid config)
Expose only:
- `metagit_workspace_status`
- `metagit_bootstrap_config_plan_only`

### Active (valid config)
Expose full set:
- `metagit_workspace_status`
- `metagit_workspace_index`
- `metagit_workspace_search`
- `metagit_upstream_hints`
- `metagit_repo_inspect`
- `metagit_repo_sync`
- `metagit_bootstrap_config`

## Error Handling

- Return explicit, machine-readable state and reason.
- Avoid stack traces in user-facing outputs.
- Log parser/validation errors with enough detail for debugging.

## Safety Rules

- Never expose mutation-capable tools in inactive state.
- Never operate outside validated workspace boundaries.
- Keep defaults read-only unless user/agent explicitly opts in.
````

## File: .cursor/skills/metagit-gitnexus/scripts/analyze-targets.zsh
````zsh
#!/usr/bin/env zsh

set -euo pipefail

workspace_root="${1:-.}"
project_name="${2:-default}"

if [[ ! -f ".metagit.yml" ]]; then
  echo "ERROR: .metagit.yml not found in current directory"
  exit 2
fi

echo "analyze repo=$(pwd)"
npx gitnexus analyze

tmp_output="$(mktemp)"
uv run python - "$workspace_root" "$project_name" <<'PY' > "$tmp_output"
import sys
from pathlib import Path
import yaml

workspace_root = Path(sys.argv[1]).expanduser().resolve()
project_name = sys.argv[2]
cfg = yaml.safe_load(Path(".metagit.yml").read_text(encoding="utf-8")) or {}
workspace = (cfg.get("workspace") or {})
projects = workspace.get("projects") or []
target = next((p for p in projects if p.get("name") == project_name), None)
if not target:
    print(f"warn project_not_found={project_name}")
    raise SystemExit(0)

for repo in target.get("repos") or []:
    name = repo.get("name")
    if not name:
        continue
    repo_path = workspace_root / project_name / name
    if repo_path.exists() and repo_path.is_dir():
        print(f"repo_path={repo_path}")
    else:
        print(f"skip_missing={repo_path}")
PY

while IFS= read -r line; do
  case "$line" in
    repo_path=*)
      path="${line#repo_path=}"
      echo "analyze repo=${path}"
      (cd "$path" && npx gitnexus analyze) || echo "fail repo=${path}"
      ;;
    *)
      echo "$line"
      ;;
  esac
done < "$tmp_output"

rm -f "$tmp_output"
````

## File: .cursor/skills/metagit-multi-repo/SKILL.md
````markdown
---
name: metagit-multi-repo
description: Coordinate implementation tasks across multiple repositories using metagit status, search, and scoped sync workflows. Use when one objective spans several repositories.
---

# Coordinating Multi-Repo Implementation

Use this skill for cross-repository feature or fix delivery.

## Workflow

1. Define objective and affected repositories.
2. Verify workspace scope and dependency hints.
3. Sequence work by dependency order.
4. Sync only required repositories.
5. Track progress and blockers per repository.

## Command Wrapper

- `zsh ./skills/metagit-control-center/scripts/control-cycle.zsh [root_path] ["query"] [preset]`

## Output Contract

Return:
- objective-to-repository map
- execution order
- current blocker + next step

## Safety

- Keep scope bounded to configured workspace repositories.
- Prefer deterministic evidence for cross-repo assumptions.
````

## File: .cursor/skills/metagit-projects/SKILL.md
````markdown
---
name: metagit-projects
description: Ongoing workspace and project management for OpenClaw and Hermes agents. Use when starting work, organizing repos, or before creating a new project folder so existing metagit projects are reused instead of duplicated.
---

# Ongoing Project Management

Use this skill when the user starts new work, reorganizes repositories, or asks you to create a project folder. Metagit is the source of truth for what already exists in the workspace.

## Concepts

Metagit uses a three-level hierarchy (see project terminology docs):

| Level | Meaning |
|-------|---------|
| **Workspace** | Root folder where projects are synced (from app config `workspace.path`, often `./.metagit/`). Holds many projects. |
| **Project** | Named group of one or more Git repositories. Multi-repo products are one project; unrelated repos can also share a workspace under different project names. |
| **Repo** | A single Git repository entry under `workspace.projects[].repos` in `.metagit.yml`. |

A **project** is not always “one product.” It is whatever grouping helps the user and agents reason about related (or intentionally grouped) repositories. A workspace may contain unrelated projects side by side (for example `default`, `client-a`, `experiments`).

The umbrella `.metagit.yml` (workspace definition, often `kind: umbrella`) lives in a coordinating repository or central config checkout. Application repos may have their own `.metagit.yml` for metadata mode.

## Mandatory: check before creating folders

**Never** create a new project directory or clone into the workspace until you have checked metagit for an existing match.

1. **Locate the workspace definition**
   - Prefer the user’s umbrella `.metagit.yml` if known.
   - Otherwise use `.metagit.yml` in the current repo with `--definition /path/to/.metagit.yml`.

2. **List configured projects and repo counts**
   ```bash
   metagit config info --config-path /path/to/.metagit.yml
   metagit project list --config /path/to/.metagit.yml --project default
   ```
   Repeat `--project` for each project name returned by `config info`.

3. **Search managed repos by name, URL fragment, or tag**
   ```bash
   metagit search "<proposed-name-or-url>" --definition /path/to/.metagit.yml
   metagit search "<name>" --definition /path/to/.metagit.yml --json
   ```

4. **Inspect on disk** (workspace path from app config, default `./.metagit/`)
   - Expected layout: `{workspace.path}/{project_name}/{repo_name}/`
   - If the directory already exists, **reuse it**; do not create a parallel tree.

5. **Decide**
   - **Match found** → use existing project/repo; run `metagit project sync` only if the user wants checkouts refreshed.
   - **No match** → proceed with registration steps below (still add to workspace; do not leave orphan folders).

## Registering new work in the workspace

### New repository in an existing project

From the directory containing the workspace `.metagit.yml` (or pass `--config`):

```bash
metagit project repo add --project <project_name> --prompt
# or non-interactive:
metagit project repo add --project <project_name> --name <repo> --url <git-url>
metagit config validate --config-path .metagit.yml
metagit project sync --project <project_name>
```

In the new application repo (if applicable):

```bash
cd /path/to/new/repo
metagit init
metagit detect repo --force   # optional: enrich .metagit.yml
```

### New project group (new `workspace.projects[]` entry)

There is no separate `project create` CLI today. Add a project block to `.metagit.yml`:

```yaml
workspace:
  projects:
    - name: my-new-project
      description: Short purpose for agents and humans
      repos: []
```

Then validate, add repos, and sync:

```bash
metagit config validate --config-path .metagit.yml
metagit project repo add --project my-new-project --prompt
metagit project sync --project my-new-project
```

Choose a **distinct project name**; avoid duplicating an existing `workspace.projects[].name`.

### New umbrella workspace

When bootstrapping a workspace coordinator repo:

```bash
metagit init --kind umbrella
metagit project repo add --project default --prompt
metagit project sync
```

## Ongoing session habits

At the start of sustained work:

1. Run **metagit-workspace-scope** (or `metagit mcp serve --status-once` when MCP is available).
2. Confirm **active project** matches the user’s intent (`metagit workspace select --project <name>` when switching).
3. Use **`metagit search`** before assuming a repo is missing or lives elsewhere.
4. For multi-repo tasks, prefer **metagit-control-center** or **metagit-multi-repo** over ad-hoc cloning.

When the user names a target folder:

- Resolve it against managed config first.
- If unmanaged but present on disk under the project sync folder, report it and offer to add via `metagit project repo add` rather than recreating.

## OpenClaw and Hermes setup

Install bundled skills (including this one) for agent hosts:

```bash
metagit skills list
metagit skills install --scope user --target openclaw --target hermes
metagit mcp install --scope user --target openclaw --target hermes
```

Use `--scope project` when installing into a specific umbrella repository checkout.

## Output contract

After project-management actions, report:

- workspace definition path used
- whether the target was **existing** or **newly registered**
- project name and repo name(s) affected
- sync status if `project sync` was run
- recommended next command (`workspace select`, `project select`, or `detect`)

## Safety

- Do not clone, delete, or overwrite sync directories without explicit user approval.
- Do not edit `.metagit.yml` without validating afterward (`metagit config validate`).
- Prefer reusing configured repos over creating duplicate checkouts.
- Keep unrelated experiments in separate `workspace.projects` entries when the user wants clear boundaries.
````

## File: .cursor/skills/metagit-upstream-scan/scripts/upstream-scan.zsh
````zsh
#!/usr/bin/env zsh
set -euo pipefail

ROOT="${1:-$PWD}"
QUERY="${2:-}"
PRESET="${3:-}"
MAX_RESULTS="${4:-20}"

if [[ -z "$QUERY" ]]; then
  echo "status=error\tmessage=query-required"
  exit 1
fi

uv run python - "$ROOT" "$QUERY" "$PRESET" "$MAX_RESULTS" <<'PY'
import sys
from pathlib import Path
from metagit.core.config.manager import MetagitConfigManager
from metagit.core.mcp.services.workspace_index import WorkspaceIndexService
from metagit.core.mcp.services.workspace_search import WorkspaceSearchService
from metagit.core.mcp.services.upstream_hints import UpstreamHintService

root = Path(sys.argv[1]).resolve()
query = sys.argv[2]
preset = sys.argv[3] or None
max_results = int(sys.argv[4])

manager = MetagitConfigManager(config_path=root / ".metagit.yml")
cfg = manager.load_config()
if isinstance(cfg, Exception):
    print(f"status=error\tmessage=config-invalid\tdetail={cfg}")
    raise SystemExit(1)

index = WorkspaceIndexService().build_index(config=cfg, workspace_root=str(root))
repo_paths = [row["repo_path"] for row in index if row.get("exists")]
search_hits = WorkspaceSearchService().search(query=query, repo_paths=repo_paths, preset=preset, max_results=max_results)
ranked = UpstreamHintService().rank(blocker=query, repo_context=index)[:5]

print(f"status=ok\trepos={len(index)}\thits={len(search_hits)}")
for row in ranked:
    print(f"hint\trepo={row['repo_name']}\tscore={row['score']}")
for hit in search_hits[:5]:
    print(f"hit\tfile={hit['file_path']}\tline={hit['line_number']}")
PY
````

## File: .cursor/skills/metagit-upstream-scan/SKILL.md
````markdown
---
name: metagit-upstream-scan
description: Use when a coding agent encounters likely upstream blockers and must find related workspace repositories, files, and probable root causes.
---

# Metagit Upstream Discovery Skill

Use this skill when the current repo does not appear to contain the full fix and related repositories may hold the source issue.

## Supported Use Cases

- Missing Terraform input in a shared module
- Docker base image/version mismatch across repos
- Shared infrastructure definitions causing local failures
- CI pipeline breakages tied to upstream templates/workflows

## Local Script Wrapper (Use First)

Use this token-efficient wrapper for upstream discovery tasks:
- `./scripts/upstream-scan.zsh [root_path] "<query>" [preset] [max_results]`

Output format:
- compact status line
- top ranked repo hints (`hint`)
- top search file hits (`hit`)

## Workflow

1. Read workspace repository map from active `.metagit.yml`.
2. Run `metagit_workspace_index` to verify repo availability and sync state.
3. Use `metagit_workspace_search` with category preset (`terraform`, `docker`, `infra`, `ci`).
4. Use `metagit_upstream_hints` to rank candidate repositories and files.
5. Return a concise action plan:
   - top candidate repos
   - likely files/definitions
   - whether sync is needed before deeper analysis

## Search Strategy

- Start narrow with issue-specific terms (error, module, variable, image tag).
- Expand to broader shared terms if no hits.
- Prefer repositories referenced by workspace metadata before searching unknown repos.

## Output Contract

Return:
- ranked candidates with rationale
- suggested next file openings
- confidence level and unresolved assumptions

## Safety Rules

- Restrict search to configured workspace repositories.
- Cap result size and duration.
- Keep this workflow read-only unless an explicit sync action is requested.
````

## File: .cursor/skills/metagit-upstream-triage/SKILL.md
````markdown
---
name: metagit-upstream-triage
description: Triage cross-repository blockers by ranking likely upstream repositories and files with metagit search and hinting tools. Use when local fixes appear incomplete.
---

# Triaging Upstream Blockers

Use this skill for failures likely rooted in another repository.

## Workflow

1. Run workspace index and search with issue-specific terms.
2. Run upstream hint ranking to prioritize repositories/files.
3. Open the top candidates and validate root-cause evidence.
4. Return a short fix path (repo, file, next action).

## Command Wrapper

- `zsh ./skills/metagit-upstream-scan/scripts/upstream-scan.zsh [root_path] "<query>" [preset] [max_results]`

## Output Contract

Return:
- ranked candidate repositories
- probable root-cause files
- confidence and assumptions

## Safety

- Keep this flow read-only unless sync is explicitly requested.
````

## File: .github/workflows/docker.yaml
````yaml
name: Docker Image

on:
  push:
  workflow_dispatch:

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository_owner }}/metagit-cli

jobs:
  image-deployment:
    name: Publish Docker Image - ${{ github.ref_name }}
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      id-token: write
      attestations: write
      
    steps:
      - name: Checkout repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0
          persist-credentials: false

      - name: Set version
        id: version
        run: |
          if [[ "${{ github.ref_type }}" == "tag" ]]; then
            VERSION=${GITHUB_REF#refs/tags/}
          else
            VERSION=dev-$(git rev-parse --short HEAD)
          fi
          echo "VERSION=$VERSION" >> $GITHUB_ENV
          echo "version=$VERSION" >> $GITHUB_OUTPUT
          echo "Version: $VERSION"

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
      
      - name: Log in to Container Registry
        if: github.event_name != 'pull_request'
        uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}}
            type=raw,value=latest,enable={{is_default_branch}}

      - name: Build and push Docker image
        uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
        id: build
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: ${{ github.event_name != 'pull_request' && github.ref_type == 'tag' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          build-args: DEPLOY_VERSION=${{ steps.version.outputs.version }}


      - name: Generate artifact attestation
        if: github.event_name != 'pull_request' && github.ref_type == 'tag'
        uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
        with:
          subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          subject-digest: ${{ steps.build.outputs.digest }}
          push-to-registry: true

      - name: Logout from Docker Registry
        run: docker logout ${{ env.REGISTRY }}
````

## File: .github/workflows/docs.yaml
````yaml
name: Docs

on:
  push:
    branches:
      - main
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  build-and-deploy-docs:
    name: Build and Deploy Docs - ${{ github.ref_name }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

      - name: Set up Python
        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
        with:
          python-version: "3.13"

      - name: Install uv
        run: |
          curl -LsSf https://astral.sh/uv/install.sh | sh
          source "$HOME/.cargo/env"
          
      - name: Install dependencies
        run: |
          uv venv
          uv pip install --system -e ".[docs]"

      - name: Build docs
        run: mkdocs build

      - name: Upload artifact
        uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
        with:
          path: site

      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
````

## File: .mex/context/decisions.md
````markdown
---
name: decisions
description: Key architectural and technical decisions with reasoning. Load when making design choices or understanding why something is built a certain way.
triggers:
  - "why do we"
  - "why is it"
  - "decision"
  - "alternative"
  - "we chose"
edges:
  - target: context/architecture.md
    condition: when a decision relates to system structure
  - target: context/stack.md
    condition: when a decision relates to technology choice
  - target: context/mcp-runtime.md
    condition: when decisions involve MCP capability scope, transport, or gating
last_updated: 2026-05-05
---

# Decisions

## Decision Log

### Keep project intelligence in repository-local `.metagit.yml`
**Date:** 2026-05-05  
**Status:** Active  
**Decision:** Project/workspace situational awareness is modeled in `.metagit.yml` and related schema/model files.  
**Reasoning:** Keeps context version-controlled, shareable, and inspectable by both humans and agents without external state dependencies.  
**Alternatives considered:** External-only metadata store (rejected — higher setup burden and weaker local/offline UX), ad-hoc markdown notes (rejected — no schema/validation guarantees).  
**Consequences:** Changes to project/workspace metadata must preserve model/schema compatibility and validation paths.

### Use Python CLI + core service modules as primary architecture
**Date:** 2026-05-05  
**Status:** Active  
**Decision:** Core functionality stays in Python modules under `src/metagit/core/*`, with Click command wrappers under `src/metagit/cli/commands/*`.  
**Reasoning:** Separates UX routing from business logic and keeps features testable in isolation.  
**Alternatives considered:** Fat command handlers (rejected — logic duplication and weaker test boundaries), full web-service-first architecture (rejected — project is CLI-first).  
**Consequences:** New features should be implemented in core services/managers with thin command glue.

### Gate MCP capabilities by valid workspace configuration
**Date:** 2026-05-05  
**Status:** Active  
**Decision:** MCP tool/resource surface is state-aware and only fully enabled when a valid `.metagit.yml` is present at resolved workspace root.  
**Reasoning:** Prevents unsafe or context-poor actions and aligns multi-repo operations with explicit workspace declarations.  
**Alternatives considered:** Always-on toolset (rejected — increases misuse risk), hard failure when config missing (rejected — poorer diagnostic/bootstrapping UX).  
**Consequences:** MCP runtime must keep gate checks, limited inactive tooling, and explicit mutation guardrails for sync operations.
````

## File: .mex/context/setup.md
````markdown
---
name: setup
description: Dev environment setup and commands. Load when setting up the project for the first time or when environment issues arise.
triggers:
  - "setup"
  - "install"
  - "environment"
  - "getting started"
  - "how do I run"
  - "local development"
edges:
  - target: context/stack.md
    condition: when specific technology versions or library details are needed
  - target: context/architecture.md
    condition: when understanding how components connect during setup
  - target: context/conventions.md
    condition: when setup issues are caused by project-specific command/style rules
last_updated: 2026-05-05
---

# Setup

## Prerequisites
- Python 3.12+ (project requires `>=3.12`).
- `uv` package/environment manager (used for install/run/test/lint commands).
- `task` CLI (Taskfile runner used for standard project workflows).
- Git (required for repository features and many metagit operations).

## First-time Setup
1. `./configure.sh`
2. `task install`
3. `uv pip install -e ".[test]"`
4. `task lint`
5. `task test`

## Environment Variables
- `GITHUB_PERSONAL_ACCESS_TOKEN` (conditionally required) — used by `task start:mcp` for GitHub MCP server container.
- `LEGACY_YAML_LOADER` (optional) — toggles legacy duplicate-key behavior in custom YAML loader.
- `.env` / `.SECRETS.env` (optional/conditional) — loaded by Taskfile; values depend on enabled workflows/providers.

## Common Commands
- `task lint` — runs `ruff check` and `ruff format --check`.
- `task format` — formats code with `ruff format`.
- `task test` — installs test extras and runs pytest suite.
- `uv run metagit --help` — verifies CLI entrypoint and available commands.
- `uv run pytest tests/core/mcp -v` — focused MCP runtime/service regression suite.
- `task docs` — builds docs with mkdocs after schema generation.

## Common Issues
**`task lint` fails after runtime edits:** run `uv run ruff format src/metagit/core/mcp/runtime.py` then rerun `task lint`.  
**MCP command blocks in tests/manual checks:** use `metagit mcp serve --status-once` for non-blocking state diagnostics.  
**Config validation failures during workspace/MCP flows:** validate and inspect `.metagit.yml` shape via `uv run metagit config validate` and model-driven fixes.  
**Tooling mismatch after dependency changes:** run `uv sync` then rerun `task install` to restore expected environment.
````

## File: .mex/context/stack.md
````markdown
---
name: stack
description: Technology stack, library choices, and the reasoning behind them. Load when working with specific technologies or making decisions about libraries and tools.
triggers:
  - "library"
  - "package"
  - "dependency"
  - "which tool"
  - "technology"
edges:
  - target: context/decisions.md
    condition: when the reasoning behind a tech choice is needed
  - target: context/conventions.md
    condition: when understanding how to use a technology in this codebase
  - target: context/setup.md
    condition: when stack choices affect local tooling or command execution
  - target: context/mcp-runtime.md
    condition: when working with MCP JSON-RPC transport, tools, and resources
last_updated: 2026-05-05
---

# Stack

## Core Technologies
- **Python 3.12** — primary language runtime (`requires-python >=3.12` in `pyproject.toml`).
- **Click** — CLI framework used by `metagit.cli.main` and command modules.
- **Pydantic v2** — schema and model validation for config/app/workspace/records.
- **uv** — package/environment and command runner used across Taskfile and setup.
- **Taskfile (`task`)** — command orchestration layer for lint/test/build/docs workflows.

## Key Libraries
- **`pydantic`** — all structured config/workspace records are modeled and validated with explicit models.
- **`PyYAML` + custom loader (`metagit.core.utils.yaml_class`)** — YAML parsing with include/envvar and duplicate-key behavior.
- **`GitPython`** — repository metadata and sync operations instead of raw shell git calls in core logic.
- **`pytest`** — primary testing framework for unit/integration tests in `tests/`.
- **`ruff`** — primary lint + format tool for current local workflow (`task lint`, `task format`).
- **`litellm` / `crewai`** — agent-oriented integrations present in dependencies and examples for AI-assisted flows.

## What We Deliberately Do NOT Use
- No JavaScript/TypeScript runtime as the primary execution path; core implementation is Python CLI/service modules.
- No monolithic web framework dependency (e.g., Django/FastAPI app server) for core product behavior.
- No direct subprocess-heavy git orchestration as the primary pattern where GitPython is available.

## Version Constraints
- Python must be 3.12+ for supported runtime behavior.
- Pydantic is v2-style; legacy v1 patterns should not be introduced.
- MCP protocol version emitted by runtime initialize response is `2024-11-05`.
````

## File: .mex/patterns/add-cli-command.md
````markdown
---
name: add-cli-command
description: Add or extend a metagit CLI command with core logic boundaries and test coverage.
triggers:
  - "add command"
  - "click command"
  - "new subcommand"
edges:
  - target: context/architecture.md
    condition: when deciding where command logic belongs vs core service logic
  - target: context/conventions.md
    condition: when implementing command/module naming and verify checklist items
  - target: patterns/debug-mcp-runtime.md
    condition: when the new command affects MCP runtime behavior and tests fail
last_updated: 2026-05-05
---

# Add CLI Command

## Context
Load `context/architecture.md` and `context/conventions.md` first. Confirm whether the task is a new command file under `src/metagit/cli/commands/` or an extension of existing command behavior.

## Steps
1. Create or update command module in `src/metagit/cli/commands/`.
2. Keep handler thin: instantiate/call core service/manager classes in `src/metagit/core/*`.
3. Register command in `src/metagit/cli/main.py` if it is a top-level subcommand.
4. Add command tests under `tests/cli/commands/` and integration tests if behavior crosses modules.
5. Run focused tests first, then project lint.

## Gotchas
- Putting substantial logic directly in Click handlers increases duplication and weakens testing boundaries.
- Forgetting registration in `main.py` makes command exist in code but unavailable in CLI.
- Command output contracts should remain stable (especially in automation-facing flows).

## Verify
- [ ] Command appears in `uv run metagit --help` or relevant group help output.
- [ ] Unit/CLI tests cover new path.
- [ ] Core behavior test exists if command bridges multiple services.
- [ ] `task lint` passes.

## Debug
- If command is missing: check `cli.add_command(...)` registration in `src/metagit/cli/main.py`.
- If context/logger missing: verify `@click.pass_context` and `ctx.obj` usage path.
- If tests hang on MCP command: use `--status-once` in test paths.

## Update Scaffold
- [ ] Update `.mex/ROUTER.md` "Current Project State" if what's working/not built has changed
- [ ] Update any `.mex/context/` files that are now out of date
- [ ] If this is a new task type without a pattern, create one in `.mex/patterns/` and add to `INDEX.md`
````

## File: .mex/patterns/add-managed-repo-search.md
````markdown
---
name: add-managed-repo-search
description: Extend or debug managed-only repo lookup across CLI, MCP, and the local JSON API.
triggers:
  - "managed repo search"
  - "metagit_repo_search"
  - "repos/search"
  - "ManagedRepoSearchService"
edges:
  - target: context/architecture.md
    condition: when changing how search flows connect CLI, MCP, and HTTP surfaces
  - target: context/mcp-runtime.md
    condition: when adjusting MCP tool schema or dispatch for managed repo search
  - target: patterns/add-mcp-tool.md
    condition: when only the MCP tool contract or registry changes
last_updated: 2026-05-12
---

# Add or change managed repo search

## Context
Managed repo search is **only** the repos declared under `workspace.projects[].repos` in `.metagit.yml`. It does not scan the filesystem for unmanaged clones. Shared logic lives in `src/metagit/core/project/search_service.py` (`ManagedRepoSearchService`) and index rows from `WorkspaceIndexService.build_index`.

## Steps
1. Confirm ranking/filter behavior in `ManagedRepoSearchService` (`search` / `resolve_one`) and adjust tests in `tests/test_project_search_service.py`.
2. **CLI:** `src/metagit/cli/commands/search.py` — keep thin; delegate to the service + `MetagitConfigManager`.
3. **MCP:** register in `tool_registry.py`, add `inputSchema` + dispatch in `runtime.py` (`metagit_repo_search`); extend `tests/core/mcp/test_runtime.py` (and integration if gating changes).
4. **HTTP API:** `src/metagit/core/api/server.py` — GET handlers only; keep JSON stable; add tests under `tests/api/`.
5. Regenerate schema if `.metagit.yml` / models affecting tags change (`task generate:schema`).

## Gotchas
- Workspace root for path resolution is the **directory containing** `.metagit.yml`, not the file path itself.
- `metagit_workspace_search` (MCP) searches **paths on disk** inside the workspace; `metagit_repo_search` searches **configured managed repos** — different semantics; do not merge without updating docs and clients.
- HTTP `409` on `/v1/repos/resolve` is expected for `ambiguous_match`; clients must handle it (e.g. `urllib` raises `HTTPError`).

## Verify
- [ ] `uv run pytest tests/test_project_search_service.py tests/cli/commands/test_search.py tests/core/mcp/test_runtime.py tests/api/test_repo_search_api.py -q`
- [ ] `task lint` and `task test` green after broader changes.

## Debug
- Empty MCP/API results: gate inactive, invalid config, or `workspace_root` mismatch vs repo `path:` entries.
- CLI vs API mismatch: compare `--definition` parent directory to API `--root`.

## Update Scaffold
- [ ] Update `docs/cli_reference.md` if flags or endpoints change.
- [ ] Update `.mex/ROUTER.md` if user-visible surfaces change materially.
````

## File: .mex/patterns/add-mcp-tool.md
````markdown
---
name: add-mcp-tool
description: Add a new MCP tool to metagit runtime with schema, dispatch, gating, and tests.
triggers:
  - "add mcp tool"
  - "tools/list"
  - "tools/call"
edges:
  - target: context/mcp-runtime.md
    condition: when implementing runtime method handlers, schemas, and dispatch logic
  - target: context/conventions.md
    condition: when validating guardrails and verify checklist compliance
  - target: patterns/debug-mcp-runtime.md
    condition: when framed message flow or tool calls produce protocol errors
last_updated: 2026-05-05
---

# Add MCP Tool

## Context
Load `context/mcp-runtime.md` first. The source of truth is `src/metagit/core/mcp/runtime.py` plus supporting services under `src/metagit/core/mcp/services/`.

## Steps
1. Define tool name and argument contract.
2. Add `inputSchema` entry to runtime `_tool_schemas`.
3. Ensure tool name is included in state-appropriate registry in `tool_registry.py`.
4. Implement dispatch branch in `_dispatch_tool` (or call a new dedicated service method).
5. Add tests in `tests/core/mcp/test_runtime.py` for list + call + invalid args.
6. Add/adjust integration tests if state-gating behavior changes.

## Gotchas
- If schema and dispatcher validation drift, clients may pass checks but fail at runtime.
- New tools must respect gate state; inactive state should not expose high-risk operations.
- Return content should be JSON-serializable and stable for agent consumption.

## Verify
- [ ] Tool is visible in `tools/list` only in intended states.
- [ ] `tools/call` succeeds with valid args and returns structured content.
- [ ] Invalid args map to `-32602` with `invalid_arguments` data.
- [ ] `task lint` and MCP test suite pass.

## Debug
- For missing tool in list: check `ToolRegistry` and state snapshot logic.
- For call rejection: inspect gate state and allowed tool set.
- For protocol mismatch: inspect framed read/write handling and JSON envelope fields.

## Update Scaffold
- [ ] Update `.mex/ROUTER.md` "Current Project State" if what's working/not built has changed
- [ ] Update any `.mex/context/` files that are now out of date
- [ ] If this is a new task type without a pattern, create one in `.mex/patterns/` and add to `INDEX.md`
````

## File: .mex/patterns/bootstrap-metagit-config.md
````markdown
---
name: bootstrap-metagit-config
description: Create or repair `.metagit.yml` using model validation and optional MCP sampling.
triggers:
  - "bootstrap config"
  - "generate .metagit.yml"
  - "metagit config invalid"
edges:
  - target: context/setup.md
    condition: when environment/command prerequisites impact config generation
  - target: context/stack.md
    condition: when config/schema model constraints need confirmation
  - target: context/mcp-runtime.md
    condition: when bootstrap is performed through MCP tool calls and sampling flow
last_updated: 2026-05-05
---

# Bootstrap Metagit Config

## Context
Use this when `.metagit.yml` is missing or invalid, or when onboarding a new workspace. Relevant modules are `metagit.core.config.manager` and MCP bootstrap services.

## Steps
1. Check for existing `.metagit.yml` and validate (`uv run metagit config validate`).
2. If missing/invalid, create baseline config using manager/create flow or bootstrap wrapper.
3. Re-validate using model-driven load path (not string-only checks).
4. If running through MCP, use `metagit_bootstrap_config` and verify returned mode (`plan_only` vs `sampled`).
5. Keep writes explicit when replacing existing config.

## Gotchas
- Writing guessed YAML without model validation creates downstream gate failures.
- Overwriting a valid `.metagit.yml` silently can erase workspace repo mappings.
- Sampling output must still pass strict config model validation before use.

## Verify
- [ ] `.metagit.yml` loads through `MetagitConfigManager.load_config()` without exception.
- [ ] Workspace section shape is valid for project/repo operations.
- [ ] MCP gate moves to active when expected.
- [ ] `task lint` and relevant tests pass.

## Debug
- If validation fails: inspect exact model error path and adjust YAML fields/types.
- If gate remains inactive: verify resolver root points to intended workspace directory.
- If sampling output fails repeatedly: run plan-only mode and patch fields manually.

## Update Scaffold
- [ ] Update `.mex/ROUTER.md` "Current Project State" if what's working/not built has changed
- [ ] Update any `.mex/context/` files that are now out of date
- [ ] If this is a new task type without a pattern, create one in `.mex/patterns/` and add to `INDEX.md`
````

## File: .mex/patterns/debug-mcp-runtime.md
````markdown
---
name: debug-mcp-runtime
description: Diagnose MCP runtime failures across framing, initialize capabilities, tool dispatch, and resource reads.
triggers:
  - "mcp error"
  - "tools/call failed"
  - "resources/read failed"
  - "stdio framing"
edges:
  - target: context/mcp-runtime.md
    condition: when tracing runtime internals and protocol method handling
  - target: context/architecture.md
    condition: when failure source may be outside runtime (service/config layer)
  - target: patterns/add-mcp-tool.md
    condition: when bug appears while adding/changing tool schemas or dispatch behavior
last_updated: 2026-05-05
---

# Debug MCP Runtime

## Context
Primary files: `src/metagit/core/mcp/runtime.py`, `tool_registry.py`, gate/root resolver, and MCP service modules. Primary tests: `tests/core/mcp/test_runtime.py` and MCP integration tests.

## Steps
1. Reproduce with smallest path first (`metagit mcp serve --status-once` for gate snapshot).
2. Check gate state and allowed tool set before debugging individual tool behavior.
3. Validate request envelope fields (`jsonrpc`, `id`, `method`, `params`) and framed transport boundaries.
4. Run focused runtime tests, then full MCP suite.
5. If method-specific failure persists, add/adjust regression tests before patching.

## Gotchas
- Mistaking inactive-gate tool denial for runtime logic failure.
- Schema/dispatcher mismatch causing valid-looking requests to fail.
- Framing bugs can masquerade as random parse/protocol errors.

## Verify
- [ ] Runtime methods (`initialize`, `tools/list`, `tools/call`, `resources/*`) pass tests.
- [ ] Invalid arguments consistently return `-32602`.
- [ ] Sampling capability detection reflects initialize input.
- [ ] MCP suite and lint pass after fix.

## Debug
- Use `tests/core/mcp/test_runtime.py` as first failure boundary.
- For tool visibility bugs, inspect `ToolRegistry.list_tools` with active/inactive statuses.
- For resource failures, inspect `ResourcePublisher` payload path and `uri` handling.

## Update Scaffold
- [ ] Update `.mex/ROUTER.md` "Current Project State" if what's working/not built has changed
- [ ] Update any `.mex/context/` files that are now out of date
- [ ] If this is a new task type without a pattern, create one in `.mex/patterns/` and add to `INDEX.md`
````

## File: .mex/patterns/debug-workspace-discovery.md
````markdown
---
name: debug-workspace-discovery
description: Diagnose missing/incorrect upstream repo discovery results in workspace index/search/hints flows.
triggers:
  - "upstream discovery"
  - "workspace search no results"
  - "hints look wrong"
edges:
  - target: context/architecture.md
    condition: when discovery issues involve config/workspace structure assumptions
  - target: context/mcp-runtime.md
    condition: when failures occur through MCP tool calls rather than direct service tests
  - target: patterns/bootstrap-metagit-config.md
    condition: when root issue is invalid or incomplete `.metagit.yml`
last_updated: 2026-05-05
---

# Debug Workspace Discovery

## Context
Discovery path spans `WorkspaceIndexService`, `WorkspaceSearchService`, and `UpstreamHintService`, usually driven by valid workspace config entries.

## Steps
1. Validate `.metagit.yml` and confirm `workspace.projects[].repos` is populated as expected.
2. Build index and inspect resolved `repo_path`, `exists`, and `sync` fields.
3. Check search inputs (`query`, `preset`, `max_results`) and whether target repo paths actually exist locally.
4. Inspect upstream hint scoring inputs (repo metadata + blocker text).
5. Run focused service tests and patch deterministic scoring/search behavior with regression tests.

## Gotchas
- Empty or invalid workspace repo definitions produce zero-index and cascade into no search/hints.
- Query term selection can be too strict or too broad depending on preset merge behavior.
- Local clone state (`exists=False`) can silently remove repos from search scope.

## Verify
- [ ] Index rows match expected repo definitions and path resolution.
- [ ] Search returns bounded hits from intended repositories/files.
- [ ] Hint ranking surfaces expected top candidates for blocker text.
- [ ] MCP tool path returns equivalent outputs when invoked via runtime.

## Debug
- If index is empty: fix config shape/path first.
- If hints rank poorly: inspect term overlap and score contributions.
- If MCP and direct service outputs differ: compare runtime dispatch argument parsing.

## Update Scaffold
- [ ] Update `.mex/ROUTER.md` "Current Project State" if what's working/not built has changed
- [ ] Update any `.mex/context/` files that are now out of date
- [ ] If this is a new task type without a pattern, create one in `.mex/patterns/` and add to `INDEX.md`
````

## File: .mex/patterns/mcp-cross-project-dependencies.md
````markdown
---
name: mcp-cross-project-dependencies
description: Map or extend MCP cross-project dependency graph tooling.
triggers:
  - "cross project dependencies"
  - "metagit_cross_project_dependencies"
edges:
  - target: context/mcp-runtime.md
    condition: when changing runtime schemas or services
  - target: patterns/mcp-project-context.md
    condition: when combining with project context switch workflows
last_updated: 2026-05-15
---

# MCP Cross-Project Dependencies

## Context
Load `context/mcp-runtime.md`. Core logic: `cross_project_dependencies.py`, `import_hint_scanner.py`, `gitnexus_registry.py`.

## Layers
1. **declared/ref** — `.metagit.yml` tags, `ProjectPath.ref`, root `dependencies` / `components`
2. **shared_config/url_match** — identical URLs or `configured_path` across projects
3. **imports** — `package.json`, `pyproject.toml`, `go.mod`, terraform module paths between local repos
4. **GitNexus status** — `~/.gitnexus/registry.json` + optional `npx gitnexus status` per repo (not full graph export)

## Steps
1. Extend collectors in `CrossProjectDependencyService._collect_edges`.
2. Add MCP schema + dispatch in `runtime.py` and `tool_registry.py`.
3. Mock `GitNexusRegistryAdapter` in unit tests to avoid slow `npx` calls.
4. Update `metagit-repo-impact` skill when agent workflow changes.

## Verify
- [ ] `uv run pytest tests/core/mcp/services/test_cross_project_dependencies.py -q`
- [ ] `docs/cli_reference.md` documents tool parameters
````

## File: .mex/patterns/mcp-project-context.md
````markdown
---
name: mcp-project-context
description: Add or operate MCP project context switch, session store, and workspace snapshot tools.
triggers:
  - "project context switch"
  - "workspace snapshot"
  - "metagit_project_context_switch"
edges:
  - target: context/mcp-runtime.md
    condition: when changing runtime schemas, dispatch, or services
  - target: patterns/add-mcp-tool.md
    condition: when adding another MCP tool in the same area
last_updated: 2026-05-15
---

# MCP Project Context

## Context
Load `context/mcp-runtime.md`. Core code lives in `session_store.py`, `project_context.py`, `workspace_snapshot.py`, and `context_models.py`.

## Steps
1. Extend Pydantic models in `context_models.py` for any new persisted fields.
2. Update `SessionStore` paths under `.metagit/sessions/` — never store secret values.
3. Implement behavior in `ProjectContextService` / `WorkspaceSnapshotService`; keep git restore out of scope (metadata only).
4. Register tool schema + dispatch in `runtime.py` and `tool_registry.py`.
5. Add unit tests under `tests/core/mcp/services/` and runtime tests in `test_runtime.py`.

## Gotchas
- `metagit_workspace_state_restore` does not checkout branches or reset dirty repos.
- `project_name` session filenames must match `^[\w.-]+$`.
- Env exports skip sensitive variable refs and session overrides are validated.

## Verify
- [ ] `uv run pytest tests/core/mcp/services/test_project_context.py tests/core/mcp/services/test_workspace_snapshot.py tests/core/mcp/test_runtime.py -q`
- [ ] Skills and `docs/cli_reference.md` mention new tools when behavior is user-visible.
````

## File: .mex/patterns/README.md
````markdown
# Patterns

This folder contains task-specific guidance — the things you would tell your agent if you were sitting next to it. Not generic instructions. Project-specific accumulated wisdom.

## How patterns get created

**During setup:** After the context/ files are populated, the agent generates starter patterns based on the project's actual stack, architecture, and conventions. These are stack-specific — a Flask API project gets different patterns than a React SPA or a CLI tool.

**Over time:** You or your agent add patterns as they emerge from real work — when something breaks, when a task has a non-obvious gotcha, when you've explained the same thing twice.

## What belongs here

A pattern file is worth creating when:
- A task type is common in this project and has a repeatable workflow
- There are integration gotchas between components that aren't obvious from code
- Something broke and you want to prevent it from breaking the same way again
- A verify checklist specific to one type of task would catch mistakes early

## When to skip a pattern

Default to generating a pattern. Only skip if:
- The exact same guidance is already in `context/conventions.md` with concrete examples
- The task truly has no project-specific gotchas (e.g. "how to write a for loop")

If in doubt, generate the pattern. A pattern that turns out to be obvious costs nothing. A missing pattern costs a broken codebase.

## Format

### Single-task pattern (one file = one task)

```markdown
---
name: [pattern-name]
description: [one line — what this pattern covers and when to use it]
triggers:
  - "[keyword that should trigger loading this file]"
edges:
  - target: "[related file path, e.g. context/conventions.md]"
    condition: "[when to follow this edge]"
last_updated: [YYYY-MM-DD]
---

# [Pattern Name]

## Context
[What to load or know before starting this task type]

## Steps
[The workflow — what to do, in what order]

## Gotchas
[The things that go wrong. What to watch out for.]

## Verify
[Checklist to run after completing this task type]

## Debug
[What to check when this task type breaks]

## Update Scaffold
- [ ] Update `.mex/ROUTER.md` "Current Project State" if what's working/not built has changed
- [ ] Update any `.mex/context/` files that are now out of date
- [ ] If this is a new task type without a pattern, create one in `.mex/patterns/` and add to `INDEX.md`
```

### Multi-section pattern (one file = multiple related tasks)

Use this when tasks share context but differ in steps. Each task gets its own
`## Task: ...` heading with sub-sections. The Context section is shared at the top.

```markdown
---
name: [pattern-name]
description: [one line — what this pattern file covers]
triggers:
  - "[keyword]"
edges:
  - target: "[related file path]"
    condition: "[when to follow this edge]"
last_updated: [YYYY-MM-DD]
---

# [Pattern Name]

## Context
[Shared context for all tasks in this file]

## Task: [First Task Name]

### Steps
[...]

### Gotchas
[...]

### Verify
[...]

## Task: [Second Task Name]

### Steps
[...]

## Update Scaffold
- [ ] Update `.mex/ROUTER.md` "Current Project State" if what's working/not built has changed
- [ ] Update any `.mex/context/` files that are now out of date
- [ ] If this is a new task type without a pattern, create one in `.mex/patterns/` and add to `INDEX.md`
```

Do NOT combine unrelated tasks into one file just to reduce file count.
Only group tasks that genuinely share context.

## How many patterns to generate

Do not use a fixed number. Generate one pattern per:
- Each major task type a developer does repeatedly in this project
- Each external dependency with non-obvious integration gotchas
- Each major failure boundary in the architecture flow

For a simple project this may be 3-4 files. For a complex project this may be 10-15.
Do not cap based on a number — cap based on whether the pattern adds real value.

## Pattern categories

Walk through each category below. For each one, check the relevant context files
and generate patterns for everything that applies to this project.

### Category 1 — Common task patterns

The repeatable tasks in this project. What does a developer do most often?

Derive from: `context/architecture.md` (what are the major components?) and
`context/conventions.md` (what patterns exist for extending them?)

Examples by project type:
- API: "add new endpoint", "add new model/entity", "add auth to a route"
- Frontend: "add new page/route", "add new component", "add form with validation"
- CLI: "add new command", "add new flag/option"
- Pipeline: "add new pipeline stage", "add new data source"
- SaaS: "add payment flow", "add user-facing feature", "add admin operation"

### Category 2 — Integration patterns

How to work with the external dependencies in this project.

Every entry in `context/stack.md` "Key Libraries" or `context/architecture.md`
"External Dependencies" that has non-obvious setup, gotchas, or failure modes
deserves a pattern. These are the most dangerous areas — the agent will
confidently write integration code that looks right but misses project-specific
configuration, error handling, or rate limiting.

Examples: "calling the payments API", "running database migrations",
"adding a new third-party service client", "configuring auth provider"

### Category 3 — Debug/diagnosis patterns

When something breaks, where do you look?

Derive from the architecture flow — each boundary between components is a
potential failure point. One debug pattern per major boundary.

Examples: "debug webhook failures", "debug pipeline stage failures",
"diagnose auth/permission issues", "debug background job failures"

### Category 4 — Deploy/release patterns

Only generate if `context/setup.md` reveals non-trivial deployment.

Examples: "deploy to staging", "rollback a release", "update environment config",
"run database migration in production"
````

## File: .mex/patterns/run-graphify-analysis.md
````markdown
---
name: run-graphify-analysis
description: Run focused `graphify` analysis on a repo or subtree, especially when a full-repo graph would be too large or noisy.
triggers:
  - "graphify"
  - "knowledge graph"
  - "graph report"
edges:
  - target: "context/architecture.md"
    condition: "when naming communities or interpreting cross-command relationships"
  - target: "context/conventions.md"
    condition: "when verifying scaffold updates after the graph run"
last_updated: 2026-05-11
---

# Run Graphify Analysis

## Context
- Use this when a user invokes `/graphify` or asks for a repo knowledge graph.
- Start with corpus detection before committing to a full run; this repo has generated/docs-heavy areas that can drown out the interesting command and core logic relationships.
- Prefer focused subtrees when the corpus is over the graphify warning threshold or when the user only needs one part of the codebase.

## Steps
1. Resolve a stable `graphify` Python interpreter and write it to `graphify-out/.graphify_python`.
2. Run `graphify.detect` first and summarize the corpus by file type.
3. If the corpus is over the graphify threshold, show the busiest subdirectories and ask the user which subtree to analyze.
4. Run structural extraction for code files. If the selected scope is code-only, skip semantic subagents and continue with the AST-only flow.
5. Build the graph, cluster it, then label communities with plain-language names before exporting HTML.
6. If the corpus is large enough for benchmarking, run `graphify benchmark`.
7. Save manifest/cost data, clean temporary graphify files, and surface the report's God Nodes, Surprising Connections, and Suggested Questions.

## Gotchas
- `uv run --with graphifyy` can resolve to a temporary interpreter path; use the installed `graphify` tool's shebang-backed Python for multi-step runs.
- Full-repo runs can be skewed by generated assets like `site/assets/*`; detect first instead of assuming `.` is the right scope.
- Code-only runs still produce useful graph structure and avoid unnecessary semantic extraction cost.
- Community labels matter; leaving generic `Community N` names makes the report and `graph.html` much harder to navigate.

## Verify
- Confirm `graphify-out/graph.html`, `graphify-out/GRAPH_REPORT.md`, and `graphify-out/graph.json` exist in the processed directory.
- Confirm the report includes named communities instead of placeholder community labels.
- If benchmark ran, capture its reduction summary for the user.
- Confirm cleanup removed temp extraction files while preserving the final outputs.

## Debug
- If interpreter resolution breaks between steps, regenerate `graphify-out/.graphify_python` from the installed `graphify` binary.
- If the graph is empty, inspect the detect output first; the scope may contain unsupported files or only skipped content.
- If the corpus crosses the threshold, do not push through the full run blindly; narrow the scope and rerun.
- If community names look mixed or misleading, inspect the top labels per community from `graph.json` before regenerating the report.

## Update Scaffold
- [ ] Update `.mex/ROUTER.md` "Current Project State" if what's working/not built has changed
- [ ] Update any `.mex/context/` files that are now out of date
- [x] If this is a new task type without a pattern, create one in `.mex/patterns/` and add to `INDEX.md`
````

## File: .mex/patterns/update-release-workflow.md
````markdown
---
name: update-release-workflow
description: Safely replace or adjust GitHub release automation when post-merge release runs fail.
triggers:
  - "release workflow failing after merge"
  - "replace release automation"
  - "github actions release pipeline"
edges:
  - target: "context/conventions.md"
    condition: "before final verification to ensure required checks are run"
  - target: "ROUTER.md"
    condition: "after completing workflow changes to keep project state current"
last_updated: 2026-05-08
---

# Update Release Workflow

## Context
- Identify the active release workflows under `.github/workflows/`.
- Locate known-good reference workflows (for example `.github/example/`).
- Confirm expected release trigger model: `main` push for tag generation, tag push for publishing.

## Steps
1. Compare current release workflow files against the known-good copies.
2. Replace release orchestration logic first (for example `release-please` to semantic-tag flow).
3. Ensure publish workflow gates package publishing on tag refs only.
4. Ensure artifact lifecycle is intact (`build` -> `publish-test` -> `publish-prod` -> GitHub release).
5. Remove deprecated/conflicting workflows so only one release orchestration path is active.
6. Update `.mex/ROUTER.md` project state to match the new release mechanism.

## Gotchas
- Do not leave both `release-please` and semantic-tag workflows enabled unless intentionally dual-tracked.
- Keep token usage consistent with repo secrets (`PAT_TOKEN` vs `GITHUB_TOKEN`) and branch protection.
- Align tag format expectations between semantic tag creator and publish workflow filters.

## Verify
- Confirm all edited workflow YAML files parse in GitHub Actions syntax.
- Confirm semantic release workflow triggers on `push` to `main`.
- Confirm publish workflow only publishes when `github.ref` is a tag.
- Confirm no obsolete release workflow file remains active.

## Debug
- If release automation fails immediately on merge, inspect missing secret or permissions scope first.
- If publish never runs, inspect tag format mismatch versus `startsWith(github.ref, 'refs/tags/...')`.
- If GitHub release creation fails, confirm release job has `contents: write`.

## Update Scaffold
- [x] Update `.mex/ROUTER.md` "Current Project State" if release behavior changed
- [x] Update relevant `.mex/patterns/*` for this task type
- [x] Add this pattern to `.mex/patterns/INDEX.md`
````

## File: .mex/config.json
````json
{
  "aiTools": [
    "cursor"
  ]
}
````

## File: docs/reference/workspace-layout-api.md
````markdown
# Workspace layout API (v2)

Rename and move operations for workspace projects and repositories. Intended for agents, CLI, MCP, and a future web UI.

## Sync root

Layout operations apply disk changes under `workspace.path` from app config (`metagit.config.yaml`). The HTTP server’s `--root` is the directory containing `.metagit.yml`.

## Endpoints

| Method | Path | Body |
|--------|------|------|
| POST | `/v2/projects/{from}/rename` | `{ "to_name": "apps" }` |
| POST | `/v2/repos/{project}/{repo}/rename` | `{ "to_name": "new-name" }` |
| POST | `/v2/repos/{project}/{repo}/move` | `{ "to_project": "platform" }` |

Query flags (or JSON body): `dry_run`, `manifest_only`, `force`, `no_update_sessions` (project rename only).

## Response shape

Same as catalog mutations:

```json
{
  "ok": true,
  "entity": "repo",
  "operation": "move",
  "project_name": "platform",
  "repo_name": "svc-a",
  "from_project": "portfolio",
  "to_project": "platform",
  "config_path": "/path/.metagit.yml",
  "data": {
    "dry_run": false,
    "manifest_changes": ["..."],
    "disk_steps": [{"action": "move", "source": "...", "target": "..."}],
    "warnings": [],
    "manifest_updated": true
  }
}
```

## CLI equivalents

```bash
metagit workspace project rename alpha apps --dry-run --json
metagit workspace repo rename -p alpha svc-a svc-b --json
metagit workspace repo move -p alpha -n svc-a --to-project beta --json
```

## MCP tools

- `metagit_workspace_project_rename`
- `metagit_workspace_repo_rename`
- `metagit_workspace_repo_move`

Always pass `dry_run: true` first when an agent is unsure about disk impact.
````

## File: docs/app.logic.md
````markdown
# Metagit CLI Application Logic

This document outlines the architecture and logic of the Metagit CLI application. It includes a Mermaid diagram to visualize the interaction between different components.

## Core Components

The application is built around a few core components:

*   **CLI (Click):** The command-line interface is implemented using the `click` library. The main entry point is in `src/metagit/cli/main.py`, which defines the main `cli` group and subcommands.
*   **Configuration:** The application uses a YAML-based configuration file (`metagit.config.yaml`) to manage settings. The schema for this file is defined in `schemas/metagit_config.schema.json`.
*   **Logging:** A unified logging system is implemented in `src/metagit/core/utils/logging.py` to provide consistent logging across the application.
*   **Subcommands:** The application is organized into several subcommands, each with its own module in `src/metagit/cli/commands`.

## Mermaid Diagram

```mermaid
graph TD
    A[User] --> B{metagit CLI};
    B --> C{main.py};
    C --> D{Click Library};
    D --> E{Subcommands};
    E --> F[detect];
    E --> G[appconfig];
    E --> H[project];
    E --> I[workspace];
    E --> J[config];
    E --> K[record];
    E --> L[init];
    C --> M{Configuration};
    M --> N[metagit.config.yaml];
    C --> O{Logging};
    O --> P[UnifiedLogger];
```

## Component Interaction

1.  The user interacts with the application through the `metagit` command-line interface.
2.  The `main.py` script is the entry point, which initializes the `click` CLI application.
3.  The `click` library parses the command-line arguments and invokes the appropriate subcommand.
4.  Each subcommand has its own dedicated module that contains the logic for that command.
5.  The application loads its configuration from `metagit.config.yaml`.
6.  Logging is handled by the `UnifiedLogger` class, which provides a consistent logging format.
````

## File: docs/architecture-diagram.py
````python
#! /usr/bin/env python3
⋮----
# Architecture Diagram Generator: Multi-Agent Git Project Graph
# Requires: uv sync --group diagrams  (optional dependency group in pyproject.toml)
⋮----
user = User("Developer / API Client")
⋮----
git_sources = [Github("GitHub"), Custom("GitLab", "./icons/gitlab.png")]
fetcher = Python("Git Fetcher")
parser = Python("Build Parser / ORT")
storage = Storage("Repo Cache")
⋮----
extractor = Python("Artifact Extractor")
normalizer = Python("Normalizer")
artifact_store = Storage("Artifact Metadata")
⋮----
graphdb = Neptune("Graph DB (Neo4j / Neptune)")
⋮----
agent_router = Python("Agent Router")
kafka = Kafka("Event Bus")
redis = Redis("Agent Memory")
⋮----
agents = [
⋮----
api = Python("FastAPI / GraphQL")
dashboard = Custom("UI Dashboard", "./icons/webapp.png")
````

## File: docs/hermes-iac-workspace-guide.md
````markdown
# Hermes agents and organization-wide IaC

This guide shows how a **Hermes** controller agent uses **Metagit** as the control plane for an entire organization's infrastructure-as-code (IaC) surface: Terraform, OpenTofu, Pulumi, policy repos, modules, and the application repos they provision.

Metagit does not replace Terraform or your cloud provider. It gives Hermes a **single, validated map** of every managed repository, plus MCP tools to search, inspect, sync, triage, and hand **layered instructions** to subagents working in specific repos.

---

## Roles in the stack

| Role | Responsibility |
|------|----------------|
| **Hermes (controller)** | Owns the user objective across many repos; reads workspace manifest; launches subagents with composed `agent_instructions`; never clones blindly. |
| **Metagit** | Source of truth for workspace layout, repo metadata, health, search, and guarded git operations. |
| **Subagents (workers)** | Execute in one repo or path at a time (module bump, policy fix, drift investigation) using instructions from the controller. |
| **Git + CI** | Actual IaC execution (`plan`/`apply`, policy checks) stay in each repository's pipelines. |

```mermaid
flowchart TB
  subgraph human [Human operator]
    U[Platform / SRE lead]
  end

  subgraph hermes [Hermes controller]
    H[Controller agent]
    H -->|composed instructions| S1[Subagent: networking]
    H -->|composed instructions| S2[Subagent: eks-modules]
    H -->|composed instructions| S3[Subagent: policy]
  end

  subgraph metagit [Metagit control plane]
    M[MCP + CLI]
    Y[".metagit.yml"]
    M --> Y
  end

  subgraph surface [IaC surface on disk]
    R1[terraform-live/]
    R2[modules/]
    R3[policy/]
    R4[gitops/]
  end

  U --> H
  H --> M
  M --> R1
  M --> R2
  M --> R3
  M --> R4
  S1 --> R1
  S2 --> R2
  S3 --> R3
```

---

## Mapping org IaC to Metagit

Think of one **umbrella workspace** that models the whole platform engineering estate.

```text
Organization IaC estate
└── Metagit workspace (umbrella .metagit.yml)
    ├── Project: platform-live          # Terragrunt / root modules per env
    │   ├── Repo: terraform-live-prod
    │   └── Repo: terraform-live-staging
    ├── Project: shared-modules         # Reusable TF modules
    │   ├── Repo: terraform-aws-vpc
    │   └── Repo: terraform-aws-eks
    ├── Project: policy                 # OPA, Sentinel, Checkov baselines
    │   └── Repo: org-policy
    └── Project: delivery               # Pipelines, Atlantis, runners
        └── Repo: cicd-terraform
```

On disk (typical layout under app config `workspace.path`, often `.metagit/`):

```text
.metagit/
├── platform-live/
│   ├── terraform-live-prod/
│   └── terraform-live-staging/
├── shared-modules/
│   ├── terraform-aws-vpc/
│   └── terraform-aws-eks/
├── policy/
│   └── org-policy/
└── delivery/
    └── cicd-terraform/
```

**Tags** on each `repos[]` entry (for example `tier: "1"`, `iac: "terraform"`, `env: "prod"`) let Hermes filter with `metagit_repo_search` / `metagit search` without scanning the filesystem.

---

## Layered `agent_instructions`

Instructions are optional at four levels. Hermes composes them when switching project context or launching a subagent focused on a repo.

| Layer | YAML location | Typical IaC content |
|-------|---------------|---------------------|
| **File** | top-level `agent_instructions` | Org-wide rules: naming, approval, no `apply` in prod without ticket. |
| **Workspace** | `workspace.agent_instructions` | How to use this umbrella: backends, state buckets, module sources. |
| **Project** | `workspace.projects[].agent_instructions` | Project boundary: "platform-live = Terragrunt only; never edit modules here." |
| **Repo** | `workspace.projects[].repos[].agent_instructions` | Repo-specific: "Subagent: only `env/prod/**`; run `terraform fmt` before commit." |

Legacy manifests may still use `agent_prompt`; Metagit accepts it on load and writes `agent_instructions` going forward.

**Example manifest excerpt:**

```yaml
name: acme-platform-iac
kind: umbrella
agent_instructions: |
  Controller: You manage Acme's IaC estate. Never run terraform apply.
  Coordinate subagents per repo. Always run plan-only unless user explicitly approves apply.

workspace:
  agent_instructions: |
    Remote state: s3://acme-tf-state/{project}/{env}.
    Module sources: git::ssh://git@github.com/acme/terraform-modules.git//...
  projects:
    - name: platform-live
      description: Live infrastructure roots per environment
      agent_instructions: |
        Use Terragrunt. Respect dependency order: network before compute.
      repos:
        - name: terraform-live-prod
          path: platform-live/terraform-live-prod
          url: git@github.com:acme/terraform-live-prod.git
          sync: true
          tags:
            iac: terraform
            env: prod
          agent_instructions: |
            Subagent scope: env/prod only. Required checks: fmt, validate, tflint.
        - name: terraform-live-staging
          path: platform-live/terraform-live-staging
          sync: true
          tags:
            iac: terraform
            env: staging
    - name: shared-modules
      agent_instructions: |
        Semver tag modules on release. No environment-specific values in modules/.
      repos:
        - name: terraform-aws-vpc
          path: shared-modules/terraform-aws-vpc
          sync: true
          tags:
            iac: terraform
            layer: module
```

**Composed output** (what subagents receive via `effective_agent_instructions`):

```text
[FILE]
Controller: You manage Acme's IaC estate...

---

[WORKSPACE]
Remote state: s3://acme-tf-state/...

---

[PROJECT]
Use Terragrunt. Respect dependency order...

---

[REPO]
Subagent scope: env/prod only...
```

---

## One-time setup for Hermes

Install the CLI and agent integrations on the host where Hermes runs (or in the umbrella repo CI image):

```bash
uv tool install metagit-cli
metagit version

# Bundled skills (control center, multi-repo, gitnexus, projects, …)
metagit skills install --scope user --target hermes

# MCP server for tool calls from Hermes
metagit mcp install --scope user --target hermes
```

Bootstrap the umbrella coordinator (if not already present):

```bash
cd /path/to/platform-umbrella
metagit init --kind umbrella
metagit project repo add --project platform-live --prompt
metagit config validate
metagit project sync
```

Point Hermes MCP at the workspace root (directory containing `.metagit.yml`):

```bash
metagit mcp serve --root /path/to/platform-umbrella
# or: metagit mcp serve --status-once   # verify gate is active
```

!!! note "Gate behavior"
    Until `.metagit.yml` exists and validates, MCP exposes only safe tools (`metagit_workspace_status`, bootstrap plan). Full workspace tools unlock when the gate is **active**.

---

## Controller session lifecycle

```mermaid
sequenceDiagram
  participant U as Operator
  participant H as Hermes controller
  participant M as Metagit MCP
  participant S as Subagent

  U->>H: Objective: bump VPC module in prod roots
  H->>M: metagit_workspace_status
  M-->>H: active gate, workspace root
  H->>M: metagit_workspace_health_check
  M-->>H: missing clones, stale branches, GitNexus
  H->>M: metagit_repo_search (tags/layer/module)
  M-->>H: terraform-aws-vpc, terraform-live-prod
  H->>M: metagit_cross_project_dependencies
  M-->>H: declared + import hints
  H->>M: metagit_project_context_switch (platform-live)
  M-->>H: effective_agent_instructions + repos
  H->>M: metagit_workspace_sync (dry_run / fetch)
  H->>S: Task + effective_agent_instructions (module repo)
  S->>S: Edit module, PR
  H->>S: Task + instructions (live repo, prod paths)
  S->>S: Bump version ref, plan-only
  H->>M: metagit_session_update (notes)
  H->>U: Summary + open PRs
```

### Step-by-step (controller checklist)

1. **Orient** — `metagit_workspace_status`, read `metagit://workspace/config` and `metagit://workspace/repos/status`.
2. **Hygiene** — `metagit_workspace_health_check` (clone missing repos, note drift/stale integration).
3. **Scope** — `metagit_repo_search` / `metagit search` with tags (`iac=terraform`, `env=prod`).
4. **Blast radius** — `metagit_cross_project_dependencies` from the module or live project; optional GitNexus `analyze` + semantic search for unknown callers.
5. **Focus** — `metagit_project_context_switch` with `project_name` and optional `primary_repo` for the first subagent.
6. **Refresh** — `metagit_workspace_sync` with `mode: fetch` (default); `pull`/`clone` only with explicit approval.
7. **Delegate** — Pass `effective_agent_instructions` and repo path to each subagent; use per-repo `agent_instructions` when tasks split across repos.
8. **Remember** — `metagit_session_update` before switching projects; optional `metagit_workspace_state_snapshot` for audit metadata.

---

## IaC-focused Metagit capabilities

### Discover and search

| Need | Tool | Notes |
|------|------|--------|
| Find all `.tf` under managed repos | `metagit_workspace_discover` | `intent: terraform` or `pattern: "**/*.tf"` |
| Grep module sources / backends | `metagit_workspace_search` | `preset: terraform`, `query: "module \""` |
| Concept-level search | `metagit_workspace_semantic_search` | Requires GitNexus index per repo |
| List repos by tag | `metagit_repo_search` | `tags`, `status`, `sync_enabled` |

```mermaid
flowchart LR
  subgraph discover [Discovery]
    D1[workspace_discover]
    D2[workspace_search preset=terraform]
    D3[semantic_search]
  end

  subgraph repos [Managed repos only]
    R[.metagit.yml registry]
  end

  R --> D1
  R --> D2
  R --> D3
```

### Upstream triage

When a subagent hits an error in one repo (provider version, shared module API), the controller uses **`metagit_upstream_hints`** with a blocker string instead of guessing which module repo to open.

Typical flow:

1. Subagent reports: `Error: Unsupported argument "xyz" in module vpc`.
2. Controller runs upstream hints + workspace search for `module "vpc"` / path hints.
3. Controller reassigns or spawns subagent on `terraform-aws-vpc` with module-level `agent_instructions`.

### Health and drift

`metagit_workspace_health_check` surfaces:

- Repos not cloned (`configured_missing` → `clone` recommendation)
- Dirty trees and commits behind origin (`sync`)
- Branch age / merge-base staleness (`review_branch_age`, `reconcile_integration`)
- GitNexus index stale (`analyze`)

For IaC teams, **integration staleness** often means live roots have not merged default branch since a module release.

### Cross-project dependencies

`metagit_cross_project_dependencies` combines:

- Declared edges in `.metagit.yml`
- Import hints from manifests (`package.json`, etc., where relevant)
- URL matches across repos

Use this before org-wide module version bumps to build an ordered rollout: **modules → policy → live roots**.

---

## Subagent launch pattern

Hermes should treat each subagent as a **scoped worker**:

```mermaid
flowchart TD
  C[Hermes controller]
  C -->|1. context_switch + primary_repo| M[Metagit MCP]
  M -->|effective_agent_instructions| C
  C -->|2. task envelope| W[Subagent]
  W -->|3. work only in repo_path| G[Git checkout]

  subgraph envelope [Task envelope]
    E1[effective_agent_instructions]
    E2[repo_path / suggested_cwd]
    E3[objective + constraints]
    E4[forbidden: apply in prod]
  end

  C --> envelope
  envelope --> W
```

**Task envelope fields (recommended):**

- `effective_agent_instructions` — full composed stack from Metagit.
- `repo_path` — absolute path from context bundle (`suggested_cwd` or `primary_repo`).
- `objective` — one sentence the controller owns.
- `evidence` — links to search hits, dependency graph snippet, health row.
- `stop_conditions` — e.g. "stop after plan output; do not push."

Subagents should **not** maintain a parallel repo list; they trust Metagit's index for path resolution.

---

## Example: org-wide provider bump

**Objective:** Bump AWS provider `~> 5.0` across all Terraform roots.

| Phase | Controller action | Metagit tools |
|-------|-------------------|---------------|
| Inventory | List prod/staging roots | `metagit_repo_search`, tags `iac=terraform` |
| Order | Modules before live | `metagit_cross_project_dependencies` |
| Hygiene | Fetch latest | `metagit_workspace_sync`, `only_if: behind_origin`, `dry_run` first |
| Work 1 | Subagent on each module repo | `project_context_switch` + repo instructions |
| Work 2 | Subagent per live repo | `workspace_search` for `required_providers` |
| Verify | Health + notes | `workspace_health_check`, `session_update` |

---

## Skills Hermes should load

Install bundled skills with `metagit skills install --target hermes`. High-value skills for IaC controllers:

| Skill | Use when |
|-------|----------|
| `metagit-control-center` | Every multi-repo session |
| `metagit-projects` | Before creating folders or registering repos |
| `metagit-workspace-scope` | Session start |
| `metagit-multi-repo` | Features spanning repos |
| `metagit-workspace-sync` | Refreshing checkouts |
| `metagit-upstream-triage` | Errors that may originate in another repo |
| `metagit-gitnexus` | Before semantic search or impact questions |
| `metagit-repo-impact` | Planning module or API changes |
| `metagit-gating` | MCP inactive / missing config |
| `metagit-release-audit` | Before hand-off (`task qa:prepush` on metagit-cli itself) |

---

## Safety rules (IaC)

- **No `apply` in controller or subagent prompts** unless the human explicitly requests it; prefer plan-only language in `agent_instructions`.
- **No unscoped sync** — use `repos: ["project/repo"]`, not `all`, unless the objective requires it.
- **Mutations** — `pull` / `clone` require `allow_mutation: true` in MCP; default to `fetch`.
- **Secrets** — never put credentials in `agent_instructions` or `env_overrides`; use existing secrets tooling and variable refs in `.metagit.yml`.
- **Config edits** — validate after every manifest change: `metagit config validate`.
- **Register before clone** — follow `metagit-projects`: search managed config before creating directories.

---

## Related documentation

- [Terminology](terminology.md) — workspace, project, repo definitions
- [CLI Reference](cli_reference.md) — MCP tool parameters
- [Installation](install.md) — CLI and skills install
- [Application Logic](app.logic.md) — component overview

For local JSON automation without MCP, `metagit api serve` exposes managed-repo search under `/v1/repos/search` (read-only); the Hermes-oriented MCP surface remains the primary control-plane integration.
````

## File: docs/hermes-orchestrator-workspace.md
````markdown
# Hermes orchestrator workspace

Use this guide when a **Hermes** controller agent should act as the DevOps and project-management
entrypoint for a portfolio of repositories and local publish paths. Metagit holds the manifest;
Hermes holds the objective across projects.

For Terraform-heavy estates, also read [Hermes & org IaC guide](hermes-iac-workspace-guide.md).

## Quick start

1. Create or choose an umbrella coordinator repository.
2. Initialize from the bundled template (interactive prompts or an answers file):

```bash
metagit init --list-templates
metagit init ./hermes-control-plane --create --template hermes-orchestrator
# non-interactive:
metagit init --target ./hermes-control-plane --create --template hermes-orchestrator \
  --answers-file examples/hermes-orchestrator/answers.example.yml \
  --no-prompt
```

   Or copy [examples/hermes-orchestrator/.metagit.yml](../examples/hermes-orchestrator/.metagit.yml)
   manually and adjust projects, repos, and instructions.

3. Enable workspace dedupe in app config when the same URL appears in multiple projects:

```yaml
config:
  workspace:
    path: ./.metagit
    dedupe:
      enabled: true
      scope: workspace
```

4. Validate and sync:

```bash
metagit config validate
metagit project sync --project portfolio
metagit project sync --project local
```

5. Serve MCP for Hermes:

```bash
metagit mcp serve --root /path/to/coordinator
```

## Projects in the example manifest

| Project | Purpose |
|---------|---------|
| `portfolio` | Git-backed services and applications |
| `local` | Non-git `path` repos for static sites and local publish workflows |
| `platform` | Optional IaC / shared infra (empty until you add repos) |

The `local` project is the pattern for “publish a folder on disk” without a remote. Each entry
uses `path` + `sync: true`; metagit symlinks into `workspace.path` (or a canonical store when
dedupe is enabled).

## Controller responsibilities

The root `agent_instructions` in the example manifest define the controller loop:

- Orient with workspace status and health.
- Search before creating directories or clones.
- Register work in `.metagit.yml` and validate.
- Switch project context; delegate single-repo work to subagents.
- Sync conservatively (fetch by default).
- Keep descriptions and per-repo instructions accurate.

## Template apply

To copy helper files into an existing synced project folder:

```bash
metagit mcp serve --root /path/to/coordinator
# Tool: metagit_project_template_apply
#   template: hermes-orchestrator
#   target_projects: ["portfolio"]
#   dry_run: true
```

Or use the MCP tool from your agent host with `confirm_apply` when ready.

## Related docs

- [Hermes & org IaC guide](hermes-iac-workspace-guide.md) — Terraform / module rollout patterns
- [Configuration exemplar](reference/metagit-config.md) — full `.metagit.yml` field reference sample
- [Skills](skills.md) — `metagit-projects`, `metagit-control-center`, workspace sync skills
````

## File: docs/secrets.analysis.md
````markdown
# Secrets and Variables Analysis

This document outlines the secrets and variables used in the `metagit-detect` project, their sources, and their roles in the application's runtime and CI/CD workflows.

## Runtime Secrets and Variables

These secrets and variables are sourced from `.env` files and are used by the application at runtime. The `.env.example` file provides a template for these variables.

-   **`GITHUB_TOKEN`**:
    -   **Description**: A GitHub Personal Access Token (PAT) used to authenticate with the GitHub API. This is required for analyzing repositories, fetching metadata, and other interactions with GitHub.
    -   **Source**: `.env` file.
    -   **Usage**: Used by the `github` provider in the application.

-   **`GITLAB_TOKEN`**:
    -   **Description**: A GitLab Personal Access Token (PAT) used to authenticate with the GitLab API. This is necessary for interacting with GitLab repositories.
    -   **Source**: `.env` file.
    -   **Usage**: Used by the `gitlab` provider in the application.

-   **`METAGIT_LLM_TOKEN`**:
    -   **Description**: The API token for the configured Large Language Model (LLM) provider. This is used for features that leverage LLMs.
    -   **Source**: `.env` file.
    -   **Usage**: Used by the LLM client in the application.

-   **`OPENROUTER_API_KEY`**:
    -   **Description**: The API key for the OpenRouter service. This is used when `openrouter` is configured as the LLM provider.
    -   **Source**: `.env` file.
    -   **Usage**: Used by the LLM client when the provider is set to `openrouter`.

## CI/CD Secrets and Variables

These secrets are configured in the GitHub repository's secrets and are used in the CI/CD workflows.

-   **`secrets.GITHUB_TOKEN`**:
    -   **Description**: A GitHub token that is automatically generated by GitHub Actions. It is used to authenticate with the GitHub API for various tasks within the CI/CD pipelines.
    -   **Source**: GitHub repository secrets.
    -   **Usage**:
        -   Publishing Docker images to the GitHub Container Registry (`ghcr.io`).
        -   Creating and managing GitHub releases.

-   **`secrets.PYPI_API_TOKEN`**:
    -   **Description**: An API token for PyPI, used to authenticate and publish the Python package to the Python Package Index.
    -   **Source**: GitHub repository secrets, configured as a trusted publisher.
    -   **Usage**: Publishing the package to PyPI in the `release.yaml` workflow.
````

## File: docs/secrets.definitions.yml
````yaml
# Secrets and Variables Definitions

secrets:
  - name: GITHUB_TOKEN
    description: "GitHub Personal Access Token for API authentication."
    provenance: "User-provided, stored in .env file."
    placement: "runtime"
    type: "string"

  - name: GITLAB_TOKEN
    description: "GitLab Personal Access Token for API authentication."
    provenance: "User-provided, stored in .env file."
    placement: "runtime"
    type: "string"

  - name: METAGIT_LLM_TOKEN
    description: "API token for the configured LLM provider."
    provenance: "User-provided, stored in .env file."
    placement: "runtime"
    type: "string"

  - name: OPENROUTER_API_KEY
    description: "API key for the OpenRouter service."
    provenance: "User-provided, stored in .env file."
    placement: "runtime"
    type: "string"

  - name: CI_GITHUB_TOKEN
    description: "GitHub token for CI/CD operations."
    provenance: "GitHub repository secrets (secrets.GITHUB_TOKEN)."
    placement: "cicd"
    type: "string"

  - name: CI_PYPI_API_TOKEN
    description: "PyPI API token for publishing the package."
    provenance: "GitHub repository secrets (secrets.PYPI_API_TOKEN)."
    placement: "cicd"
    type: "string"
````

## File: examples/hermes-orchestrator/.metagit.yml
````yaml
name: hermes-control-plane
description: |
  Umbrella workspace for a Hermes controller agent that orchestrates DevOps and
  project-management work across many repositories and local publish paths.
kind: umbrella
agent_instructions: |
  You are the Hermes controller for this workspace: the DevOps and project-management
  entrypoint for the operator. You do not improvise workspace layout.

  Session start (every time):
  1. metagit_workspace_status — confirm gate is active and note workspace root.
  2. metagit_workspace_health_check — surface missing clones, broken mounts, duplicate URLs.
  3. Read metagit://workspace/config when you need the full manifest.

  New or continued work:
  - Search before create: metagit_repo_search / `metagit search` for names, URLs, tags.
  - Reuse existing workspace.projects[] and repos[] entries; never clone into ad-hoc folders.
  - Register changes in .metagit.yml (catalog tools or validated YAML), then metagit config validate.
  - metagit_project_context_switch to focus one project; pass effective_agent_instructions to subagents.
  - Stay controller for cross-project objectives; delegate single-repo implementation to subagents.
  - metagit_workspace_sync: fetch by default; pull/clone only with explicit operator approval.
  - metagit_session_update before switching projects or ending the session.

  Documentation duty:
  - Every repo or path must have a clear description in this manifest.
  - Record build, deploy, and publish steps in per-repo agent_instructions when non-obvious.
  - When paths, URLs, or ownership change, update .metagit.yml before claiming work is done.

workspace:
  description: |
    Portfolio registry: git-backed services plus local-only publish paths. Metagit is the
    source of truth for what exists on disk under the configured workspace.path.
  agent_instructions: |
    Validate after manifest edits (`metagit config validate`). Prefer metagit catalog and
    MCP tools over hand-editing repo lists without validation. Enable workspace.dedupe in
    app config when the same remote URL appears in multiple projects.
  projects:
    - name: portfolio
      description: Git-backed applications and shared services under active development.
      agent_instructions: |
        Git operations are allowed per repo policy. Launch subagents with repo-scoped
        effective_agent_instructions for implementation; you coordinate sequencing and PRs.
      repos:
        - name: example-api
          description: Sample HTTP API service (replace with your repository).
          url: https://github.com/example-org/example-api.git
          sync: true
          kind: service
          tags:
            tier: application
          agent_instructions: |
            Run unit tests and lint before opening PRs. Note breaking API changes in PR text.

    - name: local
      description: |
        Local-only paths (no git remotes). Used to build and publish static web apps and
        personal sites for the operator.
      agent_instructions: |
        Repos here use `path`, not `url`. Do not git clone or pull. Sync creates symlinks
        into the workspace. Document publish targets in each repo's agent_instructions.
      repos:
        - name: example-site
          description: Sample static site published from a folder on disk.
          path: ~/Sites/example-site
          sync: true
          kind: website
          tags:
            publish: static
          agent_instructions: |
            Build: npm run build (or project README). Publish: copy dist/ to the host named
            in README. Update `path` in .metagit.yml if the site directory moves.

    - name: platform
      description: Optional shared infrastructure (Terraform, modules, policy). See docs/hermes-iac-workspace-guide.md.
      agent_instructions: |
        Treat IaC changes as high blast-radius. Use metagit_cross_project_dependencies and
        GitNexus before module version bumps. Subagents run plan-only unless operator approves apply.
      repos: []
````

## File: examples/hermes-orchestrator/answers.example.yml
````yaml
# Example answers for non-interactive init:
#   metagit init --template hermes-orchestrator --answers-file examples/hermes-orchestrator/answers.example.yml --no-prompt
name: hermes-control-plane
description: Umbrella workspace for a Hermes controller agent.
url: ""
portfolio_repo_name: example-api
portfolio_repo_url: https://github.com/example-org/example-api.git
local_site_name: example-site
local_site_path: ~/Sites/example-site
````

## File: examples/metagit.umbrella.yml
````yaml
description: Umbrella workspace for metagit projects
kind: umbrella
name: example_workspace
url: https://github.com/metagit-io/metagit_umbrella
workspace:
  projects:
  - name: default
    repos:
    - name: taskfiles
      description: "Shared taskfiles"
      url: git@github.com:zloeber/taskfiles.git
    - name: dotfiles
      description: "Personal dotfiles"
      url: git@github.com:zloeber/dotfiles.git
    - name: k8s-lab-terraform-libvirt
      description: "Kubernetes lab with Terraform and Libvirt"
      url: git@github.com:zloeber/k8s-lab-terraform-libvirt.git
    - name: crewai-openrouter-lab
      description: "Openrouter lab for crewai"
      url: git@github.com:zloeber/crewai-openrouter-lab.git
    - name: metagit_cli
      description: "Metagit CLI project (symlinked)"
      path: ./src/metagit/cli
````

## File: scripts/prepush-gate.zsh
````zsh
#!/usr/bin/env zsh

set -euo pipefail

if command -v uv >/dev/null 2>&1; then
  exec uv run python "./scripts/prepush-gate.py" "$@"
fi

exec python3 "./scripts/prepush-gate.py" "$@"
````

## File: scripts/validate_skills.py
````python
#!/usr/bin/env python3
"""
Validate every SKILL.md under skills/ against Claude Code skill conventions.

Usage:
    python3 scripts/validate_skills.py

Checks performed on each skills/<name>/SKILL.md:
  - YAML frontmatter is present and parseable
  - Required fields ``name`` and ``description`` are present
  - ``name`` is lowercase-hyphenated and matches the directory name
  - ``description`` is a non-empty string within the length budget
  - SKILL.md body has substantive content

Exit codes:
  0  all skills passed
  1  one or more skills failed validation
  2  configuration error (skills directory missing, PyYAML not installed)
"""
⋮----
REPO_ROOT = Path(__file__).parent.parent
SKILLS_DIR = REPO_ROOT / "skills"
⋮----
NAME_PATTERN = re.compile(r"^[a-z][a-z0-9-]*[a-z0-9]$")
MAX_NAME_LEN = 64
MAX_DESCRIPTION_LEN = 1024
MIN_DESCRIPTION_LEN = 20
MIN_BODY_LEN = 200
⋮----
REQUIRED_FIELDS = {"name", "description"}
KNOWN_FIELDS = {"name", "description", "license", "allowed-tools", "metadata"}
⋮----
FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n?(.*)$", re.DOTALL)
⋮----
def validate_skill(skill_dir: Path) -> list[str]
⋮----
errors: list[str] = []
skill_file = skill_dir / "SKILL.md"
⋮----
rel = skill_file.relative_to(REPO_ROOT)
⋮----
text = skill_file.read_text(encoding="utf-8")
match = FRONTMATTER_RE.match(text)
⋮----
frontmatter = yaml.safe_load(frontmatter_raw)
⋮----
missing = REQUIRED_FIELDS - frontmatter.keys()
⋮----
unknown = set(frontmatter.keys()) - KNOWN_FIELDS
⋮----
name = frontmatter.get("name")
⋮----
desc = frontmatter.get("description")
⋮----
stripped = desc.strip()
⋮----
body_stripped = body.strip()
⋮----
def main() -> int
⋮----
skill_dirs = sorted(
⋮----
failed = 0
⋮----
errs = validate_skill(skill_dir)
⋮----
total = len(skill_dirs)
````

## File: skills/metagit-bootstrap/scripts/bootstrap-config.zsh
````zsh
#!/usr/bin/env zsh
set -euo pipefail

ROOT="${1:-$PWD}"
FORCE="${2:-false}"
TARGET="$ROOT/.metagit.yml"

uv run python - "$ROOT" "$TARGET" "$FORCE" <<'PY'
import sys
from pathlib import Path
from metagit.core.config.manager import create_metagit_config
from metagit.core.config.manager import MetagitConfigManager

root = Path(sys.argv[1]).resolve()
target = Path(sys.argv[2]).resolve()
force = sys.argv[3].lower() in {"1", "true", "yes", "force"}

if target.exists() and not force:
    mgr = MetagitConfigManager(config_path=target)
    result = mgr.load_config()
    state = "valid" if not isinstance(result, Exception) else "invalid"
    print(f"status=exists\tvalidity={state}\tpath={target}")
    raise SystemExit(0)

yaml_out = create_metagit_config(name=root.name, kind="application", as_yaml=True)
if isinstance(yaml_out, Exception):
    print(f"status=error\tmessage={yaml_out}")
    raise SystemExit(1)

target.write_text(yaml_out, encoding="utf-8")
mgr = MetagitConfigManager(config_path=target)
result = mgr.load_config()
state = "valid" if not isinstance(result, Exception) else "invalid"
print(f"status=written\tvalidity={state}\tpath={target}")
PY
````

## File: skills/metagit-bootstrap/SKILL.md
````markdown
---
name: metagit-bootstrap
description: Use when generating or refining local .metagit.yml files using deterministic discovery plus MCP sampling.
---

# Metagit MCP Bootstrap Skill

Use this skill to create a local `.metagit.yml` using discovery-driven prompts and MCP sampling.

## Purpose

Generate schema-compliant `.metagit.yml` files with high contextual quality while preserving safety and explicit user control.

## Local Script Wrapper (Use First)

Use this token-efficient wrapper for local bootstrap tasks:
- `./scripts/bootstrap-config.zsh [root_path] [force]`

Behavior:
- Writes `.metagit.yml` when missing
- Validates via Metagit config models
- Returns a compact status line for agents

## Workflow

1. Gather deterministic discovery data from the target repository:
   - source language/framework indicators
   - package/lock/build files
   - Dockerfiles and CI workflows
   - terraform files and module usage
2. Build a strict prompt package:
   - output format contract: valid YAML only
   - required schema fields and constraints
   - extracted discovery evidence
3. If sampling is supported, call `sampling/createMessage`.
4. Validate generated YAML with Metagit config models.
5. Retry with validation feedback up to a fixed max attempt count.
6. Return draft output and write only on explicit confirmation.

## Output Modes

- **Plan-only mode**: return prompt + discovery summary if sampling unavailable.
- **Draft mode**: return `.metagit.generated.yml` content.
- **Confirmed write mode**: write to `.metagit.yml` only with explicit parameter (`confirm_write=true`).

## Quality Bar

- Preserve discovered evidence in structured fields.
- Include workspace project and related repo entries where detectable.
- Avoid invented repositories or unverifiable dependencies.

## Safety Rules

- Never overwrite `.metagit.yml` silently.
- Never emit secrets in cleartext.
- Prefer placeholders for credentials or tokens.
````

## File: skills/metagit-cli/metagit-cli/SKILL.md
````markdown
---
name: metagit-cli
description: CLI-only shortcuts for metagit agents — workspace catalog, discovery, prompts, sync, layout, and config. Use instead of MCP or HTTP API when operating from a shell or agent_mode session.
---

# Metagit CLI (agent shortcuts)

Use this skill when an agent should drive metagit **only through the `metagit` command**. Do not call MCP tools or `metagit api` from workflows covered here unless the user explicitly asks.

Set non-interactive defaults when automating:

```bash
export METAGIT_AGENT_MODE=true
```

Global flags (most commands):

- `-c path/to/metagit.config.yaml` — app config (default `metagit.config.yaml`)
- Workspace manifest: `--definition` / `-c` on catalog commands (default `.metagit.yml`)

---

## Prompt commands (all kinds)

List built-in prompt kinds:

```bash
metagit prompt list
metagit prompt list --json
```

Emit prompts (`--text-only` for paste into agent context; `--json` for structured output; `--no-instructions` to omit manifest layers):

| Scope | Command | Default kind |
|-------|---------|--------------|
| Workspace | `metagit prompt workspace -c <definition> -k <kind>` | `instructions` |
| Project | `metagit prompt project -p <project> -c <definition> -k <kind>` | `instructions` |
| Repo | `metagit prompt repo -p <project> -n <repo> -c <definition> -k <kind>` | `instructions` |

### Prompt kinds by scope

| Kind | Workspace | Project | Repo | Purpose |
|------|:---------:|:-------:|:----:|---------|
| `instructions` | yes | yes | yes | Composed `agent_instructions` from manifest layers |
| `session-start` | yes | — | — | Session bootstrap checklist |
| `catalog-edit` | yes | yes | — | Search-before-create; catalog registration |
| `health-preflight` | yes | yes | — | Pre-work workspace/repo status pass |
| `sync-safe` | yes | yes | yes | Guarded sync rules |
| `subagent-handoff` | — | yes | yes | Delegate single-repo work |
| `layout-change` | yes | yes | yes | Rename/move dry-run workflow |
| `repo-enrich` | — | — | yes | **Discover + merge** workspace repo entry |

### Prompt shortcuts (copy-paste)

```bash
# Session bootstrap
metagit prompt workspace -k session-start --text-only -c .metagit.yml

# Composed instructions at each level
metagit prompt workspace -k instructions --text-only -c .metagit.yml
metagit prompt project -p default -k instructions --text-only -c .metagit.yml
metagit prompt repo -p default -n my-api -k instructions --text-only -c .metagit.yml

# Repo catalog enrichment (detect + merge manifest entry)
metagit prompt repo -p default -n my-api -k repo-enrich --text-only -c .metagit.yml

# Catalog registration discipline
metagit prompt workspace -k catalog-edit --text-only -c .metagit.yml

# Safe sync reminder
metagit prompt repo -p default -n my-api -k sync-safe --text-only -c .metagit.yml

# Subagent handoff
metagit prompt repo -p default -n my-api -k subagent-handoff --text-only -c .metagit.yml
```

---

## Repo enrich workflow (`repo-enrich`)

Run the prompt, then execute its steps:

```bash
metagit prompt repo -p <project> -n <repo> -k repo-enrich --text-only -c .metagit.yml
```

Typical discovery chain on the checkout:

```bash
cd "$(metagit search '<repo>' -c .metagit.yml --path-only)"
metagit detect repository -p . -o json
metagit detect repo -p . -o yaml
metagit detect repo_map -p . -o json
```

Provider metadata (dry-run):

```bash
metagit project source sync --provider github --org <org> --mode discover --no-apply
```

After merging fields into `workspace.projects[].repos[]`:

```bash
metagit config validate -c .metagit.yml
metagit workspace repo list --project <project> --json
```

---

## Workspace and catalog

Per-project dedupe override in `.metagit.yml` (overrides `workspace.dedupe.enabled` in `metagit.config.yaml` for that project only):

```yaml
workspace:
  projects:
    - name: local
      dedupe:
        enabled: false
      repos: []
```

```bash
metagit appconfig show --format json
metagit config info -c .metagit.yml
metagit config show -c .metagit.yml
metagit config validate -c .metagit.yml

metagit workspace list -c .metagit.yml --json
metagit workspace project list -c .metagit.yml --json
metagit workspace repo list -c .metagit.yml --json
metagit workspace repo list -c .metagit.yml --project <name> --json

metagit workspace project add --name <name> --json
metagit workspace repo add --project <name> --name <repo> --url <url> --json
metagit workspace project remove --name <name> --json
metagit workspace repo remove --project <name> --name <repo> --json
```

Search managed repos (always before creating entries):

```bash
metagit search "<query>" -c .metagit.yml --json
metagit search "<query>" -c .metagit.yml --path-only
metagit search "<query>" -c .metagit.yml --tag tier=1 --project <name>
```

---

## Project operations

```bash
metagit project list --config .metagit.yml --all --json
metagit project add --name <name> --json
metagit project remove --name <name> --json
metagit project rename --name <old> --new-name <new> --dry-run --json
metagit project select
metagit project sync

metagit project repo list --json
metagit project repo add --project <name> --name <repo> --url <url>
metagit project repo remove --name <repo> --json
metagit project repo rename --name <old> --new-name <new> --dry-run --json
metagit project repo move --name <repo> --to-project <other> --dry-run --json
metagit project repo prune --project <name> --dry-run

metagit project source sync --provider github --org <org> --mode discover --no-apply
metagit project source sync --provider github --org <org> --mode additive --apply
```

Layout (manifest + disk; always dry-run first):

```bash
metagit workspace project rename --name <old> --new-name <new> --dry-run --json
metagit workspace repo rename --project <p> --name <old> --new-name <new> --dry-run --json
metagit workspace repo move --project <p> --name <repo> --to-project <other> --dry-run --json
```

---

## Discovery and local metadata

```bash
metagit detect project -p <path> -o yaml
metagit detect repo -p <path> -o yaml
metagit detect repo_map -p <path> -o json
metagit detect repository -p <path> -o json
metagit detect repository -p <path> -o metagit
# --save only with operator approval (blocked in agent_mode)
```

Bootstrap new trees:

```bash
metagit init --kind application
metagit init --kind umbrella --template hermes-orchestrator
```

---

## Selection and scope

```bash
metagit workspace select --project <name>
metagit project select
metagit project repo select
```

---

## Config and appconfig

```bash
metagit appconfig validate
metagit appconfig get workspace.path
metagit config example
metagit config schema
metagit config providers
```

---

## Records, skills, version

```bash
metagit record search "<query>"
metagit skills list
metagit skills show metagit-cli
metagit skills install --skill metagit-cli
metagit version
metagit info
```

---

## Agent habits

1. **Search before create** — `metagit search` then catalog add.
2. **Validate after manifest edits** — `metagit config validate`.
3. **Emit prompts instead of rewriting playbooks** — `metagit prompt … --text-only`.
4. **Enrich stale repo entries** — `metagit prompt repo … -k repo-enrich` then detect + merge.
5. **Dry-run layout** — always `--dry-run --json` before apply.
6. **Prefer `METAGIT_AGENT_MODE=true`** in CI and agent loops to skip fuzzy finder and confirm dialogs.

## Related bundled skills

Use topic skills when you need deeper playbooks (some mention MCP): `metagit-projects`, `metagit-workspace-scope`, `metagit-workspace-sync`, `metagit-config-refresh`. This skill is the **CLI-only** index and prompt reference.
````

## File: skills/metagit-config-refresh/SKILL.md
````markdown
---
name: metagit-config-refresh
description: Refresh or bootstrap `.metagit.yml` using deterministic discovery and validation flows. Use when configuration is missing, stale, or incomplete for workspace operations.
---

# Refreshing Project Config

Use this skill to keep `.metagit.yml` accurate and operational.

## Workflow

1. Check workspace activation and config validity.
2. Run bootstrap plan mode first.
3. Review generated changes against expected workspace topology.
4. Apply config updates and validate schema before continuing.

## Command Wrapper

- `zsh ./skills/metagit-bootstrap/scripts/bootstrap-config.zsh [root_path] [mode] [seed_context]`

## Output Contract

Return:
- config health state before/after
- generated update summary
- any manual follow-up needed

## Safety

- Prefer plan/dry-run first for large config updates.
- Keep changes bounded to target workspace intent.
````

## File: skills/metagit-gating/scripts/gate-status.zsh
````zsh
#!/usr/bin/env zsh
set -euo pipefail

ROOT="${1:-$PWD}"

uv run python - "$ROOT" <<'PY'
import os
import sys
from metagit.core.mcp.gate import WorkspaceGate
from metagit.core.mcp.root_resolver import WorkspaceRootResolver
from metagit.core.mcp.tool_registry import ToolRegistry

root = sys.argv[1]
resolver = WorkspaceRootResolver()
gate = WorkspaceGate()
registry = ToolRegistry()

resolved = resolver.resolve(cwd=os.path.abspath(root), cli_root=root)
status = gate.evaluate(root_path=resolved)
tools = registry.list_tools(status)
print(f"state={status.state.value}\troot={status.root_path or 'none'}\ttools={len(tools)}")
PY
````

## File: skills/metagit-gating/SKILL.md
````markdown
---
name: metagit-gating
description: Use when implementing or operating Metagit MCP server activation and tool exposure rules based on .metagit.yml presence and validity.
---

# Metagit MCP Gating Skill

Use this skill whenever you need to control whether Metagit MCP tools/resources are exposed.

## Purpose

Ensure high-risk tooling and multi-repo context are only available when a valid `.metagit.yml` exists at the resolved workspace root.

## Local Script Wrapper (Use First)

Use this token-efficient wrapper for all gating checks:
- `./scripts/gate-status.zsh [root_path]`

Expected output (single line, tab-delimited):
- `state=<value>\troot=<path|none>\ttools=<count>`

## Activation Workflow

1. Resolve workspace root:
   - `METAGIT_WORKSPACE_ROOT`
   - CLI `--root`
   - upward directory walk
2. Check for `.metagit.yml` in resolved root.
3. Validate config through existing Metagit config models.
4. Derive activation state: missing, invalid, or active.
5. Register tool surface based on state.

## Tool Exposure Contract

### Inactive (missing or invalid config)
Expose only:
- `metagit_workspace_status`
- `metagit_bootstrap_config_plan_only`

### Active (valid config)
Expose full set:
- `metagit_workspace_status`
- `metagit_workspace_index`
- `metagit_workspace_search`
- `metagit_upstream_hints`
- `metagit_repo_inspect`
- `metagit_repo_sync`
- `metagit_bootstrap_config`

## Error Handling

- Return explicit, machine-readable state and reason.
- Avoid stack traces in user-facing outputs.
- Log parser/validation errors with enough detail for debugging.

## Safety Rules

- Never expose mutation-capable tools in inactive state.
- Never operate outside validated workspace boundaries.
- Keep defaults read-only unless user/agent explicitly opts in.
````

## File: skills/metagit-gitnexus/scripts/analyze-targets.zsh
````zsh
#!/usr/bin/env zsh

set -euo pipefail

workspace_root="${1:-.}"
project_name="${2:-default}"

if [[ ! -f ".metagit.yml" ]]; then
  echo "ERROR: .metagit.yml not found in current directory"
  exit 2
fi

echo "analyze repo=$(pwd)"
npx gitnexus analyze

tmp_output="$(mktemp)"
uv run python - "$workspace_root" "$project_name" <<'PY' > "$tmp_output"
import sys
from pathlib import Path
import yaml

workspace_root = Path(sys.argv[1]).expanduser().resolve()
project_name = sys.argv[2]
cfg = yaml.safe_load(Path(".metagit.yml").read_text(encoding="utf-8")) or {}
workspace = (cfg.get("workspace") or {})
projects = workspace.get("projects") or []
target = next((p for p in projects if p.get("name") == project_name), None)
if not target:
    print(f"warn project_not_found={project_name}")
    raise SystemExit(0)

for repo in target.get("repos") or []:
    name = repo.get("name")
    if not name:
        continue
    repo_path = workspace_root / project_name / name
    if repo_path.exists() and repo_path.is_dir():
        print(f"repo_path={repo_path}")
    else:
        print(f"skip_missing={repo_path}")
PY

while IFS= read -r line; do
  case "$line" in
    repo_path=*)
      path="${line#repo_path=}"
      echo "analyze repo=${path}"
      (cd "$path" && npx gitnexus analyze) || echo "fail repo=${path}"
      ;;
    *)
      echo "$line"
      ;;
  esac
done < "$tmp_output"

rm -f "$tmp_output"
````

## File: skills/metagit-multi-repo/SKILL.md
````markdown
---
name: metagit-multi-repo
description: Coordinate implementation tasks across multiple repositories using metagit status, search, and scoped sync workflows. Use when one objective spans several repositories.
---

# Coordinating Multi-Repo Implementation

Use this skill for cross-repository feature or fix delivery.

## Workflow

1. Define objective and affected repositories.
2. Verify workspace scope and dependency hints.
3. Sequence work by dependency order.
4. Sync only required repositories.
5. Track progress and blockers per repository.

## Command Wrapper

- `zsh ./skills/metagit-control-center/scripts/control-cycle.zsh [root_path] ["query"] [preset]`

## Output Contract

Return:
- objective-to-repository map
- execution order
- current blocker + next step

## Safety

- Keep scope bounded to configured workspace repositories.
- Prefer deterministic evidence for cross-repo assumptions.
````

## File: skills/metagit-projects/SKILL.md
````markdown
---
name: metagit-projects
description: Ongoing workspace and project management for OpenClaw and Hermes agents. Use when starting work, organizing repos, or before creating a new project folder so existing metagit projects are reused instead of duplicated.
---

# Ongoing Project Management

Use this skill when the user starts new work, reorganizes repositories, or asks you to create a project folder. Metagit is the source of truth for what already exists in the workspace.

## Concepts

Metagit uses a three-level hierarchy (see project terminology docs):

| Level | Meaning |
|-------|---------|
| **Workspace** | Root folder where projects are synced (from app config `workspace.path`, often `./.metagit/`). Holds many projects. |
| **Project** | Named group of one or more Git repositories. Multi-repo products are one project; unrelated repos can also share a workspace under different project names. |
| **Repo** | A single Git repository entry under `workspace.projects[].repos` in `.metagit.yml`. |

A **project** is not always “one product.” It is whatever grouping helps the user and agents reason about related (or intentionally grouped) repositories. A workspace may contain unrelated projects side by side (for example `default`, `client-a`, `experiments`).

The umbrella `.metagit.yml` (workspace definition, often `kind: umbrella`) lives in a coordinating repository or central config checkout. Application repos may have their own `.metagit.yml` for metadata mode.

## Mandatory: check before creating folders

**Never** create a new project directory or clone into the workspace until you have checked metagit for an existing match.

1. **Locate the workspace definition**
   - Prefer the user’s umbrella `.metagit.yml` if known.
   - Otherwise use `.metagit.yml` in the current repo with `--definition /path/to/.metagit.yml`.

2. **List configured projects and repo counts**
   ```bash
   metagit config info --config-path /path/to/.metagit.yml
   metagit project list --config /path/to/.metagit.yml --project default
   ```
   Repeat `--project` for each project name returned by `config info`.

3. **Search managed repos by name, URL fragment, or tag**
   ```bash
   metagit search "<proposed-name-or-url>" --definition /path/to/.metagit.yml
   metagit search "<name>" --definition /path/to/.metagit.yml --json
   ```

4. **Inspect on disk** (workspace path from app config, default `./.metagit/`)
   - Expected layout: `{workspace.path}/{project_name}/{repo_name}/`
   - If the directory already exists, **reuse it**; do not create a parallel tree.

5. **Decide**
   - **Match found** → use existing project/repo; run `metagit project sync` only if the user wants checkouts refreshed.
   - **No match** → proceed with registration steps below (still add to workspace; do not leave orphan folders).

## Registering new work in the workspace

### New repository in an existing project

From the directory containing the workspace `.metagit.yml` (or pass `--config`):

```bash
metagit project repo add --project <project_name> --prompt
# or non-interactive:
metagit project repo add --project <project_name> --name <repo> --url <git-url>
metagit config validate --config-path .metagit.yml
metagit project sync --project <project_name>
```

In the new application repo (if applicable):

```bash
cd /path/to/new/repo
metagit init
metagit detect repo --force   # optional: enrich .metagit.yml
```

### New project group (new `workspace.projects[]` entry)

There is no separate `project create` CLI today. Add a project block to `.metagit.yml`:

```yaml
workspace:
  projects:
    - name: my-new-project
      description: Short purpose for agents and humans
      repos: []
```

Then validate, add repos, and sync:

```bash
metagit config validate --config-path .metagit.yml
metagit project repo add --project my-new-project --prompt
metagit project sync --project my-new-project
```

Choose a **distinct project name**; avoid duplicating an existing `workspace.projects[].name`.

### New umbrella workspace

When bootstrapping a workspace coordinator repo:

```bash
metagit init --kind umbrella
metagit project repo add --project default --prompt
metagit project sync
```

## Ongoing session habits

At the start of sustained work:

1. Run **metagit-workspace-scope** (or `metagit mcp serve --status-once` when MCP is available).
2. Confirm **active project** matches the user’s intent (`metagit workspace select --project <name>` when switching).
3. Use **`metagit search`** before assuming a repo is missing or lives elsewhere.
4. For multi-repo tasks, prefer **metagit-control-center** or **metagit-multi-repo** over ad-hoc cloning.

When the user names a target folder:

- Resolve it against managed config first.
- If unmanaged but present on disk under the project sync folder, report it and offer to add via `metagit project repo add` rather than recreating.

## OpenClaw and Hermes setup

Install bundled skills (including this one) for agent hosts:

```bash
metagit skills list
metagit skills install --scope user --target openclaw --target hermes
metagit mcp install --scope user --target openclaw --target hermes
```

Use `--scope project` when installing into a specific umbrella repository checkout.

## Output contract

After project-management actions, report:

- workspace definition path used
- whether the target was **existing** or **newly registered**
- project name and repo name(s) affected
- sync status if `project sync` was run
- recommended next command (`workspace select`, `project select`, or `detect`)

## Safety

- Do not clone, delete, or overwrite sync directories without explicit user approval.
- Do not edit `.metagit.yml` without validating afterward (`metagit config validate`).
- Prefer reusing configured repos over creating duplicate checkouts.
- Keep unrelated experiments in separate `workspace.projects` entries when the user wants clear boundaries.
````

## File: skills/metagit-upstream-scan/scripts/upstream-scan.zsh
````zsh
#!/usr/bin/env zsh
set -euo pipefail

ROOT="${1:-$PWD}"
QUERY="${2:-}"
PRESET="${3:-}"
MAX_RESULTS="${4:-20}"

if [[ -z "$QUERY" ]]; then
  echo "status=error\tmessage=query-required"
  exit 1
fi

uv run python - "$ROOT" "$QUERY" "$PRESET" "$MAX_RESULTS" <<'PY'
import sys
from pathlib import Path
from metagit.core.config.manager import MetagitConfigManager
from metagit.core.mcp.services.workspace_index import WorkspaceIndexService
from metagit.core.mcp.services.workspace_search import WorkspaceSearchService
from metagit.core.mcp.services.upstream_hints import UpstreamHintService

root = Path(sys.argv[1]).resolve()
query = sys.argv[2]
preset = sys.argv[3] or None
max_results = int(sys.argv[4])

manager = MetagitConfigManager(config_path=root / ".metagit.yml")
cfg = manager.load_config()
if isinstance(cfg, Exception):
    print(f"status=error\tmessage=config-invalid\tdetail={cfg}")
    raise SystemExit(1)

index = WorkspaceIndexService().build_index(config=cfg, workspace_root=str(root))
repo_paths = [row["repo_path"] for row in index if row.get("exists")]
search_hits = WorkspaceSearchService().search(query=query, repo_paths=repo_paths, preset=preset, max_results=max_results)
ranked = UpstreamHintService().rank(blocker=query, repo_context=index)[:5]

print(f"status=ok\trepos={len(index)}\thits={len(search_hits)}")
for row in ranked:
    print(f"hint\trepo={row['repo_name']}\tscore={row['score']}")
for hit in search_hits[:5]:
    print(f"hit\tfile={hit['file_path']}\tline={hit['line_number']}")
PY
````

## File: skills/metagit-upstream-scan/SKILL.md
````markdown
---
name: metagit-upstream-scan
description: Use when a coding agent encounters likely upstream blockers and must find related workspace repositories, files, and probable root causes.
---

# Metagit Upstream Discovery Skill

Use this skill when the current repo does not appear to contain the full fix and related repositories may hold the source issue.

## Supported Use Cases

- Missing Terraform input in a shared module
- Docker base image/version mismatch across repos
- Shared infrastructure definitions causing local failures
- CI pipeline breakages tied to upstream templates/workflows

## Local Script Wrapper (Use First)

Use this token-efficient wrapper for upstream discovery tasks:
- `./scripts/upstream-scan.zsh [root_path] "<query>" [preset] [max_results]`

Output format:
- compact status line
- top ranked repo hints (`hint`)
- top search file hits (`hit`)

## Workflow

1. Read workspace repository map from active `.metagit.yml`.
2. Run `metagit_workspace_index` to verify repo availability and sync state.
3. Use `metagit_workspace_search` with category preset (`terraform`, `docker`, `infra`, `ci`).
4. Use `metagit_upstream_hints` to rank candidate repositories and files.
5. Return a concise action plan:
   - top candidate repos
   - likely files/definitions
   - whether sync is needed before deeper analysis

## Search Strategy

- Start narrow with issue-specific terms (error, module, variable, image tag).
- Expand to broader shared terms if no hits.
- Prefer repositories referenced by workspace metadata before searching unknown repos.

## Output Contract

Return:
- ranked candidates with rationale
- suggested next file openings
- confidence level and unresolved assumptions

## Safety Rules

- Restrict search to configured workspace repositories.
- Cap result size and duration.
- Keep this workflow read-only unless an explicit sync action is requested.
````

## File: skills/metagit-upstream-triage/SKILL.md
````markdown
---
name: metagit-upstream-triage
description: Triage cross-repository blockers by ranking likely upstream repositories and files with metagit search and hinting tools. Use when local fixes appear incomplete.
---

# Triaging Upstream Blockers

Use this skill for failures likely rooted in another repository.

## Workflow

1. Run workspace index and search with issue-specific terms.
2. Run upstream hint ranking to prioritize repositories/files.
3. Open the top candidates and validate root-cause evidence.
4. Return a short fix path (repo, file, next action).

## Command Wrapper

- `zsh ./skills/metagit-upstream-scan/scripts/upstream-scan.zsh [root_path] "<query>" [preset] [max_results]`

## Output Contract

Return:
- ranked candidate repositories
- probable root-cause files
- confidence and assumptions

## Safety

- Keep this flow read-only unless sync is explicitly requested.
````

## File: src/metagit/cli/commands/api.py
````python
#!/usr/bin/env python
"""
Local JSON HTTP API command group.
"""
⋮----
@click.group()
def api() -> None
⋮----
"""Local JSON API commands."""
⋮----
def serve(root: str, host: str, port: int, status_once: bool) -> None
⋮----
"""Serve managed-repo search JSON endpoints on localhost."""
root_abs = str(Path(root).resolve())
server = build_server(root=root_abs, host=host, port=port)
bound_port = server.server_address[1]
````

## File: src/metagit/cli/commands/project_source.py
````python
#!/usr/bin/env python
"""
Project source sync subcommand.
"""
⋮----
@click.group(name="source")
@click.pass_context
def source(ctx: click.Context) -> None
⋮----
"""Source-backed project sync operations."""
⋮----
"""Discover and sync repositories from provider sources."""
logger: UnifiedLogger = ctx.obj["logger"]
app_config: AppConfig = ctx.obj["config"]
project_name: str = ctx.obj["project"]
local_config: MetagitConfig = ctx.obj["local_config"]
config_path: str = ctx.obj["config_path"]
⋮----
spec = SourceSpec(
⋮----
workspace_project: Optional[WorkspaceProject] = next(
⋮----
service = SourceSyncService(app_config, logger)
discovered_result = service.discover(spec)
⋮----
discovered = discovered_result
⋮----
sync_mode = SourceSyncMode(mode)
plan = service.plan(spec, workspace_project, discovered, sync_mode)
⋮----
updated_project = service.apply_plan(workspace_project, plan, sync_mode)
⋮----
config_manager = MetagitConfigManager(config_path=config_path)
save_result = config_manager.save_config(local_config, config_path)
````

## File: src/metagit/cli/commands/prompt.py
````python
#!/usr/bin/env python
"""
Emit metagit prompts for workspace, project, and repo scopes.
"""
⋮----
def _load_manifest(definition_path: str) -> MetagitConfig
⋮----
manager = MetagitConfigManager(definition_path)
loaded = manager.load_config()
⋮----
app_config: AppConfig = ctx.obj["config"]
config = _load_manifest(definition_path)
workspace_root = str(Path(app_config.workspace.path).expanduser().resolve())
⋮----
def _kind_choice(scope: str) -> click.Choice
⋮----
[str(item) for item in kinds_for_scope(scope)],  # type: ignore[arg-type]
⋮----
result = PromptService().emit(
⋮----
kind=kind,  # type: ignore[arg-type]
scope=scope,  # type: ignore[arg-type]
⋮----
@click.group(name="prompt", invoke_without_command=True)
@click.pass_context
def prompt(ctx: click.Context) -> None
⋮----
"""Emit metagit prompts for workspace, project, and repo scopes."""
⋮----
@click.pass_context
def prompt_list(ctx: click.Context, as_json: bool) -> None
⋮----
"""List available prompt kinds and valid scopes."""
_ = ctx
entries = PromptService().list_entries()
⋮----
scopes = ", ".join(entry.scopes)
⋮----
"""Emit a prompt for the whole workspace manifest."""
⋮----
"""Emit a prompt scoped to one workspace project."""
⋮----
"""Emit a prompt scoped to one repository entry."""
````

## File: src/metagit/cli/commands/web.py
````python
#!/usr/bin/env python
"""Local web UI command group."""
⋮----
@click.group()
def web() -> None
⋮----
"""Local web UI commands."""
⋮----
"""Serve the metagit web UI and workspace API on localhost."""
root_abs = str(Path(root).resolve())
appconfig_path = appconfig
⋮----
appconfig_path = str(ctx.obj["config_path"])
server = build_web_server(
bound_port = server.server_address[1]
url = f"http://{host}:{bound_port}/"
````

## File: src/metagit/cli/config_patch_ops.py
````python
#!/usr/bin/env python
"""Shared helpers for config patch/preview CLI commands."""
⋮----
def parse_cli_value(raw: str) -> Any
⋮----
"""Parse --value as JSON when possible, otherwise return the raw string."""
stripped = raw.strip()
⋮----
def load_operations_file(path: str) -> list[ConfigOperation]
⋮----
"""Load operations from a JSON file (ConfigPatchRequest or operations array)."""
file_path = Path(path)
⋮----
payload = json.loads(file_path.read_text(encoding="utf-8"))
⋮----
request = ConfigPatchRequest.model_validate(payload)
⋮----
"""Resolve operations from --file or a single --op/--path/--value triplet."""
⋮----
op_kind = ConfigOpKind(op.lower())
⋮----
allowed = ", ".join(item.value for item in ConfigOpKind)
⋮----
parsed_value = parse_cli_value(value) if value is not None else None
⋮----
"""Print patch outcome and abort on failure when not saving with errors."""
⋮----
"""Print or write YAML preview."""
⋮----
payload = result.model_dump(mode="json", exclude_none=True)
⋮----
"""Print schema tree."""
⋮----
def _print_tree_node(node: Any, *, indent: int) -> None
⋮----
prefix = "  " * indent
label = node.type_label or node.type
enabled = "on" if node.enabled else "off"
path = node.path or "(root)"
⋮----
value = node.value
⋮----
extra = ""
⋮----
extra = f", items={node.item_count}"
⋮----
extra = f"{extra}, appendable"
````

## File: src/metagit/core/api/__init__.py
````python
#!/usr/bin/env python
"""Local HTTP JSON API helpers for Metagit."""
⋮----
__all__ = ["build_server"]
````

## File: src/metagit/core/api/catalog_handler.py
````python
#!/usr/bin/env python
"""
HTTP handlers for workspace catalog list and mutation (v2 API).
"""
⋮----
JsonResponder = Callable[[int, dict[str, Any]], None]
⋮----
class CatalogApiHandler
⋮----
"""Route catalog operations for the local JSON HTTP API."""
⋮----
def __init__(self, workspace_root: str, config_path: str) -> None
⋮----
"""Dispatch catalog routes; return True when handled."""
parsed_path = urlparse(path).path
params = parse_qs(query, keep_blank_values=True)
config = self._load_config(respond)
⋮----
result = self._service.list_workspace(
⋮----
result = self._service.list_projects(config)
⋮----
payload = self._parse_body(body, respond)
⋮----
name = str(payload.get("name", "")).strip()
mutation = self._service.add_project(
⋮----
name = unquote(parsed_path.removeprefix("/v2/projects/").strip("/"))
mutation = self._service.remove_project(
⋮----
project = self._first(params, "project")
result = self._service.list_repos(
⋮----
project_name = str(payload.get("project", "")).strip()
built = self._service.build_repo_from_fields(
⋮----
mutation = self._service.add_repo(
⋮----
remainder = parsed_path.removeprefix("/v2/repos/").strip("/")
⋮----
mutation = self._service.remove_repo(
⋮----
def _load_config(self, respond: JsonResponder) -> MetagitConfig | None
⋮----
manager = MetagitConfigManager(self._config_path)
loaded = manager.load_config()
⋮----
def _parse_body(self, body: bytes, respond: JsonResponder) -> dict[str, Any] | None
⋮----
parsed = json.loads(body.decode("utf-8"))
⋮----
status = 200 if mutation.ok else 409
⋮----
status = 404
⋮----
status = 400
⋮----
@staticmethod
    def _first(params: dict[str, list[str]], key: str) -> str | None
⋮----
values = params.get(key)
⋮----
first = values[0].strip()
````

## File: src/metagit/core/api/layout_handler.py
````python
#!/usr/bin/env python
"""
HTTP handlers for workspace layout rename and move (v2 API).
"""
⋮----
JsonResponder = Callable[[int, dict[str, Any]], None]
⋮----
class LayoutApiHandler
⋮----
"""Route layout rename/move operations for the local JSON HTTP API."""
⋮----
def __init__(self, definition_root: str, config_path: str) -> None
⋮----
"""Dispatch layout routes; return True when handled."""
parsed_path = urlparse(path).path
params = parse_qs(query, keep_blank_values=True)
config = self._load_config(respond)
⋮----
flags = self._layout_flags(params, body, respond)
⋮----
from_name = unquote(
payload = flags.get("body") or {}
to_name = str(
result = self._service.rename_project(
⋮----
remainder = (
⋮----
result = self._service.rename_repo(
⋮----
result = self._service.move_repo(
⋮----
payload: dict[str, Any] = {}
⋮----
parsed = json.loads(body.decode("utf-8"))
⋮----
payload = parsed
⋮----
def _bool_param(key: str, default: bool = False) -> bool
⋮----
raw = params.get(key, [str(payload.get(key, default)).lower()])
value = raw[0] if raw else str(default).lower()
⋮----
manifest_only = _bool_param("manifest_only") or bool(
⋮----
def _load_config(self, respond: JsonResponder) -> MetagitConfig | None
⋮----
manager = MetagitConfigManager(self._config_path)
loaded = manager.load_config()
⋮----
status = 200 if mutation.ok else 409
⋮----
status = 404
⋮----
status = 400
⋮----
status = 403
⋮----
@staticmethod
    def _first(params: dict[str, list[str]], key: str) -> str | None
⋮----
values = params.get(key)
⋮----
first = values[0].strip()
````

## File: src/metagit/core/appconfig/agent_mode.py
````python
#!/usr/bin/env python
"""
Agent-mode detection for non-interactive, agent-optimized interfaces.
"""
⋮----
_ENV_VAR = "METAGIT_AGENT_MODE"
_TRUTHY = frozenset({"1", "true", "yes", "on"})
⋮----
def env_agent_mode_enabled() -> bool | None
⋮----
"""
    Read METAGIT_AGENT_MODE from the environment.

    Returns None when the variable is unset (caller should use file config).
    """
raw = os.getenv(_ENV_VAR)
⋮----
def resolve_agent_mode(config: AppConfig) -> bool
⋮----
"""Effective agent mode: METAGIT_AGENT_MODE overrides appconfig.agent_mode."""
from_env = env_agent_mode_enabled()
⋮----
def apply_agent_mode_override(config: AppConfig) -> AppConfig
⋮----
"""Apply METAGIT_AGENT_MODE to config after load."""
````

## File: src/metagit/core/appconfig/display.py
````python
#!/usr/bin/env python
"""
Render full application configuration for CLI display.
"""
⋮----
OutputFormat = Literal["yaml", "json", "minimal-yaml"]
⋮----
"""Build the document shown by `metagit appconfig show`."""
⋮----
config_body = config.model_dump(
⋮----
config_body = config.model_dump(mode="json")
⋮----
"""Serialize appconfig show output for the requested format."""
payload = build_appconfig_payload(
````

## File: src/metagit/core/config/__init__.py
````python
#!/usr/bin/env python
"""
Project package for metagit.

This package provides Pydantic models and methods for parsing and validating
.metagit.yml configuration files.
"""
⋮----
__all__ = [
⋮----
# Main configuration model
⋮----
# Enums
⋮----
# Models
````

## File: src/metagit/core/config/documentation_models.py
````python
#!/usr/bin/env python
"""
Documentation source models for .metagit.yml knowledge-graph ingestion.
"""
⋮----
def _looks_like_url(value: str) -> bool
⋮----
trimmed = value.strip().lower()
⋮----
"""
    Accept documentation as strings or dicts; normalize to DocumentationSource dicts.

    - Bare strings: markdown path or web URL (inferred from prefix).
    - Dicts: explicit kind/path/url/tags/metadata for graph pipelines.
    """
⋮----
normalized: list[dict[str, Any]] = []
⋮----
trimmed = item.strip()
⋮----
class DocumentationSource(BaseModel)
⋮----
"""One documentation source for agents and knowledge-graph ingestion."""
⋮----
model_config = ConfigDict(extra="forbid", use_enum_values=True)
⋮----
kind: str = Field(
path: Optional[str] = Field(
url: Optional[str] = Field(None, description="Remote documentation URL")
title: Optional[str] = Field(None, description="Human-readable title")
description: Optional[str] = Field(
tags: dict[str, str] = Field(
metadata: dict[str, Any] = Field(
⋮----
@field_validator("kind", mode="before")
@classmethod
    def _normalize_kind(cls, value: object) -> str
⋮----
@field_validator("tags", mode="before")
@classmethod
    def _normalize_tags(cls, value: object) -> dict[str, str]
⋮----
@model_validator(mode="after")
    def _require_path_or_url(self) -> DocumentationSource
⋮----
def graph_node_payload(self) -> dict[str, Any]
⋮----
"""Serialize for knowledge-graph or export pipelines."""
payload: dict[str, Any] = {
⋮----
DocumentationEntry = Union[str, DocumentationSource]
````

## File: src/metagit/core/config/graph_cypher_export.py
````python
#!/usr/bin/env python
"""Export .metagit.yml workspace graph data as GitNexus-ingestible Cypher."""
⋮----
class GraphCypherNode(BaseModel)
⋮----
"""Workspace graph node for export."""
⋮----
id: str
kind: Literal["workspace", "project", "repo", "documentation"]
label: str
workspace: Optional[str] = None
project: Optional[str] = None
repo: Optional[str] = None
path: Optional[str] = None
properties: dict[str, Any] = Field(default_factory=dict)
⋮----
class GraphCypherEdge(BaseModel)
⋮----
"""Workspace graph edge for export."""
⋮----
from_id: str
to_id: str
type: str
source: Literal["manual", "structure", "documentation"] = "manual"
label: Optional[str] = None
⋮----
class GraphCypherToolCall(BaseModel)
⋮----
"""MCP-shaped call for gitnexus_cypher."""
⋮----
tool: str = "gitnexus_cypher"
arguments: dict[str, Any] = Field(default_factory=dict)
⋮----
class GraphCypherExportResult(BaseModel)
⋮----
"""Cypher export bundle for CLI, MCP, and agent pipelines."""
⋮----
ok: bool = True
workspace_name: str = ""
gitnexus_repo: str = ""
schema_statements: list[str] = Field(default_factory=list)
statements: list[str] = Field(default_factory=list)
tool_calls: list[GraphCypherToolCall] = Field(default_factory=list)
nodes: list[GraphCypherNode] = Field(default_factory=list)
edges: list[GraphCypherEdge] = Field(default_factory=list)
warnings: list[str] = Field(default_factory=list)
⋮----
def _escape_cypher_string(value: str) -> str
⋮----
def _literal_string(value: str) -> str
⋮----
def _literal_json(payload: dict[str, Any]) -> str
⋮----
class GraphCypherExportService
⋮----
"""Build Metagit workspace overlay nodes/edges and Cypher ingest statements."""
⋮----
_schema_statements: tuple[str, ...] = (
⋮----
"""Export manifest graph data as Cypher statements and MCP tool calls."""
rows = self._index.build_index(config=config, workspace_root=workspace_root)
project_names = {
workspace_name = config.name or "workspace"
target_repo = gitnexus_repo or workspace_name
⋮----
nodes: dict[str, GraphCypherNode] = {}
edges: list[GraphCypherEdge] = []
warnings: list[str] = []
⋮----
manual_added = self._add_manual_edges(
⋮----
node_list = sorted(nodes.values(), key=lambda item: item.id)
schema = list(self._schema_statements) if with_schema else []
statements = self._build_statements(node_list, edges)
tool_calls = [
⋮----
node_id = f"workspace:{workspace_name}"
⋮----
node_id = f"project:{project_name}"
⋮----
node_id = f"repo:{row['project_name']}/{row['repo_name']}"
⋮----
project_id = f"project:{row['project_name']}"
repo_id = f"repo:{row['project_name']}/{row['repo_name']}"
⋮----
payload = entry.graph_node_payload()
doc_id = f"documentation:{index}"
label = entry.title or entry.path or entry.url or doc_id
⋮----
added = 0
⋮----
from_id = resolve_graph_endpoint_id(
to_id = resolve_graph_endpoint_id(
⋮----
rel_id = rel.id or f"manual:{from_id}->{to_id}:{rel.type}"
⋮----
project = node_id.split(":", 1)[1]
⋮----
body = node_id.split(":", 1)[1]
⋮----
row = next(
⋮----
statements: list[str] = []
⋮----
def _merge_node_statement(self, node: GraphCypherNode) -> str
⋮----
props = dict(node.properties)
⋮----
def _create_edge_statement(self, edge: GraphCypherEdge) -> str
⋮----
rel_props = {
````

## File: src/metagit/core/config/graph_models.py
````python
#!/usr/bin/env python
"""
Manual workspace graph relationships for cross-repo knowledge graphs.
"""
⋮----
class GraphEndpoint(BaseModel)
⋮----
"""Endpoint for a manual cross-repo relationship."""
⋮----
model_config = ConfigDict(extra="forbid")
⋮----
project: Optional[str] = Field(
repo: Optional[str] = Field(
path: Optional[str] = Field(
⋮----
class GraphRelationship(BaseModel)
⋮----
"""Manually declared edge between workspace projects or repos."""
⋮----
model_config = ConfigDict(extra="forbid", populate_by_name=True)
⋮----
id: Optional[str] = Field(
from_endpoint: GraphEndpoint = Field(
to: GraphEndpoint = Field(..., description="Relationship target")
type: str = Field(
label: Optional[str] = Field(None, description="Short label for graph UIs")
description: Optional[str] = Field(
tags: dict[str, str] = Field(
metadata: dict[str, Any] = Field(
⋮----
@field_validator("type", mode="before")
@classmethod
    def _normalize_type(cls, value: object) -> str
⋮----
class WorkspaceGraph(BaseModel)
⋮----
"""Top-level manual graph data on a .metagit.yml manifest."""
⋮----
relationships: list[GraphRelationship] = Field(
````

## File: src/metagit/core/config/graph_resolver.py
````python
#!/usr/bin/env python
"""
Resolve manual graph endpoints to workspace dependency node ids.
"""
⋮----
"""
    Map a graph endpoint to a dependency node id (project:… or repo:…/…).

    Requires project when repo is set. Repo-only matches the first indexed row.
    """
⋮----
project = endpoint.project
````

## File: src/metagit/core/config/patch_service.py
````python
#!/usr/bin/env python
"""Apply schema-tree config operations for CLI and web consumers."""
⋮----
ConfigTarget = Literal["metagit", "appconfig"]
⋮----
class PatchResult(BaseModel)
⋮----
"""Outcome of applying config patch operations."""
⋮----
ok: bool
target: ConfigTarget
config_path: str
validation_errors: list[dict[str, str]] = Field(default_factory=list)
saved: bool = False
tree: SchemaFieldNode | None = None
⋮----
class PreviewResult(BaseModel)
⋮----
"""YAML preview after optional draft operations."""
⋮----
style: PreviewStyle
yaml: str
draft: bool = False
⋮----
class TreeResult(BaseModel)
⋮----
"""Schema tree for a loaded config."""
⋮----
tree: SchemaFieldNode
⋮----
class ConfigPatchService
⋮----
"""Load, mutate, preview, and save metagit and appconfig via schema operations."""
⋮----
def __init__(self) -> None
⋮----
"""Build schema tree for the config at config_path."""
resolved = str(Path(config_path).resolve())
⋮----
loaded = self._load_metagit(resolved)
⋮----
loaded = self._load_appconfig(resolved)
⋮----
model_class = MetagitConfig if target == "metagit" else AppConfig
tree = self._schema.build_tree(
⋮----
"""Render YAML preview with optional draft operations."""
⋮----
model_class = MetagitConfig
⋮----
model_class = AppConfig
⋮----
config = loaded
validation_errors: list[dict[str, str]] = []
draft = bool(operations)
⋮----
yaml_text = read_disk_text(resolved)
⋮----
yaml_text = render_metagit_yaml(config, style=style)
⋮----
yaml_text = render_appconfig_yaml(
⋮----
"""Apply operations; optionally persist when save=True and validation passes."""
⋮----
saved = False
⋮----
save_result = MetagitConfigManager(resolved).save_config(updated)
⋮----
save_result = save_appconfig(resolved, updated)
⋮----
saved = True
tree = None
⋮----
def _load_metagit(self, config_path: str) -> MetagitConfig | Exception
⋮----
manager = MetagitConfigManager(config_path=config_path)
loaded = manager.load_config()
⋮----
def _load_appconfig(self, config_path: str) -> AppConfig | Exception
⋮----
loaded = load_appconfig(config_path)
````

## File: src/metagit/core/config/yaml_display.py
````python
#!/usr/bin/env python
"""
Human-readable YAML serialization for Metagit config display.
"""
⋮----
def _represent_str(dumper: yaml.Dumper, value: str) -> yaml.nodes.ScalarNode
⋮----
"""Use literal block style for multiline strings; preserve Unicode."""
⋮----
class _ReadableYamlDumper(yaml.SafeDumper)
⋮----
"""Dumper tuned for terminal-friendly config output."""
⋮----
def dump_config_dict(payload: dict[str, Any]) -> str
⋮----
"""
    Serialize a config dict for `metagit config show --normalized`.

    Multiline fields use `|` blocks; Unicode is not escaped.
    """
````

## File: src/metagit/core/detect/detectors/git.py
````python
#!/usr/bin/env python3
"""
Git repository detection plugin.

This module provides functionality to detect Git repository information
including branch checksum, tags, last commit timestamp, origin branch count,
and local dirty status.
"""
⋮----
class GitDetector(Detector)
⋮----
"""Git repository detection plugin."""
⋮----
name = "git"
⋮----
def should_run(self, ctx: ProjectScanContext) -> bool
⋮----
"""
        Determine if this detector should run on the given context.

        Args:
            ctx: Project scan context

        Returns:
            bool: True if the path is a Git repository
        """
⋮----
# Check if the path is a Git repository
_ = Repo(str(ctx.root_path))
⋮----
def run(self, ctx: ProjectScanContext) -> Optional[DiscoveryResult]
⋮----
"""
        Run Git repository detection.

        Args:
            ctx: Project scan context

        Returns:
            DiscoveryResult with Git repository information
        """
⋮----
repo = Repo(str(ctx.root_path))
⋮----
# Get current branch
⋮----
current_branch = repo.active_branch.name
⋮----
# Handle detached HEAD state
current_branch = "HEAD"
⋮----
# Get current branch checksum (commit hash)
checksum = repo.head.commit.hexsha
⋮----
# Get last commit timestamp
last_commit_timestamp = datetime.fromtimestamp(repo.head.commit.committed_date)
⋮----
# Get tags
tags = [tag.name for tag in repo.tags]
⋮----
# Get remote branch count
origin_branch_count = 0
⋮----
origin = repo.remotes.origin
⋮----
origin_branch_count = len(list(origin.refs))
⋮----
# No origin remote or no refs
⋮----
# Check for local dirty status
is_dirty = repo.is_dirty()
⋮----
# Create structured data
data = {
⋮----
# Create tags for the discovery result
discovery_tags = ["git", "vcs", "repository"]
````

## File: src/metagit/core/detect/__init__.py
````python
#!/usr/bin/env python3
"""
Detection module for metagit.

This module provides comprehensive repository detection and analysis capabilities,
including language detection, project classification, branch analysis, CI/CD detection,
and metrics collection.
"""
⋮----
# from .manager import DetectionManager
# from .models import (
#     BranchInfo,
#     CIConfigAnalysis,
#     DetectionManagerConfig,
#     GitBranchAnalysis,
#     LanguageDetection,
#     ProjectTypeDetection,
# )
⋮----
# __all__ = [
#     "DetectionManager",
#     "DetectionManagerConfig",
#     "LanguageDetection",
#     "ProjectTypeDetection",
#     "GitBranchAnalysis",
#     "CIConfigAnalysis",
#     "BranchInfo",
# ]
````

## File: src/metagit/core/init/__init__.py
````python
#!/usr/bin/env python
"""Metagit project initialization (templates and minimal profiles)."""
⋮----
__all__ = ["InitService", "InitWriteResult"]
````

## File: src/metagit/core/init/models.py
````python
#!/usr/bin/env python
"""Pydantic models for metagit init templates."""
⋮----
class InitPromptSpec(BaseModel)
⋮----
"""One copier-style prompt for template variable collection."""
⋮----
model_config = ConfigDict(extra="forbid")
⋮----
name: str = Field(..., description="Template variable name")
label: str = Field(..., description="Human-readable prompt label")
default: Optional[str] = Field(None, description="Static default value")
default_from: Optional[Literal["directory_name", "git_remote_url"]] = Field(
required: bool = Field(
secret: bool = Field(default=False, description="Hide input in interactive prompts")
⋮----
class InitTemplateFileSpec(BaseModel)
⋮----
"""Rendered file mapping for a template."""
⋮----
template: str = Field(
output: str = Field(
optional: bool = Field(
⋮----
class InitTemplateManifest(BaseModel)
⋮----
"""Manifest for a bundled init template (copier-style)."""
⋮----
id: str = Field(..., description="Template identifier for --template")
label: str = Field(..., description="Short label for --list-templates")
description: str = Field(..., description="Longer description of the template")
kind: str = Field(..., description="Default MetagitConfig kind for this template")
prompts: list[InitPromptSpec] = Field(default_factory=list)
files: list[InitTemplateFileSpec] = Field(default_factory=list)
````

## File: src/metagit/core/init/prompts.py
````python
#!/usr/bin/env python
"""Collect init template answers from defaults, files, and interactive prompts."""
⋮----
PromptFn = Callable[..., str]
⋮----
def load_answers_file(path: Path) -> dict[str, str]
⋮----
"""Load answers from a YAML or JSON mapping file."""
text = path.read_text(encoding="utf-8")
⋮----
loaded = json.loads(text)
⋮----
loaded = yaml.safe_load(text)
⋮----
"""Built-in default resolvers for template prompts."""
⋮----
"""Resolve the default value for one prompt spec."""
⋮----
"""
    Merge answers file, overrides, and interactive prompts.

    Raises click.UsageError when required values are missing in no_prompt mode.
    """
merged: dict[str, str] = {}
⋮----
builtins = build_builtin_defaults(
ask = prompt_fn or click.prompt
⋮----
default = resolve_prompt_default(prompt, builtins)
⋮----
value = ask(
````

## File: src/metagit/core/init/registry.py
````python
#!/usr/bin/env python
"""Load bundled init templates from package data."""
⋮----
class InitTemplateRegistry
⋮----
"""Discover and load init templates shipped under data/init-templates."""
⋮----
def __init__(self, root: Optional[Path] = None) -> None
⋮----
@property
    def root(self) -> Path
⋮----
def list_templates(self) -> list[InitTemplateManifest]
⋮----
"""Return all valid template manifests sorted by id."""
manifests: list[InitTemplateManifest] = []
⋮----
manifest = self.load_manifest(entry.name)
⋮----
def load_manifest(self, template_id: str) -> Optional[InitTemplateManifest]
⋮----
"""Load template.yaml for a template id."""
manifest_path = self._safe_template_path(template_id) / "template.yaml"
⋮----
raw = yaml.safe_load(handle)
⋮----
def template_dir(self, template_id: str) -> Path
⋮----
"""Return the directory containing template sources."""
⋮----
def _safe_template_path(self, template_id: str) -> Path
````

## File: src/metagit/core/init/renderer.py
````python
#!/usr/bin/env python
"""Render init template files and validate Metagit manifests."""
⋮----
_PLACEHOLDER = re.compile(r"\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}")
⋮----
def render_placeholders(content: str, context: dict[str, str]) -> str
⋮----
"""Replace ``{{ name }}`` placeholders (copier-style, no extra deps)."""
⋮----
def _replace(match: re.Match[str]) -> str
⋮----
key = match.group(1)
⋮----
def clean_manifest_payload(payload: dict[str, Any]) -> dict[str, Any]
⋮----
"""Remove empty optional keys before validation."""
cleaned = dict(payload)
⋮----
value = cleaned.get(key)
⋮----
def validate_metagit_yaml(content: str) -> MetagitConfig
⋮----
"""Parse and validate rendered .metagit.yml content."""
loaded = yaml.safe_load(content)
⋮----
class InitTemplateRenderer
⋮----
"""Render template files for a target directory."""
⋮----
source = template_dir / file_spec.template
⋮----
raw = source.read_text(encoding="utf-8")
⋮----
"""Render all files declared in the manifest."""
rendered: list[tuple[InitTemplateFileSpec, str]] = []
⋮----
content = self.render_file(template_dir, file_spec, context)
````

## File: src/metagit/core/init/service.py
````python
#!/usr/bin/env python
"""Orchestrate metagit init from templates or minimal kind profiles."""
⋮----
@dataclass
class InitWriteResult
⋮----
"""Files written during init."""
⋮----
metagit_yml: Path
extra_files: list[Path] = field(default_factory=list)
⋮----
@dataclass
class InitService
⋮----
"""Create .metagit.yml and optional companion files from templates or kinds."""
⋮----
registry: InitTemplateRegistry = field(default_factory=InitTemplateRegistry)
renderer: InitTemplateRenderer = field(default_factory=InitTemplateRenderer)
⋮----
def list_templates(self) -> list[InitTemplateManifest]
⋮----
def resolve_template_id(self, template: Optional[str], kind: Optional[str]) -> str
⋮----
"""Map --template or --kind to a bundled template id when a bundle exists."""
⋮----
candidate = kind.strip().lower()
⋮----
manifest = self.registry.load_manifest(template_id)
⋮----
file_answers: dict[str, str] = {}
⋮----
file_answers = load_answers_file(answers_file)
⋮----
context = collect_answers(
⋮----
template_dir = self.registry.template_dir(template_id)
rendered_files = self.renderer.render_manifest(template_dir, manifest, context)
⋮----
metagit_path = target_dir / ".metagit.yml"
⋮----
extra_paths: list[Path] = []
metagit_content: Optional[str] = None
⋮----
destination = target_dir / file_spec.output
⋮----
metagit_content = content
⋮----
"""Create a minimal validated manifest without a bundled template directory."""
⋮----
kind_value = ProjectKind(kind)
⋮----
allowed = ", ".join(item.value for item in ProjectKind)
⋮----
manager = MetagitConfigManager()
config_result = manager.create_config(
⋮----
payload = config_result.model_dump(
content = yaml.safe_dump(
````

## File: src/metagit/core/mcp/services/discovery_context.py
````python
#!/usr/bin/env python
"""
Deterministic discovery context packaging for config bootstrap.
"""
⋮----
class DiscoveryContextService
⋮----
"""Create deterministic discovery payloads for sampling prompts."""
⋮----
def build_context(self, repo_root: str) -> dict[str, str]
⋮----
"""Build a minimal deterministic context from repository files."""
root = Path(repo_root).expanduser().resolve()
hints: list[str] = []
⋮----
path = root / candidate
````

## File: src/metagit/core/mcp/services/gitnexus_registry.py
````python
#!/usr/bin/env python
"""
Read GitNexus global registry and per-repo index status.
"""
⋮----
class GitNexusRegistryAdapter
⋮----
"""Resolve GitNexus index metadata for local repository paths."""
⋮----
def __init__(self, registry_path: Optional[Path] = None) -> None
⋮----
def load_entries(self) -> list[dict[str, Any]]
⋮----
"""Load registry entries or return an empty list."""
⋮----
payload = json.loads(self._registry_path.read_text(encoding="utf-8"))
⋮----
def lookup_by_path(self, repo_path: str) -> Optional[dict[str, Any]]
⋮----
"""Find registry entry for a resolved repository path."""
target = str(Path(repo_path).expanduser().resolve())
⋮----
entry_path = entry.get("path")
⋮----
def index_status(self, repo_path: str) -> str
⋮----
"""Return indexed, stale, missing, or error for a repository path."""
entry = self.lookup_by_path(repo_path=repo_path)
⋮----
cli_status = self._status_via_cli(repo_path=repo_path)
⋮----
def _status_via_cli(self, repo_path: str) -> Optional[str]
⋮----
"""Parse `gitnexus status` output when the CLI is available."""
⋮----
completed = subprocess.run(
⋮----
output = (completed.stdout or "") + (completed.stderr or "")
⋮----
def summarize_for_paths(self, repo_paths: list[str]) -> dict[str, str]
⋮----
"""Map repo paths to gitnexus status strings."""
⋮----
def registry_name_for_path(self, repo_path: str) -> Optional[str]
⋮----
"""GitNexus registry alias for a checkout path (--repo flag value)."""
⋮----
name = entry.get("name")
````

## File: src/metagit/core/mcp/services/import_hint_scanner.py
````python
#!/usr/bin/env python
"""
Lightweight import/reference scanning between workspace repositories.
"""
⋮----
_PATH_DEP_PATTERN = re.compile(
_GO_REPLACE_PATTERN = re.compile(r"^\s*replace\s+[^\s]+\s+=>\s+(.+)$", re.MULTILINE)
_TERRAFORM_MODULE_PATTERN = re.compile(r'source\s*=\s*"([^"]+)"', re.IGNORECASE)
⋮----
class ImportHintScanner
⋮----
"""Detect cross-repo references from common manifest files."""
⋮----
"""Return import edges from one repo to other workspace repos."""
root = Path(repo_path).expanduser().resolve()
⋮----
hints: list[dict[str, Any]] = []
candidates = [
⋮----
text = candidate.read_text(encoding="utf-8")
⋮----
"""Scan npm package.json for file/workspace path dependencies."""
⋮----
payload = json.loads(text)
⋮----
sections = []
⋮----
value = payload.get(key)
⋮----
target_id = self._resolve_reference(
⋮----
"""Scan go.mod replace directives for local paths."""
⋮----
reference = match.group(1).strip()
⋮----
"""Scan generic text for file:/path: references."""
⋮----
"""Scan terraform files for module sources pointing at sibling repos."""
⋮----
text = tf_file.read_text(encoding="utf-8")
⋮----
"""Resolve a relative reference to another workspace repo node id."""
_ = file_path
cleaned = reference.removeprefix("file:").strip()
⋮----
resolved = (root / cleaned).resolve()
⋮----
repo_root = Path(repo_path).resolve()
````

## File: src/metagit/core/mcp/services/ops_log.py
````python
#!/usr/bin/env python
"""
Bounded operations log for MCP runtime.
"""
⋮----
class OperationsLogService
⋮----
"""Store a bounded in-memory operations trail."""
⋮----
def __init__(self, capacity: int = 100) -> None
⋮----
def append(self, action: str, detail: str) -> None
⋮----
"""Append an operation log entry."""
⋮----
def list_entries(self) -> list[dict[str, str]]
⋮----
"""List operation log entries."""
````

## File: src/metagit/core/mcp/services/repo_git_stats.py
````python
#!/usr/bin/env python
"""
Git statistics helpers for repository inspect and snapshot flows.
"""
⋮----
def inspect_repo_state(repo_path: str) -> dict[str, str | bool | int | float | None]
⋮----
"""Return branch, dirty flag, ahead/behind, uncommitted count, and age metrics."""
⋮----
repo = Repo(repo_path)
branch = (
dirty = repo.is_dirty(untracked_files=True)
⋮----
uncommitted = _uncommitted_count(repo=repo, dirty=dirty)
head_age_days = head_commit_age_days(repo=repo)
merge_age_days = merge_base_age_days(repo=repo)
⋮----
def _ahead_behind(repo: Repo) -> tuple[Optional[int], Optional[int]]
⋮----
"""Return ahead and behind counts relative to upstream when configured."""
⋮----
tracking = repo.active_branch.tracking_branch()
⋮----
counts = repo.git.rev_list(
parts = counts.split("\t")
⋮----
behind = int(parts[0])
ahead = int(parts[1])
⋮----
def head_commit_age_days(repo: Repo) -> Optional[float]
⋮----
"""Return days elapsed since HEAD commit timestamp (committed datetime)."""
⋮----
commit = repo.head.commit
authored = getattr(commit, "committed_datetime", None)
⋮----
authored = authored.replace(tzinfo=timezone.utc)
delta = datetime.now(timezone.utc) - authored.astimezone(timezone.utc)
⋮----
def merge_base_age_days(repo: Repo) -> Optional[float]
⋮----
"""
    Return days since merge-base(H, default remote branch) committed.

    Signals how stale the integration point with the remote default branch is.
    """
⋮----
head_ref = str(repo.head.commit.hexsha)
⋮----
head_ref = "HEAD"
default_ref = _resolve_origin_default(repo=repo)
⋮----
bases = repo.git.merge_base(head_ref, default_ref).strip()
⋮----
base_hex = bases.split()[0].strip()
commit = repo.commit(base_hex)
⋮----
def _resolve_origin_default(repo: Repo) -> Optional[str]
⋮----
"""Return ref like origin/main usable for merge-base, or None."""
⋮----
out = repo.git.symbolic_ref("refs/remotes/origin/HEAD").strip()
⋮----
def _uncommitted_count(repo: Repo, dirty: bool) -> int
⋮----
"""Estimate uncommitted change count."""
⋮----
staged = len(repo.index.diff("HEAD"))
unstaged = len(repo.index.diff(None))
untracked = len(repo.untracked_files)
````

## File: src/metagit/core/mcp/services/workspace_semantic_search.py
````python
#!/usr/bin/env python
"""
GitNexus semantic workspace search (vector-ranked query per repository).
"""
⋮----
class WorkspaceSemanticSearchService
⋮----
"""Run `gitnexus query` across workspace checkouts with a registry name."""
⋮----
_gitnexus_pkg = "gitnexus@1.6.4"
⋮----
def __init__(self, registry: Optional[GitNexusRegistryAdapter] = None) -> None
⋮----
"""Run semantic query for each indexed GitNexus repo path."""
trimmed = query.strip()
⋮----
results: list[dict[str, Any]] = []
⋮----
name = self._registry.registry_name_for_path(repo_path=repo_path)
⋮----
any_ok = any(item.get("ok") for item in results)
⋮----
"""Execute gitnexus query and parse JSON payload from stdout."""
cmd: list[str] = [
⋮----
completed = subprocess.run(
⋮----
combined = (completed.stdout or "") + "\n" + (completed.stderr or "")
payload = self._parse_query_json(stdout=combined)
⋮----
warning = payload.get("warning") if isinstance(payload, dict) else None
⋮----
def _parse_query_json(self, stdout: str) -> Optional[dict[str, Any]]
⋮----
"""Extract the primary JSON object with processes from CLI output."""
⋮----
line = line.strip()
⋮----
decoded = json.loads(line)
````

## File: src/metagit/core/mcp/services/workspace_snapshot.py
````python
#!/usr/bin/env python
"""
Workspace snapshot create and restore for MCP tools.
"""
⋮----
class WorkspaceSnapshotService
⋮----
"""Capture and restore workspace git-state manifests."""
⋮----
"""Create a snapshot manifest for scoped repositories."""
store = SessionStore(workspace_root=workspace_root)
meta = store.get_workspace_meta()
active_project = project_name or meta.active_project
rows = self._index.build_index(config=config, workspace_root=workspace_root)
scoped_rows = (
⋮----
snapshot_id = str(uuid.uuid4())
repo_states: list[SnapshotRepoState] = []
⋮----
env_key_names: list[str] = []
⋮----
env_key_names = self._context.list_env_export_keys(
⋮----
session_ref = (
snapshot = WorkspaceSnapshot(
⋮----
"""Restore session metadata from a snapshot; does not mutate git state."""
snapshot = self._load_snapshot(
⋮----
notes = [
context = None
⋮----
context = self._context.switch(
⋮----
session_path = Path(workspace_root) / snapshot.session_ref
⋮----
def _snapshot_repo_row(self, row: dict[str, Any]) -> SnapshotRepoState
⋮----
"""Build snapshot repo state from an index row."""
exists = bool(row.get("exists"))
branch: Optional[str] = None
dirty = False
ahead: Optional[int] = None
behind: Optional[int] = None
uncommitted: Optional[int] = None
inspect_error: Optional[str] = None
⋮----
inspected = inspect_repo_state(repo_path=str(row["repo_path"]))
⋮----
branch = str(inspected["branch"]) if inspected.get("branch") else None
dirty = bool(inspected.get("dirty", False))
ahead_val = inspected.get("ahead")
behind_val = inspected.get("behind")
ahead = int(ahead_val) if isinstance(ahead_val, int) else None
behind = int(behind_val) if isinstance(behind_val, int) else None
uncommitted_val = inspected.get("uncommitted_count")
uncommitted = (
⋮----
inspect_error = str(inspected.get("error", "inspect failed"))
⋮----
def _write_snapshot(self, workspace_root: str, snapshot: WorkspaceSnapshot) -> Path
⋮----
"""Write snapshot JSON to workspace .metagit/snapshots."""
snapshots_dir = Path(workspace_root) / ".metagit" / "snapshots"
⋮----
path = snapshots_dir / f"{snapshot.snapshot_id}.json"
⋮----
"""Load snapshot by id."""
path = Path(workspace_root) / ".metagit" / "snapshots" / f"{snapshot_id}.json"
⋮----
payload = json.loads(path.read_text(encoding="utf-8"))
⋮----
"""Copy a snapshot-linked session file into the live session store."""
⋮----
payload = json.loads(session_path.read_text(encoding="utf-8"))
⋮----
session = ProjectSession.model_validate(payload)
````

## File: src/metagit/core/mcp/services/workspace_sync.py
````python
#!/usr/bin/env python
"""
Batch workspace repository synchronization for MCP tools.
"""
⋮----
class WorkspaceSyncService
⋮----
"""Synchronize many workspace repositories with guardrails."""
⋮----
def __init__(self, repo_ops: Optional[RepoOperationsService] = None) -> None
⋮----
"""Sync selected repositories and return per-repo results."""
selected_rows = self._select_rows(repo_rows=repo_rows, repos=repos)
normalized_mode = mode.lower()
normalized_only_if = only_if.lower()
⋮----
parallel = max(1, min(max_parallel, 16))
results: list[dict[str, Any]] = []
⋮----
def run_row(row: dict[str, Any]) -> dict[str, Any]
⋮----
futures = {executor.submit(run_row, row): row for row in selected_rows}
⋮----
summary = {
⋮----
"""Filter index rows by repo selectors."""
⋮----
selectors = {item.strip() for item in repos if item.strip()}
selected: list[dict[str, Any]] = []
⋮----
repo_path = str(row.get("repo_path", ""))
repo_name = str(row.get("repo_name", ""))
project_name = str(row.get("project_name", ""))
keys = {repo_path, repo_name, f"{project_name}/{repo_name}"}
⋮----
"""Sync one repository row."""
base = {
⋮----
exists = bool(row.get("exists"))
is_git_repo = bool(row.get("is_git_repo"))
⋮----
origin_url = str(row.get("url")) if row.get("url") else None
⋮----
outcome = self._repo_ops.sync(
⋮----
"""Determine whether a repository should be synchronized."""
⋮----
inspected = inspect_repo_state(repo_path=repo_path)
⋮----
behind = inspected.get("behind")
````

## File: src/metagit/core/mcp/services/workspace_template.py
````python
#!/usr/bin/env python
"""
Apply packaged workspace templates to workspace projects.
"""
⋮----
class WorkspaceTemplateService
⋮----
"""Copy template files into workspace project directories."""
⋮----
def __init__(self, index_service: Optional[WorkspaceIndexService] = None) -> None
⋮----
"""Preview or apply a template to target workspace projects."""
template_dir = self._resolve_template_dir(template=template)
⋮----
rows = self._index.build_index(config=config, workspace_root=workspace_root)
project_names = {project.name for project in (config.workspace.projects or [])}
results: list[dict[str, Any]] = []
⋮----
target_root = self._project_target_root(
⋮----
planned = self._plan_copy(
⋮----
written = self._execute_copy(
⋮----
ok = all(item.get("ok") for item in results)
⋮----
def list_templates(self) -> list[str]
⋮----
"""Return available template names."""
root = self._templates_root()
⋮----
def _templates_root(self) -> Path
⋮----
"""Return packaged templates directory."""
⋮----
def _resolve_template_dir(self, template: str) -> Optional[Path]
⋮----
"""Resolve template directory if it exists."""
⋮----
candidate = self._templates_root() / template
⋮----
"""Choose a directory to receive template files for a project."""
project_rows = [row for row in rows if row.get("project_name") == project_name]
⋮----
candidate = Path(workspace_root) / project_name
⋮----
def _plan_copy(self, template_dir: Path, target_root: str) -> list[dict[str, str]]
⋮----
"""List files that would be copied."""
planned: list[dict[str, str]] = []
target = Path(target_root)
⋮----
relative = source.relative_to(template_dir)
destination = target / relative
⋮----
"""Copy planned files, skipping destinations that already exist."""
written: list[dict[str, str]] = []
⋮----
relative = item["relative_path"]
````

## File: src/metagit/core/mcp/tools/bootstrap_plan_only.py
````python
#!/usr/bin/env python
"""
Plan-only bootstrap MCP tool implementation.
"""
⋮----
def metagit_bootstrap_config_plan_only(reason: Optional[str]) -> dict[str, str]
⋮----
"""Return bootstrap guidance when MCP sampling is not yet available."""
details = reason or "Workspace is not active. Generate or fix .metagit.yml first."
````

## File: src/metagit/core/mcp/tools/workspace_status.py
````python
#!/usr/bin/env python
"""
Workspace status MCP tool implementation.
"""
⋮----
def metagit_workspace_status(status: WorkspaceStatus) -> dict[str, str | None]
⋮----
"""Return structured workspace status details."""
````

## File: src/metagit/core/mcp/__init__.py
````python
#!/usr/bin/env python
"""
Metagit MCP core package.
"""
⋮----
__all__ = ["McpActivationState", "WorkspaceStatus"]
````

## File: src/metagit/core/mcp/gate.py
````python
#!/usr/bin/env python
"""
Workspace gate evaluation for Metagit MCP runtime.
"""
⋮----
class WorkspaceGate
⋮----
"""Evaluate whether workspace is active for MCP tool exposure."""
⋮----
_config_file_name: str = ".metagit.yml"
⋮----
def evaluate(self, root_path: Optional[str]) -> WorkspaceStatus
⋮----
"""Evaluate root state as active, missing config, or invalid config."""
⋮----
config_path = os.path.join(root_path, self._config_file_name)
⋮----
manager = MetagitConfigManager(config_path=Path(config_path))
result = manager.load_config()
````

## File: src/metagit/core/mcp/models.py
````python
#!/usr/bin/env python
"""
Pydantic models for Metagit MCP runtime state.
"""
⋮----
class McpActivationState(str, Enum)
⋮----
"""Activation state for MCP workspace gating."""
⋮----
ACTIVE = "active"
INACTIVE_MISSING_CONFIG = "inactive_missing_config"
INACTIVE_INVALID_CONFIG = "inactive_invalid_config"
⋮----
class WorkspaceStatus(BaseModel)
⋮----
"""Current workspace status used by MCP tools and resources."""
⋮----
state: McpActivationState = Field(
root_path: Optional[str] = Field(
reason: Optional[str] = Field(
````

## File: src/metagit/core/mcp/protocols.py
````python
#!/usr/bin/env python
"""
Protocol contracts for Metagit MCP components.
"""
⋮----
class WorkspaceRootResolverProtocol(Protocol)
⋮----
"""Resolve the effective workspace root for the MCP runtime."""
⋮----
def resolve(self, cwd: str, cli_root: str | None = None) -> str | None
⋮----
"""Resolve and return a workspace root path, if available."""
⋮----
class WorkspaceGateProtocol(Protocol)
⋮----
"""Evaluate whether MCP should expose active tooling."""
⋮----
def evaluate(self, root_path: str | None) -> WorkspaceStatus
⋮----
"""Return the current workspace status for tool gating."""
````

## File: src/metagit/core/mcp/root_resolver.py
````python
#!/usr/bin/env python
"""
Workspace root resolution for Metagit MCP runtime.
"""
⋮----
class WorkspaceRootResolver
⋮----
"""Resolve workspace root for `.metagit.yml` gating."""
⋮----
_config_file_name: str = ".metagit.yml"
_env_var_name: str = "METAGIT_WORKSPACE_ROOT"
⋮----
def resolve(self, cwd: str, cli_root: Optional[str] = None) -> Optional[str]
⋮----
"""Resolve workspace root by env var, CLI option, then upward walk."""
env_root = os.getenv(self._env_var_name)
⋮----
def _walk_for_config(self, cwd: str) -> Optional[str]
⋮----
"""Walk up the directory tree until `.metagit.yml` is found."""
current = Path(cwd).expanduser().resolve()
⋮----
config_path = os.path.join(str(current), self._config_file_name)
⋮----
current = current.parent
````

## File: src/metagit/core/project/search_models.py
````python
#!/usr/bin/env python
"""
Runtime models for managed repository search and resolve results.
"""
⋮----
class ManagedRepoStatus(BaseModel)
⋮----
"""Resolved sync state for a managed workspace repository row."""
⋮----
resolved_path: str
exists: bool
is_git_repo: bool
sync_enabled: bool
status: str
⋮----
class ManagedRepoMatch(BaseModel)
⋮----
"""One search hit with scoring metadata."""
⋮----
project_name: str
repo_name: str
url: str | None = None
configured_path: str | None = None
tags: dict[str, str] = Field(default_factory=dict)
status: ManagedRepoStatus
match_reasons: list[str] = Field(default_factory=list)
score: int
⋮----
class ManagedRepoSearchResult(BaseModel)
⋮----
"""Ordered search hits for a query."""
⋮----
query: str
matches: list[ManagedRepoMatch] = Field(default_factory=list)
⋮----
class ManagedRepoError(BaseModel)
⋮----
"""Structured error from resolve_one."""
⋮----
kind: str
message: str
⋮----
class ManagedRepoResolveResult(BaseModel)
⋮----
"""Single-match resolution or an error."""
⋮----
match: ManagedRepoMatch | None = None
error: ManagedRepoError | None = None
````

## File: src/metagit/core/project/source_models.py
````python
#!/usr/bin/env python
"""
Models for provider-based recursive repository discovery and sync planning.
"""
⋮----
class SourceProvider(str, Enum)
⋮----
"""Supported source providers for recursive discovery."""
⋮----
GITHUB = "github"
GITLAB = "gitlab"
⋮----
class SourceSyncMode(str, Enum)
⋮----
"""Supported sync planning modes."""
⋮----
DISCOVER = "discover"
ADDITIVE = "additive"
RECONCILE = "reconcile"
⋮----
class SourceSpec(BaseModel)
⋮----
"""Input specification for source-backed repository discovery."""
⋮----
provider: SourceProvider = Field(..., description="Source provider")
org: Optional[str] = Field(None, description="GitHub organization")
user: Optional[str] = Field(None, description="GitHub user")
group: Optional[str] = Field(None, description="GitLab group path")
recursive: bool = Field(
include_archived: bool = Field(
include_forks: bool = Field(False, description="Include forked repositories")
path_prefix: Optional[str] = Field(
⋮----
@model_validator(mode="after")
    def validate_scope(self) -> "SourceSpec"
⋮----
selectors = [self.org, self.user]
⋮----
@property
    def namespace_key(self) -> str
⋮----
"""Canonical source namespace used for provenance and reconcile boundaries."""
⋮----
class DiscoveredRepo(BaseModel)
⋮----
"""Normalized repository shape discovered from provider APIs."""
⋮----
provider: SourceProvider
namespace: str
full_name: str
name: str
clone_url: str
default_branch: Optional[str] = None
description: Optional[str] = None
repo_id: Optional[str] = None
archived: bool = False
fork: bool = False
private: Optional[bool] = None
⋮----
class SourceSyncPlan(BaseModel)
⋮----
"""Computed workspace changes for source-backed sync modes."""
⋮----
discovered_count: int = 0
unchanged: int = 0
to_add: List[ProjectPath] = Field(default_factory=list)
to_update: List[ProjectPath] = Field(default_factory=list)
to_remove: List[ProjectPath] = Field(default_factory=list)
````

## File: src/metagit/core/prompt/__init__.py
````python
#!/usr/bin/env python
"""
Metagit prompt emission for agent workflows.
"""
⋮----
__all__ = [
````

## File: src/metagit/core/prompt/catalog.py
````python
#!/usr/bin/env python
"""
Built-in operational prompts for metagit agents by scope.
"""
⋮----
_CATALOG: list[PromptCatalogEntry] = [
⋮----
_SCOPE_KINDS: dict[PromptScope, frozenset[PromptKind]] = {
⋮----
def list_catalog() -> list[PromptCatalogEntry]
⋮----
"""Return all registered prompt kinds."""
⋮----
def kinds_for_scope(scope: PromptScope) -> list[PromptKind]
⋮----
"""Prompt kinds valid for a scope level."""
⋮----
def is_kind_allowed(kind: PromptKind, scope: PromptScope) -> bool
⋮----
"""True when kind can be emitted at scope."""
⋮----
"""Return built-in prompt text for a kind and scope."""
project_label = project_name or "<project>"
repo_label = repo_name or "<repo>"
templates: dict[PromptKind, str] = {
⋮----
body = templates.get(kind, "")
````

## File: src/metagit/core/prompt/models.py
````python
#!/usr/bin/env python
"""
Pydantic models for metagit prompt emission.
"""
⋮----
PromptScope = Literal["workspace", "project", "repo"]
PromptKind = Literal[
⋮----
class PromptCatalogEntry(BaseModel)
⋮----
"""Metadata for one built-in prompt kind."""
⋮----
kind: PromptKind
title: str
description: str
scopes: list[PromptScope]
⋮----
class PromptEmitResult(BaseModel)
⋮----
"""Emitted prompt for agents or humans."""
⋮----
ok: bool = True
⋮----
scope: PromptScope
project_name: Optional[str] = None
repo_name: Optional[str] = None
definition_path: str = ""
instruction_layers: list[AgentInstructionLayer] = Field(default_factory=list)
text: str = ""
metadata: dict[str, Any] = Field(default_factory=dict)
````

## File: src/metagit/core/prompt/service.py
````python
#!/usr/bin/env python
"""
Emit metagit prompts for workspace, project, and repo scopes.
"""
⋮----
class PromptServiceError(Exception)
⋮----
"""Raised when prompt emission cannot proceed."""
⋮----
class PromptService
⋮----
"""Resolve and render prompts for agent consumption."""
⋮----
def __init__(self) -> None
⋮----
def list_entries(self) -> list[PromptCatalogEntry]
⋮----
"""List catalog metadata for all prompt kinds."""
⋮----
"""Emit a prompt for the requested kind and scope."""
⋮----
allowed = ", ".join(kinds_for_scope(scope))
⋮----
composition = self._resolver.resolve(
⋮----
text = composition.effective
⋮----
body = template_body(
sections: list[str] = [body]
⋮----
project = next(
⋮----
repo = self._resolver.find_repo(project, repo_name=repo_name)
⋮----
project_count = len(config.workspace.projects) if config.workspace else 0
repo_count = 0
⋮----
repo_count = sum(len(item.repos) for item in config.workspace.projects)
effective_dedupe: Optional[bool] = None
project_dedupe_override: Optional[bool] = None
⋮----
effective = resolve_effective_dedupe(workspace_dedupe, project)
effective_dedupe = effective is not None
⋮----
project_dedupe_override = project.dedupe.enabled
````

## File: src/metagit/core/utils/logging.py
````python
#!/usr/bin/env python
⋮----
"""
logging class that does general logging via loguru and prints to the console via rich.
"""
⋮----
class LoggerProtocol(Protocol)
⋮----
"""
    Protocol for the logger interface.
    This includes:
        Standard log methods (info, debug, error, etc.)
        Rich printing methods (print_json, print_info, print_task_status, etc.)
        Optional console formatting methods (header, footer, etc.)
        Return type of Union[None, Exception]
    Notes:
        - All the ... are placeholders for the actual methods per the Protocol definition
        - This is a workaround to allow for the logger to be injected into classes that don't inherit from LoggingModel
    """
⋮----
# === Core logging methods ===
def debug(self, message: str) -> Union[None, Exception]: ...
def info(self, message: str) -> Union[None, Exception]: ...
def warning(self, message: str) -> Union[None, Exception]: ...
def error(self, message: str) -> Union[None, Exception]: ...
def critical(self, message: str) -> Union[None, Exception]: ...
def exception(self, message: str) -> Union[None, Exception]: ...
⋮----
# === Print helpers ===
def print_info(self, message: str) -> Union[None, Exception]: ...
⋮----
def print_error(self, error_message: str) -> Union[None, Exception]: ...
def print_success(self, message: str) -> Union[None, Exception]: ...
def print_input(self, input_data: dict[str, Any]) -> Union[None, Exception]: ...
def print_output(self, output_data: Any) -> Union[None, Exception]: ...
⋮----
# === Console formatting ===
⋮----
def footer(self, text: str, console: bool = True) -> Union[None, Exception]: ...
def line(self, console: bool = True) -> Union[None, Exception]: ...
def success(self, text: str, console: bool = True) -> Union[None, Exception]: ...
def proc_out(self, text: str, console: bool = True) -> Union[None, Exception]: ...
⋮----
# Add any others you rely on via injection (e.g. print_json, print_info)
⋮----
# Logger configuration
class LoggerConfig(BaseModel)
⋮----
"""
    Pydantic model for unified logger configuration.
    """
⋮----
# Logging configuration
name: Optional[str] = Field(default="metagit", description="Logger name.")
log_level: Literal["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "TRACE"] = (
log_to_file: bool = Field(default=False, description="Whether to log to a file.")
log_file_path: str = Field(default="app.log", description="Path to the log file.")
json_logs: bool = Field(
rotation: str = Field(default="10 MB", description="Log file rotation policy.")
retention: str = Field(default="7 days", description="Log file retention policy.")
backtrace: bool = Field(default=False, description="Enable backtrace in logs.")
diagnose: bool = Field(default=False, description="Enable diagnose in logs.")
⋮----
# Console output configuration
minimal_console: bool = Field(
use_rich_console: bool = Field(
terse: bool = Field(
⋮----
class Config
⋮----
env_prefix = "LOG_"
⋮----
LOG_LEVEL_MAP: dict[str, int] = {
⋮----
LOG_LEVELS: dict[int, int] = {
⋮----
}  #: a mapping of `verbose` option counts to logging levels
⋮----
class UnifiedLogger(LoggerProtocol)
⋮----
def __init__(self, config: LoggerConfig)
⋮----
# Initialize rich console if enabled
⋮----
theme = Theme(
⋮----
# Remove default loguru handler
⋮----
# Choose formatting
⋮----
log_format = "{message}"
serialize = config.json_logs
⋮----
log_format = (
serialize = False
⋮----
# Console sink
⋮----
# Optional file sink
⋮----
"""
        Set the logging level for all handlers.
        Args:
            level: The new logging level to set
        """
⋮----
# Update stdout handler
⋮----
# Update file handler if it exists
⋮----
def _intercept_std_logging(self) -> Union[None, Exception]
⋮----
"""Intercept standard logging module output to loguru."""
⋮----
class InterceptHandler(logging.Handler)
⋮----
def emit(self, record: logging.LogRecord) -> None
⋮----
level = logger.level(record.levelname).name
⋮----
level = record.levelno
⋮----
frame = frame.f_back
⋮----
def get_logger(self) -> Union[Any, Exception]
⋮----
"""Get the underlying loguru logger instance."""
⋮----
"""Print debug messages with rich formatting."""
⋮----
"""Print a message from an agent with rich formatting."""
⋮----
"""Print task status information with rich formatting."""
⋮----
content = f"{task_name}\nStatus: {status}"
⋮----
"""Print crew status messages with rich formatting."""
⋮----
def print_input(self, input_data: dict[str, Any]) -> Union[None, Exception]
⋮----
"""Print input data with rich formatting."""
⋮----
def print_output(self, output_data: Any) -> Union[None, Exception]
⋮----
"""Print output data with rich formatting."""
⋮----
def print_error(self, error_message: str) -> Union[None, Exception]
⋮----
"""Print error messages with rich formatting."""
⋮----
def print_success(self, message: str) -> Union[None, Exception]
⋮----
"""Print success messages with rich formatting."""
⋮----
def print_info(self, message: str) -> Union[None, Exception]
⋮----
"""Print informational messages with rich formatting."""
⋮----
"""Print JSON data with rich formatting and syntax highlighting."""
⋮----
json_str = json.dumps(data, indent=2)
⋮----
"""Print JSON data only if in debug mode."""
⋮----
# Direct loguru methods
def debug(self, message: str) -> Union[None, Exception]
⋮----
"""Log a debug message."""
⋮----
def info(self, message: str) -> Union[None, Exception]
⋮----
"""Log an info message."""
⋮----
def warning(self, message: str) -> Union[None, Exception]
⋮----
"""Log a warning message."""
⋮----
def error(self, message: str) -> Union[None, Exception]
⋮----
"""Log an error message."""
⋮----
def critical(self, message: str) -> Union[None, Exception]
⋮----
"""Log a critical message."""
⋮----
def exception(self, message: str) -> Union[None, Exception]
⋮----
"""Log an exception with traceback."""
⋮----
"""
        Helper to format and print output using rich console.
        Args:
            message (str): The message to print.
            style (str): The rich style to use.
            title (str, optional): The panel title. Defaults to None.
        """
⋮----
panel_title = f"[{style}]{title}[/{style}]" if title else None
⋮----
# Fallback for non-rich, non-json logging
⋮----
def header(self, text: str, console: bool = None) -> Union[None, Exception]
⋮----
"""Prints a header"""
⋮----
console = self.config.use_rich_console
⋮----
"""Prints a parameter line"""
⋮----
"""Prints a config element"""
⋮----
def footer(self, text: str, console: bool = True) -> Union[None, Exception]
⋮----
"""Prints a footer"""
⋮----
def proc_out(self, text: str, console: bool = True) -> Union[None, Exception]
⋮----
"""Prints a process output"""
⋮----
def line(self, console: bool = True) -> Union[None, Exception]
⋮----
"""Prints a line"""
⋮----
def success(self, text: str, console: bool = True) -> Union[None, Exception]
⋮----
"""Prints a success message"""
⋮----
"""
        Echo text to console with optional color and dimming.
        Args:
            text: Text to echo
            color: Color to use
            dim: Whether to dim the text
            console: Whether to output to console
        """
⋮----
style = f"{color} dim" if dim else color
⋮----
def get_logger(name: str = "metagit") -> Any
⋮----
"""
    Get a logger instance with default configuration.

    Args:
        name: Logger name

    Returns:
        UnifiedLogger instance
    """
config = LoggerConfig(name=name)
⋮----
class LoggingModel(BaseModel)
⋮----
_logger: Any = PrivateAttr()
⋮----
def __init__(self, **kwargs)
⋮----
@property
    def logger(self)
⋮----
def set_logger(self, logger)
````

## File: src/metagit/core/utils/userprompt.py
````python
#!/usr/bin/env python
"""
UserPrompt utility for dynamically prompting users for Pydantic object properties.
"""
⋮----
T = TypeVar("T", bound=BaseModel)
⋮----
_pt_cache: Optional[SimpleNamespace] = None
_prompt_style_cache: Any = None
⋮----
def _promptkit() -> SimpleNamespace
⋮----
"""
    Lazily import prompt_toolkit so CLI modules can load without it.

    Interactive commands require the dependency; install with metagit-cli or
    ``pip install 'prompt-toolkit>=3.0'``.
    """
⋮----
_pt_cache = SimpleNamespace(
⋮----
def _prompt_style() -> Any
⋮----
pk = _promptkit()
_prompt_style_cache = pk.Style.from_dict(
⋮----
class UserPrompt
⋮----
"""
    A utility class for prompting users to input values for Pydantic model properties.

    This class provides methods to interactively collect user input for required
    fields of any Pydantic model, with validation and type conversion using prompt_toolkit.
    """
⋮----
def __init__(self) -> None
⋮----
"""
        Prompt the user for fields of a Pydantic model.

        Args:
            model_class: The Pydantic model class to prompt for
            existing_data: Optional existing data to pre-populate fields
            title: Optional title to display at the top of the prompt
            fields_to_prompt: Optional list of field names to prompt for.
                             If None, prompts for all fields. If specified,
                             only prompts for these fields and uses defaults/None for others.

        Returns:
            An instance of the specified Pydantic model

        Raises:
            ValueError: If the model_class is not a valid Pydantic model
        """
⋮----
existing_data = existing_data or {}
field_data = {}
prompt_instance = UserPrompt()
⋮----
# Get model fields
model_fields = model_class.model_fields
⋮----
# Print title
title_text = pk.FormattedText([("class:title", f"\n=== {title} ===\n")])
⋮----
# Skip if field already has a value
⋮----
success_text = pk.FormattedText(
⋮----
# If fields_to_prompt is specified, only prompt for those fields
⋮----
# Use default value or None for non-prompted fields
⋮----
default_value = field_info.get_default()
⋮----
default_value = None
⋮----
# Check if field is required
is_required = field_info.is_required()
⋮----
value = prompt_instance._prompt_for_field(field_name, field_info)
⋮----
# For optional fields, prompt directly with [Optional] indicator
value = prompt_instance._prompt_for_optional_field(
⋮----
# Only assign if a value was provided (not None)
⋮----
# Create and validate the model instance
⋮----
error_text = pk.FormattedText(
⋮----
# Retry with corrected data
⋮----
"""
        Prompt the user for a specific field value.

        Args:
            field_name: Name of the field
            field_info: Field information from Pydantic

        Returns:
            The user input value, converted to appropriate type
        """
⋮----
field_type = field_info.annotation
description = field_info.description or ""
⋮----
# Get the actual default value for Pydantic v2
⋮----
# Filter out PydanticUndefined
⋮----
# Build formatted prompt message
prompt_parts = [("class:field", f"\n{field_name}")]
⋮----
prompt_text = pk.FormattedText(prompt_parts)
⋮----
# Create validator for the field type
validator = self._create_field_validator(field_type, field_info)
⋮----
# Get user input with validation
⋮----
user_input = self.session.prompt(
⋮----
# Handle default value
⋮----
# Handle empty input for required fields
⋮----
# Convert and return the input
converted_value = self._convert_input(user_input, field_type)
⋮----
# This should be caught by the validator, but as a fallback
⋮----
"""
        Prompt the user for an optional field value.

        Args:
            field_name: Name of the field
            field_info: Field information from Pydantic

        Returns:
            The user input value (converted to appropriate type) or None if no value provided
        """
⋮----
# Build formatted prompt message with [Optional] indicator
⋮----
# Add [Optional] indicator
⋮----
# Handle empty input for optional fields - return None
⋮----
"""
        Create a validator for the given field type.

        Args:
            field_type: The type to validate against
            field_info: Field information from Pydantic (optional)
            field_name: Name of the field (optional)

        Returns:
            A prompt_toolkit Validator instance or None
        """
⋮----
def validate_type(text: str) -> bool
⋮----
return True  # Allow empty input for optional fields
⋮----
# Check if this is a boolean field
is_bool_field = False
⋮----
# Method 1: Check if field_info.annotation is bool
⋮----
# Handle Optional[bool], which is Union[bool, None]
⋮----
is_bool_field = True
⋮----
# For other types, try to convert
⋮----
@staticmethod
    def _convert_input(user_input: str, target_type: Any) -> Union[Any, Exception]
⋮----
"""
        Convert user input to the target type, handling various types like lists and JSON.

        Args:
            user_input: Raw user input string
            target_type: Target type to convert to

        Returns:
            The converted value
        """
⋮----
# Handle Optional types
⋮----
# Get the non-None type from Optional[T] or Union[T, None]
args = [arg for arg in target_type.__args__ if arg is not type(None)]
⋮----
target_type = args[0]
⋮----
# For complex unions, we can't reliably convert, so just return string
⋮----
# Handle lists
⋮----
item_type = (
⋮----
)  # Default to list of strings
# Split by comma and strip whitespace
items = [item.strip() for item in user_input.split(",")]
# Convert each item to the target type
converted_list = [
exception_items = [
⋮----
# Handle JSON/Dict
⋮----
# Handle boolean conversion
⋮----
# Default conversion
⋮----
"""
        Prompt the user for a single field value.

        Args:
            field_name: Name of the field
            field_type: Type of the field
            description: Optional description of the field
            default: Optional default value

        Returns:
            The user input value, converted to the specified type
        """
⋮----
_ = {
# Mock field_info object for _prompt_for_field
mock_field_info = SimpleNamespace(
⋮----
@staticmethod
    def confirm_action(message: str = "Continue?") -> Union[bool, Exception]
⋮----
"""
        Prompt the user for a yes/no confirmation.

        Args:
            message: The confirmation message

        Returns:
            bool: True if user confirms, False otherwise
        """
⋮----
session = pk.PromptSession(style=_prompt_style())
prompt_text = pk.FormattedText([("class:field", f"\n{message} (y/n): ")])
⋮----
response = session.prompt(prompt_text).strip().lower()
⋮----
"""
        Prompt the user for specific fields of a Pydantic model.

        Args:
            model_class: The Pydantic model class to prompt for
            fields_to_prompt: List of field names to prompt for
            existing_data: Optional existing data to pre-populate fields
            title: Optional title to display at the top of the prompt

        Returns:
            An instance of the specified Pydantic model
        """
⋮----
def yes_no_prompt(message: str = "Continue?") -> bool
⋮----
"""
    Simple yes/no prompt function.

    Args:
        message: The message to display

    Returns:
        True for 'y' or 'yes', False for 'n' or 'no'
    """
⋮----
response = input(f"{message} (y/n): ").lower().strip()
````

## File: src/metagit/core/utils/yaml_class.py
````python
#!/usr/bin/env python
⋮----
"""
yaml class that can load yaml files with includes and envvars and check for duplicate keys.
"""
⋮----
LegacyYAMLLoader = (os.getenv("LEGACY_YAML_LOADER", "false")).lower() == "true"
⋮----
"""Check for duplicate keys."""
⋮----
mapping = {}
⋮----
key = loader.construct_object(key_node, deep=deep)
value = loader.construct_object(value_node, deep=deep)
⋮----
class ExtLoaderMeta(type)
⋮----
"""External yaml loader metadata class."""
⋮----
"""Add constructers to class."""
⋮----
cls = super().__new__(metacls, __name__, __bases__, __dict__)
⋮----
# register the include constructors on the class
⋮----
class ExtLoader(yaml.Loader, metaclass=ExtLoaderMeta)
⋮----
"""YAML Loader with additional constructors."""
⋮----
def __init__(self, stream: Any) -> None
⋮----
"""Initialise Loader."""
⋮----
streamdata = stream if isinstance(stream, str) else stream.name
⋮----
def construct_include(self, node: yaml.Node) -> Union[Any, Exception]
⋮----
"""Include file referenced at node."""
⋮----
file_name = os.path.abspath(
extension = os.path.splitext(file_name)[1].lstrip(".")
⋮----
data = yaml.load(  # nosec B506 — trusted workspace includes; custom Loader tags
⋮----
data = json.load(f)
⋮----
includedata = []
line = f.readline()
cnt = 0
⋮----
data = "".join(includedata)
⋮----
data = '"' + "\\n".join(includedata) + '"'
⋮----
def construct_envvar(self, node: yaml.Node) -> Union[str, None, Exception]
⋮----
"""Expand env variable at node"""
⋮----
def load(*args: Any, **kwargs: Any) -> Union[Any, Exception]
````

## File: src/metagit/core/web/__init__.py
````python
#!/usr/bin/env python
"""Local web UI server components."""
````

## File: src/metagit/core/web/config_preview.py
````python
#!/usr/bin/env python
"""Render YAML previews for metagit web config editor."""
⋮----
PreviewStyle = Literal["normalized", "minimal", "disk"]
⋮----
_SENSITIVE_KEYS = SchemaTreeService.SENSITIVE_KEYS
⋮----
def read_disk_text(path: str) -> str
⋮----
"""Return on-disk file contents or empty string when missing."""
file_path = Path(path)
⋮----
def redact_secrets(payload: Any) -> Any
⋮----
"""Return a copy of nested dict/list data with sensitive string values masked."""
⋮----
redacted: dict[str, Any] = {}
⋮----
suffix = value[-4:] if len(value) > 4 else ""
⋮----
"""Serialize a metagit manifest for preview."""
⋮----
payload = config.model_dump(
⋮----
payload = config.model_dump(exclude_none=True, mode="json")
⋮----
"""Serialize application config for preview."""
⋮----
config_body = config.model_dump(
payload: dict[str, Any] = {"config": config_body}
⋮----
config_body = config.model_dump(mode="json")
payload = {
⋮----
payload = redact_secrets(payload)
````

## File: src/metagit/core/web/graph_service.py
````python
#!/usr/bin/env python
"""
Build unified workspace graph views for the web UI.
"""
⋮----
class GraphViewNode(BaseModel)
⋮----
"""Renderable node in the workspace relationship diagram."""
⋮----
id: str
label: str
kind: Literal["project", "repo"]
project_name: Optional[str] = None
repo_name: Optional[str] = None
⋮----
class GraphViewEdge(BaseModel)
⋮----
"""Renderable edge in the workspace relationship diagram."""
⋮----
from_id: str
to_id: str
type: str
label: Optional[str] = None
source: Literal["manual", "inferred", "structure"] = "inferred"
⋮----
class WorkspaceGraphView(BaseModel)
⋮----
"""Nodes and edges for diagram rendering."""
⋮----
ok: bool = True
nodes: list[GraphViewNode] = Field(default_factory=list)
edges: list[GraphViewEdge] = Field(default_factory=list)
manual_edge_count: int = 0
inferred_edge_count: int = 0
structure_edge_count: int = 0
⋮----
class WorkspaceGraphService
⋮----
"""Assemble manifest manual graph data with optional inferred dependencies."""
⋮----
"""Return diagram-ready nodes and edges."""
rows = self._index.build_index(config=config, workspace_root=workspace_root)
project_names = {
nodes: list[GraphViewNode] = []
node_ids: set[str] = set()
⋮----
node_id = f"project:{project_name}"
⋮----
node_id = f"repo:{row['project_name']}/{row['repo_name']}"
⋮----
edges: list[GraphViewEdge] = []
edge_keys: set[tuple[str, str, str]] = set()
⋮----
project_id = f"project:{row['project_name']}"
repo_id = f"repo:{row['project_name']}/{row['repo_name']}"
⋮----
manual_count = self._append_manual_edges(
⋮----
inferred_count = 0
⋮----
inferred_count = self._append_inferred_edges(
⋮----
structure_count = sum(1 for edge in edges if edge.source == "structure")
⋮----
added = 0
⋮----
from_id = resolve_graph_endpoint_id(
to_id = resolve_graph_endpoint_id(
⋮----
label = rel.label or rel.type
⋮----
result = self._dependencies.map_dependencies(
⋮----
label = dep_edge.type
⋮----
label = str(dep_edge.evidence[0])[:48]
⋮----
key = (from_id, to_id, edge_type)
⋮----
edge_key = edge_id or f"{from_id}->{to_id}:{edge_type}"
````

## File: src/metagit/core/web/job_store.py
````python
#!/usr/bin/env python
"""In-memory sync job tracking for web UI SSE flows."""
⋮----
class SyncJobStore
⋮----
"""Thread-safe in-memory sync job tracking."""
⋮----
def __init__(self) -> None
⋮----
def create_job(self) -> str
⋮----
"""Create a pending job and return its id (uuid4 hex)."""
job_id = uuid.uuid4().hex
⋮----
def mark_running(self, job_id: str) -> None
⋮----
"""Mark an existing job as running."""
⋮----
status = self._jobs.get(job_id)
⋮----
def append_event(self, job_id: str, event: dict[str, Any]) -> None
⋮----
"""Append a server-sent event payload for a job."""
⋮----
"""Mark a job completed with summary and per-repo results."""
⋮----
def fail(self, job_id: str, error: str) -> None
⋮----
"""Mark a job as failed with an error message."""
⋮----
def get(self, job_id: str) -> SyncJobStatus | None
⋮----
"""Return a snapshot of job status, or None if unknown."""
⋮----
def drain_events(self, job_id: str) -> list[dict[str, Any]]
⋮----
"""Return pending SSE-style events for a job and clear the buffer."""
⋮----
pending = self._pending_events.pop(job_id, [])
````

## File: src/metagit/core/web/static_handler.py
````python
#!/usr/bin/env python
"""Serve bundled static assets for the metagit web UI."""
⋮----
_API_PREFIXES = ("/v1", "/v2", "/v3")
⋮----
class StaticWebHandler
⋮----
"""Serve SPA assets from the packaged web data directory."""
⋮----
def __init__(self, web_root: str | None = None) -> None
⋮----
root = Path(web_root) if web_root is not None else Path(DATA_PATH) / "web"
⋮----
"""Serve static files for non-API GET requests."""
⋮----
parsed_path = urlparse(path).path
⋮----
file_path = self._resolve_file(parsed_path)
⋮----
file_path = self._web_root / "index.html"
⋮----
def _resolve_file(self, parsed_path: str) -> Path | None
⋮----
relative = unquote(parsed_path.lstrip("/"))
candidate = (self._web_root / relative).resolve()
web_root_resolved = self._web_root.resolve()
⋮----
content_type = "application/octet-stream"
body = file_path.read_bytes()
⋮----
@staticmethod
    def is_api_path(path: str) -> bool
⋮----
"""Return True when the path belongs to a versioned API route."""
````

## File: src/metagit/core/workspace/agent_instructions.py
````python
#!/usr/bin/env python
"""
Compose layered agent instructions from .metagit.yml for controller and subagents.
"""
⋮----
_LAYER_HEADERS: dict[str, str] = {
⋮----
class AgentInstructionLayer(BaseModel)
⋮----
"""One non-empty instruction layer from the manifest."""
⋮----
layer: Literal["file", "workspace", "project", "repo"]
text: str = Field(..., min_length=1)
⋮----
class AgentInstructionsComposition(BaseModel)
⋮----
"""Structured and composed agent instructions for a scope."""
⋮----
layers: list[AgentInstructionLayer] = Field(default_factory=list)
effective: str = ""
⋮----
class AgentInstructionsResolver
⋮----
"""Build instruction stacks for workspace, project, and repo scopes."""
⋮----
"""Compose instructions from file → workspace → project → repo."""
layers: list[AgentInstructionLayer] = []
file_text = _normalized(config.agent_instructions)
⋮----
workspace_text = _normalized(config.workspace.agent_instructions)
⋮----
project_text = _normalized(project.agent_instructions)
⋮----
repo_text = _normalized(repo.agent_instructions)
⋮----
"""Locate a configured repo entry by name or resolved path."""
⋮----
normalized_path = repo_path.strip() if repo_path else None
⋮----
def _normalized(value: Optional[str]) -> Optional[str]
⋮----
stripped = value.strip()
⋮----
def _compose_text(layers: list[AgentInstructionLayer]) -> str
⋮----
blocks: list[str] = []
⋮----
header = _LAYER_HEADERS.get(item.layer, item.layer.upper())
````

## File: src/metagit/core/workspace/dedupe_resolver.py
````python
#!/usr/bin/env python
"""
Resolve effective workspace dedupe settings with per-project manifest overrides.
"""
⋮----
"""
    Return dedupe config to apply for a project, or None when dedupe is off.

    App-config ``workspace.dedupe`` supplies strategy and canonical_dir. A project
    may set ``dedupe.enabled`` in ``.metagit.yml`` to override only the enabled flag.
    """
enabled = workspace_dedupe.enabled
⋮----
enabled = project.dedupe.enabled
⋮----
"""Resolve dedupe for a named workspace project."""
project = find_project(config, project_name)
⋮----
"""Resolve dedupe for layout/sync CLI using optional project scope."""
````

## File: src/metagit/core/workspace/health_models.py
````python
#!/usr/bin/env python
"""
Pydantic models for workspace health check results.
"""
⋮----
class HealthRecommendation(BaseModel)
⋮----
"""Actionable workspace maintenance recommendation."""
⋮----
severity: Literal["info", "warning", "critical"] = "info"
action: str
message: str
project_name: Optional[str] = None
repo_name: Optional[str] = None
repo_path: Optional[str] = None
⋮----
class RepoHealthRow(BaseModel)
⋮----
"""Per-repository health signals."""
⋮----
project_name: str
repo_name: str
repo_path: str
status: str
exists: bool
is_git_repo: bool
branch: Optional[str] = None
dirty: Optional[bool] = None
ahead: Optional[int] = None
behind: Optional[int] = None
gitnexus_status: Optional[str] = None
head_commit_age_days: Optional[float] = None
merge_base_age_days: Optional[float] = None
⋮----
class WorkspaceHealthResult(BaseModel)
⋮----
"""Aggregate workspace health report."""
⋮----
ok: bool = True
workspace_root: str = ""
summary: dict[str, int] = Field(default_factory=dict)
repos: list[RepoHealthRow] = Field(default_factory=list)
recommendations: list[HealthRecommendation] = Field(default_factory=list)
````

## File: src/metagit/core/workspace/hydrate.py
````python
#!/usr/bin/env python
"""
Materialize symlinked project mounts into full directory trees.
"""
⋮----
def collect_file_copy_jobs(source: Path) -> list[tuple[Path, Path]]
⋮----
"""Return (source_file, destination_file) pairs for a recursive copy."""
root = source.resolve()
⋮----
jobs: list[tuple[Path, Path]] = []
⋮----
base = Path(dirpath)
rel = base.relative_to(root)
⋮----
src = base / name
⋮----
"""
    Replace a symlink mount with a full copy of its resolved target.

    Returns (changed, error_message). ``changed`` is False when the mount is not
    a symlink (already material or missing).
    """
⋮----
source = mount.resolve()
⋮----
label = repo_label or mount.name
⋮----
jobs = collect_file_copy_jobs(source)
⋮----
desc = f"  💧 {label}"
bar_format = (
⋮----
dest = mount / rel
⋮----
name = rel.name if len(str(rel)) <= 48 else f"…{rel.name}"
````

## File: src/metagit/core/workspace/layout_context.py
````python
#!/usr/bin/env python
"""
Resolve sync root and dedupe settings for layout operations.
"""
⋮----
"""
    Return (sync_root, dedupe) for layout operations.

    Uses app config workspace.path when load succeeds; otherwise definition_root.
    When definition_path and project_name are set, applies per-project dedupe override.
    """
loaded = AppConfig.load()
⋮----
sync_root = str(Path(loaded.workspace.path).expanduser().resolve())
dedupe = loaded.workspace.dedupe
⋮----
manager = MetagitConfigManager(definition_path)
manifest = manager.load_config()
````

## File: src/metagit/core/workspace/layout_executor.py
````python
#!/usr/bin/env python
"""
Apply planned workspace layout filesystem steps.
"""
⋮----
def apply_plan(plan: LayoutPlan, *, dry_run: bool) -> list[LayoutStep]
⋮----
"""Execute disk steps from a layout plan."""
applied: list[LayoutStep] = []
⋮----
result = _apply_step(step)
⋮----
class LayoutExecutionError(Exception)
⋮----
"""Raised when a layout step cannot be applied."""
⋮----
def _apply_step(step: LayoutStep) -> LayoutStep
⋮----
def _apply_rename_or_move(step: LayoutStep) -> LayoutStep
⋮----
source = Path(step.source)
target = Path(step.target)
⋮----
def _apply_unlink(step: LayoutStep) -> LayoutStep
⋮----
path = Path(step.source)
⋮----
def _apply_symlink(step: LayoutStep) -> LayoutStep
⋮----
mount = Path(step.target)
target = Path(step.source)
⋮----
detail = "created symlink" if changed else "symlink already correct"
⋮----
def _apply_vscode(step: LayoutStep) -> LayoutStep
⋮----
project_dir = Path(step.source)
project_name = step.target
repo_names = [
⋮----
content = create_vscode_workspace(project_name, repo_names)
⋮----
out_path = project_dir / "workspace.code-workspace"
⋮----
def _apply_session(step: LayoutStep) -> LayoutStep
````

## File: src/metagit/core/workspace/layout_models.py
````python
#!/usr/bin/env python
"""
Pydantic models for workspace layout rename and move operations.
"""
⋮----
class LayoutStep(BaseModel)
⋮----
"""Single filesystem or auxiliary layout action."""
⋮----
action: Literal[
source: Optional[str] = None
target: Optional[str] = None
detail: Optional[str] = None
applied: bool = False
⋮----
class LayoutPlan(BaseModel)
⋮----
"""Planned manifest and disk changes before execution."""
⋮----
operation: Literal["rename_project", "rename_repo", "move_repo"] = "rename_project"
dry_run: bool = False
manifest_changes: list[str] = Field(default_factory=list)
disk_steps: list[LayoutStep] = Field(default_factory=list)
warnings: list[str] = Field(default_factory=list)
⋮----
class LayoutMutationResult(BaseModel)
⋮----
"""Result of rename/move layout operations."""
⋮----
ok: bool = True
error: Optional[CatalogError] = None
entity: Literal["project", "repo"] = "project"
operation: Literal["rename", "move"] = "rename"
project_name: str = ""
repo_name: Optional[str] = None
from_project: Optional[str] = None
to_project: Optional[str] = None
config_path: str = ""
data: Optional[dict[str, Any]] = None
````

## File: src/metagit/core/workspace/layout_resolver.py
````python
#!/usr/bin/env python
"""
Resolve sync-root paths for workspace layout operations.
"""
⋮----
_NAME_PATTERN = re.compile(r"^[\w][\w.-]*$")
⋮----
def validate_layout_name(name: str, *, label: str = "name") -> Optional[str]
⋮----
"""Return an error message when name is invalid for paths and sessions."""
trimmed = name.strip()
⋮----
def sync_root_path(workspace_path: str) -> Path
⋮----
"""Resolved workspace sync root."""
⋮----
def project_dir(workspace_path: Path, project_name: str) -> Path
⋮----
"""Project folder under the sync root."""
⋮----
"""Repo mount path under a project folder."""
⋮----
"""Locate a workspace project by name."""
⋮----
"""Locate a repo entry on a project."""
⋮----
def dedupe_enabled(dedupe: Optional[WorkspaceDedupeConfig]) -> bool
⋮----
"""True when workspace dedupe layout applies."""
⋮----
"""Canonical checkout path when dedupe applies."""
identity = workspace_dedupe.build_repo_identity(repo)
````

## File: src/metagit/core/workspace/layout_service.py
````python
#!/usr/bin/env python
"""
Rename and move workspace projects and repositories (manifest + sync layout).
"""
⋮----
class WorkspaceLayoutService
⋮----
"""Rename/move workspace catalog entries and aligned sync folders."""
⋮----
"""Rename a workspace project in the manifest and on disk."""
_ = dedupe
source = from_name.strip()
target = to_name.strip()
err = validate_layout_name(source, label="from_name") or validate_layout_name(
⋮----
project = find_project(config, source)
⋮----
root = sync_root_path(workspace_path)
plan = LayoutPlan(
old_dir = project_dir(root, source)
new_dir = project_dir(root, target)
⋮----
working = copy.deepcopy(config)
working_project = find_project(working, source)
⋮----
save_err = self._save(working, config_path)
⋮----
store = SessionStore(workspace_root=str(root))
⋮----
meta = store.get_workspace_meta()
⋮----
"""Rename a repository entry and its sync mount when present."""
project_key = project_name.strip()
⋮----
project = find_project(config, project_key)
⋮----
repo = find_repo(project, source)
⋮----
old_mount = repo_mount_path(root, project_key, source)
new_mount = repo_mount_path(root, project_key, target)
⋮----
identity = workspace_dedupe.build_repo_identity(repo)
⋮----
canonical = workspace_dedupe.canonical_path(
⋮----
proj_path = project_dir(root, project_key)
⋮----
save_err = self._save(config, config_path)
⋮----
"""Move a repository entry from one project to another."""
source_project_name = from_project.strip()
target_project_name = to_project.strip()
repo_key = repo_name.strip()
⋮----
err = validate_layout_name(repo_key, label="repo_name")
⋮----
source_project = find_project(config, source_project_name)
⋮----
target_project = find_project(config, target_project_name)
⋮----
repo = find_repo(source_project, repo_key)
⋮----
existing_target = find_repo(target_project, repo_key)
⋮----
old_mount = repo_mount_path(root, source_project_name, repo_key)
new_mount = repo_mount_path(root, target_project_name, repo_key)
⋮----
proj_path = project_dir(root, proj_name)
⋮----
pop_index = next(
moved_repo = source_project.repos.pop(pop_index)
⋮----
def _git_warnings(self, mount: Path) -> list[str]
⋮----
"""Warn when a git checkout under mount has a dirty working tree."""
warnings: list[str] = []
git_dir = mount / ".git"
⋮----
repo = git.Repo(str(mount))
⋮----
def _save(self, config: MetagitConfig, config_path: str) -> Optional[Exception]
⋮----
manager = MetagitConfigManager(metagit_config=config)
result = manager.save_config(config, Path(config_path))
⋮----
data: dict[str, Any] = {
````

## File: src/metagit/core/workspace/workspace_dedupe.py
````python
#!/usr/bin/env python
"""
Workspace-scoped repository deduplication helpers (canonical store + symlinks).
"""
⋮----
@dataclass(frozen=True)
class RepoIdentity
⋮----
"""Stable identity for a workspace repo entry."""
⋮----
repo_key: str
url: Optional[str] = None
local_path: Optional[str] = None
⋮----
def build_repo_identity(repo: ProjectPath) -> Optional[RepoIdentity]
⋮----
"""
    Build a stable repo key for deduplication within one workspace manifest.

    Branch-specific entries (ref or branches) receive distinct keys.
    """
branch_suffix = _branch_suffix(repo)
⋮----
normalized = normalize_git_url(str(repo.url)) or ""
⋮----
base_key = _slugify(normalized, digest_prefix="url")
⋮----
resolved = str(Path(repo.path).expanduser().resolve())
base_key = _slugify(resolved, digest_prefix="path")
⋮----
"""Return (project_name, repo_name) pairs sharing the same identity as repo."""
target = build_repo_identity(repo)
⋮----
matches: list[tuple[str, str]] = []
⋮----
existing_identity = build_repo_identity(existing)
⋮----
"""Absolute path to the canonical checkout directory for repo_key."""
⋮----
"""Absolute path where a project exposes a repo (symlink or directory)."""
⋮----
def ensure_symlink(mount: Path, target: Path) -> tuple[bool, Optional[str]]
⋮----
"""
    Ensure mount is a symlink to target.

    Returns (changed, error_message).
    """
target_resolved = target.resolve()
⋮----
current = Path(os.readlink(mount))
⋮----
current = (mount.parent / current).resolve()
⋮----
current = current.resolve()
⋮----
"""
    Map repo_key -> list of (project_name, repo_name) manifest entries referencing it.
    """
references: dict[str, list[tuple[str, str]]] = {}
⋮----
identity = build_repo_identity(repo)
⋮----
"""Canonical directories with no manifest reference (by repo_key)."""
root = workspace_path / dedupe.canonical_dir
⋮----
referenced = set(references.keys())
orphans: list[Path] = []
⋮----
def _branch_suffix(repo: ProjectPath) -> str
⋮----
joined = "-".join(sorted(str(branch) for branch in repo.branches))
⋮----
def _slugify(value: str, *, digest_prefix: str) -> str
⋮----
compact = re.sub(r"[^a-zA-Z0-9._-]+", "-", value.strip().lower()).strip("-")
⋮----
digest = hashlib.sha256(value.encode("utf-8")).hexdigest()[:16]
short = compact[:40].strip("-") if compact else digest_prefix
````

## File: src/metagit/data/init-templates/application/.metagit.yml.tpl
````
name: {{ name }}
description: |
  {{ description }}
kind: application
url: {{ url }}
````

## File: src/metagit/data/init-templates/application/template.yaml
````yaml
id: application
label: Application (single repo)
description: Minimal .metagit.yml for one application repository.
kind: application
prompts:
  - name: name
    label: Project name
    default_from: directory_name
  - name: description
    label: Short description
    default: Application project managed by metagit.
  - name: url
    label: Git remote URL (optional)
    default_from: git_remote_url
    required: false
files:
  - template: .metagit.yml.tpl
    output: .metagit.yml
````

## File: src/metagit/data/init-templates/hermes-orchestrator/.metagit.yml.tpl
````
name: {{ name }}
description: |
  {{ description }}
kind: umbrella
url: {{ url }}
agent_instructions: |
  You are the Hermes controller for this workspace: the DevOps and project-management
  entrypoint for the operator. You do not improvise workspace layout.

  Session start (every time):
  1. metagit_workspace_status — confirm gate is active and note workspace root.
  2. metagit_workspace_health_check — surface missing clones, broken mounts, duplicate URLs.
  3. Read metagit://workspace/config when you need the full manifest.

  New or continued work:
  - Search before create: metagit_repo_search / `metagit search` for names, URLs, tags.
  - Reuse existing workspace.projects[] and repos[] entries; never clone into ad-hoc folders.
  - Register changes in .metagit.yml (catalog tools or validated YAML), then metagit config validate.
  - metagit_project_context_switch to focus one project; pass effective_agent_instructions to subagents.
  - Stay controller for cross-project objectives; delegate single-repo implementation to subagents.
  - metagit_workspace_sync: fetch by default; pull/clone only with explicit operator approval.
  - metagit_session_update before switching projects or ending the session.

  Documentation duty:
  - Every repo or path must have a clear description in this manifest.
  - Record build, deploy, and publish steps in per-repo agent_instructions when non-obvious.
  - When paths, URLs, or ownership change, update .metagit.yml before claiming work is done.

workspace:
  description: |
    Portfolio registry: git-backed services plus local-only publish paths. Metagit is the
    source of truth for what exists on disk under the configured workspace.path.
  agent_instructions: |
    Validate after manifest edits (`metagit config validate`). Prefer metagit catalog and
    MCP tools over hand-editing repo lists without validation. Enable workspace.dedupe in
    app config when the same remote URL appears in multiple projects.
  projects:
    - name: portfolio
      description: Git-backed applications and shared services under active development.
      agent_instructions: |
        Git operations are allowed per repo policy. Launch subagents with repo-scoped
        effective_agent_instructions for implementation; you coordinate sequencing and PRs.
      repos:
        - name: {{ portfolio_repo_name }}
          description: Sample HTTP API service (replace with your repository).
          url: {{ portfolio_repo_url }}
          sync: true
          kind: service
          tags:
            tier: application
          agent_instructions: |
            Run unit tests and lint before opening PRs. Note breaking API changes in PR text.

    - name: local
      description: |
        Local-only paths (no git remotes). Used to build and publish static web apps and
        personal sites for the operator.
      agent_instructions: |
        Repos here use `path`, not `url`. Do not git clone or pull. Sync creates symlinks
        into the workspace. Document publish targets in each repo's agent_instructions.
      repos:
        - name: {{ local_site_name }}
          description: Sample static site published from a folder on disk.
          path: {{ local_site_path }}
          sync: true
          kind: website
          tags:
            publish: static
          agent_instructions: |
            Build: npm run build (or project README). Publish: copy dist/ to the host named
            in README. Update `path` in .metagit.yml if the site directory moves.

    - name: platform
      description: Optional shared infrastructure (Terraform, modules, policy). See docs/hermes-iac-workspace-guide.md.
      agent_instructions: |
        Treat IaC changes as high blast-radius. Use metagit_cross_project_dependencies and
        GitNexus before module version bumps. Subagents run plan-only unless operator approves apply.
      repos: []
````

## File: src/metagit/data/init-templates/hermes-orchestrator/AGENTS.md.tpl
````
# Hermes orchestrator coordinator

This repository was initialized with the `hermes-orchestrator` metagit template.

## Controller role

You are the DevOps and project-management entrypoint for workspace **{{ name }}**.
Read root and layered `agent_instructions` in `.metagit.yml` before changing workspace layout.

## Session checklist

1. `metagit_workspace_status` and `metagit_workspace_health_check`
2. Search before create (`metagit search` / `metagit_repo_search`)
3. `metagit config validate` after manifest edits
4. `metagit_project_context_switch` when focusing a project
5. Delegate single-repo work to subagents with `effective_agent_instructions`
6. `metagit_session_update` on handoff

## Docs

- [Hermes orchestrator workspace](https://metagit-ai.github.io/metagit-cli/hermes-orchestrator-workspace/)
- [Hermes & org IaC guide](https://metagit-ai.github.io/metagit-cli/hermes-iac-workspace-guide/)
````

## File: src/metagit/data/init-templates/hermes-orchestrator/template.yaml
````yaml
id: hermes-orchestrator
label: Hermes orchestrator
description: |
  DevOps / project-management controller workspace with portfolio, local path,
  and platform projects plus AGENTS.md for the coordinator repo.
kind: umbrella
prompts:
  - name: name
    label: Workspace / manifest name
    default: hermes-control-plane
  - name: description
    label: Manifest description
    default: Umbrella workspace for a Hermes controller agent.
  - name: url
    label: Coordinator git URL (optional)
    default_from: git_remote_url
    required: false
  - name: portfolio_repo_name
    label: Example portfolio repo entry name
    default: example-api
  - name: portfolio_repo_url
    label: Example portfolio git URL
    default: https://github.com/example-org/example-api.git
  - name: local_site_name
    label: Local site entry name (non-git)
    default: example-site
  - name: local_site_path
    label: Local site filesystem path
    default: ~/Sites/example-site
files:
  - template: .metagit.yml.tpl
    output: .metagit.yml
  - template: AGENTS.md.tpl
    output: AGENTS.md
````

## File: src/metagit/data/init-templates/umbrella/.metagit.yml.tpl
````
name: {{ name }}
description: |
  {{ description }}
kind: umbrella
url: {{ url }}
workspace:
  description: Workspace projects synced under the configured workspace.path.
  projects:
    - name: default
      description: Default project group for managed repositories.
      repos: []
````

## File: src/metagit/data/init-templates/umbrella/template.yaml
````yaml
id: umbrella
label: Umbrella workspace
description: Coordinator manifest with an empty default workspace project.
kind: umbrella
prompts:
  - name: name
    label: Workspace / manifest name
    default_from: directory_name
  - name: description
    label: Short description
    default: Umbrella workspace coordinating multiple repositories.
  - name: url
    label: Coordinator git URL (optional)
    default_from: git_remote_url
    required: false
files:
  - template: .metagit.yml.tpl
    output: .metagit.yml
````

## File: src/metagit/data/prompts/gemini_prompt_example.md
````markdown
## Prompt 1

Create a yaml file named .metagit.example.yml in the root of this project that adheres to the jsonschema file
./schemas/metagit_config.schema.json that is based on the folders and files within this project. Be certain to adhere to
.gitignore when processing files. Only actually read in the file contents for any discovered CICD files, docker image files,
and other files that may have external dependency references. Do your best to infer directory purpose without reading in
everything. For example, tests with several dozen .py files would be unit tests and not valuable. Intelligently trace for
important files and project structure by using the build_files.important list found in ./src/metagit/data/build-files.yaml
as a compass. The end goal will be to create the file as instructed so that it accurately represents the languages, used
frameworks, dependencies, and other project data.

╭──────────────────────────────────╮
│                                  │
│  Agent powering down. Goodbye!   │
│                                  │
│                                  │
│  Cumulative Stats (1 Turns)      │
│                                  │
│  Input Tokens           134,416  │
│  Output Tokens            1,607  │
│  Thoughts Tokens          2,186  │
│  ──────────────────────────────  │
│  Total Tokens           138,209  │
│                                  │
│  Total duration (API)     48.1s  │
│  Total duration (wall)  16m 57s  │
│                                  │
╰──────────────────────────────────╯

## Prompt 2

Perform a comprehensive analysis of the files in this project. Update the yaml file named .metagit.example.yml in the root of this project in a manner that adheres to the jsonschema file located at ./schemas/metagit_config.schema.json. Ensure you skip files matching the patterns in .gitignore. Do your best to update even optional elements of this schema. Pay special attention to dependencies found in Dockerfiles, CICD files, and mcp definitions. When complete also create a project.metagit.md file with additional documentation on the project components that includes a mermaid diagram of how they interact.

╭─────────────────────────────────╮
│                                 │
│  Agent powering down. Goodbye!  │
│                                 │
│                                 │
│  Cumulative Stats (1 Turns)     │
│                                 │
│  Input Tokens          134,880  │
│  Output Tokens           2,037  │
│  Thoughts Tokens         1,790  │
│  ─────────────────────────────  │
│  Total Tokens          138,707  │
│                                 │
│  Total duration (API)    49.4s  │
│  Total duration (wall)  2m 26s  │
│                                 │
╰─────────────────────────────────╯

## Prompt 3

You are an expert devops and software engineer. Intelligently explore this project for its use of secrets and variables paying special attention to only those affecting the runtime behavior of the code and the cicd workflows used to release the project's artifacts. Create a report in ./docs/secrets.analysis.md on each of the secrets found and were they are sourced from. Using this data create a yaml file at ./docs/secrets.definitions.yml that represents the secrets, their provenance, and where they should be placed.

## Prompt 4 - Simplified Project Docs

Perform a comprehensive analysis of the files in this project. Update the yaml file named .metagit.example.yml in the root of this project in a manner that adheres to the jsonschema file located at ./schemas/metagit_config.schema.json. Ensure you skip files matching the patterns in .gitignore. Do your best to update even optional elements of this schema. Pay special attention to dependencies found in Dockerfiles, CICD files, and mcp definitions. When complete also create ./docs/app.logic.md with additional documentation on the project components that includes a mermaid diagram of how they interact.
````

## File: src/metagit/data/skills/metagit-bootstrap/scripts/bootstrap-config.zsh
````zsh
#!/usr/bin/env zsh
set -euo pipefail

ROOT="${1:-$PWD}"
FORCE="${2:-false}"
TARGET="$ROOT/.metagit.yml"

uv run python - "$ROOT" "$TARGET" "$FORCE" <<'PY'
import sys
from pathlib import Path
from metagit.core.config.manager import create_metagit_config
from metagit.core.config.manager import MetagitConfigManager

root = Path(sys.argv[1]).resolve()
target = Path(sys.argv[2]).resolve()
force = sys.argv[3].lower() in {"1", "true", "yes", "force"}

if target.exists() and not force:
    mgr = MetagitConfigManager(config_path=target)
    result = mgr.load_config()
    state = "valid" if not isinstance(result, Exception) else "invalid"
    print(f"status=exists\tvalidity={state}\tpath={target}")
    raise SystemExit(0)

yaml_out = create_metagit_config(name=root.name, kind="application", as_yaml=True)
if isinstance(yaml_out, Exception):
    print(f"status=error\tmessage={yaml_out}")
    raise SystemExit(1)

target.write_text(yaml_out, encoding="utf-8")
mgr = MetagitConfigManager(config_path=target)
result = mgr.load_config()
state = "valid" if not isinstance(result, Exception) else "invalid"
print(f"status=written\tvalidity={state}\tpath={target}")
PY
````

## File: src/metagit/data/skills/metagit-bootstrap/SKILL.md
````markdown
---
name: metagit-bootstrap
description: Use when generating or refining local .metagit.yml files using deterministic discovery plus MCP sampling.
---

# Metagit MCP Bootstrap Skill

Use this skill to create a local `.metagit.yml` using discovery-driven prompts and MCP sampling.

## Purpose

Generate schema-compliant `.metagit.yml` files with high contextual quality while preserving safety and explicit user control.

## Local Script Wrapper (Use First)

Use this token-efficient wrapper for local bootstrap tasks:
- `./scripts/bootstrap-config.zsh [root_path] [force]`

Behavior:
- Writes `.metagit.yml` when missing
- Validates via Metagit config models
- Returns a compact status line for agents

## Workflow

1. Gather deterministic discovery data from the target repository:
   - source language/framework indicators
   - package/lock/build files
   - Dockerfiles and CI workflows
   - terraform files and module usage
2. Build a strict prompt package:
   - output format contract: valid YAML only
   - required schema fields and constraints
   - extracted discovery evidence
3. If sampling is supported, call `sampling/createMessage`.
4. Validate generated YAML with Metagit config models.
5. Retry with validation feedback up to a fixed max attempt count.
6. Return draft output and write only on explicit confirmation.

## Output Modes

- **Plan-only mode**: return prompt + discovery summary if sampling unavailable.
- **Draft mode**: return `.metagit.generated.yml` content.
- **Confirmed write mode**: write to `.metagit.yml` only with explicit parameter (`confirm_write=true`).

## Quality Bar

- Preserve discovered evidence in structured fields.
- Include workspace project and related repo entries where detectable.
- Avoid invented repositories or unverifiable dependencies.

## Safety Rules

- Never overwrite `.metagit.yml` silently.
- Never emit secrets in cleartext.
- Prefer placeholders for credentials or tokens.
````

## File: src/metagit/data/skills/metagit-config-refresh/SKILL.md
````markdown
---
name: metagit-config-refresh
description: Refresh or bootstrap `.metagit.yml` using deterministic discovery and validation flows. Use when configuration is missing, stale, or incomplete for workspace operations.
---

# Refreshing Project Config

Use this skill to keep `.metagit.yml` accurate and operational.

## Workflow

1. Check workspace activation and config validity.
2. Run bootstrap plan mode first.
3. Review generated changes against expected workspace topology.
4. Apply config updates and validate schema before continuing.

## Command Wrapper

- `zsh ./skills/metagit-bootstrap/scripts/bootstrap-config.zsh [root_path] [mode] [seed_context]`

## Output Contract

Return:
- config health state before/after
- generated update summary
- any manual follow-up needed

## Safety

- Prefer plan/dry-run first for large config updates.
- Keep changes bounded to target workspace intent.
````

## File: src/metagit/data/skills/metagit-gating/scripts/gate-status.zsh
````zsh
#!/usr/bin/env zsh
set -euo pipefail

ROOT="${1:-$PWD}"

uv run python - "$ROOT" <<'PY'
import os
import sys
from metagit.core.mcp.gate import WorkspaceGate
from metagit.core.mcp.root_resolver import WorkspaceRootResolver
from metagit.core.mcp.tool_registry import ToolRegistry

root = sys.argv[1]
resolver = WorkspaceRootResolver()
gate = WorkspaceGate()
registry = ToolRegistry()

resolved = resolver.resolve(cwd=os.path.abspath(root), cli_root=root)
status = gate.evaluate(root_path=resolved)
tools = registry.list_tools(status)
print(f"state={status.state.value}\troot={status.root_path or 'none'}\ttools={len(tools)}")
PY
````

## File: src/metagit/data/skills/metagit-gating/SKILL.md
````markdown
---
name: metagit-gating
description: Use when implementing or operating Metagit MCP server activation and tool exposure rules based on .metagit.yml presence and validity.
---

# Metagit MCP Gating Skill

Use this skill whenever you need to control whether Metagit MCP tools/resources are exposed.

## Purpose

Ensure high-risk tooling and multi-repo context are only available when a valid `.metagit.yml` exists at the resolved workspace root.

## Local Script Wrapper (Use First)

Use this token-efficient wrapper for all gating checks:
- `./scripts/gate-status.zsh [root_path]`

Expected output (single line, tab-delimited):
- `state=<value>\troot=<path|none>\ttools=<count>`

## Activation Workflow

1. Resolve workspace root:
   - `METAGIT_WORKSPACE_ROOT`
   - CLI `--root`
   - upward directory walk
2. Check for `.metagit.yml` in resolved root.
3. Validate config through existing Metagit config models.
4. Derive activation state: missing, invalid, or active.
5. Register tool surface based on state.

## Tool Exposure Contract

### Inactive (missing or invalid config)
Expose only:
- `metagit_workspace_status`
- `metagit_bootstrap_config_plan_only`

### Active (valid config)
Expose full set:
- `metagit_workspace_status`
- `metagit_workspace_index`
- `metagit_workspace_search`
- `metagit_upstream_hints`
- `metagit_repo_inspect`
- `metagit_repo_sync`
- `metagit_bootstrap_config`

## Error Handling

- Return explicit, machine-readable state and reason.
- Avoid stack traces in user-facing outputs.
- Log parser/validation errors with enough detail for debugging.

## Safety Rules

- Never expose mutation-capable tools in inactive state.
- Never operate outside validated workspace boundaries.
- Keep defaults read-only unless user/agent explicitly opts in.
````

## File: src/metagit/data/skills/metagit-gitnexus/scripts/analyze-targets.zsh
````zsh
#!/usr/bin/env zsh

set -euo pipefail

workspace_root="${1:-.}"
project_name="${2:-default}"

if [[ ! -f ".metagit.yml" ]]; then
  echo "ERROR: .metagit.yml not found in current directory"
  exit 2
fi

echo "analyze repo=$(pwd)"
npx gitnexus analyze

tmp_output="$(mktemp)"
uv run python - "$workspace_root" "$project_name" <<'PY' > "$tmp_output"
import sys
from pathlib import Path
import yaml

workspace_root = Path(sys.argv[1]).expanduser().resolve()
project_name = sys.argv[2]
cfg = yaml.safe_load(Path(".metagit.yml").read_text(encoding="utf-8")) or {}
workspace = (cfg.get("workspace") or {})
projects = workspace.get("projects") or []
target = next((p for p in projects if p.get("name") == project_name), None)
if not target:
    print(f"warn project_not_found={project_name}")
    raise SystemExit(0)

for repo in target.get("repos") or []:
    name = repo.get("name")
    if not name:
        continue
    repo_path = workspace_root / project_name / name
    if repo_path.exists() and repo_path.is_dir():
        print(f"repo_path={repo_path}")
    else:
        print(f"skip_missing={repo_path}")
PY

while IFS= read -r line; do
  case "$line" in
    repo_path=*)
      path="${line#repo_path=}"
      echo "analyze repo=${path}"
      (cd "$path" && npx gitnexus analyze) || echo "fail repo=${path}"
      ;;
    *)
      echo "$line"
      ;;
  esac
done < "$tmp_output"

rm -f "$tmp_output"
````

## File: src/metagit/data/skills/metagit-multi-repo/SKILL.md
````markdown
---
name: metagit-multi-repo
description: Coordinate implementation tasks across multiple repositories using metagit status, search, and scoped sync workflows. Use when one objective spans several repositories.
---

# Coordinating Multi-Repo Implementation

Use this skill for cross-repository feature or fix delivery.

## Workflow

1. Define objective and affected repositories.
2. Verify workspace scope and dependency hints.
3. Sequence work by dependency order.
4. Sync only required repositories.
5. Track progress and blockers per repository.

## Command Wrapper

- `zsh ./skills/metagit-control-center/scripts/control-cycle.zsh [root_path] ["query"] [preset]`

## Output Contract

Return:
- objective-to-repository map
- execution order
- current blocker + next step

## Safety

- Keep scope bounded to configured workspace repositories.
- Prefer deterministic evidence for cross-repo assumptions.
````

## File: src/metagit/data/skills/metagit-upstream-scan/scripts/upstream-scan.zsh
````zsh
#!/usr/bin/env zsh
set -euo pipefail

ROOT="${1:-$PWD}"
QUERY="${2:-}"
PRESET="${3:-}"
MAX_RESULTS="${4:-20}"

if [[ -z "$QUERY" ]]; then
  echo "status=error\tmessage=query-required"
  exit 1
fi

uv run python - "$ROOT" "$QUERY" "$PRESET" "$MAX_RESULTS" <<'PY'
import sys
from pathlib import Path
from metagit.core.config.manager import MetagitConfigManager
from metagit.core.mcp.services.workspace_index import WorkspaceIndexService
from metagit.core.mcp.services.workspace_search import WorkspaceSearchService
from metagit.core.mcp.services.upstream_hints import UpstreamHintService

root = Path(sys.argv[1]).resolve()
query = sys.argv[2]
preset = sys.argv[3] or None
max_results = int(sys.argv[4])

manager = MetagitConfigManager(config_path=root / ".metagit.yml")
cfg = manager.load_config()
if isinstance(cfg, Exception):
    print(f"status=error\tmessage=config-invalid\tdetail={cfg}")
    raise SystemExit(1)

index = WorkspaceIndexService().build_index(config=cfg, workspace_root=str(root))
repo_paths = [row["repo_path"] for row in index if row.get("exists")]
search_hits = WorkspaceSearchService().search(query=query, repo_paths=repo_paths, preset=preset, max_results=max_results)
ranked = UpstreamHintService().rank(blocker=query, repo_context=index)[:5]

print(f"status=ok\trepos={len(index)}\thits={len(search_hits)}")
for row in ranked:
    print(f"hint\trepo={row['repo_name']}\tscore={row['score']}")
for hit in search_hits[:5]:
    print(f"hit\tfile={hit['file_path']}\tline={hit['line_number']}")
PY
````

## File: src/metagit/data/skills/metagit-upstream-scan/SKILL.md
````markdown
---
name: metagit-upstream-scan
description: Use when a coding agent encounters likely upstream blockers and must find related workspace repositories, files, and probable root causes.
---

# Metagit Upstream Discovery Skill

Use this skill when the current repo does not appear to contain the full fix and related repositories may hold the source issue.

## Supported Use Cases

- Missing Terraform input in a shared module
- Docker base image/version mismatch across repos
- Shared infrastructure definitions causing local failures
- CI pipeline breakages tied to upstream templates/workflows

## Local Script Wrapper (Use First)

Use this token-efficient wrapper for upstream discovery tasks:
- `./scripts/upstream-scan.zsh [root_path] "<query>" [preset] [max_results]`

Output format:
- compact status line
- top ranked repo hints (`hint`)
- top search file hits (`hit`)

## Workflow

1. Read workspace repository map from active `.metagit.yml`.
2. Run `metagit_workspace_index` to verify repo availability and sync state.
3. Use `metagit_workspace_search` with category preset (`terraform`, `docker`, `infra`, `ci`).
4. Use `metagit_upstream_hints` to rank candidate repositories and files.
5. Return a concise action plan:
   - top candidate repos
   - likely files/definitions
   - whether sync is needed before deeper analysis

## Search Strategy

- Start narrow with issue-specific terms (error, module, variable, image tag).
- Expand to broader shared terms if no hits.
- Prefer repositories referenced by workspace metadata before searching unknown repos.

## Output Contract

Return:
- ranked candidates with rationale
- suggested next file openings
- confidence level and unresolved assumptions

## Safety Rules

- Restrict search to configured workspace repositories.
- Cap result size and duration.
- Keep this workflow read-only unless an explicit sync action is requested.
````

## File: src/metagit/data/skills/metagit-upstream-triage/SKILL.md
````markdown
---
name: metagit-upstream-triage
description: Triage cross-repository blockers by ranking likely upstream repositories and files with metagit search and hinting tools. Use when local fixes appear incomplete.
---

# Triaging Upstream Blockers

Use this skill for failures likely rooted in another repository.

## Workflow

1. Run workspace index and search with issue-specific terms.
2. Run upstream hint ranking to prioritize repositories/files.
3. Open the top candidates and validate root-cause evidence.
4. Return a short fix path (repo, file, next action).

## Command Wrapper

- `zsh ./skills/metagit-upstream-scan/scripts/upstream-scan.zsh [root_path] "<query>" [preset] [max_results]`

## Output Contract

Return:
- ranked candidate repositories
- probable root-cause files
- confidence and assumptions

## Safety

- Keep this flow read-only unless sync is explicitly requested.
````

## File: src/metagit/data/templates/agent-standard/AGENTS.md.fragment
````
# Agent workspace conventions

- Read `.metagit.yml` before making cross-repository changes.
- Use `metagit_project_context_switch` to scope work to one workspace project.
- Run `metagit_workspace_health_check` when the workspace feels out of date.
````

## File: src/metagit/data/templates/hermes-orchestrator/AGENTS.md.fragment
````
# Hermes orchestrator (workspace coordinator)

This directory received the `hermes-orchestrator` workspace template.

1. Copy or merge `examples/hermes-orchestrator/.metagit.yml` into your umbrella coordinator repository.
2. Run `metagit config validate` on the manifest.
3. Register real repos with `metagit project repo add` or MCP catalog tools.
4. Point Hermes MCP at the coordinator root: `metagit mcp serve --root <path>`.

Controller agents should follow root `agent_instructions` in `.metagit.yml`, not this file alone.
````

## File: src/metagit/data/templates/hermes-orchestrator/README.md
````markdown
# hermes-orchestrator template

Files here are copied into workspace project directories via `metagit_project_template_apply`
(template id: `hermes-orchestrator`).

The umbrella manifest example lives at `examples/hermes-orchestrator/.metagit.yml` in the
metagit-cli repository. See `docs/hermes-orchestrator-workspace.md` for the full guide.
````

## File: src/metagit/data/web/assets/index-B315j_NF.css
````css
:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;--font-sans:system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--font-mono:ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, monospace;--radius-sm:.25rem;--radius-md:.5rem;--radius-lg:.75rem;--shadow-sm:0 1px 2px #0f172a0f;--shadow-md:0 4px 12px #0f172a14;--transition-theme:color .2s ease, background-color .2s ease, border-color .2s ease, box-shadow .2s ease}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}:root,[data-theme=light]{--color-bg:#f6f7fb;--color-bg-elevated:#fff;--color-bg-muted:#eef1f6;--color-border:#d8dee9;--color-border-strong:#c5cedb;--color-text:#0f172a;--color-text-muted:#5b6475;--color-text-subtle:#7c8798;--color-accent:#4f46e5;--color-accent-hover:#4338ca;--color-accent-soft:#4f46e51f;--color-danger:#dc2626;--color-danger-soft:#dc26261a;--color-success:#059669;--color-focus:#4f46e559;--color-tree-hover:#4f46e514;--color-tree-selected:#4f46e529;--graph-edge-manual:#7c3aed;--graph-edge-inferred:#0ea5e9;--graph-edge-structure:#94a3b8}[data-theme=dark]{--color-bg:#0f1117;--color-bg-elevated:#171a22;--color-bg-muted:#1f2430;--color-border:#2a3140;--color-border-strong:#3a4356;--color-text:#ffffffeb;--color-text-muted:#ffffffa6;--color-text-subtle:#ffffff73;--color-accent:#818cf8;--color-accent-hover:#a5b4fc;--color-accent-soft:#818cf82e;--color-danger:#f87171;--color-danger-soft:#f8717126;--color-success:#34d399;--color-focus:#818cf866;--color-tree-hover:#818cf81f;--color-tree-selected:#818cf838;--graph-edge-manual:#a78bfa;--graph-edge-inferred:#38bdf8;--graph-edge-structure:#64748b}@media (prefers-color-scheme:dark){:root:not([data-theme=light]){--color-bg:#0f1117;--color-bg-elevated:#171a22;--color-bg-muted:#1f2430;--color-border:#2a3140;--color-border-strong:#3a4356;--color-text:#ffffffeb;--color-text-muted:#ffffffa6;--color-text-subtle:#ffffff73;--color-accent:#818cf8;--color-accent-hover:#a5b4fc;--color-accent-soft:#818cf82e;--color-danger:#f87171;--color-danger-soft:#f8717126;--color-success:#34d399;--color-focus:#818cf866;--color-tree-hover:#818cf81f;--color-tree-selected:#818cf838;--graph-edge-manual:#a78bfa;--graph-edge-inferred:#38bdf8;--graph-edge-structure:#64748b}}*{box-sizing:border-box}html{transition:var(--transition-theme)}body{min-width:320px;min-height:100vh;font-family:var(--font-sans);color:var(--color-text);background-color:var(--color-bg);font-synthesis:none;text-rendering:optimizelegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;transition:var(--transition-theme);margin:0;font-weight:400;line-height:1.5}#root{min-height:100vh}button,input,select,textarea{font:inherit}._shell_1gsdz_1{min-height:100vh;transition:var(--transition-theme);flex-direction:column;display:flex}._header_1gsdz_8{border-bottom:1px solid var(--color-border);background:var(--color-bg-elevated);transition:var(--transition-theme);flex-wrap:wrap;align-items:center;gap:1rem 1.5rem;padding:1rem 1.5rem;display:flex}._title_1gsdz_19{color:var(--color-text);margin:0;font-size:1.25rem;font-weight:600}._nav_1gsdz_26{flex-wrap:wrap;flex:1;gap:.5rem;display:flex}._navLink_1gsdz_33{color:var(--color-text-muted);border-radius:var(--radius-md);transition:var(--transition-theme);padding:.35rem .65rem;text-decoration:none}._navLink_1gsdz_33:hover{color:var(--color-text);background:var(--color-tree-hover)}._navLinkActive_1gsdz_46{color:var(--color-text);background:var(--color-accent-soft)}._themeToggle_1gsdz_51{border:1px solid var(--color-border);border-radius:var(--radius-md);background:var(--color-bg-muted);width:2.25rem;height:2.25rem;color:var(--color-text);cursor:pointer;transition:var(--transition-theme);justify-content:center;align-items:center;margin-left:auto;display:inline-flex}._themeToggle_1gsdz_51:hover{border-color:var(--color-border-strong);background:var(--color-tree-hover)}._themeIcon_1gsdz_71{font-size:1.1rem;line-height:1}._main_1gsdz_76{flex:1;width:100%;max-width:80rem;margin:0 auto;padding:1.5rem}._previewPanel_tee41_1{background:var(--color-bg-elevated);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);min-height:12rem;max-height:calc(100vh - 12rem);transition:var(--transition-theme);flex-direction:column;gap:.75rem;padding:1rem;display:flex}._header_tee41_15{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:.5rem 1rem;display:flex}._title_tee41_23{text-transform:uppercase;letter-spacing:.06em;color:var(--color-text-subtle);margin:0;font-size:.8rem;font-weight:600}._controls_tee41_32{flex-wrap:wrap;align-items:center;gap:.5rem;display:flex}._select_tee41_39{color:var(--color-text);background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius-sm);padding:.35rem .5rem;font-size:.85rem}._badge_tee41_48{text-transform:uppercase;letter-spacing:.04em;border-radius:var(--radius-sm);background:var(--color-accent-muted);color:var(--color-accent);padding:.15rem .45rem;font-size:.7rem;font-weight:600}._badgeInvalid_tee41_59{background:color-mix(in srgb, var(--color-danger) 18%, transparent);color:var(--color-danger)}._codeWrap_tee41_64{border:1px solid var(--color-border);border-radius:var(--radius-md);background:var(--color-bg-code,var(--color-bg));flex:1;min-height:0;overflow:auto}._code_tee41_64{font-family:var(--font-mono);white-space:pre;color:var(--color-text);margin:0;padding:.85rem 1rem;font-size:.78rem;line-height:1.45}._state_tee41_83{color:var(--color-text-muted);margin:0;font-size:.9rem}._errors_tee41_89{color:var(--color-danger);margin:0;padding-left:1.1rem;font-size:.85rem}._panel_4l6gj_1{background:var(--color-bg-elevated);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);transition:var(--transition-theme);flex-direction:column;gap:1rem;padding:1.25rem;display:flex}._empty_4l6gj_13{color:var(--color-text-muted);margin:0}._header_4l6gj_18{flex-direction:column;gap:.35rem;display:flex}._title_4l6gj_24{margin:0;font-size:1.1rem;font-weight:600}._path_4l6gj_30{font-family:var(--font-mono);color:var(--color-text-subtle);word-break:break-all;margin:0;font-size:.8rem}._meta_4l6gj_38{color:var(--color-text-muted);flex-wrap:wrap;gap:.5rem;font-size:.8rem;display:flex}._badge_4l6gj_46{border-radius:var(--radius-sm);background:var(--color-bg-muted);border:1px solid var(--color-border);font-family:var(--font-mono);padding:.15rem .45rem}._description_4l6gj_54{color:var(--color-text-muted);margin:0;font-size:.9rem;line-height:1.5}._hint_4l6gj_61{background:var(--color-bg-muted);border-radius:var(--radius-md);color:var(--color-text-muted);margin:0;padding:.75rem 1rem;font-size:.9rem}._field_4l6gj_70{flex-direction:column;gap:.35rem;display:flex}._label_4l6gj_76{color:var(--color-text-muted);font-size:.85rem;font-weight:500}._input_4l6gj_82,._select_4l6gj_83,._textarea_4l6gj_84{border:1px solid var(--color-border);border-radius:var(--radius-md);background:var(--color-bg);width:100%;color:var(--color-text);font:inherit;transition:var(--transition-theme);padding:.55rem .75rem}._input_4l6gj_82:focus,._select_4l6gj_83:focus,._textarea_4l6gj_84:focus{border-color:var(--color-accent);box-shadow:0 0 0 3px var(--color-focus);outline:none}._input_4l6gj_82:disabled,._select_4l6gj_83:disabled{opacity:.6;cursor:not-allowed}._actions_4l6gj_109{flex-wrap:wrap;gap:.5rem;padding-top:.25rem;display:flex}._button_4l6gj_116{border-radius:var(--radius-md);border:1px solid var(--color-border);background:var(--color-bg-muted);color:var(--color-text);font:inherit;cursor:pointer;transition:var(--transition-theme);padding:.5rem 1rem;font-size:.875rem}._button_4l6gj_116:hover:not(:disabled){border-color:var(--color-border-strong)}._button_4l6gj_116:disabled{opacity:.55;cursor:not-allowed}._buttonPrimary_4l6gj_137{background:var(--color-accent);border-color:var(--color-accent);color:#fff}._buttonPrimary_4l6gj_137:hover:not(:disabled){background:var(--color-accent-hover);border-color:var(--color-accent-hover)}._errors_4l6gj_148{background:var(--color-danger-soft);border:1px solid var(--color-danger);border-radius:var(--radius-md);margin:0;padding:.75rem 1rem;list-style:none}._errors_4l6gj_148 li{color:var(--color-danger);margin:.25rem 0;font-size:.85rem}._errors_4l6gj_148 li:first-child{margin-top:0}._errors_4l6gj_148 li:last-child{margin-bottom:0}._status_4l6gj_171{color:var(--color-text-subtle);font-size:.8rem}._tree_1q1u5_1{margin:0;padding:0;list-style:none}._nested_1q1u5_7{border-left:1px solid var(--color-border);margin-left:1rem;padding-left:.5rem}._row_1q1u5_13{border-radius:var(--radius-sm);cursor:pointer;transition:var(--transition-theme);align-items:center;gap:.5rem;padding:.35rem .5rem;display:flex}._row_1q1u5_13:hover{background:var(--color-tree-hover)}._rowSelected_1q1u5_27{background:var(--color-tree-selected)}._rowDisabled_1q1u5_31{opacity:.5}._checkbox_1q1u5_35{width:1rem;height:1rem;accent-color:var(--color-accent);flex-shrink:0}._checkboxPlaceholder_1q1u5_42{flex-shrink:0;width:1rem}._label_1q1u5_47{flex:1;align-items:baseline;gap:.5rem;min-width:0;display:flex}._key_1q1u5_55{color:var(--color-text);font-weight:500}._type_1q1u5_60{color:var(--color-text-subtle);font-size:.75rem;font-family:var(--font-mono)}._required_1q1u5_66{color:var(--color-accent);text-transform:uppercase;letter-spacing:.04em;font-size:.65rem}._state_1q1u5_73{color:var(--color-text-muted);margin:1rem 0}._error_1q1u5_78{color:var(--color-danger)}._expandBtn_1q1u5_82{border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-bg-muted);width:1.5rem;height:1.5rem;color:var(--color-text-muted);cursor:pointer;flex-shrink:0;padding:0;font-size:.85rem;line-height:1}._expandBtn_1q1u5_82:hover{border-color:var(--color-border-strong);color:var(--color-text)}._listBtn_1q1u5_101{border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-bg-muted);width:1.5rem;height:1.5rem;color:var(--color-accent);cursor:pointer;flex-shrink:0;padding:0;font-size:.95rem;line-height:1}._listBtn_1q1u5_101:hover{border-color:var(--color-accent)}._listBtnDanger_1q1u5_119{color:var(--color-danger)}._listBtnDanger_1q1u5_119:hover{border-color:var(--color-danger)}._count_1q1u5_127{color:var(--color-text-subtle);font-size:.7rem}._page_2nlim_1{flex-direction:column;gap:1rem;display:flex}._header_2nlim_7{flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:.75rem 1.5rem;display:flex}._title_2nlim_15{margin:0;font-size:1.5rem;font-weight:600}._subtitle_2nlim_21{color:var(--color-text-muted);font-size:.9rem;font-family:var(--font-mono);word-break:break-all;margin:.25rem 0 0}._layout_2nlim_29{grid-template-columns:minmax(14rem,.9fr) minmax(16rem,1fr) minmax(16rem,1fr);align-items:start;gap:1.25rem;display:grid}@media (width<=1200px){._layout_2nlim_29{grid-template-columns:minmax(14rem,1fr) minmax(16rem,1.1fr)}._layout_2nlim_29>:last-child{grid-column:1/-1}}@media (width<=900px){._layout_2nlim_29{grid-template-columns:1fr}._layout_2nlim_29>:last-child{grid-column:auto}}._treePanel_2nlim_56{background:var(--color-bg-elevated);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);max-height:calc(100vh - 12rem);transition:var(--transition-theme);padding:1rem;overflow:auto}._treeHeading_2nlim_67{text-transform:uppercase;letter-spacing:.06em;color:var(--color-text-subtle);margin:0 0 .75rem;font-size:.8rem;font-weight:600}._wrap_6qkdh_1{flex-direction:column;gap:.75rem;display:flex}._legend_6qkdh_7{color:var(--color-text-muted);flex-wrap:wrap;gap:.75rem 1.25rem;font-size:.8rem;display:flex}._legendItem_6qkdh_15{align-items:center;gap:.4rem;display:inline-flex}._legendSwatch_6qkdh_21{border-radius:2px;width:1.25rem;height:.2rem}._canvasScroll_6qkdh_27{border:1px solid var(--color-border);border-radius:var(--radius-md);background:var(--color-bg-elevated);min-height:14rem;max-height:28rem;overflow:auto}._canvas_6qkdh_27{min-width:100%;height:auto;display:block}._nodeProject_6qkdh_42{fill:var(--color-accent-soft);stroke:var(--color-accent);stroke-width:1.5px}._nodeRepo_6qkdh_48{fill:var(--color-bg);stroke:var(--color-border-strong);stroke-width:1px}._nodeLabel_6qkdh_54{fill:var(--color-text);font-size:12px;font-family:var(--font-mono);pointer-events:none}._edgeLabel_6qkdh_61{fill:var(--color-text-muted);font-size:10px;font-family:var(--font-sans)}._empty_6qkdh_67{text-align:center;color:var(--color-text-muted);border:1px dashed var(--color-border);border-radius:var(--radius-md);margin:0;padding:1.5rem}._panel_uwnxw_1{background:var(--color-bg-elevated);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);transition:var(--transition-theme);flex-direction:column;gap:1rem;padding:1.25rem;display:flex}._heading_uwnxw_13{text-transform:uppercase;letter-spacing:.06em;color:var(--color-text-subtle);margin:0;font-size:.8rem;font-weight:600}._section_uwnxw_22{flex-direction:column;gap:.65rem;display:flex}._sectionTitle_uwnxw_28{margin:0;font-size:.95rem;font-weight:600}._field_uwnxw_34{flex-direction:column;gap:.35rem;display:flex}._label_uwnxw_40{color:var(--color-text-muted);font-size:.85rem;font-weight:500}._select_uwnxw_46{border:1px solid var(--color-border);border-radius:var(--radius-md);background:var(--color-bg);width:100%;color:var(--color-text);padding:.55rem .75rem}._button_uwnxw_55{border-radius:var(--radius-md);border:1px solid var(--color-border);background:var(--color-bg-muted);color:var(--color-text);cursor:pointer;transition:var(--transition-theme);padding:.5rem 1rem}._button_uwnxw_55:hover:not(:disabled){border-color:var(--color-border-strong)}._button_uwnxw_55:disabled{opacity:.55;cursor:not-allowed}._buttonPrimary_uwnxw_74{background:var(--color-accent);border-color:var(--color-accent);color:#fff}._buttonPrimary_uwnxw_74:hover:not(:disabled){background:var(--color-accent-hover);border-color:var(--color-accent-hover)}._buttonDanger_uwnxw_85{background:var(--color-danger);border-color:var(--color-danger);color:#fff}._buttonDanger_uwnxw_85:hover:not(:disabled){opacity:.9}._hint_uwnxw_95{color:var(--color-text-muted);margin:0;font-size:.8rem}._candidateList_uwnxw_101{border:1px solid var(--color-border);border-radius:var(--radius-md);background:var(--color-bg);max-height:10rem;margin:0;padding:0;list-style:none;overflow:auto}._candidateList_uwnxw_101 li{border-bottom:1px solid var(--color-border);font-family:var(--font-mono);word-break:break-all;padding:.45rem .65rem;font-size:.75rem}._candidateList_uwnxw_101 li:last-child{border-bottom:none}._checkboxRow_uwnxw_124{color:var(--color-text-muted);align-items:flex-start;gap:.5rem;font-size:.85rem;display:flex}._status_uwnxw_132{color:var(--color-text-subtle);margin:0;font-size:.8rem}._statusError_uwnxw_138{color:var(--color-danger)}._divider_uwnxw_142{background:var(--color-border);height:1px;margin:.25rem 0}._overlay_uwnxw_148{z-index:100;background:#0f172a73;justify-content:center;align-items:center;padding:1rem;display:flex;position:fixed;inset:0}._modal_uwnxw_159{background:var(--color-bg-elevated);border:1px solid var(--color-border);border-radius:var(--radius-lg);width:min(40rem,100%);max-height:calc(100vh - 2rem);box-shadow:var(--shadow-md);padding:1.25rem;overflow:auto}._modalTitle_uwnxw_170{margin:0 0 .75rem;font-size:1.15rem;font-weight:600}._summaryGrid_uwnxw_176{grid-template-columns:repeat(auto-fill,minmax(6rem,1fr));gap:.5rem;margin-bottom:1rem;display:grid}._summaryChip_uwnxw_183{border-radius:var(--radius-md);background:var(--color-bg-muted);padding:.5rem .65rem;font-size:.8rem}._summaryChip_uwnxw_183 strong{font-size:1rem;display:block}._recommendations_uwnxw_195{margin:0 0 1rem;padding:0;list-style:none}._recommendations_uwnxw_195 li{border-radius:var(--radius-md);background:var(--color-bg-muted);margin:.35rem 0;padding:.5rem .65rem;font-size:.85rem}._severityCritical_uwnxw_209{border-left:3px solid var(--color-danger)}._severityWarning_uwnxw_213{border-left:3px solid #d97706}._severityInfo_uwnxw_217{border-left:3px solid var(--color-accent)}._repoTable_uwnxw_221{border-collapse:collapse;width:100%;font-size:.8rem}._repoTable_uwnxw_221 th,._repoTable_uwnxw_221 td{text-align:left;border-bottom:1px solid var(--color-border);padding:.4rem .5rem}._repoTable_uwnxw_221 th{color:var(--color-text-subtle);text-transform:uppercase;font-size:.72rem}._modalActions_uwnxw_240{justify-content:flex-end;margin-top:1rem;display:flex}._tableWrap_1nm7q_1{border:1px solid var(--color-border);border-radius:var(--radius-lg);background:var(--color-bg-elevated);box-shadow:var(--shadow-sm);transition:var(--transition-theme);overflow:auto}._table_1nm7q_1{border-collapse:collapse;width:100%;font-size:.875rem}._table_1nm7q_1 th{text-align:left;text-transform:uppercase;letter-spacing:.05em;color:var(--color-text-subtle);background:var(--color-bg-muted);border-bottom:1px solid var(--color-border);padding:.65rem .85rem;font-size:.75rem;font-weight:600}._table_1nm7q_1 td{border-bottom:1px solid var(--color-border);vertical-align:middle;padding:.55rem .85rem}._projectRow_1nm7q_34 td{background:var(--color-bg-muted);border-bottom:1px solid var(--color-border-strong)}._projectHeader_1nm7q_39{flex-wrap:wrap;align-items:center;gap:.5rem 1rem;display:flex}._expandButton_1nm7q_46{color:var(--color-text);font:inherit;cursor:pointer;background:0 0;border:none;align-items:center;gap:.35rem;padding:0;font-weight:600;display:inline-flex}._expandButton_1nm7q_46:hover{color:var(--color-accent)}._projectMeta_1nm7q_63{color:var(--color-text-muted);font-size:.8rem;font-weight:400}._repoName_1nm7q_69{font-weight:500}._path_1nm7q_73{font-family:var(--font-mono);color:var(--color-text-muted);word-break:break-all;font-size:.78rem}._badge_1nm7q_80{border-radius:var(--radius-sm);text-transform:uppercase;letter-spacing:.04em;padding:.15rem .5rem;font-size:.75rem;font-weight:600;display:inline-block}._badgeSynced_1nm7q_90{color:var(--color-success);background:#0596691f}._badgeMissing_1nm7q_95{color:#d97706;background:#d9770624}[data-theme=dark] ._badgeMissing_1nm7q_95{color:#fbbf24}._actions_1nm7q_104{flex-wrap:wrap;gap:.35rem;display:flex}._button_1nm7q_110{border-radius:var(--radius-sm);border:1px solid var(--color-border);background:var(--color-bg);color:var(--color-text);cursor:pointer;transition:var(--transition-theme);padding:.35rem .65rem;font-size:.8rem}._button_1nm7q_110:hover{border-color:var(--color-border-strong);background:var(--color-bg-muted)}._buttonPrimary_1nm7q_126{background:var(--color-accent-soft);border-color:var(--color-accent);color:var(--color-accent)}._buttonPrimary_1nm7q_126:hover{background:var(--color-accent);color:#fff}._empty_1nm7q_137{text-align:center;color:var(--color-text-muted);margin:0;padding:2rem 1rem}._overlay_1lfb6_1{z-index:100;background:#0f172a73;justify-content:center;align-items:center;padding:1rem;display:flex;position:fixed;inset:0}._dialog_1lfb6_12{background:var(--color-bg-elevated);border:1px solid var(--color-border);border-radius:var(--radius-lg);width:min(32rem,100%);max-height:calc(100vh - 2rem);box-shadow:var(--shadow-md);transition:var(--transition-theme);padding:1.25rem;overflow:auto}._title_1lfb6_24{margin:0 0 .25rem;font-size:1.15rem;font-weight:600}._subtitle_1lfb6_30{color:var(--color-text-muted);margin:0 0 1rem;font-size:.875rem}._field_1lfb6_36{flex-direction:column;gap:.35rem;margin-bottom:.85rem;display:flex}._label_1lfb6_43{color:var(--color-text-muted);font-size:.85rem;font-weight:500}._select_1lfb6_49{border:1px solid var(--color-border);border-radius:var(--radius-md);background:var(--color-bg);width:100%;color:var(--color-text);padding:.55rem .75rem}._checkboxRow_1lfb6_58{color:var(--color-text-muted);align-items:center;gap:.5rem;margin-bottom:1rem;font-size:.9rem;display:flex}._actions_1lfb6_67{flex-wrap:wrap;justify-content:flex-end;gap:.5rem;display:flex}._button_1lfb6_74{border-radius:var(--radius-md);border:1px solid var(--color-border);background:var(--color-bg-muted);color:var(--color-text);cursor:pointer;transition:var(--transition-theme);padding:.5rem 1rem}._button_1lfb6_74:hover:not(:disabled){border-color:var(--color-border-strong)}._button_1lfb6_74:disabled{opacity:.55;cursor:not-allowed}._buttonPrimary_1lfb6_93{background:var(--color-accent);border-color:var(--color-accent);color:#fff}._buttonPrimary_1lfb6_93:hover:not(:disabled){background:var(--color-accent-hover);border-color:var(--color-accent-hover)}._status_1lfb6_104{background:var(--color-bg-muted);border-radius:var(--radius-md);color:var(--color-text-muted);margin:0 0 1rem;padding:.75rem 1rem;font-size:.875rem}._statusError_1lfb6_113{background:var(--color-danger-soft);color:var(--color-danger)}._summary_1lfb6_118{color:var(--color-text-muted);margin:0 0 1rem;padding:0;font-size:.85rem;list-style:none}._summary_1lfb6_118 li{font-family:var(--font-mono);margin:.2rem 0}._page_uygcw_1{flex-direction:column;gap:1rem;display:flex}._header_uygcw_7{flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:.75rem 1.5rem;display:flex}._title_uygcw_15{margin:0;font-size:1.5rem;font-weight:600}._subtitle_uygcw_21{color:var(--color-text-muted);font-size:.9rem;font-family:var(--font-mono);word-break:break-all;margin:.25rem 0 0}._chips_uygcw_29{flex-wrap:wrap;gap:.5rem;display:flex}._chip_uygcw_29{border-radius:var(--radius-md);background:var(--color-bg-elevated);border:1px solid var(--color-border);color:var(--color-text-muted);transition:var(--transition-theme);padding:.35rem .75rem;font-size:.85rem}._chip_uygcw_29 strong{color:var(--color-text);margin-right:.25rem}._toolbar_uygcw_50{flex-wrap:wrap;align-items:center;gap:.75rem 1rem;display:flex}._tabs_uygcw_57{flex-wrap:wrap;gap:.35rem;display:flex}._tab_uygcw_57{border-radius:var(--radius-md);border:1px solid var(--color-border);background:var(--color-bg-elevated);color:var(--color-text-muted);cursor:pointer;transition:var(--transition-theme);padding:.4rem .85rem;font-size:.85rem}._tab_uygcw_57:hover{border-color:var(--color-border-strong);color:var(--color-text)}._tabActive_uygcw_79{background:var(--color-accent-soft);border-color:var(--color-accent);color:var(--color-accent)}._search_uygcw_85{border:1px solid var(--color-border);border-radius:var(--radius-md);background:var(--color-bg-elevated);min-width:12rem;max-width:22rem;color:var(--color-text);flex:1;padding:.5rem .75rem}._search_uygcw_85:focus{border-color:var(--color-accent);box-shadow:0 0 0 3px var(--color-focus);outline:none}._layout_uygcw_102{grid-template-columns:minmax(0,1fr) minmax(14rem,18rem);align-items:start;gap:1.25rem;display:grid}@media (width<=960px){._layout_uygcw_102{grid-template-columns:1fr}}._loading_uygcw_115,._error_uygcw_116{border-radius:var(--radius-md);margin:0;padding:1rem}._loading_uygcw_115{color:var(--color-text-muted);background:var(--color-bg-elevated);border:1px solid var(--color-border)}._error_uygcw_116{color:var(--color-danger);background:var(--color-danger-soft);border:1px solid var(--color-danger)}._graphFilters_uygcw_134{flex-wrap:wrap;align-items:center;gap:.75rem 1.25rem;display:flex}._checkLabel_uygcw_141{color:var(--color-text-muted);cursor:pointer;align-items:center;gap:.4rem;font-size:.85rem;display:inline-flex}._graphPanel_uygcw_150{min-width:0}
````

## File: src/metagit/data/web/assets/index-DOullneW.js
````javascript
var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t=
⋮----
`+Ce+e+we}var Te=!1;function Ee(e,t)
⋮----
`+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(`<anonymous>`)&&(u=u.replace(`<anonymous>`,e.displayName)),u}while(1<=r&&0<=i);break}}}finally
⋮----
`+e.stack}}var ke=Object.prototype.hasOwnProperty,Ae=t.unstable_scheduleCallback,je=t.unstable_cancelCallback,Me=t.unstable_shouldYield,Ne=t.unstable_requestPaint,Pe=t.unstable_now,Fe=t.unstable_getCurrentPriorityLevel,Ie=t.unstable_ImmediatePriority,Le=t.unstable_UserBlockingPriority,Re=t.unstable_NormalPriority,ze=t.unstable_LowPriority,Be=t.unstable_IdlePriority,Ve=t.log,He=t.unstable_setDisableYieldValue,Ue=null,We=null;function Ge(e)
`).replace(Ad,``)}function Md(e,t)
⋮----
Please change the parent <Route path="$
````

## File: src/metagit/data/web/favicon.svg
````xml
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg>
````

## File: src/metagit/data/web/icons.svg
````xml
<svg xmlns="http://www.w3.org/2000/svg">
  <symbol id="bluesky-icon" viewBox="0 0 16 17">
    <g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
    <defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
  </symbol>
  <symbol id="discord-icon" viewBox="0 0 20 19">
    <path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
  </symbol>
  <symbol id="documentation-icon" viewBox="0 0 21 20">
    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
  </symbol>
  <symbol id="github-icon" viewBox="0 0 19 19">
    <path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
  </symbol>
  <symbol id="social-icon" viewBox="0 0 20 20">
    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
  </symbol>
  <symbol id="x-icon" viewBox="0 0 19 19">
    <path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
  </symbol>
</svg>
````

## File: src/metagit/__init__.py
````python
#!/usr/bin/env python
"""
Metagit detection tool

.. currentmodule:: metagit
.. moduleauthor:: Metagit <zloeber@gmail.com>
"""
⋮----
here = path.abspath(path.dirname(__file__))
⋮----
__version__ = version("metagit-cli")
⋮----
__version__ = "0.0.0"
⋮----
SCRIPT_PATH = os.path.abspath(os.path.split(__file__)[0])
CONFIG_PATH = os.getenv(
DATA_PATH = os.getenv("METAGIT_DATA", os.path.join(SCRIPT_PATH, "data"))
DEFAULT_CONFIG = os.path.join(DATA_PATH, "metagit.config.yaml")
````

## File: src/metagit/__main__.py
````python
#!/usr/bin/env python
"""Compatibility module for legacy metagit entrypoints."""
````

## File: tasks/Taskfile.gemini.yml
````yaml
# yaml-language-server: $schema=https://taskfile.dev/schema.json
version: "3"
silent: true
vars:
  GEMINI_API_KEY:
    sh: echo ${GEMINI_API_KEY:-""}

tasks:
  show:
    desc: Show python variables for this task
    cmds:
      - |
        echo "GEMINI_API_KEY: {{.GEMINI_API_KEY}}"
  docs:
    desc: Build the docs via gemini
    cmds:
      - |
        gemini -y -p 'You are an expert devops and software engineer. Explore this project for its use of secrets and variables paying special attention to only those affecting the runtime behavior of the code and the cicd workflows used to release the projects artifacts. Create a report in ./docs/secrets.analysis.md on each of the secrets found and were they are sourced from. Using this data create a yaml file at ./docs/secrets.definitions.yml that represents the secrets, their provenance, and where they should be placed.'
        gemini -y -p 'Perform a comprehensive analysis of the files in this project. Update the yaml file named .metagit.example.yml in the root of this project in a manner that adheres to the jsonschema file located at ./schemas/metagit_config.schema.json. Ensure you skip files matching the patterns in .gitignore. Do your best to update even optional elements of this schema. Pay special attention to dependencies found in Dockerfiles, CICD files, and mcp definitions. When complete also create ./docs/app.logic.md with additional documentation on the project components that includes a mermaid diagram of how they interact.'
````

## File: tasks/Taskfile.github.yml
````yaml
# yaml-language-server: $schema=https://taskfile.dev/schema.json

version: "3"
silent: true
vars:
  GITHUB_PATH:
    sh: |
      git remote get-url origin 2>/dev/null | sed -Ee 's/.*:(.+)\.git/\1/'
  GITHUB_URL:
    sh: 'echo ${GITHUB_URL:-"https://www.github.com"}'
  GITHUB_UI_PATH: "{{.GITHUB_URL}}/{{.GITHUB_PATH}}"
  GITHUB_TOKEN:
    sh: echo ${GITHUB_TOKEN:-""}

tasks:
  show:
    desc: Show github variables for this task
    cmds:
      - |
        echo "GITHUB_PATH: {{.GITHUB_PATH}}"
        echo "GITHUB_UI_PATH: {{.GITHUB_UI_PATH}}"

  ui:
    desc: Open website for github project
    cmds:
      - |
        GITHUB_UI_PATH=$(git remote get-url origin 2>/dev/null | sed -Ee 's/.*:(.+)\.git/\1/' | awk '{print "https://www.github.com/" $1}')
        echo "path: ${GITHUB_UI_PATH}"
        if [[ "{{OS}}" == "darwin"* ]]; then
          open ${GITHUB_UI_PATH}
        else
          xdg-open ${GITHUB_UI_PATH}
        fi

  actions:ui:
    desc: Open website for github project actions
    cmds:
      - |
        if [[ "$OSTYPE" == "darwin"* ]]; then
          open {{.GITHUB_UI_PATH}}/actions
        else
          xdg-open {{.GITHUB_UI_PATH}}/actions
        fi

  models:init:
    desc: Initialize models for github project
    cmds:
      - gh extension install https://github.com/github/gh-models || true

  models:list:
    desc: List models for github project
    cmds:
      - gh models list
````

## File: tasks/Taskfile.mcp.yml
````yaml
# yaml-language-server: $schema=https://taskfile.dev/schema.json

version: "3"
silent: true

tasks:
  show:
    desc: Show MCP variables for this task
    cmds:
      - |
        echo "OLLAMA_HOST: {{.OLLAMA_HOST}}"

  inspector:
    desc: Start MCP Inspector
    cmds:
      - npx -y @modelcontextprotocol/inspector
    env:
      DANGEROUSLY_OMIT_AUTH: true

  scan:
    desc: Scan MCP package for issues
    env:
      MCP_SCANNER_LLM_API_KEY: test
      MCP_SCANNER_API_KEY: test
      MCP_SCANNER_LLM_ENDPOINT: $OLLAMA_HOST
      MCP_SCANNER_LLM_MODEL: gemma4:31b
    cmds:
      - |
        uv tool install cisco-ai-mcp-scanner
        uv tool upgrade cisco-ai-mcp-scanner
        mcp-scanner --format summary \
          --stdio-command uvx \
          --stdio-arg=pl-cli --stdio-arg=mcp --stdio-arg=-c --stdio-arg=config.yaml \
          --stdio-arg=--no-ingest-on-startup \
          --format table \
          --analyzers yara \
          -o security_scan_results.md
````

## File: tests/api/test_catalog_api.py
````python
#!/usr/bin/env python
"""HTTP API tests for workspace catalog v2 endpoints."""
⋮----
def test_catalog_project_and_repo_crud(tmp_path: Path) -> None
⋮----
server = build_server(root=str(tmp_path), host="127.0.0.1", port=0)
thread = threading.Thread(target=server.serve_forever, daemon=True)
⋮----
port = server.server_address[1]
base = f"http://127.0.0.1:{port}"
⋮----
projects = json.loads(
⋮----
add_body = json.dumps({"name": "platform"}).encode("utf-8")
add_req = urllib.request.Request(
added = json.loads(urllib.request.urlopen(add_req, timeout=5).read().decode("utf-8"))
⋮----
repo_body = json.dumps(
repo_req = urllib.request.Request(
repo_added = json.loads(
⋮----
repos = json.loads(
⋮----
delete_repo = urllib.request.Request(
⋮----
delete_project = urllib.request.Request(
````

## File: tests/api/test_layout_api.py
````python
#!/usr/bin/env python
"""HTTP API tests for workspace layout v2 endpoints."""
⋮----
def test_layout_project_rename_api(tmp_path: Path) -> None
⋮----
sync_root = tmp_path / "sync"
⋮----
server = build_server(root=str(tmp_path), host="127.0.0.1", port=0)
thread = threading.Thread(target=server.serve_forever, daemon=True)
⋮----
port = server.server_address[1]
body = json.dumps({"to_name": "apps"}).encode("utf-8")
req = urllib.request.Request(
payload = json.loads(urllib.request.urlopen(req, timeout=5).read().decode("utf-8"))
````

## File: tests/api/test_repo_search_api.py
````python
#!/usr/bin/env python
"""
HTTP API tests for managed repository search.
"""
⋮----
def test_repo_search_endpoint_returns_matches(tmp_path: Path) -> None
⋮----
repo_dir = tmp_path / "platform" / "abacus-app"
⋮----
server = build_server(root=str(tmp_path), host="127.0.0.1", port=0)
thread = threading.Thread(target=server.serve_forever, daemon=True)
⋮----
port = server.server_address[1]
url = f"http://127.0.0.1:{port}/v1/repos/search?q=abacus"
payload = json.loads(
⋮----
def test_repo_resolve_ambiguous_returns_409(tmp_path: Path) -> None
⋮----
app_repo = tmp_path / "platform" / "abacus-app"
mod_repo = tmp_path / "shared" / "abacus-module"
⋮----
url = f"http://127.0.0.1:{port}/v1/repos/resolve?q=abacus"
req = urllib.request.Request(url)
⋮----
def test_unknown_path_returns_404(tmp_path: Path) -> None
⋮----
url = f"http://127.0.0.1:{port}/v1/no-such"
````

## File: tests/cli/commands/test_api.py
````python
#!/usr/bin/env python
"""
CLI tests for metagit api commands.
"""
⋮----
def test_api_cli_status_once_reports_bound_port(tmp_path: Path) -> None
⋮----
runner = CliRunner()
result = runner.invoke(
````

## File: tests/cli/commands/test_config_patch.py
````python
#!/usr/bin/env python
⋮----
"""CLI tests for config patch/preview/tree commands."""
⋮----
def _minimal_metagit(path: Path) -> None
⋮----
def test_config_patch_single_op_save() -> None
⋮----
runner = CliRunner()
⋮----
result = runner.invoke(
⋮----
on_disk = yaml.safe_load(Path(".metagit.yml").read_text(encoding="utf-8"))
⋮----
def test_config_patch_operations_file() -> None
⋮----
ops_path = Path("ops.json")
⋮----
def test_config_preview_json() -> None
⋮----
payload = json.loads(result.output)
````

## File: tests/cli/commands/test_project_source.py
````python
#!/usr/bin/env python
"""
CLI tests for project source sync commands.
"""
⋮----
def test_project_source_sync_dry_run(monkeypatch, tmp_path) -> None
⋮----
config_path = tmp_path / ".metagit.yml"
⋮----
runner = CliRunner()
result = runner.invoke(
⋮----
def test_project_source_sync_reconcile_requires_yes(monkeypatch, tmp_path) -> None
````

## File: tests/cli/commands/test_search.py
````python
#!/usr/bin/env python
"""
CLI tests for metagit search / find.
"""
⋮----
def test_search_command_returns_json_matches() -> None
⋮----
runner = CliRunner()
⋮----
repo_dir = Path("platform") / "abacus-app"
⋮----
result = runner.invoke(
⋮----
def test_find_alias_matches_search_command() -> None
⋮----
result = runner.invoke(cli, ["find", "--help"])
````

## File: tests/cli/commands/test_web.py
````python
#!/usr/bin/env python
"""CLI tests for metagit web commands."""
⋮----
def test_web_serve_status_once(tmp_path: Path) -> None
⋮----
runner = CliRunner()
⋮----
result = runner.invoke(
````

## File: tests/core/config/test_graph_cypher_export.py
````python
#!/usr/bin/env python
⋮----
"""Tests for workspace graph Cypher export."""
⋮----
def test_export_manual_relationships_produces_cypher(tmp_path: Path) -> None
⋮----
workspace_root = tmp_path / ".metagit"
⋮----
config = MetagitConfig(
⋮----
result = GraphCypherExportService().export(
⋮----
def test_export_tool_calls_only_format() -> None
````

## File: tests/core/config/test_patch_service.py
````python
#!/usr/bin/env python
⋮----
"""Tests for ConfigPatchService."""
⋮----
def _write_metagit(path: Path, payload: dict) -> None
⋮----
def test_patch_metagit_set_name_dry_run(tmp_path: Path) -> None
⋮----
config_path = tmp_path / ".metagit.yml"
⋮----
service = ConfigPatchService()
result = service.patch(
⋮----
on_disk = yaml.safe_load(config_path.read_text(encoding="utf-8"))
⋮----
def test_patch_metagit_set_name_save(tmp_path: Path) -> None
⋮----
def test_patch_append_workspace_project(tmp_path: Path) -> None
````

## File: tests/core/init/test_init_service.py
````python
#!/usr/bin/env python
"""Tests for metagit init template service."""
⋮----
def test_list_templates_includes_hermes() -> None
⋮----
service = InitService()
ids = {item.id for item in service.list_templates()}
⋮----
def test_init_hermes_with_answers_file(tmp_path: Path) -> None
⋮----
answers = tmp_path / "answers.yml"
⋮----
target = tmp_path / "coordinator"
⋮----
result = service.initialize(
manifest = yaml.safe_load((target / ".metagit.yml").read_text(encoding="utf-8"))
⋮----
local = next(p for p in manifest["workspace"]["projects"] if p["name"] == "local")
⋮----
def test_init_minimal_library_kind(tmp_path: Path) -> None
⋮----
target = tmp_path / "lib"
````

## File: tests/core/mcp/services/test_bootstrap_sampling.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.mcp.services.bootstrap_sampling
"""
⋮----
def test_sampling_disabled_returns_plan_only_payload() -> None
⋮----
service = BootstrapSamplingService(sampling_supported=False)
⋮----
result = service.generate(context={"repo_root": "/tmp/repo"}, confirm_write=False)
⋮----
def test_sampling_success_returns_draft_yaml() -> None
⋮----
def sampler(payload: dict[str, str]) -> str
⋮----
_ = payload
⋮----
service = BootstrapSamplingService(sampling_supported=True, sampler=sampler)
⋮----
result = service.generate(context={"repo_root": "/tmp/repo"}, confirm_write=True)
````

## File: tests/core/mcp/services/test_cross_project_dependencies.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.mcp.services.cross_project_dependencies
"""
⋮----
def _workspace_config(tmp_path: Path) -> tuple[MetagitConfig, str]
⋮----
root = tmp_path / "workspace"
shared_url = "https://github.com/example/shared-lib.git"
alpha_repo = root / "alpha" / "api"
beta_repo = root / "beta" / "worker"
⋮----
relative_api = os.path.relpath(alpha_repo, beta_repo)
⋮----
def test_map_dependencies_finds_url_match_and_imports(tmp_path: Path) -> None
⋮----
registry = MagicMock()
⋮----
service = CrossProjectDependencyService(registry=registry)
⋮----
result = service.map_dependencies(
⋮----
edge_types = {edge.type for edge in result.edges}
⋮----
def test_map_dependencies_unknown_project(tmp_path: Path) -> None
⋮----
service = CrossProjectDependencyService(registry=MagicMock())
⋮----
def test_map_dependencies_respects_depth(tmp_path: Path) -> None
⋮----
shallow = service.map_dependencies(
deep = service.map_dependencies(
````

## File: tests/core/mcp/services/test_import_hint_scanner.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.mcp.services.import_hint_scanner
"""
⋮----
def test_scan_package_json_file_dependency(tmp_path: Path) -> None
⋮----
lib = tmp_path / "lib-repo"
app = tmp_path / "app-repo"
⋮----
path_to_id = {
scanner = ImportHintScanner()
⋮----
hints = scanner.scan_repo(repo_path=str(app), path_to_repo_id=path_to_id)
````

## File: tests/core/mcp/services/test_repo_ops.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.mcp.services.repo_ops
"""
⋮----
def test_pull_requires_explicit_mutation_enable(tmp_path: Path) -> None
⋮----
repo_dir = tmp_path / "repo"
⋮----
service = RepoOperationsService()
⋮----
result = service.sync(repo_path=str(repo_dir), mode="pull", allow_mutation=False)
⋮----
def test_inspect_reports_repo_status(tmp_path: Path) -> None
⋮----
result = service.inspect(repo_path=str(repo_dir))
````

## File: tests/core/mcp/services/test_session_store.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.mcp.services.session_store
"""
⋮----
def test_get_workspace_meta_returns_defaults_when_missing(tmp_path: Path) -> None
⋮----
store = SessionStore(workspace_root=str(tmp_path))
meta = store.get_workspace_meta()
⋮----
def test_set_active_project_persists_workspace_meta(tmp_path: Path) -> None
⋮----
meta = store.set_active_project(project_name="alpha")
⋮----
reloaded = store.get_workspace_meta()
⋮----
def test_project_session_roundtrip(tmp_path: Path) -> None
⋮----
session = store.update_project_session(
reloaded = store.get_project_session(project_name="alpha")
⋮----
def test_corrupt_project_session_returns_empty(tmp_path: Path) -> None
⋮----
path = store.sessions_dir / "alpha.json"
⋮----
session = store.get_project_session(project_name="alpha")
⋮----
def test_env_override_secret_value_rejected(tmp_path: Path) -> None
⋮----
def test_save_workspace_meta_writes_json(tmp_path: Path) -> None
⋮----
raw = json.loads((store.sessions_dir / "_workspace.json").read_text(encoding="utf-8"))
````

## File: tests/core/mcp/services/test_upstream_hints.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.mcp.services.upstream_hints
"""
⋮----
def test_terraform_blocker_ranks_infra_repos_higher() -> None
⋮----
service = UpstreamHintService()
repo_context = [
⋮----
ranked = service.rank(
````

## File: tests/core/mcp/services/test_workspace_health.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.mcp.services.workspace_health
"""
⋮----
def _config(tmp_path: Path) -> tuple[MetagitConfig, str]
⋮----
root = tmp_path / "workspace"
present = root / "alpha" / "api"
⋮----
def test_health_check_reports_missing_repo(tmp_path: Path) -> None
⋮----
registry = MagicMock()
⋮----
service = WorkspaceHealthService(registry=registry)
⋮----
result = service.check(
⋮----
actions = {item.action for item in result.recommendations}
⋮----
repo = next(r for r in result.repos if r.repo_name == "api")
````

## File: tests/core/mcp/services/test_workspace_index.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.mcp.services.workspace_index
"""
⋮----
def test_workspace_index_resolves_repo_paths(tmp_path: Path) -> None
⋮----
workspace_root = tmp_path / "workspace"
⋮----
repo_path = workspace_root / "repo-a"
⋮----
config = MetagitConfig(
service = WorkspaceIndexService()
⋮----
rows = service.build_index(config=config, workspace_root=str(workspace_root))
````

## File: tests/core/mcp/services/test_workspace_semantic_search.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.mcp.services.workspace_semantic_search
"""
⋮----
def test_search_across_repos_empty_query() -> None
⋮----
service = WorkspaceSemanticSearchService(registry=MagicMock())
result = service.search_across_repos(query="   ", repo_paths=["/a"])
⋮----
def test_search_runs_gitnexus_and_parses_json(mock_run: MagicMock) -> None
⋮----
registry = MagicMock()
⋮----
proc = MagicMock()
⋮----
service = WorkspaceSemanticSearchService(registry=registry)
out = service.search_across_repos(
⋮----
row = out["results"][0]
⋮----
call_kw = mock_run.call_args.kwargs
⋮----
cmd = mock_run.call_args.args[0]
⋮----
def test_parse_query_json_finds_embedded_line() -> None
⋮----
blob = "log line\n" + json.dumps({"processes": [], "note": "x"}) + "\n"
parsed = service._parse_query_json(blob)
````

## File: tests/core/mcp/services/test_workspace_snapshot.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.mcp.services.workspace_snapshot
"""
⋮----
def _write_workspace(tmp_path: Path) -> str
⋮----
repo_path = tmp_path / "alpha" / "repo-one"
⋮----
def _load_config(tmp_path: Path)
⋮----
manager = MetagitConfigManager(config_path=tmp_path / ".metagit.yml")
loaded = manager.load_config()
⋮----
def test_create_writes_snapshot_file(tmp_path: Path) -> None
⋮----
root = _write_workspace(tmp_path)
config = _load_config(tmp_path)
service = WorkspaceSnapshotService()
⋮----
payload = service.create(
⋮----
snapshot_path = tmp_path / ".metagit" / "snapshots" / f"{payload['snapshot_id']}.json"
⋮----
raw = json.loads(snapshot_path.read_text(encoding="utf-8"))
⋮----
def test_restore_missing_snapshot_returns_error(tmp_path: Path) -> None
⋮----
result = service.restore(
⋮----
def test_restore_switches_active_project(tmp_path: Path) -> None
⋮----
context = ProjectContextService()
⋮----
snapshot_service = WorkspaceSnapshotService()
created = snapshot_service.create(config=config, workspace_root=root)
⋮----
store = SessionStore(workspace_root=root)
⋮----
cleared = store.get_workspace_meta()
⋮----
restored = snapshot_service.restore(
⋮----
meta = store.get_workspace_meta()
````

## File: tests/core/mcp/services/test_workspace_sync.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.mcp.services.workspace_sync
"""
⋮----
def test_sync_many_dry_run_skips_git_calls(tmp_path: Path) -> None
⋮----
repo_path = tmp_path / "alpha" / "repo-one"
⋮----
rows = [
repo_ops = MagicMock()
service = WorkspaceSyncService(repo_ops=repo_ops)
⋮----
payload = service.sync_many(rows, dry_run=True)
⋮----
def test_sync_many_only_if_missing_skips_existing(tmp_path: Path) -> None
⋮----
payload = service.sync_many(rows, only_if="missing", dry_run=False)
````

## File: tests/core/mcp/services/test_workspace_template.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.mcp.services.workspace_template
"""
⋮----
def test_template_apply_dry_run_lists_files(tmp_path: Path) -> None
⋮----
root = tmp_path / "workspace"
repo = root / "alpha" / "api"
⋮----
config = MetagitConfig(
service = WorkspaceTemplateService()
⋮----
result = service.apply(
⋮----
def test_template_apply_requires_confirm_when_not_dry_run(tmp_path: Path) -> None
````

## File: tests/core/mcp/test_gate.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.mcp.gate
"""
⋮----
def test_missing_root_is_inactive_missing() -> None
⋮----
gate = WorkspaceGate()
⋮----
result = gate.evaluate(root_path=None)
⋮----
def test_missing_config_file_is_inactive_missing(tmp_path: Path) -> None
⋮----
result = gate.evaluate(root_path=str(tmp_path))
⋮----
def test_invalid_config_file_is_inactive_invalid(tmp_path: Path) -> None
⋮----
config_path = tmp_path / ".metagit.yml"
⋮----
def test_valid_config_file_is_active(tmp_path: Path) -> None
````

## File: tests/core/mcp/test_models.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.mcp.models
"""
⋮----
def test_activation_state_values() -> None
⋮----
def test_workspace_status_model() -> None
⋮----
status = WorkspaceStatus(
````

## File: tests/core/mcp/test_resources.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.mcp.resources
"""
⋮----
def test_workspace_resources_available_when_active() -> None
⋮----
ops = OperationsLogService()
⋮----
publisher = ResourcePublisher(ops_log=ops)
config = MetagitConfig(
⋮----
config_resource = publisher.get_resource(
repos_resource = publisher.get_resource(
ops_resource = publisher.get_resource(uri="metagit://workspace/ops-log")
````

## File: tests/core/mcp/test_root_resolver.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.mcp.root_resolver
"""
⋮----
def test_env_root_has_highest_precedence(monkeypatch, tmp_path: Path) -> None
⋮----
env_root = tmp_path / "env-root"
⋮----
resolver = WorkspaceRootResolver()
⋮----
result = resolver.resolve(cwd=str(tmp_path), cli_root=str(tmp_path / "cli-root"))
⋮----
def test_cli_root_used_when_env_unset(monkeypatch, tmp_path: Path) -> None
⋮----
cli_root = tmp_path / "cli-root"
⋮----
result = resolver.resolve(cwd=str(tmp_path), cli_root=str(cli_root))
⋮----
def test_walk_up_finds_workspace_root(monkeypatch, tmp_path: Path) -> None
⋮----
root = tmp_path / "workspace-root"
nested = root / "services" / "api"
⋮----
result = resolver.resolve(cwd=str(nested), cli_root=None)
````

## File: tests/core/prompt/test_prompt_service.py
````python
#!/usr/bin/env python
"""Tests for metagit prompt emission service."""
⋮----
def _sample_config() -> MetagitConfig
⋮----
def test_catalog_lists_instructions_kind() -> None
⋮----
kinds = [entry.kind for entry in list_catalog()]
⋮----
def test_emit_instructions_workspace() -> None
⋮----
result = PromptService().emit(
⋮----
def test_emit_instructions_repo() -> None
⋮----
def test_emit_session_start_includes_manifest_when_requested() -> None
⋮----
def test_emit_repo_enrich_includes_detect_commands() -> None
⋮----
def test_kind_not_allowed_for_scope() -> None
````

## File: tests/core/web/__init__.py
````python

````

## File: tests/core/web/test_config_preview.py
````python
#!/usr/bin/env python
"""Tests for config YAML preview rendering."""
⋮----
def test_render_metagit_yaml_normalized() -> None
⋮----
config = MetagitConfig.model_validate({"name": "demo", "kind": "application"})
rendered = render_metagit_yaml(config, style="normalized")
⋮----
def test_render_appconfig_yaml_masks_secrets() -> None
⋮----
config = AppConfig.model_validate(
rendered = render_appconfig_yaml(
⋮----
def test_redact_secrets_nested() -> None
⋮----
payload = {"providers": {"gitlab": {"api_token": "glpat-secret"}}}
redacted = redact_secrets(payload)
````

## File: tests/core/web/test_graph_service.py
````python
#!/usr/bin/env python
"""Tests for workspace graph web view builder."""
⋮----
def test_build_view_includes_manual_and_structure(tmp_path: Path) -> None
⋮----
workspace_root = tmp_path / ".metagit"
⋮----
config = MetagitConfig(
view = WorkspaceGraphService().build_view(
⋮----
manual = [edge for edge in view.edges if edge.source == "manual"]
⋮----
structure = [edge for edge in view.edges if edge.source == "structure"]
````

## File: tests/core/web/test_job_store.py
````python
#!/usr/bin/env python
"""Unit tests for SyncJobStore."""
⋮----
def test_job_lifecycle_and_events() -> None
⋮----
store = SyncJobStore()
job_id = store.create_job()
⋮----
status = store.get(job_id)
⋮----
events = store.drain_events(job_id)
````

## File: tests/core/web/test_ops_handler.py
````python
#!/usr/bin/env python
"""HTTP tests for web ops routes (v3 API)."""
⋮----
manifest_lines = [
⋮----
server = build_web_server(
thread = threading.Thread(target=server.serve_forever, daemon=True)
⋮----
port = server.server_address[1]
base = f"http://127.0.0.1:{port}"
⋮----
def _post_json(url: str, payload: dict) -> tuple[int, dict]
⋮----
body = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
⋮----
raw = exc.read().decode("utf-8")
⋮----
def test_health_endpoint_returns_ok(tmp_path: Path) -> None
⋮----
def test_prune_preview_empty(tmp_path: Path) -> None
⋮----
def test_sync_dry_run_job_completes(tmp_path: Path) -> None
⋮----
job_id = created["job_id"]
⋮----
deadline = time.time() + 5.0
final_state = ""
⋮----
job_status = json.loads(resp.read().decode("utf-8"))
final_state = job_status["state"]
````

## File: tests/core/workspace/test_agent_instructions.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.workspace.agent_instructions
"""
⋮----
def test_file_level_instructions_without_workspace_block() -> None
⋮----
config = MetagitConfig(
result = AgentInstructionsResolver().resolve(config)
⋮----
def test_legacy_agent_prompt_alias_on_load() -> None
⋮----
config = MetagitConfig.model_validate(
⋮----
def test_compose_all_layers_including_repo() -> None
⋮----
project = config.workspace.projects[0]
repo = project.repos[0]
result = AgentInstructionsResolver().resolve(config, project=project, repo=repo)
````

## File: tests/core/workspace/test_context_models.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.workspace.context_models
"""
⋮----
def test_validate_env_key_accepts_metagit_style_keys() -> None
⋮----
def test_validate_env_key_rejects_lowercase() -> None
⋮----
def test_validate_env_value_rejects_bearer_token() -> None
⋮----
def test_project_session_rejects_long_agent_notes() -> None
⋮----
def test_project_session_caps_recent_repos() -> None
⋮----
session = ProjectSession(
````

## File: tests/core/workspace/test_dedupe_resolver.py
````python
#!/usr/bin/env python
"""Tests for per-project dedupe resolution."""
⋮----
def _config_with_projects() -> MetagitConfig
⋮----
def test_resolve_effective_dedupe_inherits_workspace_default() -> None
⋮----
workspace_dedupe = WorkspaceDedupeConfig(enabled=True)
⋮----
def test_resolve_effective_dedupe_project_disable_override() -> None
⋮----
project = WorkspaceProject(
⋮----
def test_resolve_effective_dedupe_project_enable_override() -> None
⋮----
workspace_dedupe = WorkspaceDedupeConfig(enabled=False)
⋮----
def test_resolve_effective_dedupe_for_project_by_name() -> None
⋮----
config = _config_with_projects()
⋮----
def test_resolve_dedupe_for_layout_without_project() -> None
⋮----
def test_workspace_project_rejects_unknown_dedupe_keys() -> None
````

## File: tests/core/workspace/test_hydrate.py
````python
#!/usr/bin/env python
"""Tests for symlink mount hydration."""
⋮----
class _DummyLogger
⋮----
def set_level(self, _: str) -> None
⋮----
def warning(self, _: str) -> None
⋮----
def debug(self, _: str) -> None
⋮----
def test_collect_file_copy_jobs_counts_nested_files(tmp_path: Path) -> None
⋮----
root = tmp_path / "src"
⋮----
jobs = collect_file_copy_jobs(root)
⋮----
def test_materialize_symlink_mount_replaces_link_with_directory(tmp_path: Path) -> None
⋮----
source = tmp_path / "canonical"
⋮----
mount = tmp_path / "project" / "repo"
⋮----
def test_project_sync_hydrate_after_deduped_symlink(tmp_path: Path) -> None
⋮----
source = tmp_path / "user-site"
⋮----
workspace_root = tmp_path / ".metagit"
dedupe = WorkspaceDedupeConfig(enabled=True, canonical_dir="_canonical")
manager = ProjectManager(workspace_root, _DummyLogger(), dedupe=dedupe)
repo = ProjectPath(name="site", path=str(source), sync=True, kind=ProjectKind.WEBSITE)
project = WorkspaceProject(name="local", repos=[repo])
⋮----
mount = workspace_root / "local" / "site"
````

## File: tests/core/workspace/test_layout_service.py
````python
#!/usr/bin/env python
"""Tests for workspace layout rename and move service."""
⋮----
def _setup_workspace(tmp_path: Path) -> tuple[Path, str, str]
⋮----
sync_root = tmp_path / "sync"
⋮----
manifest = {
config_path = tmp_path / ".metagit.yml"
⋮----
manager = MetagitConfigManager(str(config_path))
loaded = manager.load_config()
⋮----
def test_rename_project_moves_sync_folder(tmp_path: Path) -> None
⋮----
service = WorkspaceLayoutService()
result = service.rename_project(
⋮----
manager = MetagitConfigManager(config_path)
reloaded = manager.load_config()
⋮----
def test_rename_repo_moves_mount(tmp_path: Path) -> None
⋮----
result = service.rename_repo(
⋮----
def test_move_repo_between_projects(tmp_path: Path) -> None
⋮----
result = service.move_repo(
⋮----
beta = next(p for p in reloaded.workspace.projects if p.name == "beta")
⋮----
def test_dry_run_does_not_mutate(tmp_path: Path) -> None
````

## File: tests/scripts/test_prepush_gate_security.py
````python
#!/usr/bin/env python
⋮----
"""Tests for context-aware security steps in prepush-gate."""
⋮----
def _prepush_gate_module()
⋮----
path = Path(__file__).resolve().parents[2] / "scripts" / "prepush-gate.py"
spec = importlib.util.spec_from_file_location("prepush_gate", path)
⋮----
module = importlib.util.module_from_spec(spec)
⋮----
def test_security_scan_plan_full_when_unknown() -> None
⋮----
gate = _prepush_gate_module()
⋮----
def test_security_scan_plan_deps_triggers_sync() -> None
⋮----
def test_security_scan_plan_src_without_sync() -> None
⋮----
def test_security_scan_plan_skips_docs_only() -> None
````

## File: tests/test_config_example_generator.py
````python
#!/usr/bin/env python
"""Tests for MetagitConfig exemplar generation."""
⋮----
def test_render_yaml_includes_header_and_workspace() -> None
⋮----
generator = ConfigExampleGenerator(overrides=load_example_overrides())
rendered = generator.render_yaml(include_workspace=True, comment_style="line")
⋮----
def test_build_merges_overrides() -> None
⋮----
generator = ConfigExampleGenerator(overrides={"name": "override-name"})
payload = generator.build(include_workspace=False)
⋮----
def test_generated_payload_validates_when_overrides_used() -> None
⋮----
payload = generator.build(include_workspace=True)
config = MetagitConfig.model_validate(payload)
````

## File: tests/test_config_yaml_display.py
````python
#!/usr/bin/env python
"""Tests for config YAML display helpers."""
⋮----
def test_dump_config_dict_uses_literal_block_for_multiline() -> None
⋮----
rendered = dump_config_dict(
⋮----
rendered_unicode = dump_config_dict({"note": "status — ok"})
````

## File: tests/test_documentation_graph_models.py
````python
#!/usr/bin/env python
"""Tests for documentation sources and manual graph relationships."""
⋮----
def test_documentation_accepts_strings_and_dicts() -> None
⋮----
config = MetagitConfig(
⋮----
nodes = config.documentation_graph_nodes()
⋮----
def test_graph_relationships_and_export() -> None
⋮----
exported = config.graph_export_payload()
⋮----
def test_load_metagit_yml_documentation_block(tmp_path: Path) -> None
⋮----
manifest = {
path = tmp_path / ".metagit.yml"
⋮----
loaded = MetagitConfigManager(str(path)).load_config()
⋮----
def test_manual_graph_edges_in_dependency_map(tmp_path: Path) -> None
⋮----
workspace_root = tmp_path / ".metagit"
⋮----
result = CrossProjectDependencyService().map_dependencies(
manual = [edge for edge in result.edges if edge.type == "manual"]
````

## File: tests/test_project_manager_dedupe.py
````python
#!/usr/bin/env python
"""Integration tests for workspace dedupe sync layout."""
⋮----
class _DummyLogger
⋮----
def set_level(self, _: str) -> None
⋮----
def warning(self, _: str) -> None
⋮----
def debug(self, _: str) -> None
⋮----
source = tmp_path / "user-site"
⋮----
workspace_root = tmp_path / ".metagit"
dedupe = WorkspaceDedupeConfig(enabled=True, canonical_dir="_canonical")
manager = ProjectManager(workspace_root, _DummyLogger(), dedupe=dedupe)
⋮----
repo = ProjectPath(name="site", path=str(source), sync=True, kind=ProjectKind.WEBSITE)
project_a = WorkspaceProject(name="local", repos=[repo])
project_b = WorkspaceProject(
⋮----
mount_a = workspace_root / "local" / "site"
mount_b = workspace_root / "mirror" / "site"
canonical = workspace_root / "_canonical"
canonical_dirs = [path for path in canonical.iterdir() if path.is_dir() or path.is_symlink()]
⋮----
def _fake_clone(url: str, target: str, progress=None) -> None:  # noqa: ARG001
⋮----
url = "https://github.com/example/remote.git"
project_a = WorkspaceProject(
⋮----
mount_a = workspace_root / "p1" / "remote"
mount_b = workspace_root / "p2" / "remote"
````

## File: tests/test_project_manager_prune.py
````python
#!/usr/bin/env python
"""Tests for unmanaged sync directory listing and prune helpers."""
⋮----
class _DummyLogger
⋮----
def set_level(self, _: str) -> None
⋮----
def warning(self, _: str) -> None
⋮----
def debug(self, _: str) -> None
⋮----
def _config_one_repo() -> MetagitConfig
⋮----
def test_list_unmanaged_sync_directories_excludes_managed(tmp_path: Path) -> None
⋮----
workspace_root = tmp_path / ".metagit"
proj = workspace_root / "platform"
⋮----
mgr = ProjectManager(workspace_root, _DummyLogger())
unmanaged = mgr.list_unmanaged_sync_directories(
⋮----
hidden_on = mgr.list_unmanaged_sync_directories(
⋮----
hidden_off = mgr.list_unmanaged_sync_directories(
⋮----
"""Dot-directories should not appear in the fuzzy finder when ignore_hidden is true."""
⋮----
project_root = workspace_root / "platform"
⋮----
captured: dict = {}
⋮----
class _DummyFinder
⋮----
def __init__(self, config) -> None
⋮----
def run(self)
⋮----
_ = mgr.select_repo(
names = [item.name for item in captured["config"].items]
````

## File: tests/test_project_manager_select_repo.py
````python
#!/usr/bin/env python
"""
Unit tests for ProjectManager.select_repo behavior.
"""
⋮----
class _DummyLogger
⋮----
def set_level(self, _: str) -> None
⋮----
def warning(self, _: str) -> None
⋮----
def debug(self, _: str) -> None
⋮----
def _build_metagit_config() -> MetagitConfig
⋮----
def test_select_repo_respects_gitignore_and_sets_total_count(tmp_path, monkeypatch) -> None
⋮----
workspace_root = tmp_path / "workspace"
project_root = workspace_root / "proj-one"
⋮----
captured = {}
⋮----
class _DummyFinder
⋮----
def __init__(self, config) -> None
⋮----
def run(self) -> Optional[ProjectPath]
⋮----
manager = ProjectManager(workspace_root, _DummyLogger())
_ = manager.select_repo(_build_metagit_config(), "proj-one", show_preview=True)
⋮----
finder_config = captured["config"]
item_names = [item.name for item in finder_config.items]
⋮----
def test_select_repo_preview_contains_extended_metadata(tmp_path, monkeypatch) -> None
⋮----
target_item = next(
preview = target_item.description
````

## File: tests/test_project_source_models.py
````python
#!/usr/bin/env python
"""
Tests for source sync input model validation.
"""
⋮----
def test_source_spec_accepts_github_org() -> None
⋮----
spec = SourceSpec(provider=SourceProvider.GITHUB, org="metagit-ai")
⋮----
def test_source_spec_rejects_invalid_github_scope() -> None
⋮----
def test_source_spec_accepts_gitlab_group() -> None
⋮----
spec = SourceSpec(provider=SourceProvider.GITLAB, group="my-group/sub-group")
````

## File: tests/test_project_source_sync.py
````python
#!/usr/bin/env python
"""
Tests for source sync planner and applier.
"""
⋮----
def _service() -> SourceSyncService
⋮----
def test_plan_additive_adds_missing_repo() -> None
⋮----
service = _service()
spec = SourceSpec(provider=SourceProvider.GITHUB, org="metagit-ai")
project = WorkspaceProject(name="default", repos=[])
discovered = [
plan = service.plan(spec, project, discovered, SourceSyncMode.ADDITIVE)
⋮----
def test_plan_reconcile_removes_unmatched_provider_managed_repo() -> None
⋮----
project = WorkspaceProject(
⋮----
plan = service.plan(spec, project, discovered, SourceSyncMode.RECONCILE)
⋮----
def test_apply_plan_reconcile_preserves_protected_repo() -> None
⋮----
plan = service.plan(
updated = service.apply_plan(project, plan, SourceSyncMode.RECONCILE)
````

## File: tests/test_utils_fuzzyfinder.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.utils.fuzzyfinder
"""
⋮----
def test_fuzzyfinder_basic()
⋮----
collection = ["apple", "banana", "grape", "apricot"]
results = list(fuzzyfinder.fuzzyfinder("ap", collection))
⋮----
def test_fuzzyfinder_empty()
⋮----
def test_fuzzyfinder_no_match()
⋮----
collection = ["cat", "dog"]
⋮----
def test_fuzzyfinder_app_search_not_capped_by_max_results()
⋮----
config = FuzzyFinderConfig(items=["a", "b", "c"], max_results=1)
app = FuzzyFinderApp(config)
results = app._search("")
````

## File: tests/test_workspace_dedupe.py
````python
#!/usr/bin/env python
"""Tests for workspace repository deduplication helpers."""
⋮----
def test_build_repo_identity_same_url_different_projects() -> None
⋮----
url = "https://github.com/example/org-repo.git"
left = ProjectPath(name="a", url=url)
right = ProjectPath(name="b", url=url + "/")
⋮----
def test_build_repo_identity_branch_suffix_differs() -> None
⋮----
base = ProjectPath(name="svc", url="https://github.com/example/svc.git")
branched = ProjectPath(
⋮----
def test_find_duplicate_identities_reports_existing() -> None
⋮----
shared = ProjectPath(
config = MetagitConfig(
incoming = ProjectPath(
matches = workspace_dedupe.find_duplicate_identities(config, incoming)
⋮----
def test_ensure_symlink_creates_and_repairs(tmp_path: Path) -> None
⋮----
target = tmp_path / "canonical"
⋮----
mount = tmp_path / "project" / "repo"
⋮----
broken = tmp_path / "broken-link"
⋮----
def test_list_orphan_canonical_dirs(tmp_path: Path) -> None
⋮----
dedupe = WorkspaceDedupeConfig(enabled=True, canonical_dir="_canonical")
workspace_root = tmp_path / ".metagit"
canonical_root = workspace_root / "_canonical"
⋮----
references = {"used-key": [("alpha", "repo")]}
orphans = workspace_dedupe.list_orphan_canonical_dirs(
````

## File: web/public/favicon.svg
````xml
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg>
````

## File: web/public/icons.svg
````xml
<svg xmlns="http://www.w3.org/2000/svg">
  <symbol id="bluesky-icon" viewBox="0 0 16 17">
    <g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
    <defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
  </symbol>
  <symbol id="discord-icon" viewBox="0 0 20 19">
    <path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
  </symbol>
  <symbol id="documentation-icon" viewBox="0 0 21 20">
    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
  </symbol>
  <symbol id="github-icon" viewBox="0 0 19 19">
    <path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
  </symbol>
  <symbol id="social-icon" viewBox="0 0 20 20">
    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
  </symbol>
  <symbol id="x-icon" viewBox="0 0 19 19">
    <path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
  </symbol>
</svg>
````

## File: web/src/assets/react.svg
````xml
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
````

## File: web/src/assets/vite.svg
````xml
<svg xmlns="http://www.w3.org/2000/svg" width="77" height="47" fill="none" aria-labelledby="vite-logo-title" viewBox="0 0 77 47"><title id="vite-logo-title">Vite</title><style>.parenthesis{fill:#000}@media (prefers-color-scheme:dark){.parenthesis{fill:#fff}}</style><path fill="#9135ff" d="M40.151 45.71c-.663.844-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.493c-.92 0-1.457-1.04-.92-1.788l7.479-10.471c1.07-1.498 0-3.578-1.842-3.578H15.443c-.92 0-1.456-1.04-.92-1.788l9.696-13.576c.213-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.472c-1.07 1.497 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.087.89 1.83L40.153 45.712z"/><mask id="a" width="48" height="47" x="14" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M40.047 45.71c-.663.843-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.389c-.92 0-1.457-1.04-.92-1.788l7.479-10.472c1.07-1.497 0-3.578-1.842-3.578H15.34c-.92 0-1.456-1.04-.92-1.788l9.696-13.575c.213-.297.556-.474.92-.474H53.93c.92 0 1.456 1.04.92 1.788L47.37 13.03c-1.07 1.498 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.088.89 1.831L40.049 45.712z"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#eee6ff" rx="5.508" ry="14.704" transform="rotate(269.814 20.96 11.29)scale(-1 1)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#eee6ff" rx="10.399" ry="29.851" transform="rotate(89.814 -16.902 -8.275)scale(1 -1)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#8900ff" rx="5.508" ry="30.487" transform="rotate(89.814 -19.197 -7.127)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.928 4.177)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.738 5.52)scale(1 -1)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#eee6ff" rx="14.072" ry="22.078" transform="rotate(93.35 31.245 55.578)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx="14.592" cy="9.743" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(39.51 14.592 9.743)"/></g><g filter="url(#k)"><ellipse cx="61.728" cy="-5.321" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 61.728 -5.32)"/></g><g filter="url(#l)"><ellipse cx="55.618" cy="7.104" fill="#00c2ff" rx="5.971" ry="9.665" transform="rotate(37.892 55.618 7.104)"/></g><g filter="url(#m)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#n)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#o)"><ellipse cx="49.857" cy="30.678" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 49.857 30.678)"/></g><g filter="url(#p)"><ellipse cx="52.623" cy="33.171" fill="#00c2ff" rx="5.971" ry="15.297" transform="rotate(37.892 52.623 33.17)"/></g></g><path d="M6.919 0c-9.198 13.166-9.252 33.575 0 46.789h6.215c-9.25-13.214-9.196-33.623 0-46.789zm62.424 0h-6.215c9.198 13.166 9.252 33.575 0 46.789h6.215c9.25-13.214 9.196-33.623 0-46.789" class="parenthesis"/><defs><filter id="b" width="60.045" height="41.654" x="-5.564" y="16.92" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-40.407" y="-6.762" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-35.435" y="2.801" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-30.84" y="20.8" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-29.307" y="21.949" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="29.961" y="-17.13" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="37.754" y="3.055" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="37.754" y="3.055" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-13.43" y="-22.082" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="34.321" y="-37.644" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="38.847" y="-10.552" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-15.081" y="6.78" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-15.081" y="6.78" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="22.45" y="-1.645" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="32.919" y="11.36" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter></defs></svg>
````

## File: web/src/components/ConfigPreview.module.css
````css
.previewPanel {
⋮----
.header {
⋮----
.title {
⋮----
.controls {
⋮----
.select {
⋮----
.badge {
⋮----
.badgeInvalid {
⋮----
.codeWrap {
⋮----
.code {
⋮----
.state {
⋮----
.errors {
````

## File: web/src/components/ConfigPreview.tsx
````typescript
import { useQuery } from '@tanstack/react-query'
import { useMemo, useState } from 'react'
import type { ConfigOperation, ConfigPreviewStyle } from '../api/client'
import { fetchConfigPreview, type ConfigTarget } from '../pages/configQueries'
import styles from './ConfigPreview.module.css'
⋮----
interface ConfigPreviewProps {
  target: ConfigTarget
  pendingOps: ConfigOperation[]
}
````

## File: web/src/components/FieldEditor.module.css
````css
.panel {
⋮----
.empty {
⋮----
.header {
⋮----
.title {
⋮----
.path {
⋮----
.meta {
⋮----
.badge {
⋮----
.description {
⋮----
.hint {
⋮----
.field {
⋮----
.label {
⋮----
.input,
⋮----
.input:focus,
⋮----
.input:disabled,
⋮----
.actions {
⋮----
.button {
⋮----
.button:hover:not(:disabled) {
⋮----
.button:disabled {
⋮----
.buttonPrimary {
⋮----
.buttonPrimary:hover:not(:disabled) {
⋮----
.errors {
⋮----
.errors li {
⋮----
.errors li:first-child {
⋮----
.errors li:last-child {
⋮----
.status {
````

## File: web/src/components/GraphDiagram.module.css
````css
.wrap {
⋮----
.legend {
⋮----
.legendItem {
⋮----
.legendSwatch {
⋮----
.canvasScroll {
⋮----
.canvas {
⋮----
.nodeProject {
⋮----
.nodeRepo {
⋮----
.nodeLabel {
⋮----
.edgeLabel {
⋮----
.empty {
````

## File: web/src/components/GraphDiagram.tsx
````typescript
import { useMemo } from 'react'
import type { GraphViewEdge, GraphViewNode } from '../api/client'
import styles from './GraphDiagram.module.css'
⋮----
export interface GraphDiagramProps {
  nodes: GraphViewNode[]
  edges: GraphViewEdge[]
  manualEdgeCount: number
  inferredEdgeCount: number
  structureEdgeCount: number
}
⋮----
interface LayoutNode {
  node: GraphViewNode
  x: number
  y: number
  width: number
  height: number
}
⋮----
function edgeStroke(source: GraphViewEdge['source'], type: string): string
⋮----
function computeLayout(nodes: GraphViewNode[]): Map<string, LayoutNode>
⋮----
function center(layout: LayoutNode):
````

## File: web/src/components/Layout.module.css
````css
.shell {
⋮----
.header {
⋮----
.title {
⋮----
.nav {
⋮----
.navLink {
⋮----
.navLink:hover {
⋮----
.navLinkActive {
⋮----
.themeToggle {
⋮----
.themeToggle:hover {
⋮----
.themeIcon {
⋮----
.main {
````

## File: web/src/components/OpsPanel.module.css
````css
.panel {
⋮----
.heading {
⋮----
.section {
⋮----
.sectionTitle {
⋮----
.field {
⋮----
.label {
⋮----
.select {
⋮----
.button {
⋮----
.button:hover:not(:disabled) {
⋮----
.button:disabled {
⋮----
.buttonPrimary {
⋮----
.buttonPrimary:hover:not(:disabled) {
⋮----
.buttonDanger {
⋮----
.buttonDanger:hover:not(:disabled) {
⋮----
.hint {
⋮----
.candidateList {
⋮----
.candidateList li {
⋮----
.candidateList li:last-child {
⋮----
.checkboxRow {
⋮----
.status {
⋮----
.statusError {
⋮----
.divider {
⋮----
.overlay {
⋮----
.modal {
⋮----
.modalTitle {
⋮----
.summaryGrid {
⋮----
.summaryChip {
⋮----
.summaryChip strong {
⋮----
.recommendations {
⋮----
.recommendations li {
⋮----
.severityCritical {
⋮----
.severityWarning {
⋮----
.severityInfo {
⋮----
.repoTable {
⋮----
.repoTable th,
⋮----
.repoTable th {
⋮----
.modalActions {
````

## File: web/src/components/OpsPanel.tsx
````typescript
import { useMemo, useState } from 'react'
import {
  ApiError,
  postHealth,
  postPrune,
  postPrunePreview,
  type PruneCandidate,
  type WorkspaceHealthResult,
  type WorkspaceProjectEntry,
} from '../api/client'
import styles from './OpsPanel.module.css'
⋮----
export interface OpsPanelProps {
  projects: WorkspaceProjectEntry[]
  onWorkspaceRefresh?: () => void
}
⋮----
const runHealth = async () =>
⋮----
const runPrunePreview = async () =>
⋮----
const runPruneExecute = async () =>
⋮----
onClick=
⋮----
````

## File: web/src/components/RepoTable.module.css
````css
.tableWrap {
⋮----
.table {
⋮----
.table th {
⋮----
.table td {
⋮----
.projectRow td {
⋮----
.projectHeader {
⋮----
.expandButton {
⋮----
.expandButton:hover {
⋮----
.projectMeta {
⋮----
.repoName {
⋮----
.path {
⋮----
.badge {
⋮----
.badgeSynced {
⋮----
.badgeMissing {
⋮----
[data-theme='dark'] .badgeMissing {
⋮----
.actions {
⋮----
.button {
⋮----
.button:hover {
⋮----
.buttonPrimary {
⋮----
.buttonPrimary:hover {
⋮----
.empty {
````

## File: web/src/components/RepoTable.tsx
````typescript
import { useMemo, useState } from 'react'
import type { WorkspaceProjectEntry, WorkspaceRepoIndexRow } from '../api/client'
import { repoSelector, type StatusFilter } from '../pages/workspaceQueries'
import styles from './RepoTable.module.css'
⋮----
export interface RepoTableProps {
  projects: WorkspaceProjectEntry[]
  reposIndex: WorkspaceRepoIndexRow[]
  statusFilter: StatusFilter
  search: string
  onSync: (repos: string[], title: string) => void
}
⋮----
interface ProjectGroup {
  project: WorkspaceProjectEntry
  repos: WorkspaceRepoIndexRow[]
}
⋮----
function matchesFilter(
  row: WorkspaceRepoIndexRow,
  statusFilter: StatusFilter,
): boolean
⋮----
function matchesSearch(row: WorkspaceRepoIndexRow, search: string): boolean
⋮----
const toggleProject = (name: string) =>
⋮----
interface ProjectSectionProps {
  project: WorkspaceProjectEntry
  repos: WorkspaceRepoIndexRow[]
  collapsed: boolean
  onToggle: () => void
  onSync: (repos: string[], title: string) => void
  onSyncAll: () => void
}
⋮----
onSync(
````

## File: web/src/components/SyncDialog.module.css
````css
.overlay {
⋮----
.dialog {
⋮----
.title {
⋮----
.subtitle {
⋮----
.field {
⋮----
.label {
⋮----
.select {
⋮----
.checkboxRow {
⋮----
.actions {
⋮----
.button {
⋮----
.button:hover:not(:disabled) {
⋮----
.button:disabled {
⋮----
.buttonPrimary {
⋮----
.buttonPrimary:hover:not(:disabled) {
⋮----
.status {
⋮----
.statusError {
⋮----
.summary {
⋮----
.summary li {
````

## File: web/src/components/SyncDialog.tsx
````typescript
import { useCallback, useEffect, useState } from 'react'
import { useQueryClient } from '@tanstack/react-query'
import {
  ApiError,
  getSyncJob,
  postSync,
  type SyncJobRequest,
} from '../api/client'
import { workspaceQueryKey } from '../pages/workspaceQueries'
import styles from './SyncDialog.module.css'
⋮----
export interface SyncDialogProps {
  open: boolean
  title: string
  repos: string[]
  onClose: () => void
}
⋮----
type Phase = 'idle' | 'running' | 'done' | 'error'
⋮----
const poll = async () =>
⋮----
const handleSubmit = async () =>
⋮----

⋮----
setMode(event.target.value as SyncJobRequest['mode'])
````

## File: web/src/pages/graphQueries.ts
````typescript
import { getWorkspaceGraph, type WorkspaceGraphView } from '../api/client'
⋮----
export const graphQueryKey = (includeInferred: boolean, includeStructure: boolean)
⋮----
export async function fetchWorkspaceGraph(
  includeInferred: boolean,
  includeStructure: boolean,
): Promise<WorkspaceGraphView>
````

## File: web/src/pages/workspaceQueries.ts
````typescript
import { getWorkspace, type WorkspaceData } from '../api/client'
⋮----
export async function fetchWorkspace(): Promise<WorkspaceData>
⋮----
export type StatusFilter = 'all' | 'synced' | 'missing'
⋮----
export function repoSelector(projectName: string, repoName: string): string
````

## File: web/src/theme/ThemeProvider.tsx
````typescript
import { useEffect, type ReactNode } from 'react'
import { useThemeStore } from './useThemeStore'
⋮----
interface ThemeProviderProps {
  children: ReactNode
}
⋮----
export default function ThemeProvider(
⋮----
const onChange = ()
````

## File: web/src/theme/useThemeStore.ts
````typescript
import { create } from 'zustand'
⋮----
export type ThemeMode = 'light' | 'dark' | 'system'
⋮----
function readStoredMode(): ThemeMode
⋮----
function resolveTheme(mode: ThemeMode): 'light' | 'dark'
⋮----
function applyTheme(mode: ThemeMode): void
⋮----
interface ThemeState {
  mode: ThemeMode
  resolved: 'light' | 'dark'
  setMode: (mode: ThemeMode) => void
  toggleResolved: () => void
  init: () => void
  syncSystemTheme: () => void
}
````

## File: web/.gitignore
````
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
````

## File: web/eslint.config.js
````javascript

````

## File: web/index.html
````html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>web</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
````

## File: web/package.json
````json
{
  "name": "web",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "@tanstack/react-query": "^5.100.11",
    "react": "^19.2.6",
    "react-dom": "^19.2.6",
    "react-router-dom": "^7.15.1",
    "zustand": "^5.0.13"
  },
  "devDependencies": {
    "@eslint/js": "^10.0.1",
    "@types/node": "^24.12.3",
    "@types/react": "^19.2.14",
    "@types/react-dom": "^19.2.3",
    "@vitejs/plugin-react": "^6.0.1",
    "eslint": "^10.3.0",
    "eslint-plugin-react-hooks": "^7.1.1",
    "eslint-plugin-react-refresh": "^0.5.2",
    "globals": "^17.6.0",
    "typescript": "~6.0.2",
    "typescript-eslint": "^8.59.2",
    "vite": "^8.0.12"
  }
}
````

## File: web/README.md
````markdown
# React + TypeScript + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)

## React Compiler

The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).

## Expanding the ESLint configuration

If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:

```js
export default defineConfig([
  globalIgnores(['dist']),
  {
    files: ['**/*.{ts,tsx}'],
    extends: [
      // Other configs...

      // Remove tseslint.configs.recommended and replace with this
      tseslint.configs.recommendedTypeChecked,
      // Alternatively, use this for stricter rules
      tseslint.configs.strictTypeChecked,
      // Optionally, add this for stylistic rules
      tseslint.configs.stylisticTypeChecked,

      // Other configs...
    ],
    languageOptions: {
      parserOptions: {
        project: ['./tsconfig.node.json', './tsconfig.app.json'],
        tsconfigRootDir: import.meta.dirname,
      },
      // other options...
    },
  },
])
```

You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:

```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'

export default defineConfig([
  globalIgnores(['dist']),
  {
    files: ['**/*.{ts,tsx}'],
    extends: [
      // Other configs...
      // Enable lint rules for React
      reactX.configs['recommended-typescript'],
      // Enable lint rules for React DOM
      reactDom.configs.recommended,
    ],
    languageOptions: {
      parserOptions: {
        project: ['./tsconfig.node.json', './tsconfig.app.json'],
        tsconfigRootDir: import.meta.dirname,
      },
      // other options...
    },
  },
])
```
````

## File: web/tsconfig.app.json
````json
{
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
    "target": "es2023",
    "lib": ["ES2023", "DOM"],
    "module": "esnext",
    "types": ["vite/client"],
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "verbatimModuleSyntax": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "erasableSyntaxOnly": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"]
}
````

## File: web/tsconfig.json
````json
{
  "files": [],
  "references": [
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" }
  ]
}
````

## File: web/tsconfig.node.json
````json
{
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
    "target": "es2023",
    "lib": ["ES2023"],
    "module": "esnext",
    "types": ["node"],
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "verbatimModuleSyntax": true,
    "moduleDetection": "force",
    "noEmit": true,

    /* Linting */
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "erasableSyntaxOnly": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["vite.config.ts"]
}
````

## File: web/vite.config.ts
````typescript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
⋮----
// https://vite.dev/config/
````

## File: .env.example
````
# Git Provider API Tokens
GITHUB_TOKEN="ghp_your_github_token_here"
GITHUB_URL="https://github.company.com/api/v3"
GITLAB_TOKEN="glpat_your_gitlab_token_here"
GITLAB_URL="https://gitlab.company.com/api/v4"

# Agent-optimized mode: no fuzzy finder, prompts, or editor (true/false)
# METAGIT_AGENT_MODE=false

METAGIT_LLM="openrouter"
METAGIT_LLM_MODEL="qwen/qwen2.5-vl-72b-instruct:free"
METAGIT_LLM_TOKEN=""

# OPENAI_API_KEY=""
# OPENROUTER_API_KEY=""
# OPENAI_MODEL_NAME="openrouter/nousresearch/deephermes-3-mistral-24b-preview:free"
# OPENAI_API_BASE="https://openrouter.ai/api/v1"
# OPENAI_PROXY_MODELS="openrouter/nousresearch/deephermes-3-mistral-24b-preview:free"

# GitLab Personal Access Token for repository metrics
#GITLAB_TOKEN="glpat_your_gitlab_token_here"
# Custom API URLs (for self-hosted instances)
#GITHUB_URL="https://github.company.com/api/v3"
#GITLAB_URL="https://gitlab.company.com/api/v4"
````

## File: .gitsecrets.lock
````
{
  "version": "1.0",
  "secrets": {},
  "secretfile": {
    "filename": "Secretfile.yml",
    "hash": "34ce5cc41aaab6f6832b541070a1aa8405b5f2262356b2e20caa49ba00c61209",
    "synced_at": "2026-05-06T16:59:13.263343+00:00",
    "var_files": [],
    "variables_hash": "cd258ea8b5adce1d02de944cab545b051be7a2d1da5f593790747636fa5f37da",
    "sync_identity": {
      "client": "cli",
      "secretzero_version": "0.13.3",
      "os_user": "zacharyloeber",
      "os_uid": 501,
      "os_euid": 501,
      "hostname": "Zacharys-MBP.home.loeber.live",
      "host_fqdn": "Zacharys-MBP.home.loeber.live",
      "platform": "macOS-26.3.1-arm64-arm-64bit",
      "git_user_name": "Zachary Loeber",
      "git_user_email": "zloeber@gmail.com",
      "git_commit_sha": "cafd6da"
    },
    "manifest_spec_version": "1"
  },
  "metadata": {}
}
````

## File: .python-version
````
3.12
````

## File: configure.sh
````bash
#!/usr/bin/env bash

# install mise if not already installed to local path
if ! command -v ~/.local/bin/mise &>/dev/null; then
    echo "mise is not installed. Would you like to install it now? (y/N)"
    read -r response
    if [[ "$response" =~ ^[Yy]$ ]]; then
        curl https://mise.run | sh
        echo ""
        echo "Load mise into your path (add to your ~/.zshrc profile to always be available): "
        echo '   echo 'eval "$(~/.local/bin/mise activate zsh)"' >> ~/.zshrc'
        # Load mise into the current script shell instead of zsh
        eval "$(~/.local/bin/mise activate bash)"
    else
        echo "Skipping mise installation."
        exit 1
    fi
else
    eval "$(~/.local/bin/mise activate bash)"
fi

mise install -y
uv sync
````

## File: LICENSE.md
````markdown
MIT License

Copyright (c) 2025 Zachary Loeber

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
````

## File: Secretfile.yml
````yaml
variables:
  github_org: metagit-ai
  github_repo: metagit-cli

providers:
  github:
    kind: github
    auth:
      kind: token
      config:
        token: ${METAGIT_GITHUB_TOKEN}

secrets:
  # ── Release token with write access to contents & PRs ──────────────
  - name: release_token
    kind: static
    config:
      default: ${PAT_TOKEN}
    targets:
      - provider: github
        kind: github_secret
        config:
          owner: ${github_org}
          repo: ${github_repo}
          secret_name: PAT_TOKEN

metadata:
  project: metagit-cli
  owner: metagit-ai
  description: PyPI API tokens for metagit-cli package publishing
  environments:
  - testpypi
  - production
````

## File: .cursor/skills/metagit-cli/SKILL.md
````markdown
---
name: metagit-cli
description: CLI-only shortcuts for metagit agents — workspace catalog, discovery, prompts, sync, layout, and config. Use instead of MCP or HTTP API when operating from a shell or agent_mode session.
---

# Metagit CLI (agent shortcuts)

Use this skill when an agent should drive metagit **only through the `metagit` command**. Do not call MCP tools or `metagit api` from workflows covered here unless the user explicitly asks.

Set non-interactive defaults when automating:

```bash
export METAGIT_AGENT_MODE=true
```

Global flags (most commands):

- `-c path/to/metagit.config.yaml` — app config (default `metagit.config.yaml`)
- Workspace manifest: `--definition` / `-c` on catalog commands (default `.metagit.yml`)

---

## Manifest editing fast map (`.metagit.yml`)

Use this table first when changing a workspace manifest from the CLI. Prefer **catalog commands** for projects/repos; use **`config patch`** for everything else in the schema (documentation, graph, dedupe overrides, nested fields).

| Task | Command |
|------|---------|
| **Inspect** manifest on disk | `metagit config show -c .metagit.yml` |
| **Inspect** normalized model | `metagit config show -c .metagit.yml --normalized` |
| **Inspect** as JSON (agents) | `metagit config show -c .metagit.yml --json` |
| **Browse** fields / paths | `metagit config tree -c .metagit.yml` or `… --json` |
| **Validate** after edits | `metagit config validate -c .metagit.yml` |
| **Dry-run** schema change | `metagit config preview -c .metagit.yml --file ops.json` |
| **Apply** schema change | `metagit config patch -c .metagit.yml --file ops.json --save` |
| **Set one field** | `metagit config patch -c .metagit.yml --op set --path <path> --value <v> --save` |
| **Enable** optional block | `metagit config patch … --op enable --path <path> --save` |
| **Add list item** | `metagit config patch … --op append --path <list.path> --save` then `set` on `[index].field` |
| **Remove list item** | `metagit config patch … --op remove --path <list.path>[index] --save` |

### Catalog shortcuts (projects & repos)

| Task | Command |
|------|---------|
| List projects | `metagit workspace project list -c .metagit.yml --json` |
| List repos (all / one project) | `metagit workspace repo list -c .metagit.yml --json` / `… --project <p> --json` |
| Add project | `metagit workspace project add -c .metagit.yml --name <p> --json` |
| Remove project | `metagit workspace project remove -c .metagit.yml --name <p> --json` |
| Rename project (dry-run) | `metagit workspace project rename -c .metagit.yml --name <old> --new-name <new> --dry-run --json` |
| Add repo | `metagit workspace repo add -c .metagit.yml --project <p> --name <r> --url <url> --json` |
| Remove repo (manifest only) | `metagit workspace repo remove -c .metagit.yml --project <p> --name <r> --json` |
| Rename / move repo (dry-run) | `metagit workspace repo rename …` / `metagit workspace repo move … --dry-run --json` |
| Search before adding | `metagit search "<name>" -c .metagit.yml --json` |

Active project context (optional): `metagit project repo add --name <r> --url <url>` after `metagit project select`.

### Schema patch examples (`ops.json`)

Paths use dot/bracket notation (same as web Config Studio). `--value` accepts JSON for objects.

```json
{
  "operations": [
    { "op": "set", "path": "name", "value": "my-workspace" },
    { "op": "enable", "path": "documentation" },
    { "op": "append", "path": "documentation" },
    { "op": "set", "path": "documentation[0]", "value": { "kind": "markdown", "path": "AGENTS.md" } },
    { "op": "enable", "path": "graph" },
    { "op": "append", "path": "graph.relationships" },
    {
      "op": "set",
      "path": "graph.relationships[0]",
      "value": {
        "id": "api-depends-infra",
        "from": { "project": "platform", "repo": "api" },
        "to": { "project": "infra", "repo": "terraform" },
        "type": "depends_on"
      }
    },
    { "op": "set", "path": "workspace.projects[0].dedupe.enabled", "value": false }
  ]
}
```

```bash
metagit config preview -c .metagit.yml --file ops.json
metagit config patch -c .metagit.yml --file ops.json --save
metagit config validate -c .metagit.yml
```

### App config vs manifest

| File | Scope | Edit via |
|------|-------|----------|
| `.metagit.yml` | Workspace manifest (projects, repos, docs, graph) | `metagit config …` + `metagit workspace …` |
| `metagit.config.yaml` | Tooling (paths, dedupe default, providers, profiles) | `metagit appconfig …` |

```bash
metagit appconfig show --format minimal-yaml
metagit appconfig patch --op set --path workspace.dedupe.enabled --value false --save
metagit config providers --show
```

### After every manifest edit

1. `metagit config validate -c .metagit.yml`
2. `metagit workspace list -c .metagit.yml --json` (sanity-check catalog)
3. If repos changed on disk: `metagit project sync` or `metagit project sync --hydrate`

### Export manual graph to GitNexus (Cypher)

| Task | Command |
|------|---------|
| Full export bundle (JSON) | `metagit config graph export -c .metagit.yml --json` |
| Raw Cypher script | `metagit config graph export -c .metagit.yml --format cypher -o graph.cypher` |
| MCP tool_calls only | `metagit config graph export -c .metagit.yml --format tool-calls` |
| Manual edges only | `metagit config graph export -c .metagit.yml --manual-only --format tool-calls` |
| MCP from agent | `metagit_export_workspace_graph_cypher` |

Ingest workflow: run `schema_statements` once via `gitnexus_cypher`, then each statement in `tool_calls` (or pipe `--format cypher`). Overlay tables: `MetagitEntity`, `MetagitLink`.

---

## Prompt commands (all kinds)

List built-in prompt kinds:

```bash
metagit prompt list
metagit prompt list --json
```

Emit prompts (`--text-only` for paste into agent context; `--json` for structured output; `--no-instructions` to omit manifest layers):

| Scope | Command | Default kind |
|-------|---------|--------------|
| Workspace | `metagit prompt workspace -c <definition> -k <kind>` | `instructions` |
| Project | `metagit prompt project -p <project> -c <definition> -k <kind>` | `instructions` |
| Repo | `metagit prompt repo -p <project> -n <repo> -c <definition> -k <kind>` | `instructions` |

### Prompt kinds by scope

| Kind | Workspace | Project | Repo | Purpose |
|------|:---------:|:-------:|:----:|---------|
| `instructions` | yes | yes | yes | Composed `agent_instructions` from manifest layers |
| `session-start` | yes | — | — | Session bootstrap checklist |
| `catalog-edit` | yes | yes | — | Search-before-create; catalog registration |
| `health-preflight` | yes | yes | — | Pre-work workspace/repo status pass |
| `sync-safe` | yes | yes | yes | Guarded sync rules |
| `subagent-handoff` | — | yes | yes | Delegate single-repo work |
| `layout-change` | yes | yes | yes | Rename/move dry-run workflow |
| `repo-enrich` | — | — | yes | **Discover + merge** workspace repo entry |

### Prompt shortcuts (copy-paste)

```bash
# Session bootstrap
metagit prompt workspace -k session-start --text-only -c .metagit.yml

# Composed instructions at each level
metagit prompt workspace -k instructions --text-only -c .metagit.yml
metagit prompt project -p default -k instructions --text-only -c .metagit.yml
metagit prompt repo -p default -n my-api -k instructions --text-only -c .metagit.yml

# Repo catalog enrichment (detect + merge manifest entry)
metagit prompt repo -p default -n my-api -k repo-enrich --text-only -c .metagit.yml

# Catalog registration discipline
metagit prompt workspace -k catalog-edit --text-only -c .metagit.yml

# Safe sync reminder
metagit prompt repo -p default -n my-api -k sync-safe --text-only -c .metagit.yml

# Subagent handoff
metagit prompt repo -p default -n my-api -k subagent-handoff --text-only -c .metagit.yml
```

---

## Repo enrich workflow (`repo-enrich`)

Run the prompt, then execute its steps:

```bash
metagit prompt repo -p <project> -n <repo> -k repo-enrich --text-only -c .metagit.yml
```

Typical discovery chain on the checkout:

```bash
cd "$(metagit search '<repo>' -c .metagit.yml --path-only)"
metagit detect repository -p . -o json
metagit detect repo -p . -o yaml
metagit detect repo_map -p . -o json
```

Provider metadata (dry-run):

```bash
metagit project source sync --provider github --org <org> --mode discover --no-apply
```

After merging fields into `workspace.projects[].repos[]`:

```bash
metagit config validate -c .metagit.yml
metagit workspace repo list --project <project> --json
```

---

## Workspace and catalog

Per-project dedupe override in `.metagit.yml` (overrides `workspace.dedupe.enabled` in `metagit.config.yaml` for that project only):

```yaml
workspace:
  projects:
    - name: local
      dedupe:
        enabled: false
      repos: []
```

```bash
metagit appconfig show --format json
metagit config info -c .metagit.yml
metagit config show -c .metagit.yml
metagit config validate -c .metagit.yml

metagit workspace list -c .metagit.yml --json
metagit workspace project list -c .metagit.yml --json
metagit workspace repo list -c .metagit.yml --json
metagit workspace repo list -c .metagit.yml --project <name> --json

metagit workspace project add --name <name> --json
metagit workspace repo add --project <name> --name <repo> --url <url> --json
metagit workspace project remove --name <name> --json
metagit workspace repo remove --project <name> --name <repo> --json
```

Search managed repos (always before creating entries):

```bash
metagit search "<query>" -c .metagit.yml --json
metagit search "<query>" -c .metagit.yml --path-only
metagit search "<query>" -c .metagit.yml --tag tier=1 --project <name>
```

---

## Project operations

```bash
metagit project list --config .metagit.yml --all --json
metagit project add --name <name> --json
metagit project remove --name <name> --json
metagit project rename --name <old> --new-name <new> --dry-run --json
metagit project select
metagit project sync
metagit project sync --hydrate   # symlink mounts → full directory copies (per-file progress)

metagit project repo list --json
metagit project repo add --project <name> --name <repo> --url <url>
metagit project repo remove --name <repo> --json
metagit project repo rename --name <old> --new-name <new> --dry-run --json
metagit project repo move --name <repo> --to-project <other> --dry-run --json
metagit project repo prune --project <name> --dry-run

metagit project source sync --provider github --org <org> --mode discover --no-apply
metagit project source sync --provider github --org <org> --mode additive --apply
```

Layout (manifest + disk; always dry-run first):

```bash
metagit workspace project rename --name <old> --new-name <new> --dry-run --json
metagit workspace repo rename --project <p> --name <old> --new-name <new> --dry-run --json
metagit workspace repo move --project <p> --name <repo> --to-project <other> --dry-run --json
```

---

## Discovery and local metadata

```bash
metagit detect project -p <path> -o yaml
metagit detect repo -p <path> -o yaml
metagit detect repo_map -p <path> -o json
metagit detect repository -p <path> -o json
metagit detect repository -p <path> -o metagit
# --save only with operator approval (blocked in agent_mode)
```

Bootstrap new trees:

```bash
metagit init --kind application
metagit init --kind umbrella --template hermes-orchestrator
```

---

## Selection and scope

```bash
metagit workspace select --project <name>
metagit project select
metagit project repo select
```

---

## Config and appconfig (reference)

See **Manifest editing fast map** above for day-to-day manifest work. Additional commands:

```bash
metagit config info -c .metagit.yml
metagit config example
metagit config schema
metagit appconfig validate
metagit appconfig get --name config.workspace.path
metagit appconfig tree --json
metagit appconfig preview --file ops.json
metagit appconfig patch --file ops.json --save
metagit config providers --show
```

---

## Records, skills, version

```bash
metagit record search "<query>"
metagit skills list
metagit skills show metagit-cli
metagit skills install --skill metagit-cli
metagit version
metagit info
```

---

## Agent habits

1. **Search before create** — `metagit search` then catalog add.
2. **Validate after manifest edits** — `metagit config validate`.
3. **Emit prompts instead of rewriting playbooks** — `metagit prompt … --text-only`.
4. **Enrich stale repo entries** — `metagit prompt repo … -k repo-enrich` then detect + merge.
5. **Dry-run layout** — always `--dry-run --json` before apply.
6. **Prefer `METAGIT_AGENT_MODE=true`** in CI and agent loops to skip fuzzy finder and confirm dialogs.

## Related bundled skills

Use topic skills when you need deeper playbooks (some mention MCP): `metagit-projects`, `metagit-workspace-scope`, `metagit-workspace-sync`, `metagit-config-refresh`. This skill is the **CLI-only** index and prompt reference.
````

## File: .cursor/skills/metagit-control-center/scripts/control-cycle.zsh
````zsh
#!/usr/bin/env zsh
set -euo pipefail

ROOT="${1:-$PWD}"
QUERY="${2:-}"
PRESET="${3:-infra}"

"$(dirname "$0")/../../metagit-gating/scripts/gate-status.zsh" "$ROOT"

if [[ -n "$QUERY" ]]; then
  "$(dirname "$0")/../../metagit-upstream-scan/scripts/upstream-scan.zsh" "$ROOT" "$QUERY" "$PRESET" 20
else
  echo "status=ok\tmessage=no-query-provided"
fi
````

## File: .cursor/skills/metagit-control-center/SKILL.md
````markdown
---
name: metagit-control-center
description: Use when running metagit as an MCP control center for multi-repo awareness, guarded sync, and operational knowledge across ongoing agent tasks.
---

# Metagit Control Center Skill

Use this skill when an agent should actively coordinate repository context and task execution across a workspace.

## Purpose

Provide a repeatable control-center workflow where Metagit MCP guides awareness, synchronization, and operational continuity over multiple related repositories.

## Local Script Wrapper (Use First)

Use this token-efficient wrapper for control-center cycles:
- `./scripts/control-cycle.zsh [root_path] ["query"] [preset]`

Wrapper behavior:
- runs gating status first
- optionally runs upstream discovery for blocker queries
- emits compact, machine-readable lines

## Core Workflows

### 1) Session Initialization
- Validate active workspace gate.
- Read `metagit://workspace/config` and `metagit://workspace/repos/status`.
- Call `metagit_project_context_switch` when the objective is tied to a workspace project.
- Run `metagit_workspace_health_check` or read `metagit://workspace/health` for maintenance signals.
- Identify stale repos and unresolved blockers from prior activity.

### 2) Active Task Support
- For each coding objective, map impacted repos.
- Use workspace search and upstream hints before broad exploration.
- Sync only repos that are required by the active objective.

### 3) Guarded Synchronization
- Default to `fetch` for visibility.
- Use `pull` or `clone` only with explicit permission and rationale.
- Track sync outcomes in operations log resource.

### 4) Operational Memory
- Before switching projects: `metagit_session_update` (notes + recent repos), optional `metagit_workspace_state_snapshot`.
- After returning: `metagit_workspace_state_restore` when a snapshot was taken (metadata only; git tree is unchanged).

Maintain bounded local records of:
- sync actions
- issue signatures searched
- candidate upstream repos identified
- unresolved dependencies and follow-ups

## Decision Guidelines

- Use metagit search first when blocker appears external to current repo.
- Prefer deterministic evidence over speculative jumps.
- Keep operations minimal and auditable.

## Output Contract

For each control-center cycle, provide:
- current objective
- repositories examined
- actions taken (or intentionally deferred)
- next recommended step

## Safety Rules

- Never mutate repositories without explicit authorization.
- Never broaden scope beyond configured workspace boundaries.
- Always preserve a clear audit trail of control actions.
````

## File: .cursor/skills/metagit-gitnexus/SKILL.md
````markdown
---
name: metagit-gitnexus
description: Run gitnexus analysis for a target workspace and selected project repositories before graph-dependent tasks. Use when index staleness is detected or cross-repo graph results are needed.
---

# Running GitNexus Analysis

Use this skill whenever GitNexus context is stale or missing for target repositories.

## Local Wrapper (Use First)

- `zsh ./skills/metagit-gitnexus/scripts/analyze-targets.zsh <workspace_root> [project_name]`

## Workflow

1. Analyze the current repository where the command is run.
2. Resolve target project repositories from `.metagit.yml`.
3. Run `npx gitnexus analyze` in each local repository path found.
4. Report per-repository success/failure and next actions.

## Manual workspace graph (`.metagit.yml` → Cypher)

Export manifest `graph.relationships` for GitNexus overlay ingest:

```bash
metagit config graph export -c .metagit.yml --format tool-calls
# or MCP: metagit_export_workspace_graph_cypher
```

Run returned `gitnexus_cypher` tool calls against the umbrella repo index (`--gitnexus-repo` when names differ). Schema DDL (`MetagitEntity` / `MetagitLink`) runs once per index.

## Output Contract

Return:
- analyzed repositories
- failures and reasons
- whether graph queries are safe to run now

## Safety

- Skip repositories that do not exist locally.
- Do not mutate repo content; analysis should be read-only indexing.
````

## File: .cursor/skills/metagit-repo-impact/SKILL.md
````markdown
---
name: metagit-repo-impact
description: Plan repository change impact before edits by combining metagit workspace context and graph-based dependency analysis. Use when a change may affect multiple repositories.
---

# Planning Repo Impact

Use this skill before risky or cross-repo modifications.

## Workflow

1. Identify target symbols/files for change.
2. Use metagit workspace context to bound repository scope.
3. Use graph impact tooling to estimate blast radius.
4. Produce a change plan with test and rollback focus.

## Commands

- `metagit workspace select --project <name>`
- MCP `metagit_cross_project_dependencies` with `source_project` and `dependency_types` before large cross-project edits
- MCP `metagit_project_context_switch` to bound scope to one workspace project
- `npx gitnexus analyze` on affected repos when `graph_status` is `stale` or `missing`
- `npx gitnexus query` / `gitnexus impact` for symbol-level analysis after indexes are fresh

## Output Contract

Return:
- impacted repositories and interfaces
- highest-risk change points
- minimum validation plan

## Safety

- Do not execute mutating steps in this planning phase.
````

## File: .cursor/skills/metagit-workspace-scope/SKILL.md
````markdown
---
name: metagit-workspace-scope
description: Discover active metagit workspace scope, project boundaries, and repository status. Use when an agent starts work in a multi-repo workspace and needs fast, scoped context before editing.
---

# Discovering Workspace Scope

Use this skill at session start for workspace-aware tasks.

## Workflow

1. Run workspace gate and status checks first.
2. Read configured projects and repository status from metagit resources/tools.
3. Build a compact map of active project names, repo names, and sync state.
4. Report a bounded scope for the current objective.

## Commands

- `zsh ./skills/metagit-gating/scripts/gate-status.zsh [root_path]`
- `metagit workspace select --project <name>` (interactive repo picker)
- MCP `metagit_project_context_switch` with `project_name` (structured context + session restore)
- MCP `metagit_workspace_state_snapshot` before leaving a project for a long switch

## Output Contract

Return:
- active/inactive workspace state and reason
- candidate project to operate on
- top repositories to inspect first

## Safety

- Keep operations read-only in this step.
- Do not sync or mutate repositories without explicit request.
````

## File: .cursor/skills/metagit-workspace-sync/SKILL.md
````markdown
---
name: metagit-workspace-sync
description: Sync workspace repositories safely using metagit with scoped fetch, pull, or clone actions. Use when repository content must be refreshed for implementation.
---

# Syncing Workspace Repositories

Use this skill when repository state must be updated.

## Workflow

1. Confirm active workspace and target project.
2. Start with read-only status and fetch where possible.
3. Use pull/clone only when required for the current objective.
4. Summarize which repositories changed and what remains stale.

## Commands

- `metagit project sync --project <name>`
- MCP `metagit_workspace_sync` for batch fetch/pull/clone (`repos`, `only_if`, `dry_run`, `max_parallel`)
- MCP `metagit_repo_sync` for a single repository (requires `allow_mutation` for pull/clone)
- `zsh ./skills/metagit-control-center/scripts/control-cycle.zsh [root_path] ["query"] [preset]`

## Output Contract

Return:
- repositories synced
- sync mode used (fetch/pull/clone)
- failures and retry guidance

## Safety

- Limit sync to repos defined in `.metagit.yml`.
- Avoid broad synchronization if only a subset is needed.
````

## File: .github/workflows/semantic-release.yaml
````yaml
name: Semantic Release
run-name: Semantic Release - ${{ github.ref_name }}

on:
  push:
    branches:
      - main
  workflow_dispatch:

env:
  PROJECT_NAME: metagit-cli

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: false

permissions:
  contents: write

jobs:
  semantic-release:
    name: Generate Semantic Release Tag
    runs-on: ubuntu-latest
    environment:
      name: production

    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0
          token: ${{ secrets.PAT_TOKEN }}

      - name: Set up Git
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"

      - name: Get previous tag
        id: prev_tag
        run: |
          PREV_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0")
          echo "tag=$PREV_TAG" >> $GITHUB_OUTPUT

      - name: Parse commits and determine version bump
        id: version
        run: |
          PREV_TAG=${{ steps.prev_tag.outputs.tag }}
          PREV_VERSION="${PREV_TAG#v}"

          if git rev-parse "${{ steps.prev_tag.outputs.tag }}" >/dev/null 2>&1; then
            COMMITS=$(git log "${{ steps.prev_tag.outputs.tag }}..HEAD" --pretty=format:%s)
          else
            COMMITS=$(git log --pretty=format:%s)
          fi

          echo "Commits since last release:"
          echo "$COMMITS"

          MAJOR_BUMP=0
          MINOR_BUMP=0
          PATCH_BUMP=0

          echo "$COMMITS" | while IFS= read -r line; do
            if [[ "$line" =~ ^BREAKING\ CHANGE|^feat\!: ]]; then
              MAJOR_BUMP=1
            elif [[ "$line" =~ ^feat: ]]; then
              MINOR_BUMP=1
            elif [[ "$line" =~ ^fix:|^refactor:|^perf: ]]; then
              PATCH_BUMP=1
            fi
          done

          if [[ "$COMMITS" =~ BREAKING\ CHANGE|feat\!: ]]; then
            MAJOR_BUMP=1
          elif [[ "$COMMITS" =~ feat: ]]; then
            MINOR_BUMP=1
          elif [[ "$COMMITS" =~ fix:|refactor:|perf: ]]; then
            PATCH_BUMP=1
          fi

          IFS='.' read -r MAJOR MINOR PATCH <<< "$PREV_VERSION"
          MAJOR=${MAJOR:-0}
          MINOR=${MINOR:-0}
          PATCH=${PATCH:-0}

          if [[ $MAJOR_BUMP -eq 1 ]]; then
            MAJOR=$((MAJOR + 1))
            MINOR=0
            PATCH=0
          elif [[ $MINOR_BUMP -eq 1 ]]; then
            MINOR=$((MINOR + 1))
            PATCH=0
          elif [[ $PATCH_BUMP -eq 1 ]] || git rev-parse "${{ steps.prev_tag.outputs.tag }}" >/dev/null 2>&1 && [[ "$PREV_TAG" != "0.0.0" ]]; then
            PATCH=$((PATCH + 1))
          else
            if [[ "$PREV_TAG" == "0.0.0" ]] && [[ $PATCH_BUMP -eq 0 ]] && [[ $MINOR_BUMP -eq 0 ]] && [[ $MAJOR_BUMP -eq 0 ]]; then
              PATCH=1
            fi
          fi

          NEW_VERSION="$MAJOR.$MINOR.$PATCH"
          echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
          echo "New version: $NEW_VERSION"

      - name: Check if tag already exists
        id: check_tag
        run: |
          if git rev-parse "refs/tags/${{ steps.version.outputs.new_version }}" >/dev/null 2>&1; then
            echo "tag_exists=true" >> $GITHUB_OUTPUT
            echo "Tag ${{ steps.version.outputs.new_version }} already exists"
          else
            echo "tag_exists=false" >> $GITHUB_OUTPUT
          fi

      - name: Create and push tag
        if: steps.check_tag.outputs.tag_exists == 'false'
        run: |
          git tag -a "${{ steps.version.outputs.new_version }}" -m "Release ${{ steps.version.outputs.new_version }}"
          git push origin "${{ steps.version.outputs.new_version }}"
          echo "Created and pushed tag: ${{ steps.version.outputs.new_version }}"

      - name: Create Release Notes
        if: steps.check_tag.outputs.tag_exists == 'false'
        id: release_notes
        run: |
          PREV_TAG=${{ steps.prev_tag.outputs.tag }}
          NEW_VERSION="${{ steps.version.outputs.new_version }}"

          if git rev-parse "$PREV_TAG" >/dev/null 2>&1; then
            CHANGELOG=$(git log "$PREV_TAG..HEAD" --pretty=format:"- %s (%h)")
          else
            CHANGELOG=$(git log --pretty=format:"- %s (%h)")
          fi

          echo "changelog<<EOF" >> $GITHUB_OUTPUT
          echo "$CHANGELOG" >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT

      - name: Create GitHub Release
        if: steps.check_tag.outputs.tag_exists == 'false'
        uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
        with:
          tag_name: ${{ steps.version.outputs.new_version }}
          name: Release ${{ steps.version.outputs.new_version }}
          body: |
            ## What's Changed

            ${{ steps.release_notes.outputs.changelog }}

            **Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ steps.prev_tag.outputs.tag }}...${{ steps.version.outputs.new_version }}
          draft: false
          prerelease: false

      - name: Log Output
        run: |
          echo "Previous tag: ${{ steps.prev_tag.outputs.tag }}"
          echo "New version: ${{ steps.version.outputs.new_version }}"
          echo "Tag exists: ${{ steps.check_tag.outputs.tag_exists }}"
````

## File: .github/workflows/test.yaml
````yaml
name: Test

on: [push, pull_request]

concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

env:
  PROJECT_PATH: src/metagit

permissions:
  contents: read

jobs:
  test:
    name: Test - ${{ matrix.os }} - ${{ matrix.python-version }} - ${{ github.ref_name }}
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        python-version: ["3.13"]
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
        with:
          python-version: ${{ matrix.python-version }}
          cache: pip
          cache-dependency-path: setup.py

      - name: Set up environment
        run: |
          pip install uv
          uv venv
          uv pip install -e ".[test]"

      - name: Unit Tests
        run: |
          uv run pytest --maxfail=1 --disable-warnings -v tests

      - name: Linting
        if: matrix.os != 'windows-latest'
        run: |
          uv run ruff check ${{ env.PROJECT_PATH }}
          uv run ruff format --check ${{ env.PROJECT_PATH }}
      # - name: Type Checking
      #   if: matrix.os != 'windows-latest'
      #   run: |
      #     mypy metagit
      - name: Build Check
        run: |
          uv sync
          uv build
````

## File: .mex/context/architecture.md
````markdown
---
name: architecture
description: How the major pieces of this project connect and flow. Load when working on system design, integrations, or understanding how components interact.
triggers:
  - "architecture"
  - "system design"
  - "how does X connect to Y"
  - "integration"
  - "flow"
edges:
  - target: context/stack.md
    condition: when specific technology details are needed
  - target: context/decisions.md
    condition: when understanding why the architecture is structured this way
  - target: context/conventions.md
    condition: when implementing changes across CLI, core services, and tests
  - target: context/mcp-runtime.md
    condition: when task scope includes MCP tools, resources, stdio runtime, or sampling
last_updated: 2026-05-12
---

# Architecture

## System Overview
User runs `metagit` CLI command via `src/metagit/cli/main.py` and command modules under `src/metagit/cli/commands/`.
CLI command loads app config (`metagit.config.yaml`) through `metagit.core.appconfig` and initializes `UnifiedLogger`.
Command handlers call core managers/services in `src/metagit/core/*` (config manager, detection manager, workspace/project helpers, record managers).
For project metadata, `.metagit.yml` is loaded/validated via `MetagitConfigManager` and Pydantic models in `metagit.core.config.models`.
For MCP mode, `metagit mcp serve` enters `MetagitMcpRuntime`, evaluates workspace gate state, exposes tools/resources, and dispatches calls to MCP services (including `metagit_repo_search` for managed-repo-only lookup via `ManagedRepoSearchService`).
For a **local** JSON HTTP surface (development and agents), `metagit api serve` serves `src/metagit/core/api/server.py` endpoints backed by the same search service — not a hosted production API.
Testing flow is pytest-driven from `tests/` with focused unit tests per core module and integration checks for cross-module behavior.

## Key Components
- **CLI command layer (`src/metagit/cli/commands/*.py`)** — routes user actions (`config`, `detect`, `project`, `record`, `workspace`, `mcp`, `search`/`find`, `api`), depends on Click context + core managers.
- **Config subsystem (`metagit.core.config.*`)** — loads/creates/saves `.metagit.yml` and validates schema via Pydantic models; foundational for workspace and MCP gating behavior.
- **Detection subsystem (`metagit.core.detect.*`)** — infers repository metadata (language/framework/dependencies) and feeds generated config/context output.
- **Record subsystem (`metagit.core.record.*`)** — manages normalized records and conversions; used for storage/search flows beyond raw config files.
- **MCP runtime (`metagit.core.mcp.*`)** — stdio JSON-RPC server for tools/resources with state-aware gating, workspace path search/index, managed-repo search (`metagit_repo_search`), upstream hints, repo ops, and bootstrap sampling flow.
- **Managed repo search (`metagit.core.project.search_service`, `search_models`)** — ranks `.metagit.yml` workspace repos with tags/status; shared by CLI, MCP, and the local HTTP API.
- **Local HTTP API (`metagit.core.api.server`)** — optional `ThreadingHTTPServer` with read-only JSON routes for the same managed-repo search and resolve semantics.

## External Dependencies
- **Git providers (GitHub/GitLab APIs via provider modules)** — used for metadata/provider operations; provider wiring is optional/config-driven.
- **Git repository access (`GitPython`)** — used for repo introspection and sync-like operations (`inspect`, `fetch`, `pull`, `clone`) in project/MCP flows.
- **YAML/Pydantic stack (`PyYAML` + `pydantic`)** — enforces `.metagit.yml` and app config shape; all config entry points depend on this validation boundary.
- **MCP client host (Cursor/other MCP-compatible runtime)** — provides stdio transport and optional sampling capability (`sampling/createMessage`) for bootstrap generation.

## What Does NOT Exist Here
- No production-grade multi-tenant HTTP deployment; the bundled HTTP server is for local use alongside the CLI and MCP.
- No relational application database backing core runtime flows; state is file/workspace/repo driven.
- No centralized remote orchestration control plane for enterprise-wide repo execution in this repository.
- No full SBOM pipeline; project focuses on situational awareness metadata, not exhaustive dependency inventory.
````

## File: .mex/AGENTS.md
````markdown
---
name: agents
description: Always-loaded project anchor. Read this first. Contains project identity, non-negotiables, commands, and pointer to ROUTER.md for full context.
last_updated: 2026-05-05
---

# Metagit CLI

## What This Is
Metagit CLI manages and validates repository/workspace metadata (`.metagit.yml`) and exposes that context to humans and agents, including via MCP runtime tooling.

## Non-Negotiables
- Never commit secrets, tokens, or credential material into repo-tracked files.
- Keep `.metagit.yml` / config model compatibility intact; do not bypass validation paths.
- Keep mutating repo operations explicitly guarded (especially MCP sync modes).
- Put business logic in `src/metagit/core/*`; keep CLI command handlers thin.
- Run lint + relevant tests before claiming a change is complete.
- Default commit messages to patch semantics (`fix:`). Use `feat:` only for additive backward-compatible changes, and use major/breaking prefixes only when schema/config compatibility is intentionally broken.

## Commands
- Setup: `./configure.sh && task install`
- Lint: `task lint`
- Format: `task format`
- Tests: `task test`
- Build: `task build`
- MCP runtime: `uv run metagit mcp serve --status-once`

## Scaffold Growth
After every task: if no pattern exists for the task type you just completed, create one. If a pattern or context file is now out of date, update it. The scaffold grows from real work, not just setup. See the GROW step in `ROUTER.md` for details.

## Navigation
At the start of every session, read `ROUTER.md` before doing anything else.
For full project context, patterns, and task guidance — everything is there.
````

## File: .mex/SETUP.md
````markdown
# Setup — Populate This Scaffold

This file contains the prompts to populate the scaffold. It is NOT the dev environment setup — for that, see `context/setup.md` after population.

This scaffold is currently empty. Follow the steps below to populate it for your project.

## Recommended: Use setup.sh

```bash
.mex/setup.sh
```

The script handles everything automatically:
1. Detects your project state (existing codebase, fresh project, or partial)
2. Asks which AI tool you use and copies the right config file
3. Pre-scans your codebase with `mex init` to build a structured brief (~5-8k tokens vs ~50k from AI exploration)
4. Builds and runs the population prompt — or prints it for manual paste

If you want to populate manually instead, use the prompts below.

## Detecting Your State

**Existing codebase?** Follow Option A.
**Fresh project, nothing built yet?** Follow Option B.
**Partially built?** Follow Option A — the agent will flag empty slots it cannot fill yet.

---

## Option A — Existing Codebase

Paste the following prompt into your agent:

---

**SETUP PROMPT — copy everything between the lines:**

```
You are going to populate an AI context scaffold for this project.
The scaffold lives in the root of this repository.

Read the following files in order before doing anything else:
1. ROUTER.md — understand the scaffold structure
2. context/architecture.md — read the annotation comments to understand what belongs there
3. context/stack.md — same
4. context/conventions.md — same
5. context/decisions.md — same
6. context/setup.md — same

Then explore this codebase:
- Read the main entry point(s)
- Read the folder structure
- Read 2-3 representative files from each major layer
- Read any existing README or documentation

PASS 1 — Populate knowledge files:

Populate each context/ file by replacing the annotation comments
with real content from this codebase. Follow the annotation instructions exactly.
For each slot:
- Use the actual names, patterns, and structures from this codebase
- Do not use generic examples
- Do not leave any slot empty — if you cannot determine the answer,
  write "[TO DETERMINE]" and explain what information is needed
- Keep length within the guidance given in each annotation

Then assess: does this project have domains complex enough that cramming
them into architecture.md would make it too long or too shallow?
If yes, create additional domain-specific context files in context/.
Examples: a project with a complex auth system gets context/auth.md.
A data pipeline gets context/ingestion.md. A project with Stripe gets
context/payments.md. Use the same YAML frontmatter format (name,
description, triggers, edges, last_updated). Only create these for
domains that have real depth — not for simple integrations that fit
in a few lines of architecture.md.

After populating context/ files, update ROUTER.md:
- Fill in the Current Project State section based on what you found
- Add rows to the routing table for any domain-specific context files you created

Update AGENTS.md:
- Fill in the project name, one-line description, non-negotiables, and commands

PASS 2 — Generate starter patterns:

Read patterns/README.md for the format and categories.

Generate 3-5 starter patterns for the most common and most dangerous task
types in this project. Focus on:
- The 1-2 tasks a developer does most often (e.g., add endpoint, add component)
- The 1-2 integrations with the most non-obvious gotchas
- 1 debug pattern for the most common failure boundary

Each pattern should be specific to this project — real file paths, real gotchas,
real verify steps derived from the code you read in Pass 1.
Use the format in patterns/README.md. Name descriptively (e.g., add-endpoint.md).

Do NOT try to generate a pattern for every possible task type. The scaffold
grows incrementally — the behavioural contract (step 5: GROW) will create
new patterns from real work as the project evolves. Setup just seeds the most
critical ones.

After generating patterns, update patterns/INDEX.md with a row for each
pattern file you created. For multi-section patterns, add one row per task
section using anchor links (see INDEX.md annotation for format).

PASS 3 — Wire the web:

Re-read every file you just wrote (context/ files, pattern files, ROUTER.md).
For each file, add or update the `edges` array in the YAML frontmatter.
Each edge should point to another scaffold file that is meaningfully related,
with a `condition` explaining when an agent should follow that edge.

Rules for edges:
- Every context/ file should have at least 2 edges
- Every pattern file should have at least 1 edge (usually to the relevant context file)
- Edges should be bidirectional where it makes sense (if A links to B, consider B linking to A)
- Use relative paths (e.g., context/stack.md, patterns/add-endpoint.md)
- Pattern files can edge to other patterns (e.g., debug pattern → related task pattern)

Important: only write content derived from the codebase.
Do not include system-injected text (dates, reminders, etc.)
in any scaffold file.

When done, confirm which files were populated and flag any slots
you could not fill with confidence.
```

---

## Option B — Fresh Project

Paste the following prompt into your agent:

---

**SETUP PROMPT — copy everything between the lines:**

```
You are going to populate an AI context scaffold for a project that
is just starting. Nothing is built yet.

Read the following files in order before doing anything else:
1. ROUTER.md — understand the scaffold structure
2. All files in context/ — read the annotation comments in each

Then ask me the following questions one section at a time.
Wait for my answer before moving to the next section:

1. What does this project do? (one sentence)
2. What are the hard rules — things that must never happen in this codebase?
3. What is the tech stack? (language, framework, database, key libraries)
4. Why did you choose this stack over alternatives?
5. How will the major pieces connect? Describe the flow of a typical request/action.
6. What patterns do you want to enforce from day one?
7. What are you deliberately NOT building or using?

After I answer, populate the context/ files based on my answers.
For any slot you cannot fill yet, write "[TO BE DETERMINED]" and note
what needs to be decided before it can be filled.

Then assess: based on my answers, does this project have domains complex
enough that cramming them into architecture.md would make it too long
or too shallow? If yes, create additional domain-specific context files
in context/. Examples: a project with a complex auth system gets
context/auth.md. A data pipeline gets context/ingestion.md. A project
with Stripe gets context/payments.md. Use the same YAML frontmatter
format (name, description, triggers, edges, last_updated). Only create
these for domains that have real depth — not for simple integrations
that fit in a few lines of architecture.md. For fresh projects, mark
domain-specific unknowns with "[TO BE DETERMINED — populate after first
implementation]".

Update ROUTER.md current state to reflect that this is a new project.
Add rows to the routing table for any domain-specific context files you created.
Update AGENTS.md with the project name, description, non-negotiables, and commands.

Read patterns/README.md for the format and categories.

Generate 2-3 starter patterns for the most obvious task types you can
anticipate for this stack. Focus on the tasks a developer will do first.
Mark unknowns with "[VERIFY AFTER FIRST IMPLEMENTATION]".

Do NOT try to anticipate every possible pattern. The scaffold grows
incrementally — the behavioural contract (step 5: GROW) will create
new patterns from real work as the project evolves. Setup just seeds
the most critical ones.

After generating patterns, update patterns/INDEX.md with a row for each
pattern file you created.

PASS 3 — Wire the web:

Re-read every file you just wrote (context/ files, pattern files, ROUTER.md).
For each file, add or update the `edges` array in the YAML frontmatter.
Each edge should point to another scaffold file that is meaningfully related,
with a `condition` explaining when an agent should follow that edge.

Rules for edges:
- Every context/ file should have at least 2 edges
- Every pattern file should have at least 1 edge
- Edges should be bidirectional where it makes sense
- Use relative paths (e.g., context/stack.md, patterns/add-endpoint.md)

Important: only write content derived from your answers.
Do not include system-injected text (dates, reminders, etc.)
in any scaffold file.
```

---

## After Setup

**Verify** by starting a fresh session and asking your agent:
"Read `.mex/ROUTER.md` and tell me what you now know about this project."

A well-populated scaffold should give the agent enough to:
- Describe the architecture without looking at code
- Name the non-negotiable conventions
- Know which files to load for any given task type
- Know which patterns exist for common task types

## Keeping It Fresh

Once the scaffold is populated, use these to keep it aligned with your codebase:

- **`mex check`** — detect drift (zero tokens, zero AI)
- **`.mex/sync.sh`** — interactive drift check + targeted or full resync
- **`mex watch`** — auto drift score after every commit

## Commit Prefix Policy

When committing scaffold updates, default to `fix:` (patch intent). Use `feat:` only for additive backward-compatible updates. Use breaking markers (`type(scope)!:` or `BREAKING CHANGE:`) only for intentional schema/config compatibility breaks.
````

## File: .mex/SYNC.md
````markdown
# Sync — Realign This Scaffold

## Recommended: Use sync.sh

```bash
.mex/sync.sh
```

The script runs drift detection first, shows you exactly what's wrong, then offers:
1. **Targeted sync** — AI fixes only the flagged files (fastest, cheapest)
2. **Full resync** — AI re-reads everything and updates all scaffold files
3. **Prompt export** — shows the prompts for manual paste
4. **Exit** — fix it yourself

## Quick Check

```bash
mex check              # full drift report
mex check --quiet      # one-liner: "drift score 85/100 (1 error)"
mex sync --dry-run     # preview targeted fix prompts
```

## Manual Resync

If you prefer to paste a prompt manually, or don't have the CLI built:

---

**SYNC PROMPT — copy everything between the lines:**

```
You are going to resync the AI context scaffold for this project.
The scaffold may be out of date — the codebase has changed since it was last populated.

First, read all files in context/ to understand the current scaffold state.
Then explore what has changed in the codebase since the scaffold was last updated.
Check the last_updated dates in the YAML frontmatter of each file.

For each context/ file:
1. Compare the current scaffold content to the actual codebase
2. Identify what has changed, been added, or been removed
3. Update the file to reflect the current state

Critical rules for updating:
- Use surgical, targeted edits — NOT full file rewrites. Read the existing content,
  identify what changed, and update only those sections.
- PRESERVE YAML frontmatter structure. Never delete or rewrite the entire frontmatter block.
  Edit individual fields only. The edges, triggers, name, and description fields must
  survive every sync. If you need to update edges, add or remove individual entries —
  do not replace the entire array.
- In context/decisions.md: NEVER delete existing decisions.
  If a decision has changed, mark the old entry as "Superseded by [new decision title]"
  and add the new decision above it with today's date.
- In all other files: update content to reflect current reality
- Update last_updated in the YAML frontmatter of every file you change
- After updating each file, update ROUTER.md Current Project State
- Commit-message semantics: default to `fix:` for patch-level updates; use `feat:` only for additive backward-compatible changes; use breaking markers only when intentionally breaking schema/config compatibility.

When done, report:
- Which files were updated and what changed
- Any decisions that were superseded
- Any slots that could not be filled with confidence
```
````

## File: docs/install.md
````markdown
# Installation and usage guide

This guide covers practical ways to install and run Metagit locally, plus how to enable project skills for AI-agent workflows.

## Requirements

- Python 3.12+
- `uv`
- `git`

Optional:

- Docker (for container usage)
- `gitleaks` (for local security checks in QA workflows)

## Option 1: global CLI install with uv (recommended)

Install Metagit globally as a tool:

```bash
uv tool install metagit-cli
```

Upgrade later:

```bash
uv tool install -U metagit-cli
```

Verify:

```bash
metagit version
metagit --help
```

## Option 2: install from source (local development)

Clone and bootstrap:

```bash
git clone https://github.com/metagit-ai/metagit-cli.git
cd metagit-cli
uv sync
```

Install as an editable local package:

```bash
uv pip install -e .
```

Build a wheel and install that wheel:

```bash
task build
uv tool install dist/metagit-*-py3-none-any.whl
```

## Option 3: container usage

Run via container image:

```bash
docker pull ghcr.io/metagit-ai/metagit-cli:latest
docker run --rm ghcr.io/metagit-ai/metagit-cli:latest --help
```

## First local use

In a target Git repository:

```bash
metagit init --list-templates          # bundled profiles (application, umbrella, hermes-orchestrator, …)
metagit init ./my-coordinator --template hermes-orchestrator --create
metagit init --target ../hermes-control-plane --template hermes-orchestrator --no-prompt \
  --answers-file examples/hermes-orchestrator/answers.example.yml
metagit init --kind service --minimal  # any ProjectKind without a bundled template
```

This creates `.metagit.yml` and updates `.gitignore`.

Useful first checks:

```bash
metagit config validate
metagit info
```

## Local MCP runtime usage

Start MCP runtime in current workspace:

```bash
metagit mcp serve
```

Run one-shot status snapshot:

```bash
metagit mcp serve --status-once
```

Pin a specific workspace root:

```bash
metagit mcp serve --root /path/to/workspace
```

## AI-agent skill setup for this project

If you cloned this repository and want skills available for local agent discovery:

```bash
task skills:sync
task generate:schema
```

One-shot combined:

```bash
task skills:sync generate:schema
```

This syncs project skills into `.cursor/skills/` and regenerates config schemas.

## Session closeout checks

Before push or hand-off:

```bash
task skills:sync generate:schema
task qa:prepush
```

## Troubleshooting

- If `metagit` fails after tool install, reinstall:
  - `uv tool install -U --reinstall metagit-cli`
- If command not found, ensure `uv` tool bin path is on `PATH`.
- If provider-backed features fail, configure API tokens in app config or environment:
  - `METAGIT_GITHUB_API_TOKEN`
  - `METAGIT_GITLAB_API_TOKEN`
````

## File: examples/enhanced_fuzzyfinder_demo.py
````python
#!/usr/bin/env python
"""
Example demonstrating the enhanced FuzzyFinder features:
- Optional item opacity
- Vertical scrolling for large lists
- Enhanced navigation (Page Up/Down, Home/End)
"""
⋮----
# Add the metagit package to the path
⋮----
def main()
⋮----
"""Demonstrate enhanced FuzzyFinder features."""
⋮----
# Create a larger dataset to show scrolling
programming_languages = [
⋮----
# Configure with new features
config = FuzzyFinderConfig(
⋮----
max_results=25,  # Show many results to demonstrate scrolling
⋮----
item_opacity=0.8,  # 80% opacity for subtle transparency
highlight_color="bold white bg:#2563eb",  # Nice blue highlight
⋮----
# Create and run the fuzzy finder
finder = FuzzyFinder(config)
⋮----
result = finder.run()
````

## File: examples/fuzzyfinder_simple_colors.py
````python
#!/usr/bin/env python
"""
Simple example showing how to use custom colors in FuzzyFinder.
This demonstrates coloring items based on their type or priority.
"""
⋮----
# Add the metagit package to the path
⋮----
def main()
⋮----
"""Demonstrate custom colors with a simple task list."""
⋮----
# Create a list of tasks with different types
tasks = [
⋮----
# Define colors based on task type (prefix)
task_colors = {
⋮----
"bug_fix_login": "bold red",  # Critical bugs in red
"bug_fix_payment": "bold red",  # Critical bugs in red
"feature_dashboard": "green",  # Features in green
"feature_notifications": "green",  # Features in green
"docs_api_reference": "blue",  # Documentation in blue
"docs_user_guide": "blue",  # Documentation in blue
"test_user_flow": "cyan",  # Tests in cyan
"test_integration": "cyan",  # Tests in cyan
⋮----
# Configure FuzzyFinder with custom colors
config = FuzzyFinderConfig(
⋮----
# Run the finder
finder = FuzzyFinder(config)
⋮----
result = finder.run()
⋮----
task_type = result.split("_")[0]
````

## File: examples/get_git_files.py
````python
#!/usr/bin/env python
"""
List git files in a repository.
"""
⋮----
# Add the metagit package to the path
⋮----
def main()
⋮----
"""List all git files in the current repository."""
⋮----
# Get the current working directory
repo_path = Path.cwd()
⋮----
# List git files
git_files = list_git_files(repo_path)
````

## File: scripts/prepush-gate.py
````python
#!/usr/bin/env python3
"""Cross-agent pre-push quality gate runner."""
⋮----
def run_step(name: str, cmd: list[str], logs_dir: Path, shell: bool = False) -> bool
⋮----
log_path = logs_dir / f"{name}.log"
⋮----
completed = subprocess.run(
⋮----
lines = log_path.read_text(encoding="utf-8", errors="replace").splitlines()
⋮----
def resolve_pytest_cmd() -> list[str]
⋮----
SECURITY_DEP_FILES = frozenset({"pyproject.toml", "uv.lock"})
SECURITY_SRC_PREFIX = "src/"
⋮----
def _git_lines(args: list[str]) -> list[str] | None
⋮----
def changed_paths_for_security() -> set[str] | None
⋮----
"""Collect changed paths; None means run the full security pipeline."""
chunks: list[str] = []
⋮----
lines = _git_lines(spec)
⋮----
upstream = _git_lines(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"])
⋮----
branch_diff = _git_lines(["diff", "--name-only", f"{upstream[0]}...HEAD"])
⋮----
def security_scan_plan(changed: set[str] | None) -> tuple[bool, bool, bool]
⋮----
"""Return (sync_deps, pip_audit, bandit) for context-aware security."""
⋮----
deps = any(path in SECURITY_DEP_FILES for path in changed)
src = any(path.startswith(SECURITY_SRC_PREFIX) for path in changed)
⋮----
def run_security_scan(logs_dir: Path) -> bool
⋮----
"""Run pip-audit/bandit when lockfile or src/ changed; skip for docs-only diffs."""
changed = changed_paths_for_security()
⋮----
failed = False
⋮----
def main() -> int
⋮----
parser = argparse.ArgumentParser()
⋮----
args = parser.parse_args()
⋮----
logs_dir = Path(".metagit")
round_idx = 1
⋮----
security_ok = run_step(
⋮----
failed = True
````

## File: skills/metagit-control-center/scripts/control-cycle.zsh
````zsh
#!/usr/bin/env zsh
set -euo pipefail

ROOT="${1:-$PWD}"
QUERY="${2:-}"
PRESET="${3:-infra}"

"$(dirname "$0")/../../metagit-gating/scripts/gate-status.zsh" "$ROOT"

if [[ -n "$QUERY" ]]; then
  "$(dirname "$0")/../../metagit-upstream-scan/scripts/upstream-scan.zsh" "$ROOT" "$QUERY" "$PRESET" 20
else
  echo "status=ok\tmessage=no-query-provided"
fi
````

## File: skills/metagit-control-center/SKILL.md
````markdown
---
name: metagit-control-center
description: Use when running metagit as an MCP control center for multi-repo awareness, guarded sync, and operational knowledge across ongoing agent tasks.
---

# Metagit Control Center Skill

Use this skill when an agent should actively coordinate repository context and task execution across a workspace.

## Purpose

Provide a repeatable control-center workflow where Metagit MCP guides awareness, synchronization, and operational continuity over multiple related repositories.

## Local Script Wrapper (Use First)

Use this token-efficient wrapper for control-center cycles:
- `./scripts/control-cycle.zsh [root_path] ["query"] [preset]`

Wrapper behavior:
- runs gating status first
- optionally runs upstream discovery for blocker queries
- emits compact, machine-readable lines

## Core Workflows

### 1) Session Initialization
- Validate active workspace gate.
- Read `metagit://workspace/config` and `metagit://workspace/repos/status`.
- Call `metagit_project_context_switch` when the objective is tied to a workspace project.
- Run `metagit_workspace_health_check` or read `metagit://workspace/health` for maintenance signals.
- Identify stale repos and unresolved blockers from prior activity.

### 2) Active Task Support
- For each coding objective, map impacted repos.
- Use workspace search and upstream hints before broad exploration.
- Sync only repos that are required by the active objective.

### 3) Guarded Synchronization
- Default to `fetch` for visibility.
- Use `pull` or `clone` only with explicit permission and rationale.
- Track sync outcomes in operations log resource.

### 4) Operational Memory
- Before switching projects: `metagit_session_update` (notes + recent repos), optional `metagit_workspace_state_snapshot`.
- After returning: `metagit_workspace_state_restore` when a snapshot was taken (metadata only; git tree is unchanged).

Maintain bounded local records of:
- sync actions
- issue signatures searched
- candidate upstream repos identified
- unresolved dependencies and follow-ups

## Decision Guidelines

- Use metagit search first when blocker appears external to current repo.
- Prefer deterministic evidence over speculative jumps.
- Keep operations minimal and auditable.

## Output Contract

For each control-center cycle, provide:
- current objective
- repositories examined
- actions taken (or intentionally deferred)
- next recommended step

## Safety Rules

- Never mutate repositories without explicit authorization.
- Never broaden scope beyond configured workspace boundaries.
- Always preserve a clear audit trail of control actions.
````

## File: skills/metagit-gitnexus/SKILL.md
````markdown
---
name: metagit-gitnexus
description: Run gitnexus analysis for a target workspace and selected project repositories before graph-dependent tasks. Use when index staleness is detected or cross-repo graph results are needed.
---

# Running GitNexus Analysis

Use this skill whenever GitNexus context is stale or missing for target repositories.

## Local Wrapper (Use First)

- `zsh ./skills/metagit-gitnexus/scripts/analyze-targets.zsh <workspace_root> [project_name]`

## Workflow

1. Analyze the current repository where the command is run.
2. Resolve target project repositories from `.metagit.yml`.
3. Run `npx gitnexus analyze` in each local repository path found.
4. Report per-repository success/failure and next actions.

## Manual workspace graph (`.metagit.yml` → Cypher)

Export manifest `graph.relationships` for GitNexus overlay ingest:

```bash
metagit config graph export -c .metagit.yml --format tool-calls
# or MCP: metagit_export_workspace_graph_cypher
```

Run returned `gitnexus_cypher` tool calls against the umbrella repo index (`--gitnexus-repo` when names differ). Schema DDL (`MetagitEntity` / `MetagitLink`) runs once per index.

## Output Contract

Return:
- analyzed repositories
- failures and reasons
- whether graph queries are safe to run now

## Safety

- Skip repositories that do not exist locally.
- Do not mutate repo content; analysis should be read-only indexing.
````

## File: skills/metagit-repo-impact/SKILL.md
````markdown
---
name: metagit-repo-impact
description: Plan repository change impact before edits by combining metagit workspace context and graph-based dependency analysis. Use when a change may affect multiple repositories.
---

# Planning Repo Impact

Use this skill before risky or cross-repo modifications.

## Workflow

1. Identify target symbols/files for change.
2. Use metagit workspace context to bound repository scope.
3. Use graph impact tooling to estimate blast radius.
4. Produce a change plan with test and rollback focus.

## Commands

- `metagit workspace select --project <name>`
- MCP `metagit_cross_project_dependencies` with `source_project` and `dependency_types` before large cross-project edits
- MCP `metagit_project_context_switch` to bound scope to one workspace project
- `npx gitnexus analyze` on affected repos when `graph_status` is `stale` or `missing`
- `npx gitnexus query` / `gitnexus impact` for symbol-level analysis after indexes are fresh

## Output Contract

Return:
- impacted repositories and interfaces
- highest-risk change points
- minimum validation plan

## Safety

- Do not execute mutating steps in this planning phase.
````

## File: skills/metagit-workspace-scope/SKILL.md
````markdown
---
name: metagit-workspace-scope
description: Discover active metagit workspace scope, project boundaries, and repository status. Use when an agent starts work in a multi-repo workspace and needs fast, scoped context before editing.
---

# Discovering Workspace Scope

Use this skill at session start for workspace-aware tasks.

## Workflow

1. Run workspace gate and status checks first.
2. Read configured projects and repository status from metagit resources/tools.
3. Build a compact map of active project names, repo names, and sync state.
4. Report a bounded scope for the current objective.

## Commands

- `zsh ./skills/metagit-gating/scripts/gate-status.zsh [root_path]`
- `metagit workspace select --project <name>` (interactive repo picker)
- MCP `metagit_project_context_switch` with `project_name` (structured context + session restore)
- MCP `metagit_workspace_state_snapshot` before leaving a project for a long switch

## Output Contract

Return:
- active/inactive workspace state and reason
- candidate project to operate on
- top repositories to inspect first

## Safety

- Keep operations read-only in this step.
- Do not sync or mutate repositories without explicit request.
````

## File: skills/metagit-workspace-sync/SKILL.md
````markdown
---
name: metagit-workspace-sync
description: Sync workspace repositories safely using metagit with scoped fetch, pull, or clone actions. Use when repository content must be refreshed for implementation.
---

# Syncing Workspace Repositories

Use this skill when repository state must be updated.

## Workflow

1. Confirm active workspace and target project.
2. Start with read-only status and fetch where possible.
3. Use pull/clone only when required for the current objective.
4. Summarize which repositories changed and what remains stale.

## Commands

- `metagit project sync --project <name>`
- MCP `metagit_workspace_sync` for batch fetch/pull/clone (`repos`, `only_if`, `dry_run`, `max_parallel`)
- MCP `metagit_repo_sync` for a single repository (requires `allow_mutation` for pull/clone)
- `zsh ./skills/metagit-control-center/scripts/control-cycle.zsh [root_path] ["query"] [preset]`

## Output Contract

Return:
- repositories synced
- sync mode used (fetch/pull/clone)
- failures and retry guidance

## Safety

- Limit sync to repos defined in `.metagit.yml`.
- Avoid broad synchronization if only a subset is needed.
````

## File: src/metagit/cli/commands/mcp.py
````python
#!/usr/bin/env python
"""
MCP command group for Metagit CLI.
"""
⋮----
@click.group()
def mcp() -> None
⋮----
"""Metagit MCP server commands."""
⋮----
@click.pass_context
def serve(ctx: click.Context, root: Optional[str], status_once: bool) -> None
⋮----
"""Start MCP runtime over stdio."""
runtime = MetagitMcpRuntime(root=root)
⋮----
snapshot = runtime.status_snapshot()
⋮----
logger = ctx.obj.get("logger") if ctx.obj else None
⋮----
"""Install metagit MCP server entry into supported agent configs."""
logger = ctx.obj["logger"] if ctx.obj else None
selected_targets = resolve_targets(
⋮----
results = install_mcp_for_targets(
````

## File: src/metagit/cli/commands/search.py
````python
#!/usr/bin/env python
"""
Managed workspace repository search (`.metagit.yml` corpus only).
"""
⋮----
def _parse_tag_filters(tag_values: tuple[str, ...]) -> dict[str, str] | None
⋮----
"""Parse repeated `--tag key=value` into a tag filter dict."""
⋮----
parsed: dict[str, str] = {}
⋮----
"""Search managed repositories declared in `.metagit.yml` (alias: `metagit find`)."""
_ = ctx
manager = MetagitConfigManager(definition_path)
config = manager.load_config()
⋮----
workspace_root = str(Path(definition_path).resolve().parent)
tag_filters = _parse_tag_filters(tag_values)
service = ManagedRepoSearchService()
status_filter = list(status_values) if status_values else None
result = service.search(
⋮----
resolved = service.resolve_one(
````

## File: src/metagit/cli/commands/skills.py
````python
#!/usr/bin/env python
"""
Skills command group for bundled skill management.
"""
⋮----
@click.group(name="skills", invoke_without_command=True)
@click.pass_context
def skills(ctx: click.Context) -> None
⋮----
"""Bundled skill management commands."""
⋮----
@skills.command("list")
@click.pass_context
def skills_list(ctx: click.Context) -> None
⋮----
"""List bundled skills available for install."""
logger = ctx.obj["logger"]
bundled = list_bundled_skills()
⋮----
@skills.command("show")
@click.argument("skill_name", required=False)
@click.pass_context
def skills_show(ctx: click.Context, skill_name: str | None) -> None
⋮----
"""Show a bundled skill document."""
⋮----
skill_names = list_bundled_skills()
⋮----
content = skill_markdown(skill_name)
⋮----
"""Install bundled skills into supported agent targets."""
⋮----
selected_targets = resolve_targets(
⋮----
selected_skills = resolve_skill_names(list(skills) if skills else None)
⋮----
message = str(exc)
⋮----
results = install_skills_for_targets(
⋮----
line = f"[{result.target}] {result.details} -> {result.path}"
````

## File: src/metagit/cli/json_output.py
````python
#!/usr/bin/env python
"""
Shared JSON output helpers for agentic CLI use.
"""
⋮----
def emit_json(payload: BaseModel | dict[str, Any]) -> None
⋮----
"""Print a JSON document to stdout."""
⋮----
data: dict[str, Any] = payload.model_dump(mode="json")
⋮----
data = payload
⋮----
"""Emit mutation result and exit non-zero when ok is false."""
⋮----
ok = bool(getattr(result, "ok", True))
error = getattr(result, "error", None)
⋮----
entity = getattr(result, "entity", "entity")
operation = getattr(result, "operation", "updated")
name = getattr(result, "repo_name", None) or getattr(
⋮----
"""Emit layout mutation result; show plan summary on dry-run."""
⋮----
data = getattr(result, "data", None) or {}
````

## File: src/metagit/core/config/example_generator.py
````python
#!/usr/bin/env python
"""
Generate a documented YAML exemplar for MetagitConfig with field descriptions as comments.
"""
⋮----
def load_example_overrides() -> dict[str, Any]
⋮----
"""Load optional nested overrides for the generated exemplar."""
override_path = Path(DATA_PATH) / "config-example-overrides.yml"
⋮----
loaded = metagit_yaml.safe_load(handle)
⋮----
def deep_merge(base: dict[str, Any], overrides: dict[str, Any]) -> dict[str, Any]
⋮----
"""Recursively merge overrides into base (overrides win)."""
merged = dict(base)
⋮----
class ConfigExampleGenerator
⋮----
"""Build a representative MetagitConfig-shaped dict for documentation."""
⋮----
def __init__(self, overrides: dict[str, Any] | None = None) -> None
⋮----
def build(self, *, include_workspace: bool = True) -> dict[str, Any]
⋮----
"""Return a nested dict suitable for YAML serialization."""
payload = self._sample_model(MetagitConfig)
⋮----
"""Render YAML with per-field description comments."""
payload = self.build(include_workspace=include_workspace)
header = (
⋮----
lines = header.splitlines(keepends=True)
⋮----
"""Render one mapping using model field order and descriptions."""
pad = "  " * indent
lines: list[str] = []
fields = model.model_fields
⋮----
field_info = fields[key]
description = field_info.description
⋮----
annotation = field_info.annotation
nested_model = self._nested_model_for_annotation(annotation)
⋮----
def _render_key_value(self, key: str, value: Any, *, indent: int) -> list[str]
⋮----
lines = [f"{pad}{key}:\n"]
⋮----
def _format_scalar(self, value: Any) -> str
⋮----
escaped = str(value).replace("\n", "\\n")
⋮----
def _sample_model(self, model: type[BaseModel]) -> dict[str, Any]
⋮----
payload: dict[str, Any] = {}
⋮----
inner = self._sample_annotation(field_info.annotation, name)
⋮----
def _sample_annotation(self, annotation: Any, field_name: str) -> Any
⋮----
origin = get_origin(annotation)
⋮----
args = [arg for arg in get_args(annotation) if arg is not type(None)]
⋮----
inner_args = get_args(annotation)
inner = inner_args[0] if inner_args else Any
sampled = self._sample_annotation(inner, field_name)
⋮----
nested = self._nested_model_for_annotation(annotation)
⋮----
nested = self._nested_model_for_annotation(arg)
⋮----
args = get_args(annotation)
⋮----
def _scalar_for_name(self, field_name: str) -> Any
⋮----
lowered = field_name.lower()
````

## File: src/metagit/core/detect/detectors/docker.py
````python
class DockerDetector
⋮----
name = "DockerDetector"
⋮----
def should_run(self, ctx: ProjectScanContext) -> bool
⋮----
def run(self, ctx: ProjectScanContext) -> Optional[DiscoveryResult]
⋮----
dockerfiles = [p for p in ctx.all_files if "dockerfile" in p.name.lower()]
composefiles = [
⋮----
containers = []
⋮----
base_image = None
exposed_ports = []
entrypoint = None
cmd = None
⋮----
line = line.strip()
⋮----
base_image = line.split(None, 1)[1]
⋮----
entrypoint = line[len("ENTRYPOINT") :].strip()
⋮----
cmd = line[len("CMD") :].strip()
⋮----
continue  # corrupt file
⋮----
services = []
⋮----
data = yaml.safe_load(f)
````

## File: src/metagit/core/detect/detectors/python.py
````python
class PythonDetector
⋮----
name = "PythonDetector"
⋮----
def should_run(self, ctx: ProjectScanContext) -> bool
⋮----
indicators = ["pyproject.toml", "requirements.txt", "setup.py"]
⋮----
def run(self, ctx: ProjectScanContext) -> Optional[DiscoveryResult]
⋮----
dependencies = self._get_dependencies(ctx.root_path)
⋮----
def _get_dependencies(self, root: Path) -> list[str]
⋮----
deps = []
⋮----
pyproject = os.path.join(root, "pyproject.toml")
⋮----
data = tomli.load(f)
# Handle both Poetry and PEP 621
⋮----
reqs = root / "requirements.txt"
⋮----
setup = root / "setup.py"
⋮----
)  # You could parse with `ast`, but keep it simple here
````

## File: src/metagit/core/detect/detectors/terraform.py
````python
def classify_module_source(source: str) -> str
⋮----
source = source.strip()
⋮----
class TerraformDetector
⋮----
name = "TerraformDetector"
⋮----
def should_run(self, ctx: ProjectScanContext) -> bool
⋮----
def run(self, ctx: ProjectScanContext) -> Optional[DiscoveryResult]
⋮----
provider_set = set()
module_sources = []
backend_type = None
⋮----
parsed = hcl2.load(f)
⋮----
# Providers
⋮----
# Modules
⋮----
source = attrs.get("source")
⋮----
# Terraform block (for backend)
⋮----
backend = block.get("backend")
⋮----
backend_type = next(iter(backend.keys()), None)
````

## File: src/metagit/core/mcp/services/bootstrap_sampling.py
````python
#!/usr/bin/env python
"""
Sampling-assisted bootstrap service for `.metagit.yml`.
"""
⋮----
class BootstrapSamplingService
⋮----
"""Generate `.metagit.yml` using sampling when available, otherwise fallback."""
⋮----
"""Generate config draft and return write guidance."""
⋮----
errors: list[str] = []
⋮----
prompt = self._build_prompt(context=context, validation_errors=errors)
draft_yaml = self._sampler({"prompt": prompt})
validation = self._validate_yaml(draft_yaml=draft_yaml)
⋮----
errors = [validation["error"]]
⋮----
errors = "\n".join(validation_errors or [])
⋮----
def _validate_yaml(self, draft_yaml: str) -> dict[str, str | bool]
⋮----
loaded = yaml.safe_load(draft_yaml)
````

## File: src/metagit/core/mcp/services/cross_project_dependencies.py
````python
#!/usr/bin/env python
"""
Map cross-project dependencies from workspace configuration and import hints.
"""
⋮----
class CrossProjectDependencyService
⋮----
"""Build a dependency graph across workspace projects."""
⋮----
_valid_types = {
⋮----
"""Return dependency nodes and edges reachable from a source project."""
⋮----
project_names = {project.name for project in config.workspace.projects}
⋮----
selected_types = self._normalize_types(dependency_types=dependency_types)
rows = self._index.build_index(config=config, workspace_root=workspace_root)
⋮----
edges = self._collect_edges(
⋮----
source_id = f"project:{source_project}"
⋮----
graph_status = self._registry.summarize_for_paths(
⋮----
impact = self._build_impact_summary(
⋮----
def _normalize_types(self, dependency_types: Optional[list[str]]) -> set[str]
⋮----
"""Normalize requested dependency type filters."""
⋮----
selected = {item.lower() for item in dependency_types}
unknown = selected - self._valid_types
⋮----
selected = selected & self._valid_types
⋮----
"""Build project and repo nodes from workspace index rows."""
nodes: list[DependencyNode] = []
id_to_node: dict[str, DependencyNode] = {}
path_to_id: dict[str, str] = {}
project_names = {row["project_name"] for row in rows}
⋮----
node_id = f"project:{project_name}"
node = DependencyNode(
⋮----
repo_path = str(row.get("repo_path", ""))
node_id = f"repo:{row['project_name']}/{row['repo_name']}"
⋮----
"""Collect dependency edges from all enabled collectors."""
edges: list[DependencyEdge] = []
project_names = {
⋮----
"""Edges from top-level graph.relationships in .metagit.yml."""
⋮----
from_id = resolve_graph_endpoint_id(
to_id = resolve_graph_endpoint_id(
⋮----
evidence = ["manifest graph.relationships"]
⋮----
"""Edges from explicit refs and root-level dependency declarations."""
⋮----
repo_by_name: dict[str, list[dict[str, Any]]] = {}
⋮----
from_id = f"repo:{row['project_name']}/{row['repo_name']}"
tags = row.get("tags") or {}
⋮----
ref_target = self._ref_target(repo=repo, project_names=project_names)
⋮----
ref_target = self._ref_target(repo=dep, project_names=project_names)
⋮----
matched = repo_by_name.get(dep.name, [])
⋮----
"""Edges from shared URLs and configured path references."""
⋮----
by_url: dict[str, list[dict[str, Any]]] = {}
⋮----
url = row.get("url")
⋮----
normalized = self._normalize_url(str(url))
⋮----
configured_paths: dict[str, list[dict[str, Any]]] = {}
⋮----
configured = row.get("configured_path")
⋮----
"""Edges from manifest import hints between repositories."""
⋮----
hints = self._import_scanner.scan_repo(
⋮----
to_id = hint.get("to_id")
⋮----
"""Keep nodes and edges within N project hops from the source."""
adjacency: dict[str, set[str]] = {}
⋮----
visited = {source_id}
queue: deque[tuple[str, int]] = deque([(source_id, 0)])
⋮----
source_project = source_id.split(":", 1)[1]
⋮----
filtered_edges = [
filtered_nodes = [node for node in nodes if node.id in visited]
⋮----
"""Summarize risk and affected projects."""
affected_projects = sorted(
affected_repos = sorted(
notes: list[str] = []
stale_count = sum(1 for status in graph_status.values() if status == "stale")
missing_count = sum(
⋮----
risk = "low"
⋮----
risk = "high"
⋮----
risk = "medium"
⋮----
def _ref_target(self, repo: ProjectPath, project_names: set[str]) -> Optional[str]
⋮----
"""Resolve a ProjectPath ref to a workspace project name."""
⋮----
def _edge_is_internal(self, edge: DependencyEdge, project_names: set[str]) -> bool
⋮----
"""Return whether an edge stays inside configured workspace projects."""
⋮----
project = node_id.split(":", 1)[1]
⋮----
def _normalize_url(self, url: str) -> str
⋮----
"""Normalize repository URLs for comparison."""
cleaned = url.strip().lower().rstrip("/")
⋮----
cleaned = cleaned[:-4]
parsed = urlparse(cleaned)
⋮----
def _dedupe_edges(self, edges: list[DependencyEdge]) -> list[DependencyEdge]
⋮----
"""Remove duplicate edges while preserving evidence."""
merged: dict[tuple[str, str, str], DependencyEdge] = {}
⋮----
key = (edge.from_id, edge.to_id, edge.type)
⋮----
existing = merged[key]
combined = list(dict.fromkeys(existing.evidence + edge.evidence))
````

## File: src/metagit/core/mcp/services/project_context.py
````python
#!/usr/bin/env python
"""
Project context switching and environment export for MCP tools.
"""
⋮----
_INSPECT_LIMIT = 20
_EXPORTABLE_VARIABLE_KINDS = {
⋮----
class ProjectContextService
⋮----
"""Switch active workspace project and build agent context bundles."""
⋮----
"""Switch to a workspace project and return a context bundle."""
project = self._find_project(config=config, project_name=project_name)
⋮----
store = SessionStore(workspace_root=workspace_root)
prior_meta = store.get_workspace_meta()
⋮----
prior_session = store.get_project_session(
⋮----
bundle = self._build_bundle(
⋮----
"""Return context for active or named project without changing active project."""
⋮----
meta = store.get_workspace_meta()
target = project_name or meta.active_project
⋮----
project = self._find_project(config=config, project_name=target)
⋮----
"""Build a project context bundle without updating active project."""
⋮----
rows = self._index.build_index(config=config, workspace_root=workspace_root)
project_rows = [row for row in rows if row["project_name"] == project_name]
inspect_truncated = len(project_rows) > _INSPECT_LIMIT
inspect_rows = project_rows[:_INSPECT_LIMIT]
⋮----
repo_contexts: list[ProjectRepoContext] = []
⋮----
session_state = ProjectContextSession(restored=False)
⋮----
saved = store.get_project_session(project_name=project_name)
session_state = ProjectContextSession(
⋮----
env_bundle = (
⋮----
suggested = self._resolve_suggested_cwd(
focus_repo_entry = None
focus_repo_name: Optional[str] = None
⋮----
focus_row = self._match_repo_target(rows=project_rows, target=primary_repo)
⋮----
focus_repo_name = str(focus_row.get("repo_name", "")) or None
focus_repo_entry = self._instructions.find_repo(
⋮----
focus_row = self._match_repo_target(rows=project_rows, target=suggested)
⋮----
instructions = self._instructions.resolve(
⋮----
"""Return sorted env export keys for a project without switching context."""
⋮----
"""Persist session fields for a project."""
⋮----
session = store.update_project_session(
⋮----
"""Locate workspace project by name."""
⋮----
"""Build repo context with optional git inspect."""
exists = bool(row.get("exists"))
repo_name = str(row.get("repo_name", ""))
repo_entry = self._instructions.find_repo(
branch: Optional[str] = None
dirty: Optional[bool] = None
inspect_error: Optional[str] = None
⋮----
inspected = inspect_repo_state(repo_path=str(row["repo_path"]))
⋮----
branch = str(inspected["branch"]) if inspected.get("branch") else None
dirty = (
⋮----
inspect_error = str(inspected.get("error", "inspect failed"))
⋮----
"""Build safe environment exports and hints."""
exports: dict[str, str] = {
hints: list[str] = []
⋮----
"""Pick a suggested working directory for agents."""
⋮----
match = self._match_repo_target(rows=project_rows, target=primary_repo)
⋮----
"""Match repo by name or resolved path."""
normalized = target.strip()
````

## File: src/metagit/core/mcp/services/repo_ops.py
````python
#!/usr/bin/env python
"""
Repository inspection and guarded synchronization operations.
"""
⋮----
class RepoOperationsService
⋮----
"""Provide safe repo inspect/sync operations for MCP tools."""
⋮----
def inspect(self, repo_path: str) -> dict[str, str | bool]
⋮----
"""Inspect repository branch and dirty state."""
⋮----
repo = Repo(repo_path)
⋮----
"""Synchronize repository with mutation guardrails."""
normalized_mode = mode.lower()
⋮----
origin = repo.remote(name="origin")
````

## File: src/metagit/core/mcp/services/session_store.py
````python
#!/usr/bin/env python
"""
Persist workspace and per-project session state under .metagit/sessions/.
"""
⋮----
_PROJECT_FILE_PATTERN = re.compile(r"^[\w.-]+$")
⋮----
class SessionStore
⋮----
"""Read and write session JSON under the workspace .metagit directory."""
⋮----
def __init__(self, workspace_root: str) -> None
⋮----
@property
    def sessions_dir(self) -> Path
⋮----
"""Return the sessions directory path."""
⋮----
def ensure_dirs(self) -> None
⋮----
"""Create session directories with restrictive permissions when possible."""
⋮----
def get_workspace_meta(self) -> WorkspaceSessionMeta
⋮----
"""Load workspace session metadata or return defaults."""
payload = self._read_json(path=self._workspace_meta_path)
⋮----
def save_workspace_meta(self, meta: WorkspaceSessionMeta) -> None
⋮----
"""Persist workspace session metadata."""
⋮----
def set_active_project(self, project_name: str) -> WorkspaceSessionMeta
⋮----
"""Set active project on workspace metadata."""
meta = self.get_workspace_meta()
⋮----
def get_project_session(self, project_name: str) -> ProjectSession
⋮----
"""Load a project session or return an empty session."""
path = self._project_session_path(project_name=project_name)
payload = self._read_json(path=path)
⋮----
session = ProjectSession.model_validate(payload)
⋮----
def save_project_session(self, session: ProjectSession) -> None
⋮----
"""Persist a project session."""
⋮----
path = self._project_session_path(project_name=session.project_name)
⋮----
"""Merge updates into a project session."""
session = self.get_project_session(project_name=project_name)
⋮----
merged = dict(session.env_overrides)
⋮----
def rename_project_session(self, from_name: str, to_name: str) -> bool
⋮----
"""
        Rename a per-project session file when a workspace project is renamed.

        Returns True when a session file was migrated.
        """
old_path = self._project_session_path(project_name=from_name)
new_path = self._project_session_path(project_name=to_name)
⋮----
session = self.get_project_session(project_name=to_name)
⋮----
def link_snapshot(self, snapshot_id: str, project_name: Optional[str]) -> None
⋮----
"""Record snapshot id on workspace and optional project session."""
⋮----
def _project_session_path(self, project_name: str) -> Path
⋮----
"""Resolve sanitized per-project session file path."""
⋮----
def _read_json(self, path: Path) -> Optional[dict]
⋮----
"""Read JSON object from path."""
⋮----
raw = path.read_text(encoding="utf-8")
data = json.loads(raw)
⋮----
def _write_json(self, path: Path, payload: dict) -> None
⋮----
"""Write JSON object to path."""
````

## File: src/metagit/core/mcp/services/upstream_hints.py
````python
#!/usr/bin/env python
"""
Upstream issue hint ranking service.
"""
⋮----
class UpstreamHintService
⋮----
"""Rank repositories likely to contain upstream root causes."""
⋮----
_category_terms: dict[str, list[str]] = {
⋮----
"""Return ranked repository candidates for the blocker description."""
blocker_lower = blocker.lower()
results: list[dict[str, Any]] = []
⋮----
score = self._score_repo(blocker_lower=blocker_lower, repo=repo)
⋮----
def _score_repo(self, blocker_lower: str, repo: dict[str, Any]) -> float
⋮----
score = 0.0
name = str(repo.get("repo_name", "")).lower()
project = str(repo.get("project_name", "")).lower()
path = str(repo.get("repo_path", "")).lower()
⋮----
def _rationale(self, blocker_lower: str, repo: dict[str, Any]) -> str
⋮----
name = str(repo.get("repo_name", "unknown"))
````

## File: src/metagit/core/project/search_service.py
````python
#!/usr/bin/env python
"""
Search and resolve managed workspace repositories from `.metagit.yml` only.
"""
⋮----
class ManagedRepoSearchService
⋮----
"""Match queries against configured workspace repos using WorkspaceIndexService rows."""
⋮----
def __init__(self) -> None
⋮----
"""Return ranked matches for a free-text query and optional filters."""
rows = self._index.build_index(config=config, workspace_root=workspace_root)
status_filter = set(status) if status else None
matches: list[ManagedRepoMatch] = []
⋮----
ordered = self._sort_matches(matches=matches, sort=sort)
⋮----
"""Return a single best match or a structured not_found / ambiguous error."""
result = self.search(
⋮----
status = ManagedRepoStatus(
⋮----
"""Return whether an index row satisfies structural filters."""
⋮----
row_url = row.get("url")
⋮----
row_sync = bool(row.get("sync"))
⋮----
row_tags = row.get("tags") or {}
⋮----
"""Sort matches by score, project, or repository name."""
normalized = sort.lower()
⋮----
reasons: list[str] = []
q = query.strip()
⋮----
name = row.get("repo_name") or ""
⋮----
score = 0
q_lower = q.lower()
name_lower = name.lower()
⋮----
url = row.get("url") or ""
⋮----
project_name = row.get("project_name") or ""
````

## File: src/metagit/core/project/source_sync.py
````python
#!/usr/bin/env python
"""
Provider-backed recursive repository discovery and workspace planning.
"""
⋮----
class SourceSyncService
⋮----
"""Discovers repositories from providers and builds/apply sync plans."""
⋮----
def __init__(self, app_config: AppConfig, logger: UnifiedLogger)
⋮----
def discover(self, spec: SourceSpec) -> Union[List[DiscoveredRepo], Exception]
⋮----
discovered_project_paths = [self._to_project_path(repo) for repo in discovered]
discovered_by_url = {
existing_by_url = {
⋮----
plan = SourceSyncPlan(discovered_count=len(discovered))
⋮----
existing = existing_by_url.get(url_key)
⋮----
url_key = self._normalized_url(repo.url)
⋮----
repos: List[ProjectPath] = list(project.repos)
repo_index: Dict[str, int] = {}
⋮----
url_key = self._normalized_url(candidate.url)
⋮----
remove_keys = set()
⋮----
repos = [
⋮----
provider_cfg = self._app_config.providers.github
⋮----
session = requests.Session()
⋮----
scope_kind = "orgs" if spec.org else "users"
scope_value = spec.org if spec.org else spec.user
endpoint = f"{provider_cfg.base_url}/{scope_kind}/{scope_value}/repos"
⋮----
discovered: List[DiscoveredRepo] = []
page = 1
⋮----
response = session.get(
⋮----
items = response.json()
⋮----
candidate = DiscoveredRepo(
⋮----
provider_cfg = self._app_config.providers.gitlab
⋮----
group_ref = requests.utils.quote(spec.group or "", safe="")
endpoint = f"{provider_cfg.base_url}/groups/{group_ref}/projects"
⋮----
def _include_candidate(self, spec: SourceSpec, candidate: DiscoveredRepo) -> bool
⋮----
def _to_project_path(self, repo: DiscoveredRepo) -> ProjectPath
⋮----
def _needs_update(self, current: ProjectPath, incoming: ProjectPath) -> bool
⋮----
tracked_fields: List[Tuple[Optional[str], Optional[str]]] = [
⋮----
def _normalized_url(self, url: Optional[object]) -> str
````

## File: src/metagit/core/skills/__init__.py
````python
#!/usr/bin/env python
"""
Skills installation helpers.
"""
⋮----
__all__ = [
````

## File: src/metagit/core/web/ops_handler.py
````python
#!/usr/bin/env python
"""HTTP handlers for workspace ops routes (health, prune, sync)."""
⋮----
JsonResponder = Callable[[int, dict[str, Any]], None]
⋮----
_SYNC_JOB_PATH = re.compile(
_SYNC_EVENTS_PATH = re.compile(
⋮----
_JOB_STORE = SyncJobStore()
⋮----
class OpsWebHandler
⋮----
"""Route workspace health, prune, and sync operations for the web HTTP API."""
⋮----
"""Dispatch JSON ops routes; return True when handled."""
parsed_path = path if path.startswith("/") else f"/{path}"
⋮----
status_match = _SYNC_JOB_PATH.match(parsed_path)
⋮----
def sync_events_job_id(self, method: str, path: str) -> str | None
⋮----
"""Return job id when path is a sync SSE events route."""
⋮----
events_match = _SYNC_EVENTS_PATH.match(parsed_path)
⋮----
def stream_sync_events(self, job_id: str, stream: BinaryIO) -> None
⋮----
"""Write server-sent events for a sync job until it finishes."""
⋮----
def _post_health(self, body: bytes, respond: JsonResponder) -> None
⋮----
config = self._load_metagit(respond)
⋮----
app_config = self._load_appconfig(respond)
⋮----
payload = self._parse_body(body, respond, required=False) or {}
project_raw = payload.get("project")
project_name = str(project_raw).strip() if project_raw else None
⋮----
project_name = None
dedupe = (
result = self._health.check(
⋮----
def _get_graph(self, query: str, respond: JsonResponder) -> None
⋮----
params = parse_qs(query.lstrip("?"))
include_inferred = (
include_structure = (
view = self._graph.build_view(
⋮----
def _post_prune_preview(self, body: bytes, respond: JsonResponder) -> None
⋮----
payload = self._parse_body(body, respond, required=True)
⋮----
project = str(payload.get("project", "")).strip()
⋮----
include_hidden = bool(payload.get("include_hidden", False))
ignore_hidden = (
⋮----
project_manager = project_manager_from_app(
⋮----
candidates = project_manager.list_unmanaged_sync_directories(
⋮----
def _post_prune(self, body: bytes, respond: JsonResponder) -> None
⋮----
raw_paths = payload.get("paths")
⋮----
dry_run = bool(payload.get("dry_run", False))
force = bool(payload.get("force", False))
⋮----
project_sync = (Path(self._workspace_root) / project).resolve()
resolved_paths: list[Path] = []
⋮----
candidate = Path(str(item)).expanduser()
resolved = (
⋮----
removed: list[str] = []
errors: list[dict[str, str]] = []
⋮----
def _post_sync(self, body: bytes, respond: JsonResponder) -> None
⋮----
request = SyncJobRequest.model_validate(payload)
⋮----
job_id = self._job_store.create_job()
thread = threading.Thread(
⋮----
status = self._job_store.get(job_id)
⋮----
def _get_sync_status(self, job_id: str, respond: JsonResponder) -> None
⋮----
def _stream_sync_events(self, job_id: str, stream: BinaryIO) -> None
⋮----
events = self._job_store.drain_events(job_id)
⋮----
payload = json.dumps(event, separators=(",", ":"))
⋮----
payload = json.dumps(
⋮----
rows = self._index.build_index(
⋮----
payload = self._sync.sync_many(
⋮----
error = str(payload.get("error", "sync failed"))
⋮----
summary = payload.get("summary")
results = payload.get("results")
⋮----
def _load_metagit(self, respond: JsonResponder) -> MetagitConfig | None
⋮----
manager = MetagitConfigManager(self._config_path)
loaded = manager.load_config()
⋮----
def _load_appconfig(self, respond: JsonResponder) -> AppConfig | None
⋮----
loaded = load_appconfig(self._appconfig_path)
⋮----
parsed = json.loads(body.decode("utf-8"))
````

## File: src/metagit/core/workspace/catalog_models.py
````python
#!/usr/bin/env python
"""
Pydantic models for workspace catalog list and mutation results.
"""
⋮----
class CatalogError(BaseModel)
⋮----
"""Structured catalog operation error."""
⋮----
kind: str
message: str
⋮----
class CatalogResult(BaseModel)
⋮----
"""Uniform envelope for agentic CLI, MCP, and API responses."""
⋮----
ok: bool = True
error: Optional[CatalogError] = None
data: Optional[dict[str, Any]] = None
⋮----
class WorkspaceSummary(BaseModel)
⋮----
"""Workspace section of a manifest with project roll-up."""
⋮----
definition_path: str
workspace_root: str
file_name: str
file_description: Optional[str] = None
file_agent_instructions: Optional[str] = None
workspace: Optional[Workspace] = None
project_count: int = 0
repo_count: int = 0
⋮----
class ProjectListEntry(BaseModel)
⋮----
"""Project row for list operations."""
⋮----
name: str
description: Optional[str] = None
agent_instructions: Optional[str] = None
dedupe_enabled: Optional[bool] = Field(
⋮----
class RepoListEntry(BaseModel)
⋮----
"""Repository row for list operations."""
⋮----
project_name: str
repo: ProjectPath
configured_path: Optional[str] = None
repo_path: Optional[str] = None
exists: Optional[bool] = None
status: Optional[str] = None
⋮----
class CatalogMutationResult(BaseModel)
⋮----
"""Result of add/remove mutations."""
⋮----
entity: Literal["project", "repo"] = "project"
operation: Literal["add", "remove"] = "add"
project_name: str = ""
repo_name: Optional[str] = None
config_path: str = ""
````

## File: src/metagit/core/workspace/context_models.py
````python
#!/usr/bin/env python
"""
Pydantic models for workspace project context and snapshot persistence.
"""
⋮----
_ENV_KEY_PATTERN = re.compile(r"^[A-Z][A-Z0-9_]{0,63}$")
_SECRET_VALUE_PATTERNS = (
_MAX_AGENT_NOTES = 4096
_MAX_RECENT_REPOS = 10
⋮----
def utc_now_iso() -> str
⋮----
"""Return current UTC timestamp in ISO8601 format."""
⋮----
def validate_env_key(key: str) -> str
⋮----
"""Validate environment variable key naming."""
⋮----
def validate_env_value(value: str) -> str
⋮----
"""Reject values that look like secrets."""
⋮----
class WorkspaceSessionMeta(BaseModel)
⋮----
"""Workspace-level session metadata."""
⋮----
active_project: Optional[str] = None
last_switch_at: Optional[str] = None
last_snapshot_id: Optional[str] = None
⋮----
class ProjectSession(BaseModel)
⋮----
"""Per-project persisted session state."""
⋮----
project_name: str
updated_at: str = Field(default_factory=utc_now_iso)
recent_repos: list[str] = Field(default_factory=list)
primary_repo_path: Optional[str] = None
agent_notes: Optional[str] = None
env_overrides: dict[str, str] = Field(default_factory=dict)
⋮----
@field_validator("agent_notes")
@classmethod
    def validate_agent_notes(cls, value: Optional[str]) -> Optional[str]
⋮----
"""Bound agent notes length."""
⋮----
@field_validator("recent_repos")
@classmethod
    def validate_recent_repos(cls, value: list[str]) -> list[str]
⋮----
"""Cap recent repo list length."""
⋮----
@field_validator("env_overrides")
@classmethod
    def validate_env_overrides(cls, value: dict[str, str]) -> dict[str, str]
⋮----
"""Validate non-secret environment overrides."""
validated: dict[str, str] = {}
⋮----
class ProjectRepoContext(BaseModel)
⋮----
"""Repository row included in a project context bundle."""
⋮----
repo_name: str
repo_path: str
configured_path: Optional[str] = None
exists: bool = False
branch: Optional[str] = None
dirty: Optional[bool] = None
tags: dict[str, str] = Field(default_factory=dict)
agent_instructions: Optional[str] = None
inspect_error: Optional[str] = None
⋮----
class ProjectContextEnv(BaseModel)
⋮----
"""Environment exports and hints for agents."""
⋮----
export: dict[str, str] = Field(default_factory=dict)
hints: list[str] = Field(default_factory=list)
⋮----
class ProjectContextSession(BaseModel)
⋮----
"""Session slice returned with a context bundle."""
⋮----
restored: bool = False
⋮----
class ProjectContextBundle(BaseModel)
⋮----
"""Result of switching or showing project context."""
⋮----
ok: bool = True
error: Optional[str] = None
project_name: str = ""
workspace_root: str = ""
project_description: Optional[str] = None
⋮----
instruction_layers: list[AgentInstructionLayer] = Field(default_factory=list)
effective_agent_instructions: str = ""
focus_repo_name: Optional[str] = None
repos: list[ProjectRepoContext] = Field(default_factory=list)
env: ProjectContextEnv = Field(default_factory=ProjectContextEnv)
session: ProjectContextSession = Field(default_factory=ProjectContextSession)
suggested_cwd: Optional[str] = None
inspect_truncated: bool = False
⋮----
class SnapshotRepoState(BaseModel)
⋮----
"""Git state for one repository in a snapshot."""
⋮----
dirty: bool = False
ahead: Optional[int] = None
behind: Optional[int] = None
uncommitted_count: Optional[int] = None
⋮----
class WorkspaceSnapshot(BaseModel)
⋮----
"""Immutable workspace state manifest."""
⋮----
snapshot_id: str
created_at: str = Field(default_factory=utc_now_iso)
⋮----
label: Optional[str] = None
repos: list[SnapshotRepoState] = Field(default_factory=list)
env_key_names: list[str] = Field(default_factory=list)
session_ref: Optional[str] = None
⋮----
class WorkspaceSnapshotRestoreResult(BaseModel)
⋮----
"""Result of restoring a workspace snapshot."""
⋮----
snapshot_id: str = ""
context: Optional[ProjectContextBundle] = None
notes: list[str] = Field(default_factory=list)
````

## File: src/metagit/core/workspace/dependency_models.py
````python
#!/usr/bin/env python
"""
Pydantic models for cross-project dependency graph results.
"""
⋮----
DependencyEdgeType = Literal[
⋮----
class DependencyNode(BaseModel)
⋮----
"""Node in a workspace dependency graph."""
⋮----
id: str
kind: Literal["project", "repo"]
label: str
project_name: Optional[str] = None
repo_path: Optional[str] = None
gitnexus_indexed: Optional[bool] = None
gitnexus_status: Optional[str] = None
⋮----
class DependencyEdge(BaseModel)
⋮----
"""Directed dependency edge with evidence."""
⋮----
from_id: str
to_id: str
type: DependencyEdgeType
evidence: list[str] = Field(default_factory=list)
⋮----
class ImpactSummary(BaseModel)
⋮----
"""High-level impact assessment for dependency exploration."""
⋮----
risk: Literal["low", "medium", "high"] = "low"
affected_projects: list[str] = Field(default_factory=list)
affected_repos: list[str] = Field(default_factory=list)
edge_count: int = 0
notes: list[str] = Field(default_factory=list)
⋮----
class CrossProjectDependencyResult(BaseModel)
⋮----
"""Result of cross-project dependency mapping."""
⋮----
ok: bool = True
error: Optional[str] = None
source_project: str = ""
dependency_types: list[str] = Field(default_factory=list)
depth: int = 1
graph_status: dict[str, str] = Field(default_factory=dict)
nodes: list[DependencyNode] = Field(default_factory=list)
edges: list[DependencyEdge] = Field(default_factory=list)
impact_summary: ImpactSummary = Field(default_factory=ImpactSummary)
````

## File: src/metagit/data/skills/metagit-control-center/scripts/control-cycle.zsh
````zsh
#!/usr/bin/env zsh
set -euo pipefail

ROOT="${1:-$PWD}"
QUERY="${2:-}"
PRESET="${3:-infra}"

"$(dirname "$0")/../../metagit-gating/scripts/gate-status.zsh" "$ROOT"

if [[ -n "$QUERY" ]]; then
  "$(dirname "$0")/../../metagit-upstream-scan/scripts/upstream-scan.zsh" "$ROOT" "$QUERY" "$PRESET" 20
else
  echo "status=ok\tmessage=no-query-provided"
fi
````

## File: src/metagit/data/skills/metagit-control-center/SKILL.md
````markdown
---
name: metagit-control-center
description: Use when running metagit as an MCP control center for multi-repo awareness, guarded sync, and operational knowledge across ongoing agent tasks.
---

# Metagit Control Center Skill

Use this skill when an agent should actively coordinate repository context and task execution across a workspace.

## Purpose

Provide a repeatable control-center workflow where Metagit MCP guides awareness, synchronization, and operational continuity over multiple related repositories.

## Local Script Wrapper (Use First)

Use this token-efficient wrapper for control-center cycles:
- `./scripts/control-cycle.zsh [root_path] ["query"] [preset]`

Wrapper behavior:
- runs gating status first
- optionally runs upstream discovery for blocker queries
- emits compact, machine-readable lines

## Core Workflows

### 1) Session Initialization
- Validate active workspace gate.
- Read `metagit://workspace/config` and `metagit://workspace/repos/status`.
- Call `metagit_project_context_switch` when the objective is tied to a workspace project.
- Run `metagit_workspace_health_check` or read `metagit://workspace/health` for maintenance signals.
- Identify stale repos and unresolved blockers from prior activity.

### 2) Active Task Support
- For each coding objective, map impacted repos.
- Use workspace search and upstream hints before broad exploration.
- Sync only repos that are required by the active objective.

### 3) Guarded Synchronization
- Default to `fetch` for visibility.
- Use `pull` or `clone` only with explicit permission and rationale.
- Track sync outcomes in operations log resource.

### 4) Operational Memory
- Before switching projects: `metagit_session_update` (notes + recent repos), optional `metagit_workspace_state_snapshot`.
- After returning: `metagit_workspace_state_restore` when a snapshot was taken (metadata only; git tree is unchanged).

Maintain bounded local records of:
- sync actions
- issue signatures searched
- candidate upstream repos identified
- unresolved dependencies and follow-ups

## Decision Guidelines

- Use metagit search first when blocker appears external to current repo.
- Prefer deterministic evidence over speculative jumps.
- Keep operations minimal and auditable.

## Output Contract

For each control-center cycle, provide:
- current objective
- repositories examined
- actions taken (or intentionally deferred)
- next recommended step

## Safety Rules

- Never mutate repositories without explicit authorization.
- Never broaden scope beyond configured workspace boundaries.
- Always preserve a clear audit trail of control actions.
````

## File: src/metagit/data/skills/metagit-gitnexus/SKILL.md
````markdown
---
name: metagit-gitnexus
description: Run gitnexus analysis for a target workspace and selected project repositories before graph-dependent tasks. Use when index staleness is detected or cross-repo graph results are needed.
---

# Running GitNexus Analysis

Use this skill whenever GitNexus context is stale or missing for target repositories.

## Local Wrapper (Use First)

- `zsh ./skills/metagit-gitnexus/scripts/analyze-targets.zsh <workspace_root> [project_name]`

## Workflow

1. Analyze the current repository where the command is run.
2. Resolve target project repositories from `.metagit.yml`.
3. Run `npx gitnexus analyze` in each local repository path found.
4. Report per-repository success/failure and next actions.

## Manual workspace graph (`.metagit.yml` → Cypher)

Export manifest `graph.relationships` for GitNexus overlay ingest:

```bash
metagit config graph export -c .metagit.yml --format tool-calls
# or MCP: metagit_export_workspace_graph_cypher
```

Run returned `gitnexus_cypher` tool calls against the umbrella repo index (`--gitnexus-repo` when names differ). Schema DDL (`MetagitEntity` / `MetagitLink`) runs once per index.

## Output Contract

Return:
- analyzed repositories
- failures and reasons
- whether graph queries are safe to run now

## Safety

- Skip repositories that do not exist locally.
- Do not mutate repo content; analysis should be read-only indexing.
````

## File: src/metagit/data/skills/metagit-projects/SKILL.md
````markdown
---
name: metagit-projects
description: Ongoing workspace and project management for OpenClaw and Hermes agents. Use when starting work, organizing repos, or before creating a new project folder so existing metagit projects are reused instead of duplicated.
---

# Ongoing Project Management

Use this skill when the user starts new work, reorganizes repositories, or asks you to create a project folder. Metagit is the source of truth for what already exists in the workspace.

## Concepts

Metagit uses a three-level hierarchy (see project terminology docs):

| Level | Meaning |
|-------|---------|
| **Workspace** | Root folder where projects are synced (from app config `workspace.path`, often `./.metagit/`). Holds many projects. |
| **Project** | Named group of one or more Git repositories. Multi-repo products are one project; unrelated repos can also share a workspace under different project names. |
| **Repo** | A single Git repository entry under `workspace.projects[].repos` in `.metagit.yml`. |

A **project** is not always “one product.” It is whatever grouping helps the user and agents reason about related (or intentionally grouped) repositories. A workspace may contain unrelated projects side by side (for example `default`, `client-a`, `experiments`).

The umbrella `.metagit.yml` (workspace definition, often `kind: umbrella`) lives in a coordinating repository or central config checkout. Application repos may have their own `.metagit.yml` for metadata mode.

## Mandatory: check before creating folders

**Never** create a new project directory or clone into the workspace until you have checked metagit for an existing match.

1. **Locate the workspace definition**
   - Prefer the user’s umbrella `.metagit.yml` if known.
   - Otherwise use `.metagit.yml` in the current repo with `--definition /path/to/.metagit.yml`.

2. **List configured projects and repo counts**
   ```bash
   metagit config info --config-path /path/to/.metagit.yml
   metagit project list --config /path/to/.metagit.yml --project default
   ```
   Repeat `--project` for each project name returned by `config info`.

3. **Search managed repos by name, URL fragment, or tag**
   ```bash
   metagit search "<proposed-name-or-url>" --definition /path/to/.metagit.yml
   metagit search "<name>" --definition /path/to/.metagit.yml --json
   ```

4. **Inspect on disk** (workspace path from app config, default `./.metagit/`)
   - Expected layout: `{workspace.path}/{project_name}/{repo_name}/`
   - If the directory already exists, **reuse it**; do not create a parallel tree.

5. **Decide**
   - **Match found** → use existing project/repo; run `metagit project sync` only if the user wants checkouts refreshed.
   - **No match** → proceed with registration steps below (still add to workspace; do not leave orphan folders).

## Registering new work in the workspace

### New repository in an existing project

From the directory containing the workspace `.metagit.yml` (or pass `--config`):

```bash
metagit project repo add --project <project_name> --prompt
# or non-interactive:
metagit project repo add --project <project_name> --name <repo> --url <git-url>
metagit config validate --config-path .metagit.yml
metagit project sync --project <project_name>
```

In the new application repo (if applicable):

```bash
cd /path/to/new/repo
metagit init
metagit detect repo --force   # optional: enrich .metagit.yml
```

### New project group (new `workspace.projects[]` entry)

There is no separate `project create` CLI today. Add a project block to `.metagit.yml`:

```yaml
workspace:
  projects:
    - name: my-new-project
      description: Short purpose for agents and humans
      repos: []
```

Then validate, add repos, and sync:

```bash
metagit config validate --config-path .metagit.yml
metagit project repo add --project my-new-project --prompt
metagit project sync --project my-new-project
```

Choose a **distinct project name**; avoid duplicating an existing `workspace.projects[].name`.

### New umbrella workspace

When bootstrapping a workspace coordinator repo:

```bash
metagit init --kind umbrella
metagit project repo add --project default --prompt
metagit project sync
```

## Ongoing session habits

At the start of sustained work:

1. Run **metagit-workspace-scope** (or `metagit mcp serve --status-once` when MCP is available).
2. Confirm **active project** matches the user’s intent (`metagit workspace select --project <name>` when switching).
3. Use **`metagit search`** before assuming a repo is missing or lives elsewhere.
4. For multi-repo tasks, prefer **metagit-control-center** or **metagit-multi-repo** over ad-hoc cloning.

When the user names a target folder:

- Resolve it against managed config first.
- If unmanaged but present on disk under the project sync folder, report it and offer to add via `metagit project repo add` rather than recreating.

## OpenClaw and Hermes setup

Install bundled skills (including this one) for agent hosts:

```bash
metagit skills list
metagit skills install --scope user --target openclaw --target hermes
metagit mcp install --scope user --target openclaw --target hermes
```

Use `--scope project` when installing into a specific umbrella repository checkout.

## Output contract

After project-management actions, report:

- workspace definition path used
- whether the target was **existing** or **newly registered**
- project name and repo name(s) affected
- sync status if `project sync` was run
- recommended next command (`workspace select`, `project select`, or `detect`)

## Rename and move (layout)

Use layout commands when reorganizing project or repo names. **Always dry-run first.**

```bash
metagit workspace project rename old-name new-name --dry-run --json
metagit workspace repo rename -p <project> old-repo new-repo --dry-run --json
metagit workspace repo move -p <from> -n <repo> --to-project <to> --dry-run --json
```

MCP: `metagit_workspace_project_rename`, `metagit_workspace_repo_rename`, `metagit_workspace_repo_move`.

- Renames update `.metagit.yml` and synced folders under `workspace.path` when they exist.
- Moves relocate repo mounts between projects (dedupe mode relinks symlinks to canonical checkouts).
- Use `--manifest-only` to change the manifest without touching disk.
- Run `metagit config validate` after any layout change.

## Safety

- Do not clone, delete, or overwrite sync directories without explicit user approval.
- Do not edit `.metagit.yml` without validating afterward (`metagit config validate`).
- Prefer reusing configured repos over creating duplicate checkouts.
- Keep unrelated experiments in separate `workspace.projects` entries when the user wants clear boundaries.
````

## File: src/metagit/data/skills/metagit-repo-impact/SKILL.md
````markdown
---
name: metagit-repo-impact
description: Plan repository change impact before edits by combining metagit workspace context and graph-based dependency analysis. Use when a change may affect multiple repositories.
---

# Planning Repo Impact

Use this skill before risky or cross-repo modifications.

## Workflow

1. Identify target symbols/files for change.
2. Use metagit workspace context to bound repository scope.
3. Use graph impact tooling to estimate blast radius.
4. Produce a change plan with test and rollback focus.

## Commands

- `metagit workspace select --project <name>`
- MCP `metagit_cross_project_dependencies` with `source_project` and `dependency_types` before large cross-project edits
- MCP `metagit_project_context_switch` to bound scope to one workspace project
- `npx gitnexus analyze` on affected repos when `graph_status` is `stale` or `missing`
- `npx gitnexus query` / `gitnexus impact` for symbol-level analysis after indexes are fresh

## Output Contract

Return:
- impacted repositories and interfaces
- highest-risk change points
- minimum validation plan

## Safety

- Do not execute mutating steps in this planning phase.
````

## File: src/metagit/data/skills/metagit-workspace-scope/SKILL.md
````markdown
---
name: metagit-workspace-scope
description: Discover active metagit workspace scope, project boundaries, and repository status. Use when an agent starts work in a multi-repo workspace and needs fast, scoped context before editing.
---

# Discovering Workspace Scope

Use this skill at session start for workspace-aware tasks.

## Workflow

1. Run workspace gate and status checks first.
2. Read configured projects and repository status from metagit resources/tools.
3. Build a compact map of active project names, repo names, and sync state.
4. Report a bounded scope for the current objective.

## Commands

- `zsh ./skills/metagit-gating/scripts/gate-status.zsh [root_path]`
- `metagit workspace select --project <name>` (interactive repo picker)
- MCP `metagit_project_context_switch` with `project_name` (structured context + session restore)
- MCP `metagit_workspace_state_snapshot` before leaving a project for a long switch

## Output Contract

Return:
- active/inactive workspace state and reason
- candidate project to operate on
- top repositories to inspect first

## Safety

- Keep operations read-only in this step.
- Do not sync or mutate repositories without explicit request.
````

## File: src/metagit/data/skills/metagit-workspace-sync/SKILL.md
````markdown
---
name: metagit-workspace-sync
description: Sync workspace repositories safely using metagit with scoped fetch, pull, or clone actions. Use when repository content must be refreshed for implementation.
---

# Syncing Workspace Repositories

Use this skill when repository state must be updated.

## Workflow

1. Confirm active workspace and target project.
2. Start with read-only status and fetch where possible.
3. Use pull/clone only when required for the current objective.
4. Summarize which repositories changed and what remains stale.

## Commands

- `metagit project sync --project <name>`
- MCP `metagit_workspace_sync` for batch fetch/pull/clone (`repos`, `only_if`, `dry_run`, `max_parallel`)
- MCP `metagit_repo_sync` for a single repository (requires `allow_mutation` for pull/clone)
- `zsh ./skills/metagit-control-center/scripts/control-cycle.zsh [root_path] ["query"] [preset]`

## Output Contract

Return:
- repositories synced
- sync mode used (fetch/pull/clone)
- failures and retry guidance

## Safety

- Limit sync to repos defined in `.metagit.yml`.
- Avoid broad synchronization if only a subset is needed.
````

## File: src/metagit/data/config-example-overrides.yml
````yaml
# Merged last when generating docs/reference/metagit-config.full-example.yml
name: example-umbrella
description: Example umbrella workspace for documentation (not for production).
kind: umbrella
agent_instructions: |
  Hermes controller example: search the workspace before creating folders or clones.
  Use metagit_workspace_status and metagit_workspace_health_check at session start.
url: https://github.com/example-org/hermes-control-plane.git
workspace:
  description: Portfolio of git repos and local publish paths managed by metagit.
  agent_instructions: |
    Validate after manifest edits (metagit config validate). Default sync is fetch-only;
  projects:
    - name: portfolio
      description: Git-backed applications and services.
      agent_instructions: |
        Delegate single-repo work to subagents with effective_agent_instructions.
      repos:
        - name: example-service
          description: Sample git-backed service.
          url: https://github.com/example-org/example-service.git
          sync: true
          agent_instructions: |
            Run tests before opening PRs; keep tags updated in this manifest.
    - name: local
      description: Local-only paths for static sites (no git remotes).
      dedupe:
        enabled: false
      agent_instructions: |
        Do not git clone or pull repos in this project; paths are symlinks to user dirs.
        Dedupe is disabled for this project (overrides workspace.dedupe in metagit.config.yaml).
      repos:
        - name: example-site
          description: Sample static site published from a local folder.
          path: ~/Sites/example-site
          sync: true
          kind: website
          agent_instructions: |
            Document build and publish steps here; update path if the site moves.
````

## File: tests/cli/commands/test_init.py
````python
#!/usr/bin/env python
"""
CLI tests for metagit init command.
"""
⋮----
def test_resolve_project_metadata_non_git_directory(tmp_path: Path) -> None
⋮----
project_dir = tmp_path / "my-project"
⋮----
def test_resolve_project_metadata_git_repo_without_remote(tmp_path: Path) -> None
⋮----
project_dir = tmp_path / "local-repo"
⋮----
def test_init_succeeds_outside_git_repository() -> None
⋮----
runner = CliRunner()
⋮----
result = runner.invoke(
⋮----
config_path = Path(".metagit.yml")
⋮----
loaded = yaml.safe_load(config_path.read_text(encoding="utf-8"))
⋮----
def test_init_list_templates() -> None
⋮----
result = runner.invoke(cli, ["init", "--list-templates"])
⋮----
def test_init_hermes_template_no_prompt() -> None
⋮----
answers = {
answers_path = Path("answers.yml")
⋮----
loaded = yaml.safe_load(Path(".metagit.yml").read_text(encoding="utf-8"))
⋮----
def test_resolve_target_dir_create(tmp_path: Path) -> None
⋮----
target = tmp_path / "new-coordinator"
resolved = resolve_target_dir(str(target), create=True)
⋮----
def test_init_writes_to_target_folder() -> None
⋮----
target = Path("coordinator")
⋮----
def test_init_target_option_overrides_positional(tmp_path: Path) -> None
⋮----
chosen = tmp_path / "chosen"
ignored = tmp_path / "ignored"
⋮----
def test_init_minimal_service_kind() -> None
````

## File: tests/cli/commands/test_mcp.py
````python
#!/usr/bin/env python
"""
CLI tests for metagit mcp commands.
"""
⋮----
def test_mcp_serve_accepts_root_option(tmp_path) -> None
⋮----
runner = CliRunner()
⋮----
result = runner.invoke(
⋮----
def test_mcp_install_project_target_updates_config() -> None
⋮----
config_data = json.loads(Path(".opencode/mcp.json").read_text(encoding="utf-8"))
````

## File: tests/cli/commands/test_project_repo.py
````python
#!/usr/bin/env python
"""CLI tests for metagit project repo prune."""
⋮----
def test_project_repo_prune_dry_run_lists_unmanaged(tmp_path: Path) -> None
⋮----
runner = CliRunner()
workspace = tmp_path / ".metagit"
platform = workspace / "platform"
⋮----
metagit_yml = tmp_path / ".metagit.yml"
⋮----
app_cfg = tmp_path / "metagit.config.yaml"
⋮----
result = runner.invoke(
⋮----
def test_project_repo_prune_force_removes_without_prompt(tmp_path: Path) -> None
⋮----
leftover = platform / "leftover"
````

## File: tests/cli/commands/test_skills.py
````python
#!/usr/bin/env python
"""
CLI tests for metagit skills commands.
"""
⋮----
def test_skills_list_displays_bundled_skills() -> None
⋮----
runner = CliRunner()
result = runner.invoke(cli, ["skills", "list"])
⋮----
def test_skills_install_project_target_creates_skill_directory() -> None
⋮----
result = runner.invoke(
⋮----
destination = Path(".opencode/skills")
⋮----
def test_skills_install_single_skill_only() -> None
⋮----
installed = [
⋮----
def test_skills_install_dry_run_does_not_write_files() -> None
⋮----
def test_skills_install_unknown_skill_fails() -> None
````

## File: tests/core/mcp/services/test_project_context.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.mcp.services.project_context
"""
⋮----
def _write_multi_project_workspace(tmp_path: Path) -> str
⋮----
alpha_repo = tmp_path / "alpha" / "repo-one"
⋮----
beta_repo = tmp_path / "beta" / "repo-two"
⋮----
def _load_config(tmp_path: Path) -> MetagitConfig
⋮----
manager = MetagitConfigManager(config_path=tmp_path / ".metagit.yml")
loaded = manager.load_config()
⋮----
def test_switch_sets_active_project_and_returns_repos(tmp_path: Path) -> None
⋮----
root = _write_multi_project_workspace(tmp_path)
config = _load_config(tmp_path)
service = ProjectContextService()
⋮----
bundle = service.switch(
⋮----
meta = SessionStore(workspace_root=root).get_workspace_meta()
⋮----
def test_switch_unknown_project_returns_error(tmp_path: Path) -> None
⋮----
def test_env_export_includes_metagit_and_config_variables(tmp_path: Path) -> None
⋮----
def test_update_session_persists_notes(tmp_path: Path) -> None
⋮----
result = service.update_session(
⋮----
session = SessionStore(workspace_root=root).get_project_session(project_name="alpha")
⋮----
def test_env_export_skips_sensitive_variable_ref(tmp_path: Path) -> None
⋮----
config = MetagitConfig(
````

## File: tests/core/workspace/test_catalog_service.py
````python
#!/usr/bin/env python
"""Tests for workspace catalog list and mutation service."""
⋮----
def _write_manifest(tmp_path: Path, projects: list[dict] | None = None) -> tuple[Path, str]
⋮----
manifest = {
config_path = tmp_path / ".metagit.yml"
⋮----
manager = MetagitConfigManager(str(config_path))
loaded = manager.load_config()
⋮----
def test_list_projects_and_repos(tmp_path: Path) -> None
⋮----
service = WorkspaceCatalogService()
projects = service.list_projects(config)
⋮----
repos = service.list_repos(config, str(tmp_path), project_name="platform")
⋮----
workspace = service.list_workspace(config, config_path, str(tmp_path))
⋮----
def test_add_and_remove_project(tmp_path: Path) -> None
⋮----
added = service.add_project(config, config_path, name="infra")
⋮----
manager = MetagitConfigManager(config_path)
reloaded = manager.load_config()
⋮----
removed = service.remove_project(reloaded, config_path, name="infra")
⋮----
def test_add_and_remove_repo(tmp_path: Path) -> None
⋮----
built = service.build_repo_from_fields(
⋮----
added = service.add_repo(
⋮----
removed = service.remove_repo(
⋮----
def test_add_repo_rejects_duplicate_identity(tmp_path: Path) -> None
⋮----
result = service.add_repo(
````

## File: tests/integration/test_mcp_workspace_flow.py
````python
#!/usr/bin/env python
"""
Integration tests for MCP workspace activation flow.
"""
⋮----
def test_end_to_end_workspace_activation_and_discovery(tmp_path: Path) -> None
⋮----
runner = CliRunner()
workspace_root = tmp_path / "workspace"
⋮----
inactive_result = runner.invoke(
⋮----
active_result = runner.invoke(
⋮----
runtime = MetagitMcpRuntime(root=str(workspace_root))
tools_response = runtime._handle_request(
⋮----
tool_names = [item["name"] for item in tools_response["result"]["tools"]]
````

## File: tests/test_appconfig_display.py
````python
#!/usr/bin/env python
"""Tests for appconfig show display and agent_mode."""
⋮----
def test_appconfig_show_includes_dedupe_and_agent_mode() -> None
⋮----
config = AppConfig()
payload = build_appconfig_payload(config, config_path="/tmp/metagit.config.yaml")
⋮----
def test_metagit_agent_mode_env_overrides_config(monkeypatch) -> None
⋮----
config = AppConfig(agent_mode=False)
config = AppConfig._override_from_environment(config)
⋮----
def test_render_appconfig_show_json() -> None
⋮----
rendered = render_appconfig_show(
````

## File: tests/test_project_search_service.py
````python
#!/usr/bin/env python
"""
Unit tests for ManagedRepoSearchService.
"""
⋮----
def _config(tmp_path: Path) -> MetagitConfig
⋮----
workspace_root = tmp_path / "workspace"
app_repo = workspace_root / "platform" / "abacus-app"
module_repo = workspace_root / "shared" / "abacus-module"
⋮----
def test_search_prioritizes_exact_repo_name(tmp_path: Path) -> None
⋮----
service = ManagedRepoSearchService()
result = service.search(
⋮----
def test_search_can_filter_by_tag(tmp_path: Path) -> None
⋮----
def test_search_filters_by_status_and_has_url(tmp_path: Path) -> None
⋮----
missing_repo = tmp_path / "workspace" / "platform" / "missing-app"
⋮----
config = _config(tmp_path)
⋮----
def test_search_sorts_by_project_name(tmp_path: Path) -> None
⋮----
project_names = [match.project_name for match in result.matches]
⋮----
def test_resolve_one_returns_ambiguous_match(tmp_path: Path) -> None
⋮----
resolved = service.resolve_one(
````

## File: tests/test_skills_installer.py
````python
#!/usr/bin/env python
"""
Unit tests for skills installer helpers.
"""
⋮----
def test_resolve_targets_respects_disable() -> None
⋮----
targets = resolve_targets(
⋮----
def test_autodetect_targets_project_scope(monkeypatch, tmp_path) -> None
⋮----
project_root = tmp_path / "project"
⋮----
detected = autodetect_targets(mode="skills", scope="project")
⋮----
def test_resolve_skill_names_rejects_unknown() -> None
⋮----
def test_install_skills_for_targets_single_skill(monkeypatch, tmp_path) -> None
⋮----
results = install_skills_for_targets(
destination = project_root / ".opencode" / "skills"
⋮----
installed = [p.name for p in destination.iterdir() if p.is_dir()]
⋮----
def test_install_skills_dry_run_writes_nothing(monkeypatch, tmp_path) -> None
````

## File: tests/test_workspace_index_service.py
````python
#!/usr/bin/env python
"""
Unit tests for WorkspaceIndexService managed repo rows (tags, status).
"""
⋮----
def test_build_index_synced_git_repo_with_tags_and_paths(tmp_path: Path) -> None
⋮----
workspace_root = tmp_path / "workspace"
⋮----
repo_dir = workspace_root / "svc-auth"
⋮----
config = MetagitConfig(
service = WorkspaceIndexService()
rows = service.build_index(config=config, workspace_root=str(workspace_root))
⋮----
row = rows[0]
⋮----
def test_build_index_url_only_repo_uses_project_mount_path(tmp_path: Path) -> None
⋮----
workspace_root = tmp_path / ".metagit"
⋮----
repo_dir = workspace_root / "platform" / "svc-auth"
⋮----
def test_build_index_missing_path_is_configured_missing(tmp_path: Path) -> None
````

## File: web/src/components/Layout.tsx
````typescript
import { NavLink, Outlet } from 'react-router-dom'
import { useThemeStore } from '../theme/useThemeStore'
import styles from './Layout.module.css'
⋮----
const navLinkClass = (
````

## File: web/src/components/SchemaTree.module.css
````css
.tree {
⋮----
.nested {
⋮----
.row {
⋮----
.row:hover {
⋮----
.rowSelected {
⋮----
.rowDisabled {
⋮----
.checkbox {
⋮----
.checkboxPlaceholder {
⋮----
.label {
⋮----
.key {
⋮----
.type {
⋮----
.required {
⋮----
.state {
⋮----
.error {
⋮----
.expandBtn {
⋮----
.expandBtn:hover {
⋮----
.listBtn {
⋮----
.listBtn:hover {
⋮----
.listBtnDanger {
⋮----
.listBtnDanger:hover {
⋮----
.count {
````

## File: web/src/components/SchemaTree.tsx
````typescript
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useCallback, useState } from 'react'
import type { ConfigOperation, SchemaFieldNode } from '../api/client'
import {
  configTreeQueryKey,
  fetchConfigTree,
  patchConfigTree,
  type ConfigTarget,
} from '../pages/configQueries'
import styles from './SchemaTree.module.css'
⋮----
interface SchemaTreeProps {
  target: ConfigTarget
  selectedPath: string | null
  onSelect: (node: SchemaFieldNode) => void
  onOperationApplied?: (op: ConfigOperation) => void
}
⋮----
function isOptionalToggleable(node: SchemaFieldNode): boolean
⋮----
function displayType(node: SchemaFieldNode): string
⋮----
function isListItemNode(node: SchemaFieldNode): boolean
⋮----
interface TreeNodesProps {
  nodes: SchemaFieldNode[]
  selectedPath: string | null
  onSelect: (node: SchemaFieldNode) => void
  onToggle: (node: SchemaFieldNode, checked: boolean) => void
  onAppend: (node: SchemaFieldNode) => void
  onRemove: (node: SchemaFieldNode) => void
  mutationPending: boolean
}
⋮----
onClick=
⋮----
event.stopPropagation()
onToggle(node, event.target.checked)
⋮----
onAppend(node)
⋮----
onRemove(node)
⋮----
setExpanded((value)
````

## File: web/src/pages/AppconfigPage.tsx
````typescript
import ConfigPage from './ConfigPage'
⋮----
export default function AppconfigPage()
````

## File: web/src/pages/ConfigPage.module.css
````css
.page {
⋮----
.header {
⋮----
.title {
⋮----
.subtitle {
⋮----
.layout {
⋮----
.layout > :last-child {
⋮----
.treePanel {
⋮----
.treeHeading {
````

## File: web/src/pages/ConfigPage.tsx
````typescript
import { useQuery } from '@tanstack/react-query'
import { useCallback, useMemo, useState } from 'react'
import type { ConfigOperation, SchemaFieldNode } from '../api/client'
import ConfigPreview from '../components/ConfigPreview'
import FieldEditor from '../components/FieldEditor'
import SchemaTree from '../components/SchemaTree'
import {
  configTreeQueryKey,
  fetchConfigTree,
  type ConfigTarget,
} from './configQueries'
import styles from './ConfigPage.module.css'
⋮----
interface ConfigPageProps {
  target: ConfigTarget
  title: string
}
⋮----
function findNodeByPath(
  root: SchemaFieldNode | undefined,
  path: string | null,
): SchemaFieldNode | null
⋮----
function mergePendingOp(
  pending: ConfigOperation[],
  op: ConfigOperation,
): ConfigOperation[]
⋮----
export default function ConfigPage(
````

## File: web/src/pages/configQueries.ts
````typescript
import {
  getAppconfigTree,
  getMetagitConfigTree,
  patchAppconfig,
  patchMetagitConfig,
  postConfigPreview,
  type ConfigOperation,
  type ConfigPreviewResponse,
  type ConfigPreviewStyle,
  type ConfigTreeResponse,
} from '../api/client'
⋮----
export type ConfigTarget = 'metagit' | 'appconfig'
⋮----
export const configTreeQueryKey = (target: ConfigTarget)
⋮----
export function fetchConfigTree(target: ConfigTarget): Promise<ConfigTreeResponse>
⋮----
export function patchConfigTree(
  target: ConfigTarget,
  ops: ConfigOperation[],
  save: boolean,
): Promise<ConfigTreeResponse>
⋮----
export function fetchConfigPreview(
  target: ConfigTarget,
  style: ConfigPreviewStyle,
  operations: ConfigOperation[],
): Promise<ConfigPreviewResponse>
````

## File: web/src/pages/MetagitConfigPage.tsx
````typescript
import ConfigPage from './ConfigPage'
⋮----
export default function MetagitConfigPage()
````

## File: web/src/pages/WorkspacePage.module.css
````css
.page {
⋮----
.header {
⋮----
.title {
⋮----
.subtitle {
⋮----
.chips {
⋮----
.chip {
⋮----
.chip strong {
⋮----
.toolbar {
⋮----
.tabs {
⋮----
.tab {
⋮----
.tab:hover {
⋮----
.tabActive {
⋮----
.search {
⋮----
.search:focus {
⋮----
.layout {
⋮----
.loading,
⋮----
.loading {
⋮----
.error {
⋮----
.graphFilters {
⋮----
.checkLabel {
⋮----
.graphPanel {
````

## File: web/src/theme/tokens.css
````css
:root {
⋮----
:root,
⋮----
[data-theme='dark'] {
⋮----
:root:not([data-theme='light']) {
````

## File: web/src/App.css
````css
/* Route-level styles live in page/component CSS modules. */
````

## File: web/src/App.tsx
````typescript
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'
import Layout from './components/Layout'
import ThemeProvider from './theme/ThemeProvider'
import AppconfigPage from './pages/AppconfigPage'
import MetagitConfigPage from './pages/MetagitConfigPage'
import WorkspacePage from './pages/WorkspacePage'
````

## File: web/src/index.css
````css
* {
⋮----
html {
⋮----
body {
⋮----
#root {
⋮----
button,
````

## File: web/src/main.tsx
````typescript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
⋮----
import App from './App.tsx'
````

## File: .metagit.example.yml
````yaml
name: metagit-cli
description: Metagit-ai multi-repo management tool
url: https://github.com/metagit-ai/metagit-cli
kind: cli
documentation:
  - https://metagit-ai.github.io/metagit-cli/
license:
  kind: Apache-2.0
  file: LICENSE
maintainers:
  - name: Zachary Loeber
    email: zloeber@gmail.com
    role: author
branch_strategy: githubflow
taskers:
  - kind: Taskfile
paths:
  - name: metagit-cli
    description: The main CLI application
    kind: cli
    path: src/metagit
    language: python
    language_version: "3.13"
    package_manager: uv
    frameworks:
      - click
      - pydantic
      - loguru
cicd:
  platform: GitHub
  pipelines:
    - name: test
      ref: .github/workflows/test.yaml
    - name: release
      ref: .github/workflows/release.yaml
    - name: docker
      ref: .github/workflows/docker.yaml
    - name: docs
      ref: .github/workflows/docs.yaml
artifacts:
  - type: docker
    definition: Dockerfile
    location: ghcr.io/metagit-ai/metagit-cli
    version_strategy: semver
  - type: python_package
    definition: pyproject.toml
    location: https://pypi.org/project/metagit-cli/
    version_strategy: semver
deployment:
  strategy: pipeline
observability:
  logging_provider: console
  monitoring_providers:
    - sentry
secrets_management:
  - github_secrets
secrets:
  - name: GITHUB_TOKEN
    kind: access_token
    ref: ${{ secrets.GITHUB_TOKEN }}
  - name: PYPI_API_TOKEN
    kind: api_key
    ref: ${{ secrets.PYPI_API_TOKEN }}
````

## File: AGENTS.md
````markdown
<!-- gitnexus:start -->
# GitNexus — Code Intelligence

This project is indexed by GitNexus as **metagit-cli** (3672 symbols, 5447 relationships, 73 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.

> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.

## Always Do

- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows.
- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`.

## Repo quality gate

- **`task qa:prepush`** from the repo root must run successfully before you report implementation work as complete whenever this session modified files in the tree (see `scripts/prepush-gate.py`). Fix failures and re-run. Skip only when the session made no edits or the user waived the gate in this thread.

## Never Do

- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph.
- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope.

## Resources

| Resource | Use for |
|----------|---------|
| `gitnexus://repo/metagit-cli/context` | Codebase overview, check index freshness |
| `gitnexus://repo/metagit-cli/clusters` | All functional areas |
| `gitnexus://repo/metagit-cli/processes` | All execution flows |
| `gitnexus://repo/metagit-cli/process/{name}` | Step-by-step execution trace |

## CLI

| Task | Read this skill file |
|------|---------------------|
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` |
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` |
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` |
| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
| Work in the Examples area (87 symbols) | `.claude/skills/generated/examples/SKILL.md` |
| Work in the Mcp area (40 symbols) | `.claude/skills/generated/mcp/SKILL.md` |
| Work in the Commands area (38 symbols) | `.claude/skills/generated/commands/SKILL.md` |
| Work in the Record area (26 symbols) | `.claude/skills/generated/record/SKILL.md` |
| Work in the Services area (20 symbols) | `.claude/skills/generated/services/SKILL.md` |
| Work in the Providers area (16 symbols) | `.claude/skills/generated/providers/SKILL.md` |
| Work in the Cluster_45 area (11 symbols) | `.claude/skills/generated/cluster-45/SKILL.md` |
| Work in the Cluster_53 area (11 symbols) | `.claude/skills/generated/cluster-53/SKILL.md` |
| Work in the Cluster_41 area (8 symbols) | `.claude/skills/generated/cluster-41/SKILL.md` |
| Work in the Detect area (8 symbols) | `.claude/skills/generated/detect/SKILL.md` |
| Work in the Gitcache area (8 symbols) | `.claude/skills/generated/gitcache/SKILL.md` |
| Work in the Detectors area (7 symbols) | `.claude/skills/generated/detectors/SKILL.md` |
| Work in the Config area (6 symbols) | `.claude/skills/generated/config/SKILL.md` |
| Work in the Project area (6 symbols) | `.claude/skills/generated/project/SKILL.md` |
| Work in the Tests area (6 symbols) | `.claude/skills/generated/tests/SKILL.md` |
| Work in the Appconfig area (5 symbols) | `.claude/skills/generated/appconfig/SKILL.md` |
| Work in the Cluster_58 area (5 symbols) | `.claude/skills/generated/cluster-58/SKILL.md` |
| Work in the Cli area (4 symbols) | `.claude/skills/generated/cli/SKILL.md` |
| Work in the Cluster_52 area (4 symbols) | `.claude/skills/generated/cluster-52/SKILL.md` |

<!-- gitnexus:end -->
````

## File: .cursor/skills/metagit-release-audit/SKILL.md
````markdown
---
name: metagit-release-audit
description: Mandatory before calling work complete when the session changed repo files. Runs format, lint, tests, integration tests, context-aware pip-audit/bandit, and optional gitleaks via task qa:prepush. Use before push, release, or hand-off.
---

# Auditing Release Readiness

Use this skill **whenever** your session added or edited tracked files in this repository and you are about to hand off or say the task is done—not only “release” workflows. Read-only Q&A with no writes can skip it.

## Workflow

1. Run the pre-push quality gate.
2. Capture failing stage logs and iterate fixes.
3. Re-run until all required checks pass.
4. Return a short readiness summary.

## Commands

- `task qa:prepush`
- `task qa:prepush:loop -- 3` (optional bounded retry loop)

## Output Contract

Return:
- pass/fail status by stage (including `security_sync` / `security_audit` / `security_bandit` when triggered)
- unresolved blockers (if any)
- push readiness recommendation

Security in the gate is context-aware: lockfile changes run `uv sync --frozen --all-extras` + `pip-audit` + `bandit`; `src/` changes run `pip-audit` + `bandit`; docs-only diffs skip security. Use `task security:scan` for a full manual run.

## Safety

- Do not claim readiness unless checks are actually green.
````

## File: .mex/context/mcp-runtime.md
````markdown
---
name: mcp-runtime
description: Metagit MCP runtime architecture, tool/resource dispatch, gating, and sampling behavior.
triggers:
  - "mcp runtime"
  - "tools/list"
  - "tools/call"
  - "resources/read"
  - "sampling"
  - "stdio framing"
edges:
  - target: context/architecture.md
    condition: when MCP changes affect broader CLI/core system flow
  - target: context/conventions.md
    condition: when implementing or reviewing runtime/service code patterns
  - target: context/stack.md
    condition: when protocol/library/runtime version constraints matter
  - target: patterns/add-mcp-tool.md
    condition: when adding a new MCP tool or changing tool schemas
  - target: patterns/debug-mcp-runtime.md
    condition: when MCP message loop, framing, or tool dispatch fails
last_updated: 2026-05-15
---

# MCP Runtime

## Overview
- Entry command is `metagit mcp serve` from `src/metagit/cli/commands/mcp.py`.
- Runtime implementation lives in `src/metagit/core/mcp/runtime.py`.
- Runtime uses stdio JSON-RPC with MCP framing (`Content-Length` header + body).
- Gate state is resolved from workspace root + `.metagit.yml` validation before exposing tools.

## Active Runtime Services
- `WorkspaceRootResolver` + `WorkspaceGate` + `ToolRegistry` for state-aware tool visibility.
- Workspace services: index, path-based workspace search (`metagit_workspace_search`), GitNexus semantic query fan-out (`metagit_workspace_semantic_search`), upstream hints.
- `ManagedRepoSearchService` for `metagit_repo_search` (managed `.metagit.yml` repos only, with tags/status).
- Repo ops service for inspect/sync with mutation guardrails.
- Project context service (`metagit_project_context_switch`, `metagit_session_update`) with session store under `.metagit/sessions/`.
- Workspace snapshot service (`metagit_workspace_state_snapshot`, `metagit_workspace_state_restore`) under `.metagit/snapshots/`.
- Workspace search uses ripgrep when `rg` is on PATH; `metagit_workspace_sync` batches guarded fetch/pull/clone across index rows.
- `metagit_cross_project_dependencies` combines config-declared edges, manifest import hints, shared URL/path detection, and GitNexus per-repo index status.
- Phase 3: `metagit_workspace_health_check` (branch-age / integration staleness when enabled), `metagit_workspace_discover`, `metagit_project_template_apply`, resources `metagit://workspace/health` and `metagit://workspace/context`.
- Resource publisher for config/repo-status/ops-log resources.
- Bootstrap sampling service with fallback and optional client sampling flow.

## Protocol Notes
- Supported methods: `initialize`, `tools/list`, `tools/call`, `resources/list`, `resources/read`, `ping`.
- Invalid tool arguments normalize to JSON-RPC error `-32602` with `data.kind=invalid_arguments`.
- Runtime tracks client sampling capability from initialize params and can call `sampling/createMessage`.

## Guardrails
- Inactive gate state only exposes safe baseline tools.
- Mutating sync operations require explicit opt-in arguments/flags.
- Tool schemas are explicitly published in `tools/list` and should stay aligned with dispatcher validation.
````

## File: docs/reference/metagit-config.full-example.yml
````yaml
# NON-PRODUCTION EXEMPLAR — generated by `metagit config example`.
# Field descriptions come from Pydantic models; edit before use.

# Project name
name: example-umbrella
...
# Project description
description: Example umbrella workspace for documentation (not for production).
# Optional instructions for controller agents; applies with or without a workspace block
agent_instructions: 'Hermes controller example: search the workspace before creating folders or clones.\nUse
  metagit_workspace_status and metagit_workspace_health_check at session start.\n'
# Project URL
url: https://github.com/example-org/hermes-control-plane.git
...
# Project kind. This is used to determine the type of project and the best way to manage it.
kind: umbrella
# Documentation sources: bare strings (path or URL) or objects with kind, path/url, tags, and metadata for knowledge-graph ingestion
documentation:
  -
    # Source type (markdown, web, confluence, sharepoint, wiki, api, other)
    kind: example-value
...
    # Repo-relative or absolute path to a documentation file
    path: example-value
...
    # Remote documentation URL
    url: https://example.com/org/repo
...
    # Human-readable title
    title: example-value
...
    # Short summary for indexing and graph nodes
    description: Example text for documentation; replace before production use.
    # Flat metadata tags for filtering and graph edges
    tags:
      example: documentation
    # Extensible key-value payload for downstream graph ingestors
    metadata:
      source: metagit-config-example
...
# Manual cross-repo relationships and graph metadata for exports and GitNexus-style dependency maps
graph:
  # Manually entered cross-repo or cross-project edges
  relationships:
    -
      # Stable identifier for exports and graph merges
      id: example-value
...
      # Relationship source
      from_endpoint:
        # Workspace project name
        project: example-value
...
        # Repository name under the project
        repo: example-value
...
        # Optional file or directory path within the repo
        path: example-value
...
      # Relationship target
      to:
        # Workspace project name
        project: example-value
...
        # Repository name under the project
        repo: example-value
...
        # Optional file or directory path within the repo
        path: example-value
...
      # Relationship type (depends_on, documents, consumes, owns, related, …)
      type: example-value
...
      # Short label for graph UIs
      label: example-value
...
      # Longer explanation for agents and exports
      description: Example text for documentation; replace before production use.
      # Flat tags for filtering graph exports
      tags:
        example: documentation
      # Extensible payload for GitNexus or other graph ingestors
      metadata:
        source: metagit-config-example
...
  # Graph-level metadata for export pipelines
  metadata:
    source: metagit-config-example
...
# License information
license:
  # License type
  kind: None
  # License file path
  file: example
# Project maintainers
maintainers:
  -
    # Maintainer name
    name: example-name
...
    # Maintainer email
    email: maintainer@example.com
...
    # Maintainer role
    role: example
# Branch strategy used by the project.
branch_strategy: trunk
# Task management tools employed by the project.
taskers:
  -
    # Tasker type
    kind: Taskfile
# Branch naming patterns used by the project.
branch_naming:
  -
    # Branch strategy
    kind: trunk
    # Branch naming pattern
    pattern: example
# Generated artifacts from the project.
artifacts:
  -
    # Artifact type
    type: docker
    # Artifact definition
    definition: example
    # Artifact location
    location: example
    # Version strategy
    version_strategy: semver
# Secrets management tools employed by the project.
secrets_management:
  - example-value
...
# Secret definitions
secrets:
  -
    # Secret name
    name: example-name
...
    # Secret type
    kind: remote_jwt
    # Secret reference
    ref: example
# Variable definitions
variables:
  -
    # Variable name
    name: example-name
...
    # Variable type
    kind: string
    # Variable reference
    ref: example
# CI/CD configuration
cicd:
  # CI/CD platform
  platform: GitHub
  # List of pipelines
  pipelines:
    -
      # Pipeline name
      name: example-name
...
      # Pipeline reference
      ref: example
      # Pipeline variables
      variables:
        - example-value
...
# Deployment configuration
deployment:
  # Deployment strategy
  strategy: blue/green
  # Deployment environments
  environments:
    -
      # Environment name
      name: example-name
...
      # Environment URL
      url: https://example.com/org/repo
...
  # Infrastructure configuration
  infrastructure:
    # Provisioning tool
    provisioning_tool: Terraform
    # Hosting platform
    hosting: EC2
# Observability configuration
observability:
  # Logging provider
  logging_provider: console
  # Monitoring providers
  monitoring_providers:
    - prometheus
  # Alerting channels
  alerting_channels:
    -
      # Alerting channel name
      name: example-name
...
      # Alerting channel type
      type: slack
      # Alerting channel URL
      url: https://example.com/org/repo
...
  # Monitoring dashboards
  dashboards:
    -
      # Dashboard name
      name: example-name
...
      # Dashboard tool
      tool: example-value
...
      # Dashboard URL
      url: https://example.com/org/repo
...
# Important local project paths. In a monorepo, this would include any sub-projects typically found being built in the CICD pipelines.
paths:
  -
    # Friendly name for the path or project
    name: example-name
...
    # Short description of the path or project
    description: Example text for documentation; replace before production use.
    # Project kind
    kind: monorepo
    # Reference in the current project for the target project, used in dependencies
    ref: example
    # Project path
    path: example-value
...
    # Project branches
    branches:
      - example-value
...
    # Project URL
    url: https://example.com/org/repo
...
    # Sync setting
    sync: true
    # Programming language
    language: python
    # Language version
    language_version: 3.12
    # Package manager used by the project
    package_manager: uv
    # Frameworks used by the project
    frameworks:
      - example-value
...
    # Provider used to discover this repository
    source_provider: example-value
...
    # Source namespace identifier (org/user/group)
    source_namespace: example-value
...
    # Provider-native repository identifier
    source_repo_id: example-value
...
    # Flat metadata tags for managed repo search and filtering
    tags:
      example: documentation
    # If true, reconcile mode must not remove this repository automatically
    protected: false
    # Optional instructions for subagents operating in this repo or path
    agent_instructions: Example text for documentation; replace before production use.
# Additional project dependencies not found in the paths or components lists. These include docker images, helm charts, or terraform modules.
dependencies:
  -
    # Friendly name for the path or project
    name: example-name
...
    # Short description of the path or project
    description: Example text for documentation; replace before production use.
    # Project kind
    kind: monorepo
    # Reference in the current project for the target project, used in dependencies
    ref: example
    # Project path
    path: example-value
...
    # Project branches
    branches:
      - example-value
...
    # Project URL
    url: https://example.com/org/repo
...
    # Sync setting
    sync: true
    # Programming language
    language: python
    # Language version
    language_version: 3.12
    # Package manager used by the project
    package_manager: uv
    # Frameworks used by the project
    frameworks:
      - example-value
...
    # Provider used to discover this repository
    source_provider: example-value
...
    # Source namespace identifier (org/user/group)
    source_namespace: example-value
...
    # Provider-native repository identifier
    source_repo_id: example-value
...
    # Flat metadata tags for managed repo search and filtering
    tags:
      example: documentation
    # If true, reconcile mode must not remove this repository automatically
    protected: false
    # Optional instructions for subagents operating in this repo or path
    agent_instructions: Example text for documentation; replace before production use.
# Additional project component paths that may be useful in other projects.
components:
  -
    # Friendly name for the path or project
    name: example-name
...
    # Short description of the path or project
    description: Example text for documentation; replace before production use.
    # Project kind
    kind: monorepo
    # Reference in the current project for the target project, used in dependencies
    ref: example
    # Project path
    path: example-value
...
    # Project branches
    branches:
      - example-value
...
    # Project URL
    url: https://example.com/org/repo
...
    # Sync setting
    sync: true
    # Programming language
    language: python
    # Language version
    language_version: 3.12
    # Package manager used by the project
    package_manager: uv
    # Frameworks used by the project
    frameworks:
      - example-value
...
    # Provider used to discover this repository
    source_provider: example-value
...
    # Source namespace identifier (org/user/group)
    source_namespace: example-value
...
    # Provider-native repository identifier
    source_repo_id: example-value
...
    # Flat metadata tags for managed repo search and filtering
    tags:
      example: documentation
    # If true, reconcile mode must not remove this repository automatically
    protected: false
    # Optional instructions for subagents operating in this repo or path
    agent_instructions: Example text for documentation; replace before production use.
# Workspaces are a collection of projects that are related to each other. They are used to group projects together for a specific purpose. These are manually defined by the user. The internal workspace name is reservice
workspace:
  # Human-readable description of this workspace
  description: Portfolio of git repos and local publish paths managed by metagit.
  # Optional instructions for agents working in this workspace
  agent_instructions: Validate after manifest edits (metagit config validate). Default sync is fetch-only;\n
...
  # Workspace projects
  projects:
    -
      # Workspace project name
      name: portfolio
      # Human-readable description of this workspace project
      description: Git-backed applications and services.
...
      # Optional instructions for agents working in this workspace project
      agent_instructions: Delegate single-repo work to subagents with effective_agent_instructions.\n
...
      # Repository list
      repos:
        -
          # Friendly name for the path or project
          name: example-service
...
          # Short description of the path or project
          description: Sample git-backed service.
...
          # Project URL
          url: https://github.com/example-org/example-service.git
...
          # Sync setting
          sync: true
          # Optional instructions for subagents operating in this repo or path
          agent_instructions: Run tests before opening PRs; keep tags updated in this manifest.\n
    -
      # Workspace project name
      name: local
      # Human-readable description of this workspace project
      description: Local-only paths for static sites (no git remotes).
...
      # Optional override of app-config workspace.dedupe for this project (currently supports enabled only)
      dedupe:
        # When set, overrides workspace.dedupe.enabled from metagit.config.yaml for sync and layout under this project only
        enabled: false
      # Optional instructions for agents working in this workspace project
      agent_instructions: Do not git clone or pull repos in this project; paths are symlinks to user dirs.\nDedupe is disabled for this project (overrides workspace.dedupe in metagit.config.yaml).\n
      # Repository list
      repos:
        -
          # Friendly name for the path or project
          name: example-site
...
          # Short description of the path or project
          description: Sample static site published from a local folder.
          # Project path
          path: ~/Sites/example-site
...
          # Sync setting
          sync: true
          # Project kind
          kind: website
          # Optional instructions for subagents operating in this repo or path
          agent_instructions: Document build and publish steps here; update path if the site moves.\n
````

## File: examples/fuzzyfinder_custom_colors_demo.py
````python
#!/usr/bin/env python
"""
Example demonstrating custom colors per list item in FuzzyFinder.
This shows different ways to assign colors to individual items, including:
1. FuzzyFinderTarget objects with built-in color/opacity properties
2. Custom color mapping via custom_colors configuration
3. Mixed approaches and fallback behavior
"""
⋮----
# Add the metagit package to the path
⋮----
def test_fuzzyfindertarget_with_colors()
⋮----
"""Test FuzzyFinderTarget objects with built-in color and opacity properties."""
⋮----
# Create FuzzyFinderTarget objects with individual colors and opacity
items = [
⋮----
color="bold white bg:#cc0000",  # Red background for critical
⋮----
color="bold yellow",  # Yellow for high priority
⋮----
color="green",  # Green for normal priority
⋮----
color="#0088cc",  # Blue hex color for docs
⋮----
color="cyan",  # Cyan for testing
⋮----
color="magenta bg:#001122",  # Custom background
⋮----
# Note: custom_colors is defined but will be overridden by FuzzyFinderTarget properties
fallback_colors = {
⋮----
"Critical Bug Fix": "red",  # This won't be used - object has its own color
"Feature Development": "blue",  # This won't be used either
⋮----
config = FuzzyFinderConfig(
⋮----
custom_colors=fallback_colors,  # These are fallbacks, won't be used
item_opacity=0.5,  # This is fallback opacity, objects have their own
⋮----
finder = FuzzyFinder(config)
⋮----
result = finder.run()
⋮----
def test_mixed_object_types()
⋮----
"""Test mixing FuzzyFinderTarget objects with regular objects."""
⋮----
# Mix FuzzyFinderTarget objects with regular objects
class SimpleTask
⋮----
def __init__(self, name, priority)
⋮----
# FuzzyFinderTarget with built-in colors
⋮----
# Regular objects that will use custom_colors mapping
⋮----
# Custom colors for regular objects (by name)
color_mapping = {
⋮----
item_opacity=0.6,  # Fallback opacity for regular objects
⋮----
def test_priority_demonstration()
⋮----
"""Demonstrate the priority system: FuzzyFinderTarget properties override custom_colors."""
⋮----
# Create items where custom_colors would conflict with object properties
⋮----
color="green",  # Object says GREEN
⋮----
color="blue",  # Object says BLUE
⋮----
# No color/opacity set - will use fallbacks
⋮----
# Custom colors that would conflict with object properties
conflicting_colors = {
⋮----
"Override Test": "red",  # Object overrides this with GREEN
"Another Override": "yellow",  # Object overrides this with BLUE
"No Color Set": "purple",  # This will be used (no object color)
⋮----
item_opacity=0.5,  # Fallback opacity
⋮----
def test_string_items_with_colors()
⋮----
"""Test custom colors with string items."""
⋮----
# Create items with different priorities/types
⋮----
# Define custom colors for different item types
color_map = {
⋮----
def test_object_items_with_colors()
⋮----
"""Test custom colors with object items."""
⋮----
# Create task objects
class Task
⋮----
def __init__(self, name, priority, category)
⋮----
def __str__(self)
⋮----
tasks = [
⋮----
# Color by priority
priority_colors = {
⋮----
color_field="priority",  # Use priority field for color mapping
⋮----
def test_mixed_color_formats()
⋮----
"""Test different color format specifications."""
⋮----
# Demonstrate different color formats
color_formats = {
⋮----
"hex_color": "#ff6600",  # Hex color
"named_color": "cyan",  # Named color
"background_only": "bg:#ffff00",  # Background only
"text_and_bg": "blue bg:#ffffcc",  # Text and background
"rich_markup": "bold magenta",  # Rich markup
⋮----
def test_preview_with_colors()
⋮----
"""Test custom colors with preview enabled."""
⋮----
class ColoredItem
⋮----
def __init__(self, name, color_type, description)
⋮----
ui_colors = {
⋮----
def test_fuzzyfindertarget_with_preview()
⋮----
"""Test FuzzyFinderTarget objects with preview functionality."""
⋮----
# Create FuzzyFinderTarget objects for UI components with rich descriptions
⋮----
def main()
⋮----
"""Run all custom color demonstrations."""
⋮----
# New tests showcasing FuzzyFinderTarget objects
⋮----
# Legacy tests for custom_colors mapping
````

## File: skills/metagit-release-audit/SKILL.md
````markdown
---
name: metagit-release-audit
description: Mandatory before calling work complete when the session changed repo files. Runs format, lint, tests, integration tests, context-aware pip-audit/bandit, and optional gitleaks via task qa:prepush. Use before push, release, or hand-off.
---

# Auditing Release Readiness

Use this skill **whenever** your session added or edited tracked files in this repository and you are about to hand off or say the task is done—not only “release” workflows. Read-only Q&A with no writes can skip it.

## Workflow

1. Run the pre-push quality gate.
2. Capture failing stage logs and iterate fixes.
3. Re-run until all required checks pass.
4. Return a short readiness summary.

## Commands

- `task qa:prepush`
- `task qa:prepush:loop -- 3` (optional bounded retry loop)

## Output Contract

Return:
- pass/fail status by stage (including `security_sync` / `security_audit` / `security_bandit` when triggered)
- unresolved blockers (if any)
- push readiness recommendation

Security in the gate is context-aware: lockfile changes run `uv sync --frozen --all-extras` + `pip-audit` + `bandit`; `src/` changes run `pip-audit` + `bandit`; docs-only diffs skip security. Use `task security:scan` for a full manual run.

## Safety

- Do not claim readiness unless checks are actually green.
````

## File: src/metagit/cli/commands/init.py
````python
#!/usr/bin/env python
"""
Init subcommand

Creates a validated `.metagit.yml` from bundled templates (copier-style prompts or
answers files) or from a minimal kind profile aligned with the current schema.
"""
⋮----
def _resolve_project_metadata(directory: Path) -> Tuple[str, Optional[str]]
⋮----
"""
    Resolve project name and optional remote URL from a directory.

    Works inside a Git repository or any ordinary folder.
    """
name = directory.name
url: Optional[str] = None
⋮----
git_repo = Repo(directory)
name = Path(git_repo.working_dir).name
⋮----
remote_url = git_repo.remotes[0].url
url = remote_url if remote_url else None
⋮----
def _kind_choice() -> list[str]
⋮----
"""
    Resolve and validate the directory to initialize.

    Args:
        target: Path string (relative, absolute, or ~).
        create: When true, create the directory tree if missing.

    Returns:
        Absolute resolved target path.
    """
path = Path(target).expanduser().resolve()
⋮----
"""
    Initialize metagit with a validated manifest and optional companion files.

    TARGET is the folder to write into (default: current directory). Use --target
    to pass a path when you prefer a flag over the positional argument.
    """
logger: UnifiedLogger = ctx.obj["logger"]
app_config: AppConfig = ctx.obj["config"]
service = InitService()
target_path = resolve_target_dir(
⋮----
rows = service.list_templates()
⋮----
overrides: dict[str, str] = {}
⋮----
template_id = service.resolve_template_id(template, kind if not minimal else None)
manifest = service.registry.load_manifest(template_id)
use_bundled = manifest is not None and not minimal
⋮----
agent_mode = bool(ctx.obj.get("agent_mode", False))
⋮----
no_prompt = True
⋮----
result = service.initialize(
effective_kind = manifest.kind
⋮----
desc = description or f"{kind} project managed by metagit."
result = service.initialize_minimal(
effective_kind = kind
⋮----
def _sanitize_workspace_path(workspace_path: str) -> str
⋮----
"""Sanitize workspace path for .gitignore."""
⋮----
sanitized = workspace_path[2:]
⋮----
sanitized = workspace_path
⋮----
"""Update .gitignore file to include workspace path."""
target_path = _sanitize_workspace_path(workspace_path)
⋮----
lines = handle.readlines()
````

## File: src/metagit/cli/commands/workspace.py
````python
"""
Workspace subcommand
"""
⋮----
def _catalog_ctx(ctx: click.Context) -> tuple[MetagitConfig, str, str]
⋮----
local_config: MetagitConfig = ctx.obj["local_config"]
config_path: str = ctx.obj["config_path"]
app_config: AppConfig = ctx.obj["config"]
workspace_root = str(Path(app_config.workspace.path).expanduser().resolve())
⋮----
def _layout_ctx(ctx: click.Context) -> tuple[MetagitConfig, str, str, AppConfig]
⋮----
@click.pass_context
def workspace(ctx: click.Context, config_path: str) -> None
⋮----
"""Workspace subcommands"""
⋮----
logger = ctx.obj["logger"]
⋮----
config_manager = MetagitConfigManager(config_path)
local_config = config_manager.load_config()
⋮----
@click.pass_context
def workspace_list(ctx: click.Context, as_json: bool, no_index: bool) -> None
⋮----
"""List workspace manifest summary, projects, and repository index."""
⋮----
service = WorkspaceCatalogService()
result = service.list_workspace(
⋮----
summary = (result.data or {}).get("summary", {})
⋮----
@workspace.group("project")
@click.pass_context
def workspace_project(_ctx: click.Context) -> None
⋮----
"""Manage workspace projects in the manifest."""
⋮----
@click.pass_context
def workspace_project_list(ctx: click.Context, as_json: bool) -> None
⋮----
"""List projects defined in the workspace manifest."""
⋮----
result = WorkspaceCatalogService().list_projects(local_config)
⋮----
"""Add a project to the workspace manifest."""
⋮----
result = WorkspaceCatalogService().add_project(
⋮----
@click.pass_context
def workspace_project_remove(ctx: click.Context, name: str, as_json: bool) -> None
⋮----
"""Remove a project (and its repos) from the workspace manifest."""
⋮----
result = WorkspaceCatalogService().remove_project(
⋮----
"""Rename a workspace project (manifest and sync folder when present)."""
⋮----
dedupe = resolve_dedupe_for_layout(
result = WorkspaceLayoutService().rename_project(
⋮----
@workspace.group("repo")
@click.pass_context
def workspace_repo(_ctx: click.Context) -> None
⋮----
"""Manage repository entries in the workspace manifest."""
⋮----
"""List repositories in the workspace manifest."""
⋮----
result = WorkspaceCatalogService().list_repos(
⋮----
repo = row.get("repo", {})
⋮----
"""Add a repository entry to a workspace project (manifest only)."""
⋮----
built = service.build_repo_from_fields(
⋮----
result = service.add_repo(
⋮----
"""Remove a repository entry from the workspace manifest (does not delete files)."""
⋮----
result = WorkspaceCatalogService().remove_repo(
⋮----
"""Rename a repository entry and its sync mount when present."""
⋮----
result = WorkspaceLayoutService().rename_repo(
⋮----
"""Move a repository entry to another workspace project."""
⋮----
result = WorkspaceLayoutService().move_repo(
⋮----
@click.pass_context
def workspace_select(ctx: click.Context, project: str = None) -> None
⋮----
"""Select project repo to work on"""
⋮----
project = app_config.workspace.default_project
````

## File: src/metagit/core/api/server.py
````python
#!/usr/bin/env python
"""
Minimal local JSON HTTP API for managed workspace repository search.
"""
⋮----
def _parse_tag_filters_from_query(tag_values: list[str]) -> dict[str, str] | None
⋮----
"""Parse repeated `tag=key=value` style query pairs into a tag filter dict."""
⋮----
parsed: dict[str, str] = {}
⋮----
"""Return the first query value for a key, or default if missing or empty."""
values = params.get(key)
⋮----
first = values[0]
⋮----
def build_server(root: str, host: str, port: int) -> ThreadingHTTPServer
⋮----
"""Build a threading HTTP server rooted at a workspace directory."""
root_resolved = str(Path(root).resolve())
config_path = os.path.join(root_resolved, ".metagit.yml")
service = ManagedRepoSearchService()
catalog = CatalogApiHandler(
layout = LayoutApiHandler(
⋮----
class ReusableThreadingHTTPServer(ThreadingHTTPServer)
⋮----
allow_reuse_address = True
⋮----
class Handler(BaseHTTPRequestHandler)
⋮----
def log_message(self, format: str, *args: Any) -> None
⋮----
_ = (format, args)
⋮----
def do_GET(self) -> None
⋮----
parsed = urlparse(self.path)
params = parse_qs(parsed.query, keep_blank_values=True)
⋮----
manager = MetagitConfigManager(config_path)
loaded = manager.load_config()
⋮----
config = loaded
⋮----
limit_raw = _first(params, "limit", "10") or "10"
⋮----
limit_val = int(limit_raw)
⋮----
limit_val = 10
limit_val = max(1, min(limit_val, 500))
project_raw = _first(params, "project")
project_filter = (
result = service.search(
⋮----
resolved = service.resolve_one(
⋮----
code = 200
⋮----
code = 409
⋮----
code = 404
⋮----
def do_POST(self) -> None
⋮----
length = int(self.headers.get("Content-Length", "0") or "0")
body = self.rfile.read(length) if length > 0 else b""
⋮----
def do_DELETE(self) -> None
⋮----
def _json(self, status: int, payload: dict[str, Any]) -> None
⋮----
body = json.dumps(payload).encode("utf-8")
````

## File: src/metagit/core/appconfig/__init__.py
````python
#!/usr/bin/env python
⋮----
__all__ = [
⋮----
def load_config(config_path: str) -> Union[AppConfig, Exception]
⋮----
"""
    Load and validate the YAML configuration file.
    """
⋮----
config_file = Path(config_path)
⋮----
config_data = yaml.safe_load(file)
⋮----
config = AppConfig(**config_data["config"])
⋮----
def save_config(config_path: str, config: AppConfig) -> Union[None, Exception]
⋮----
"""
    Save the AppConfig object to a YAML file.
    """
⋮----
config_dict = {"config": config.model_dump(exclude_none=True, mode="json")}
⋮----
"""Set appconfig values"""
⋮----
logger = UnifiedLogger(
⋮----
config_path = name.split(".")
current_level = appconfig
⋮----
current_level = getattr(current_level, element)
⋮----
last_element = config_path[-1]
⋮----
field_type = type(getattr(current_level, last_element))
⋮----
converted_value = True
⋮----
converted_value = False
⋮----
converted_value = field_type(value)
⋮----
"""Retrieve appconfig values"""
⋮----
# Map LOG_LEVELS[3] (which is logging.INFO) to the string 'INFO'
⋮----
appconfig_dict = appconfig.model_dump(
output_value = {"config": appconfig_dict}
⋮----
element_value = output_value[element]
⋮----
output_value = element_value.__dict__
⋮----
output_value = element_value
⋮----
output_value = list(output_value.keys())
⋮----
output_result = []
⋮----
output_value = output_result
⋮----
base_yaml.Dumper.ignore_aliases = lambda *args: True  # noqa: ARG005
````

## File: src/metagit/core/config/models.py
````python
#!/usr/bin/env python
"""
Pydantic models for .metagit.yml configuration file.

This module defines the data models used to parse and validate
the .metagit.yml configuration file structure.
"""
⋮----
class LicenseKind(str, Enum)
⋮----
"""Enumeration of license kinds."""
⋮----
NONE = "None"
MIT = "MIT"
APACHE_2_0 = "Apache-2.0"
GPL_3_0 = "GPL-3.0"
BSD_3_CLAUSE = "BSD-3-Clause"
PROPRIETARY = "proprietary"
CUSTOM = "custom"
UNKNOWN = "unknown"
⋮----
class BranchStrategy(str, Enum)
⋮----
"""Enumeration of branch strategies."""
⋮----
TRUNK = "trunk"
GITFLOW = "gitflow"
GITHUBFLOW = "githubflow"
GITLABFLOW = "gitlabflow"
FORK = "fork"
NONE = "none"
⋮----
class TaskerKind(str, Enum)
⋮----
"""Enumeration of tasker kinds."""
⋮----
TASKFILE = "Taskfile"
MAKEFILE = "Makefile"
JEST = "Jest"
NPM = "NPM"
ATMOS = "Atmos"
⋮----
MISE_TASKS = "mise_tasks"
⋮----
class ArtifactType(str, Enum)
⋮----
"""Enumeration of artifact types."""
⋮----
DOCKER = "docker"
GITHUB_RELEASE = "github_release"
HELM_CHART = "helm_chart"
NPM_PACKAGE = "npm_package"
STATIC_WEBSITE = "static_website"
PYTHON_PACKAGE = "python_package"
NODE_PACKAGE = "node_package"
RUBY_PACKAGE = "ruby_package"
JAVA_PACKAGE = "java_package"
C_PACKAGE = "c_package"
CPP_PACKAGE = "cpp_package"
CSHARP_PACKAGE = "csharp_package"
GO_PACKAGE = "go_package"
RUST_PACKAGE = "rust_package"
PHP_PACKAGE = "php_package"
DOTNET_PACKAGE = "dotnet_package"
ELIXIR_PACKAGE = "elixir_package"
HASKELL_PACKAGE = "haskell_package"
⋮----
OTHER = "other"
PLUGIN = "plugin"
TEMPLATE = "template"
CONFIG = "config"
BINARY = "binary"
ARCHIVE = "archive"
⋮----
class VersionStrategy(str, Enum)
⋮----
"""Enumeration of version strategies."""
⋮----
SEMVER = "semver"
⋮----
class SecretKind(str, Enum)
⋮----
"""Enumeration of secret kinds."""
⋮----
REMOTE_JWT = "remote_jwt"
REMOTE_API_KEY = "remote_api_key"
GENERATED_STRING = "generated_string"
⋮----
DYNAMIC = "dynamic"
PRIVATE_KEY = "private_key"
PUBLIC_KEY = "public_key"
SECRET_KEY = "secret_key"
API_KEY = "api_key"
ACCESS_TOKEN = "access_token"
REFRESH_TOKEN = "refresh_token"
PASSWORD = "password"
DATABASE_PASSWORD = "database_password"
⋮----
class VariableKind(str, Enum)
⋮----
"""Enumeration of variable kinds."""
⋮----
STRING = "string"
INTEGER = "integer"
BOOLEAN = "boolean"
⋮----
class CICDPlatform(str, Enum)
⋮----
"""Enumeration of CI/CD platforms."""
⋮----
GITHUB = "GitHub"
GITLAB = "GitLab"
CIRCLECI = "CircleCI"
JENKINS = "Jenkins"
JX = "jx"
TEKTON = "tekton"
⋮----
class DeploymentStrategy(str, Enum)
⋮----
"""Enumeration of deployment strategies."""
⋮----
BLUE_GREEN = "blue/green"
ROLLING = "rolling"
MANUAL = "manual"
GITOPS = "gitops"
PIPELINE = "pipeline"
⋮----
class ProvisioningTool(str, Enum)
⋮----
"""Enumeration of provisioning tools."""
⋮----
TERRAFORM = "Terraform"
CLOUDFORMATION = "CloudFormation"
CDKTF = "CDKTF"
AWS_CDK = "AWS CDK"
BICEP = "Bicep"
⋮----
class Hosting(str, Enum)
⋮----
"""Enumeration of hosting options."""
⋮----
EC2 = "EC2"
VMWARE = "VMware"
ORACLE = "Oracle"
KUBERNETES = "Kubernetes"
VERCEL = "Vercel"
ECS = "ECS"
AWS_LAMBDA = "AWS Lambda"
AWS_FARGATE = "AWS Fargate"
AWS_EKS = "AWS EKS"
AWS_ECS = "AWS ECS"
AWS_ECS_FARGATE = "AWS ECS Fargate"
AWS_ECS_FARGATE_SPOT = "AWS ECS Fargate Spot"
AWS_ECS_FARGATE_SPOT_SPOT = "AWS ECS Fargate Spot Spot"
ELASTIC_BEANSTALK = "Elastic Beanstalk"
AZURE_APP_SERVICE = "Azure App Service"
AZURE_FUNCTIONS = "Azure Functions"
AZURE_CONTAINER_INSTANCES = "Azure Container Instances"
AZURE_CONTAINER_APPS = "Azure Container Apps"
AZURE_CONTAINER_APPS_ENVIRONMENT = "Azure Container Apps Environment"
AZURE_CONTAINER_APPS_ENVIRONMENT_SERVICE = (
AZURE_CONTAINER_APPS_ENVIRONMENT_SERVICE_SERVICE = (
⋮----
class LoggingProvider(str, Enum)
⋮----
"""Enumeration of logging providers."""
⋮----
CONSOLE = "console"
CLOUDWATCH = "cloudwatch"
ELK = "elk"
SENTRY = "sentry"
⋮----
class MonitoringProvider(str, Enum)
⋮----
"""Enumeration of monitoring providers."""
⋮----
PROMETHEUS = "prometheus"
DATADOG = "datadog"
GRAFANA = "grafana"
⋮----
class AlertingChannelType(str, Enum)
⋮----
"""Enumeration of alerting channel types."""
⋮----
SLACK = "slack"
TEAMS = "teams"
EMAIL = "email"
SMS = "sms"
WEBHOOK = "webhook"
⋮----
class ComponentKind(str, Enum)
⋮----
"""Enumeration of component kinds."""
⋮----
ENTRY_POINT = "entry_point"
⋮----
class DependencyKind(str, Enum)
⋮----
"""Enumeration of dependency kinds."""
⋮----
DOCKER_IMAGE = "docker_image"
REPOSITORY = "repository"
⋮----
class Maintainer(BaseModel)
⋮----
"""Model for project maintainer information."""
⋮----
name: str = Field(..., description="Maintainer name")
email: str = Field(..., description="Maintainer email")
role: str = Field(..., description="Maintainer role")
⋮----
class Config
⋮----
"""Pydantic configuration."""
⋮----
use_enum_values = True
extra = "forbid"
⋮----
class License(BaseModel)
⋮----
"""Model for project license information."""
⋮----
kind: LicenseKind = Field(..., description="License type")
file: str = Field(default="", description="License file path")
⋮----
class Tasker(BaseModel)
⋮----
"""Model for task management tools."""
⋮----
kind: TaskerKind = Field(..., description="Tasker type")
⋮----
class BranchNaming(BaseModel)
⋮----
"""Model for branch naming patterns."""
⋮----
kind: BranchStrategy = Field(..., description="Branch strategy")
pattern: str = Field(..., description="Branch naming pattern")
⋮----
class Branch(BaseModel)
⋮----
"""Model for branch information."""
⋮----
name: str = Field(..., description="Branch name")
environment: Optional[str] = Field(None, description="Environment for this branch")
⋮----
class Artifact(BaseModel)
⋮----
"""Model for generated artifacts."""
⋮----
type: ArtifactType = Field(..., description="Artifact type")
definition: str = Field(..., description="Artifact definition")
location: Union[HttpUrl, str] = Field(..., description="Artifact location")
version_strategy: VersionStrategy = Field(..., description="Version strategy")
⋮----
@field_serializer("location")
    def serialize_location(self, location: Union[HttpUrl, str], _info: Any) -> str
⋮----
"""Serialize location to string."""
⋮----
class Secret(BaseModel)
⋮----
"""Model for secret definitions."""
⋮----
name: str = Field(..., description="Secret name")
kind: SecretKind = Field(..., description="Secret type")
ref: str = Field(..., description="Secret reference")
⋮----
class Variable(BaseModel)
⋮----
"""Model for variable definitions."""
⋮----
name: str = Field(..., description="Variable name")
kind: VariableKind = Field(..., description="Variable type")
ref: str = Field(..., description="Variable reference")
⋮----
class Pipeline(BaseModel)
⋮----
"""Model for CI/CD pipeline."""
⋮----
name: str = Field(..., description="Pipeline name")
ref: str = Field(..., description="Pipeline reference")
variables: Optional[List[str]] = Field(None, description="Pipeline variables")
⋮----
@field_validator("variables", mode="before")
    def validate_variables(cls, v: Any) -> Any
⋮----
"""Validate variables field."""
⋮----
class CICD(BaseModel)
⋮----
"""Model for CI/CD configuration."""
⋮----
platform: CICDPlatform = Field(..., description="CI/CD platform")
pipelines: List[Pipeline] = Field(..., description="List of pipelines")
⋮----
class Environment(BaseModel)
⋮----
"""Model for deployment environment."""
⋮----
name: str = Field(..., description="Environment name")
url: Optional[HttpUrl] = Field(None, description="Environment URL")
⋮----
@field_serializer("url")
    def serialize_url(self, url: Optional[HttpUrl], _info: Any) -> Optional[str]
⋮----
"""Serialize URL to string."""
⋮----
class Infrastructure(BaseModel)
⋮----
"""Model for infrastructure configuration."""
⋮----
provisioning_tool: ProvisioningTool = Field(..., description="Provisioning tool")
hosting: Hosting = Field(..., description="Hosting platform")
⋮----
class Deployment(BaseModel)
⋮----
"""Model for deployment configuration."""
⋮----
strategy: DeploymentStrategy = Field(..., description="Deployment strategy")
environments: Optional[List[Environment]] = Field(
infrastructure: Optional[Infrastructure] = Field(
⋮----
class AlertingChannel(BaseModel)
⋮----
"""Model for alerting channel."""
⋮----
name: str = Field(..., description="Alerting channel name")
type: AlertingChannelType = Field(..., description="Alerting channel type")
url: Union[HttpUrl, str] = Field(..., description="Alerting channel URL")
⋮----
@field_serializer("url")
    def serialize_url(self, url: Union[HttpUrl, str], _info: Any) -> str
⋮----
class Dashboard(BaseModel)
⋮----
"""Model for monitoring dashboard."""
⋮----
name: str = Field(..., description="Dashboard name")
tool: str = Field(..., description="Dashboard tool")
url: HttpUrl = Field(..., description="Dashboard URL")
⋮----
@field_serializer("url")
    def serialize_url(self, url: HttpUrl, _info: Any) -> str
⋮----
class Observability(BaseModel)
⋮----
"""Model for observability configuration."""
⋮----
logging_provider: Optional[LoggingProvider] = Field(
monitoring_providers: Optional[List[MonitoringProvider]] = Field(
alerting_channels: Optional[List[AlertingChannel]] = Field(
dashboards: Optional[List[Dashboard]] = Field(
⋮----
class Visibility(str, Enum)
⋮----
"""Enumeration of repository visibility types."""
⋮----
PUBLIC = "public"
PRIVATE = "private"
INTERNAL = "internal"
⋮----
class ProjectType(str, Enum)
⋮----
"""Enumeration of project types."""
⋮----
APPLICATION = "application"
LIBRARY = "library"
MICROSERVICE = "microservice"
CLI = "cli"
IAC = "iac"
⋮----
DATA_SCIENCE = "data-science"
⋮----
DOCS = "docs"
TEST = "test"
⋮----
class ProjectDomain(str, Enum)
⋮----
"""Enumeration of project domains."""
⋮----
WEB = "web"
MOBILE = "mobile"
DEVOPS = "devops"
ML = "ml"
DATABASE = "database"
SECURITY = "security"
FINANCE = "finance"
GAMING = "gaming"
IOT = "iot"
AGENT = "agent"
⋮----
DOCUMENTATION = "documentation"
⋮----
class BuildTool(str, Enum)
⋮----
"""Enumeration of build tools."""
⋮----
MAKE = "make"
CMAKE = "cmake"
BAZEL = "bazel"
⋮----
class LicenseType(str, Enum)
⋮----
"""Enumeration of license types."""
⋮----
class Owner(BaseModel)
⋮----
"""Model for repository owner information."""
⋮----
org: str = Field(..., description="Organization name")
team: str = Field(..., description="Team name")
contact: str = Field(..., description="Contact email")
⋮----
class Language(BaseModel)
⋮----
"""Model for project language information."""
⋮----
primary: str = Field(..., description="Primary programming language")
secondary: Optional[List[str]] = Field(
⋮----
class Project(BaseModel)
⋮----
"""Model for project information."""
⋮----
description: Optional[str] = Field(
agent_instructions: Optional[str] = Field(
type: ProjectType = Field(..., description="Project type")
domain: ProjectDomain = Field(..., description="Project domain")
language: Language = Field(..., description="Language information")
framework: Optional[List[str]] = Field(None, description="Frameworks used")
package_managers: Optional[List[str]] = Field(
build_tool: Optional[BuildTool] = Field(None, description="Build tool used")
deploy_targets: Optional[List[str]] = Field(None, description="Deployment targets")
⋮----
class RepoMetadata(BaseModel)
⋮----
"""Model for repository metadata."""
⋮----
tags: Optional[List[str]] = Field(None, description="Repository tags")
created_at: Optional[datetime] = Field(None, description="Repository creation date")
last_commit_at: Optional[datetime] = Field(None, description="Last commit date")
default_branch: Optional[str] = Field(None, description="Default branch name")
topics: Optional[List[str]] = Field(None, description="Repository topics")
forked_from: Optional[Union[HttpUrl, str]] = Field(
archived: Optional[bool] = Field(
template: Optional[bool] = Field(
has_ci: Optional[bool] = Field(False, description="Whether repository has CI/CD")
has_tests: Optional[bool] = Field(False, description="Whether repository has tests")
has_docs: Optional[bool] = Field(
has_docker: Optional[bool] = Field(
has_iac: Optional[bool] = Field(
⋮----
"""Serialize forked_from to string."""
⋮----
class CommitFrequency(str, Enum)
⋮----
"""Enumeration of commit frequency types."""
⋮----
DAILY = "daily"
WEEKLY = "weekly"
MONTHLY = "monthly"
⋮----
class PullRequests(BaseModel)
⋮----
"""Model for pull request metrics."""
⋮----
open: int = Field(..., description="Number of open pull requests")
merged_last_30d: int = Field(
⋮----
class Metrics(BaseModel)
⋮----
"""Model for repository metrics."""
⋮----
stars: int = Field(..., description="Number of stars")
forks: int = Field(..., description="Number of forks")
open_issues: int = Field(..., description="Number of open issues")
pull_requests: PullRequests = Field(..., description="Pull request metrics")
contributors: int = Field(..., description="Number of contributors")
commit_frequency: CommitFrequency = Field(..., description="Commit frequency")
⋮----
# New configuration models for AppConfig
class MetagitConfig(BaseModel)
⋮----
"""Main model for .metagit.yml configuration file."""
⋮----
name: str = Field(..., description="Project name")
⋮----
url: Optional[Union[HttpUrl, GitUrl]] = Field(None, description="Project URL")
kind: Optional[ProjectKind] = Field(
documentation: Optional[List[DocumentationSource]] = Field(
graph: Optional[WorkspaceGraph] = Field(
license: Optional[License] = Field(None, description="License information")
maintainers: Optional[List[Maintainer]] = Field(
branch_strategy: Optional[BranchStrategy] = Field(
taskers: Optional[List[Tasker]] = Field(
branch_naming: Optional[List[BranchNaming]] = Field(
artifacts: Optional[List[Artifact]] = Field(
secrets_management: Optional[List[str]] = Field(
secrets: Optional[List[Secret]] = Field(None, description="Secret definitions")
variables: Optional[List[Variable]] = Field(
cicd: Optional[CICD] = Field(None, description="CI/CD configuration")
deployment: Optional[Deployment] = Field(
observability: Optional[Observability] = Field(
paths: Optional[List[ProjectPath]] = Field(
dependencies: Optional[List[ProjectPath]] = Field(
components: Optional[List[ProjectPath]] = Field(
workspace: Optional[Workspace] = Field(
⋮----
@field_validator("documentation", mode="before")
@classmethod
    def _coerce_documentation(cls, value: object) -> object
⋮----
"""Accept strings or dicts in YAML documentation lists."""
⋮----
def documentation_graph_nodes(self) -> list[dict[str, Any]]
⋮----
"""Export documentation entries for knowledge-graph ingestors."""
⋮----
def graph_export_payload(self) -> dict[str, Any]
⋮----
"""Export manual graph relationships and metadata for external tools."""
⋮----
relationships = []
⋮----
@property
    def local_workspace_project(self) -> WorkspaceProject
⋮----
"""Get the local workspace project configuration."""
# Combine paths and dependencies into a single list of repos
repos = []
⋮----
validate_assignment = True
⋮----
class TenantConfig(AppConfig)
⋮----
"""Model for tenant configuration that inherits from AppConfig to include all Boundary settings."""
⋮----
# Tenant-specific fields (in addition to all AppConfig fields)
enabled: bool = Field(default=False, description="Whether multi-tenancy is enabled")
default_tenant: str = Field(default="default", description="Default tenant ID")
tenant_header: str = Field(default="X-Tenant-ID", description="Tenant header name")
tenant_required: bool = Field(
allowed_tenants: List[str] = Field(
⋮----
@classmethod
    def _override_from_environment(cls, config: "TenantConfig") -> "TenantConfig"
⋮----
"""
        Override configuration with environment variables, including tenant-specific ones.

        Args:
            config: TenantConfig to override

        Returns:
            Updated TenantConfig
        """
# Call parent method first
config = super()._override_from_environment(config)
⋮----
# Tenant-specific environment variables
⋮----
@classmethod
    def load(cls, config_path: str = None) -> Union["TenantConfig", Exception]
⋮----
"""
        Load TenantConfig from file.

        Args:
            config_path: Path to configuration file (optional)

        Returns:
            TenantConfig object or Exception
        """
⋮----
config_path = os.path.join(
⋮----
config_file = Path(config_path)
⋮----
config_data = yaml.safe_load(f)
⋮----
config = cls(**config_data["config"])
⋮----
config = cls(**config_data)
⋮----
# Override with environment variables
config = cls._override_from_environment(config)
````

## File: src/metagit/core/detect/manager.py
````python
#!/usr/bin/env python3
⋮----
# from metagit.core.detect.detectors.terraform import TerraformModuleDiscovery
⋮----
class DetectionManager(MetagitRecord, LoggingModel)
⋮----
"""
    Single entrypoint for performing detection analysis of a target git project or git project path.

    This class inherits from MetagitRecord and includes all RepositoryAnalysis functionality.
    Existing metagitconfig data is loaded first if a config file exists in the project.
    """
⋮----
# Detection-specific configuration
detection_config: DetectionManagerConfig = Field(
⋮----
# Internal tracking
analysis_completed: bool = Field(
⋮----
@property
    def project_path(self) -> str
⋮----
"""Get the project path."""
⋮----
@project_path.setter
    def project_path(self, value: str) -> None
⋮----
"""Set the project path."""
⋮----
"""
        Create a DetectionManager from a local path.

        Args:
            path: Path to the git repository or project directory
            logger: Logger instance to use
            config: Detection configuration

        Returns:
            DetectionManager instance or Exception
        """
logger = logger or UnifiedLogger(LoggerConfig()).get_logger()
⋮----
# Load existing metagitconfig if it exists
existing_config = cls._load_existing_config(path)
⋮----
# Create base MetagitRecord data
record_data = {
⋮----
# Merge with existing config if found
⋮----
# Create DetectionManager instance
manager = cls(
⋮----
"""
        Create a DetectionManager from a git URL (clones the repository).

        Args:
            url: Git repository URL
            temp_dir: Temporary directory for cloning
            logger: Logger instance to use
            config: Detection configuration

        Returns:
            DetectionManager instance or Exception
        """
⋮----
normalized_url = normalize_git_url(url)
⋮----
# Create temporary directory if not provided
⋮----
temp_dir = tempfile.mkdtemp(prefix="metagit_")
⋮----
# Clone the repository
⋮----
_ = Repo.clone_from(normalized_url, temp_dir)
⋮----
# Load existing metagitconfig if it exists in the cloned repo
existing_config = cls._load_existing_config(temp_dir)
⋮----
@staticmethod
    def _load_existing_config(path: str) -> Optional[MetagitConfig]
⋮----
"""Load existing metagitconfig if it exists in the project."""
config_paths = [
⋮----
config_data = yaml.safe_load(f)
⋮----
def run_all(self) -> Union[None, Exception]
⋮----
"""
        Run all enabled analysis methods.

        Returns:
            None if successful, Exception if failed
        """
⋮----
# Check if this is a git repository
⋮----
_ = Repo(self.path)
⋮----
language_result = self._detect_languages()
⋮----
# Run project type detection
type_result = self._detect_project_type()
⋮----
# Run branch analysis if enabled
⋮----
# Run CI/CD analysis if enabled
⋮----
# Run directory summary analysis if enabled
⋮----
# Run directory details analysis if enabled
⋮----
file_lookup = FileExtensionLookup()
⋮----
# Analyze files
⋮----
# Detect metrics
⋮----
# Update MetagitRecord fields
⋮----
def run_specific(self, method_name: str) -> Union[None, Exception]
⋮----
"""
        Run a specific analysis method.

        Args:
            method_name: Name of the method to run

        Returns:
            None if successful, Exception if failed
        """
⋮----
result = self._detect_languages()
⋮----
result = self._detect_project_type()
⋮----
result = GitBranchAnalysis.from_repo(self.path, self.logger)
⋮----
result = self._ci_config_analysis()
⋮----
result = directory_summary(self.path)
⋮----
result = directory_details(self.path, file_lookup)
⋮----
def _extract_metadata(self) -> None
⋮----
"""Extract basic repository metadata."""
⋮----
# Extract name from path if not set
⋮----
# Try to extract description from README files
readme_files = ["README.md", "README.txt", "README.rst", "README"]
⋮----
readme_path = Path(self.path) / readme_file
⋮----
content = f.read()
# Extract first line as description
lines = content.split("\n")
⋮----
line = line.strip()
⋮----
self.description = line[:200]  # Limit to 200 chars
⋮----
def _detect_languages(self) -> Union[LanguageDetection, Exception]
⋮----
"""Detect programming languages in the repository."""
⋮----
# This is a simplified language detection
# In a real implementation, you would use more sophisticated detection
detected_languages = []
frameworks = []
package_managers = []
build_tools = []
⋮----
# Check for common file extensions
⋮----
# Check for framework and tool indicators
⋮----
package_managers = list(set(package_managers))
⋮----
# Determine primary language (most common)
⋮----
primary = max(set(detected_languages), key=detected_languages.count)
secondary = list(set(detected_languages) - {primary})
⋮----
primary = "Unknown"
secondary = []
⋮----
def _detect_project_type(self) -> Union[ProjectTypeDetection, Exception]
⋮----
"""Detect project type and domain."""
⋮----
# This is a simplified project type detection
⋮----
indicators = []
project_type = "other"
domain = "other"
confidence = 0.5
⋮----
# Check for common project indicators
⋮----
project_type = "application"
confidence = 0.7
⋮----
confidence = 0.8
⋮----
confidence = 0.6
⋮----
domain = "documentation"
⋮----
def _analyze_files(self) -> None
⋮----
"""Analyze files in the repository."""
⋮----
# Check for various file types
⋮----
# Categorize detected files
⋮----
def _detect_metrics(self) -> None
⋮----
"""Detect repository metrics."""
⋮----
repo = Repo(self.path)
⋮----
# Create metrics object
⋮----
stars=0,  # Would be fetched from provider API
forks=0,  # Would be fetched from provider API
open_issues=0,  # Would be fetched from provider API
⋮----
# Create metadata object
⋮----
def _update_metagit_record(self) -> None
⋮----
"""Update MetagitRecord fields with analysis results."""
⋮----
# Update language information
⋮----
# Update project type information
⋮----
# Update branch information
⋮----
# Convert BranchInfo to Branch objects
⋮----
# Update detection timestamp
⋮----
def summary(self) -> Union[str, Exception]
⋮----
"""
        Generate a summary of the repository analysis.

        Returns:
            Summary string or Exception
        """
⋮----
lines = [f"Repository Analysis for: {self.name or self.path}"]
⋮----
# Language detection
⋮----
# Project type detection
⋮----
# Branch analysis
⋮----
# CI/CD analysis
⋮----
# Directory analysis
⋮----
# File analysis
⋮----
# Metrics
⋮----
def to_yaml(self) -> Union[str, Exception]
⋮----
"""
        Convert DetectionManager to YAML string.

        Returns:
            YAML string or Exception
        """
⋮----
data = self.model_dump(exclude_none=True, exclude_defaults=True)
⋮----
# Handle complex objects that can't be serialized directly
def convert_objects(obj)
⋮----
# Convert nested objects
⋮----
def to_json(self) -> Union[str, Exception]
⋮----
"""
        Convert DetectionManager to JSON string.

        Returns:
            JSON string or Exception
        """
⋮----
"""
        Analyze CI/CD configuration in the repository.

        Args:
            repo_path: Path to the repository

        Returns:
            CIConfigAnalysis object or Exception
        """
⋮----
repo_path = self.path
⋮----
repo_path_obj = Path(repo_path)
⋮----
analysis = CIConfigAnalysis()
⋮----
# Check for common CI/CD configuration files
ci_files = self.detection_config.data_ci_file_source
⋮----
full_path = os.path.join(repo_path_obj, file_path)
⋮----
# Read configuration content
⋮----
# Count pipelines (basic heuristic)
⋮----
# Simple pipeline counting based on common patterns
pipeline_indicators = ["job:", "stage:", "pipeline:", "workflow:"]
⋮----
"""
        Analyze the git repository at the given path and return branch information and a strategy guess.
        Uses GitPython for all git operations.

        Args:
            repo_path: Path to the repository

        Returns:
            GitBranchAnalysis object or Exception

        Notes:
          - Should look to replace this with a more sophisticated analysis
          - Should replace GitPython with a more lightweight library
        """
⋮----
repo = Repo(repo_path)
⋮----
# Get local branches
local_branches = [
⋮----
if branch.name != "HEAD"  # Exclude HEAD branch
⋮----
# Get remote branches
remote_branches = []
⋮----
# Remove remote name prefix (e.g., 'origin/')
branch_name = ref.name.split("/", 1)[1] if "/" in ref.name else ref.name
# Exclude HEAD branch from remote branches
⋮----
# Combine and deduplicate branches (prefer local if name overlaps)
all_branches_dict = {b.name: b for b in remote_branches}
⋮----
all_branches = list(all_branches_dict.values())
⋮----
# Analyze branching strategy
strategy_guess = self._analyze_branching_strategy(all_branches)
⋮----
def _analyze_branching_strategy(self, branches: List[BranchInfo]) -> BranchStrategy
⋮----
"""Analyze the branching strategy based on branch names and patterns.

        Args:
            branches: List of BranchInfo objects

        Returns:
            BranchStrategy enum value

        Notes:
          - Should look to replace this with a more sophisticated analysis
          - Should replace GitPython with a more lightweight library
          - Consider custom branch names via appconfig for analysis of additional strategies
        """
⋮----
# Only remote branches matter
remote_branch_names = [b.name for b in branches if b.is_remote]
# local_branch_names = [b.name for b in branches if not b.is_remote]
⋮----
# Check for Git Flow patterns
⋮----
# Check for GitHub Flow patterns
⋮----
if len(remote_branch_names) <= 2:  # main/master + feature branches
⋮----
# Check for GitLab Flow patterns
⋮----
# Check for Trunk-Based Development
⋮----
# Check for Release Branching
⋮----
# def cleanup(self) -> None:
#     """Clean up temporary files if this was a cloned repository."""
#     if self.is_cloned and self.temp_dir and os.path.exists(self.temp_dir):
#         try:
#             shutil.rmtree(self.temp_dir)
#             self.logger.debug(f"Cleaned up temporary directory: {self.temp_dir}")
#         except Exception as e:
#             self.logger.warning(f"Failed to clean up temporary directory: {e}")
⋮----
# class ProjectDiscovery(Protocol):
#     def discover(self, metadata: ProjectMetadata) -> None:
#         """Mutates the metadata object by adding discovered dependencies"""
#         ...
⋮----
# def run_discovery(root_dir: Path, logger: UnifiedLogger) -> ProjectMetadata:
#     """Create a map of files and folders in a repository for further analysis."""
⋮----
#     try:
#         summary = directory_summary(root_dir, file_lookup=FileExtensionLookup())
#     except Exception as e:
#         logger.error(f"Error creating directory summary at {root_dir}: {e}")
#         return Exception(f"Error creating directory summary at {root_dir}: {e}")
⋮----
#         details = directory_details(target_path=root_dir, file_lookup=FileExtensionLookup())
⋮----
#         logger.error(f"Error creating directory details at {root_dir}: {e}")
#         return Exception(f"Error creating directory details at {root_dir}: {e}")
⋮----
#     metadata = ProjectMetadata(project_root=root_dir, directory_summary=summary, directory_details=details)
⋮----
#     detectors = [TerraformModuleDiscovery()]  # Add more detectors here
⋮----
#     for detector in detectors:
#         detector.discover(metadata)
⋮----
#     return metadata
⋮----
class ProjectDetection
⋮----
"""
    Class to manage project detection and analysis.
    This class is responsible for running various detection methods on a project.
    """
⋮----
def __init__(self, logger: Optional[UnifiedLogger] = None)
⋮----
# Dynamically register all detectors found in src/metagit/core/detect/detectors/
⋮----
def _load_detectors(self) -> None
⋮----
"""
        Load all detector modules dynamically from the detectors package.
        This method uses pkgutil to find all modules in the detectors package and registers
        all classes that are subclasses of Detector.
        """
⋮----
module = importlib.import_module(
# Register all classes that are subclasses of Detector
⋮----
attr = getattr(module, attr_name)
⋮----
def run(self, path: str) -> Union[List[DiscoveryResult], Exception]
⋮----
"""
        Run all registered detectors on the specified path.

        Args:
            path (str): The path to the project directory.

        Returns:
            Union[List[DiscoveryResult], Exception]: A list of discovery results if detection is successful, otherwise an exception.
        """
⋮----
path_files = list_git_files(path)
⋮----
scan_context = ProjectScanContext(root_path=Path(path), all_files=path_files)
results = []
⋮----
result = detector.run(scan_context)
⋮----
def all_files(self, path: str) -> Union[List[Path], Exception]
⋮----
"""
        Get all files in the specified path.
        This method uses the list_git_files utility to enumerate all files in the project directory rapidly.

        Args:
            path (str): The path to the project directory.

        Returns:
            List[Path]: A list of all file paths in the project directory.
        """
files = list_git_files(path)
file_list = [f.as_posix() for f in files]
````

## File: src/metagit/core/detect/models.py
````python
#!/usr/bin/env python3
"""
Combined models for the detect module.

This module contains all the Pydantic models used in the detection system,
including language detection, project type detection, branch analysis, CI/CD analysis,
and detection manager configuration.
"""
⋮----
class LanguageDetection(BaseModel)
⋮----
"""Model for language detection results."""
⋮----
primary: str = Field(default="Unknown", description="Primary programming language")
secondary: List[str] = Field(
frameworks: List[str] = Field(
package_managers: List[str] = Field(
build_tools: List[str] = Field(
⋮----
class Config
⋮----
use_enum_values = True
extra = "forbid"
⋮----
class ProjectTypeDetection(BaseModel)
⋮----
"""Model for project type detection results."""
⋮----
type: ProjectType = Field(
domain: ProjectDomain = Field(
confidence: float = Field(default=0.0, description="Confidence score (0.0 to 1.0)")
indicators: List[str] = Field(
⋮----
class BranchInfo(BaseModel)
⋮----
"""Model for branch information."""
⋮----
name: str = Field(..., description="Branch name")
is_remote: bool = Field(
⋮----
class BranchStrategy(str, Enum)
⋮----
"""Model for branch strategy."""
⋮----
GIT_FLOW = "Git Flow"
GITHUB_FLOW = "GitHub Flow"
GITLAB_FLOW = "GitLab Flow"
TRUNK_BASED_DEVELOPMENT = "Trunk-Based Development"
RELEASE_BRANCHING = "Release Branching"
UNKNOWN = "Unknown"
OTHER = "Other"
⋮----
class GitBranchAnalysis(LoggingModel)
⋮----
"""Model for Git branch analysis results."""
⋮----
branches: List[BranchInfo] = Field(
strategy_guess: Optional[BranchStrategy] = Field(
⋮----
# @classmethod
# def from_repo(
#     cls, repo_path: str = ".", logger: Optional[UnifiedLogger] = None
# ) -> Union["GitBranchAnalysis", Exception]:
#     """
#     Analyze the git repository at the given path and return branch information and a strategy guess.
#     Uses GitPython for all git operations.
⋮----
#     logger = logger or UnifiedLogger().get_logger()
⋮----
#     try:
#         repo = Repo(repo_path)
#     except (InvalidGitRepositoryError, NoSuchPathError) as e:
#         logger.exception(f"Invalid git repository at '{repo_path}': {e}")
#         return ValueError(f"Invalid git repository at '{repo_path}': {e}")
⋮----
#     # Get local branches
#     local_branches = [
#         BranchInfo(name=branch.name, is_remote=False)
#         for branch in repo.branches
#         if branch.name != "HEAD"  # Exclude HEAD branch
#     ]
#     logger.debug(f"Found {len(local_branches)} local branches")
⋮----
#     # Get remote branches
#     remote_branches = []
#     for remote in repo.remotes:
#         for ref in remote.refs:
#             # Remove remote name prefix (e.g., 'origin/')
#             branch_name = ref.name.split("/", 1)[1] if "/" in ref.name else ref.name
#             # Exclude HEAD branch from remote branches
#             if branch_name != "HEAD":
#                 remote_branches.append(BranchInfo(name=branch_name, is_remote=True))
#     logger.debug(f"Found {len(remote_branches)} remote branches")
⋮----
#     # Combine and deduplicate branches (prefer local if name overlaps)
#     all_branches_dict = {b.name: b for b in remote_branches}
#     all_branches_dict.update({b.name: b for b in local_branches})
#     all_branches = list(all_branches_dict.values())
⋮----
#     # Analyze branching strategy
#     strategy_guess = cls._analyze_branching_strategy(all_branches, logger)
⋮----
#     return cls(branches=all_branches, strategy_guess=strategy_guess)
⋮----
# @staticmethod
# def _analyze_branching_strategy(
#     branches: List[BranchInfo], logger: UnifiedLogger
# ) -> str:
#     """Analyze the branching strategy based on branch names and patterns."""
#     branch_names = [b.name for b in branches]
#     local_branches = [b.name for b in branches if not b.is_remote]
⋮----
#     logger.debug(f"Analyzing branching strategy for branches: {branch_names}")
⋮----
#     # Check for Git Flow patterns
#     if any(name in branch_names for name in ["develop", "master", "main"]):
#         if "develop" in branch_names:
#             return "Git Flow"
⋮----
#     # Check for GitHub Flow patterns
#     if "main" in branch_names or "master" in branch_names:
#         if len(local_branches) <= 2:  # main/master + feature branches
#             return "GitHub Flow"
⋮----
#     # Check for GitLab Flow patterns
#     if any(name in branch_names for name in ["staging", "production"]):
#         return "GitLab Flow"
⋮----
#     # Check for Trunk-Based Development
#     if len(local_branches) <= 1:
#         return "Trunk-Based Development"
⋮----
#     # Check for Release Branching
#     if any(name.startswith("release/") for name in branch_names):
#         return "Release Branching"
⋮----
#     return "Unknown"
⋮----
class CIConfigAnalysis(LoggingModel)
⋮----
"""Model for CI/CD configuration analysis results."""
⋮----
detected_tool: Optional[str] = Field(None, description="Detected CI/CD tool")
ci_config_path: Optional[str] = Field(
config_content: Optional[str] = Field(
pipeline_count: int = Field(default=0, description="Number of detected pipelines")
triggers: Optional[List[str]] = Field(default=[], description="Detected triggers")
⋮----
# ) -> Union["CIConfigAnalysis", Exception]:
⋮----
#     Analyze CI/CD configuration in the repository.
⋮----
#     Args:
#         repo_path: Path to the repository
#         logger: Logger instance
⋮----
#     Returns:
#         CIConfigAnalysis object or Exception
⋮----
#         repo_path_obj = Path(repo_path)
#         analysis = cls()
⋮----
#         # Check for common CI/CD configuration files
#         ci_files = {
#             ".github/workflows/": "GitHub Actions",
#             ".gitlab-ci.yml": "GitLab CI",
#             ".circleci/config.yml": "CircleCI",
#             "Jenkinsfile": "Jenkins",
#             ".travis.yml": "Travis CI",
#             "azure-pipelines.yml": "Azure DevOps",
#             "bitbucket-pipelines.yml": "Bitbucket Pipelines",
#         }
⋮----
#         for file_path, tool_name in ci_files.items():
#             full_path = os.path.join(repo_path_obj, file_path)
#             if full_path.exists():
#                 analysis.detected_tool = tool_name
#                 analysis.ci_config_path = str(full_path)
⋮----
#                 # Read configuration content
#                 if full_path.is_dir():
#                     for file in full_path.iterdir():
#                         if file.is_file():
#                             with open(file, "r", encoding="utf-8") as f:
#                                 analysis.config_content = f.read()
#                 else:
#                     try:
#                         with open(full_path, "r", encoding="utf-8") as f:
#                             analysis.config_content = f.read()
#                     except Exception as e:
#                         logger.warning(
#                             f"Could not read CI config file {full_path}: {e}"
#                         )
⋮----
#                 logger.debug(f"Detected CI/CD tool: {tool_name}")
#                 break
⋮----
#         # Count pipelines (basic heuristic)
#         if analysis.config_content:
#             # Simple pipeline counting based on common patterns
#             pipeline_indicators = ["job:", "stage:", "pipeline:", "workflow:"]
#             analysis.pipeline_count = sum(
#                 1
#                 for indicator in pipeline_indicators
#                 if indicator in analysis.config_content
#             )
⋮----
#         return analysis
⋮----
#     except Exception as e:
#         logger.exception(f"CI/CD analysis failed: {e}")
#         return e
⋮----
class DetectionManagerConfig(BaseModel)
⋮----
"""
    Configuration for DetectionManager specifying which analysis methods are enabled.
    """
⋮----
branch_analysis_enabled: bool = Field(
ci_config_analysis_enabled: bool = Field(
directory_summary_enabled: bool = Field(
directory_details_enabled: bool = Field(
# Future analysis methods
commit_analysis_enabled: bool = Field(
tag_analysis_enabled: bool = Field(
data_file_type_source: Optional[str] = Field(
data_ci_file_source: Optional[str] = Field(
data_cd_file_source: Optional[str] = Field(
data_package_manager_source: Optional[str] = Field(
⋮----
@classmethod
    def all_enabled(cls) -> "DetectionManagerConfig"
⋮----
"""Create a configuration with all analysis methods enabled."""
⋮----
@classmethod
    def minimal(cls) -> "DetectionManagerConfig"
⋮----
"""Create a configuration with only essential analysis methods enabled."""
⋮----
def get_enabled_methods(self) -> list[str]
⋮----
"""Get a list of enabled analysis method names."""
enabled = []
⋮----
class DiscoveryResult(BaseModel)
⋮----
name: str
description: Optional[str] = None
tags: List[str] = []
confidence: float = 1.0
data: dict[str, Any] = {}  # detector-specific structured data
⋮----
class ProjectScanContext(BaseModel)
⋮----
root_path: Path
all_files: List[Path]
⋮----
@runtime_checkable
class Detector(Protocol)
⋮----
def should_run(self, ctx: ProjectScanContext) -> bool: ...
⋮----
def run(self, ctx: ProjectScanContext) -> Optional[DiscoveryResult]: ...
````

## File: src/metagit/core/mcp/services/workspace_health.py
````python
#!/usr/bin/env python
"""
Workspace integrity and maintenance health checks.
"""
⋮----
class WorkspaceHealthService
⋮----
"""Validate workspace integrity and emit maintenance recommendations."""
⋮----
"""Run selected health checks across workspace repositories."""
gate_status = self._gate.evaluate(root_path=workspace_root)
rows = self._index.build_index(config=config, workspace_root=workspace_root)
⋮----
rows = [row for row in rows if row["project_name"] == project_name]
⋮----
recommendations: list[HealthRecommendation] = []
repo_rows: list[RepoHealthRow] = []
gitnexus_map: dict[str, str] = {}
⋮----
paths = [str(row["repo_path"]) for row in rows if row.get("exists")]
gitnexus_map = self._registry.summarize_for_paths(repo_paths=paths)
⋮----
missing_count = 0
dirty_count = 0
behind_count = 0
stale_gn_count = 0
stale_head_warn_count = 0
stale_head_critical_count = 0
integration_stale_count = 0
⋮----
repo_path = str(row.get("repo_path", ""))
inspect_git = (
inspected = inspect_repo_state(repo_path=repo_path) if inspect_git else {}
gn_status = gitnexus_map.get(repo_path) if check_gitnexus else None
head_raw = inspected.get("head_commit_age_days")
head_age_days = (
merge_raw = inspected.get("merge_base_age_days")
merge_age_days = (
ahead_raw = inspected.get("ahead")
behind_raw = inspected.get("behind")
⋮----
warn_td = min(branch_head_warning_days, branch_head_critical_days)
crit_td = max(branch_head_warning_days, branch_head_critical_days)
⋮----
behind = inspected.get("behind")
⋮----
summary = {
critical = sum(1 for item in recommendations if item.severity == "critical")
⋮----
"""Warn when multiple repos share the same configured URL."""
by_url: dict[str, list[dict[str, Any]]] = {}
⋮----
url = row.get("url")
⋮----
warnings: list[HealthRecommendation] = []
⋮----
names = ", ".join(
action = "review_config"
message = f"Multiple repos share URL {url}: {names}"
⋮----
action = "resync_canonical"
message = (
⋮----
"""Recommend repair when a configured repo path is a broken symlink."""
⋮----
repo_path = Path(str(row.get("repo_path", "")))
⋮----
"""Warn about canonical directories not referenced in the manifest."""
references = workspace_dedupe.list_canonical_references(
orphans = workspace_dedupe.list_orphan_canonical_dirs(
⋮----
"""Sort recommendations by severity."""
order = {"critical": 0, "warning": 1, "info": 2}
````

## File: src/metagit/core/mcp/resources.py
````python
#!/usr/bin/env python
"""
Resource publishing for Metagit MCP runtime.
"""
⋮----
class ResourcePublisher
⋮----
"""Serve MCP resources for workspace config and status views."""
⋮----
def __init__(self, ops_log: OperationsLogService) -> None
⋮----
"""Return a resource payload for known URIs."""
⋮----
meta = SessionStore(workspace_root=workspace_root).get_workspace_meta()
session = (
````

## File: src/metagit/core/project/models.py
````python
#!/usr/bin/env python
"""
Pydantic models for project configuration.
"""
⋮----
class GitUrl(str)
⋮----
"""Custom type for Git repository URLs."""
⋮----
GIT_URL_REGEX = re.compile(
⋮----
@classmethod
    def validate(cls, value: str, _: Any) -> "GitUrl"
⋮----
class ProjectKind(str, Enum)
⋮----
"""Enumeration of project kinds."""
⋮----
MONOREPO = "monorepo"
UMBRELLA = "umbrella"
APPLICATION = "application"
GITOPS = "gitops"
INFRASTRUCTURE = "infrastructure"
SERVICE = "service"
LIBRARY = "library"
WEBSITE = "website"
OTHER = "other"
DOCKER_IMAGE = "docker_image"
REPOSITORY = "repository"
CLI = "cli"
⋮----
class ProjectPath(BaseModel)
⋮----
"""Model for project path, dependency, component, or workspace project information."""
⋮----
name: str = Field(..., description="Friendly name for the path or project")
description: Optional[str] = Field(
kind: Optional[ProjectKind] = Field(None, description="Project kind")
ref: Optional[str] = Field(
path: Optional[str] = Field(None, description="Project path")
branches: Optional[List[str]] = Field(None, description="Project branches")
url: Optional[Union[HttpUrl, GitUrl]] = Field(None, description="Project URL")
sync: Optional[bool] = Field(None, description="Sync setting")
language: Optional[str] = Field(None, description="Programming language")
language_version: Optional[Union[str, float, int]] = Field(
package_manager: Optional[str] = Field(
frameworks: Optional[List[str]] = Field(
source_provider: Optional[str] = Field(
source_namespace: Optional[str] = Field(
source_repo_id: Optional[str] = Field(
tags: dict[str, str] = Field(
protected: Optional[bool] = Field(
agent_instructions: Optional[str] = Field(
⋮----
@field_validator("language_version", mode="before")
    def validate_language_version(cls, v: Any) -> Optional[str]
⋮----
"""Serialize the URL to a string."""
⋮----
class Config
⋮----
"""Pydantic configuration."""
⋮----
use_enum_values = True
extra = "forbid"
````

## File: src/metagit/core/skills/installer.py
````python
#!/usr/bin/env python
"""
Installer utilities for bundled skills and MCP config updates.
"""
⋮----
InstallScope = Literal["project", "user"]
InstallMode = Literal["skills", "mcp"]
⋮----
SUPPORTED_TARGETS = [
⋮----
class TargetPaths(BaseModel)
⋮----
"""Paths for target deployment in each scope."""
⋮----
project_skills_path: str = Field(
user_skills_path: str = Field(..., description="User-global skills destination")
project_mcp_path: str = Field(..., description="Project-local MCP config path")
user_mcp_path: str = Field(..., description="User-global MCP config path")
⋮----
class InstallResult(BaseModel)
⋮----
"""Summary for a single target installation."""
⋮----
target: str
mode: InstallMode
scope: InstallScope
applied: bool
path: str
details: str
dry_run: bool = Field(
⋮----
TARGET_PATHS: Dict[str, TargetPaths] = {
⋮----
def bundled_skills_root() -> Path
⋮----
"""Resolve bundled skill source path."""
⋮----
def list_bundled_skills() -> List[str]
⋮----
"""Return bundled skill names."""
skills_root = bundled_skills_root()
⋮----
def skill_markdown(skill_name: str) -> Optional[str]
⋮----
"""Load SKILL.md content for a bundled skill."""
skill_file = bundled_skills_root() / skill_name / "SKILL.md"
⋮----
def resolve_skill_names(skill_names: Optional[List[str]]) -> List[str]
⋮----
"""Validate and resolve bundled skill names for install."""
bundled = list_bundled_skills()
⋮----
unknown = sorted({name for name in skill_names if name not in bundled})
⋮----
available = ", ".join(bundled) if bundled else "(none)"
⋮----
"""Resolve install targets by explicit include/exclude or auto-detection."""
disabled = set(disable_targets)
⋮----
detected = autodetect_targets(mode=mode, scope=scope)
⋮----
def autodetect_targets(mode: InstallMode, scope: InstallScope) -> List[str]
⋮----
"""Detect target applications by existing config/directories."""
resolved: List[str] = []
⋮----
target_paths = TARGET_PATHS[target]
⋮----
candidate = _expand_target_path(
⋮----
"""Build a human-readable install summary line."""
verb = "Would install" if dry_run else "Installed"
⋮----
names = ", ".join(installed_names)
⋮----
"""Install bundled skills for selected targets."""
source_root = bundled_skills_root()
results: List[InstallResult] = []
⋮----
selected_skills = resolve_skill_names(skill_names)
⋮----
destination = _expand_target_path(
⋮----
installed_names: List[str] = []
⋮----
source_skill = source_root / skill_name
⋮----
dest_skill = destination / skill_name
⋮----
"""Install/update MCP server configuration for selected targets."""
⋮----
config_path = _expand_target_path(
⋮----
config_data = _read_json_with_comments(config_path)
⋮----
config_data = {}
mcp_servers = config_data.get("mcpServers")
⋮----
mcp_servers = {}
⋮----
def _read_json_with_comments(path: Path) -> Dict[str, object]
⋮----
"""Parse JSON or JSONC-style files."""
⋮----
content = path.read_text(encoding="utf-8").strip()
⋮----
no_line_comments = re.sub(r"^\s*//.*$", "", content, flags=re.MULTILINE)
no_block_comments = re.sub(r"/\*.*?\*/", "", no_line_comments, flags=re.DOTALL)
⋮----
def _expand_target_path(path_value: str) -> Path
⋮----
expanded = Path(os.path.expanduser(path_value))
````

## File: src/metagit/core/utils/files.py
````python
#! /usr/bin/env python3
"""
File reader tool for the detect flow
"""
⋮----
def directory_tree(paths: List[Path], show_files: bool = False) -> str
⋮----
"""
    Generate a tree diagram of directory structure from a list of paths.

    Args:
        paths: List of Path objects representing files and directories
        show_files: Whether to display individual files in each directory

    Returns:
        String representation of the directory tree
    """
⋮----
# Build a tree structure from the paths
tree = {}
⋮----
# Normalize all paths
normalized_paths = [path.resolve() for path in paths]
⋮----
# Build tree structure
⋮----
parts = path.parts
current = tree
⋮----
current = current[part]
⋮----
# Generate tree string representation
⋮----
lines = []
keys = sorted(tree_dict.keys())
⋮----
is_last_item = i == len(keys) - 1
connector = "└── " if is_last_item else "├── "
⋮----
if tree_dict[key]:  # Has children
extension = "    " if is_last_item else "│   "
child_lines = _build_tree(
⋮----
def is_binary_file(file_path: str) -> bool
⋮----
"""
    Check if a file is binary by examining its content.

    Args:
        file_path: Path to the file to check

    Returns:
        True if the file is binary, False if it's text
    """
⋮----
# Read first 1024 bytes to check for binary content
chunk = f.read(1024)
⋮----
# Check for null bytes (common in binary files)
⋮----
# Check if the chunk contains mostly printable ASCII characters
text_chars = bytearray({7, 8, 9, 10, 12, 13, 27} | set(range(0x20, 0x7F)))
⋮----
# If we can't read the file, assume it's not binary
⋮----
def get_file_size(file_path: str) -> int
⋮----
"""
    Get the size of a file in bytes.

    Args:
        file_path: Path to the file

    Returns:
        File size in bytes, 0 if file doesn't exist
    """
⋮----
def list_files(directory_path: str) -> List[str]
⋮----
"""
    List all files in a directory recursively.

    Args:
        directory_path: Path to the directory

    Returns:
        List of file paths
    """
⋮----
files = []
⋮----
def list_git_files(directory_path: str) -> List[Path]
⋮----
"""
    List all files in a Git repository.

    Args:
        directory_path: Path to the Git repository
    Returns:
        List of file paths in the repository
    """
⋮----
repo = Repo(directory_path)
⋮----
values = repo.git.ls_files(
⋮----
def read_file_lines(file_path: str) -> List[str]
⋮----
"""
    Read all lines from a file.

    Args:
        file_path: Path to the file

    Returns:
        List of lines (without newline characters)
    """
⋮----
def write_file_lines(file_path: str, lines: List[str]) -> bool
⋮----
"""
    Write lines to a file.

    Args:
        file_path: Path to the file
        lines: List of lines to write

    Returns:
        True if successful, False otherwise
    """
⋮----
def copy_file(source_path: str, dest_path: str) -> bool
⋮----
"""
    Copy a file from source to destination.

    Args:
        source_path: Path to source file
        dest_path: Path to destination file

    Returns:
        True if successful, False otherwise
    """
⋮----
def remove_file(file_path: str) -> bool
⋮----
"""
    Remove a file.

    Args:
        file_path: Path to the file to remove

    Returns:
        True if successful, False otherwise
    """
⋮----
def make_dir(dir_path: str) -> bool
⋮----
"""
    Create a directory.

    Args:
        dir_path: Path to the directory to create

    Returns:
        True if successful, False otherwise
    """
⋮----
def remove_dir(dir_path: str) -> bool
⋮----
"""
    Remove a directory and its contents.

    Args:
        dir_path: Path to the directory to remove

    Returns:
        True if successful, False otherwise
    """
⋮----
class FileTypeInfo(BaseModel)
⋮----
kind: str
type: str
⋮----
class FileTypeWithPercent(BaseModel)
⋮----
percent: float
⋮----
class DirectoryDetails(BaseModel)
⋮----
path: str
num_files: int
file_types: Dict[str, List[FileTypeWithPercent]]
subpaths: List["DirectoryDetails"]
⋮----
class FileExtensionLookup
⋮----
# Parse JSON data
⋮----
data = json.loads(f.read())
⋮----
# Create extension to info mapping for O(1) lookup
⋮----
# Handle the JSON structure which has data wrapped in "extensions" key
⋮----
items = data["extensions"]
⋮----
items = data
⋮----
kind = item.get("kind", "")
file_type = item.get("type", "")
extensions = item.get("extensions", [])
⋮----
# Store each extension with its corresponding info
info = FileTypeInfo(kind=kind, type=file_type)
⋮----
# Normalize extension (lowercase, ensure leading dot)
ext = ext.lower()
⋮----
ext = f".{ext}"
⋮----
def get_file_info(self, filename: str) -> Optional[FileTypeInfo]
⋮----
"""
        Look up file type information based on file extension.

        Args:
            filename: File name or path to check

        Returns:
            FileTypeInfo tuple containing name and type, or None if not found
        """
# Extract extension and normalize
⋮----
def parse_gitignore(ignore_file: Path) -> Set[str]
⋮----
"""
    Parse .gitignore files.

    Args:
        directory_path: Path to the current directory to check for .gitignore
        base_path: Base directory path for the analysis (root of the tree)

    Returns:
        Set of patterns to ignore (combined from all .gitignore files in the path)
    """
ignore_patterns = set()
⋮----
line = line.strip()
# Skip empty lines and comments
⋮----
# Remove trailing slash from patterns
line = line.rstrip("/")
⋮----
def should_ignore_path(path: Path, ignore_patterns: Set[str], base_path: Path) -> bool
⋮----
"""
    Check if a path should be ignored based on ignored patterns.

    Args:
        path: Path to check
        ignore_patterns: Set of patterns from .gitignore files
        base_path: Base directory path for relative pattern matching

    Returns:
        True if the path should be ignored, False otherwise
    """
⋮----
# Get relative path from base directory
⋮----
relative_path = path.relative_to(base_path)
⋮----
# Path is not relative to base, use the path name
relative_path = Path(path.name)
⋮----
relative_str = str(relative_path)
⋮----
# Check each pattern
⋮----
# Handle file patterns
⋮----
"""
    Recursively walks a directory and builds detailed metadata structure using FileExtensionLookup.

    Args:
        target_path: Path to the target directory to analyze
        file_lookup: Single instance of FileExtensionLookup for file type information
        ignore_patterns: Set of patterns to ignore (applied to all subdirectories)

    Returns:
        DirectoryDetails: NamedTuple containing directory structure and detailed file statistics grouped by category
    """
path = Path(target_path)
ignore_file = os.path.join(path, ".gitignore")
ignore_patterns = ignore_patterns or set()
ignore_patterns = ignore_patterns.union(parse_gitignore(ignore_file))
⋮----
# Initialize data structures
file_type_counts: Dict[str, Dict[str, int]] = {
subpaths: List[DirectoryDetails] = []
num_files = 0
⋮----
# Process directory contents
⋮----
# Always ignore .git folders
⋮----
# Check if item should be ignored based on ignore_patterns
⋮----
# Recursively process subdirectory with the same ignore_patterns
sub_metadata = directory_details(
⋮----
# Count file and get detailed type information
⋮----
file_info = file_lookup.get_file_info(item.name)
⋮----
# Group by type category and count by kind
category = file_info.type
kind = file_info.kind
⋮----
# Convert counts to percentages based on total files in directory
file_types_by_category: Dict[str, List[FileTypeWithPercent]] = {}
⋮----
if num_files > 0:  # Only calculate percentages if there are files
⋮----
if kinds:  # Only include categories that have files
⋮----
final_path = path.resolve() if resolve_path else path
⋮----
class FileType(BaseModel)
⋮----
count: int
⋮----
class DirectorySummary(BaseModel)
⋮----
file_types: List[FileType]
subpaths: List["DirectorySummary"]
⋮----
"""
    Recursively walks a directory and builds a metadata structure for a directory summary.
    This is a simplified version of directory_details that only returns the file types and counts.
    This will adhere to .gitignore files.

    Args:
        target_path: Path to the target directory to analyze
        ignore_patterns: Set of patterns to ignore (applied to all subdirectories)

    Returns:
        DirectorySummary: Pydantic model containing directory structure and file statistics
    """
⋮----
file_types: Dict[str, int] = {}
subpaths: List[DirectorySummary] = []
⋮----
sub_metadata = directory_summary(str(item), ignore_patterns, resolve_path)
⋮----
# Count file and type
⋮----
file_ext = (
⋮----
)  # Only the extension without the dot, or full name if no extension
⋮----
# Convert file types to list of FileType models
file_types_list = [
````

## File: src/metagit/core/web/config_handler.py
````python
#!/usr/bin/env python
"""HTTP handlers for metagit and appconfig schema tree routes (v3 API)."""
⋮----
JsonResponder = Callable[[int, dict[str, Any]], None]
⋮----
ConfigTarget = Literal["metagit", "appconfig"]
ValidateTarget = Literal["metagit", "appconfig", "both"]
⋮----
class ConfigWebHandler
⋮----
"""Route config tree and patch operations for the local web HTTP API."""
⋮----
"""Dispatch config routes; return True when handled."""
parsed_path = urlparse(path).path
⋮----
loaded = config
errors = list(validation_errors or [])
⋮----
loaded_result = self._load_metagit(respond)
⋮----
loaded = loaded_result
response = self._tree_response(
⋮----
loaded_result = self._load_appconfig(respond)
⋮----
def _patch_metagit(self, body: bytes, respond: JsonResponder) -> None
⋮----
loaded = self._load_metagit(respond)
⋮----
patch = self._parse_patch(body, respond)
⋮----
saved = False
⋮----
manager = MetagitConfigManager(self._metagit_config_path)
save_result = manager.save_config(updated)
⋮----
saved = True
⋮----
def _patch_appconfig(self, body: bytes, respond: JsonResponder) -> None
⋮----
loaded = self._load_appconfig(respond)
⋮----
save_result = save_appconfig(self._appconfig_path, updated)
⋮----
def _validate_configs(self, body: bytes, respond: JsonResponder) -> None
⋮----
payload = self._parse_body(body, respond, required=False) or {}
target_raw = payload.get("target", "both")
⋮----
target: ValidateTarget = target_raw
results: list[dict[str, Any]] = []
targets: list[ConfigTarget] = (
⋮----
errors = self._validation_errors_for_metagit()
⋮----
errors = self._validation_errors_for_appconfig()
⋮----
def _validation_errors_for_metagit(self) -> list[dict[str, str]]
⋮----
loaded = manager.load_config()
⋮----
def _validation_errors_for_appconfig(self) -> list[dict[str, str]]
⋮----
loaded = load_appconfig(self._appconfig_path)
⋮----
request = self._parse_preview_request(query, body, respond)
⋮----
config = loaded
validation_errors: list[dict[str, str]] = []
draft = bool(request.operations)
⋮----
yaml_text = read_disk_text(self._metagit_config_path)
⋮----
yaml_text = render_metagit_yaml(config, style=request.style)
response = ConfigPreviewResponse(
⋮----
yaml_text = read_disk_text(self._appconfig_path)
⋮----
yaml_text = render_appconfig_yaml(
⋮----
params = parse_qs(query, keep_blank_values=True)
style_raw = (params.get("style") or ["normalized"])[0]
⋮----
style: PreviewStyle = style_raw
⋮----
payload = self._parse_body(body, respond, required=False)
⋮----
parsed = ConfigPreviewRequest.model_validate(
⋮----
tree = self._schema.build_tree(
⋮----
def _load_metagit(self, respond: JsonResponder) -> MetagitConfig | None
⋮----
def _load_appconfig(self, respond: JsonResponder) -> AppConfig | None
⋮----
payload = self._parse_body(body, respond, required=True)
⋮----
parsed = json.loads(body.decode("utf-8"))
⋮----
@staticmethod
    def _format_error_path(loc: tuple[Any, ...]) -> str
⋮----
parts: list[str] = []
````

## File: src/metagit/core/web/models.py
````python
#!/usr/bin/env python
"""Pydantic models for metagit web API."""
⋮----
class ConfigOpKind(str, Enum)
⋮----
ENABLE = "enable"
DISABLE = "disable"
SET = "set"
APPEND = "append"
REMOVE = "remove"
⋮----
class ConfigOperation(BaseModel)
⋮----
op: ConfigOpKind
path: str
value: Any | None = None
⋮----
class ConfigPatchRequest(BaseModel)
⋮----
save: bool = False
operations: list[ConfigOperation] = Field(default_factory=list)
⋮----
class SchemaFieldNode(BaseModel)
⋮----
key: str
type: str
type_label: str | None = None
description: str | None = None
required: bool = False
enabled: bool = False
editable: bool = True
sensitive: bool = False
default_value: Any | None = None
⋮----
enum_options: list[str] = Field(default_factory=list)
item_count: int | None = None
can_append: bool = False
children: list["SchemaFieldNode"] = Field(default_factory=list)
⋮----
class ConfigTreeResponse(BaseModel)
⋮----
ok: bool
target: Literal["metagit", "appconfig"]
config_path: str
tree: SchemaFieldNode
validation_errors: list[dict[str, str]] = Field(default_factory=list)
saved: bool = False
⋮----
class ConfigPreviewRequest(BaseModel)
⋮----
style: Literal["normalized", "minimal", "disk"] = "normalized"
⋮----
class ConfigPreviewResponse(BaseModel)
⋮----
style: Literal["normalized", "minimal", "disk"]
yaml: str
draft: bool = False
⋮----
class SyncJobRequest(BaseModel)
⋮----
repos: list[str] | None = None
mode: Literal["fetch", "pull", "clone"] = "fetch"
dry_run: bool = False
allow_mutation: bool = True
max_parallel: int = 4
⋮----
class SyncJobStatus(BaseModel)
⋮----
job_id: str
state: Literal["pending", "running", "completed", "failed"]
summary: dict[str, Any] = Field(default_factory=dict)
results: list[dict[str, Any]] = Field(default_factory=list)
error: str | None = None
````

## File: src/metagit/core/web/server.py
````python
#!/usr/bin/env python
"""Local HTTP server for the metagit web UI."""
⋮----
def _resolve_workspace_root(root: str, workspace_path: str) -> str
⋮----
"""Resolve workspace sync root from appconfig path (relative to manifest root)."""
path = Path(workspace_path).expanduser()
⋮----
path = (Path(root) / path).resolve()
⋮----
path = path.resolve()
⋮----
"""Build a threading HTTP server for web UI routes."""
root_resolved = str(Path(root).resolve())
config_path = os.path.join(root_resolved, ".metagit.yml")
appconfig_resolved = str(Path(appconfig_path).resolve())
app_config = load_appconfig(appconfig_resolved)
⋮----
workspace_root = _resolve_workspace_root(
static_handler = StaticWebHandler()
catalog_handler = CatalogApiHandler(
layout_handler = LayoutApiHandler(
config_handler = ConfigWebHandler(
ops_handler = OpsWebHandler(
⋮----
class ReusableThreadingHTTPServer(ThreadingHTTPServer)
⋮----
allow_reuse_address = True
⋮----
class Handler(BaseHTTPRequestHandler)
⋮----
def log_message(self, format: str, *args: Any) -> None
⋮----
_ = (format, args)
⋮----
def do_GET(self) -> None
⋮----
def do_PATCH(self) -> None
⋮----
def do_POST(self) -> None
⋮----
def do_DELETE(self) -> None
⋮----
def _dispatch(self, method: str) -> None
⋮----
parsed = urlparse(self.path)
events_job_id = ops_handler.sync_events_job_id(method, parsed.path)
⋮----
length = int(self.headers.get("Content-Length", "0") or "0")
body = self.rfile.read(length) if length > 0 else b""
⋮----
def _json(self, status: int, payload: dict[str, Any]) -> None
⋮----
body = json.dumps(payload).encode("utf-8")
````

## File: src/metagit/core/workspace/catalog_service.py
````python
#!/usr/bin/env python
"""
List and mutate workspace projects and repositories in `.metagit.yml`.
"""
⋮----
class WorkspaceCatalogService
⋮----
"""CRUD-style catalog operations for workspace manifests."""
⋮----
"""Return workspace summary, projects, and optional index rows."""
summary = self._workspace_summary(
projects = self.list_projects(config=config).data or {}
payload: dict[str, Any] = {
⋮----
def list_projects(self, config: MetagitConfig) -> CatalogResult
⋮----
"""List workspace projects defined in the manifest."""
⋮----
entries = [
⋮----
"""List configured repositories, optionally scoped to one project."""
⋮----
index_rows: list[dict[str, Any]] = []
⋮----
index_rows = self._index.build_index(
index_by_key = {
repos: list[dict[str, Any]] = []
⋮----
row = index_by_key.get((project.name, repo.name), {})
entry = RepoListEntry(
⋮----
"""Add a workspace project (group) to the manifest."""
trimmed = name.strip()
⋮----
save_err = self._save(config=config, config_path=config_path)
⋮----
"""Remove a workspace project from the manifest (repos are removed with it)."""
⋮----
before = len(config.workspace.projects)
⋮----
"""Add a repository entry under a workspace project."""
project = self._find_project(config=config, project_name=project_name)
⋮----
duplicates = find_duplicate_identities(config, repo)
⋮----
locations = ", ".join(f"{proj}/{name}" for proj, name in duplicates)
⋮----
"""Remove a repository entry from a workspace project (manifest only)."""
⋮----
before = len(project.repos)
⋮----
"""Construct a ProjectPath from API/MCP/CLI fields."""
trimmed_name = name.strip()
⋮----
kind_val: ProjectKind | None = None
⋮----
kind_val = ProjectKind(kind)
⋮----
project_count = len(config.workspace.projects) if config.workspace else 0
repo_count = 0
⋮----
repo_count = sum(
⋮----
def _save(self, config: MetagitConfig, config_path: str) -> Optional[Exception]
⋮----
manager = MetagitConfigManager(metagit_config=config)
result = manager.save_config(config, Path(config_path))
````

## File: src/metagit/core/workspace/models.py
````python
#!/usr/bin/env python
"""
Pydantic models for .metagit.yml workspace configuration.
"""
⋮----
class ProjectDedupeOverride(BaseModel)
⋮----
"""Per-project override of app-config ``workspace.dedupe``."""
⋮----
enabled: Optional[bool] = Field(
⋮----
class Config
⋮----
"""Pydantic configuration."""
⋮----
extra = "forbid"
⋮----
class WorkspaceProject(BaseModel)
⋮----
"""Model for workspace project."""
⋮----
name: str = Field(..., description="Workspace project name")
description: Optional[str] = Field(
agent_instructions: Optional[str] = Field(
dedupe: Optional[ProjectDedupeOverride] = Field(
repos: List[ProjectPath] = Field(..., description="Repository list")
⋮----
@field_validator("repos", mode="before")
    def validate_repos(cls, v: Any) -> Any
⋮----
"""Handle YAML anchors and complex repo structures."""
⋮----
# Flatten any nested lists that might come from YAML anchors
flattened: List[Any] = []
⋮----
use_enum_values = True
⋮----
class Workspace(BaseModel)
⋮----
"""Model for workspace configuration."""
⋮----
projects: List[WorkspaceProject] = Field(..., description="Workspace projects")
````

## File: src/metagit/data/skills/metagit-release-audit/SKILL.md
````markdown
---
name: metagit-release-audit
description: Mandatory before calling work complete when the session changed repo files. Runs format, lint, tests, integration tests, context-aware pip-audit/bandit, and optional gitleaks via task qa:prepush. Use before push, release, or hand-off.
---

# Auditing Release Readiness

Use this skill **whenever** your session added or edited tracked files in this repository and you are about to hand off or say the task is done—not only "release" workflows. Read-only Q&A with no writes can skip it.

## Workflow

1. Run the pre-push quality gate.
2. Capture failing stage logs and iterate fixes.
3. Re-run until all required checks pass.
4. Return a short readiness summary.

## Commands

- `task qa:prepush`
- `task qa:prepush:loop -- 3` (optional bounded retry loop)

## Output Contract

Return:
- pass/fail status by stage (including `security_sync` / `security_audit` / `security_bandit` when triggered)
- unresolved blockers (if any)
- push readiness recommendation

Security in the gate is context-aware: lockfile changes run `uv sync --frozen --all-extras` + `pip-audit` + `bandit`; `src/` changes run `pip-audit` + `bandit`; docs-only diffs skip security. Use `task security:scan` for a full manual run.

## Safety

- Do not claim readiness unless checks are actually green.
````

## File: src/metagit/data/metagit.config.yaml
````yaml
config:
  description: "Configuration for metagit CLI"
  agent_mode: false
  # Reserved for future use
  api_url: ""
  # Reserved for future use
  api_version: ""
  # Reserved for future use
  api_key: ""
  # Reserved for future use
  cicd_file_data: data/cicd-files.json
  file_type_data: data/file-types.json
  package_manager_data: data/package-managers.json
  default_profile: default

  llm:
    provider: "openrouter"
    provider_model: "gpt-4o-mini"
    embedder: "ollama"
    embedder_model: "nomic-embed-text"
    api_key: ""

  workspace:
    # Default workspace path, should be added to .gitignore
    path: ./.metagit
    default_project: default
    ui_ignore_hidden: true
    dedupe:
      enabled: false
      canonical_dir: _canonical

  profiles:
    # Profiles are used to define multiple configurations for different organizations
    - name: default
    # Boundaries are used to define the boundaries of the organization
    # They are used to determine which repositories are part of the organization
    # and which are not. This is important for auto-detection of internal and external repositories.
    # Any artifact, repository, or registry that is not part of the defined boundaries is considered external.
      boundaries:
      - name: github
        values: []
      - name: jfrog
        values: []
      - name: gitlab
        values: []
      - name: bitbucket
        values: []
      - name: azure_devops
        values: []
      - name: dockerhub
        values: []
      - name: domain
        values:
          - localhost
          - "127.0.0.1"
          - "0.0.0.0"
          - 192.168.*
          - 10.0.*
          - 172.16.*
````

## File: tests/core/mcp/services/test_workspace_search.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.mcp.services.workspace_search
"""
⋮----
def test_workspace_search_returns_scoped_hits(tmp_path: Path) -> None
⋮----
repo_path = tmp_path / "repo-1"
⋮----
tf_file = repo_path / "main.tf"
⋮----
service = WorkspaceSearchService()
⋮----
results = service.search(
⋮----
def test_filter_repo_paths_supports_project_repo_selector() -> None
⋮----
rows = [
paths = service.filter_repo_paths(rows, repos=["alpha/repo-one"])
⋮----
def test_workspace_search_includes_context_when_rg_available(tmp_path: Path) -> None
⋮----
sample = repo_path / "sample.txt"
⋮----
context = results[0]["context_before"] + results[0]["context_after"]
⋮----
def test_discover_files_returns_categorized_entries(tmp_path: Path) -> None
⋮----
payload = service.discover_files(
````

## File: tests/core/mcp/test_runtime.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.mcp.runtime
"""
⋮----
def test_initialize_request_returns_capabilities(tmp_path: Path) -> None
⋮----
runtime = MetagitMcpRuntime(root=str(tmp_path))
⋮----
response = runtime._handle_request(
⋮----
def test_tools_list_returns_inactive_tools_without_config(tmp_path: Path) -> None
⋮----
names = [item["name"] for item in response["result"]["tools"]]
⋮----
workspace_status_tool = next(
⋮----
def test_tools_call_workspace_status_returns_text_payload(tmp_path: Path) -> None
⋮----
payload = json.loads(response["result"]["content"][0]["text"])
⋮----
def test_resources_read_ops_log_returns_json_content(tmp_path: Path) -> None
⋮----
def test_tools_call_invalid_arguments_returns_mcp_invalid_params(tmp_path: Path) -> None
⋮----
def test_tools_call_workspace_semantic_search_requires_query(tmp_path: Path) -> None
⋮----
def test_initialize_can_enable_sampling_capability(tmp_path: Path) -> None
⋮----
def test_bootstrap_uses_sampling_when_client_supports_it(tmp_path: Path) -> None
⋮----
runtime._request_client_sampling = lambda context: {  # type: ignore[method-assign]
⋮----
def test_tools_list_includes_repo_search_for_active_workspace(tmp_path: Path) -> None
⋮----
def test_tools_call_repo_search_returns_matches(tmp_path: Path) -> None
⋮----
repo_dir = tmp_path / "platform" / "abacus-app"
⋮----
def test_tools_list_includes_project_context_tools(tmp_path: Path) -> None
⋮----
def test_tools_call_project_context_switch_unknown_project(tmp_path: Path) -> None
⋮----
def test_tools_call_cross_project_dependencies(tmp_path: Path) -> None
````

## File: tests/core/mcp/test_tool_registry.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.mcp.tool_registry
"""
⋮----
def test_inactive_registry_exposes_only_safe_tools() -> None
⋮----
registry = ToolRegistry()
status = WorkspaceStatus(
⋮----
tools = registry.list_tools(status=status)
⋮----
def test_active_registry_exposes_full_toolset() -> None
````

## File: tests/core/web/test_config_handler.py
````python
#!/usr/bin/env python
"""HTTP tests for web config tree routes (v3 API)."""
⋮----
server = build_web_server(
thread = threading.Thread(target=server.serve_forever, daemon=True)
⋮----
port = server.server_address[1]
base = f"http://127.0.0.1:{port}"
⋮----
def _patch_json(base: str, target: str, body: dict) -> tuple[int, dict]
⋮----
patch_body = json.dumps(body).encode("utf-8")
patch_req = urllib.request.Request(
⋮----
raw = exc.read().decode("utf-8")
⋮----
def test_get_metagit_config_tree(tmp_path: Path) -> None
⋮----
payload = json.loads(
⋮----
name_node = next(
⋮----
def test_patch_metagit_set_name_without_save(tmp_path: Path) -> None
⋮----
patch_body = json.dumps(
⋮----
patched = json.loads(
⋮----
on_disk = (tmp_path / ".metagit.yml").read_text(encoding="utf-8")
⋮----
def test_patch_metagit_set_name_with_save(tmp_path: Path) -> None
⋮----
kind_node = next(
⋮----
def test_get_metagit_preview_normalized(tmp_path: Path) -> None
⋮----
def test_post_metagit_preview_draft_operations(tmp_path: Path) -> None
⋮----
body = json.dumps(
req = urllib.request.Request(
payload = json.loads(urllib.request.urlopen(req, timeout=5).read().decode("utf-8"))
````

## File: tests/test_config_models.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.config.models
"""
⋮----
def test_license_kind_enum()
⋮----
def test_branch_strategy_enum()
⋮----
def test_license_model()
⋮----
lic = models.License(kind=models.LicenseKind.MIT, file="LICENSE")
⋮----
def test_branch_naming_model()
⋮----
bn = models.BranchNaming(kind=models.BranchStrategy.TRUNK, pattern="main/*")
⋮----
def test_artifact_model()
⋮----
art = models.Artifact(
⋮----
def test_secret_model()
⋮----
sec = models.Secret(
⋮----
def test_variable_model()
⋮----
var = models.Variable(
⋮----
def test_pipeline_and_cicd()
⋮----
pipe = models.Pipeline(
cicd = models.CICD(platform=models.CICDPlatform.GITHUB, pipelines=[pipe])
⋮----
def test_environment_and_deployment()
⋮----
env = models.Environment(name="prod", url="http://prod.example.com")
infra = models.Infrastructure(
dep = models.Deployment(
⋮----
def test_observability()
⋮----
alert = models.AlertingChannel(
dash = models.Dashboard(name="main", tool="grafana", url="http://grafana.com")
obs = models.Observability(
⋮----
def test_project_and_metadata()
⋮----
lang = models.Language(primary="python", secondary=["js"])
proj = models.Project(
⋮----
meta = models.RepoMetadata(tags=["tag1"], created_at=datetime.now())
⋮----
def test_metrics_and_pull_requests()
⋮----
pr = models.PullRequests(open=2, merged_last_30d=5)
metrics = models.Metrics(
⋮----
def test_metagit_config_minimal()
⋮----
cfg = models.MetagitConfig(name="proj")
⋮----
# Test serialization
⋮----
def test_workspace_description_and_agent_instructions()
⋮----
"""Workspace and workspace projects accept optional description and agent_instructions."""
repo = ProjectPath(
ws = models.Workspace(
⋮----
def test_workspace_project_dedupe_override() -> None
⋮----
"""Workspace projects accept optional dedupe.enabled override."""
project = models.WorkspaceProject(
⋮----
class TestMetagitConfig
⋮----
"""Test MetagitConfig class."""
⋮----
def test_metagit_config_basic(self)
⋮----
"""Test basic MetagitConfig creation."""
config = models.MetagitConfig(
⋮----
def test_metagit_config_with_optional_fields(self)
⋮----
"""Test MetagitConfig with optional fields."""
⋮----
def test_metagit_config_validation_error(self)
⋮----
"""Test MetagitConfig validation error."""
⋮----
models.MetagitConfig()  # Missing required name field
⋮----
def test_metagit_config_does_not_have_detection_attributes(self)
⋮----
"""Test that MetagitConfig does not have detection-specific attributes."""
config = models.MetagitConfig(name="test-project")
⋮----
# These attributes should not exist on MetagitConfig
````

## File: web/src/components/FieldEditor.tsx
````typescript
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useEffect, useMemo, useState } from 'react'
import type { ConfigOperation, SchemaFieldNode } from '../api/client'
import {
  configTreeQueryKey,
  fetchConfigTree,
  patchConfigTree,
  type ConfigTarget,
} from '../pages/configQueries'
import styles from './FieldEditor.module.css'
⋮----
interface FieldEditorProps {
  target: ConfigTarget
  node: SchemaFieldNode | null
  pendingOps: ConfigOperation[]
  onPendingChange: (ops: ConfigOperation[]) => void
}
⋮----
function scalarTypes(): Set<string>
⋮----
function formatValidationErrors(
  errors: Array<Record<string, string>>,
): Array<
⋮----
function isMaskedSensitiveValue(node: SchemaFieldNode): boolean
⋮----
function shouldSkipSensitiveSet(node: SchemaFieldNode, draft: string): boolean
⋮----
function normalizeDraftValue(node: SchemaFieldNode): unknown
⋮----
function parseDraftValue(node: SchemaFieldNode, raw: string): unknown
⋮----
const queueSetOp = (save: boolean) =>
⋮----
const saveAllPending = () =>
⋮----
const revert = () =>
````

## File: web/src/pages/WorkspacePage.tsx
````typescript
import { useQuery } from '@tanstack/react-query'
import { useMemo, useState } from 'react'
import GraphDiagram from '../components/GraphDiagram'
import OpsPanel from '../components/OpsPanel'
import RepoTable from '../components/RepoTable'
import SyncDialog from '../components/SyncDialog'
import { fetchWorkspaceGraph, graphQueryKey } from './graphQueries'
import {
  fetchWorkspace,
  workspaceQueryKey,
  type StatusFilter,
} from './workspaceQueries'
import styles from './WorkspacePage.module.css'
⋮----
type WorkspaceView = 'repos' | 'graph'
⋮----
interface SyncTarget {
  repos: string[]
  title: string
}
⋮----

⋮----
void refetch()
````

## File: .cursorrules
````
---
name: agents
description: Always-loaded project anchor. Read this first. Contains project identity, non-negotiables, commands, and pointer to ROUTER.md for full context.
last_updated: 2026-05-15
---

# [Project Name]

## What This Is
<!-- One sentence. What does this project do?
     Length: 1 sentence maximum.
     Not a tagline — a factual description of what the software does.
     Example: "A REST API for managing inventory across multiple warehouse locations." -->

## Non-Negotiables
<!-- Hard rules the agent must never violate. Not preferences — rules.
     These are the things that, if broken, cause real damage to the codebase.
     Length: 3-7 items maximum. More than 7 means the list has not been prioritised.
     Example:
     - Never write database queries outside of the repository layer
     - Never commit secrets or API keys
     - Always handle errors explicitly — no silent failures -->

## Commands
<!-- The exact commands needed to work on this project.
     Include: run dev server, run tests, run linter, build.
     Use the actual commands from this codebase — not placeholders.
     Example:
     - Dev: `npm run dev`
     - Test: `npm test`
     - Lint: `npm run lint`
     - Build: `npm run build` -->

## After Every Task
After completing any task: update `.mex/ROUTER.md` project state and any `.mex/` files that are now out of date. If no pattern existed for the task you just completed, create one in `.mex/patterns/`.

**Before you tell the user the work is done** — if this conversation introduced or edited project files under this repo — run **`task qa:prepush`** from the repository root (`scripts/prepush-gate.py` via `scripts/prepush-gate.zsh`). Fix failures and re-run until green. Only skip when the task was strictly read-only (no writes) or the user explicitly waived the QA gate in this thread.

## Commands (minimum)
- QA gate before hand-off / “work complete”: `task qa:prepush` (format, lint, tests, integration tests, context-aware pip-audit/bandit; optional `gitleaks` when installed)
- Full unit tests only: `task test`

## Navigation
At the start of every session, read `.mex/ROUTER.md` before doing anything else.
For full project context, patterns, and task guidance — everything is there.
````

## File: .metagit.yml
````yaml
documentation:
  # Strings are allowed and will be treated as a markdown file in the root of the project
  - README.md
  - CHANGELOG.md
  - CONTRIBUTING.md
  - CODE_OF_CONDUCT.md
  - SECURITY.md
  # URLs are allowed and will be treated as a web page.
  - https://metagit-ai.github.io/metagit-cli/
  # All of these are duplicates of the strings above, but they are here to demonstrate 
  # the different types of documentation that can be used.
  - kind: markdown
    path: README.md
  - kind: markdown
    path: CHANGELOG.md
  - kind: markdown
    path: CONTRIBUTING.md
  - kind: markdown
    path: CODE_OF_CONDUCT.md
  - kind: markdown
    path: SECURITY.md
  - kind: web
    url: https://metagit-ai.github.io/metagit-cli/
  # Explicitly defined documentation types.
  - kind: confluence
    url: https://confluence.example.com/display/METAGIT/Metagit+Documentation
    tags:
      - playbook
      - tutorial
  - kind: sharepoint
    url: https://sharepoint.example.com/display/METAGIT/Metagit+DevOps
    tags:
      - devops
  - kind: web
    url: https://hub.docker.com/_/python
    tags:
      - docker
      - python

artifacts:
  - definition: pyproject.toml
    location: dist/
    type: python_package
    version_strategy: semver
  - definition: Dockerfile
    location: .
    type: docker
    version_strategy: semver

branch_strategy: trunk

cicd:
  pipelines:
    - name: Lint and Test
      ref: .github/workflows/lint-and-test.yaml
      variables: []
    - name: Docs
      ref: .github/workflows/docs.yaml
      variables: []
    - name: PR and Push
      ref: .github/workflows/pr-and-push.yaml
      variables: []
    - name: Publish CLI
      ref: .github/workflows/publish-cli.yaml
      variables: []
  platform: GitHub
dependencies:
  - description:
      Ubuntu is a Linux distribution based on Debian and composed mostly
      of free and open-source software.
    kind: docker_image
    name: ubuntu_latest
    ref: ./.github/workflows/lint-and-test.yaml
  - kind: docker_image
    name: python:3.12-slim
    ref: ./Dockerfile
    url: https://hub.docker.com/_/python
description:
  Metagit is situational awareness for developers and agents. It can make
  a sprawling multi-repo project feel more like a monorepo or be used as a standalone
  tool to provide concise information on the project's used languages/frameworks,
  artifacts, upstream dependencies, and more.
license:
  file: LICENSE.md
  kind: BSD-3-Clause
maintainers:
  - email: zloeber@gmail.com
    name: Zachary Loeber
    role: Contributor
name: metagit-cli
observability: {}
  # dashboards:
  #   - name: OpenSearch Dashboards
  #     tool: OpenSearch
  #     url: http://localhost:5601/
  # logging_provider: elk
  # monitoring_providers:
  #   - grafana
paths:
  - description: The main project path
    kind: application
    language: python
    language_version: "3.13"
    name: metagit
    package_manager: uv
    frameworks:
      - CrewAI
      - Click
    path: ./metagit
taskers:
  - kind: Taskfile
workspace:
  projects: []
    # - name: default
    #   description: Default project for repositories that don't fit into other projects. Includes most IaC deployments.
````

## File: .github/workflows/release.yaml
````yaml
name: Publish to PyPI, TestPyPI, and GitHub

on:
  push:
  workflow_dispatch:
  
env:
  PROJECT_NAME: metagit-cli

concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

permissions:
  contents: read

jobs:
  build:
    name: Build distribution
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0
          persist-credentials: false

      - name: Set up Python
        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
        with:
          python-version: "3.13"

      - name: Set up uv
        uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0

      - name: Install dependencies
        run: |
          uv sync --frozen

      - name: Build package
        run: |
          uv build
      
      - name: Store the distribution packages
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: python-package-distributions
          path: dist/


  publish-to-testpypi:
    name: Publish to PyPI (TEST)
    needs:
    - build
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/')
    environment:
      name: testpypi
      url: https://test.pypi.org/p/${{ env.PROJECT_NAME }}
    permissions:
      id-token: write
    steps:
    - name: Download all the dists
      uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
      with:
        name: python-package-distributions
        path: dist/
    - name: Publish
      uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
      with:
        repository-url: https://test.pypi.org/legacy/

  publish-to-pypi:
    name: Publish to PyPI (PROD)
    runs-on: ubuntu-latest
    # only publish to PyPI on tag pushes
    if: startsWith(github.ref, 'refs/tags/')
    needs: 
      - build
      - publish-to-testpypi
    permissions:
      id-token: write
    environment:
      name: production
      url: https://pypi.org/p/${{ env.PROJECT_NAME }}
    steps:
      - name: Download all the dists
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: python-package-distributions
          path: dist/

      - name: Publish
        uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0

  create-release:
    name: Create GitHub Release
    permissions:
      contents: write
    runs-on: ubuntu-latest
    needs: publish-to-pypi
    if: github.ref_type == 'tag'
    steps:
      - name: Create Release
        uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
        with:
          files: dist/*
          make_latest: true
          generate_release_notes: true
````

## File: .mex/patterns/metagit-web-api.md
````markdown
# Metagit local web API (`metagit web serve`)

## Models

- Pydantic shapes for the localhost web UI live in `src/metagit/core/web/models.py`.
- Extend that module for new request/response types; keep CLI and HTTP handlers thin and delegate validation to these models.

## Server wiring

- `build_web_server(root, appconfig_path, host, port)` in `src/metagit/core/web/server.py` — `ThreadingHTTPServer` + `BaseHTTPRequestHandler`, same JSON helper pattern as `metagit.core.api.server`.
- Static assets: `StaticWebHandler` (`src/metagit/core/web/static_handler.py`) serves `DATA_PATH/web/`; SPA fallback for non-API GET paths.
- GET dispatch order: static (non-API) → v2 catalog → v2 layout → v3 config → v3 ops → 404 JSON for unknown `/v*`.
- POST/DELETE/PATCH: layout, catalog, config, ops (no static).
- Config routes live in `ConfigWebHandler` (`src/metagit/core/web/config_handler.py`): GET/PATCH `/v3/config/{metagit|appconfig}[/tree]`, POST `/v3/config/validate`.
- Ops routes live in `OpsWebHandler` (`src/metagit/core/web/ops_handler.py`): POST `/v3/ops/health`, `/v3/ops/prune/preview`, `/v3/ops/prune`, `/v3/ops/sync`; GET `/v3/ops/sync/{job_id}`; GET `/v3/ops/sync/{job_id}/events` (SSE). Module-level `SyncJobStore` tracks async sync jobs.

## When adding routes later

- Extend `ConfigWebHandler` / `OpsWebHandler` or add sibling handlers; register in `server.py` `Handler._dispatch` (SSE: match path before JSON dispatch).
- Run `task qa:prepush` before hand-off.
````

## File: docs/reference/metagit-config.md
````markdown
# Metagit configuration exemplar

The file [metagit-config.full-example.yml](metagit-config.full-example.yml) is a **generated,
non-production** sample of `.metagit.yml` with representative values and Pydantic field
descriptions as comments.

## Regenerate

From the repository root:

```bash
task generate:schema
```

That runs `metagit config schema`, `metagit appconfig schema`, and `metagit config example`.

To emit only the YAML exemplar:

```bash
metagit config example --output docs/reference/metagit-config.full-example.yml
```

Overrides merged from `src/metagit/data/config-example-overrides.yml` keep the workspace and
Hermes-oriented examples readable.

## Machine-readable schema

JSON Schema for editors and CI:

- [schemas/metagit_config.schema.json](../../schemas/metagit_config.schema.json)
- [schemas/metagit_appconfig.schema.json](../../schemas/metagit_appconfig.schema.json)

## Validate your manifest

```bash
metagit config validate --config-path .metagit.yml
```

## Schema-backed editing (CLI)

The same operation model as the web Config Studio is available on the CLI:

| Command | Purpose |
|---------|---------|
| `metagit config tree` | Browse fields, types, and paths |
| `metagit config preview` | Apply draft ops and print YAML (no save) |
| `metagit config patch --save` | Apply ops and write `.metagit.yml` when valid |
| `metagit appconfig tree` | App config field tree |
| `metagit appconfig preview` | Draft preview (secrets redacted) |
| `metagit appconfig patch --save` | Apply ops to `metagit.config.yaml` |

Operations: `enable`, `disable`, `set`, `append`, `remove`. Paths use dot/bracket notation
(e.g. `workspace.projects[0].name`, `documentation[0].path`).

```bash
# Single field
metagit config patch --op set --path name --value my-workspace --save

# Batch from JSON (same shape as web PATCH body)
metagit config patch --file ops.json --save
```

`ops.json` may be `{"operations": [...]}` or a bare array of operation objects.

Do not deploy the generated exemplar verbatim; copy sections you need and replace placeholders.

## Documentation sources

The top-level `documentation` list accepts **bare strings** or **objects**:

- A string without `http(s)://` is treated as `kind: markdown` with `path` set to that string.
- A URL string becomes `kind: web`.
- Objects support `kind`, `path`, `url`, `title`, `description`, `tags` (list or map), and `metadata` (map) for knowledge-graph ingestion.

Use `MetagitConfig.documentation_graph_nodes()` (or export from your tooling) to emit normalized node payloads.

## Manual graph relationships

The optional top-level `graph` block declares cross-repo edges that are not inferred from imports or URLs:

```yaml
graph:
  metadata:
    source: manual
  relationships:
    - id: platform-api-uses-infra-tf
      from:
        project: platform
        repo: api
      to:
        project: infra
        repo: terraform-modules
      type: depends_on
      label: API stack depends on shared modules
      tags:
        layer: platform
```

These edges are merged into cross-project dependency maps (`type: manual`) and available via `MetagitConfig.graph_export_payload()` for GitNexus-style exports. Request dependency type `manual` when calling `metagit_cross_project_dependencies` to focus on manifest-declared edges.

### Export to GitNexus (Cypher)

Export manual relationships (and optional structure/documentation nodes) as Cypher for `gitnexus_cypher` MCP tool calls:

```bash
metagit config graph export -c .metagit.yml --format json
metagit config graph export -c .metagit.yml --format cypher --output workspace-graph.cypher
metagit config graph export -c .metagit.yml --format tool-calls --manual-only
```

The exporter creates overlay tables `MetagitEntity` and `MetagitLink` (run `schema_statements` once per target GitNexus index), then `MERGE`/`CREATE` workspace nodes and manual edges. MCP: `metagit_export_workspace_graph_cypher`.

Pass `--gitnexus-repo <name>` when the umbrella workspace is indexed under a different GitNexus repo name than the manifest `name` field.
````

## File: docs/reference/metagit-web.md
````markdown
# Metagit Web

Bundled SPA + local HTTP handler that ships with the **`metagit-cli`** Python package.

## Purpose

Metagit Web is a developer-focused browser UI for workspace awareness and maintenance
while you work in a `.metagit.yml` umbrella. It sits next to `metagit` CLI commands and
the local HTTP handlers (for example `/v2` workspace catalog/layout and `/v3` config
and ops endpoints). Use it when you prefer point-and-click inspection and edits over
printing JSON in the terminal.

## `metagit web serve`

From a directory containing a valid `.metagit.yml` (or pointing at one with `--root`):

```bash
metagit web serve [OPTIONS]
```

| Flag | Default | Meaning |
|------|---------|---------|
| `--root` | `.` | Workspace directory that contains `.metagit.yml`. Resolved to an absolute path for logging and API handlers. |
| `--appconfig` | *(inherit)* | Overrides the usual Metagit app-config file path (`metagit` passes this from CLI context); required if `ctx.obj` is missing — run via installed `metagit` CLI, or pass `--appconfig` explicitly. |
| `--host` | `127.0.0.1` | Bind address for the bundled HTTP server. |
| `--port` | `8787` | TCP port for the bundled HTTP server (OS may probe if busy). |
| `--open` / `--no-open` | `no-open` | When `--open` is set, the default browser opens the UI URL once the server starts. |
| `--status-once` | *(off)* | Bind once, print a single `web_state=ready host=… port=… url=…` line, then exit immediately (agents and scripts use this for startup checks without leaving a process running). |

The server serves:

- Static assets packaged under `src/metagit/data/web/` from the SPA build.
- API routes delegated to workspace config, catalog, ops, and related handlers (`/v2`, `/v3`, etc.). See `.mex/patterns/metagit-web-api.md` in this repo for handler/model patterns.

Typical foreground run:

```bash
cd /path/to/workspace
metagit web serve
```

Browse to the printed URL (default root is `/`).

## UI tour

### Config Studio

**Config Studio** is the paired Metagit and application configuration editors accessed from the top navigation:

- **Metagit config** (`/config/metagit`): schema-backed tree navigation and field edits for `.metagit.yml` semantics (PATCH flows against `/v3` config APIs).
- **App config** (`/config/appconfig`): same interaction model against application configuration persisted via Metagit's app-config path.

These screens share TanStack Query data loading and theme styling with the shell.

Each config editor includes a **YAML preview** panel with three render modes:

| Mode | Meaning |
|------|---------|
| **Normalized** | Full config from the validated Pydantic model (same serializer as `metagit config show --normalized`). |
| **Minimal** | Non-default fields only (`exclude_defaults`), useful for seeing what differs from schema defaults. |
| **On disk** | Raw file contents as stored on disk (no draft overlay). |

When you **Apply** edits without saving, pending operations are merged into the preview and a **Draft** badge appears. Validation errors from draft operations surface above the YAML block. App-config previews redact sensitive tokens (`***` + last four characters).

API: `POST /v3/config/metagit/preview` and `POST /v3/config/appconfig/preview` with `{ "style": "normalized", "operations": [...] }`.

CLI parity (same operation model):

```bash
metagit config tree
metagit config preview --file ops.json
metagit config patch --file ops.json --save
metagit appconfig patch --op set --path workspace.dedupe.enabled --value false --save
```

See [metagit-config.md](metagit-config.md#schema-backed-editing-cli) for operation shapes and path examples.

### Workspace Console

The **Workspace Console** is **Workspace** in the chrome (`/workspace`): catalog-level context (projects/repos index, search/filter) plus the **workspace operations** side panel (health/prune/sync style actions routed through `/v3/ops`). This is meant for situational awareness and lightweight maintenance; destructive actions remain gated as in the CLI and API.

Use the **Repositories | Graph** toggle on the workspace toolbar:

- **Repositories** — filterable table of projects and repos (synced / missing) with per-repo sync actions.
- **Graph** — SVG diagram of workspace relationships: manual edges from `.metagit.yml` `graph.relationships`, optional inferred cross-project dependencies, and project → repo structure edges. Checkboxes control inferred and structure layers.

Graph data is loaded from `GET /v3/ops/graph`:

| Query param | Default | Meaning |
|-------------|---------|---------|
| `include_inferred` | `true` | Include edges inferred from cross-project dependency analysis. |
| `include_structure` | `true` | Include project → repo containment edges. |

Response shape: `{ ok, nodes[], edges[], manual_edge_count, inferred_edge_count, structure_edge_count }`. Each node has `id`, `label`, `kind` (`project` \| `repo`). Each edge has `from_id`, `to_id`, `type`, optional `label`, and `source` (`manual` \| `inferred` \| `structure`).

## Frontend development workflow

Prerequisites under `web/`: npm (Node toolchain). Prefer the Task targets from the repo root.

### Hot reload (`task web:dev`)

```bash
task web:dev
```

Runs `npm run dev` (Vite) with a dev-server proxy:

- Paths under `/v2` and `/v3` proxy to **`http://127.0.0.1:8787`** (`web/vite.config.ts`).

Therefore the usual workflow is:

1. In one shell: **`metagit web serve`** (or `--port`/`--host` overrides if you customize Vite targets).
2. In another shell: **`task web:dev`** and open Vite's origin (shown in npm output).

The browser talks to Vite while API calls traverse the proxy to Python.

### Production-like bundle (`task web:build`)

```bash
task web:build
```

This runs `npm ci || npm install` under `web/`, then `npm run build`, emitting into
`src/metagit/data/web/` (`emptyOutDir: true`). Commit those generated assets whenever
shipping UI fixes so `metagit web serve` picks them up without a local Node install.

Continuous integration hooks in `task qa:prepush`/`scripts/prepush-gate.py` focus on Python
tests and lint; they do **not** run **`task web:build`**. If you alter source under `web/`,
run **`task web:build`** manually before tagging or merging UI changes unless your team
delegates builds to CI elsewhere.

## Security

- **`--host` defaults to localhost** (`127.0.0.1`): the bundled server expects local-only use.
- **No authentication layer in Web v1** — anyone who can reach the TCP port can call the APIs the UI exposes. Do not expose an open `--host`/`--port` endpoint on shared networks until an auth model lands.
- Treat `metagit web serve` like any other localhost admin utility: firewall exposure is your responsibility once you bind `0.0.0.0` or comparable.
````

## File: docs/development.md
````markdown
# Metagit Development Guide

Upon making changes run the following to validate everything before submitting a PR

```bash
task format lint:fix test
```

## MCP Development Notes

- Use `metagit mcp serve` to start the MCP stdio runtime.
- Use `--root <path>` to test workspace gating against a specific folder.
- Use `--status-once` for quick diagnostics without starting the message loop.
- MCP gating states:
  - `inactive_missing_config` when `.metagit.yml` is not present
  - `inactive_invalid_config` when `.metagit.yml` fails validation
  - `active` when `.metagit.yml` loads successfully

## Provider Source Sync

Use source sync to discover repositories from GitHub/GitLab and plan/apply workspace updates:

- Discover-only: `metagit project source sync --provider github --org <org> --mode discover`
- Additive apply: `metagit project source sync --provider github --org <org> --mode additive --apply`
- Reconcile apply: `metagit project source sync --provider gitlab --group <group> --mode reconcile --apply --yes`

## Semantic Release Tags

`release-please` runs on merges to `main` and uses conventional commit prefixes to prepare/version releases.

- It opens/updates a release PR with computed next version + changelog.
- When that PR is merged, it creates the semantic tag (`vX.Y.Z`) and GitHub release.

- `fix:` -> patch release (`X.Y.Z+1`) **default for most updates**
- `feat:` -> minor release (`X.Y+1.0`)
- `type(scope)!:` or `BREAKING CHANGE:` -> major release (`X+1.0.0`)

### Commit Prefix Guidance

Use patch semantics first (`fix:`) unless schema/config compatibility is intentionally broken.

- Use `fix:` for normal maintenance and safe behavior changes.
- Use `feat:` only for additive, backward-compatible functionality.
- Use `!` / `BREAKING CHANGE` when changing `.metagit.yml` or app config schema in a non-backward-compatible way.
````

## File: skills/metagit-cli/SKILL.md
````markdown
---
name: metagit-cli
description: CLI-only shortcuts for metagit agents — workspace catalog, discovery, prompts, sync, layout, and config. Use instead of MCP or HTTP API when operating from a shell or agent_mode session.
---

# Metagit CLI (agent shortcuts)

Use this skill when an agent should drive metagit **only through the `metagit` command**. Do not call MCP tools or `metagit api` from workflows covered here unless the user explicitly asks.

Set non-interactive defaults when automating:

```bash
export METAGIT_AGENT_MODE=true
```

Global flags (most commands):

- `-c path/to/metagit.config.yaml` — app config (default `metagit.config.yaml`)
- Workspace manifest: `--definition` / `-c` on catalog commands (default `.metagit.yml`)

---

## Manifest editing fast map (`.metagit.yml`)

Use this table first when changing a workspace manifest from the CLI. Prefer **catalog commands** for projects/repos; use **`config patch`** for everything else in the schema (documentation, graph, dedupe overrides, nested fields).

| Task | Command |
|------|---------|
| **Inspect** manifest on disk | `metagit config show -c .metagit.yml` |
| **Inspect** normalized model | `metagit config show -c .metagit.yml --normalized` |
| **Inspect** as JSON (agents) | `metagit config show -c .metagit.yml --json` |
| **Browse** fields / paths | `metagit config tree -c .metagit.yml` or `… --json` |
| **Validate** after edits | `metagit config validate -c .metagit.yml` |
| **Dry-run** schema change | `metagit config preview -c .metagit.yml --file ops.json` |
| **Apply** schema change | `metagit config patch -c .metagit.yml --file ops.json --save` |
| **Set one field** | `metagit config patch -c .metagit.yml --op set --path <path> --value <v> --save` |
| **Enable** optional block | `metagit config patch … --op enable --path <path> --save` |
| **Add list item** | `metagit config patch … --op append --path <list.path> --save` then `set` on `[index].field` |
| **Remove list item** | `metagit config patch … --op remove --path <list.path>[index] --save` |

### Catalog shortcuts (projects & repos)

| Task | Command |
|------|---------|
| List projects | `metagit workspace project list -c .metagit.yml --json` |
| List repos (all / one project) | `metagit workspace repo list -c .metagit.yml --json` / `… --project <p> --json` |
| Add project | `metagit workspace project add -c .metagit.yml --name <p> --json` |
| Remove project | `metagit workspace project remove -c .metagit.yml --name <p> --json` |
| Rename project (dry-run) | `metagit workspace project rename -c .metagit.yml --name <old> --new-name <new> --dry-run --json` |
| Add repo | `metagit workspace repo add -c .metagit.yml --project <p> --name <r> --url <url> --json` |
| Remove repo (manifest only) | `metagit workspace repo remove -c .metagit.yml --project <p> --name <r> --json` |
| Rename / move repo (dry-run) | `metagit workspace repo rename …` / `metagit workspace repo move … --dry-run --json` |
| Search before adding | `metagit search "<name>" -c .metagit.yml --json` |

Active project context (optional): `metagit project repo add --name <r> --url <url>` after `metagit project select`.

### Schema patch examples (`ops.json`)

Paths use dot/bracket notation (same as web Config Studio). `--value` accepts JSON for objects.

```json
{
  "operations": [
    { "op": "set", "path": "name", "value": "my-workspace" },
    { "op": "enable", "path": "documentation" },
    { "op": "append", "path": "documentation" },
    { "op": "set", "path": "documentation[0]", "value": { "kind": "markdown", "path": "AGENTS.md" } },
    { "op": "enable", "path": "graph" },
    { "op": "append", "path": "graph.relationships" },
    {
      "op": "set",
      "path": "graph.relationships[0]",
      "value": {
        "id": "api-depends-infra",
        "from": { "project": "platform", "repo": "api" },
        "to": { "project": "infra", "repo": "terraform" },
        "type": "depends_on"
      }
    },
    { "op": "set", "path": "workspace.projects[0].dedupe.enabled", "value": false }
  ]
}
```

```bash
metagit config preview -c .metagit.yml --file ops.json
metagit config patch -c .metagit.yml --file ops.json --save
metagit config validate -c .metagit.yml
```

### App config vs manifest

| File | Scope | Edit via |
|------|-------|----------|
| `.metagit.yml` | Workspace manifest (projects, repos, docs, graph) | `metagit config …` + `metagit workspace …` |
| `metagit.config.yaml` | Tooling (paths, dedupe default, providers, profiles) | `metagit appconfig …` |

```bash
metagit appconfig show --format minimal-yaml
metagit appconfig patch --op set --path workspace.dedupe.enabled --value false --save
metagit config providers --show
```

### After every manifest edit

1. `metagit config validate -c .metagit.yml`
2. `metagit workspace list -c .metagit.yml --json` (sanity-check catalog)
3. If repos changed on disk: `metagit project sync` or `metagit project sync --hydrate`

### Export manual graph to GitNexus (Cypher)

| Task | Command |
|------|---------|
| Full export bundle (JSON) | `metagit config graph export -c .metagit.yml --json` |
| Raw Cypher script | `metagit config graph export -c .metagit.yml --format cypher -o graph.cypher` |
| MCP tool_calls only | `metagit config graph export -c .metagit.yml --format tool-calls` |
| Manual edges only | `metagit config graph export -c .metagit.yml --manual-only --format tool-calls` |
| MCP from agent | `metagit_export_workspace_graph_cypher` |

Ingest workflow: run `schema_statements` once via `gitnexus_cypher`, then each statement in `tool_calls` (or pipe `--format cypher`). Overlay tables: `MetagitEntity`, `MetagitLink`.

---

## Prompt commands (all kinds)

List built-in prompt kinds:

```bash
metagit prompt list
metagit prompt list --json
```

Emit prompts (`--text-only` for paste into agent context; `--json` for structured output; `--no-instructions` to omit manifest layers):

| Scope | Command | Default kind |
|-------|---------|--------------|
| Workspace | `metagit prompt workspace -c <definition> -k <kind>` | `instructions` |
| Project | `metagit prompt project -p <project> -c <definition> -k <kind>` | `instructions` |
| Repo | `metagit prompt repo -p <project> -n <repo> -c <definition> -k <kind>` | `instructions` |

### Prompt kinds by scope

| Kind | Workspace | Project | Repo | Purpose |
|------|:---------:|:-------:|:----:|---------|
| `instructions` | yes | yes | yes | Composed `agent_instructions` from manifest layers |
| `session-start` | yes | — | — | Session bootstrap checklist |
| `catalog-edit` | yes | yes | — | Search-before-create; catalog registration |
| `health-preflight` | yes | yes | — | Pre-work workspace/repo status pass |
| `sync-safe` | yes | yes | yes | Guarded sync rules |
| `subagent-handoff` | — | yes | yes | Delegate single-repo work |
| `layout-change` | yes | yes | yes | Rename/move dry-run workflow |
| `repo-enrich` | — | — | yes | **Discover + merge** workspace repo entry |

### Prompt shortcuts (copy-paste)

```bash
# Session bootstrap
metagit prompt workspace -k session-start --text-only -c .metagit.yml

# Composed instructions at each level
metagit prompt workspace -k instructions --text-only -c .metagit.yml
metagit prompt project -p default -k instructions --text-only -c .metagit.yml
metagit prompt repo -p default -n my-api -k instructions --text-only -c .metagit.yml

# Repo catalog enrichment (detect + merge manifest entry)
metagit prompt repo -p default -n my-api -k repo-enrich --text-only -c .metagit.yml

# Catalog registration discipline
metagit prompt workspace -k catalog-edit --text-only -c .metagit.yml

# Safe sync reminder
metagit prompt repo -p default -n my-api -k sync-safe --text-only -c .metagit.yml

# Subagent handoff
metagit prompt repo -p default -n my-api -k subagent-handoff --text-only -c .metagit.yml
```

---

## Repo enrich workflow (`repo-enrich`)

Run the prompt, then execute its steps:

```bash
metagit prompt repo -p <project> -n <repo> -k repo-enrich --text-only -c .metagit.yml
```

Typical discovery chain on the checkout:

```bash
cd "$(metagit search '<repo>' -c .metagit.yml --path-only)"
metagit detect repository -p . -o json
metagit detect repo -p . -o yaml
metagit detect repo_map -p . -o json
```

Provider metadata (dry-run):

```bash
metagit project source sync --provider github --org <org> --mode discover --no-apply
```

After merging fields into `workspace.projects[].repos[]`:

```bash
metagit config validate -c .metagit.yml
metagit workspace repo list --project <project> --json
```

---

## Workspace and catalog

Per-project dedupe override in `.metagit.yml` (overrides `workspace.dedupe.enabled` in `metagit.config.yaml` for that project only):

```yaml
workspace:
  projects:
    - name: local
      dedupe:
        enabled: false
      repos: []
```

```bash
metagit appconfig show --format json
metagit config info -c .metagit.yml
metagit config show -c .metagit.yml
metagit config validate -c .metagit.yml

metagit workspace list -c .metagit.yml --json
metagit workspace project list -c .metagit.yml --json
metagit workspace repo list -c .metagit.yml --json
metagit workspace repo list -c .metagit.yml --project <name> --json

metagit workspace project add --name <name> --json
metagit workspace repo add --project <name> --name <repo> --url <url> --json
metagit workspace project remove --name <name> --json
metagit workspace repo remove --project <name> --name <repo> --json
```

Search managed repos (always before creating entries):

```bash
metagit search "<query>" -c .metagit.yml --json
metagit search "<query>" -c .metagit.yml --path-only
metagit search "<query>" -c .metagit.yml --tag tier=1 --project <name>
```

---

## Project operations

```bash
metagit project list --config .metagit.yml --all --json
metagit project add --name <name> --json
metagit project remove --name <name> --json
metagit project rename --name <old> --new-name <new> --dry-run --json
metagit project select
metagit project sync
metagit project sync --hydrate   # symlink mounts → full directory copies (per-file progress)

metagit project repo list --json
metagit project repo add --project <name> --name <repo> --url <url>
metagit project repo remove --name <repo> --json
metagit project repo rename --name <old> --new-name <new> --dry-run --json
metagit project repo move --name <repo> --to-project <other> --dry-run --json
metagit project repo prune --project <name> --dry-run

metagit project source sync --provider github --org <org> --mode discover --no-apply
metagit project source sync --provider github --org <org> --mode additive --apply
```

Layout (manifest + disk; always dry-run first):

```bash
metagit workspace project rename --name <old> --new-name <new> --dry-run --json
metagit workspace repo rename --project <p> --name <old> --new-name <new> --dry-run --json
metagit workspace repo move --project <p> --name <repo> --to-project <other> --dry-run --json
```

---

## Discovery and local metadata

```bash
metagit detect project -p <path> -o yaml
metagit detect repo -p <path> -o yaml
metagit detect repo_map -p <path> -o json
metagit detect repository -p <path> -o json
metagit detect repository -p <path> -o metagit
# --save only with operator approval (blocked in agent_mode)
```

Bootstrap new trees:

```bash
metagit init --kind application
metagit init --kind umbrella --template hermes-orchestrator
```

---

## Selection and scope

```bash
metagit workspace select --project <name>
metagit project select
metagit project repo select
```

---

## Config and appconfig (reference)

See **Manifest editing fast map** above for day-to-day manifest work. Additional commands:

```bash
metagit config info -c .metagit.yml
metagit config example
metagit config schema
metagit appconfig validate
metagit appconfig get --name config.workspace.path
metagit appconfig tree --json
metagit appconfig preview --file ops.json
metagit appconfig patch --file ops.json --save
metagit config providers --show
```

---

## Records, skills, version

```bash
metagit record search "<query>"
metagit skills list
metagit skills show metagit-cli
metagit skills install --skill metagit-cli
metagit version
metagit info
```

---

## Agent habits

1. **Search before create** — `metagit search` then catalog add.
2. **Validate after manifest edits** — `metagit config validate`.
3. **Emit prompts instead of rewriting playbooks** — `metagit prompt … --text-only`.
4. **Enrich stale repo entries** — `metagit prompt repo … -k repo-enrich` then detect + merge.
5. **Dry-run layout** — always `--dry-run --json` before apply.
6. **Prefer `METAGIT_AGENT_MODE=true`** in CI and agent loops to skip fuzzy finder and confirm dialogs.

## Related bundled skills

Use topic skills when you need deeper playbooks (some mention MCP): `metagit-projects`, `metagit-workspace-scope`, `metagit-workspace-sync`, `metagit-config-refresh`. This skill is the **CLI-only** index and prompt reference.
````

## File: src/metagit/cli/commands/appconfig.py
````python
"""
Appconfig subcommand
"""
⋮----
@click.group(name="appconfig", invoke_without_command=True)
@click.pass_context
def appconfig(ctx: click.Context) -> None
⋮----
"""Application configuration subcommands"""
# If no subcommand is provided, show help
⋮----
@appconfig.command("info")
@click.pass_context
def appconfig_info(ctx: click.Context) -> None
⋮----
"""
    Information about the application configuration.
    """
logger = ctx.obj.get("logger") or UnifiedLogger(LoggerConfig())
⋮----
@click.pass_context
def appconfig_show(ctx: click.Context, output_format: str) -> None
⋮----
"""Show the full active application configuration."""
logger = ctx.obj["logger"]
⋮----
config: AppConfig = ctx.obj["config"]
config_path: str = ctx.obj["config_path"]
minimal = output_format == "minimal-yaml"
⋮----
rendered = render_appconfig_show(
⋮----
"""Validate a configuration file"""
⋮----
config_path = os.path.join(DATA_PATH, "metagit.config.yaml")
⋮----
# Step 1: Load YAML
⋮----
config_data = yaml_load(f)
⋮----
# Step 2: Validate structure with Pydantic model
⋮----
_ = AppConfig(**config_data["config"])
⋮----
@click.option("--output", default="json", help="Output format (json/yaml)")
@click.pass_context
def appconfig_get(ctx: click.Context, name: str, show_keys: bool, output: str) -> None
⋮----
"""Display appconfig value"""
⋮----
config = ctx.obj["config"]
result = get_config(
⋮----
logger = ctx.obj.get("logger")
⋮----
@click.pass_context
def appconfig_create(ctx: click.Context, config_path: str = None) -> None
⋮----
"""Create default application config"""
⋮----
default_config = load_config(DEFAULT_CONFIG)
⋮----
output = base_yaml.dump(
⋮----
@appconfig.command("set")
@click.option("--name", help="Appconfig element to target")
@click.option("--value", help="Value to set")
@click.pass_context
def appconfig_set(ctx: click.Context, name: str, value: str) -> None
⋮----
"""Set a value in the application configuration."""
⋮----
config_path = ctx.obj["config_path"]
⋮----
updated_config = set_config(appconfig=config, name=name, value=value)
⋮----
save_result = save_config(config_path=config_path, config=updated_config)
⋮----
@click.pass_context
def appconfig_tree(ctx: click.Context, as_json: bool) -> None
⋮----
"""Show schema-backed field tree for metagit.config.yaml."""
⋮----
result = ConfigPatchService().build_tree("appconfig", config_path)
⋮----
"""Preview app config after draft operations (secrets redacted in output)."""
⋮----
operations = (
result = ConfigPatchService().preview(
⋮----
"""
    Apply schema operations to metagit.config.yaml (enable/disable/set/append/remove).

    Paths use AppConfig field names (e.g. workspace.dedupe.enabled), not the config: wrapper.
    """
⋮----
operations = resolve_operations(
result = ConfigPatchService().patch(
⋮----
@click.pass_context
def appconfig_schema(ctx: click.Context, output_path: str) -> None
⋮----
"""
    Generate a JSON schema for the AppConfig class and write it to a file.
    """
⋮----
schema = AppConfig.model_json_schema()
````

## File: src/metagit/cli/commands/detect.py
````python
"""
Detect subcommand
"""
⋮----
@click.group(name="detect", invoke_without_command=True, help="Detection subcommands")
@click.pass_context
def detect(ctx: click.Context) -> None
⋮----
"""Detection subcommands"""
# If no subcommand is provided, show help
⋮----
@click.pass_context
def detect_project(ctx: click.Context, path: str, output: str) -> None
⋮----
"""Perform project detection and analysis."""
logger = ctx.obj["logger"]
⋮----
path_files = list_git_files(path)
⋮----
detection = ProjectDetection(logger=logger)
⋮----
results = detection.run(path)
⋮----
detections = []
⋮----
summary = {
⋮----
# .model_dump(exclude_none=True, exclude_defaults=True)
full_result = {
⋮----
yaml_output = yaml.safe_dump(
⋮----
json_output = json.dumps(full_result, indent=2)
⋮----
@click.pass_context
def detect_repo_map(ctx: click.Context, path: str, output: str) -> None
⋮----
"""Create a map of files and folders in a repository for further analysis."""
⋮----
summary = directory_summary(path)
⋮----
details = directory_details(target_path=path, file_lookup=FileExtensionLookup())
⋮----
result = {
⋮----
json_output = json.dumps(result, indent=2)
⋮----
@click.pass_context
def detect_repo(ctx: click.Context, path: str, output: str) -> None
⋮----
"""Detect the codebase."""
⋮----
# Create DetectionManager with all analyses enabled
config = DetectionManagerConfig.all_enabled()
project = DetectionManager.from_path(path, logger, config)
⋮----
run_result = project.run_all()
⋮----
yaml_output = project.to_yaml()
⋮----
json_output = project.to_json()
⋮----
summary_output = project.summary()
⋮----
"""Comprehensive repository analysis and MetagitConfig generation using DetectionManager."""
⋮----
app_config = ctx.obj["config"]
⋮----
# Configure providers
⋮----
# Try to load AppConfig and configure providers
⋮----
# app_config = AppConfig.load()
⋮----
# Fall back to environment variables
⋮----
# Use environment variables only
⋮----
# Override with CLI options if provided
⋮----
# Clear existing providers and configure with CLI options
⋮----
github_provider = GitHubProvider(
⋮----
gitlab_provider = GitLabProvider(
⋮----
# Log configured providers
providers = registry.get_all_providers()
⋮----
provider_names = [p.get_name() for p in providers]
⋮----
# Default to current directory if no path or URL provided
path = os.getcwd()
⋮----
# if not output and not save:
#     output = "summary"
⋮----
detection_manager = DetectionManager.from_path(path, logger, config)
⋮----
detection_manager = DetectionManager.from_url(url, temp_dir, logger, config)
⋮----
# Run all analyses
run_result = detection_manager.run_all()
⋮----
result = None
⋮----
# Output all detection data including MetagitRecord fields
⋮----
result = detection_manager.model_dump(
⋮----
result = yaml.safe_dump(
⋮----
result = detection_manager.to_yaml()
⋮----
# Convert to MetagitConfig (remove detection-specific fields)
config_data = detection_manager.model_dump(
⋮----
result = detection_manager.summary()
⋮----
result = yaml.dump(
⋮----
result = json.dumps(
⋮----
# Save as MetagitConfig (not DetectionManager)
⋮----
# Clean up if this was a cloned repository
````

## File: src/metagit/core/mcp/services/workspace_search.py
````python
#!/usr/bin/env python
"""
Workspace-scoped search service using ripgrep with a bounded fallback scanner.
"""
⋮----
class WorkspaceSearchService
⋮----
"""Search configured repositories with bounded, text-based matching."""
⋮----
_preset_terms: dict[str, list[str]] = {
⋮----
_intent_globs: dict[str, list[str]] = {
⋮----
_default_exclude: list[str] = [
⋮----
_generated_exclude: list[str] = [
⋮----
_category_rules: list[tuple[str, list[str]]] = [
⋮----
"""Search across scoped repository paths and return bounded hits."""
⋮----
glob_paths = list(paths or [])
⋮----
glob_paths = list(dict.fromkeys(glob_paths + self._intent_globs[intent]))
⋮----
glob_paths = list(dict.fromkeys(glob_paths + self._intent_globs[preset]))
exclude_globs = list(dict.fromkeys((exclude or []) + self._default_exclude))
search_query = self._compose_query(query=query, preset=preset)
results: list[dict[str, Any]] = []
⋮----
root = Path(repo_path).expanduser().resolve()
⋮----
remaining = max_results - len(results)
⋮----
hits: list[dict[str, Any]] = []
⋮----
hits = self._search_with_rg(
⋮----
hits = self._search_fallback(
⋮----
"""Resolve repo path list from index rows and optional repo selectors."""
⋮----
selectors = {item.strip() for item in repos if item.strip()}
selected: list[str] = []
⋮----
repo_path = str(row.get("repo_path", ""))
repo_name = str(row.get("repo_name", ""))
project_name = str(row.get("project_name", ""))
keys = {repo_path, repo_name, project_name, f"{project_name}/{repo_name}"}
⋮----
def _compose_query(self, query: str, preset: Optional[str]) -> str
⋮----
"""Build ripgrep query text from user query and optional preset terms."""
terms = self._terms(query=query, preset=preset)
⋮----
"""Run ripgrep and parse JSON output into hit records."""
⋮----
cmd = [
⋮----
completed = subprocess.run(
⋮----
context_before: list[str] = []
context_after: list[str] = []
⋮----
payload = json.loads(line)
⋮----
message_type = payload.get("type")
data = payload.get("data", {})
⋮----
context_text = str(data.get("lines", {}).get("text", "")).rstrip("\n")
⋮----
path_text = str(data.get("path", {}).get("text", ""))
line_number = int(data.get("line_number", 0))
line_text = str(data.get("lines", {}).get("text", "")).rstrip("\n")
hit = {
⋮----
context_before = []
context_after = []
⋮----
"""Fallback scanner when ripgrep is unavailable."""
raw_terms = [term for term in self._terms(query=query, preset=preset) if term]
terms = [term.lower() for term in raw_terms]
⋮----
terms = [t.lower() for t in query.split() if t]
⋮----
lines = file_path.read_text(encoding="utf-8").splitlines()
⋮----
lower_line = line.lower()
⋮----
"""Discover files by intent or glob pattern across repositories."""
glob_paths: list[str] = []
⋮----
glob_paths = ["**/*"]
exclude_globs = list(self._default_exclude)
⋮----
files: list[dict[str, str]] = []
⋮----
discovered = self._list_files_with_rg(
⋮----
payload: dict[str, Any] = {
⋮----
"""List files in a repo root using ripgrep or directory walk."""
⋮----
cmd = ["rg", "--files", "--no-messages"]
⋮----
completed = None
⋮----
lines = [
⋮----
"""Fallback file listing without ripgrep."""
_ = exclude_globs
results: list[str] = []
⋮----
"""Group discovered files into coarse categories."""
categories: dict[str, list[dict[str, str]]] = {}
⋮----
category = self._category_for_path(file_path=item.get("file_path", ""))
⋮----
def _category_for_path(self, file_path: str) -> str
⋮----
"""Assign a category label for a file path."""
lower = file_path.lower()
⋮----
def _terms(self, query: str, preset: Optional[str]) -> list[str]
⋮----
query_terms = [term for term in query.split() if term]
⋮----
def _is_ignored(self, file_path: Path) -> bool
⋮----
name = file_path.name
````

## File: src/metagit/data/skills/metagit-cli/SKILL.md
````markdown
---
name: metagit-cli
description: CLI-only shortcuts for metagit agents — workspace catalog, discovery, prompts, sync, layout, and config. Use instead of MCP or HTTP API when operating from a shell or agent_mode session.
---

# Metagit CLI (agent shortcuts)

Use this skill when an agent should drive metagit **only through the `metagit` command**. Do not call MCP tools or `metagit api` from workflows covered here unless the user explicitly asks.

Set non-interactive defaults when automating:

```bash
export METAGIT_AGENT_MODE=true
```

Global flags (most commands):

- `-c path/to/metagit.config.yaml` — app config (default `metagit.config.yaml`)
- Workspace manifest: `--definition` / `-c` on catalog commands (default `.metagit.yml`)

---

## Manifest editing fast map (`.metagit.yml`)

Use this table first when changing a workspace manifest from the CLI. Prefer **catalog commands** for projects/repos; use **`config patch`** for everything else in the schema (documentation, graph, dedupe overrides, nested fields).

| Task | Command |
|------|---------|
| **Inspect** manifest on disk | `metagit config show -c .metagit.yml` |
| **Inspect** normalized model | `metagit config show -c .metagit.yml --normalized` |
| **Inspect** as JSON (agents) | `metagit config show -c .metagit.yml --json` |
| **Browse** fields / paths | `metagit config tree -c .metagit.yml` or `… --json` |
| **Validate** after edits | `metagit config validate -c .metagit.yml` |
| **Dry-run** schema change | `metagit config preview -c .metagit.yml --file ops.json` |
| **Apply** schema change | `metagit config patch -c .metagit.yml --file ops.json --save` |
| **Set one field** | `metagit config patch -c .metagit.yml --op set --path <path> --value <v> --save` |
| **Enable** optional block | `metagit config patch … --op enable --path <path> --save` |
| **Add list item** | `metagit config patch … --op append --path <list.path> --save` then `set` on `[index].field` |
| **Remove list item** | `metagit config patch … --op remove --path <list.path>[index] --save` |

### Catalog shortcuts (projects & repos)

| Task | Command |
|------|---------|
| List projects | `metagit workspace project list -c .metagit.yml --json` |
| List repos (all / one project) | `metagit workspace repo list -c .metagit.yml --json` / `… --project <p> --json` |
| Add project | `metagit workspace project add -c .metagit.yml --name <p> --json` |
| Remove project | `metagit workspace project remove -c .metagit.yml --name <p> --json` |
| Rename project (dry-run) | `metagit workspace project rename -c .metagit.yml --name <old> --new-name <new> --dry-run --json` |
| Add repo | `metagit workspace repo add -c .metagit.yml --project <p> --name <r> --url <url> --json` |
| Remove repo (manifest only) | `metagit workspace repo remove -c .metagit.yml --project <p> --name <r> --json` |
| Rename / move repo (dry-run) | `metagit workspace repo rename …` / `metagit workspace repo move … --dry-run --json` |
| Search before adding | `metagit search "<name>" -c .metagit.yml --json` |

Active project context (optional): `metagit project repo add --name <r> --url <url>` after `metagit project select`.

### Schema patch examples (`ops.json`)

Paths use dot/bracket notation (same as web Config Studio). `--value` accepts JSON for objects.

```json
{
  "operations": [
    { "op": "set", "path": "name", "value": "my-workspace" },
    { "op": "enable", "path": "documentation" },
    { "op": "append", "path": "documentation" },
    { "op": "set", "path": "documentation[0]", "value": { "kind": "markdown", "path": "AGENTS.md" } },
    { "op": "enable", "path": "graph" },
    { "op": "append", "path": "graph.relationships" },
    {
      "op": "set",
      "path": "graph.relationships[0]",
      "value": {
        "id": "api-depends-infra",
        "from": { "project": "platform", "repo": "api" },
        "to": { "project": "infra", "repo": "terraform" },
        "type": "depends_on"
      }
    },
    { "op": "set", "path": "workspace.projects[0].dedupe.enabled", "value": false }
  ]
}
```

```bash
metagit config preview -c .metagit.yml --file ops.json
metagit config patch -c .metagit.yml --file ops.json --save
metagit config validate -c .metagit.yml
```

### App config vs manifest

| File | Scope | Edit via |
|------|-------|----------|
| `.metagit.yml` | Workspace manifest (projects, repos, docs, graph) | `metagit config …` + `metagit workspace …` |
| `metagit.config.yaml` | Tooling (paths, dedupe default, providers, profiles) | `metagit appconfig …` |

```bash
metagit appconfig show --format minimal-yaml
metagit appconfig patch --op set --path workspace.dedupe.enabled --value false --save
metagit config providers --show
```

### After every manifest edit

1. `metagit config validate -c .metagit.yml`
2. `metagit workspace list -c .metagit.yml --json` (sanity-check catalog)
3. If repos changed on disk: `metagit project sync` or `metagit project sync --hydrate`

### Export manual graph to GitNexus (Cypher)

| Task | Command |
|------|---------|
| Full export bundle (JSON) | `metagit config graph export -c .metagit.yml --json` |
| Raw Cypher script | `metagit config graph export -c .metagit.yml --format cypher -o graph.cypher` |
| MCP tool_calls only | `metagit config graph export -c .metagit.yml --format tool-calls` |
| Manual edges only | `metagit config graph export -c .metagit.yml --manual-only --format tool-calls` |
| MCP from agent | `metagit_export_workspace_graph_cypher` |

Ingest workflow: run `schema_statements` once via `gitnexus_cypher`, then each statement in `tool_calls` (or pipe `--format cypher`). Overlay tables: `MetagitEntity`, `MetagitLink`.

---

## Prompt commands (all kinds)

List built-in prompt kinds:

```bash
metagit prompt list
metagit prompt list --json
```

Emit prompts (`--text-only` for paste into agent context; `--json` for structured output; `--no-instructions` to omit manifest layers):

| Scope | Command | Default kind |
|-------|---------|--------------|
| Workspace | `metagit prompt workspace -c <definition> -k <kind>` | `instructions` |
| Project | `metagit prompt project -p <project> -c <definition> -k <kind>` | `instructions` |
| Repo | `metagit prompt repo -p <project> -n <repo> -c <definition> -k <kind>` | `instructions` |

### Prompt kinds by scope

| Kind | Workspace | Project | Repo | Purpose |
|------|:---------:|:-------:|:----:|---------|
| `instructions` | yes | yes | yes | Composed `agent_instructions` from manifest layers |
| `session-start` | yes | — | — | Session bootstrap checklist |
| `catalog-edit` | yes | yes | — | Search-before-create; catalog registration |
| `health-preflight` | yes | yes | — | Pre-work workspace/repo status pass |
| `sync-safe` | yes | yes | yes | Guarded sync rules |
| `subagent-handoff` | — | yes | yes | Delegate single-repo work |
| `layout-change` | yes | yes | yes | Rename/move dry-run workflow |
| `repo-enrich` | — | — | yes | **Discover + merge** workspace repo entry |

### Prompt shortcuts (copy-paste)

```bash
# Session bootstrap
metagit prompt workspace -k session-start --text-only -c .metagit.yml

# Composed instructions at each level
metagit prompt workspace -k instructions --text-only -c .metagit.yml
metagit prompt project -p default -k instructions --text-only -c .metagit.yml
metagit prompt repo -p default -n my-api -k instructions --text-only -c .metagit.yml

# Repo catalog enrichment (detect + merge manifest entry)
metagit prompt repo -p default -n my-api -k repo-enrich --text-only -c .metagit.yml

# Catalog registration discipline
metagit prompt workspace -k catalog-edit --text-only -c .metagit.yml

# Safe sync reminder
metagit prompt repo -p default -n my-api -k sync-safe --text-only -c .metagit.yml

# Subagent handoff
metagit prompt repo -p default -n my-api -k subagent-handoff --text-only -c .metagit.yml
```

---

## Repo enrich workflow (`repo-enrich`)

Run the prompt, then execute its steps:

```bash
metagit prompt repo -p <project> -n <repo> -k repo-enrich --text-only -c .metagit.yml
```

Typical discovery chain on the checkout:

```bash
cd "$(metagit search '<repo>' -c .metagit.yml --path-only)"
metagit detect repository -p . -o json
metagit detect repo -p . -o yaml
metagit detect repo_map -p . -o json
```

Provider metadata (dry-run):

```bash
metagit project source sync --provider github --org <org> --mode discover --no-apply
```

After merging fields into `workspace.projects[].repos[]`:

```bash
metagit config validate -c .metagit.yml
metagit workspace repo list --project <project> --json
```

---

## Workspace and catalog

Per-project dedupe override in `.metagit.yml` (overrides `workspace.dedupe.enabled` in `metagit.config.yaml` for that project only):

```yaml
workspace:
  projects:
    - name: local
      dedupe:
        enabled: false
      repos: []
```

```bash
metagit appconfig show --format json
metagit config info -c .metagit.yml
metagit config show -c .metagit.yml
metagit config validate -c .metagit.yml

metagit workspace list -c .metagit.yml --json
metagit workspace project list -c .metagit.yml --json
metagit workspace repo list -c .metagit.yml --json
metagit workspace repo list -c .metagit.yml --project <name> --json

metagit workspace project add --name <name> --json
metagit workspace repo add --project <name> --name <repo> --url <url> --json
metagit workspace project remove --name <name> --json
metagit workspace repo remove --project <name> --name <repo> --json
```

Search managed repos (always before creating entries):

```bash
metagit search "<query>" -c .metagit.yml --json
metagit search "<query>" -c .metagit.yml --path-only
metagit search "<query>" -c .metagit.yml --tag tier=1 --project <name>
```

---

## Project operations

```bash
metagit project list --config .metagit.yml --all --json
metagit project add --name <name> --json
metagit project remove --name <name> --json
metagit project rename --name <old> --new-name <new> --dry-run --json
metagit project select
metagit project sync
metagit project sync --hydrate   # symlink mounts → full directory copies (per-file progress)

metagit project repo list --json
metagit project repo add --project <name> --name <repo> --url <url>
metagit project repo remove --name <repo> --json
metagit project repo rename --name <old> --new-name <new> --dry-run --json
metagit project repo move --name <repo> --to-project <other> --dry-run --json
metagit project repo prune --project <name> --dry-run

metagit project source sync --provider github --org <org> --mode discover --no-apply
metagit project source sync --provider github --org <org> --mode additive --apply
```

Layout (manifest + disk; always dry-run first):

```bash
metagit workspace project rename --name <old> --new-name <new> --dry-run --json
metagit workspace repo rename --project <p> --name <old> --new-name <new> --dry-run --json
metagit workspace repo move --project <p> --name <repo> --to-project <other> --dry-run --json
```

---

## Discovery and local metadata

```bash
metagit detect project -p <path> -o yaml
metagit detect repo -p <path> -o yaml
metagit detect repo_map -p <path> -o json
metagit detect repository -p <path> -o json
metagit detect repository -p <path> -o metagit
# --save only with operator approval (blocked in agent_mode)
```

Bootstrap new trees:

```bash
metagit init --kind application
metagit init --kind umbrella --template hermes-orchestrator
```

---

## Selection and scope

```bash
metagit workspace select --project <name>
metagit project select
metagit project repo select
```

---

## Config and appconfig (reference)

See **Manifest editing fast map** above for day-to-day manifest work. Additional commands:

```bash
metagit config info -c .metagit.yml
metagit config example
metagit config schema
metagit appconfig validate
metagit appconfig get --name config.workspace.path
metagit appconfig tree --json
metagit appconfig preview --file ops.json
metagit appconfig patch --file ops.json --save
metagit config providers --show
```

---

## Records, skills, version

```bash
metagit record search "<query>"
metagit skills list
metagit skills show metagit-cli
metagit skills install --skill metagit-cli
metagit version
metagit info
```

---

## Agent habits

1. **Search before create** — `metagit search` then catalog add.
2. **Validate after manifest edits** — `metagit config validate`.
3. **Emit prompts instead of rewriting playbooks** — `metagit prompt … --text-only`.
4. **Enrich stale repo entries** — `metagit prompt repo … -k repo-enrich` then detect + merge.
5. **Dry-run layout** — always `--dry-run --json` before apply.
6. **Prefer `METAGIT_AGENT_MODE=true`** in CI and agent loops to skip fuzzy finder and confirm dialogs.

## Related bundled skills

Use topic skills when you need deeper playbooks (some mention MCP): `metagit-projects`, `metagit-workspace-scope`, `metagit-workspace-sync`, `metagit-config-refresh`. This skill is the **CLI-only** index and prompt reference.
````

## File: src/metagit/data/skills/README.md
````markdown
# Bundled Metagit skills

Installed via `metagit skills install`. All names use the `metagit-` prefix.

- `metagit-cli` — CLI-only agent shortcuts (prompts, catalog, detect, sync; no MCP/API)
- `metagit-projects` — workspace project lifecycle
- `metagit-workspace-scope` — scope discovery
- `metagit-control-center` — multi-repo control center
- `metagit-workspace-sync` — guarded sync
- `metagit-config-refresh` / `metagit-bootstrap` — config refresh and bootstrap
- `metagit-gating` — MCP gate checks
- `metagit-upstream-scan` / `metagit-upstream-triage` — upstream discovery
- `metagit-repo-impact` / `metagit-multi-repo` — impact and implementation
- `metagit-gitnexus` — GitNexus analysis
- `metagit-release-audit` — release readiness

See [docs/skills.md](../../../../docs/skills.md) for install instructions.
````

## File: tests/core/web/test_schema_tree.py
````python
#!/usr/bin/env python
"""Unit tests for SchemaTreeService."""
⋮----
def test_build_metagit_tree_marks_present_fields() -> None
⋮----
service = SchemaTreeService()
config = MetagitConfig.model_validate({"name": "demo", "kind": "application"})
root = service.build_tree(config, MetagitConfig)
⋮----
name_node = service.find_node(root, "name")
kind_node = service.find_node(root, "kind")
⋮----
def test_enum_field_exposes_all_options() -> None
⋮----
def test_disable_optional_field_removes_key() -> None
⋮----
def test_enable_optional_field_adds_default() -> None
⋮----
def test_set_field_updates_value() -> None
⋮----
def test_appconfig_sensitive_field_masked_in_tree() -> None
⋮----
raw = {
config = AppConfig(**raw)
root = service.build_tree(config, AppConfig, mask_secrets=True)
token_node = service.find_node(root, "providers.github.api_token")
⋮----
def test_apply_operations_returns_original_instance_on_validation_error() -> None
⋮----
def test_sensitive_token_unchanged_after_masked_set() -> None
⋮----
def test_type_label_shows_model_name() -> None
⋮----
config = MetagitConfig.model_validate(
⋮----
workspace_node = service.find_node(root, "workspace")
⋮----
def test_enable_optional_list_defaults_empty() -> None
⋮----
def test_append_and_remove_list_items() -> None
⋮----
root = service.build_tree(appended, MetagitConfig)
artifacts_node = service.find_node(root, "artifacts")
````

## File: mise.toml
````toml
[env]
_.source = '.envrc'

[settings]
python.uv_venv_auto = true

[tools]
uv = "latest"
python = "3.12"
task = "latest"
npm = "latest"
gh = "latest"
gitleaks = "latest"
glow = "latest"
rg = "latest"
node = "latest"
"npm:repomix" = "latest"
````

## File: .cursor/rules/project-level.mdc
````
---
description: Metagit
globs: *
alwaysApply: true
---

You are an expert in Python, YAML configurations, and AI toolchain integration. Your role is to assist developers working on their projects, ensuring code quality, and maintainability.

## Code Style and Structure

- Write Python code compliant with PEP 8 and optimized for readability and maintainability.
- Use type hints for all function parameters and return types.
- Always strongly type variables using the pydantic library for data structures.
- Always use Protocol definitions for interface definitions.
- Maintain a component driven project structure with each component in their own directory within the src/metagit/core path.
- Avoid duplication by using clear, modular code organization.
- All file paths should be constructed using os.path.join() instead of string concatenation.
- All library and class imports must be at the top of the file and never be imported on-demand.
- Remove all unused imports from each python file after editing them.
- Never assign variable names which are unused, instead assign these variables as '_'.
- Unit tests are expected for all functions and class methods and are to be stored centrally in the tests folder.
- Combine if statements instead of nesting them where possible.
- Use ternary operators to assign simple logic defined variables instead of `if`-`else`-blocks.
- Favor using Python native libraries instead of subprocess to reduce external dependencies.
- Use 2 spaces for indentation.
- Private class members should be prefixed with an underscore (`_`).
- Use `isinstance()` for type comparisons.

## Naming Conventions

- Use snake_case for variables, functions, and filenames.
- Use PascalCase for class names.
- Prefix environment variables with provider name (e.g., `OLLAMA_`, `OPENAI_`).
- Use descriptive names for configuration files (e.g., `agents.yaml`, `tasks.yaml`).

## Environment and Configuration

- Use `python-dotenv` to manage environment variables.
- Maintain `.env.example` as a template for environment setup.
- Structure YAML files clearly and validate on load:
  - Use `yaml.safe_load` for security.
  - Include clear error messages for missing or invalid keys.

## Syntax and Formatting

- New Python files should always include `#!/usr/bin/env python` as the very first line.
- Format code with tools like Black and lint with Flake8.
- Follow Pythonic idioms and best practices.
- Use early returns in functions to simplify logic.
- Write clear docstrings for all classes and functions.

## Error Handling and Validation

- Validate environment variables at startup.
- Use try-except blocks with meaningful error messages.
- Never create bare exception statements.
- Be as explicit as possible when handling exceptions.
- Log errors appropriately using the UnifiedLogger module.
- Ensure secure loading of configuration files.
- All functions and methods that produce exceptions should return a union of the expected result type and Exception and be handled appropriately when called.

## Regarding Dependencies

- Avoid introducing new external dependencies unless absolutely necessary.
- If a new dependency is required, please state the reason.

## Security

- Never hardcode sensitive data; always use environment variables.
- Keep API keys and sensitive data in `.env` (gitignored).
- Sanitize all inputs passed to external services.

## Documentation

- Maintain clear and comprehensive README.md:
  - Installation and setup instructions.
  - Environment configuration examples.
  - YAML file examples and structure.
- Document code with clear inline comments.
- Keep CHANGELOG.md updated with all changes.

## Project Structure

- Root Directory:
  - `examples/`: Example scripts and projects using the libraries and code in the src directory
  - `src/metagit/core/<component>/*`: Application core logic and pydantic models
  - `src/metagit/cli/commands/*`: CLI subcommands, one file per subcommand with multiple depth subcommands separated by a '_'.
  - `docs/`: Documentation as markdown
  - `tests/`: Unit tests

## Command-Line Tools

### GitHub
- Use the `gh` command-line to interact with GitHub.

### Markdown
- Use the `glow` command-line to present markdown content.

### JSON
- Use the `jq` command to read and extract information from JSON files.

### RipGrep
- The `rg` (ripgrep) command is available for fast searches in text files.

### Clipboard
- Pipe content into `pbcopy` to copy it into the clipboard. Example: `echo "hello" | pbcopy`.
- Pipe from `pbpaste` to get the contents of the clipboard. Example: `pbpaste > fromclipboard.txt`.

### Python
- Unless instructed otherwise, always use the `uv` Python environment and package manager for Python.
  - `uv run ...` for running a python script.
  - `uvx ...` for running program directly from a PyPI package.
  - `uv ... ...` for managing environments, installing packages, etc...

### JavaScript / TypeScript
- Unless instructed otherwise, always use `deno` to run .js or .ts scripts.
- Use `npx` for running commands directly from npm packages.

## Task closeout

- After changes to this codebase, before implying the task is finished, run **`task qa:prepush`** from the repository root (`scripts/prepush-gate.zsh`). It runs format, lint, unit tests, integration tests, and context-aware security (`pip-audit` / `bandit` when `src/` or lockfiles changed). Fix failures and re-run until it passes unless the conversation was read-only (no edits) or the user explicitly waived the gate.

## Documentation Sources
- If working with a new library or tool, consider looking for its documentation from its website, GitHub project, or the relevant llms.txt.
  - It is always better to have accurate, up-to-date documentation at your disposal, rather than relying on your pre-trained knowledge.
- You can search the following directories for llms.txt collections for many projects:
  - https://llmstxt.site/
  - https://directory.llmstxt.cloud/
- If you find a relevant llms.txt file, follow the links until you have access to the complete documentation.
````

## File: .mex/context/conventions.md
````markdown
---
name: conventions
description: How code is written in this project — naming, structure, patterns, and style. Load when writing new code or reviewing existing code.
triggers:
  - "convention"
  - "pattern"
  - "naming"
  - "style"
  - "how should I"
  - "what's the right way"
edges:
  - target: context/architecture.md
    condition: when a convention depends on understanding the system structure
  - target: context/stack.md
    condition: when selecting libraries/tools for a change
  - target: patterns/INDEX.md
    condition: when implementing task-specific workflows and verify/debug sequences
last_updated: 2026-05-15
---

# Conventions

## Naming
- Python files/modules/functions/variables use **snake_case**; classes use **PascalCase**.
- CLI subcommand modules are one file per command under `src/metagit/cli/commands/` (e.g., `project.py`, `mcp.py`).
- Private class members and internals are prefixed with `_` (consistent with core services/runtime patterns).
- Config file names are explicit (`.metagit.yml`, `metagit.config.yaml`, schema files under `schemas/`).

## Structure
- CLI entrypoint stays in `src/metagit/cli/main.py`; reusable logic belongs in `src/metagit/core/*`, not command functions.
- Each core concern is grouped by component directory (`config`, `detect`, `record`, `mcp`, `utils`, etc.) with focused manager/service classes.
- Tests live in centralized `tests/` and mirror module responsibility by filename (`test_*`), not colocated beside source files.
- MCP runtime flow is split into thin runtime dispatch + dedicated service classes (`workspace_index`, `workspace_search`, `upstream_hints`, `repo_ops`).

## Patterns
Prefer explicit exception-return handling in manager/service methods (consistent with existing union return style):
```python
result = manager.load_config()
if isinstance(result, Exception):
    return result
return result
```

Use state-based gating before exposing tool actions in MCP/runtime logic:
```python
allowed = set(registry.list_tools(status=status))
if tool_name not in allowed:
    raise InvalidToolArgumentsError(...)
```

Use bounded operations for search/sync style tasks:
```python
hits = search_service.search(query=query, repo_paths=paths, max_results=25)
if mode in {"pull", "clone"} and not allow_mutation:
    return {"ok": False, "error": "..."}
```

## Verify Checklist
Before presenting any code:
- [ ] New/changed CLI behavior is covered by command-level tests under `tests/cli` or integration tests when flow crosses modules.
- [ ] Core logic changes are covered by targeted unit tests under `tests/` for the touched service/manager/runtime area.
- [ ] `.metagit.yml`/config model interactions still pass validation paths (no silent schema drift).
- [ ] Lint/format checks pass via project commands (`task lint`, and format if needed).
- [ ] Mutating operations remain explicitly guarded (especially MCP sync/tool paths).
- [ ] Run `task skills:sync generate:schema` for session closeout sync/schema regeneration.
- [ ] **`task qa:prepush`** from repo root has passed — required before declaring work complete whenever this session modified project files; do not summarize success without evidence. Omit only if the session was read-only or the user explicitly waived the gate.

## Commit Semantics
- Default to `fix:` commit prefixes (patch intent) for normal maintenance and safe behavior updates.
- Use `feat:` only for additive backward-compatible capabilities.
- Use `type(scope)!:` or `BREAKING CHANGE:` only when changes intentionally break schema/config compatibility.
````

## File: docs/cli_reference.md
````markdown
# CLI Reference

This page contains the auto-generated documentation for the `metagit` command-line interface.

::: mkdocs-click
    :module: metagit.cli.main
    :command: cli
    :prog_name: metagit 

## MCP Command Notes

- Start MCP stdio runtime:
  - `metagit mcp serve`
- Start against a specific workspace root:
  - `metagit mcp serve --root /path/to/workspace`
- Print status snapshot and exit:
  - `metagit mcp serve --status-once`
- When the workspace gate is **active**, the tool **`metagit_repo_search`** searches only repos listed under `workspace.projects[].repos` in `.metagit.yml` (tags, sync status, resolved paths). Optional filters: `status[]`, `has_url`, `sync_enabled`, `sort` (`score`|`project`|`name`). Use query `*` for filter-only listing.
- **`metagit_workspace_search`** uses ripgrep when available (`repos`, `paths`, `exclude`, `context_lines`, `intent`, `include_paths`). Falls back to a bounded scanner if `rg` is not installed.
- **`metagit_workspace_sync`** batch-syncs repos (`repos: ["all"]` or selectors), with `only_if` (`any`|`missing`|`dirty`|`behind_origin`), `max_parallel`, and `dry_run`.
- **`metagit_cross_project_dependencies`** maps relationships from a `source_project` using `dependency_types` (`declared`, `imports`, `shared_config`, `url_match`, `ref`), `depth`, and per-repo GitNexus `graph_status` (`indexed`|`stale`|`missing`).
- **`metagit_workspace_health_check`** returns per-repo git/GitNexus signals, optional staleness metrics (`head_commit_age_days`, `merge_base_age_days` when `check_stale_branches`), tunable thresholds (`branch_head_warning_days`, `branch_head_critical_days`, `integration_stale_days`), and prioritized recommendations (`sync`, `analyze`, `clone`, `fix_config`, `review_branch_age`, `reconcile_integration`, …).
- **`metagit_workspace_semantic_search`** runs `npx gitnexus query -r <registry>` per selected repo (`query` required; optional `repos`, `task_context`, `goal`, `limit_per_repo`, `timeout_seconds`) for GitNexus-ranked processes.
- **`metagit_workspace_discover`** lists files by `intent` or `pattern` with optional `categorize` grouping (requires `intent` or `pattern`).
- **`metagit_project_template_apply`** previews or applies bundled templates from `src/metagit/data/templates/` (`dry_run` default; `confirm_apply` required for writes).
- **Resources (active gate):** `metagit://workspace/health`, `metagit://workspace/context` (active project session from `.metagit/sessions/`).
- **Project context (active gate):**
  - `metagit_project_context_switch` — set active workspace project; return repo branch/dirty summary, safe env exports (`METAGIT_*`), and restored session fields from `.metagit/sessions/<project>.json`.
  - `metagit_session_update` — persist `agent_notes`, `recent_repos`, and non-secret `env_overrides` before switching away.
  - `metagit_workspace_state_snapshot` — write a git-state manifest to `.metagit/snapshots/<id>.json` (not a file copy).
  - `metagit_workspace_state_restore` — reload snapshot metadata and optionally re-run context switch; does **not** reset git branches or uncommitted changes.

## Workspace configuration

Under `workspace.projects[].repos`, each repository entry may include a flat string-to-string `tags` map (for example `tier: "1"`). These tags are carried into the workspace index and into `metagit search` / `metagit find` for filtering.

## Managed repository search

- `metagit search QUERY` — list managed repositories from the workspace definition that match the query (name, URL substring, tag keys/values, project name). Only repos declared under `workspace.projects[].repos` are considered. Supports `--status` (repeatable) and `--sort score|project|name`.
- `metagit find QUERY` — alias for `metagit search`.
- `--definition PATH` — `.metagit.yml` to load (default: `.metagit.yml` in the current directory). The workspace root for resolving `path:` entries is the parent directory of that file.
- `--json` — print search results as JSON (matches include `match_reasons` and scores).
- `--path-only` — resolve to exactly one local directory (fails if there is no match or more than one match).
- `--tag key=value` — repeat to require matching tag values (all given pairs must match).
- `--project`, `--exact`, `--synced-only`, and `--limit` narrow or rank results further.

## Local JSON API (`metagit api`)

- `metagit api serve` — bind a `ThreadingHTTPServer` on `--host` / `--port` (default `127.0.0.1:7878`) under `--root` (directory containing `.metagit.yml`).
- `metagit api serve --status-once` — allocate a port (use `--port 0` for ephemeral), print `api_state=ready host=… port=…`, and exit (for tests and automation).
- `GET /v1/repos/search?q=…` — same managed-repo search as the CLI; optional query params: `project`, `exact=true|false`, `synced_only=true|false`, `limit`, repeat `tag=key=value`.
- `GET /v1/repos/resolve?q=…` — single-match resolution; HTTP `404` when not found, `409` when ambiguous (body includes `ManagedRepoResolveResult` JSON).
````

## File: skills/README.md
````markdown
# Metagit skills

Metagit ships agent **skills** (`skills/metagit-*/SKILL.md`): short playbooks for when and how to use the CLI and MCP. All bundled skills use a `metagit-` prefix.

## Install the CLI

```bash
uv tool install metagit-cli
uv tool install -U metagit-cli   # upgrade
metagit version
```

> PyPI package name: **`metagit-cli`** (not `metagit`).

## Install skills

```bash
metagit skills list
metagit skills show metagit-projects
metagit skills install
metagit skills install --scope user --target openclaw --target hermes
metagit skills install --skill metagit-projects --target openclaw
metagit skills install --skill metagit-projects --target openclaw --dry-run
```

Use `--skill` (repeatable) to install one or more bundled skills instead of the full set. Omit `--skill` to install every bundled skill. Add `--dry-run` to preview targets and paths without writing files.

Optional MCP registration:

```bash
metagit mcp install --scope user --target openclaw --target hermes
```

Other targets: `opencode`, `claude_code`, `github_copilot`.

## Skill catalog

| Skill | Use when |
|-------|----------|
| `metagit-cli` | CLI-only agent workflows: all `metagit prompt` kinds, catalog, detect, sync, layout (no MCP/API) |
| `metagit-projects` | Starting work; check for existing projects/repos before new folders |
| `metagit-workspace-scope` | Session start; workspace and sync context |
| `metagit-control-center` | Ongoing multi-repo coordination |
| `metagit-workspace-sync` | Guarded fetch/pull/clone |
| `metagit-config-refresh` | `.metagit.yml` missing or stale |
| `metagit-bootstrap` | Generate or refine config with discovery/MCP |
| `metagit-gating` | MCP workspace gate status |
| `metagit-upstream-scan` | Search other managed repos for causes |
| `metagit-upstream-triage` | Rank upstream blockers |
| `metagit-repo-impact` | Plan cross-repo changes |
| `metagit-multi-repo` | Implement across several repos |
| `metagit-gitnexus` | GitNexus index and graph workflows |
| `metagit-release-audit` | Pre-push / release readiness |

For workspace vs project vs repo definitions, see [Terminology](terminology.md).

## Source development

```bash
task skills:validate
task skills:sync    # mirrors into .cursor/skills/
```

Update both `skills/` and `src/metagit/data/skills/` when changing bundled skills.
````

## File: src/metagit/cli/commands/project.py
````python
"""
Project subcommand
"""
⋮----
@click.pass_context
def project(ctx: click.Context, config: str, project: str = None) -> None
⋮----
"""Project subcommands"""
logger = ctx.obj["logger"]
# If no subcommand is provided, show help
⋮----
app_config: AppConfig = ctx.obj["config"]
⋮----
project: str = app_config.workspace.default_project
⋮----
config_manager: MetagitConfigManager = MetagitConfigManager(config)
local_config: MetagitConfig = config_manager.load_config()
⋮----
# Add repo group to project group
⋮----
@click.pass_context
def project_list(ctx: click.Context, list_all: bool, as_json: bool) -> None
⋮----
"""List project configuration (YAML, JSON, or all projects)."""
logger: UnifiedLogger = ctx.obj["logger"]
project: str = ctx.obj["project"]
local_config: MetagitConfig = ctx.obj["local_config"]
⋮----
service = WorkspaceCatalogService()
⋮----
result = service.list_projects(local_config)
⋮----
# Handle special "local" project case
⋮----
# Check if there's an existing project named "local" in workspace
⋮----
workspace_project: WorkspaceProject = next(
⋮----
# Use existing "local" project
project_dict = workspace_project.model_dump(exclude_none=True)
⋮----
# Use computed local_workspace_project
workspace_project = local_config.local_workspace_project
⋮----
# No workspace config, use computed local_workspace_project
⋮----
# Handle regular project names
⋮----
yaml_output = yaml.dump(project_dict, default_flow_style=False, sort_keys=False)
⋮----
"""Add a workspace project to the manifest."""
⋮----
config_path: str = ctx.obj["config_path"]
result = WorkspaceCatalogService().add_project(
⋮----
@click.pass_context
def project_remove(ctx: click.Context, name: str, as_json: bool) -> None
⋮----
"""Remove a workspace project from the manifest."""
⋮----
result = WorkspaceCatalogService().remove_project(
⋮----
"""Rename a workspace project (alias for workspace project rename)."""
⋮----
workspace_root = str(Path(app_config.workspace.path).expanduser().resolve())
dedupe = resolve_dedupe_for_layout(
result = WorkspaceLayoutService().rename_project(
⋮----
@project.command("select")
@click.pass_context
def project_select(ctx: click.Context) -> None
⋮----
"""Shortcut: Uses 'project repo select' to select workspace project repo to work on"""
# Call the repo_select function
⋮----
@click.pass_context
def project_sync(ctx: click.Context, hydrate: bool) -> None
⋮----
"""Sync project within workspace"""
⋮----
project_manager: ProjectManager = project_manager_from_app(
⋮----
sync_result: bool = project_manager.sync(workspace_project, hydrate=hydrate)
````

## File: src/metagit/core/mcp/services/workspace_index.py
````python
#!/usr/bin/env python
"""
Workspace repository indexing service.
"""
⋮----
class WorkspaceIndexService
⋮----
"""Build normalized repository status rows from workspace configuration."""
⋮----
"""Return repository index rows for all configured workspace repos."""
rows: list[dict[str, Any]] = []
⋮----
resolved_path = self._resolve_repo_path(
mount = Path(resolved_path)
exists = mount.is_dir() or (
is_git_repo = (
status = "synced" if exists and is_git_repo else "configured_missing"
⋮----
"""Resolve repository mount path (matches project sync layout)."""
⋮----
path = Path(configured_path).expanduser()
````

## File: src/metagit/core/web/schema_tree.py
````python
#!/usr/bin/env python
"""Build and mutate Pydantic config schema trees for the web UI."""
⋮----
_PATH_SEGMENT_RE = re.compile(r"([^.\[\]]+)|\[(\d+|\*)\]")
⋮----
class SchemaTreeService
⋮----
"""Walk Pydantic models into editable schema trees and apply config operations."""
⋮----
SENSITIVE_KEYS = frozenset({"api_token", "token", "password", "secret"})
⋮----
def __init__(self) -> None
⋮----
"""Build schema tree rooted at synthetic node key='root', path=''."""
dump = model_instance.model_dump(exclude_none=True, mode="python")
⋮----
def find_node(self, root: SchemaFieldNode, path: str) -> SchemaFieldNode | None
⋮----
"""Find node by dot/bracket path like name, workspace.projects[0].name."""
⋮----
segments = self._parse_path(path)
⋮----
"""Apply enable/disable/set ops; return (updated_instance, validation_errors)."""
data = instance.model_dump(mode="python")
⋮----
children: list[SchemaFieldNode] = []
⋮----
child_path = self._join_path(parent_path, field_name)
value = getattr(model_instance, field_name, None)
field_dump = dump.get(field_name) if isinstance(dump, dict) else None
enabled = self._field_enabled(field_info, value, field_dump)
annotation = self._unwrap_optional(field_info.annotation)
node_type = self._type_name(annotation)
type_label = self._type_label(annotation)
sensitive = self._is_sensitive(field_name)
display_value = self._display_value(
default_value = self._default_for_field(field_info, annotation)
enum_options = self._enum_options(annotation) if node_type == "enum" else []
list_meta = self._list_node_meta(annotation, field_dump, enabled)
node = SchemaFieldNode(
⋮----
origin = get_origin(annotation)
⋮----
nested_dump = (
⋮----
args = get_args(annotation)
item_annotation = args[0] if args else Any
item_unwrapped = self._unwrap_optional(item_annotation)
is_model = isinstance(item_unwrapped, type) and issubclass(
items = value if isinstance(value, list) else []
⋮----
scalar_label = self._type_label(item_unwrapped)
⋮----
item_type = item_unwrapped
⋮----
item_label = self._type_label(item_type)
⋮----
item_dump = (
item_path = self._join_path(parent_path, f"[{index}]")
⋮----
field_info = model_at_parent.model_fields[str(leaf)]
⋮----
field_name = str(leaf)
field_info = model_at_parent.model_fields[field_name]
⋮----
current = parent.get(field_name)
⋮----
current = []
⋮----
def _set_at_path(self, data: dict[str, Any], path: str, value: Any) -> None
⋮----
parent: Any = data
index = 0
⋮----
segment = segments[index]
next_segment = segments[index + 1]
⋮----
parent = parent[segment]
⋮----
bucket = parent[segment]
⋮----
parent = bucket
⋮----
parent = bucket[next_segment]
⋮----
leaf = segments[-1]
⋮----
"""Return parent container, model class at leaf field, and leaf key/index."""
⋮----
current_class: type[BaseModel] = model_class
⋮----
field_info = current_class.model_fields[segment]
⋮----
current_class = self._list_item_type(annotation)
⋮----
parent = parent[segment][next_segment]
⋮----
current_class = annotation
⋮----
errors = [
⋮----
def _format_error_path(self, loc: tuple[Any, ...]) -> str
⋮----
parts: list[str] = []
⋮----
def _parse_path(self, path: str) -> list[str | int]
⋮----
segments: list[str | int] = []
⋮----
def _join_path(self, parent: str, segment: str) -> str
⋮----
def _unwrap_optional(self, annotation: Any) -> Any
⋮----
args = [arg for arg in get_args(annotation) if arg is not type(None)]
⋮----
def _list_item_type(self, annotation: Any) -> type[BaseModel]
⋮----
annotation = self._unwrap_optional(annotation)
⋮----
item = args[0] if args else Any
⋮----
def _enum_options(self, annotation: Any) -> list[str]
⋮----
def _type_name(self, annotation: Any) -> str
⋮----
def _type_label(self, annotation: Any) -> str
⋮----
inner = args[0] if args else Any
inner_label = self._type_label(inner)
⋮----
count = len(field_dump) if isinstance(field_dump, list) else 0
⋮----
def _default_list_item(self, annotation: Any) -> Any
⋮----
args = get_args(annotation) if get_origin(annotation) is list else (annotation,)
⋮----
item = self._unwrap_optional(item)
⋮----
def _is_sensitive(self, key: str) -> bool
⋮----
def _default_for_field(self, field_info: FieldInfo, annotation: Any) -> Any
⋮----
default = field_info.get_default(call_default_factory=True)
⋮----
def _default_model_dict(self, model_class: type[BaseModel]) -> dict[str, Any]
⋮----
"""Build a valid sample dict for a nested model."""
⋮----
def _accepts_none(self, annotation: Any) -> bool
````

## File: tests/test_appconfig_models.py
````python
#!/usr/bin/env python
"""
Unit tests for metagit.core.appconfig.models
"""
⋮----
def test_boundary_model()
⋮----
b = Boundary(name="internal", values=["foo", "bar"])
⋮----
def test_profiles_model()
⋮----
p = Profile()
⋮----
def test_workspace_model()
⋮----
w = WorkspaceConfig()
⋮----
def test_llm_model()
⋮----
llm = LLM()
⋮----
def test_github_provider_model()
⋮----
gh = GitHubProvider()
⋮----
def test_gitlab_provider_model()
⋮----
gl = GitLabProvider()
⋮----
def test_providers_model()
⋮----
p = Providers()
⋮----
def test_appconfig_defaults()
⋮----
cfg = AppConfig()
⋮----
def test_appconfig_load_and_save(tmp_path)
⋮----
# Create a config file
config_path = os.path.join(tmp_path, "testconfig.yaml")
data = {"config": AppConfig().model_dump(mode="json")}
⋮----
# Load config
cfg = AppConfig.load(str(config_path))
⋮----
# Print debug info on failure
⋮----
# Try to load the YAML directly to see what's in it
⋮----
loaded_data = yaml.safe_load(f)
⋮----
# Save config
save_path = tmp_path / "saved.yaml"
result = cfg.save(str(save_path))
⋮----
# Load saved config
loaded = AppConfig.load(str(save_path))
⋮----
def test_appconfig_load_file_not_found(tmp_path)
⋮----
result = AppConfig.load(str(tmp_path / "nope.yaml"))
⋮----
def test_appconfig_load_ignores_legacy_version_key(tmp_path)
⋮----
"""Older metagit.config.yaml files stored a frozen CLI version; ignore it on load."""
config_path = os.path.join(tmp_path, "legacy.yaml")
⋮----
cfg = AppConfig.load(config_path)
````

## File: web/src/api/client.ts
````typescript
export type ConfigOpKind = 'enable' | 'disable' | 'set' | 'append' | 'remove'
⋮----
export interface ConfigOperation {
  op: ConfigOpKind
  path: string
  value?: unknown
}
⋮----
export interface SchemaFieldNode {
  path: string
  key: string
  type: string
  type_label?: string | null
  description?: string | null
  required?: boolean
  enabled?: boolean
  editable?: boolean
  sensitive?: boolean
  default_value?: unknown
  value?: unknown
  enum_options?: string[]
  item_count?: number | null
  can_append?: boolean
  children?: SchemaFieldNode[]
}
⋮----
export interface ConfigTreeResponse {
  ok: boolean
  target: 'metagit' | 'appconfig'
  config_path: string
  tree: SchemaFieldNode
  validation_errors: Array<Record<string, string>>
  saved?: boolean
}
⋮----
export type ConfigPreviewStyle = 'normalized' | 'minimal' | 'disk'
⋮----
export interface ConfigPreviewResponse {
  ok: boolean
  target: 'metagit' | 'appconfig'
  config_path: string
  style: ConfigPreviewStyle
  yaml: string
  draft?: boolean
  validation_errors: Array<{ path?: string; message?: string }>
}
⋮----
export interface SyncJobRequest {
  repos?: string[] | null
  mode?: 'fetch' | 'pull' | 'clone'
  dry_run?: boolean
  allow_mutation?: boolean
  max_parallel?: number
}
⋮----
export interface SyncJobStatus {
  job_id: string
  state: 'pending' | 'running' | 'completed' | 'failed'
  summary: Record<string, unknown>
  results: Array<Record<string, unknown>>
  error?: string | null
}
⋮----
export class ApiError extends Error
⋮----
constructor(status: number, message: string, body?: unknown)
⋮----
async function requestJson<T>(path: string, init?: RequestInit): Promise<T>
⋮----
export function getMetagitConfigTree(): Promise<ConfigTreeResponse>
⋮----
export function getAppconfigTree(): Promise<ConfigTreeResponse>
⋮----
export function patchMetagitConfig(
  ops: ConfigOperation[],
  save: boolean,
): Promise<ConfigTreeResponse>
⋮----
export function patchAppconfig(
  ops: ConfigOperation[],
  save: boolean,
): Promise<ConfigTreeResponse>
⋮----
export function postConfigPreview(
  target: 'metagit' | 'appconfig',
  style: ConfigPreviewStyle,
  operations: ConfigOperation[],
): Promise<ConfigPreviewResponse>
⋮----
export interface CatalogEnvelope<T> {
  ok: boolean
  error?: { kind: string; message: string } | null
  data?: T
}
⋮----
export interface WorkspaceProjectEntry {
  name: string
  description?: string | null
  agent_instructions?: string | null
  dedupe_enabled?: boolean | null
  repo_count: number
}
⋮----
export interface WorkspaceRepoIndexRow {
  project_name: string
  repo_name: string
  configured_path: string | null
  repo_path: string
  exists: boolean
  is_git_repo: boolean
  status: 'synced' | 'configured_missing' | string
  url?: string | null
  sync?: boolean
  tags?: Record<string, string>
}
⋮----
export interface WorkspaceData {
  summary: Record<string, unknown>
  projects: WorkspaceProjectEntry[]
  repos_index: WorkspaceRepoIndexRow[]
}
⋮----
export interface HealthRecommendation {
  severity: 'info' | 'warning' | 'critical'
  action: string
  message: string
  project_name?: string | null
  repo_name?: string | null
  repo_path?: string | null
}
⋮----
export interface RepoHealthRow {
  project_name: string
  repo_name: string
  repo_path: string
  status: string
  exists: boolean
  is_git_repo: boolean
  branch?: string | null
  dirty?: boolean | null
}
⋮----
export interface WorkspaceHealthResult {
  ok: boolean
  workspace_root: string
  summary: Record<string, number>
  repos: RepoHealthRow[]
  recommendations: HealthRecommendation[]
}
⋮----
export interface PruneCandidate {
  path: string
  name: string
}
⋮----
export function getWorkspace(): Promise<CatalogEnvelope<WorkspaceData>>
⋮----
export interface GraphViewNode {
  id: string
  label: string
  kind: 'project' | 'repo'
  project_name?: string | null
  repo_name?: string | null
}
⋮----
export interface GraphViewEdge {
  id: string
  from_id: string
  to_id: string
  type: string
  label?: string | null
  source: 'manual' | 'inferred' | 'structure'
}
⋮----
export interface WorkspaceGraphView {
  ok: boolean
  nodes: GraphViewNode[]
  edges: GraphViewEdge[]
  manual_edge_count: number
  inferred_edge_count: number
  structure_edge_count: number
}
⋮----
export function getWorkspaceGraph(options?: {
  includeInferred?: boolean
  includeStructure?: boolean
}): Promise<WorkspaceGraphView>
⋮----
export function postHealth(
  body: Record<string, unknown> = {},
): Promise<WorkspaceHealthResult>
⋮----
export function postSync(body: SyncJobRequest): Promise<Record<string, unknown>>
⋮----
export function getSyncJob(id: string): Promise<SyncJobStatus>
⋮----
export interface PrunePreviewResponse {
  ok: boolean
  candidates: PruneCandidate[]
}
⋮----
export interface PruneExecuteResponse {
  ok: boolean
  dry_run: boolean
  force: boolean
  removed: string[]
  paths?: string[]
  errors?: Array<{ path: string; message: string }>
}
⋮----
export function postPrunePreview(body: {
  project: string
  include_hidden?: boolean
}): Promise<PrunePreviewResponse>
⋮----
export function postPrune(body: {
  project: string
  paths: string[]
  dry_run?: boolean
  force?: boolean
}): Promise<PruneExecuteResponse>
````

## File: .gitignore
````
# Python-generated files
build/
dist/
wheels/
*.egg-info

# Virtual environments
.venv
venv

.env
__pycache__
.ruff*
.DS_Store
unit_test_results.txt
.pytest_cache
.mypy_cache
build/
*.egg-info/
__pycache__/
*.pyc

secret_results.json
# Metagit workspace
.metagit/
src/metagit/_version.py
site

# Crush contextual files
.crush
.gitnexus
graphify-*
docs/superpowers
web/node_modules
````

## File: metagit.config.yaml
````yaml
config:
  description: Configuration for metagit CLI
  agent_mode: false
  editor: code
  api_url: ''
  api_version: ''
  api_key: ''
  cicd_file_data: data/cicd-files.json
  file_type_data: data/file-types.json
  package_manager_data: data/package-managers.json
  llm:
    enabled: false
    provider: openrouter
    provider_model: gpt-4o-mini
    embedder: ollama
    embedder_model: nomic-embed-text
    api_key: ''
  workspace:
    path: ./.metagit
    default_project: default
    dedupe:
      enabled: false
      canonical_dir: _canonical
    ui_show_preview: true
    ui_menu_length: 20
    ui_ignore_hidden: true
  profiles:
  - name: default
    boundaries:
    - name: github
      values: []
    - name: jfrog
      values: []
    - name: gitlab
      values: []
    - name: bitbucket
      values: []
    - name: azure_devops
      values: []
    - name: dockerhub
      values: []
    - name: domain
      values:
      - localhost
      - 127.0.0.1
      - 0.0.0.0
      - 192.168.*
      - 10.0.*
      - 172.16.*
  providers:
    github:
      enabled: true
      api_token: ''
      base_url: https://api.github.com
    gitlab:
      enabled: false
      api_token: ''
      base_url: https://gitlab.com/api/v4
````

## File: .mex/patterns/INDEX.md
````markdown
# Pattern Index

Lookup table for all pattern files in this directory. Check here before starting any task — if a pattern exists, follow it.

<!-- This file is populated during setup (Pass 2) and updated whenever patterns are added.
     Each row maps a pattern file (or section) to its trigger — when should the agent load it?

     Format — simple (one task per file):
     | [filename.md](filename.md) | One-line description of when to use this pattern |

     Format — anchored (multi-section file, one row per task):
     | [filename.md#task-first-task](filename.md#task-first-task) | When doing the first task |
     | [filename.md#task-second-task](filename.md#task-second-task) | When doing the second task |

     Example (from a Flask API project):
     | [add-api-client.md](add-api-client.md) | Adding a new external service integration |
     | [debug-pipeline.md](debug-pipeline.md) | Diagnosing failures in the request pipeline |
     | [crud-operations.md#task-add-endpoint](crud-operations.md#task-add-endpoint) | Adding a new API route with validation |
     | [crud-operations.md#task-add-model](crud-operations.md#task-add-model) | Adding a new database model |

     Keep this table sorted alphabetically. One row per task (not per file).
     If you create a new pattern, add it here. If you delete one, remove it. -->

| Pattern | Use when |
|---------|----------|
| [add-cli-command.md](add-cli-command.md) | Adding or extending a Click CLI command while keeping core logic in `src/metagit/core/*` |
| [add-managed-repo-search.md](add-managed-repo-search.md) | Extending or debugging managed-only repo search (CLI, MCP `metagit_repo_search`, local JSON API) |
| [add-mcp-tool.md](add-mcp-tool.md) | Adding/changing MCP tools, schemas, dispatch behavior, and runtime tests |
| [mcp-project-context.md](mcp-project-context.md) | Project context switch, session store, or workspace snapshot MCP tools |
| [mcp-cross-project-dependencies.md](mcp-cross-project-dependencies.md) | Cross-project dependency graph MCP tool and collectors |
| [metagit-web-api.md](metagit-web-api.md) | Adding or changing Pydantic models and routes for `metagit web serve` |
| [bootstrap-metagit-config.md](bootstrap-metagit-config.md) | Creating, validating, or repairing `.metagit.yml` for workspace and MCP flows |
| [debug-mcp-runtime.md](debug-mcp-runtime.md) | Diagnosing MCP runtime protocol, framing, gating, and tool/resource failures |
| [debug-workspace-discovery.md](debug-workspace-discovery.md) | Diagnosing empty/incorrect workspace index, search hits, or upstream hint ranking |
| [run-graphify-analysis.md](run-graphify-analysis.md) | Running `graphify` on the repo or a focused subtree and turning the result into usable graph/report outputs |
| [update-release-workflow.md](update-release-workflow.md) | Replacing or repairing GitHub release automation and tag-driven publish flow |
````

## File: schemas/metagit_config.schema.json
````json
{
  "$defs": {
    "AlertingChannel": {
      "additionalProperties": false,
      "description": "Model for alerting channel.",
      "properties": {
        "name": {
          "description": "Alerting channel name",
          "title": "Name",
          "type": "string"
        },
        "type": {
          "$ref": "#/$defs/AlertingChannelType",
          "description": "Alerting channel type"
        },
        "url": {
          "anyOf": [
            {
              "format": "uri",
              "maxLength": 2083,
              "minLength": 1,
              "type": "string"
            },
            {
              "type": "string"
            }
          ],
          "description": "Alerting channel URL",
          "title": "Url"
        }
      },
      "required": [
        "name",
        "type",
        "url"
      ],
      "title": "AlertingChannel",
      "type": "object"
    },
    "AlertingChannelType": {
      "description": "Enumeration of alerting channel types.",
      "enum": [
        "slack",
        "teams",
        "email",
        "sms",
        "webhook",
        "custom",
        "unknown",
        "other"
      ],
      "title": "AlertingChannelType",
      "type": "string"
    },
    "Artifact": {
      "additionalProperties": false,
      "description": "Model for generated artifacts.",
      "properties": {
        "type": {
          "$ref": "#/$defs/ArtifactType",
          "description": "Artifact type"
        },
        "definition": {
          "description": "Artifact definition",
          "title": "Definition",
          "type": "string"
        },
        "location": {
          "anyOf": [
            {
              "format": "uri",
              "maxLength": 2083,
              "minLength": 1,
              "type": "string"
            },
            {
              "type": "string"
            }
          ],
          "description": "Artifact location",
          "title": "Location"
        },
        "version_strategy": {
          "$ref": "#/$defs/VersionStrategy",
          "description": "Version strategy"
        }
      },
      "required": [
        "type",
        "definition",
        "location",
        "version_strategy"
      ],
      "title": "Artifact",
      "type": "object"
    },
    "ArtifactType": {
      "description": "Enumeration of artifact types.",
      "enum": [
        "docker",
        "github_release",
        "helm_chart",
        "npm_package",
        "static_website",
        "python_package",
        "node_package",
        "ruby_package",
        "java_package",
        "c_package",
        "cpp_package",
        "csharp_package",
        "go_package",
        "rust_package",
        "php_package",
        "dotnet_package",
        "elixir_package",
        "haskell_package",
        "custom",
        "none",
        "unknown",
        "other",
        "plugin",
        "template",
        "config",
        "binary",
        "archive"
      ],
      "title": "ArtifactType",
      "type": "string"
    },
    "BranchNaming": {
      "additionalProperties": false,
      "description": "Model for branch naming patterns.",
      "properties": {
        "kind": {
          "$ref": "#/$defs/BranchStrategy",
          "description": "Branch strategy"
        },
        "pattern": {
          "description": "Branch naming pattern",
          "title": "Pattern",
          "type": "string"
        }
      },
      "required": [
        "kind",
        "pattern"
      ],
      "title": "BranchNaming",
      "type": "object"
    },
    "BranchStrategy": {
      "description": "Enumeration of branch strategies.",
      "enum": [
        "trunk",
        "gitflow",
        "githubflow",
        "gitlabflow",
        "fork",
        "none",
        "custom",
        "unknown"
      ],
      "title": "BranchStrategy",
      "type": "string"
    },
    "CICD": {
      "additionalProperties": false,
      "description": "Model for CI/CD configuration.",
      "properties": {
        "platform": {
          "$ref": "#/$defs/CICDPlatform",
          "description": "CI/CD platform"
        },
        "pipelines": {
          "description": "List of pipelines",
          "items": {
            "$ref": "#/$defs/Pipeline"
          },
          "title": "Pipelines",
          "type": "array"
        }
      },
      "required": [
        "platform",
        "pipelines"
      ],
      "title": "CICD",
      "type": "object"
    },
    "CICDPlatform": {
      "description": "Enumeration of CI/CD platforms.",
      "enum": [
        "GitHub",
        "GitLab",
        "CircleCI",
        "Jenkins",
        "jx",
        "tekton",
        "custom",
        "none",
        "unknown",
        "other"
      ],
      "title": "CICDPlatform",
      "type": "string"
    },
    "Dashboard": {
      "additionalProperties": false,
      "description": "Model for monitoring dashboard.",
      "properties": {
        "name": {
          "description": "Dashboard name",
          "title": "Name",
          "type": "string"
        },
        "tool": {
          "description": "Dashboard tool",
          "title": "Tool",
          "type": "string"
        },
        "url": {
          "description": "Dashboard URL",
          "format": "uri",
          "maxLength": 2083,
          "minLength": 1,
          "title": "Url",
          "type": "string"
        }
      },
      "required": [
        "name",
        "tool",
        "url"
      ],
      "title": "Dashboard",
      "type": "object"
    },
    "Deployment": {
      "additionalProperties": false,
      "description": "Model for deployment configuration.",
      "properties": {
        "strategy": {
          "$ref": "#/$defs/DeploymentStrategy",
          "description": "Deployment strategy"
        },
        "environments": {
          "anyOf": [
            {
              "items": {
                "$ref": "#/$defs/Environment"
              },
              "type": "array"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Deployment environments",
          "title": "Environments"
        },
        "infrastructure": {
          "anyOf": [
            {
              "$ref": "#/$defs/Infrastructure"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Infrastructure configuration"
        }
      },
      "required": [
        "strategy"
      ],
      "title": "Deployment",
      "type": "object"
    },
    "DeploymentStrategy": {
      "description": "Enumeration of deployment strategies.",
      "enum": [
        "blue/green",
        "rolling",
        "manual",
        "gitops",
        "pipeline",
        "custom",
        "none",
        "unknown",
        "other"
      ],
      "title": "DeploymentStrategy",
      "type": "string"
    },
    "DocumentationSource": {
      "additionalProperties": false,
      "description": "One documentation source for agents and knowledge-graph ingestion.",
      "properties": {
        "kind": {
          "description": "Source type (markdown, web, confluence, sharepoint, wiki, api, other)",
          "title": "Kind",
          "type": "string"
        },
        "path": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Repo-relative or absolute path to a documentation file",
          "title": "Path"
        },
        "url": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Remote documentation URL",
          "title": "Url"
        },
        "title": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Human-readable title",
          "title": "Title"
        },
        "description": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Short summary for indexing and graph nodes",
          "title": "Description"
        },
        "tags": {
          "additionalProperties": {
            "type": "string"
          },
          "description": "Flat metadata tags for filtering and graph edges",
          "title": "Tags",
          "type": "object"
        },
        "metadata": {
          "additionalProperties": true,
          "description": "Extensible key-value payload for downstream graph ingestors",
          "title": "Metadata",
          "type": "object"
        }
      },
      "required": [
        "kind"
      ],
      "title": "DocumentationSource",
      "type": "object"
    },
    "Environment": {
      "additionalProperties": false,
      "description": "Model for deployment environment.",
      "properties": {
        "name": {
          "description": "Environment name",
          "title": "Name",
          "type": "string"
        },
        "url": {
          "anyOf": [
            {
              "format": "uri",
              "maxLength": 2083,
              "minLength": 1,
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Environment URL",
          "title": "Url"
        }
      },
      "required": [
        "name"
      ],
      "title": "Environment",
      "type": "object"
    },
    "GraphEndpoint": {
      "additionalProperties": false,
      "description": "Endpoint for a manual cross-repo relationship.",
      "properties": {
        "project": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Workspace project name",
          "title": "Project"
        },
        "repo": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Repository name under the project",
          "title": "Repo"
        },
        "path": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Optional file or directory path within the repo",
          "title": "Path"
        }
      },
      "title": "GraphEndpoint",
      "type": "object"
    },
    "GraphRelationship": {
      "additionalProperties": false,
      "description": "Manually declared edge between workspace projects or repos.",
      "properties": {
        "id": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Stable identifier for exports and graph merges",
          "title": "Id"
        },
        "from": {
          "$ref": "#/$defs/GraphEndpoint",
          "description": "Relationship source"
        },
        "to": {
          "$ref": "#/$defs/GraphEndpoint",
          "description": "Relationship target"
        },
        "type": {
          "default": "depends_on",
          "description": "Relationship type (depends_on, documents, consumes, owns, related, \u2026)",
          "title": "Type",
          "type": "string"
        },
        "label": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Short label for graph UIs",
          "title": "Label"
        },
        "description": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Longer explanation for agents and exports",
          "title": "Description"
        },
        "tags": {
          "additionalProperties": {
            "type": "string"
          },
          "description": "Flat tags for filtering graph exports",
          "title": "Tags",
          "type": "object"
        },
        "metadata": {
          "additionalProperties": true,
          "description": "Extensible payload for GitNexus or other graph ingestors",
          "title": "Metadata",
          "type": "object"
        }
      },
      "required": [
        "from",
        "to"
      ],
      "title": "GraphRelationship",
      "type": "object"
    },
    "Hosting": {
      "description": "Enumeration of hosting options.",
      "enum": [
        "EC2",
        "VMware",
        "Oracle",
        "Kubernetes",
        "Vercel",
        "ECS",
        "AWS Lambda",
        "AWS Fargate",
        "AWS EKS",
        "AWS ECS",
        "AWS ECS Fargate",
        "AWS ECS Fargate Spot",
        "AWS ECS Fargate Spot Spot",
        "Elastic Beanstalk",
        "Azure App Service",
        "Azure Functions",
        "Azure Container Instances",
        "Azure Container Apps",
        "Azure Container Apps Environment",
        "Azure Container Apps Environment Service",
        "Azure Container Apps Environment Service Service",
        "custom",
        "none",
        "unknown"
      ],
      "title": "Hosting",
      "type": "string"
    },
    "Infrastructure": {
      "additionalProperties": false,
      "description": "Model for infrastructure configuration.",
      "properties": {
        "provisioning_tool": {
          "$ref": "#/$defs/ProvisioningTool",
          "description": "Provisioning tool"
        },
        "hosting": {
          "$ref": "#/$defs/Hosting",
          "description": "Hosting platform"
        }
      },
      "required": [
        "provisioning_tool",
        "hosting"
      ],
      "title": "Infrastructure",
      "type": "object"
    },
    "License": {
      "additionalProperties": false,
      "description": "Model for project license information.",
      "properties": {
        "kind": {
          "$ref": "#/$defs/LicenseKind",
          "description": "License type"
        },
        "file": {
          "default": "",
          "description": "License file path",
          "title": "File",
          "type": "string"
        }
      },
      "required": [
        "kind"
      ],
      "title": "License",
      "type": "object"
    },
    "LicenseKind": {
      "description": "Enumeration of license kinds.",
      "enum": [
        "None",
        "MIT",
        "Apache-2.0",
        "GPL-3.0",
        "BSD-3-Clause",
        "proprietary",
        "custom",
        "unknown"
      ],
      "title": "LicenseKind",
      "type": "string"
    },
    "LoggingProvider": {
      "description": "Enumeration of logging providers.",
      "enum": [
        "console",
        "cloudwatch",
        "elk",
        "sentry",
        "custom",
        "none",
        "unknown",
        "other"
      ],
      "title": "LoggingProvider",
      "type": "string"
    },
    "Maintainer": {
      "additionalProperties": false,
      "description": "Model for project maintainer information.",
      "properties": {
        "name": {
          "description": "Maintainer name",
          "title": "Name",
          "type": "string"
        },
        "email": {
          "description": "Maintainer email",
          "title": "Email",
          "type": "string"
        },
        "role": {
          "description": "Maintainer role",
          "title": "Role",
          "type": "string"
        }
      },
      "required": [
        "name",
        "email",
        "role"
      ],
      "title": "Maintainer",
      "type": "object"
    },
    "MonitoringProvider": {
      "description": "Enumeration of monitoring providers.",
      "enum": [
        "prometheus",
        "datadog",
        "grafana",
        "sentry",
        "custom",
        "none",
        "unknown",
        "other"
      ],
      "title": "MonitoringProvider",
      "type": "string"
    },
    "Observability": {
      "additionalProperties": false,
      "description": "Model for observability configuration.",
      "properties": {
        "logging_provider": {
          "anyOf": [
            {
              "$ref": "#/$defs/LoggingProvider"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Logging provider"
        },
        "monitoring_providers": {
          "anyOf": [
            {
              "items": {
                "$ref": "#/$defs/MonitoringProvider"
              },
              "type": "array"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Monitoring providers",
          "title": "Monitoring Providers"
        },
        "alerting_channels": {
          "anyOf": [
            {
              "items": {
                "$ref": "#/$defs/AlertingChannel"
              },
              "type": "array"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Alerting channels",
          "title": "Alerting Channels"
        },
        "dashboards": {
          "anyOf": [
            {
              "items": {
                "$ref": "#/$defs/Dashboard"
              },
              "type": "array"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Monitoring dashboards",
          "title": "Dashboards"
        }
      },
      "title": "Observability",
      "type": "object"
    },
    "Pipeline": {
      "additionalProperties": false,
      "description": "Model for CI/CD pipeline.",
      "properties": {
        "name": {
          "description": "Pipeline name",
          "title": "Name",
          "type": "string"
        },
        "ref": {
          "description": "Pipeline reference",
          "title": "Ref",
          "type": "string"
        },
        "variables": {
          "anyOf": [
            {
              "items": {
                "type": "string"
              },
              "type": "array"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Pipeline variables",
          "title": "Variables"
        }
      },
      "required": [
        "name",
        "ref"
      ],
      "title": "Pipeline",
      "type": "object"
    },
    "ProjectDedupeOverride": {
      "additionalProperties": false,
      "description": "Per-project override of app-config ``workspace.dedupe``.",
      "properties": {
        "enabled": {
          "anyOf": [
            {
              "type": "boolean"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "When set, overrides workspace.dedupe.enabled from metagit.config.yaml for sync and layout under this project only",
          "title": "Enabled"
        }
      },
      "title": "ProjectDedupeOverride",
      "type": "object"
    },
    "ProjectKind": {
      "description": "Enumeration of project kinds.",
      "enum": [
        "monorepo",
        "umbrella",
        "application",
        "gitops",
        "infrastructure",
        "service",
        "library",
        "website",
        "other",
        "docker_image",
        "repository",
        "cli"
      ],
      "title": "ProjectKind",
      "type": "string"
    },
    "ProjectPath": {
      "additionalProperties": false,
      "description": "Model for project path, dependency, component, or workspace project information.",
      "properties": {
        "name": {
          "description": "Friendly name for the path or project",
          "title": "Name",
          "type": "string"
        },
        "description": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Short description of the path or project",
          "title": "Description"
        },
        "kind": {
          "anyOf": [
            {
              "$ref": "#/$defs/ProjectKind"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Project kind"
        },
        "ref": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Reference in the current project for the target project, used in dependencies",
          "title": "Ref"
        },
        "path": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Project path",
          "title": "Path"
        },
        "branches": {
          "anyOf": [
            {
              "items": {
                "type": "string"
              },
              "type": "array"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Project branches",
          "title": "Branches"
        },
        "url": {
          "anyOf": [
            {
              "format": "uri",
              "maxLength": 2083,
              "minLength": 1,
              "type": "string"
            },
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Project URL",
          "title": "Url"
        },
        "sync": {
          "anyOf": [
            {
              "type": "boolean"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Sync setting",
          "title": "Sync"
        },
        "language": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Programming language",
          "title": "Language"
        },
        "language_version": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "number"
            },
            {
              "type": "integer"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Language version",
          "title": "Language Version"
        },
        "package_manager": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Package manager used by the project",
          "title": "Package Manager"
        },
        "frameworks": {
          "anyOf": [
            {
              "items": {
                "type": "string"
              },
              "type": "array"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Frameworks used by the project",
          "title": "Frameworks"
        },
        "source_provider": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Provider used to discover this repository",
          "title": "Source Provider"
        },
        "source_namespace": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Source namespace identifier (org/user/group)",
          "title": "Source Namespace"
        },
        "source_repo_id": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Provider-native repository identifier",
          "title": "Source Repo Id"
        },
        "tags": {
          "additionalProperties": {
            "type": "string"
          },
          "description": "Flat metadata tags for managed repo search and filtering",
          "title": "Tags",
          "type": "object"
        },
        "protected": {
          "anyOf": [
            {
              "type": "boolean"
            },
            {
              "type": "null"
            }
          ],
          "default": false,
          "description": "If true, reconcile mode must not remove this repository automatically",
          "title": "Protected"
        },
        "agent_instructions": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Optional instructions for subagents operating in this repo or path",
          "title": "Agent Instructions"
        }
      },
      "required": [
        "name"
      ],
      "title": "ProjectPath",
      "type": "object"
    },
    "ProvisioningTool": {
      "description": "Enumeration of provisioning tools.",
      "enum": [
        "Terraform",
        "CloudFormation",
        "CDKTF",
        "AWS CDK",
        "Bicep",
        "custom",
        "none",
        "unknown",
        "other"
      ],
      "title": "ProvisioningTool",
      "type": "string"
    },
    "Secret": {
      "additionalProperties": false,
      "description": "Model for secret definitions.",
      "properties": {
        "name": {
          "description": "Secret name",
          "title": "Name",
          "type": "string"
        },
        "kind": {
          "$ref": "#/$defs/SecretKind",
          "description": "Secret type"
        },
        "ref": {
          "description": "Secret reference",
          "title": "Ref",
          "type": "string"
        }
      },
      "required": [
        "name",
        "kind",
        "ref"
      ],
      "title": "Secret",
      "type": "object"
    },
    "SecretKind": {
      "description": "Enumeration of secret kinds.",
      "enum": [
        "remote_jwt",
        "remote_api_key",
        "generated_string",
        "custom",
        "dynamic",
        "private_key",
        "public_key",
        "secret_key",
        "api_key",
        "access_token",
        "refresh_token",
        "password",
        "database_password",
        "unknown",
        "other"
      ],
      "title": "SecretKind",
      "type": "string"
    },
    "Tasker": {
      "additionalProperties": false,
      "description": "Model for task management tools.",
      "properties": {
        "kind": {
          "$ref": "#/$defs/TaskerKind",
          "description": "Tasker type"
        }
      },
      "required": [
        "kind"
      ],
      "title": "Tasker",
      "type": "object"
    },
    "TaskerKind": {
      "description": "Enumeration of tasker kinds.",
      "enum": [
        "Taskfile",
        "Makefile",
        "Jest",
        "NPM",
        "Atmos",
        "custom",
        "none",
        "mise_tasks"
      ],
      "title": "TaskerKind",
      "type": "string"
    },
    "Variable": {
      "additionalProperties": false,
      "description": "Model for variable definitions.",
      "properties": {
        "name": {
          "description": "Variable name",
          "title": "Name",
          "type": "string"
        },
        "kind": {
          "$ref": "#/$defs/VariableKind",
          "description": "Variable type"
        },
        "ref": {
          "description": "Variable reference",
          "title": "Ref",
          "type": "string"
        }
      },
      "required": [
        "name",
        "kind",
        "ref"
      ],
      "title": "Variable",
      "type": "object"
    },
    "VariableKind": {
      "description": "Enumeration of variable kinds.",
      "enum": [
        "string",
        "integer",
        "boolean",
        "custom",
        "unknown",
        "other"
      ],
      "title": "VariableKind",
      "type": "string"
    },
    "VersionStrategy": {
      "description": "Enumeration of version strategies.",
      "enum": [
        "semver",
        "none",
        "custom",
        "unknown",
        "other"
      ],
      "title": "VersionStrategy",
      "type": "string"
    },
    "Workspace": {
      "description": "Model for workspace configuration.",
      "properties": {
        "description": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Human-readable description of this workspace",
          "title": "Description"
        },
        "agent_instructions": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Optional instructions for agents working in this workspace",
          "title": "Agent Instructions"
        },
        "projects": {
          "description": "Workspace projects",
          "items": {
            "$ref": "#/$defs/WorkspaceProject"
          },
          "title": "Projects",
          "type": "array"
        }
      },
      "required": [
        "projects"
      ],
      "title": "Workspace",
      "type": "object"
    },
    "WorkspaceGraph": {
      "additionalProperties": false,
      "description": "Top-level manual graph data on a .metagit.yml manifest.",
      "properties": {
        "relationships": {
          "description": "Manually entered cross-repo or cross-project edges",
          "items": {
            "$ref": "#/$defs/GraphRelationship"
          },
          "title": "Relationships",
          "type": "array"
        },
        "metadata": {
          "additionalProperties": true,
          "description": "Graph-level metadata for export pipelines",
          "title": "Metadata",
          "type": "object"
        }
      },
      "title": "WorkspaceGraph",
      "type": "object"
    },
    "WorkspaceProject": {
      "additionalProperties": false,
      "description": "Model for workspace project.",
      "properties": {
        "name": {
          "description": "Workspace project name",
          "title": "Name",
          "type": "string"
        },
        "description": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Human-readable description of this workspace project",
          "title": "Description"
        },
        "agent_instructions": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Optional instructions for agents working in this workspace project",
          "title": "Agent Instructions"
        },
        "dedupe": {
          "anyOf": [
            {
              "$ref": "#/$defs/ProjectDedupeOverride"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Optional override of app-config workspace.dedupe for this project (currently supports enabled only)"
        },
        "repos": {
          "description": "Repository list",
          "items": {
            "$ref": "#/$defs/ProjectPath"
          },
          "title": "Repos",
          "type": "array"
        }
      },
      "required": [
        "name",
        "repos"
      ],
      "title": "WorkspaceProject",
      "type": "object"
    }
  },
  "additionalProperties": false,
  "description": "Main model for .metagit.yml configuration file.",
  "properties": {
    "name": {
      "description": "Project name",
      "title": "Name",
      "type": "string"
    },
    "description": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": "No description",
      "description": "Project description",
      "title": "Description"
    },
    "agent_instructions": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Optional instructions for controller agents; applies with or without a workspace block",
      "title": "Agent Instructions"
    },
    "url": {
      "anyOf": [
        {
          "format": "uri",
          "maxLength": 2083,
          "minLength": 1,
          "type": "string"
        },
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Project URL",
      "title": "Url"
    },
    "kind": {
      "anyOf": [
        {
          "$ref": "#/$defs/ProjectKind"
        },
        {
          "type": "null"
        }
      ],
      "default": "application",
      "description": "Project kind. This is used to determine the type of project and the best way to manage it."
    },
    "documentation": {
      "anyOf": [
        {
          "items": {
            "$ref": "#/$defs/DocumentationSource"
          },
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Documentation sources: bare strings (path or URL) or objects with kind, path/url, tags, and metadata for knowledge-graph ingestion",
      "title": "Documentation"
    },
    "graph": {
      "anyOf": [
        {
          "$ref": "#/$defs/WorkspaceGraph"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Manual cross-repo relationships and graph metadata for exports and GitNexus-style dependency maps"
    },
    "license": {
      "anyOf": [
        {
          "$ref": "#/$defs/License"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "License information"
    },
    "maintainers": {
      "anyOf": [
        {
          "items": {
            "$ref": "#/$defs/Maintainer"
          },
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Project maintainers",
      "title": "Maintainers"
    },
    "branch_strategy": {
      "anyOf": [
        {
          "$ref": "#/$defs/BranchStrategy"
        },
        {
          "type": "null"
        }
      ],
      "default": "unknown",
      "description": "Branch strategy used by the project."
    },
    "taskers": {
      "anyOf": [
        {
          "items": {
            "$ref": "#/$defs/Tasker"
          },
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Task management tools employed by the project.",
      "title": "Taskers"
    },
    "branch_naming": {
      "anyOf": [
        {
          "items": {
            "$ref": "#/$defs/BranchNaming"
          },
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Branch naming patterns used by the project.",
      "title": "Branch Naming"
    },
    "artifacts": {
      "anyOf": [
        {
          "items": {
            "$ref": "#/$defs/Artifact"
          },
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "description": "Generated artifacts from the project.",
      "title": "Artifacts"
    },
    "secrets_management": {
      "anyOf": [
        {
          "items": {
            "type": "string"
          },
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Secrets management tools employed by the project.",
      "title": "Secrets Management"
    },
    "secrets": {
      "anyOf": [
        {
          "items": {
            "$ref": "#/$defs/Secret"
          },
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Secret definitions",
      "title": "Secrets"
    },
    "variables": {
      "anyOf": [
        {
          "items": {
            "$ref": "#/$defs/Variable"
          },
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Variable definitions",
      "title": "Variables"
    },
    "cicd": {
      "anyOf": [
        {
          "$ref": "#/$defs/CICD"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "CI/CD configuration"
    },
    "deployment": {
      "anyOf": [
        {
          "$ref": "#/$defs/Deployment"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Deployment configuration"
    },
    "observability": {
      "anyOf": [
        {
          "$ref": "#/$defs/Observability"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Observability configuration"
    },
    "paths": {
      "anyOf": [
        {
          "items": {
            "$ref": "#/$defs/ProjectPath"
          },
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "description": "Important local project paths. In a monorepo, this would include any sub-projects typically found being built in the CICD pipelines.",
      "title": "Paths"
    },
    "dependencies": {
      "anyOf": [
        {
          "items": {
            "$ref": "#/$defs/ProjectPath"
          },
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "description": "Additional project dependencies not found in the paths or components lists. These include docker images, helm charts, or terraform modules.",
      "title": "Dependencies"
    },
    "components": {
      "anyOf": [
        {
          "items": {
            "$ref": "#/$defs/ProjectPath"
          },
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Additional project component paths that may be useful in other projects.",
      "title": "Components"
    },
    "workspace": {
      "anyOf": [
        {
          "$ref": "#/$defs/Workspace"
        },
        {
          "type": "null"
        }
      ],
      "description": "Workspaces are a collection of projects that are related to each other. They are used to group projects together for a specific purpose. These are manually defined by the user. The internal workspace name is reservice"
    }
  },
  "required": [
    "name"
  ],
  "title": "MetagitConfig",
  "type": "object"
}
````

## File: src/metagit/cli/commands/project_repo.py
````python
#!/usr/bin/env python
"""
Project repo subcommand
"""
⋮----
@click.group(name="repo")
@click.pass_context
def repo(ctx: click.Context) -> None
⋮----
"""Repository subcommands"""
⋮----
@repo.command("select")
@click.pass_context
def repo_select(ctx: click.Context) -> None
⋮----
"""Select project repo to work on"""
logger = ctx.obj["logger"]
local_config: MetagitConfig = ctx.obj["local_config"]
project = ctx.obj["project"]
app_config: AppConfig = ctx.obj["config"]
project_manager = project_manager_from_app(
agent_mode = bool(ctx.obj.get("agent_mode", False))
selected_repo = project_manager.select_repo(
⋮----
editor_result = open_editor(app_config.editor, selected_repo)
⋮----
@click.pass_context
def repo_list(ctx: click.Context, as_json: bool) -> None
⋮----
"""List repositories for the current workspace project."""
project: str = ctx.obj["project"]
⋮----
workspace_root = str(Path(app_config.workspace.path).expanduser().resolve())
result = WorkspaceCatalogService().list_repos(
⋮----
repo_row = row.get("repo", {})
⋮----
@click.pass_context
def repo_remove(ctx: click.Context, name: str, as_json: bool) -> None
⋮----
"""Remove a repository from the manifest (does not delete files)."""
⋮----
config_path: str = ctx.obj["config_path"]
⋮----
result = WorkspaceCatalogService().remove_repo(
⋮----
"""Rename a repository in the current project."""
⋮----
dedupe = resolve_dedupe_for_layout(
result = WorkspaceLayoutService().rename_repo(
⋮----
"""Move a repository to another workspace project."""
⋮----
result = WorkspaceLayoutService().move_repo(
⋮----
"""Add a repository to the current project"""
logger: UnifiedLogger = ctx.obj["logger"]
⋮----
local_config = ctx.obj["local_config"]
config_path = ctx.obj["config_path"]
⋮----
# Initialize ProjectManager and MetagitConfigManager
⋮----
catalog = WorkspaceCatalogService()
⋮----
result = project_manager.add(
⋮----
repo_data = {
repo_data = {k: v for k, v in repo_data.items() if v is not None}
project_path = ProjectPath(**repo_data)
⋮----
mutation = catalog.add_repo(
⋮----
repo_name = result.name if result.name else "repository"
⋮----
"""
    Walk the project sync folder and offer to remove directories not in .metagit.yml.

    Only considers immediate children of the project directory (same layout as sync).
    """
⋮----
workspace_root = Path(app_config.workspace.path).expanduser().resolve()
project_sync_folder = (workspace_root / project).resolve()
⋮----
ignore_hidden = (
candidates = project_manager.list_unmanaged_sync_directories(
dedupe = resolve_effective_dedupe_for_project(
⋮----
references = workspace_dedupe.list_canonical_references(
orphans = workspace_dedupe.list_orphan_canonical_dirs(
⋮----
removed = 0
⋮----
rel = path.name
````

## File: src/metagit/cli/main.py
````python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
This is the entry point for the command-line interface (CLI) application.

It can be used as a handy facility for running the task from a command line.

.. note::

    To learn more about Click visit the
    `project website <http://click.pocoo.org/5/>`_.  There is also a very
    helpful `tutorial video <https://www.youtube.com/watch?v=kNke39OZ2k0>`_.

    To learn more about running Luigi, visit the Luigi project's
    `Read-The-Docs <http://luigi.readthedocs.io/en/stable/>`_ page.

.. currentmodule:: metagit_detect.cli
.. moduleauthor:: Zachary Loeber <zloeber@gmail.com>
"""
⋮----
CONTEXT_SETTINGS: dict = {
⋮----
@click.pass_context
def cli(ctx: click.Context, config: str, debug: bool, verbose: bool) -> None
⋮----
"""
    Metagit CLI: A multi-purpose CLI tool with YAML configuration.
    """
# If no subcommand is provided, show help
⋮----
log_level: str = "INFO"
minimal_console: bool = True
⋮----
log_level = "INFO"
minimal_console = False
⋮----
log_level = "DEBUG"
⋮----
logger: UnifiedLogger = UnifiedLogger(
⋮----
config = DEFAULT_CONFIG
cfg = load_config(config)
⋮----
# Store the configuration and logger in the context
⋮----
logger = UnifiedLogger(LoggerConfig())
⋮----
@cli.command()
@click.pass_context
def info(ctx: click.Context) -> None
⋮----
"""
    Display the current configuration.
    """
logger = ctx.obj.get("logger") or UnifiedLogger(LoggerConfig())
⋮----
@cli.command()
@click.pass_context
def version(ctx: click.Context) -> None
⋮----
"""Get the application version."""
⋮----
def main() -> None
````

## File: src/metagit/core/mcp/tool_registry.py
````python
#!/usr/bin/env python
"""
Tool registry for Metagit MCP runtime.
"""
⋮----
class ToolRegistry
⋮----
"""State-aware registry of available MCP tool names."""
⋮----
_inactive_tools: list[str] = [
_active_tools: list[str] = [
⋮----
def list_tools(self, status: WorkspaceStatus) -> list[str]
⋮----
"""List available tools for the provided workspace status."""
````

## File: src/metagit/data/web/index.html
````html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>web</title>
    <script type="module" crossorigin src="/assets/index-DOullneW.js"></script>
    <link rel="stylesheet" crossorigin href="/assets/index-B315j_NF.css">
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
````

## File: pyproject.toml
````toml
[project]
name = "metagit-cli"
dynamic = ["version"]
description = "Metagit-ai multi-repo management tool"
readme = "README.md"
license-files = ["LICENSE"]

authors = [
    {name = "Zachary Loeber", email = "zloeber@gmail.com"}
]
classifiers = [
    "Programming Language :: Python :: 3",
    "Operating System :: OS Independent",
]
requires-python = ">=3.12"
dependencies = [
    "logging>=0.4.9.6",
    "loguru>=0.7.3",
    "pydantic>=2.11.4",
    "requests>=2.33.0",
    "click>=8.1.7",
    "dotenv>=0.9.9",
    "gitpython>=3.1.50",
    "rich>=14.0.0",
    "pyyaml>=6.0.2",
    "pygithub>=2.6.1",
    "jsonschema>=4.24.0",
    "python-dotenv>=1.2.2",
    "rapidfuzz>=3.13.0",
    "tqdm>=4.66.4",
    "types-tqdm>=4.67.0.20250516",
    "aiofiles>=23.2.1",
    "async>=0.6.2",
    "setuptools-scm>=8.3.1",
    "litellm>=1.83.7",
    "crewai>=0.95.0",
    "textual-dev>=1.7.0",
    "tomli>=2.2.1",
    "python-hcl2>=7.3.1",
    "prompt-toolkit>=3.0.51",
]

[project.urls]
Homepage = "https://github.com/metagit-ai/metagit-cli"
Documentation = "https://metagit-ai.github.io/metagit-cli/"
Issues = "https://github.com/metagit-ai/metagit-cli/issues"
CI = "https://github.com/metagit-ai/metagit-cli/actions"

[project.optional-dependencies]
standard = []
test = ["pytest>=9.0.3", "pytest-cov", "mypy", "ruff", "httpx", "pytest-asyncio", "black>=26.3.1"]
dev = ["ruff", "black>=26.3.1", "isort", "mypy"]
docs = ["mkdocs", "mkdocs-material", "mkdocstrings", "mkdocs-click", "mdtoc", "mkdocs-mermaid2-plugin", "mkdocs-material"]
lint = ["ruff", "black", "isort", "mypy"]
format = ["black>=26.3.1", "isort"]
typecheck = ["mypy"]

[project.scripts]
metagit = "metagit.cli.main:cli"

[lint]
select = ['E', 'W', 'F', 'I', 'B', 'C4', 'ARG', 'SIM']
ignore = ['W291', 'W292', 'W293', 'E501', 'SIM115']

[tool.ruff]
exclude = [".venv", "records", ".metagit", "examples", "docs", "tests"]

[tool.uv]
override-dependencies = [
    "aiohttp>=3.13.4",
    "cryptography>=46.0.7",
    "filelock>=3.20.3",
    "idna>=3.15",
    "orjson>=3.11.5",
    "pdfminer-six>=20251230",
    "pillow>=12.2.0",
    "protobuf>=6.33.5",
    "pyasn1>=0.6.3",
    "pygments>=2.20.0",
    "pyjwt>=2.12.0",
    "pymdown-extensions>=10.21.3",
    "pynacl>=1.6.2",
    "urllib3>=2.7.0",
    "uv>=0.11.6",
    "virtualenv>=20.36.1",
]

[dependency-groups]
dev = [
    "mypy>=1.16.1",
    "ruff>=0.11.11",
    "bandit",
    "pip-audit",
]
# docs/architecture-diagram.py only — not default sync (OSV false positive PYSEC-2024-270).
diagrams = ["diagrams>=0.25.1"]

[tool.setuptools]
package-dir = {"" = "src"}

[tool.setuptools_scm]
version_file = "src/metagit/_version.py"

[tool.setuptools.packages.find]
where = ["src"]
include = ["metagit*"]

[tool.setuptools.package-data]
metagit = ["data/**/*"]

[build-system]
requires = ["setuptools>=64", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"
````

## File: src/metagit/cli/commands/config.py
````python
"""
Config subcommand
"""
⋮----
@click.pass_context
def config(ctx: click.Context, config_path: str) -> None
⋮----
"""Configuration subcommands"""
⋮----
# If no subcommand is provided, show help
⋮----
# Initialize a dummy logger for testing purposes if not already present
⋮----
logger = ctx.obj.get("logger")
⋮----
@click.pass_context
def config_show(ctx: click.Context, normalized: bool, as_json: bool) -> None
⋮----
"""Show metagit configuration (source file by default)."""
logger = ctx.obj["logger"]
⋮----
config_path = ctx.obj["config_path"]
config_manager = MetagitConfigManager(config_path=config_path)
config_result = config_manager.load_config()
⋮----
path = Path(config_path)
⋮----
raw = path.read_text(encoding="utf-8")
⋮----
output = dump_config_dict(
⋮----
"""Create metagit config files"""
⋮----
config_file = create_metagit_config(
⋮----
@config.command("validate")
@click.option("--config-path", help="Path to the configuration file", default=None)
@click.pass_context
def config_validate(ctx: click.Context, config_path: Union[str, None] = None) -> None
⋮----
"""Validate metagit configuration"""
⋮----
target_path = config_path or ctx.obj["config_path"]
⋮----
config_manager = MetagitConfigManager(config_path=target_path)
result = config_manager.load_config()
⋮----
"""Manage git provider plugin configuration."""
⋮----
# Load current configuration
app_config = AppConfig.load(config_path)
⋮----
# Show current configuration
⋮----
# Update configuration
modified = False
⋮----
# GitHub configuration
⋮----
modified = True
⋮----
# GitLab configuration
⋮----
# Save configuration if modified
⋮----
result = app_config.save(config_path)
⋮----
@config.command("set")
@click.argument("key")
@click.argument("value")
@click.pass_context
def config_set(ctx: click.Context, key: str, value: str) -> None
⋮----
"""Set a specific configuration value."""
⋮----
current_config = config_manager.load_config()
⋮----
# Helper to set nested attributes
def set_nested_attr(obj, attr_path, val)
⋮----
parts = attr_path.split(".")
⋮----
obj = getattr(obj, part)
⋮----
# Save the updated configuration
result = config_manager.save_config(current_config)
⋮----
@config.command("info")
@click.pass_context
def config_info(ctx: click.Context) -> None
⋮----
"""
    Display information about the local project configuration.
    """
⋮----
project_count = (
⋮----
"""
    Generate a non-production YAML exemplar with optional field descriptions.

    Merges src/metagit/data/config-example-overrides.yml when present.
    """
⋮----
generator = ConfigExampleGenerator(overrides=load_example_overrides())
rendered = generator.render_yaml(
⋮----
@click.pass_context
def config_tree(ctx: click.Context, as_json: bool) -> None
⋮----
"""Show schema-backed field tree for .metagit.yml (same model as web Config Studio)."""
⋮----
result = ConfigPatchService().build_tree("metagit", config_path)
⋮----
"""Preview .metagit.yml after applying draft operations (no save)."""
⋮----
operations = (
result = ConfigPatchService().preview(
⋮----
"""
    Apply schema operations to .metagit.yml (enable/disable/set/append/remove).

    Same operation model as the web Config Studio PATCH API. Example operations file:

    {"operations": [{"op": "set", "path": "name", "value": "my-workspace"}]}
    """
⋮----
operations = resolve_operations(
result = ConfigPatchService().patch(
⋮----
@config.group("graph")
@click.pass_context
def config_graph(ctx: click.Context) -> None
⋮----
"""Export workspace graph data for GitNexus / Cypher ingest."""
⋮----
"""
    Export manual graph.relationships as GitNexus-ingestible Cypher.

    Emits MERGE/CREATE statements for MetagitEntity / MetagitLink overlay tables and
    matching gitnexus_cypher MCP tool_calls. Run schema statements once per target index.
    """
⋮----
app_config = ctx.obj.get("config")
⋮----
manager = MetagitConfigManager(config_path=config_path)
loaded = manager.load_config()
⋮----
root = workspace_root or str(
result = GraphCypherExportService().export(
⋮----
lines = [*result.schema_statements, *result.statements]
rendered = "\n".join(lines) + ("\n" if lines else "")
⋮----
rendered = json.dumps(
⋮----
rendered = json.dumps(result.model_dump(mode="json"), indent=2)
⋮----
@click.pass_context
def config_schema(ctx: click.Context, output_path: str) -> None
⋮----
"""
    Generate a JSON schema for the MetagitConfig class and write it to a file.
    """
⋮----
schema = MetagitConfig.model_json_schema()
````

## File: mkdocs.yml
````yaml
site_name: Metagit-AI Documentation
theme:
  palette:
    scheme: slate
  name: material
nav:
  - 'index.md'
  - 'Installation': 'install.md'
  - 'Terminology': 'terminology.md'
  - 'Application Logic': 'app.logic.md'
  - 'Development': 'development.md'
  - 'Secrets': 'secrets.analysis.md'
  - 'CLI Reference': 'cli_reference.md'
  - 'Features':
    - 'Repository Detection': 'repository_detection.md'
    - 'Hermes & org IaC guide': 'hermes-iac-workspace-guide.md'
    - 'Hermes orchestrator workspace': 'hermes-orchestrator-workspace.md'
    - 'Configuration exemplar': 'reference/metagit-config.md'
    - 'Workspace layout API': 'reference/workspace-layout-api.md'
    - 'Metagit Web UI': 'reference/metagit-web.md'
    - 'Skills': 'skills.md'
markdown_extensions:
  - mkdocs-click
  - admonition         # Adds boxes for Notes, Warnings, etc.
  - attr_list          # Adds attribute lists to generated HTML
  - codehilite         # Syntax coloring for code blocks
  - def_list           # Adds syntax for definition lists
  - sane_lists         # Enables list items with multiple paragraphs
  - toc:               # Builds a table of contents
      permalink: "#"   # Puts a "#" at the end of each header
  - pymdownx.superfences:
        # make exceptions to highlighting of code:
      custom_fences:
        - name: mermaid
          class: mermaid
          format: !!python/name:mermaid2.fence_mermaid_custom
````

## File: src/metagit/core/appconfig/models.py
````python
#!/usr/bin/env python
⋮----
success_blurb: str = "Success! ✅"
failure_blurb: str = "Failed! ❌"
⋮----
class WorkspaceDedupeScope(str, Enum)
⋮----
"""Where repository deduplication is applied."""
⋮----
WORKSPACE = "workspace"
GLOBAL = "global"
⋮----
class WorkspaceDedupeStrategy(str, Enum)
⋮----
"""How duplicate repository identities share disk layout."""
⋮----
SYMLINK = "symlink"
⋮----
class WorkspaceDedupeConfig(BaseModel)
⋮----
"""Deduplicate synced repos within a workspace via a canonical directory."""
⋮----
model_config = ConfigDict(use_enum_values=True, extra="forbid")
⋮----
enabled: bool = Field(
scope: WorkspaceDedupeScope = Field(
strategy: WorkspaceDedupeStrategy = Field(
canonical_dir: str = Field(
⋮----
class WorkspaceConfig(BaseModel)
⋮----
"""Model for workspace configuration in AppConfig."""
⋮----
path: str = Field(default="./.metagit", description="Workspace path")
default_project: str = Field(default="default", description="Default project")
dedupe: WorkspaceDedupeConfig = Field(
ui_show_preview: Optional[bool] = Field(
ui_menu_length: Optional[int] = Field(
ui_preview_height: Optional[int] = Field(
ui_ignore_hidden: bool = Field(
⋮----
class Config
⋮----
"""Pydantic configuration."""
⋮----
extra = "forbid"
⋮----
class LLM(BaseModel)
⋮----
"""Model for LLM configuration in AppConfig."""
⋮----
enabled: bool = Field(default=False, description="Whether LLM is enabled")
provider: str = Field(default="openrouter", description="LLM provider")
provider_model: str = Field(default="gpt-4o-mini", description="LLM provider model")
embedder: str = Field(default="ollama", description="Embedding provider")
embedder_model: str = Field(
api_key: str = Field(default="", description="API key for LLM provider")
⋮----
class Boundary(BaseModel)
⋮----
"""Model for organization boundaries in AppConfig."""
⋮----
name: str = Field(..., description="Boundary name")
values: List[str] = Field(default_factory=list, description="Boundary values")
⋮----
class Profile(BaseModel)
⋮----
"""Model for profile configuration."""
⋮----
name: str = Field(default="default", description="Profile name")
boundaries: Optional[List[Boundary]] = Field(
⋮----
"0.0.0.0",  # nosec B104 — domain boundary allowlist, not a bind address
⋮----
class GitHubProvider(BaseModel)
⋮----
"""Model for GitHub provider configuration in AppConfig."""
⋮----
api_token: str = Field(default="", description="GitHub API token")
base_url: str = Field(
⋮----
class GitLabProvider(BaseModel)
⋮----
"""Model for GitLab provider configuration in AppConfig."""
⋮----
api_token: str = Field(default="", description="GitLab API token")
⋮----
class Providers(BaseModel)
⋮----
"""Model for Git provider configuration in AppConfig."""
⋮----
github: GitHubProvider = Field(
gitlab: GitLabProvider = Field(
⋮----
class AppConfig(BaseModel)
⋮----
"""Application-level settings (not the Metagit package release version — use `metagit version`)."""
⋮----
model_config = ConfigDict(extra="ignore")
⋮----
agent_mode: bool = Field(
description: str = "Metagit configuration"
editor: str = Field(default="code", description="The editor to use for the CLI")
# Reserved for future use
api_url: str = Field(default="", description="The API URL to use for the CLI")
⋮----
api_version: str = Field(
⋮----
api_key: str = Field(default="", description="The API key to use for the CLI")
⋮----
cicd_file_data: str = Field(
file_type_data: str = Field(
package_manager_data: str = Field(
llm: LLM = Field(default=LLM(), description="The LLM configuration")
workspace: WorkspaceConfig = Field(
profiles: List[Profile] = Field(
providers: Providers = Field(
⋮----
@classmethod
    def load(cls, config_path: str = None) -> Union["AppConfig", Exception]
⋮----
"""
        Load AppConfig from file.

        Args:
            config_path: Path to configuration file (optional)

        Returns:
            AppConfig object or Exception
        """
⋮----
config_path = os.path.join(
⋮----
config_file = Path(config_path)
⋮----
config_data = yaml.safe_load(f)
⋮----
config = cls(**config_data["config"])
⋮----
config = cls(**config_data)
⋮----
# Override with environment variables
config = cls._override_from_environment(config)
⋮----
@classmethod
    def _override_from_environment(cls, config: "AppConfig") -> "AppConfig"
⋮----
"""
        Override configuration with environment variables.

        Args:
            config: AppConfig to override

        Returns:
            Updated AppConfig
        """
⋮----
# LLM configuration
⋮----
# API configuration
⋮----
# Workspace configuration
⋮----
# GitHub provider configuration
⋮----
# GitLab provider configuration
⋮----
def save(self, config_path: str = None) -> Union[bool, Exception]
⋮----
"""
        Save AppConfig to file.

        Args:
            config_path: Path to save configuration file (optional)

        Returns:
            True if successful, Exception if failed
        """
````

## File: schemas/metagit_appconfig.schema.json
````json
{
  "$defs": {
    "Boundary": {
      "additionalProperties": false,
      "description": "Model for organization boundaries in AppConfig.",
      "properties": {
        "name": {
          "description": "Boundary name",
          "title": "Name",
          "type": "string"
        },
        "values": {
          "description": "Boundary values",
          "items": {
            "type": "string"
          },
          "title": "Values",
          "type": "array"
        }
      },
      "required": [
        "name"
      ],
      "title": "Boundary",
      "type": "object"
    },
    "GitHubProvider": {
      "additionalProperties": false,
      "description": "Model for GitHub provider configuration in AppConfig.",
      "properties": {
        "enabled": {
          "default": false,
          "description": "Whether GitHub provider is enabled",
          "title": "Enabled",
          "type": "boolean"
        },
        "api_token": {
          "default": "",
          "description": "GitHub API token",
          "title": "Api Token",
          "type": "string"
        },
        "base_url": {
          "default": "https://api.github.com",
          "description": "GitHub API base URL",
          "title": "Base Url",
          "type": "string"
        }
      },
      "title": "GitHubProvider",
      "type": "object"
    },
    "GitLabProvider": {
      "additionalProperties": false,
      "description": "Model for GitLab provider configuration in AppConfig.",
      "properties": {
        "enabled": {
          "default": false,
          "description": "Whether GitLab provider is enabled",
          "title": "Enabled",
          "type": "boolean"
        },
        "api_token": {
          "default": "",
          "description": "GitLab API token",
          "title": "Api Token",
          "type": "string"
        },
        "base_url": {
          "default": "https://gitlab.com/api/v4",
          "description": "GitLab API base URL",
          "title": "Base Url",
          "type": "string"
        }
      },
      "title": "GitLabProvider",
      "type": "object"
    },
    "LLM": {
      "additionalProperties": false,
      "description": "Model for LLM configuration in AppConfig.",
      "properties": {
        "enabled": {
          "default": false,
          "description": "Whether LLM is enabled",
          "title": "Enabled",
          "type": "boolean"
        },
        "provider": {
          "default": "openrouter",
          "description": "LLM provider",
          "title": "Provider",
          "type": "string"
        },
        "provider_model": {
          "default": "gpt-4o-mini",
          "description": "LLM provider model",
          "title": "Provider Model",
          "type": "string"
        },
        "embedder": {
          "default": "ollama",
          "description": "Embedding provider",
          "title": "Embedder",
          "type": "string"
        },
        "embedder_model": {
          "default": "nomic-embed-text",
          "description": "Embedding model",
          "title": "Embedder Model",
          "type": "string"
        },
        "api_key": {
          "default": "",
          "description": "API key for LLM provider",
          "title": "Api Key",
          "type": "string"
        }
      },
      "title": "LLM",
      "type": "object"
    },
    "Profile": {
      "additionalProperties": false,
      "description": "Model for profile configuration.",
      "properties": {
        "name": {
          "default": "default",
          "description": "Profile name",
          "title": "Name",
          "type": "string"
        },
        "boundaries": {
          "anyOf": [
            {
              "items": {
                "$ref": "#/$defs/Boundary"
              },
              "type": "array"
            },
            {
              "type": "null"
            }
          ],
          "default": [
            {
              "name": "github",
              "values": []
            },
            {
              "name": "jfrog",
              "values": []
            },
            {
              "name": "gitlab",
              "values": []
            },
            {
              "name": "bitbucket",
              "values": []
            },
            {
              "name": "azure_devops",
              "values": []
            },
            {
              "name": "dockerhub",
              "values": []
            },
            {
              "name": "domain",
              "values": [
                "localhost",
                "127.0.0.1",
                "0.0.0.0",
                "192.168.*",
                "10.0.*",
                "172.16.*"
              ]
            }
          ],
          "description": "Organization boundaries. Items in this list are internal to the profile.",
          "title": "Boundaries"
        }
      },
      "title": "Profile",
      "type": "object"
    },
    "Providers": {
      "additionalProperties": false,
      "description": "Model for Git provider configuration in AppConfig.",
      "properties": {
        "github": {
          "$ref": "#/$defs/GitHubProvider",
          "description": "GitHub provider configuration"
        },
        "gitlab": {
          "$ref": "#/$defs/GitLabProvider",
          "description": "GitLab provider configuration"
        }
      },
      "title": "Providers",
      "type": "object"
    },
    "WorkspaceConfig": {
      "additionalProperties": false,
      "description": "Model for workspace configuration in AppConfig.",
      "properties": {
        "path": {
          "default": "./.metagit",
          "description": "Workspace path",
          "title": "Path",
          "type": "string"
        },
        "default_project": {
          "default": "default",
          "description": "Default project",
          "title": "Default Project",
          "type": "string"
        },
        "dedupe": {
          "$ref": "#/$defs/WorkspaceDedupeConfig",
          "description": "Optional workspace-scoped repository deduplication settings"
        },
        "ui_show_preview": {
          "anyOf": [
            {
              "type": "boolean"
            },
            {
              "type": "null"
            }
          ],
          "default": true,
          "description": "Show preview in fuzzy finder console UI",
          "title": "Ui Show Preview"
        },
        "ui_menu_length": {
          "anyOf": [
            {
              "type": "integer"
            },
            {
              "type": "null"
            }
          ],
          "default": 10,
          "description": "Number of items to show in menu",
          "title": "Ui Menu Length"
        },
        "ui_preview_height": {
          "anyOf": [
            {
              "type": "integer"
            },
            {
              "type": "null"
            }
          ],
          "default": 3,
          "description": "Height of preview in fuzzy finder console UI",
          "title": "Ui Preview Height"
        },
        "ui_ignore_hidden": {
          "default": true,
          "description": "When true, hide dotfiles and dot-directories from repo picker UI",
          "title": "Ui Ignore Hidden",
          "type": "boolean"
        }
      },
      "title": "WorkspaceConfig",
      "type": "object"
    },
    "WorkspaceDedupeConfig": {
      "additionalProperties": false,
      "description": "Deduplicate synced repos within a workspace via a canonical directory.",
      "properties": {
        "enabled": {
          "default": false,
          "description": "When true, clone once under canonical_dir and symlink per project",
          "title": "Enabled",
          "type": "boolean"
        },
        "scope": {
          "$ref": "#/$defs/WorkspaceDedupeScope",
          "default": "workspace",
          "description": "Dedupe scope (v1 implements workspace only)"
        },
        "strategy": {
          "$ref": "#/$defs/WorkspaceDedupeStrategy",
          "default": "symlink",
          "description": "How project mounts reference canonical checkouts"
        },
        "canonical_dir": {
          "default": "_canonical",
          "description": "Directory under workspace.path holding canonical checkouts",
          "title": "Canonical Dir",
          "type": "string"
        }
      },
      "title": "WorkspaceDedupeConfig",
      "type": "object"
    },
    "WorkspaceDedupeScope": {
      "description": "Where repository deduplication is applied.",
      "enum": [
        "workspace",
        "global"
      ],
      "title": "WorkspaceDedupeScope",
      "type": "string"
    },
    "WorkspaceDedupeStrategy": {
      "description": "How duplicate repository identities share disk layout.",
      "enum": [
        "symlink"
      ],
      "title": "WorkspaceDedupeStrategy",
      "type": "string"
    }
  },
  "description": "Application-level settings (not the Metagit package release version \u2014 use `metagit version`).",
  "properties": {
    "agent_mode": {
      "default": false,
      "description": "When true, disable interactive UIs (fuzzy finder, prompts, editor). Overridden by METAGIT_AGENT_MODE when set.",
      "title": "Agent Mode",
      "type": "boolean"
    },
    "description": {
      "default": "Metagit configuration",
      "title": "Description",
      "type": "string"
    },
    "editor": {
      "default": "code",
      "description": "The editor to use for the CLI",
      "title": "Editor",
      "type": "string"
    },
    "api_url": {
      "default": "",
      "description": "The API URL to use for the CLI",
      "title": "Api Url",
      "type": "string"
    },
    "api_version": {
      "default": "",
      "description": "Reserved for a future remote API contract version (METAGIT_API_VERSION)",
      "title": "Api Version",
      "type": "string"
    },
    "api_key": {
      "default": "",
      "description": "The API key to use for the CLI",
      "title": "Api Key",
      "type": "string"
    },
    "cicd_file_data": {
      "default": "/Users/zacharyloeber/Zach-Projects/Personal/active/metagit-cli/src/metagit/data/cicd-files.json",
      "description": "The path to the cicd file data",
      "title": "Cicd File Data",
      "type": "string"
    },
    "file_type_data": {
      "default": "/Users/zacharyloeber/Zach-Projects/Personal/active/metagit-cli/src/metagit/data/file-types.json",
      "description": "The path to the file type data",
      "title": "File Type Data",
      "type": "string"
    },
    "package_manager_data": {
      "default": "/Users/zacharyloeber/Zach-Projects/Personal/active/metagit-cli/src/metagit/data/package-managers.json",
      "description": "The path to the package manager data",
      "title": "Package Manager Data",
      "type": "string"
    },
    "llm": {
      "$ref": "#/$defs/LLM",
      "default": {
        "enabled": false,
        "provider": "openrouter",
        "provider_model": "gpt-4o-mini",
        "embedder": "ollama",
        "embedder_model": "nomic-embed-text",
        "api_key": ""
      },
      "description": "The LLM configuration"
    },
    "workspace": {
      "$ref": "#/$defs/WorkspaceConfig",
      "default": {
        "path": "./.metagit",
        "default_project": "default",
        "dedupe": {
          "canonical_dir": "_canonical",
          "enabled": false,
          "scope": "workspace",
          "strategy": "symlink"
        },
        "ui_show_preview": true,
        "ui_menu_length": 10,
        "ui_preview_height": 3,
        "ui_ignore_hidden": true
      },
      "description": "The workspace configuration"
    },
    "profiles": {
      "default": [
        {
          "name": "default",
          "boundaries": [
            {
              "name": "github",
              "values": []
            },
            {
              "name": "jfrog",
              "values": []
            },
            {
              "name": "gitlab",
              "values": []
            },
            {
              "name": "bitbucket",
              "values": []
            },
            {
              "name": "azure_devops",
              "values": []
            },
            {
              "name": "dockerhub",
              "values": []
            },
            {
              "name": "domain",
              "values": [
                "localhost",
                "127.0.0.1",
                "0.0.0.0",
                "192.168.*",
                "10.0.*",
                "172.16.*"
              ]
            }
          ]
        }
      ],
      "description": "The profiles available to this appconfig",
      "items": {
        "$ref": "#/$defs/Profile"
      },
      "title": "Profiles",
      "type": "array"
    },
    "providers": {
      "$ref": "#/$defs/Providers",
      "default": {
        "github": {
          "api_token": "",
          "base_url": "https://api.github.com",
          "enabled": false
        },
        "gitlab": {
          "api_token": "",
          "base_url": "https://gitlab.com/api/v4",
          "enabled": false
        }
      },
      "description": "Git provider plugin configuration"
    }
  },
  "title": "AppConfig",
  "type": "object"
}
````

## File: src/metagit/core/mcp/runtime.py
````python
#!/usr/bin/env python
"""
Minimal MCP stdio runtime for Metagit tools and resources.
"""
⋮----
class InvalidToolArgumentsError(Exception)
⋮----
"""Raised when tool call arguments are invalid."""
⋮----
class MetagitMcpRuntime
⋮----
"""MCP runtime over stdio transport."""
⋮----
def __init__(self, root: Optional[str] = None) -> None
⋮----
def status_snapshot(self) -> dict[str, Any]
⋮----
"""Return a one-shot runtime status snapshot."""
⋮----
def run_stdio(self) -> None
⋮----
"""Run JSON-RPC message loop over stdio framing."""
⋮----
request = self._read_message()
⋮----
response = self._handle_request(request=request)
⋮----
def _handle_request(self, request: dict[str, Any]) -> Optional[dict[str, Any]]
⋮----
request_id = request.get("id")
method = request.get("method")
params = request.get("params", {})
⋮----
result = self._handle_initialize()
⋮----
result = self._handle_tools_list()
⋮----
result = self._handle_tools_call(params=params)
⋮----
result = self._handle_resources_list()
⋮----
result = self._handle_resources_read(params=params)
⋮----
result = {}
⋮----
def _handle_initialize(self) -> dict[str, Any]
⋮----
# MCP clients advertise capabilities at initialize time.
# We use this to decide whether server-driven sampling can be requested.
params = self._last_init_params
client_capabilities = params.get("capabilities", {}) if params else {}
⋮----
def _handle_tools_list(self) -> dict[str, Any]
⋮----
tool_names = self._registry.list_tools(status=status)
tools: list[dict[str, Any]] = []
⋮----
def _handle_tools_call(self, params: dict[str, Any]) -> dict[str, Any]
⋮----
name = params.get("name", "")
arguments = params.get("arguments", {}) or {}
⋮----
allowed = set(self._registry.list_tools(status=status))
⋮----
result = self._dispatch_tool(
⋮----
def _handle_resources_list(self) -> dict[str, Any]
⋮----
resources = []
⋮----
def _handle_resources_read(self, params: dict[str, Any]) -> dict[str, Any]
⋮----
uri = params.get("uri")
⋮----
repos = self._build_repo_index(status=status, config=config)
health_payload = None
⋮----
health_payload = self._workspace_health.check(
payload = self._resources.get_resource(
⋮----
raw_repos = arguments.get("repos")
repo_selectors = (
repo_paths = self._search_service.filter_repo_paths(
query = str(arguments.get("query", "")).strip()
⋮----
raw_paths = arguments.get("paths")
raw_exclude = arguments.get("exclude")
⋮----
raw_sem_repos = arguments.get("repos")
sem_selectors = (
⋮----
sem_query = str(arguments.get("query", "")).strip()
⋮----
limit_sem = int(arguments.get("limit_per_repo", 5))
⋮----
timeout_sem = int(arguments.get("timeout_seconds", 120))
⋮----
raw_tags = arguments.get("tags")
tag_filter: dict[str, str] | None = None
⋮----
tag_filter = {str(k): str(v) for k, v in raw_tags.items()}
limit_raw = arguments.get("limit", 10)
⋮----
limit_val = int(limit_raw)
⋮----
raw_status = arguments.get("status")
status_filter = (
sort_val = str(arguments.get("sort", "score"))
⋮----
has_url = arguments.get("has_url")
has_url_val = bool(has_url) if isinstance(has_url, bool) else None
sync_enabled = arguments.get("sync_enabled")
sync_enabled_val = (
result = self._managed_repo_search.search(
⋮----
blocker = str(arguments.get("blocker", "")).strip()
⋮----
repo_path = str(arguments.get("repo_path", "")).strip()
⋮----
only_if = str(arguments.get("only_if", "any"))
⋮----
max_parallel_raw = arguments.get("max_parallel", 4)
⋮----
max_parallel = int(max_parallel_raw)
⋮----
project_name = str(arguments.get("project_name", "")).strip()
⋮----
bundle = self._project_context.switch(
⋮----
snapshot_id = str(arguments.get("snapshot_id", "")).strip()
⋮----
raw_warn = arguments.get("branch_head_warning_days")
raw_crit = arguments.get("branch_head_critical_days")
raw_integration = arguments.get("integration_stale_days")
⋮----
branch_warn = float(raw_warn) if raw_warn is not None else 180.0
branch_crit = float(raw_crit) if raw_crit is not None else 365.0
integration_td = (
⋮----
loaded_app = AppConfig.load()
dedupe_cfg = (
⋮----
intent = arguments.get("intent")
pattern = arguments.get("pattern")
⋮----
raw_scope = arguments.get("project_scope")
selectors = (
⋮----
template = str(arguments.get("template", "")).strip()
raw_targets = arguments.get("target_projects")
⋮----
source_project = str(arguments.get("source_project", "")).strip()
⋮----
raw_types = arguments.get("dependency_types")
dependency_types = (
depth_raw = arguments.get("depth", 2)
⋮----
depth_val = int(depth_raw)
⋮----
gitnexus_repo = arguments.get("gitnexus_repo")
repo_name = (
⋮----
_ = (config_path, workspace_root)
⋮----
project_name = str(arguments.get("name", "")).strip()
⋮----
_ = config_path
project_filter = arguments.get("project_name")
⋮----
built = self._workspace_catalog.build_repo_from_fields(
⋮----
dry_run = bool(arguments.get("dry_run", False))
move_disk = bool(arguments.get("move_disk", True))
force = bool(arguments.get("force", False))
⋮----
recent = arguments.get("recent_repos")
recent_repos = (
env_raw = arguments.get("env_overrides")
env_overrides = (
⋮----
root = status.root_path or str(Path.cwd())
context = self._discovery_service.build_context(repo_root=root)
⋮----
sampling_payload = self._request_client_sampling(context=context)
⋮----
sampled_text = self._extract_sampling_text(sampling_payload)
⋮----
def sampler(_payload: dict[str, str]) -> str
⋮----
_ = _payload
⋮----
sampled_service = BootstrapSamplingService(
⋮----
def _resolve_status_and_config(self) -> tuple[WorkspaceStatus, Any]
⋮----
resolved_root = self._resolver.resolve(
status = self._gate.evaluate(root_path=resolved_root)
config = None
⋮----
manager = MetagitConfigManager(
loaded = manager.load_config()
config = None if isinstance(loaded, Exception) else loaded
⋮----
def _catalog_paths(self, status: WorkspaceStatus, config: Any) -> tuple[str, str]
⋮----
config_path = str(Path(status.root_path) / ".metagit.yml")
⋮----
error: dict[str, Any] = {"code": code, "message": message}
⋮----
def _read_message(self) -> Optional[dict[str, Any]]
⋮----
"""Read one MCP-framed JSON-RPC message from stdin."""
content_length: Optional[int] = None
⋮----
header_line = sys.stdin.buffer.readline()
⋮----
header_text = header_line.decode("utf-8").strip()
⋮----
content_length = int(value.strip())
⋮----
body = sys.stdin.buffer.read(content_length)
⋮----
def _write_message(self, payload: dict[str, Any]) -> None
⋮----
"""Write one MCP-framed JSON-RPC message to stdout."""
body = json.dumps(payload).encode("utf-8")
header = f"Content-Length: {len(body)}\r\n\r\n".encode("utf-8")
⋮----
def _request_client_sampling(self, context: dict[str, str]) -> dict[str, Any]
⋮----
"""Request client sampling synchronously when supported."""
request_id = self._next_server_request_id
⋮----
sampling_request = {
⋮----
inbound = self._read_message()
⋮----
# Handle regular inbound requests while waiting on sampling response.
response = self._handle_request(request=inbound)
⋮----
def _extract_sampling_text(self, sampling_result: dict[str, Any]) -> Optional[str]
⋮----
"""Extract sampled text from sampling/createMessage result."""
content = sampling_result.get("content")
````

## File: src/metagit/core/utils/fuzzyfinder.py
````python
#! /usr/bin/env python3
⋮----
"""
This is a fuzzy finder that uses Textual and rapidfuzz to find items in a list.
I'm only doing this because I don't want to have to wrap the fzf binary in a python script.
"""
⋮----
class FuzzyFinderTarget(BaseModel)
⋮----
"""A target for a fuzzy finder."""
⋮----
name: str
description: str
color: Optional[str] = None
opacity: Optional[float] = None
⋮----
class FuzzyFinderConfig(BaseModel)
⋮----
"""Configuration for a fuzzy finder using Textual and rapidfuzz."""
⋮----
items: List[Union[str, Any]] = Field(
display_field: Optional[str] = Field(
score_threshold: float = Field(
max_results: int = Field(
scorer: str = Field(
prompt_text: str = Field(
case_sensitive: bool = Field(
multi_select: bool = Field(False, description="Allow selecting multiple items.")
enable_preview: bool = Field(
preview_field: Optional[str] = Field(
preview_header: Optional[str] = Field(None, description="Header for preview pane.")
sort_items: bool = Field(True, description="Whether to sort the items.")
# Styling options
highlight_color: str = Field(
normal_color: str = Field("white", description="Color/style for normal items.")
prompt_color: str = Field("bold cyan", description="Color/style for prompt text.")
separator_color: str = Field("gray", description="Color/style for separator line.")
item_opacity: Optional[float] = Field(
custom_colors: Optional[Dict[str, str]] = Field(
color_field: Optional[str] = Field(
total_count: Optional[int] = Field(
query_mode_label: str = Field(
⋮----
@field_validator("items")
@classmethod
    def validate_items(cls, v: List[Any], info: Any) -> List[Any]
⋮----
"""Ensure items are valid and consistent with display_field."""
⋮----
@field_validator("scorer")
@classmethod
    def validate_scorer(cls, v: str) -> str
⋮----
"""Ensure scorer is valid."""
valid_scorers = ["partial_ratio", "ratio", "token_sort_ratio"]
⋮----
@field_validator("preview_field")
@classmethod
    def validate_preview_field(cls, v: Optional[str], info: Any) -> Optional[str]
⋮----
"""Ensure preview_field is valid if enable_preview is True."""
⋮----
def get_scorer_function(self) -> Union[Callable[..., float], Exception]
⋮----
"""Return the rapidfuzz scorer function based on configuration."""
⋮----
scorer_map: Dict[str, Callable[..., float]] = {
⋮----
def get_display_value(self, item: Any) -> Union[str, Exception]
⋮----
"""Extract the display value from an item."""
⋮----
def get_preview_value(self, item: Any) -> Union[Optional[str], Exception]
⋮----
"""Extract the preview value from an item if preview is enabled."""
⋮----
def get_item_color(self, item: Any) -> Optional[str]
⋮----
"""Get the color for an item, prioritizing FuzzyFinderTarget.color over custom_colors."""
⋮----
# First check if item is a FuzzyFinderTarget with a color property
⋮----
# Fall back to custom_colors mapping if available
⋮----
# Determine the key to use for color lookup
⋮----
# Use specified color field
⋮----
color_key = item
⋮----
color_key = str(getattr(item, self.color_field))
⋮----
# Use display field
color_key = str(getattr(item, self.display_field))
⋮----
# Use string representation
color_key = str(item)
⋮----
def get_item_opacity(self, item: Any) -> Optional[float]
⋮----
"""Get the opacity for an item, prioritizing FuzzyFinderTarget.opacity over config.item_opacity."""
⋮----
# First check if item is a FuzzyFinderTarget with an opacity property
⋮----
# Fall back to config's item_opacity
⋮----
class FuzzyFinderApp(App)
⋮----
"""A Textual app for fuzzy finding."""
⋮----
CSS = """
⋮----
BINDINGS = [
⋮----
def __init__(self, config: FuzzyFinderConfig, **kwargs)
⋮----
def compose(self) -> ComposeResult
⋮----
"""Create child widgets for the app."""
⋮----
# Input field
⋮----
# Just results
⋮----
def on_mount(self) -> None
⋮----
"""Called when app starts."""
# Initial search with empty query
⋮----
# Focus the input
⋮----
def on_input_changed(self, event: Input.Changed) -> None
⋮----
"""Called when the input changes."""
⋮----
def _perform_search(self, query: str) -> None
⋮----
"""Perform fuzzy search and update results."""
⋮----
results = self._search(query)
⋮----
# Handle error - for now just show empty results
results = []
⋮----
# Handle error gracefully
⋮----
def _update_results_meta(self, query: str) -> None
⋮----
"""Show concise result counters for current query."""
⋮----
meta = self.query_one("#results_meta", Static)
shown_count = len(self.current_results)
total_count = (
cap_count = self.config.max_results
mode_label = self.config.query_mode_label
query_label = query if query else "all"
⋮----
def _update_results_list(self) -> None
⋮----
"""Update the results ListView."""
results_list = self.query_one("#results_list", ListView)
⋮----
display_value = self.config.get_display_value(result)
⋮----
display_value = str(result)
⋮----
# Create list item
item = ListItem(Label(display_value))
⋮----
# Apply highlighting
⋮----
# Apply custom color if configured
custom_color = self.config.get_item_color(result)
⋮----
# Parse and apply the custom color
⋮----
# Apply opacity - prioritize FuzzyFinderTarget.opacity over config.item_opacity
item_opacity = self.config.get_item_opacity(result)
⋮----
# Set opacity via inline style
⋮----
# Set the ListView's index to match our highlighted_index
⋮----
def _apply_custom_color(self, item: ListItem, color_spec: str) -> None
⋮----
"""Apply custom color to a list item based on color specification."""
⋮----
# Handle different color formats
⋮----
# Hex color
⋮----
# Background color
bg_color = color_spec[3:]  # Remove 'bg:' prefix
⋮----
# Color with background (e.g., "white bg:#ff0000")
parts = color_spec.split(" bg:")
⋮----
# Basic color names
⋮----
# Try to apply as-is (could be a rich color spec)
⋮----
# If color application fails, silently continue
⋮----
def _update_preview(self) -> None
⋮----
"""Update the preview pane."""
⋮----
preview_pane = self.query_one("#preview_pane", Static)
⋮----
highlighted_item = self.current_results[self.highlighted_index]
preview_value = self.config.get_preview_value(highlighted_item)
⋮----
preview_value = str(highlighted_item)
⋮----
preview_text = f"{self.config.preview_header}\n\n{preview_value}"
⋮----
preview_text = preview_value
⋮----
def on_list_view_selected(self, event: ListView.Selected) -> None
⋮----
"""Called when a list item is selected."""
⋮----
# Update highlighted index based on selection
⋮----
def on_list_view_highlighted(self, event: ListView.Highlighted) -> None
⋮----
"""Called when a list item is highlighted (but not selected)."""
⋮----
# Keep our highlighted_index in sync with ListView
⋮----
def action_cursor_up(self) -> None
⋮----
"""Move cursor up."""
⋮----
def action_cursor_down(self) -> None
⋮----
"""Move cursor down."""
⋮----
def _scroll_to_highlighted(self) -> None
⋮----
"""Scroll the results list to ensure the highlighted item is visible."""
⋮----
# Get the highlighted list item
highlighted_item = results_list.children[self.highlighted_index]
# Scroll to make the item visible
⋮----
# If scrolling fails, continue without it
⋮----
def action_page_up(self) -> None
⋮----
"""Move cursor up by a page (10 items)."""
⋮----
page_size = 10
⋮----
def action_page_down(self) -> None
⋮----
"""Move cursor down by a page (10 items)."""
⋮----
max_index = len(self.current_results) - 1
⋮----
def action_cursor_home(self) -> None
⋮----
"""Move cursor to the first item."""
⋮----
def action_cursor_end(self) -> None
⋮----
"""Move cursor to the last item."""
⋮----
def action_select(self) -> None
⋮----
"""Select the highlighted item."""
# First try to get the current selection from the ListView
⋮----
# Select the highlighted item
⋮----
def action_quit(self) -> None
⋮----
"""Quit the application."""
⋮----
def _search(self, query: str) -> Union[List[Any], Exception]
⋮----
"""Perform fuzzy search based on the query."""
⋮----
items_to_search = self.config.items
⋮----
# Sort items based on their display value
items_to_search = sorted(
⋮----
# If sorting fails, proceed without sorting
⋮----
choices_with_originals = [
# Check for exceptions
choice_exceptions = [
⋮----
choices = [str(c[0]) for c in choices_with_originals]
⋮----
# Prepare query for case-insensitive matching
query_lower = query.lower() if not self.config.case_sensitive else query
⋮----
scorer_func = self.config.get_scorer_function()
⋮----
# Get fuzzy search results
results = process.extract(
⋮----
limit=len(choices),  # Get all results for custom sorting
⋮----
# Custom scoring and sorting to prioritize exact matches
scored_results = []
⋮----
choice_lower = (
⋮----
# Calculate custom score based on match type
custom_score = score
⋮----
# Bonus for exact matches
⋮----
# Bonus for prefix matches
⋮----
# Bonus for longer matches (more specific)
⋮----
length_bonus = min(100, (len(choice_lower) - len(query_lower)) * 10)
⋮----
# Sort by custom score (highest first) and then by original string length (shorter first for same score)
⋮----
# Return all matched results to allow full manual scrolling.
⋮----
class FuzzyFinder
⋮----
"""A reusable fuzzy finder using Textual and rapidfuzz with navigation support."""
⋮----
def __init__(self, config: FuzzyFinderConfig)
⋮----
"""Initialize the fuzzy finder with a configuration."""
⋮----
def run(self) -> Union[Optional[Union[str, List[str], Any]], Exception]
⋮----
"""Run the fuzzy finder application."""
⋮----
app = FuzzyFinderApp(self.config)
result = app.run()
⋮----
# Multi-select not fully implemented yet
⋮----
def fuzzyfinder(query: str, collection: List[str]) -> List[str]
⋮----
"""
    Simple fuzzy finder function that returns matching items from a collection.

    Args:
        query: Search query string
        collection: List of strings to search in

    Returns:
        List of matching strings
    """
⋮----
# Use rapidfuzz to find matches
⋮----
# Return items with score >= 70
````

## File: README.md
````markdown
# Metagit

Metagit gives you situational awareness across Git repositories. It helps multi-repo projects feel manageable by keeping stack details, generated artifacts, dependencies, and related metadata in one place.

## About

This tool works well for scenarios like:

1. At-a-glance view of a project's technical stacks, languages, external dependencies, and generated artifacts.
2. Switching between many Git projects during the day without losing context.
3. Isolating outside dependencies that weaken the security and dependability of your software delivery pipelines.
4. Automated documentation of a code's provenance.
5. Helping new contributors get from onboarding to first commit faster.

Metagit is designed for developers, SREs, and AI agents who work across connected repositories. It tracks the dependencies and project relationships that are easy to miss when you only look at one repo at a time.

## Quick start

Install or upgrade the CLI globally with [uv](https://docs.astral.sh/uv/):

```bash
uv tool install metagit-cli
uv tool install -U metagit-cli   # upgrade later
metagit version
```

> Use the PyPI package name **`metagit-cli`**. The `metagit` package on PyPI is a different project.

Install bundled agent skills (OpenClaw, Hermes, Claude Code, and others):

```bash
metagit skills list
metagit skills install --scope user --target openclaw --target hermes
```

Use `--scope project` when installing into a specific umbrella repository checkout. See [Skills](docs/skills.md) for targets, MCP install, and the project-management skill for agents.

## Audience

This tool targets:

- DevOps Engineers
- Polyglot developers
- New team members
- Project Managers
- SREs
- Solution Engineers
- AI Agents (more to come!)

## Metagit is NOT...

### ...an SBOM tool

SBOM output is often thousands of lines and includes full transitive dependency trees. That level of detail is usually too heavy for day-to-day situational awareness and agent context. Metagit may read SBOM manifests as an input in the future, but it is not trying to replace SBOM tooling.

Metagit uses common project files (for example `go.mod`, `package.json`, and `requirements.txt`) for detection and validation boundaries. These are used to identify stack composition, not to provide exhaustive version intelligence.

### ...a Git client

Despite the name, this still relies on Git and your existing hosting platform.

## ...a full project packer

Metagit intentionally focuses on the highest-value project signals. It does not package full repositories. If you need full-project packing, use [repomix](https://github.com/yamadashy/repomix/tree/main).

### Why brevity?

One of the core goals is reducing cognitive load when understanding project relationships. A practical side effect is lower token usage for automated AI workflows.

## How It Works

Metagit stores project configuration metadata in `.metagit.yml` inside the repository. That file follows a schema that the CLI can validate and read.

If you use Metagit for dozens of repositories (an umbrella workspace), you can edit the config manually or refresh it with heuristics and AI-assisted workflows.

## Modes

Metagit supports several operating modes:

### Workspace Mode

This is the first planned open-source CLI mode.

In this mode, you group related repositories into one workspace that you can open in VS Code or access individually from the terminal.

> **AKA** Multi-repo as Monorepo

You use one top-level umbrella project with a single metagit definition file that tracks related repositories and local target folders. You can then sync that workspace locally.

The metagit configuration file is committed to version control as its own project artifact.

**Managed repo lookup:** Use `metagit search` / `metagit find` for quick CLI lookup of repos declared in `.metagit.yml` (with optional tags and JSON output). MCP clients can call `metagit_repo_search`, and `metagit api serve` exposes the same search and resolve behavior over a small local JSON HTTP API for agents and scripts.

This mode is useful for:

- Creating umbrella projects for new team members of a multi-repo project
- Individual power users that need to quickly pivot between several project repositories that comprise a larger team effort
- Keeping loosely coupled Git projects grouped without relying on submodules

## Metadata Mode

This mode uses the same config file as workspace mode, with additional metadata such as primary language, frameworks, and other context you want available when entering a repo.

Configuring this by hand for one project is simple. Doing it across dozens or thousands of repos is not. Metagit uses detection heuristics to automate as much as possible and can use AI workflows where deterministic code is not enough.

> **Note**: AI-assisted detection should be monitored and converted into deterministic logic over time.

In this mode, Metagit helps answer questions like:

- What other projects are related to this project?
- What application and development stacks does this project use?
- What external dependencies exist for this project?
- What artifacts does this project create?
- What branch strategy is employed?
- What version strategy is employed?

> **External dependencies** are a common source of pipeline instability.

## Install

Global install and skill setup are covered in [Quick start](#quick-start) above.

### Local first-run

Inside any Git repository:

```bash
metagit init
```

That creates `.metagit.yml` and updates `.gitignore`.

## Skills

Bundled skills ship with the package and install via `metagit skills install` (see [docs/skills.md](docs/skills.md)). For development in this repository, `skills/` is the source tree; run `task skills:sync` to mirror into `.cursor/skills/`.

## Agent guides

- [Hermes agents and organization-wide IaC](hermes-iac-workspace-guide.md) — illustrated workflow for using Metagit as a control plane across Terraform, policy, and module repositories (controller + subagents, layered `agent_instructions`, MCP tools).

## Documentation

For installation guidance, detailed usage, including full CLI command surface, local MCP runtime setup, API-oriented flows, and advanced examples, use the documentation site:

- [Documentation](https://metagit-ai.github.io/metagit-cli/)

## License

This project is licensed under the MIT License. See the [LICENSE](./LICENSE.md) file for details.
````

## File: src/metagit/core/project/manager.py
````python
#!/usr/bin/env python
"""
Class for managing projects.
"""
⋮----
"""Construct a ProjectManager using workspace path and effective dedupe settings."""
project: Optional[WorkspaceProject] = None
⋮----
project = find_project(metagit_config, project_name)
dedupe = resolve_effective_dedupe(app_config.workspace.dedupe, project)
⋮----
class ProjectManager
⋮----
"""
    Manager class for handling projects within a workspace.
    """
⋮----
"""
        Initialize the ProjectManager.

        Args:
            workspace_path: The root path of the workspace.
            logger: The logger instance for output.
            dedupe: When enabled, sync uses a canonical directory and project symlinks.
        """
⋮----
"""
        Add a repository to a specific project in the configuration.

        Args:
            project_name: The name of the project to add the repository to.
            repo: The ProjectPath object representing the repository to add. If None, will prompt for data.
            metagit_config: The MetagitConfig instance to work with configuration data.

        Returns:
            Union[ProjectPath, Exception]: ProjectPath if successful, Exception if failed.
        """
config_manager = MetagitConfigManager(metagit_config=metagit_config)
⋮----
# Validate inputs
⋮----
# Check if workspace configuration exists
⋮----
# Find the target project
target_project = None
⋮----
target_project = project
⋮----
repo_result = UserPrompt.prompt_for_model(
⋮----
repo = repo_result
⋮----
# Check if name already exists in the project
⋮----
duplicates = workspace_dedupe.find_duplicate_identities(
⋮----
locations = ", ".join(f"{proj}/{name}" for proj, name in duplicates)
⋮----
# Add the repository to the project
⋮----
# Save the updated configuration
save_result = config_manager.save_config(metagit_config, config_path)
⋮----
def sync(self, project: WorkspaceProject, *, hydrate: bool = False) -> bool
⋮----
"""
        Sync a workspace project concurrently.

        Iterates through each repository in the project and either creates a
        symbolic link for local paths or clones it if it's a remote repository.
        After syncing, creates a VS Code workspace file.

        When ``hydrate`` is True, symlink mounts are replaced with full directory
        copies (see ``hydrate_project``).

        Returns:
            bool: True if sync is successful, False otherwise.
        """
project_dir = os.path.join(self.workspace_path, project.name)
⋮----
future_to_repo = {
⋮----
repo = future_to_repo[future]
⋮----
hydrate_ok = self.hydrate_project(project)
⋮----
# Create VS Code workspace file after successful sync
workspace_result = self._create_vscode_workspace(project, project_dir)
⋮----
# Don't fail the entire sync for workspace file creation issues
# else:
#     tqdm.write(f"Created VS Code workspace file: {workspace_result}")
⋮----
def hydrate_project(self, project: WorkspaceProject) -> bool
⋮----
"""
        Materialize symlink mounts under a project into full directory copies.

        Returns:
            bool: True when all hydrate steps succeed or are skipped cleanly.
        """
project_dir = self.workspace_path / project.name
⋮----
ok = True
⋮----
mount = project_dir / repo.name
⋮----
ok = False
⋮----
"""
        Create a VS Code workspace file for the project.

        Args:
            project: The workspace project containing repository information
            project_dir: The directory where the project is located

        Returns:
            Path to the created workspace file on success, Exception on failure
        """
⋮----
# Get list of repository names that were successfully synced
repo_names = []
⋮----
repo_path = os.path.join(project_dir, repo.name)
⋮----
# Create workspace file content
workspace_content = create_vscode_workspace(project.name, repo_names)
⋮----
# Write workspace file
workspace_file_path = os.path.join(project_dir, "workspace.code-workspace")
⋮----
"""
        Sync a single repository.

        This method is called by the thread pool executor.
        """
mount_path = os.path.join(project_dir, repo.name)
⋮----
"""Sync using canonical storage and a per-project symlink mount."""
⋮----
identity = workspace_dedupe.build_repo_identity(repo)
⋮----
canonical = workspace_dedupe.canonical_path(
mount = Path(mount_path)
⋮----
"""Place source under canonical (symlink) and mount project symlink."""
⋮----
desc = f"  ✅   🔗 {repo.name}"
⋮----
bar_format = (
⋮----
"""Clone into canonical when missing, then symlink the project mount."""
⋮----
desc = f"  🔗 {repo.name}"
⋮----
def _sync_local(self, repo: ProjectPath, target_path: str, position: int) -> None
⋮----
"""Handle syncing of a local repository via symlink."""
source_path = Path(repo.path).expanduser().resolve()
⋮----
mount = Path(target_path)
⋮----
def _sync_remote(self, repo: ProjectPath, target_path: str, position: int) -> None
⋮----
"""Handle syncing of a remote repository via git clone."""
⋮----
class CloneProgressHandler(git.RemoteProgress)
⋮----
def __init__(self, pbar: tqdm) -> None
⋮----
op_code: int,  # noqa: ARG002
⋮----
self.pbar.update(0)  # Manually update the progress bar
⋮----
desc = f"  ⤵️ {repo.name}"
⋮----
"""
        Directories or symlinks under the project sync folder that are not declared repos.

        Uses the same .gitignore rules as the repo picker. When ``ignore_hidden`` is true,
        dotfiles and dot-directories are skipped.
        """
project_path = Path(self.workspace_path) / project
⋮----
workspace_project = metagit_config.local_workspace_project
⋮----
matches = [
⋮----
workspace_project = matches[0]
⋮----
managed = {repo.name for repo in workspace_project.repos}
ignore_patterns = parse_gitignore(project_path / ".gitignore")
unmanaged: List[Path] = []
⋮----
@staticmethod
    def remove_sync_directory(path: Path) -> None
⋮----
"""Remove a synced repo directory or symlink under a project folder."""
⋮----
"""
        Select a repository from a synced project.
        """
⋮----
project_path: str = os.path.join(self.workspace_path, project)
⋮----
workspace_project = [
⋮----
project_dict = {}
ignore_patterns = parse_gitignore(Path(project_path) / ".gitignore")
# Iterate through the project path and add the directories and symlinks to the project_dict
⋮----
# Determine type
item_type = "Symlink" if f.is_symlink() else "Directory"
⋮----
# Check for git repository
has_git = (f / ".git").exists() if f.is_dir() else False
⋮----
# Check for .metagit.yml file
has_metagit_yml = (f / ".metagit.yml").exists() if f.is_dir() else False
⋮----
# Build initial description
description_parts = [
⋮----
target_path = f.readlink()
# Convert absolute path to relative path from project_path
⋮----
relative_target = os.path.relpath(target_path, project_path)
⋮----
# Fallback to original path if relative path calculation fails
⋮----
# Add git and metagit info
⋮----
# Track which items exist in the project configuration
managed_repos = {repo.name for repo in workspace_project.repos}
⋮----
# Iterate through the workspace project and add the repo descriptions to the project_dict
⋮----
# Update the description with management status and repo description
summary_lines = self._build_project_repo_summary(repo)
⋮----
# This repo is configured but doesn't exist on filesystem
⋮----
# Add unmanaged status to items that exist on filesystem but not in config
⋮----
current_description = project_dict[item_name]
⋮----
projects: List[FuzzyFinderTarget] = []
⋮----
# Determine color based on directory type (check filesystem)
target_path = Path(project_path) / target
is_symlink = target_path.is_symlink()
⋮----
# Set color: white for directories, light blue for symlinks
color = (
⋮----
)  # light blue for symlinks, white for directories
⋮----
# Set opacity: 1.0 if managed (exists in project list), 0.5 if not managed
opacity = 1.0 if target in managed_repos else 0.5
⋮----
finder_config = FuzzyFinderConfig(
finder = FuzzyFinder(finder_config)
selected = finder.run()
⋮----
selected_path = os.path.join(project_path, selected.name)
# If the path starts with ../../, replace it with ./
⋮----
selected_path = selected_path.replace("../../", "./", 1)
⋮----
@staticmethod
    def _append_preview_lines(existing: str, lines: List[str]) -> str
⋮----
"""Append non-empty lines to an existing preview block."""
final_lines = [line for line in lines if line]
⋮----
@staticmethod
    def _build_preview_sections(lines: List[str]) -> str
⋮----
"""Build a readable preview body from collected lines."""
⋮----
@staticmethod
    def _build_project_repo_summary(repo: ProjectPath) -> List[str]
⋮----
"""Build metadata lines for configured project repositories."""
summary_lines = ["Status: ✅ Managed"]
````

## File: CHANGELOG.md
````markdown
# Changelog

## Unreleased

### Added

- `.metagit.yml` **documentation** entries support rich objects (`kind`, `path`, `url`, `tags`, `metadata`) plus legacy bare strings; `documentation_graph_nodes()` exports ingest payloads.
- Top-level **graph** block for manual cross-repo **relationships** (merged into cross-project dependency maps and `graph_export_payload()` for GitNexus-style exports).
- Per-project `dedupe.enabled` override on `workspace.projects[]` in `.metagit.yml` (overrides app-config `workspace.dedupe.enabled` for sync and layout under that project).
- `metagit prompt` kind `repo-enrich` (repo scope): CLI workflow to discover repo metadata (`metagit detect`, `project source sync`) and merge into the workspace manifest entry.
- Bundled skill `metagit-cli`: CLI-only shortcuts for agents, including every `metagit prompt` kind and common catalog/detect/sync commands (no MCP or HTTP API).
- `metagit prompt` command group: `list`, `workspace`, `project`, and `repo` subcommands emit built-in operational prompts or composed manifest `agent_instructions` (`--kind`, `--json`, `--text-only`).
- Top-level `agent_mode` in app config (default false), overridable via `METAGIT_AGENT_MODE`; disables interactive UIs (fuzzy finder, prompts, editor, prune confirms) across CLI when enabled.
- `metagit appconfig show` prints the full active configuration with `--format yaml|json|minimal-yaml` (includes `workspace.dedupe` and effective `agent_mode`).

### Added

- `metagit project sync --hydrate` materializes symlink mounts into full directory copies with per-file tqdm progress.

### Changed

- `workspace.dedupe.enabled` defaults to **false** in app config; enable in `metagit.config.yaml` or per-project `dedupe.enabled` in `.metagit.yml` when canonical checkouts are desired.
- `load_config()` applies environment variable overrides (same as `AppConfig.load()`), including `METAGIT_AGENT_MODE` and `METAGIT_WORKSPACE_DEDUPE_ENABLED`.
- `metagit config show` prints the source `.metagit.yml` by default (preserves your formatting); use `--normalized` for a readable model round-trip (`|` blocks, Unicode not escaped) or `--json` for agents.

### Added

- Workspace layout rename/move: rename projects and repos (manifest + sync folders), move repos across projects; CLI (`workspace project rename`, `workspace repo rename|move`), MCP (`metagit_workspace_project_rename`, `metagit_workspace_repo_rename`, `metagit_workspace_repo_move`), HTTP v2 (`POST /v2/projects/{name}/rename`, `/v2/repos/.../rename|move`). Supports `--dry-run`, `--manifest-only`, dedupe symlink mounts, and session file migration on project rename. See `docs/reference/workspace-layout-api.md`.
- Workspace catalog CRUD with JSON output: CLI (`metagit workspace list|project|repo`, `metagit project list --all`, `project add|remove`, `project repo list|remove`, `--json` on catalog commands), MCP tools (`metagit_workspace_list`, `metagit_workspace_projects_list`, `metagit_workspace_project_add|remove`, `metagit_workspace_repos_list`, `metagit_workspace_repo_add|remove`), and HTTP API v2 (`/v2/workspace`, `/v2/projects`, `/v2/repos`). Manifest-only repo/project removal; use `project repo prune` to delete unmanaged directories on disk.
- Docs: [Hermes agents and organization-wide IaC](docs/hermes-iac-workspace-guide.md) — illustrated controller/subagent workflow, manifest examples, and MCP tool map for platform IaC estates.
- Layered `agent_instructions` on `.metagit.yml` (file, workspace, project, repo/path); legacy `agent_prompt` accepted on load. `AgentInstructionsResolver` composes stacks for MCP project context (`instruction_layers`, `effective_agent_instructions`, per-repo `agent_instructions`).
- MCP `metagit_workspace_semantic_search` runs GitNexus `query` per managed repo (requires registry + index) for vector-ranked process results.
- MCP `metagit_workspace_health_check` includes branch age (`head_commit_age_days`, `merge_base_age_days`) when `check_stale_branches` is enabled, with thresholds `branch_head_warning_days` / `branch_head_critical_days` / `integration_stale_days` and summary counters for stale HEAD and integration drift.
- MCP Phase 3 workspace intelligence: `metagit_workspace_health_check`, `metagit_workspace_discover`, and `metagit_project_template_apply` (dry-run by default), plus resources `metagit://workspace/health` and `metagit://workspace/context`.
- MCP `metagit_cross_project_dependencies` to map declared, import-hint, and shared-config relationships between workspace projects with GitNexus index status per repo.
- MCP Phase 1 search/sync improvements: `metagit_repo_search` filters (`status`, `has_url`, `sync_enabled`, `sort`), `metagit_workspace_search` ripgrep-backed hits with `repos`/`paths`/`exclude`/`context_lines`/`intent`, and batch `metagit_workspace_sync` with `only_if` and `dry_run`.
- MCP project context tools: `metagit_project_context_switch`, `metagit_workspace_state_snapshot`, `metagit_workspace_state_restore`, and `metagit_session_update` for switching workspace projects with persisted session state under `.metagit/sessions/` and git-state snapshots under `.metagit/snapshots/`.
- Managed repository search across `.metagit.yml` workspace repos: CLI (`metagit search` / `metagit find`), MCP tool `metagit_repo_search`, and local JSON HTTP API (`metagit api serve` with `/v1/repos/search` and `/v1/repos/resolve`).
- `metagit project repo prune` to review and remove sync-folder directories not declared in `.metagit.yml` (with `--dry-run`, `--include-hidden`, and `--force` to skip prompts).
- `workspace.ui_ignore_hidden` in app config (default true) to hide dot-directories from the repo picker UI.

### Changed

- Removed redundant `config.version` from application config; use `metagit version` for the installed package. Legacy `version` keys in YAML are ignored on load. `api_version` remains for a future remote API contract (default empty; `METAGIT_API_VERSION` still applies).

### Fixed

- Workspace search: preset names that map to intent globs (e.g. `terraform`) now pass `**/*.tf` include globs to ripgrep; if ripgrep returns no hits while a `preset` or `intent` is set, the term-based filesystem fallback runs so Ubuntu/CI still gets matches when `rg` is installed but misbehaves or misparses.
- Workspace search fallback without `rg` matches preset-expanded terms (e.g. `preset=terraform`) instead of treating the composed `|` pattern as one literal string; fixes empty results when ripgrep is not installed.
- `task test` now runs `uv run pytest` so tests use the project virtualenv (fixes `ModuleNotFoundError: loguru` when `pytest` was not the venv binary).

## [0.2.2](https://github.com/metagit-ai/metagit-cli/compare/v0.2.1...v0.2.2) (2026-05-06)


### Bug Fixes

* revamp release workflow ([cafd6da](https://github.com/metagit-ai/metagit-cli/commit/cafd6dac4777c8528cc1c996bb8f1a394c40d53d))
````

## File: Taskfile.yml
````yaml
# yaml-language-server: $schema=https://taskfile.dev/schema.json

version: "3"
silent: true
env:
  PROJECT:
    sh: 'echo "$(basename $(pwd))"'
  LOCAL_BIN_PATH:
    sh: 'echo "{{.ROOT_DIR}}/.venv/bin"'
  BUILD_DATE:
    sh: "date '+%Y-%m-%d-%H:%M:%S'"
  BUILD_DATE_SHORT:
    sh: "date '+%Y-%m-%d-%H%M%S'"
  GIT_LATEST_TAG:
    sh: 'git tag -l 2>/dev/null | sort -r -V | head -n 1 2>/dev/null || echo "not a git repo"'
  TERM: screen-256color
  DOCS_PATH: "{{.ROOT_DIR}}/docs"
  PYTHON_VENV_PATH: "{{.ROOT_DIR}}/.venv"
  SCRIPT_PATH: "{{.ROOT_DIR}}/scripts"
  VERSION: '{{default "unknown" .GIT_LATEST_TAG}}'
  COMPOSE_BAKE: true

dotenv:
  - ".env"
  - ".SECRETS.env"

includes:
  docker:
    taskfile: ./tasks/Taskfile.docker.yml
    optional: true
  github:
    taskfile: ./tasks/Taskfile.github.yml
    optional: true
  python:
    taskfile: ./tasks/Taskfile.python.yml
    optional: true
  mcp:
    taskfile: ./tasks/Taskfile.mcp.yml
    optional: true

tasks:
  default:
    cmds:
      - |
        task -l

  show:
    desc: Show task variables
    cmds:
      - |
        echo "ROOT_PATH: {{.ROOT_DIR}}"
        echo "PROJECT: {{.PROJECT}}"
        echo "VERSION: {{.VERSION}}"
        echo "OS: {{OS}}"
        echo "ARCH: {{ARCH}}"
        echo "LOCAL_BIN_PATH: {{.LOCAL_BIN_PATH}}"
        echo "PYTHON_VENV_PATH: {{.PYTHON_VENV_PATH}}"
        echo "SCRIPT_PATH: {{.SCRIPT_PATH}}"
        echo "BUILD_DATE: {{.BUILD_DATE}}"
        echo "GIT_LATEST_TAG: {{.GIT_LATEST_TAG}}"

  show:all:
    desc: Show all variables for task namespaces
    cmds:
      - |
        echo "## Show ##";
        task show
        echo ""
        for taskitem in $(task -l | cut -d " " -f2 | grep show | sed 's/.$//'); do
          if [[ "$taskitem" != "show:all" ]]; then
            echo "## Show - ${taskitem} ##";
            task $taskitem;
            echo "";
          fi
        done

  list:
    desc: List tasks by namespace (task list -- <namespace>)
    cmds:
      - |
        if [[ "{{.CLI_ARGS}}" != "" ]]; then
          task -l | grep {{default "" .CLI_ARGS}}
        else
          task -l
        fi

  toc:
    desc: Update the table of contents in README.md
    cmds:
      - uv pip install -e ".[docs]"
      - uv run mdtoc README.md

  secrets:
    desc: Create template .SECRETS file if one does not already exist.
    cmds:
      - cp ./config/.SECRETS.example.env ./.SECRETS.env
    status:
      - "test -f .SECRETS.env"

  autocomplete:
    desc: Setup task autocomplete (zsh)
    cmds:
      - sudo curl https://raw.githubusercontent.com/go-task/task/main/completion/zsh/_task \
        -o /usr/local/share/zsh/site-functions/_task
    status:
      - "test -f /usr/local/share/zsh/site-functions/_task"

  install:
    desc: Install everything required to run most scripts.
    cmds:
      - |
        uv sync

  run:
    desc: Run the app
    cmds:
      - |
        uv run -m metagit_detect.example

  lint:
    desc: Run python linting
    cmds:
      - |
        uv run ruff check .
        uv run ruff format --check .

  lint:fix:
    desc: Run python linting and fix issues
    cmds:
      - |
        uv run ruff check --fix .

  format:
    desc: Run python formatting
    cmds:
      - |
        uv run ruff format .
  
  start:mcp:
    desc: Start MCP servers
    cmds:
      - |
        docker run -i --rm -e GITHUB_PERSONAL_ACCESS_TOKEN=${GITHUB_PERSONAL_ACCESS_TOKEN} -e GITHUB_READ_ONLY=1 -e GITHUB_DYNAMIC_TOOLSETS=1 ghcr.io/github/github-mcp-server
  
  fake:workspace:
    desc: Create a test workspace folder structure with 10 projects
    cmds:
      - |
        for i in {1..10}; do
          echo "Creating workspace $i"
          mkdir -p "./.metagit/default/project-$i"
        done

  docs:
    desc: Build the docs
    cmds:
      - task: generate:schema
      - |
        uv pip install -e ".[docs]"
        #gemini -y -p 'You are an expert devops and software engineer. Explore this project for its use of secrets and variables paying special attention to only those affecting the runtime behavior of the code and the cicd workflows used to release the projects artifacts. Create a report in ./docs/secrets.analysis.md on each of the secrets found and were they are sourced from. Using this data create a yaml file at ./docs/secrets.definitions.yml that represents the secrets, their provenance, and where they should be placed.'
        #gemini -y -p 'Perform a comprehensive analysis of the files in this project. Update the yaml file named .metagit.example.yml in the root of this project in a manner that adheres to the jsonschema file located at ./schemas/metagit_config.schema.json. Ensure you skip files matching the patterns in .gitignore. Do your best to update even optional elements of this schema. Pay special attention to dependencies found in Dockerfiles, CICD files, and mcp definitions. When complete also create ./docs/app.logic.md with additional documentation on the project components that includes a mermaid diagram of how they interact.'
        uv run mkdocs build

  test:
    desc: Run the tests
    cmds:
      - |
        uv pip install -e ".[test]"
        uv run pytest --maxfail=1 --disable-warnings -v tests

  qa:prepush:
    desc: Token-optimized local pre-push quality gate
    cmds:
      - |
        zsh ./scripts/prepush-gate.zsh

  qa:prepush:loop:
    desc: Repeat local pre-push gate (use -- rounds=N)
    cmds:
      - |
        zsh ./scripts/prepush-gate.zsh --loop --rounds={{default "3" .CLI_ARGS}}

  skills:sync:
    desc: Sync local skills into .cursor/skills mirrors
    cmds:
      - |
        uv run python - <<'PY'
        from pathlib import Path
        import shutil

        src_root = Path("skills")
        dst_root = Path(".cursor/skills")
        dst_root.mkdir(parents=True, exist_ok=True)

        source_skill_dirs = [p for p in src_root.iterdir() if p.is_dir()]
        for src_dir in source_skill_dirs:
            dst_dir = dst_root / src_dir.name
            if dst_dir.exists():
                shutil.rmtree(dst_dir)
            shutil.copytree(src_dir, dst_dir)
            print(f"synced {src_dir} -> {dst_dir}")
        PY

  typecheck:
    desc: Run the type checker
    cmds:
      - |
        uv run mypy metagit/core

  secret:search:
    desc: Find secrets in the codebase and in git history
    cmds:
      - |
        gitleaks detect --source ./ -f json -r secret_results.json
  
  build:
    desc: Build the project
    cmds:
      - |
        uv build

  clean:
    desc: Clean the project
    cmds:
      - |
        find . -type d -name "__pycache__" -exec rm -rf {} +
        find . -type d -name "*.egg-info" -exec rm -rf {} +
        find . -type f -name ".DS_Store" -delete
        find . -type f -name "*.pyc" -delete
        find . -type f -name "*.pyo" -delete
        rm -rf .mypy_cache
        rm -rf .pytest_cache
        rm -rf .ruff_cache
        rm -rf dist
        rm -rf .coverage
        rm -rf .coverage.*
        rm -rf .coverage.xml
        rm -rf .DS_Store
        rm -rf .pytest_cache
        rm -rf .venv
        rm -rf src/metagit/_version.py


  deep:clean:
    desc: Clean the project deeply. Includes uv cache removal.
    cmds:
      - uv cache clean
      - task: clean

  generate:schema:
    desc: Generate the schema for the config and appconfig files
    cmds:
      - |
        uv pip install -e .
        uv run metagit config schema --output-path ./schemas/metagit_config.schema.json
        uv run metagit appconfig schema --output-path ./schemas/metagit_appconfig.schema.json
        mkdir -p docs/reference
        uv run metagit config example --output docs/reference/metagit-config.full-example.yml

  repomix:
    desc: Run the repo mixin
    cmds:
      - |
        repomix --style=markdown --output=docs/llm.txt --compress
        repomix --style=markdown --output=docs/llm-detailed.txt

  update:github:actions:
    desc: Update GitHub Actions in workflows
    cmds:
      - npx actions-up -y

  skills:validate:
    desc: Validate every SKILL.md under skills/ (frontmatter, naming, required fields)
    cmds:
      - |
        uv pip install -e ".[dev]"
        uv run python scripts/validate_skills.py

  web:install:
    desc: Install web UI dependencies
    dir: web
    cmds:
      - npm ci || npm install

  web:build:
    desc: Build web UI into package data (not part of qa:prepush — run explicitly when changing web/)
    deps: [web:install]
    dir: web
    cmds:
      - npm run build

  web:dev:
    desc: Run Vite dev server
    dir: web
    cmds:
      - npm run dev

  security:deps:
    desc: Frozen sync all extras and audit dependencies (pip-audit)
    cmds:
      - |
        uv sync --frozen --all-extras
        uv run pip-audit

  security:bandit:
    desc: Bandit SAST on src/
    cmds:
      - |
        uv run bandit -r src -ll

  security:gate:
    desc: Fast security gate without lockfile sync (pip-audit + bandit)
    cmds:
      - |
        uv run pip-audit
        uv run bandit -r src -ll

  security:scan:
    desc: Full Python security scans (sync + pip-audit + bandit)
    cmds:
      - task: security:deps
      - task: security:bandit
````

## File: .mex/ROUTER.md
````markdown
---
name: router
description: Session bootstrap and navigation hub. Read at the start of every session before any task. Contains project state, routing table, and behavioural contract.
edges:
  - target: context/architecture.md
    condition: when working on system design, integrations, or understanding how components connect
  - target: context/stack.md
    condition: when working with specific technologies, libraries, or making tech decisions
  - target: context/conventions.md
    condition: when writing new code, reviewing code, or unsure about project patterns
  - target: context/decisions.md
    condition: when making architectural choices or understanding why something is built a certain way
  - target: context/setup.md
    condition: when setting up the dev environment or running the project for the first time
  - target: context/mcp-runtime.md
    condition: when implementing MCP runtime, tool schemas, resource handlers, or protocol behavior
  - target: patterns/INDEX.md
    condition: when starting a task — check the pattern index for a matching pattern file
last_updated: 2026-05-20
---

# Session Bootstrap

If you haven't already read `AGENTS.md`, read it now — it contains the project identity, non-negotiables, and commands.

Then read this file fully before doing anything else in this session.

## Current Project State
**Working:**
- Core CLI command surface (`config`, `detect`, `project`, `record`, `workspace`, `mcp`, `search` / `find`, `api serve` for local JSON v1 search + **v2 catalog CRUD**, `project repo prune` for sync-folder cleanup) with shared app config + logger bootstrapping.
- **Workspace catalog** (`WorkspaceCatalogService`): list/add/remove projects and repos in `.metagit.yml` via CLI (`--json`), MCP (`metagit_workspace_*` catalog tools), and HTTP `/v2/*`.
- **`metagit prompt`**: scoped prompt emission (`workspace` / `project` / `repo`) for agents — manifest instructions plus operational templates (session-start, catalog-edit, sync-safe, **repo-enrich**, etc.). Bundled **`metagit-cli`** skill documents CLI-only shortcuts (all prompt kinds; no MCP/API).
- **`agent_mode`** / **`METAGIT_AGENT_MODE`**: disables interactive CLI (fuzzy finder, prompts, editor); `metagit appconfig show --format json` exposes full config including `workspace.dedupe` (default **disabled**).
- **Workspace layout** (`WorkspaceLayoutService`): rename/move projects and repos (manifest + sync folders, dedupe-aware, session migration); CLI, MCP, HTTP v2 — see `docs/reference/workspace-layout-api.md`.
- `.metagit.yml` manager/model pipeline for load/create/save/validate operations.
- MCP runtime with state-aware gating, tool/resource handlers (search, **semantic search**, sync, cross-project dependencies, project context, snapshots, health check with branch-age staleness, file discover, template apply), resources for health/context, protocol-framed stdio loop, and runtime tests.
- Workspace index/search/upstream hint services, `ManagedRepoSearchService` for managed-only repo matching, local read-only HTTP routes under `metagit.core.api`, and guarded repo inspect/sync flows.
- Skill scaffold + local wrapper scripts in `skills/*/scripts` for token-efficient agent workflows, including `metagit-projects` for OpenClaw/Hermes workspace project lifecycle (check-before-create, register in `.metagit.yml`).
- `docs/skills.md` documents global install, `metagit skills install`, and bundled skill overview.
- Runtime packaging compatibility path for version lookup and `python -m metagit` entrypoint behavior in minimal Python environments.
- Docs build path resolves CLI imports correctly in CI by including interactive prompt runtime dependency.
- A semantic-release workflow now computes and pushes tags from conventional commits on `main`, and tag pushes drive PyPI/TestPyPI publish workflows.
- **`task qa:prepush`** (via `scripts/prepush-gate.py` / `prepush-gate.zsh`) is mandatory in the behavioural contract whenever a session modifies tracked files — not optional “session closeout only.”
- Provider source sync is available via `metagit project source sync` for GitHub org/user and GitLab group recursive discovery with discover/additive/reconcile modes.
- Fuzzy finder repo selection UX now shows result counters, keeps full scrollable match sets, respects project `.gitignore` entries during filesystem candidate discovery, and provides richer repo metadata in preview.
- New `skills` CLI command (`list`, `show`, `install`) plus `mcp install` now support auto-detected agent targets across project/user scopes, with bundled package skills deployed from `src/metagit/data/skills`.
- Focused `graphify` runs on subtrees produce `graphify-out/` HTML/report artifacts quickly enough to use for local command-surface exploration without analyzing the entire repository.
- **Workspace dedupe:** `workspace.dedupe` in app config (default disabled); optional per-project `workspace.projects[].dedupe.enabled` in `.metagit.yml` overrides the flag. `metagit project sync --hydrate` materializes symlink mounts into full copies.
- **`metagit config example`:** generates `docs/reference/metagit-config.full-example.yml` (via `task generate:schema`) with field-description comments.
- **Hermes orchestrator template:** `hermes-orchestrator` under `src/metagit/data/templates/`, example manifest at `examples/hermes-orchestrator/.metagit.yml`, guide at `docs/hermes-orchestrator-workspace.md`.
- **`metagit init`:** bundled init templates (`application`, `umbrella`, `hermes-orchestrator`) with copier-style `{{ var }}` rendering, `--answers-file`, `--no-prompt`, all `ProjectKind` values via `--minimal`.
- **`metagit web serve` groundwork:** Pydantic request/response models for the local web UI API live in `src/metagit/core/web/models.py` (`ConfigTreeResponse`, sync job shapes, config patch types). Thread-safe in-memory sync job tracking + SSE event buffers live in `src/metagit/core/web/job_store.py` (`SyncJobStore`).
- **`metagit web serve` config HTTP:** `build_web_server` in `src/metagit/core/web/server.py` exposes v3 config tree/patch/validate routes via `ConfigWebHandler` (`metagit` + `appconfig` targets, `SchemaTreeService` mutations). PATCH with `save=true` returns HTTP 422 and skips disk write when validation fails; masked sensitive tokens are preserved on noop set.
- **`metagit web serve` ops HTTP:** `OpsWebHandler` (`src/metagit/core/web/ops_handler.py`) — POST health/prune/sync, GET sync job status, SSE sync events; wired in `build_web_server` with workspace root from appconfig.
- **`metagit web serve` static + full server:** `StaticWebHandler` serves packaged SPA from `src/metagit/data/web/`; `build_web_server` dispatches static, v2 catalog/layout, v3 config/ops; CLI `metagit web serve` (`src/metagit/cli/commands/web.py`).
- **Metagit Web UI scaffold:** Vite + React + TypeScript in `web/` (build output → `src/metagit/data/web/`); typed API client, router shell, Taskfile `web:*` tasks.
- **Metagit Web Config Studio:** schema tree + field editor for `/config/metagit` and `/config/appconfig` (TanStack Query PATCH flow, theme toggle, `enum_options` on schema nodes).
- **Metagit Web:** local `metagit web serve` + packaged SPA (**Config Studio** on `/config/*`, **Workspace Console** on `/workspace`) with `task web:dev` / `task web:build` workflow documented in [`docs/reference/metagit-web.md`](../docs/reference/metagit-web.md).

**Not yet built:**
- **Metagit Web hardened/exposed deployments:** intentional v1 localhost-only framing; authentication and safe non-local binds are future scope.
- Full production-grade MCP lifecycle extras (e.g., richer notifications, broader method surface, advanced capability negotiation details).
- End-to-end enterprise mode features described in README (continuous org-wide code mining).
- Matured sampling execution path with robust timeout/retry/error telemetry across diverse MCP hosts.

**Known issues:**
- Local `black` execution path is unstable in this environment; project lint path currently relies on Ruff workflow.
- Some MCP schema/tool contracts are still evolving and may require downstream client adjustments.
- Pydantic deprecation warnings are present in test output due to existing class-based config usage.

## Routing Table

Load the relevant file based on the current task. Always load `context/architecture.md` first if not already in context this session.

| Task type | Load |
|-----------|------|
| Understanding how the system works | `context/architecture.md` |
| Working with a specific technology | `context/stack.md` |
| Writing or reviewing code | `context/conventions.md` |
| Making a design decision | `context/decisions.md` |
| Setting up or running the project | `context/setup.md` |
| Working on MCP runtime/tools/resources/protocol | `context/mcp-runtime.md` |
| Any specific task | Check `patterns/INDEX.md` for a matching pattern |

## Behavioural Contract

For every task, follow this loop:

1. **CONTEXT** — Load the relevant context file(s) from the routing table above. Check `patterns/INDEX.md` for a matching pattern. If one exists, follow it. Narrate what you load: "Loading architecture context..."
2. **BUILD** — Do the work. If a pattern exists, follow its Steps. If you are about to deviate from an established pattern, say so before writing any code — state the deviation and why.
3. **VERIFY** — Load `context/conventions.md` and run the Verify Checklist item by item. State each item and whether the output passes. Do not summarise — enumerate explicitly.
4. **DEBUG** — If verification fails or something breaks, check `patterns/INDEX.md` for a debug pattern. Follow it. Fix the issue and re-run VERIFY.
5. **GROW** — After completing the task:
   - If no pattern exists for this task type, create one in `patterns/` using the format in `patterns/README.md`. Add it to `patterns/INDEX.md`. Flag it: "Created `patterns/<name>.md` from this session."
   - If a pattern exists but you deviated from it or discovered a new gotcha, update it with what you learned.
   - If any `context/` file is now out of date because of this work, update it surgically — do not rewrite entire files.
   - Update the "Current Project State" section above if the work was significant.
6. **QA GATE (mandatory for any delivered work)** — If you changed or added tracked project files in this conversation, run `task qa:prepush` from the repo root before reporting the task as finished. Fix failures and re-run until green. Also run `task skills:sync generate:schema` when bundled skills/schemas need to stay mirrored (see conventions). Omit the QA gate only for strictly read‑only exploration (no file writes) or when the user explicitly waived it in this thread. Document any intentional blockers plainly.

## Commit Message Semantics
- Use `fix:` by default (patch-level intent).
- Use `feat:` only for additive backward-compatible behavior.
- Use breaking-change markers (`type(scope)!:` or `BREAKING CHANGE:`) only when intentionally breaking schema/config compatibility (for example `.metagit.yml` or app configuration schema changes).
````
