(** cn_lib.ml — Core CN library (pure, no I/O)
    
    DESIGN: This is the "lib" in Unix convention — the core library
    that everything else depends on. Like libc for C programs.
    
    Layering (deliberate):
      cn.ml     → CLI wiring, calls cn_io and cn_lib
      cn_io.ml  → Protocol I/O (side effects)
      cn_lib.ml → Types, parsing, help (THIS FILE - pure)
      git.ml    → Raw git operations
    
    Why pure?
    - Fully testable with ppx_expect (57+ tests)
    - No mocking needed — pure functions, deterministic
    - Can run tests in CI without git repos
    
    What lives here:
    - Command types (type command = Help | Sync | ...)
    - Parsing (string list → command option)
    - Help text
    - Utility functions (frontmatter parsing, etc.)
    
    What does NOT live here:
    - File I/O (→ cn_io.ml)
    - Git operations (→ git.ml via cn_io.ml)
    - CLI wiring (→ cn.ml)
    
    Follows FUNCTIONAL.md:
    - Pattern matching over conditionals
    - Pipelines over sequences
    - Total functions (Option, not exceptions)
    - Semantic types (Inbox.cmd, Gtd.cmd, etc.) *)

(* Re-export inbox types for convenience *)
type triage = Inbox_lib.triage =
  | Delete of Inbox_lib.reason
  | Defer of Inbox_lib.reason
  | Delegate of Inbox_lib.actor
  | Do of Inbox_lib.action

(* === String Helpers (total functions) === *)

let starts_with ~prefix s =
  let plen = String.length prefix in
  String.length s >= plen && String.sub s 0 plen = prefix

let strip_prefix ~prefix s =
  let plen = String.length prefix in
  if starts_with ~prefix s 
  then Some (String.sub s plen (String.length s - plen))
  else None

let ends_with ~suffix s =
  let slen = String.length suffix in
  let len = String.length s in
  len >= slen && String.sub s (len - slen) slen = suffix

let non_empty s = String.trim s <> ""

(* === CLI Commands (exhaustive variants) === *)

(* Subcommand modules - clean names, module-scoped *)
module Inbox = struct
  type cmd = Check | Process | Flush
end

module Outbox = struct
  type cmd = Check | Flush
end

module Peer = struct
  type cmd = List | Add of string * string | Remove of string | Sync
end

module Queue = struct
  type cmd = List | Clear
end

module Mca = struct
  type cmd = List | Add of string
end

module Agent = struct
  type mode = Cron | Process | Daemon | Stdio
end

module Deps = struct
  type cmd = List | Restore | Doctor | Add of string | Remove of string
           | Update of string option | Vendor
end

module Build = struct
  type cmd = Packages | Check | Clean
end

module Logs = struct
  type cmd = Show of string list  (* remaining args passed through *)
end

module Gtd = struct
  type cmd =
    | Delete of string
    | Defer of string * string option
    | Delegate of string * string
    | Do of string
    | Done of string
end

(* === Agent Output — the ONLY way agent can respond ===
   
   GTD protocol for handling input:
   - Do: complete with an op for cn to execute
   - Defer: postpone with reason
   - Delegate: forward to peer
   - Delete: discard with reason
   
   cn auto-notifies input creator of action taken. *)

module Out = struct
  (* What cn executes when agent does Do *)
  type op =
    | Reply of { message: string }
    | Send of { to_: string; message: string }
    | Surface of { desc: string }
    | Noop of { reason: string }  (* no action, but creator notified *)
    | Commit of { artifact: string }
  
  (* GTD protocol — ONLY 4 OPTIONS *)
  type gtd =
    | Do of op
    | Defer of { reason: string }
    | Delegate of { to_: string }
    | Delete of { reason: string }
end

(* === Agent Output Operations ===
   
   These are operations an agent can write to output.md.
   cn parses output.md and executes matching operations. *)

type agent_op =
  | Ack of string                      (* Acknowledge input, no further action *)
  | Done of string                     (* Mark input/thread complete *)
  | Fail of string * string            (* Failed to process: id, reason *)
  | Reply of string * string           (* Reply to thread: id, message *)
  | Send of string * string * string option  (* Send to peer: peer, message, body *)
  | Delegate of string * string        (* Forward to peer: id, peer *)
  | Defer of string * string option    (* Postpone: id, until *)
  | Delete of string                   (* Discard: id *)
  | Surface of string                  (* Surface MCA for community *)

let string_of_agent_op = function
  | Ack id -> "ack:" ^ id
  | Done id -> "done:" ^ id
  | Fail (id, reason) -> "fail:" ^ id ^ " reason:" ^ reason
  | Reply (id, _) -> "reply:" ^ id
  | Send (peer, _, _) -> "send:" ^ peer
  | Delegate (id, peer) -> "delegate:" ^ id ^ " to:" ^ peer
  | Defer (id, None) -> "defer:" ^ id
  | Defer (id, Some until) -> "defer:" ^ id ^ " until:" ^ until
  | Delete id -> "delete:" ^ id
  | Surface desc -> "surface:" ^ desc

(* Split on first '|' — bounds-checked, no partial String.sub *)
let split_pipe s =
  match String.index_opt s '|' with
  | None -> (s, None)
  | Some i ->
      (String.sub s 0 i,
       Some (String.sub s (i + 1) (String.length s - i - 1)))

(* Parse agent_op from frontmatter key-value pairs *)
let parse_agent_op key value =
  match key with
  | "ack" -> Some (Ack value)
  | "done" -> Some (Done value)
  | "fail" ->
      let (id, rest) = split_pipe value in
      Some (Fail (id, Option.value rest ~default:"unspecified"))
  | "reply" ->
      let (id, rest) = split_pipe value in
      rest |> Option.map (fun msg -> Reply (id, msg))
  | "send" ->
      (match split_pipe value with
       | (_, None) -> None
       | (peer, Some rest) ->
           let (message, body) = split_pipe rest in
           Some (Send (peer, message, body)))
  | "delegate" ->
      let (id, rest) = split_pipe value in
      rest |> Option.map (fun peer -> Delegate (id, peer))
  | "defer" ->
      let (id, until) = split_pipe value in
      Some (Defer (id, until))
  | "delete" -> Some (Delete value)
  | "surface" -> Some (Surface value)
  | "mca" -> Some (Surface value)  (* alias *)
  | _ -> None

(* Extract all agent_ops from frontmatter *)
let extract_ops frontmatter =
  frontmatter |> List.filter_map (fun (k, v) -> parse_agent_op k v)

type command =
  | Help
  | Version
  | Status
  | Doctor
  | Init of string option
  | Inbox of Inbox.cmd
  | Outbox of Outbox.cmd
  | Peer of Peer.cmd
  | Queue of Queue.cmd
  | Mca of Mca.cmd
  | Sync
  | Next
  | Read of string
  | Reply of string * string
  | Send of string * string
  | Gtd of Gtd.cmd
  | Out of Out.gtd  (* Agent output — ONLY way to respond *)
  | Commit of string option
  | Push
  (* Save removed in v3.37.0 (#184) — now a package command at
     src/agent/commands/save/cn-save, dispatched via Cn_command. *)
  | Agent of Agent.mode  (* replaces Inbound — Step 10 *)
  | Update
  | Adhoc of string  (* Create adhoc thread *)
  (* Daily / Weekly removed in v3.37.0 (#184) — now package commands at
     src/agent/commands/{daily,weekly}/cn-<id>, dispatched via
     Cn_command.find → dispatch. *)
  | Setup            (* Hub setup: materialize assets + deps *)
  | Deps of Deps.cmd (* Dependency management *)
  | Build of Build.cmd (* Package assembly from src/agent/ *)
  | Logs of Logs.cmd   (* Unified log reader *)

(* Exhaustive - compiler warns on missing cases *)
let string_of_command = function
  | Help -> "help"
  | Version -> "version"
  | Status -> "status"
  | Doctor -> "doctor"
  | Init None -> "init"
  | Init (Some n) -> "init " ^ n
  | Inbox Inbox.Check -> "inbox check"
  | Inbox Inbox.Process -> "inbox process"
  | Inbox Inbox.Flush -> "inbox flush"
  | Outbox Outbox.Check -> "outbox check"
  | Outbox Outbox.Flush -> "outbox flush"
  | Peer Peer.List -> "peer list"
  | Peer (Peer.Add (n, _)) -> "peer add " ^ n
  | Peer (Peer.Remove n) -> "peer remove " ^ n
  | Peer Peer.Sync -> "peer sync"
  | Queue Queue.List -> "queue list"
  | Queue Queue.Clear -> "queue clear"
  | Mca Mca.List -> "mca list"
  | Mca (Mca.Add d) -> "mca add " ^ d
  | Sync -> "sync"
  | Next -> "next"
  | Read t -> "read " ^ t
  | Reply (t, _) -> "reply " ^ t
  | Send (p, _) -> "send " ^ p
  | Gtd (Gtd.Delete t) -> "delete " ^ t
  | Gtd (Gtd.Defer (t, _)) -> "defer " ^ t
  | Gtd (Gtd.Delegate (t, p)) -> "delegate " ^ t ^ " " ^ p
  | Gtd (Gtd.Do t) -> "do " ^ t
  | Gtd (Gtd.Done t) -> "done " ^ t
  | Out (Out.Do (Out.Reply { message = _ })) -> "out do reply"
  | Out (Out.Do (Out.Send { to_; message = _ })) -> "out do send " ^ to_
  | Out (Out.Do (Out.Surface { desc = _ })) -> "out do surface"
  | Out (Out.Do (Out.Noop { reason = _ })) -> "out do noop"
  | Out (Out.Do (Out.Commit { artifact })) -> "out do commit " ^ artifact
  | Out (Out.Defer { reason = _ }) -> "out defer"
  | Out (Out.Delegate { to_ }) -> "out delegate " ^ to_
  | Out (Out.Delete { reason = _ }) -> "out delete"
  | Commit None -> "commit"
  | Commit (Some m) -> "commit " ^ m
  | Push -> "push"
  | Agent Agent.Cron -> "agent"
  | Agent Agent.Process -> "agent --process"
  | Agent Agent.Daemon -> "agent --daemon"
  | Agent Agent.Stdio -> "agent --stdio"
  | Update -> "update"
  | Adhoc t -> "adhoc " ^ t
  | Setup -> "setup"
  | Deps Deps.List -> "deps list"
  | Deps Deps.Restore -> "deps restore"
  | Deps Deps.Doctor -> "deps doctor"
  | Deps (Deps.Add pkg) -> "deps add " ^ pkg
  | Deps (Deps.Remove pkg) -> "deps remove " ^ pkg
  | Deps (Deps.Update None) -> "deps update"
  | Deps (Deps.Update (Some pkg)) -> "deps update " ^ pkg
  | Deps Deps.Vendor -> "deps vendor"
  | Build Build.Packages -> "build"
  | Build Build.Check -> "build --check"
  | Build Build.Clean -> "build clean"
  | Logs (Logs.Show _) -> "logs"

(* === Alias Expansion === *)

let expand_alias = function
  | "i" -> "inbox"
  | "o" -> "outbox"
  | "s" -> "status"
  | "d" -> "doctor"
  | "p" -> "peer"
  | "l" -> "logs"
  | s -> s  (* identity for non-aliases *)

(* === Command Parsing (Option for totality) === *)

let parse_inbox_cmd = function
  | [] | ["check"] -> Some Inbox.Check
  | ["process"] -> Some Inbox.Process
  | ["flush"] -> Some Inbox.Flush
  | _ -> None

let parse_outbox_cmd = function
  | [] | ["check"] -> Some Outbox.Check
  | ["flush"] -> Some Outbox.Flush
  | _ -> None

let parse_peer_cmd = function
  | [] | ["list"] -> Some Peer.List
  | ["add"; name; url] -> Some (Peer.Add (name, url))
  | ["remove"; name] -> Some (Peer.Remove name)
  | ["sync"] -> Some Peer.Sync
  | _ -> None

let parse_queue_cmd = function
  | [] | ["list"] -> Some Queue.List
  | ["clear"] -> Some Queue.Clear
  | _ -> None

let parse_mca_cmd = function
  | [] | ["list"] -> Some Mca.List
  | "add" :: rest when rest <> [] -> Some (Mca.Add (String.concat " " rest))
  | _ -> None

let parse_deps_cmd = function
  | [] | ["list"] -> Some Deps.List
  | ["restore"] -> Some Deps.Restore
  | ["doctor"] -> Some Deps.Doctor
  | ["add"; pkg] -> Some (Deps.Add pkg)
  | ["remove"; pkg] -> Some (Deps.Remove pkg)
  | ["update"] -> Some (Deps.Update None)
  | ["update"; pkg] -> Some (Deps.Update (Some pkg))
  | ["vendor"] -> Some Deps.Vendor
  | _ -> None

let parse_build_cmd = function
  | [] -> Some Build.Packages
  | ["--check"] -> Some Build.Check
  | ["clean"] -> Some Build.Clean
  | _ -> None

let parse_agent_mode = function
  | [] -> Some Agent.Cron
  | ["--process"] -> Some Agent.Process
  | ["--daemon"] -> Some Agent.Daemon
  | ["--stdio"] -> Some Agent.Stdio
  | _ -> None

let parse_gtd_cmd = function
  | ["delete"; t] -> Some (Gtd.Delete t)
  | "defer" :: t :: rest -> Some (Gtd.Defer (t, List.nth_opt rest 0))
  | ["delegate"; t; p] -> Some (Gtd.Delegate (t, p))
  | ["do"; t] -> Some (Gtd.Do t)
  | ["done"; t] -> Some (Gtd.Done t)
  | _ -> None

(* Parse --key value pairs from args *)
let rec parse_flags_from_args acc = function
  | [] -> acc
  | "--message" :: v :: rest -> parse_flags_from_args (("message", v) :: acc) rest
  | "--to" :: v :: rest -> parse_flags_from_args (("to", v) :: acc) rest
  | "--reason" :: v :: rest -> parse_flags_from_args (("reason", v) :: acc) rest
  | "--desc" :: v :: rest -> parse_flags_from_args (("desc", v) :: acc) rest
  | "--artifact" :: v :: rest -> parse_flags_from_args (("artifact", v) :: acc) rest
  | "--id" :: v :: rest -> parse_flags_from_args (("id", v) :: acc) rest
  | _ :: rest -> parse_flags_from_args acc rest

let get_flag key flags = List.find_map (fun (k, v) -> if k = key then Some v else None) flags

(* Parse cn out commands *)
let parse_out_cmd args =
  let flags = parse_flags_from_args [] args in
  match args with
  | "do" :: "reply" :: _ ->
      get_flag "message" flags |> Option.map (fun m -> Out.Do (Out.Reply { message = m }))
  | "do" :: "send" :: _ ->
      (match get_flag "to" flags, get_flag "message" flags with
       | Some t, Some m -> Some (Out.Do (Out.Send { to_ = t; message = m }))
       | _ -> None)
  | "do" :: "surface" :: _ ->
      get_flag "desc" flags |> Option.map (fun d -> Out.Do (Out.Surface { desc = d }))
  | "do" :: "noop" :: _ ->
      (match get_flag "reason" flags with
       | None -> None
       | Some r ->
           (* Reason must be coherent - reject trivial *)
           let trivial = ["ack"; "ok"; "done"; "yes"; "no"; "acknowledged"; "noted"] in
           let r_lower = String.lowercase_ascii r in
           if List.mem r_lower trivial || String.length r < 10 then None
           else Some (Out.Do (Out.Noop { reason = r })))
  | "do" :: "commit" :: _ ->
      get_flag "artifact" flags |> Option.map (fun a -> Out.Do (Out.Commit { artifact = a }))
  | "defer" :: _ ->
      get_flag "reason" flags |> Option.map (fun r -> Out.Defer { reason = r })
  | "delegate" :: _ ->
      get_flag "to" flags |> Option.map (fun t -> Out.Delegate { to_ = t })
  | "delete" :: _ ->
      get_flag "reason" flags |> Option.map (fun r -> Out.Delete { reason = r })
  | _ -> None

let join_rest rest = match rest with [] -> None | _ -> Some (String.concat " " rest)

let rec parse_command = function
  | [] | ["help"] | ["-h"] | ["--help"] -> Some Help
  | ["--version"] | ["-V"] -> Some Version
  | ["status"] -> Some Status
  | ["doctor"] -> Some Doctor
  | ["init"] -> Some (Init None)
  | ["init"; n] -> Some (Init (Some n))
  | "inbox" :: rest -> parse_inbox_cmd rest |> Option.map (fun c -> Inbox c)
  | "outbox" :: rest -> parse_outbox_cmd rest |> Option.map (fun c -> Outbox c)
  | "peer" :: rest -> parse_peer_cmd rest |> Option.map (fun c -> Peer c)
  | "queue" :: rest -> parse_queue_cmd rest |> Option.map (fun c -> Queue c)
  | "deps" :: rest -> parse_deps_cmd rest |> Option.map (fun c -> Deps c)
  | "build" :: rest -> parse_build_cmd rest |> Option.map (fun c -> Build c)
  | "logs" :: rest -> Some (Logs (Logs.Show rest))
  | "mca" :: rest -> parse_mca_cmd rest |> Option.map (fun c -> Mca c)
  | ["sync"] -> Some Sync
  | ["next"] -> Some Next
  | "agent" :: rest -> parse_agent_mode rest |> Option.map (fun m -> Agent m)
  | ["in"] | ["inbound"] | ["process"] -> Some (Agent Agent.Cron)  (* aliases for agent *)
  | ["read"; t] -> Some (Read t)
  | "reply" :: t :: rest -> Some (Reply (t, String.concat " " rest))
  | "send" :: p :: rest -> Some (Send (p, String.concat " " rest))
  | "delete" :: rest -> parse_gtd_cmd ("delete" :: rest) |> Option.map (fun c -> Gtd c)
  | "defer" :: rest -> parse_gtd_cmd ("defer" :: rest) |> Option.map (fun c -> Gtd c)
  | "delegate" :: rest -> parse_gtd_cmd ("delegate" :: rest) |> Option.map (fun c -> Gtd c)
  | "do" :: rest -> parse_gtd_cmd ("do" :: rest) |> Option.map (fun c -> Gtd c)
  | "done" :: rest -> parse_gtd_cmd ("done" :: rest) |> Option.map (fun c -> Gtd c)
  | "commit" :: rest -> Some (Commit (join_rest rest))
  | ["push"] -> Some Push
  (* "save" removed in v3.37.0 (#184) — package command *)
  | ["update"] -> Some Update
  (* release removed — use scripts/release.sh *)
  | ["setup"] -> Some Setup
  | "adhoc" :: rest -> join_rest rest |> Option.map (fun t -> Adhoc t)
  (* "daily" / "weekly" removed in v3.37.0 (#184) — package commands *)
  | "out" :: rest -> parse_out_cmd rest |> Option.map (fun c -> Out c)
  | [alias] ->
      let expanded = expand_alias alias in
      if expanded <> alias then parse_command [expanded] else None
  | _ -> None

(* === Flags (immutable record) === *)

type flags = {
  json: bool;
  quiet: bool;
  verbose: bool;
  dry_run: bool;
}

let default_flags = { json = false; quiet = false; verbose = false; dry_run = false }

(* Tail-recursive flag parsing *)
let parse_flags args =
  let rec go flags remaining = function
    | [] -> (flags, List.rev remaining)
    | "--json" :: rest -> go { flags with json = true } remaining rest
    | "-q" :: rest -> go { flags with quiet = true } remaining rest
    | "--quiet" :: rest -> go { flags with quiet = true } remaining rest
    | "-v" :: rest -> go { flags with verbose = true } remaining rest
    | "--verbose" :: rest -> go { flags with verbose = true } remaining rest
    | "--dry-run" :: rest -> go { flags with dry_run = true } remaining rest
    | arg :: rest -> go flags (arg :: remaining) rest
  in
  go default_flags [] args

(* === Hub Name Derivation === *)

let derive_name hub_path =
  hub_path
  |> String.split_on_char '/'
  |> List.rev
  |> (function base :: _ -> base | [] -> "hub")
  |> fun base -> strip_prefix ~prefix:"cn-" base |> Option.value ~default:base

(* === Frontmatter (pure parsing) === *)

let parse_frontmatter content =
  match String.split_on_char '\n' content with
  | "---" :: rest ->
      let rec collect acc = function
        | "---" :: _ -> List.rev acc
        | line :: rest -> collect (line :: acc) rest
        | [] -> List.rev acc
      in
      collect [] rest
      |> List.filter_map (fun line ->
          match String.index_opt line ':' with
          | None -> None
          | Some i ->
              let key = String.trim (String.sub line 0 i) in
              let value = String.trim (String.sub line (i + 1) (String.length line - i - 1)) in
              if key = "" then None else Some (key, value))
  | _ -> []

(** Extract the markdown body below the frontmatter closing fence.
    Returns None if the body is blank. If there is no frontmatter,
    returns the trimmed content (or None if empty). *)
let extract_body content =
  match String.split_on_char '\n' content with
  | "---" :: rest ->
      let rec skip = function "---" :: r -> r | _ :: r -> skip r | [] -> [] in
      let body = String.concat "\n" (skip rest) |> String.trim in
      if body = "" then None else Some body
  | _ ->
      let trimmed = String.trim content in
      if trimmed = "" then None else Some trimmed

(** Apply body consumption rules: markdown body wins over frontmatter
    notification for reply and send ops. *)
let resolve_payload body (op : agent_op) : agent_op =
  match body, op with
  | Some b, Reply (id, _msg) -> Reply (id, b)
  | Some b, Send (peer, msg, None) -> Send (peer, msg, Some b)
  | _, op -> op

let update_frontmatter content updates =
  let existing = parse_frontmatter content in
  let merged = 
    updates |> List.fold_left (fun acc (k, v) ->
      (k, v) :: List.filter (fun (k', _) -> k' <> k) acc
    ) existing
  in
  let body = 
    match String.split_on_char '\n' content with
    | "---" :: rest ->
        let rec skip = function "---" :: r -> r | _ :: r -> skip r | [] -> [] in
        String.concat "\n" (skip rest)
    | _ -> content
  in
  let fm = merged |> List.map (fun (k, v) -> k ^ ": " ^ v) |> String.concat "\n" in
  "---\n" ^ fm ^ "\n---\n" ^ body

(* === Peer Info (semantic record) === *)

type peer_info = {
  name: string;
  hub: string option;
  clone: string option;
  kind: string option;
}

let empty_peer name = { name; hub = None; clone = None; kind = None }

let parse_peer_field peer line =
  let trimmed = String.trim line in
  match strip_prefix ~prefix:"hub: " trimmed with
  | Some v -> { peer with hub = Some v }
  | None ->
      match strip_prefix ~prefix:"clone: " trimmed with
      | Some v -> { peer with clone = Some v }
      | None ->
          match strip_prefix ~prefix:"kind: " trimmed with
          | Some v -> { peer with kind = Some v }
          | None -> peer

let parse_peers_md content =
  content
  |> String.split_on_char '\n'
  |> List.fold_left (fun (current, peers) line ->
      match strip_prefix ~prefix:"- name: " (String.trim line) with
      | Some name ->
          let peers' = match current with Some p -> p :: peers | None -> peers in
          (Some (empty_peer name), peers')
      | None ->
          let current' = Option.map (fun p -> parse_peer_field p line) current in
          (current', peers)
    ) (None, [])
  |> fun (current, peers) ->
      (match current with Some p -> p :: peers | None -> peers)
      |> List.rev

(* === Cadence (exhaustive variant) === *)

type cadence = 
  | Inbox | Outbox | Daily | Weekly | Monthly 
  | Quarterly | Yearly | Adhoc | Doing | Deferred | Unknown

let cadence_of_string = function
  | "inbox" -> Inbox
  | "outbox" -> Outbox
  | "daily" -> Daily
  | "weekly" -> Weekly
  | "monthly" -> Monthly
  | "quarterly" -> Quarterly
  | "yearly" -> Yearly
  | "adhoc" -> Adhoc
  | "doing" -> Doing
  | "deferred" -> Deferred
  | _ -> Unknown

let string_of_cadence = function
  | Inbox -> "inbox"
  | Outbox -> "outbox"
  | Daily -> "daily"
  | Weekly -> "weekly"
  | Monthly -> "monthly"
  | Quarterly -> "quarterly"
  | Yearly -> "yearly"
  | Adhoc -> "adhoc"
  | Doing -> "doing"
  | Deferred -> "deferred"
  | Unknown -> "unknown"

let cadence_of_path path =
  path
  |> String.split_on_char '/'
  |> List.find_map (fun part ->
      match cadence_of_string part with
      | Unknown -> None
      | c -> Some c)
  |> Option.value ~default:Unknown

(* === Help Text === *)

let help_text = {|cn - Coherent Network agent CLI

Usage: cn <command> [options]

Commands:
  # Agent decisions (output)
  delete <thread>     GTD: discard
  defer <thread>      GTD: postpone
  delegate <t> <peer> GTD: forward
  do <thread>         GTD: claim/start
  done <thread>       GTD: complete → archive
  reply <thread> <msg> Append to thread
  send <peer> <msg>   Message to peer (or self)
  
  # cn operations (orchestrator)
  agent               Run one cycle: dequeue → LLM → execute (alias: in, inbound)
  agent --process     Single-shot: process one queued item
  agent --daemon      Daemon scheduler: continuous loop + optional Telegram
  agent --stdio       Interactive mode (stdin → LLM → stdout)
  sync                Fetch inbound + send outbound
  queue [list|clear]  View or clear the task queue
  mca [list|add <desc>] Surface MCAs for community pickup
  inbox               List inbox threads
  outbox              List outbox threads
  next                Get next inbox item (with cadence)
  read <thread>       Read thread with cadence
  
  # Thread creation
  adhoc <title>       Create adhoc thread
  (daily, weekly      Package commands — see `cn help` external list
   and save are       or the installed cnos.core package for details)
   package commands)

  # Dependencies
  deps [list]         List installed packages
  deps restore        Install from lockfile (deterministic)
                      Downloads each package tarball over HTTPS, verifies
                      SHA-256 against the lockfile, extracts into
                      .cn/vendor/packages/<name>@<version>/, and validates
                      cn.package.json. Already-installed packages are
                      skipped.
  deps doctor         Verify installed assets match lockfile
                      Manually vendored packages with a valid cn.package.json
                      are recognized as installed.
  deps add <pkg>      Add dependency to .cn/deps.json
  deps remove <pkg>   Remove dependency
  deps update [pkg]   Update lockfile (re-resolve within ranges)
  deps vendor         Commit vendor tree for airgapped use
                      Offline-first: skips fetch entirely when every locked
                      package is already present under .cn/vendor/packages/.

  # Build
  build               Assemble packages/ from src/agent/ sources
  build --check       Verify packages/ matches src/agent/ (CI mode)
  build clean         Remove generated content from packages/

  # Observability
  logs                Tail the unified log stream
  logs --since <dur>  Time filter (e.g. 2h, 30m, 1d)
  logs --msg <id>     Trace one message end-to-end
  logs --errors       Errors and warnings only
  logs --json         Raw JSONL for piping
  logs --kind <kind>  Filter by event kind

  # Hub management
  init [name]         Create new hub
  setup               Set up hub: install packages, write deps, restore
  status              Show hub state
  commit [msg]        Stage + commit
  push                Push to origin
  peer                Manage peers
  doctor              Health check
  update              Update cn to latest version

Aliases:
  i = inbox, o = outbox, s = status, d = doctor, l = logs

Flags:
  --help, -h          Show help
  --version, -V       Show version
  --json              Machine-readable output
  --quiet, -q         Minimal output
  --dry-run           Show what would happen

Runtime:
  Oneshot scheduler (cn agent) runs one cycle:
  1. maintain_once: sync, inbox, outbox, update check, review, cleanup
  2. drain_queue: dequeue → pack context → call LLM → execute ops
  All state mutation under atomic lock. Recovery handles crash at any point.
  Daemon scheduler (--daemon) runs continuous loop with periodic maintenance.
  Telegram poll is enabled when TELEGRAM_TOKEN is set.
|}

let version = Cn_version.version
let cnos_commit = Cn_build_info.cnos_commit
let cnos_repo = Cn_repo_info.repo

(* === Version Comparison (pure, semantic) === *)

let version_to_tuple v =
  let v = if String.length v > 0 && v.[0] = 'v' then String.sub v 1 (String.length v - 1) else v in
  match String.split_on_char '.' v with
  | [maj; min; patch] ->
      (try Some (int_of_string maj, int_of_string min, int_of_string patch)
       with Failure _ -> None)
  | _ -> None

let is_newer_version remote local =
  match version_to_tuple remote, version_to_tuple local with
  | Some (r1, r2, r3), Some (l1, l2, l3) ->
      (r1, r2, r3) > (l1, l2, l3)
  | _ -> false
