Monorepo management for opam overlays

Monopam CLI Improvements Plan#

Design Decisions (Clarified)#

  1. Verse remotes: Auto-add on monopam sync, remove outdated ones for all verse members
  2. Add/remove commands: Removed from CLI - use agent skills instead
  3. Doctor output: Structured JSON with per-repo recommendations, rendered to text by CLI with --json option
  4. Verse remote URL: Point to src/ checkout (individual repo)
  5. Opam sync direction: Local metadata always trumps opam-repo metadata
  6. Claude usage: Always use Claude (via ocaml-claude library) for doctor command

Current CLI Commands#

After removing add/remove:

  • monopam status - Show sync status and verse fork analysis
  • monopam sync [--remote] [--skip-push] [--skip-pull] [package] - Primary sync command
  • monopam changes - Generate changelogs with Claude
  • monopam verse init - Initialize workspace
  • monopam verse members - List registry members
  • monopam verse pull - Pull verse member repos
  • monopam verse sync - Sync verse workspace

Implementation Plan#

Phase 1: Verse Remotes (Auto-managed)#

Changes to monopam sync#

Add verse remote management to the sync process:

  1. During fetch phase: For each verse member in registry:

    • Scan their monorepo for subtrees
    • For matching subtrees in our src/:
      • Add git remote named verse/<handle> pointing to their src/ checkout
      • If remote exists but URL changed, update it
      • Fetch from the remote
  2. Cleanup: Remove verse remotes for:

    • Members no longer in registry
    • Repos we no longer have

Data Flow#

sync starts
  ├── push phase (existing)
  ├── fetch phase (existing)
  │   └── NEW: for each verse member with matching repos:
  │       └── ensure git remote in src/<repo> → verse/<member>/src/<repo>
  │       └── git fetch verse/<handle>
  ├── merge phase (existing)
  ├── subtree phase (existing)
  ├── finalize phase (existing)
  └── remote phase (existing)

Implementation in monopam.ml#

(* New function to manage verse remotes for a repo *)
let ensure_verse_remotes ~proc ~fs ~config ~verse_config pkg =
  let checkouts_root = Config.Paths.checkouts config in
  let checkout_dir = Package.checkout_dir ~checkouts_root pkg in
  let repo_name = Package.repo_name pkg in

  (* Get all verse members who have this repo *)
  let verse_subtrees = Verse.get_verse_subtrees ~proc ~fs ~config:verse_config () in
  let members_with_repo =
    Hashtbl.find_opt verse_subtrees repo_name
    |> Option.value ~default:[]
  in

  (* For each member, ensure remote exists *)
  List.iter (fun (handle, verse_mono_path) ->
    let remote_name = "verse/" ^ handle in
    let verse_src = Fpath.(verse_mono_path / ".." / "src" / repo_name) in
    (* Add or update remote *)
    Git.ensure_remote ~proc ~fs ~name:remote_name ~url:verse_src checkout_dir
  ) members_with_repo

Phase 2: Opam Metadata Sync#

New Command: monopam opam sync#

Synchronize .opam files from monorepo subtrees to opam-repo.

monopam opam sync           # Sync all packages
monopam opam sync eio       # Sync specific package

Behavior:

  • For each package in monorepo:
    • Read .opam file from subtree
    • Compare with opam-repo version
    • If different, copy monorepo → opam-repo (local always wins)
    • Stage changes in opam-repo

Integration with monopam sync#

Add --opam flag:

monopam sync --opam --remote  # Full sync including opam metadata

Or make it part of finalize phase (always sync opam).

Implementation#

let sync_opam_files ~proc ~fs ~config pkgs =
  let monorepo = Config.Paths.monorepo config in
  let opam_repo = Config.Paths.opam_repo config in

  List.iter (fun pkg ->
    let name = Package.name pkg in
    let subtree_opam = Fpath.(monorepo / Package.subtree_prefix pkg / (name ^ ".opam")) in
    let repo_opam = Fpath.(opam_repo / "packages" / name / (name ^ ".dev") / "opam") in

    (* Read both files *)
    let subtree_content = read_file_opt ~fs subtree_opam in
    let repo_content = read_file_opt ~fs repo_opam in

    match subtree_content with
    | None -> () (* No opam file in subtree, skip *)
    | Some content when Some content <> repo_content ->
        (* Copy to opam-repo *)
        write_file ~fs repo_opam content;
        Git.add ~proc ~fs opam_repo [Fpath.to_string repo_opam]
    | _ -> () (* Already in sync *)
  ) pkgs

Phase 3: Doctor Command#

New Command: monopam doctor#

Claude-powered workspace health analysis.

monopam doctor              # Full analysis, text output
monopam doctor --json       # JSON output for tooling
monopam doctor eio          # Analyze specific repo

Output Structure (JSON)#

{
  "timestamp": "2026-01-21T12:00:00Z",
  "workspace": "/home/user/tangled",
  "summary": {
    "repos_total": 39,
    "repos_need_sync": 2,
    "repos_behind_upstream": 3,
    "verse_divergences": 5
  },
  "repos": [
    {
      "name": "eio",
      "local_sync": "in_sync",
      "remote_sync": { "ahead": 0, "behind": 0 },
      "verse_analysis": [
        {
          "handle": "alice.bsky.social",
          "their_commits": [
            {
              "hash": "abc1234",
              "subject": "Add Eio.Path.symlink support",
              "category": "feature",
              "priority": "medium",
              "recommendation": "review-first",
              "conflict_risk": "low",
              "summary": "Adds symlink creation support to Eio.Path module"
            },
            {
              "hash": "def5678",
              "subject": "Fix race condition in Eio.Fiber.fork",
              "category": "bug-fix",
              "priority": "high",
              "recommendation": "merge-now",
              "conflict_risk": "none",
              "summary": "Fixes potential deadlock when forking fibers under load"
            }
          ],
          "suggested_action": "git fetch verse/alice.bsky.social && git cherry-pick def5678"
        }
      ]
    }
  ],
  "recommendations": [
    {
      "priority": "high",
      "action": "Merge alice's bug fix for eio (def5678)",
      "command": "cd src/eio && git cherry-pick def5678"
    },
    {
      "priority": "medium",
      "action": "Run monopam sync to resolve local sync issues",
      "command": "monopam sync"
    }
  ],
  "warnings": [
    "opam-repo has uncommitted changes",
    "verse/alice.bsky.social/ is 10 commits behind"
  ]
}

Text Rendering#

=== Monopam Doctor Report ===
Generated: 2026-01-21 12:00:00

Summary:
  39 repos tracked
  2 need local sync
  3 behind upstream
  5 verse divergences

─────────────────────────────────────────

eio (diverged from alice.bsky.social)

  Their commits (2):

  [HIGH] def5678 Fix race condition in Eio.Fiber.fork
         Category: bug-fix | Risk: none | Action: merge-now
         → Fixes potential deadlock when forking fibers under load

  [MED]  abc1234 Add Eio.Path.symlink support
         Category: feature | Risk: low | Action: review-first
         → Adds symlink creation support to Eio.Path module

  Suggested: git fetch verse/alice.bsky.social && git cherry-pick def5678

─────────────────────────────────────────

Recommendations:
  1. [HIGH] Merge alice's bug fix for eio (def5678)
     $ cd src/eio && git cherry-pick def5678

  2. [MED] Run monopam sync to resolve local sync issues
     $ monopam sync

Warnings:
  • opam-repo has uncommitted changes
  • verse/alice.bsky.social/ is 10 commits behind

Claude Integration#

Use the existing claude OCaml library for analysis:

module Doctor = struct
  type commit_analysis = {
    hash: string;
    subject: string;
    category: [`Security_fix | `Bug_fix | `Feature | `Refactor | `Docs | `Test];
    priority: [`Critical | `High | `Medium | `Low];
    recommendation: [`Merge_now | `Review_first | `Skip | `Needs_discussion];
    conflict_risk: [`None | `Low | `Medium | `High];
    summary: string;
  }

  let analyze_commits ~commits ~our_branch ~their_handle =
    let prompt = Format.asprintf {|
You are analyzing git commits from a collaborator's repository.

Repository context:
- Our branch: %s
- Their handle: %s

Commits to analyze:
%s

For each commit, provide JSON with:
- category: security-fix, bug-fix, feature, refactor, docs, test
- priority: critical, high, medium, low
- recommendation: merge-now, review-first, skip, needs-discussion
- conflict_risk: none, low, medium, high
- summary: one-line description of what the commit does

Respond with a JSON array of analyses.
|} our_branch their_handle (format_commits commits) in

    Claude.chat ~model:"claude-sonnet-4-20250514" ~messages:[
      Claude.Message.user prompt
    ] ()
    |> parse_analysis_response
end

Implementation Order#

  1. Phase 1a: Add ensure_verse_remotes function to monopam.ml
  2. Phase 1b: Integrate verse remote management into sync fetch phase
  3. Phase 2a: Add sync_opam_files function
  4. Phase 2b: Add monopam opam sync command (or integrate into sync)
  5. Phase 3a: Create doctor.ml module with types and Claude integration
  6. Phase 3b: Add monopam doctor command with JSON/text output

Files to Modify/Create#

Modify#

  • monopam/lib/monopam.ml - Add verse remote management, opam sync
  • monopam/lib/monopam.mli - Export new functions
  • monopam/bin/main.ml - Add doctor command, opam subcommand

Create#

  • monopam/lib/doctor.ml - Doctor analysis logic
  • monopam/lib/doctor.mli - Doctor interface
  • monopam/lib/opam_sync.ml - Opam metadata sync logic (optional, could be in monopam.ml)