Skip to main content

git_lore/cli/
mod.rs

1use std::path::PathBuf;
2use std::io::{Read, Write};
3
4use anyhow::{Context, Result};
5use clap::{Args, Parser, Subcommand, ValueEnum};
6use flate2::read::GzDecoder;
7use flate2::write::GzEncoder;
8use flate2::Compression;
9use uuid::Uuid;
10
11use crate::git;
12use crate::lore::prism::{PrismSignal, PRISM_STALE_TTL_SECONDS};
13use crate::lore::{AtomState, LoreAtom, LoreKind, Workspace, WorkspaceState};
14use crate::mcp::{McpService, PreflightSeverity, ProposalRequest};
15
16#[derive(Parser, Debug)]
17#[command(
18    name = "git-lore",
19    version,
20    about = "Capture and synchronize project rationale"
21)]
22struct Cli {
23    #[command(subcommand)]
24    command: Commands,
25}
26
27#[derive(Subcommand, Debug)]
28enum Commands {
29    /// Initialize a new Git-Lore workspace
30    Init(InitArgs),
31    /// Create a new structured Lore Atom
32    Mark(MarkArgs),
33    /// Display the status of the local workspace
34    Status(StatusArgs),
35    /// Freeze a cryptographic snapshot of the current lore state
36    Checkpoint(CheckpointArgs),
37    /// Integrate lore directly into Git commit trailers
38    Commit(CommitArgs),
39    /// Emit ephemeral PRISM signals (soft-locks)
40    Signal(SignalArgs),
41    /// Fetch active lore constraints and history for a file
42    Context(ContextArgs),
43    /// Propose a new Lore Atom
44    Propose(ProposeArgs),
45    /// Spawn the Model Context Protocol (MCP) server
46    Mcp(McpArgs),
47    /// Explain the rationale of code based on bordered lore
48    Explain(ExplainArgs),
49    /// Run CI logic checks over the workspace vs canon Lore
50    Validate(ValidateArgs),
51    /// Synchronize the hot-workspace with cold-storage
52    Sync(SyncArgs),
53    /// Strap Git-Lore into local Git hooks
54    Install(InstallArgs),
55    /// The underlying reconciliator invoked automatically by Git
56    Merge(MergeArgs),
57    /// Alter the lifecycle state of an existing Lore Atom
58    SetState(SetStateArgs),
59    /// Generates LLM integration instructions/skills (e.g. for GitHub Copilot)
60    Generate(GenerateArgs),
61    /// Interactively resolve active content contradictions at a specific location
62    Resolve(ResolveArgs),
63}
64
65#[derive(Args, Debug)]
66struct ResolveArgs {
67    /// The conflict location key (e.g. "path/to/file.rs::my_scope")
68    #[arg(value_name = "LOCATION")]
69    location: String,
70    /// Workspace path
71    #[arg(long, default_value = ".")]
72    path: PathBuf,
73    /// Pre-select the winning atom ID (bypasses interactive prompt)
74    #[arg(long)]
75    winner_id: Option<String>,
76    /// Optional reason for this resolution
77    #[arg(long)]
78    reason: Option<String>,
79}
80
81#[derive(Args, Debug)]
82struct GenerateArgs {
83    /// Target file to write integration skill/instruction to
84    #[arg(default_value = ".github/git-lore-skills.md")]
85    output: PathBuf,
86}
87
88#[derive(Args, Debug)]
89struct InitArgs {
90    /// Workspace path
91    #[arg(default_value = ".")]
92    path: PathBuf,
93}
94
95#[derive(Args, Debug)]
96struct MarkArgs {
97    /// The brief identifier or name of the rule
98    #[arg(long)]
99    title: String,
100    /// Explanatory text that provides context (the "Why")
101    #[arg(long)]
102    body: Option<String>,
103    /// The scope boundary, like a function name or class
104    #[arg(long)]
105    scope: Option<String>,
106    /// The target directory or file this rule binds to
107    #[arg(long)]
108    path: Option<PathBuf>,
109    /// A literal shell command that validates the atom when preflight runs
110    #[arg(long = "validation-script")]
111    validation_script: Option<String>,
112    /// The typology of the lore
113    #[arg(long, value_enum, default_value = "decision")]
114    kind: LoreKindArg,
115}
116
117#[derive(Args, Debug)]
118struct StatusArgs {
119    /// Workspace path
120    #[arg(default_value = ".")]
121    path: PathBuf,
122}
123
124#[derive(Args, Debug)]
125struct CheckpointArgs {
126    /// Workspace path
127    #[arg(default_value = ".")]
128    path: PathBuf,
129    /// Optional message outlining the checkpoint reason
130    #[arg(long)]
131    message: Option<String>,
132}
133
134#[derive(Args, Debug)]
135struct CommitArgs {
136    /// Workspace path
137    #[arg(default_value = ".")]
138    path: PathBuf,
139    /// Your commit message subject
140    #[arg(long)]
141    message: String,
142    /// Allow committing even if no files changed (lore changes only)
143    #[arg(long, default_value_t = true)]
144    allow_empty: bool,
145}
146
147#[derive(Args, Debug)]
148struct SignalArgs {
149    /// Workspace path
150    #[arg(default_value = ".")]
151    workspace: PathBuf,
152    /// Identifier for the active AI session (auto-generated if missing)
153    #[arg(long)]
154    session_id: Option<String>,
155    /// The name/identity of the emitting agent
156    #[arg(long)]
157    agent: Option<String>,
158    /// Target code scope
159    #[arg(long)]
160    scope: Option<String>,
161    /// The affected file(s) or directories globs
162    #[arg(long = "path", value_name = "GLOB")]
163    paths: Vec<String>,
164    /// The temporary assumptions running in memory
165    #[arg(long = "assumption")]
166    assumptions: Vec<String>,
167    /// A tentative brief goal or decision
168    #[arg(long)]
169    decision: Option<String>,
170}
171
172#[derive(Args, Debug)]
173struct ContextArgs {
174    /// Workspace path
175    #[arg(default_value = ".")]
176    path: PathBuf,
177    /// The target script/file
178    #[arg(long)]
179    file: PathBuf,
180    /// Specific line number for tree-sitter drilling
181    #[arg(long)]
182    cursor_line: Option<usize>,
183}
184
185#[derive(Args, Debug)]
186struct ProposeArgs {
187    /// Workspace path
188    #[arg(default_value = ".")]
189    path: PathBuf,
190    /// The target script/file
191    #[arg(long)]
192    file: PathBuf,
193    /// The headline of the new rule
194    #[arg(long)]
195    title: String,
196    /// The context and reasoning body
197    #[arg(long)]
198    body: Option<String>,
199    /// A literal shell command that validates the atom when preflight runs
200    #[arg(long = "validation-script")]
201    validation_script: Option<String>,
202    /// Targeted line number
203    #[arg(long)]
204    cursor_line: Option<usize>,
205    /// Type of the proposed element
206    #[arg(long, value_enum, default_value = "decision")]
207    kind: LoreKindArg,
208}
209
210#[derive(Args, Debug)]
211struct McpArgs {
212    /// Workspace path
213    #[arg(default_value = ".")]
214    path: PathBuf,
215}
216
217#[derive(Args, Debug)]
218struct ExplainArgs {
219    /// Workspace path
220    #[arg(default_value = ".")]
221    path: PathBuf,
222    /// The target file
223    #[arg(long)]
224    file: PathBuf,
225    /// Specific line number for tree-sitter drilling
226    #[arg(long)]
227    cursor_line: Option<usize>,
228}
229
230#[derive(Args, Debug)]
231struct ValidateArgs {
232    /// Workspace path
233    #[arg(default_value = ".")]
234    path: PathBuf,
235}
236
237#[derive(Args, Debug)]
238struct SyncArgs {
239    /// Workspace path
240    #[arg(default_value = ".")]
241    path: PathBuf,
242}
243
244#[derive(Args, Debug)]
245struct InstallArgs {
246    /// Workspace path
247    #[arg(default_value = ".")]
248    path: PathBuf,
249}
250
251#[derive(Args, Debug)]
252struct MergeArgs {
253    /// Base commit/lore state
254    base: PathBuf,
255    /// Current commit/lore state
256    current: PathBuf,
257    /// Other branch commit/lore state
258    other: PathBuf,
259}
260
261#[derive(Args, Debug)]
262struct SetStateArgs {
263    /// Workspace path
264    #[arg(default_value = ".")]
265    path: PathBuf,
266    /// The ID of the atom to update
267    #[arg(long = "atom-id")]
268    atom_id: String,
269    /// The new state for the atom
270    #[arg(long, value_enum)]
271    state: AtomStateArg,
272    /// The reason for changing the state
273    #[arg(long)]
274    reason: String,
275    /// The actor making the change
276    #[arg(long)]
277    actor: Option<String>,
278}
279
280#[derive(Clone, Copy, Debug, ValueEnum)]
281enum LoreKindArg {
282    Decision,
283    Assumption,
284    OpenQuestion,
285    Signal,
286}
287
288#[derive(Clone, Copy, Debug, ValueEnum)]
289enum AtomStateArg {
290    Draft,
291    Proposed,
292    Accepted,
293    Deprecated,
294}
295
296pub fn run() -> Result<()> {
297    let cli = Cli::parse();
298
299    match cli.command {
300        Commands::Init(args) => init(args),
301        Commands::Mark(args) => mark(args),
302        Commands::Status(args) => status(args),
303        Commands::Checkpoint(args) => checkpoint(args),
304        Commands::Commit(args) => commit(args),
305        Commands::Signal(args) => signal(args),
306        Commands::Context(args) => context(args),
307        Commands::Propose(args) => propose(args),
308        Commands::Mcp(args) => mcp(args),
309        Commands::Explain(args) => explain(args),
310        Commands::Validate(args) => validate(args),
311        Commands::Sync(args) => sync(args),
312        Commands::Install(args) => install(args),
313        Commands::Merge(args) => merge(args),
314        Commands::SetState(args) => set_state(args),
315        Commands::Generate(args) => generate(args),
316        Commands::Resolve(args) => resolve(args),
317    }
318}
319
320fn generate(args: GenerateArgs) -> Result<()> {
321    if let Some(parent) = args.output.parent() {
322        if !parent.exists() {
323            std::fs::create_dir_all(parent).with_context(|| format!("failed to create dir: {}", parent.display()))?;
324        }
325    }
326    
327    let content = r#"---
328description: >-
329  Git-Lore Integration Skill. Helps capture architectural rationale, rules, and assumptions tightly bound to the codebase via the Git-Lore CLI and MCP server tools. Use when a user asks to establish a new codebase rule, architectural decision, or convention.
330---
331
332# Git-Lore Skills
333
334Keep architectural decisions and knowledge strongly bound to codebase states.
335
336**When to Use:**
337- When adding notes/assumptions explicitly requested by the user.
338- When a user asks "document this pattern for later", "mark this assumption", or "save this rule".
339- Upon discovering a consistent convention not currently documented in `.lore`.
340
341## Instructions
342
343<instructions>
344You are an AI assistant empowered to use `git-lore`, a tool that anchors rationale as structured "lore atoms" directly bounded to codebase paths and scopes.
345
346### 1. Discovering Lore (Context)
347When you navigate to a new file or need to understand how it should be implemented, read the context using:
348- **MCP Tool:** `git_lore_context` (pass the file path) or `git_lore_memory_search` (pass a query).
349- **CLI Alternative:** Tell the user to run `git-lore context --file <file>` or `git-lore explain --file <file>`.
350
351### 2. Recording Lore (Propose / Mark)
352When the user and you make an important architectural decision, or establish a convention that other AI agents should know:
353- **MCP Tool:** Call `git_lore_propose`. **Crucial:** You must first call `git_lore_state_snapshot` to get the `state_checksum` and `snapshot_generated_unix_seconds` required for proposing.
354- **CLI Alternative:** Suggest the user run:
355  `git-lore mark --title "Your concise rule constraint" --body "The reason why this exists" --path "<relative_file_path>"`
356
357### 3. Git Workflows
358When the task is done, gently remind the user they can commit this knowledge firmly to Git by running `git-lore commit --message "feat: your task"`.
359
360# Flujo de Trabajo: Git-Lore + Git
361
362**¿En qué etapa de desarrollo te encuentras?**
363
364## 1. Modificando código críptico o legado
365Necesitas arreglar un bug o extender un módulo, pero el código es confuso y no sabes qué romperás si lo cambias.
366
367**Flujo:**
368*   **Git:** Crear rama: `git checkout -b fix/module`
369*   **Git-Lore:** Obtener reglas: `git-lore context --file module.rs`
370
371> **¿Cómo ayuda?** Resuelve la paradoja de "Chesterton's Fence". Antes de borrar o cambiar código, el sistema te expone *por qué* se hizo (las decisiones históricas que enmarcan ese archivo), evitantando que re-introduzcas bugs antiguos.
372
373## 2. Tomando decisiones arquitectónicas clave
374Estás liderando un nuevo feature y has decidido usar un patrón de diseño o herramienta específica para este módulo.
375
376**Flujo:**
377*   **Git:** Programar la lógica principal y hacer `git add .`
378*   **Git-Lore:** Marcar: `git-lore mark --kind decision --title "Usar Patrón Builder..."`
379*   **Integración:** Confirmar: `git-lore commit -m "feat: xyz"`
380
381> **¿Cómo ayuda?** Al usar `git-lore commit`, el contexto no solo se queda local, sino que se inyecta como un *Git Trailer* en el historial puro de Git. Cualquiera (incluso sin tener git-lore) puede ver en `git log` la traza de la decisión junto al código que la implementó.
382
383## 3. Delegando código complejo a una IA (Copilot, Agentes)
384Le estás pidiendo a un Agente IA que genere un refactor masivo o construya un nuevo servicio desde tu editor (VS Code).
385
386**Flujo:**
387*   **MCP Server:** La IA pide contexto silenciosamente: `git_lore_context(scope)`
388*   **Desarrollo:** La IA genera código respetando las restricciones inyectadas.
389*   **Evolución:** La IA sugiere reglas: `git_lore_propose(...)`
390
391> **¿Cómo ayuda?** Alimenta automáticamente a la IA (Zero-Shot compliance). Previene que el Agente alucine patrones equivocados o traiga dependencias prohibidas. La IA "nace" conociendo cómo funciona este equipo o proyecto.
392
393## 4. Revisión de un Pull Request
394Un colega sube su código para que lo apruebes y se funda con la rama principal.
395
396**Flujo:**
397*   **Git / CI:** Se levanta la Pull Request en GitHub/GitLab.
398*   **Git-Lore:** CI verifica o el humano ejecuta `git-lore validate`.
399
400> **¿Cómo ayuda?** Transforma las opiniones subjetivas en revisiones objetivas. El validador (o el revisor) puede chequear si el código en revisión rompe alguna regla que fue previamente "Acordada y Aceptada" en el lore del directorio afectado.
401
402## 5. Explorando la Memoria del Proyecto (Discovery)
403No recuerdas por qué se tomó una decisión hace meses, o le pides a una IA que investigue el proyecto antes de proponer código nuevo.
404
405**Flujo:**
406*   **MCP Server:** La IA busca intenciones difusas: `git_lore_memory_search("auth architecture")`
407*   **Git-Lore:** Obtener justificación detallada: `git-lore explain --file src/auth.rs`
408
409> **¿Cómo ayuda?** Democratiza el conocimiento histórico. A través del buscador léxico y semántico del MCP, puedes encontrar conocimiento por "intención" y "recencia", en lugar de buscar a ciegas en Slack o Jira.
410
411## 6. Evolución del Conocimiento (Estado y Ciclo de Vida)
412El código cambia, y las reglas también deben hacerlo. Una convención propuesta por IA necesita ser aceptada, o una regla antigua queda obsoleta.
413
414**Flujo:**
415*   **MCP Server:** La IA sugiere cambios: `git_lore_propose(target_state="Proposed")`
416*   **Git-Lore:** El humano formaliza: `git-lore set-state --state accepted`
417*   **Git-Lore:** Las reglas viejas se retiran: `git-lore set-state --state deprecated`
418
419> **¿Cómo ayuda?** El canon (lore) nunca es inmutable y no se convierte en una Wiki zombie. Pasa por estados `Draft -> Proposed -> Accepted -> Deprecated`, dándole al equipo y agentes control explícito sobre la validez del conocimiento sobre el tiempo.
420
421## 7. Flujos Activos Autoriales (Signals & Preflight)
422Agentes IA autónomos necesitan verificar la seguridad de la memoria y alertar al equipo de sus intenciones transitorias antes de destruir estados del repositorio accidentalmente.
423
424**Flujo:**
425*   **Git-Lore:** Crear instantánea segura: `git-lore checkpoint / git-lore status`
426*   **MCP Server:** Validaciones de estado: `git_lore_memory_preflight("commit")`
427*   **Git-Lore:** Agentes emiten eventos cortos: `git-lore signal --agent "Codegen"`
428
429> **¿Cómo ayuda?** Permite la colaboración segura (Safe Writes) con Inteligencia Artificial. Con verificaciones previas como `transition_preview` y `preflight`, se evita la sobrescritura y entropía donde la IA accidentalmente contradiga decisiones base de otras ramas.
430
431## 8. Congelando el Conocimiento (Checkpoints)
432Estás a punto de hacer un refactor masivo de reglas de negocio o estás orquestando múltiples agentes de IA simultáneos. Necesitas asegurar un punto de restauración seguro de las intenciones de tu equipo.
433
434**Flujo:**
435*   **Git-Lore:** Congelar el estado base: `git-lore checkpoint --message "Pre-refactor de auth"`
436*   **MCP Server:** Agentes IA validan checksums: `git_lore_state_snapshot()`
437*   **Integración:** Fallo rápido (Fail-fast) preventivo en caso de discrepancias temporales.
438
439> **¿Cómo ayuda?** Resuelve la desalineación de estados o condiciones de carrera entre ramas, humanos y Agentes de IA. Un 'checkpoint' crea una fotografía instantánea del *Lore*. Si el código muta o un agente propone un cambio basándose en información desactualizada, el archivo bloquea la sobre-escritura (Strict State-First Check).
440
441## 9. Fusión y Reconciliación de Conocimiento (Merge)
442Trabajas en una rama feature donde propusiste nuevas decisiones, mientras que en la rama 'main' otra persona agregó o deprecó otras reglas. Ahora necesitas fusionar ambas ramas sin perder ni contradecir el Lore.
443
444**Flujo:**
445*   **Git:** Comienza la fusión de archivos: `git merge feature/branch`
446*   **Git-Lore:** Git dispara el merge driver: `git-lore merge <base> <current> <other>`
447*   **Git-Lore:** Reconciliación: deduplica IDs, verifica estados (Ej. "Accepted" vence a "Proposed").
448
449> **¿Cómo ayuda?** Git-Lore se instala como un 'Merge Driver' personalizado (vía `git-lore install`). A diferencia de fusionar código o JSON manualmente, este previene colisiones semánticas. Si un átomo en 'main' fue marcado como `Deprecated`, pero en tu rama lo habías actualizado, el algoritmo de reconciliación lo fusionará inteligentemente.
450
451## 10. Proposiciones y Señales Contextuales (Propose & Signal)
452Durante un sprint rápido, un desarrollador o una IA lanza una "Suposición" temporal (Signal) al aire para que la IA que trabaje en el código asociado la tenga en cuenta temporalmente, o "proponga" formalmente (Propose) una nueva convención.
453
454**Flujo:**
455*   **Git-Lore:** Crear señal efímera: `git-lore signal --assumption "Asumo que la API devuelve XML" --path src/`
456*   **MCP Server:** Subagentes leen la señal: `git_lore_memory_search()` expone la suposición fresca.
457*   **Git-Lore:** Validación: `git-lore propose --title "API responde JSON" --kind decision` reemplaza la suposición.
458
459> **¿Cómo funciona el salto de Señal a Decisión internamente?**
460>
461> 1.  **La Señal (Conocimiento Efímero):** `git-lore signal` NO crea un Registro permanente. Crea un archivo temporal (PrismSignal) con un Tiempo de Vida (TTL) programado para expirar. Actúa como un cerrojo suave ("Soft-lock") para avisar a otros agentes: *"Ojo, estoy asumiendo esto en la memoria ahora mismo"*.
462> 2.  **La Decisión (Conocimiento Canónico):** `git-lore propose --kind decision` crea un "Átomo" real, un archivo JSON estructurado con un UUID que entra formalmente al ciclo de evaluación (Proposed / Accepted).
463> 3.  **El Reemplazo:** La "asunción" inicial no se sobre-escribe mágicamente código sobre código. En cambio, cuando el agente termina su trabajo y formaliza la regla con `propose`, el servidor inscribe el Átomo permanente. En procesos de guardado posteriores, Git-Lore invoca una limpieza (`prune_stale_prism_signals`) evaporando las señales vencidas de la carpeta `.lore/signals/`. El conocimiento fugaz muere, y el canon estructurado prevalece inmutable.
464</instructions>
465"#;
466    let mut file = std::fs::File::create(&args.output).context("failed to create output file")?;
467    file.write_all(content.as_bytes()).context("failed to write content")?;
468    
469    println!("Successfully generated Git-Lore skill at: {}", args.output.display());
470    Ok(())
471}
472
473fn init(args: InitArgs) -> Result<()> {
474    let workspace = Workspace::init(&args.path)
475        .with_context(|| format!("failed to initialize workspace at {}", args.path.display()))?;
476
477    println!(
478        "Initialized Git-Lore workspace at {}",
479        workspace.root().display()
480    );
481    Ok(())
482}
483
484fn mark(args: MarkArgs) -> Result<()> {
485    let workspace = Workspace::discover(".")?;
486    enforce_cli_write_guard(workspace.root(), "edit")?;
487
488    let atom = LoreAtom::new(
489        args.kind.into(),
490        AtomState::Proposed,
491        args.title,
492        args.body,
493        args.scope,
494        args.path,
495    )
496    .with_validation_script(args.validation_script);
497    let atom_id = atom.id.clone();
498
499    workspace.record_atom(atom)?;
500    println!("Recorded proposed lore atom {atom_id}");
501    Ok(())
502}
503
504fn status(args: StatusArgs) -> Result<()> {
505    let workspace = Workspace::discover(&args.path)?;
506    let state = workspace.load_state()?;
507    let report = workspace.entropy_report()?;
508
509    println!("Workspace: {}", workspace.root().display());
510    println!("Total atoms: {}", state.atoms.len());
511    println!("Entropy score: {}/100", report.score);
512
513    for atom in state.atoms.iter().rev().take(5) {
514        println!(
515            "- [{}] {:?} {:?}: {}",
516            atom.id, atom.kind, atom.state, atom.title
517        );
518    }
519
520    if report.contradictions.is_empty() {
521        println!("Contradictions: none");
522    } else {
523        println!("Contradictions:");
524        for contradiction in report.contradictions.iter().take(5) {
525            println!("- {:?} {}: {}", contradiction.kind, contradiction.key, contradiction.message);
526        }
527    }
528
529    if !report.notes.is_empty() {
530        println!("Entropy notes:");
531        for note in report.notes {
532            println!("- {note}");
533        }
534    }
535
536    Ok(())
537}
538
539fn checkpoint(args: CheckpointArgs) -> Result<()> {
540    let workspace = Workspace::discover(&args.path)?;
541    enforce_cli_write_guard(workspace.root(), "commit")?;
542
543    let checkpoint = workspace.write_checkpoint(args.message)?;
544    let subject = checkpoint
545        .message
546        .as_deref()
547        .unwrap_or("git-lore checkpoint");
548    let commit_message = git::build_commit_message(subject, &checkpoint.atoms);
549
550    if let Ok(repository) = git::discover_repository(&workspace.root()) {
551        println!("Git repository: {}", git::repository_root(&repository).display());
552    }
553
554    println!("Checkpoint {} written", checkpoint.id);
555    if !commit_message.is_empty() {
556        println!();
557        println!("{commit_message}");
558    }
559
560    Ok(())
561}
562
563fn commit(args: CommitArgs) -> Result<()> {
564    let workspace = Workspace::discover(&args.path)?;
565    enforce_cli_write_guard(workspace.root(), "commit")?;
566
567    let repository_root = git::discover_repository(workspace.root())?;
568    let state = workspace.load_state()?;
569
570    let validation_issues = git::validate_workspace_against_git(&repository_root, &workspace)?;
571    if !validation_issues.is_empty() {
572        anyhow::bail!(
573            "validation failed: {}",
574            validation_issues.join("; ")
575        );
576    }
577
578    let commit_message = git::build_commit_message(args.message, &state.atoms);
579
580    let hash = git::commit_lore_message(&repository_root, commit_message, args.allow_empty)?;
581    workspace.accept_active_atoms(Some(&hash))?;
582
583    for atom in state.atoms.iter().filter(|atom| atom.state != AtomState::Deprecated) {
584        git::write_lore_ref(&repository_root, atom, &hash)?;
585    }
586
587    println!("Committed lore checkpoint {}", hash);
588    Ok(())
589}
590
591fn signal(args: SignalArgs) -> Result<()> {
592    if args.paths.is_empty() {
593        anyhow::bail!("at least one --path glob is required to broadcast a PRISM signal");
594    }
595
596    let workspace = Workspace::discover(&args.workspace)?;
597    enforce_cli_write_guard(workspace.root(), "edit")?;
598
599    let pruned_stale = workspace.prune_stale_prism_signals(PRISM_STALE_TTL_SECONDS)?;
600    if pruned_stale > 0 {
601        println!("Pruned {pruned_stale} stale PRISM signal(s) before broadcasting");
602    }
603
604    let signal = PrismSignal::new(
605        args.session_id.unwrap_or_else(|| Uuid::new_v4().to_string()),
606        args.agent,
607        args.scope,
608        args.paths,
609        args.assumptions,
610        args.decision,
611    );
612
613    workspace.write_prism_signal(&signal)?;
614    let conflicts = workspace.scan_prism_conflicts(&signal)?;
615
616    println!("Broadcast PRISM signal {}", signal.session_id);
617
618    if conflicts.is_empty() {
619        println!("No soft-lock conflicts detected.");
620        return Ok(());
621    }
622
623    println!("Soft-lock warnings:");
624    for conflict in conflicts {
625        let agent = conflict.agent.as_deref().unwrap_or("unknown-agent");
626        let scope = conflict.scope.as_deref().unwrap_or("unknown-scope");
627        let decision = conflict.decision.as_deref().unwrap_or("no decision recorded");
628        println!(
629            "- session {} ({agent}, {scope}) overlaps on {}: {decision}",
630            conflict.session_id,
631            conflict.overlapping_paths.join(", "),
632        );
633    }
634
635    Ok(())
636}
637
638fn context(args: ContextArgs) -> Result<()> {
639    let service = McpService::new(&args.path);
640    let snapshot = service.context(&args.file, args.cursor_line)?;
641
642    println!("Workspace: {}", snapshot.workspace_root.display());
643    println!("File: {}", snapshot.file_path.display());
644
645    if let Some(scope) = snapshot.scope {
646        println!("Scope: {} {} ({}-{})", scope.kind_label(), scope.name, scope.start_line, scope.end_line);
647    }
648
649    if snapshot.constraints.is_empty() {
650        println!("No matching lore constraints.");
651    } else {
652        println!("Constraints:");
653        for constraint in snapshot.constraints {
654            println!("- {constraint}");
655        }
656    }
657
658    Ok(())
659}
660
661fn propose(args: ProposeArgs) -> Result<()> {
662    let service = McpService::new(&args.path);
663    enforce_cli_write_guard(&args.path, "edit")?;
664
665    let result = service.propose(ProposalRequest {
666        file_path: args.file,
667        cursor_line: args.cursor_line,
668        kind: args.kind.into(),
669        title: args.title,
670        body: args.body,
671        scope: None,
672        validation_script: args.validation_script,
673    })?;
674
675    println!("Proposed lore atom {}", result.atom.id);
676    if let Some(scope) = result.scope {
677        println!("Scope: {} {} ({}-{})", scope.kind_label(), scope.name, scope.start_line, scope.end_line);
678    }
679
680    Ok(())
681}
682
683fn mcp(args: McpArgs) -> Result<()> {
684    let server = crate::mcp::McpServer::new(&args.path);
685    server.run_stdio()
686}
687
688fn explain(args: ExplainArgs) -> Result<()> {
689    let service = McpService::new(&args.path);
690    let snapshot = service.context(&args.file, args.cursor_line)?;
691
692    println!("Workspace: {}", snapshot.workspace_root.display());
693    println!("File: {}", snapshot.file_path.display());
694
695    if let Some(scope) = snapshot.scope {
696        println!("Scope: {} {} ({}-{})", scope.kind_label(), scope.name, scope.start_line, scope.end_line);
697    }
698
699    if snapshot.historical_decisions.is_empty() {
700        println!("Historical decisions: none");
701    } else {
702        println!("Historical decisions:");
703        for decision in snapshot.historical_decisions {
704            println!("- {} {}", decision.commit_hash, decision.trailer_value);
705        }
706    }
707
708    if snapshot.constraints.is_empty() {
709        println!("No matching constraints.");
710    } else {
711        println!("Constraints:");
712        for constraint in snapshot.constraints {
713            println!("- {constraint}");
714        }
715    }
716
717    Ok(())
718}
719
720fn validate(args: ValidateArgs) -> Result<()> {
721    let workspace = Workspace::discover(&args.path)?;
722    let repository_root = git::discover_repository(workspace.root())?;
723    let issues = git::validate_workspace_against_git(&repository_root, &workspace)?;
724
725    if issues.is_empty() {
726        println!("Validation passed");
727        return Ok(());
728    }
729
730    println!("Validation issues:");
731    for issue in issues {
732        println!("- {issue}");
733    }
734
735    anyhow::bail!("validation failed");
736}
737
738fn sync(args: SyncArgs) -> Result<()> {
739    let workspace = Workspace::discover(&args.path)?;
740    enforce_cli_write_guard(workspace.root(), "sync")?;
741
742    let repository_root = git::discover_repository(workspace.root())?;
743    let atoms = git::sync_workspace_from_git_history(&repository_root, &workspace)?;
744
745    #[cfg(feature = "semantic-search")]
746    {
747        let state = workspace.load_state()?;
748        let accepted = workspace.load_accepted_atoms()?;
749        crate::mcp::semantic::rebuild_index(workspace.root(), &state.atoms, &accepted)?;
750        println!("Rebuilt Memvid semantic local index.");
751    }
752
753    println!("Synchronized {} lore atoms from Git history", atoms.len());
754    Ok(())
755}
756
757fn install(args: InstallArgs) -> Result<()> {
758    let workspace = Workspace::discover(&args.path)?;
759    let repository_root = git::discover_repository(workspace.root())?;
760    git::install_git_lore_integration(&repository_root)?;
761
762    println!("Installed Git-Lore hooks and merge driver in {}", repository_root.display());
763    Ok(())
764}
765
766fn merge(args: MergeArgs) -> Result<()> {
767    let base = read_workspace_state_file(&args.base)
768        .with_context(|| format!("failed to read base merge file {}", args.base.display()))?;
769    let current = read_workspace_state_file(&args.current)
770        .with_context(|| format!("failed to read current merge file {}", args.current.display()))?;
771    let other = read_workspace_state_file(&args.other)
772        .with_context(|| format!("failed to read other merge file {}", args.other.display()))?;
773
774    let merged_version = base
775        .state
776        .version
777        .max(current.state.version)
778        .max(other.state.version);
779    let outcome = crate::lore::merge::reconcile_lore(&base.state, &current.state, &other.state);
780    let merged_state = WorkspaceState {
781        version: merged_version,
782        atoms: outcome.merged,
783    };
784
785    let write_gzip = base.was_gzip || current.was_gzip || other.was_gzip;
786    write_workspace_state_file(&args.current, &merged_state, write_gzip)
787        .with_context(|| format!("failed to write merged lore file {}", args.current.display()))?;
788
789    if outcome.conflicts.is_empty() {
790        println!("Merged lore state with {} atom(s)", merged_state.atoms.len());
791        return Ok(());
792    }
793
794    eprintln!(
795        "Lore merge produced {} conflict(s); manual review required",
796        outcome.conflicts.len()
797    );
798    for conflict in outcome.conflicts {
799        eprintln!("- {:?} {}: {}", conflict.kind, conflict.key, conflict.message);
800    }
801
802    anyhow::bail!("lore merge requires manual resolution")
803}
804
805fn set_state(args: SetStateArgs) -> Result<()> {
806    let workspace = Workspace::discover(&args.path)?;
807    enforce_cli_write_guard(workspace.root(), "edit")?;
808
809    let actor = args.actor.or_else(|| std::env::var("USER").ok());
810    let updated = workspace.transition_atom_state(
811        &args.atom_id,
812        args.state.into(),
813        args.reason,
814        actor,
815    )?;
816
817    println!(
818        "Transitioned lore atom {} to {:?}",
819        updated.id, updated.state
820    );
821    Ok(())
822}
823
824impl From<LoreKindArg> for LoreKind {
825    fn from(value: LoreKindArg) -> Self {
826        match value {
827            LoreKindArg::Decision => LoreKind::Decision,
828            LoreKindArg::Assumption => LoreKind::Assumption,
829            LoreKindArg::OpenQuestion => LoreKind::OpenQuestion,
830            LoreKindArg::Signal => LoreKind::Signal,
831        }
832    }
833}
834
835impl From<AtomStateArg> for AtomState {
836    fn from(value: AtomStateArg) -> Self {
837        match value {
838            AtomStateArg::Draft => AtomState::Draft,
839            AtomStateArg::Proposed => AtomState::Proposed,
840            AtomStateArg::Accepted => AtomState::Accepted,
841            AtomStateArg::Deprecated => AtomState::Deprecated,
842        }
843    }
844}
845
846#[derive(Clone, Debug)]
847struct EncodedWorkspaceState {
848    state: WorkspaceState,
849    was_gzip: bool,
850}
851
852fn read_workspace_state_file(path: &std::path::Path) -> Result<EncodedWorkspaceState> {
853    let bytes = std::fs::read(path)
854        .with_context(|| format!("failed to read lore state file {}", path.display()))?;
855    let is_gzip = bytes.starts_with(&[0x1f, 0x8b]);
856
857    let content = if is_gzip {
858        let mut decoder = GzDecoder::new(bytes.as_slice());
859        let mut decoded = Vec::new();
860        decoder
861            .read_to_end(&mut decoded)
862            .with_context(|| format!("failed to decompress lore state file {}", path.display()))?;
863        decoded
864    } else {
865        bytes
866    };
867
868    let state: WorkspaceState = serde_json::from_slice(&content)
869        .with_context(|| format!("failed to parse lore state file {}", path.display()))?;
870
871    Ok(EncodedWorkspaceState {
872        state,
873        was_gzip: is_gzip,
874    })
875}
876
877fn write_workspace_state_file(
878    path: &std::path::Path,
879    state: &WorkspaceState,
880    write_gzip: bool,
881) -> Result<()> {
882    let encoded = serde_json::to_vec_pretty(state)
883        .with_context(|| format!("failed to encode merged lore state {}", path.display()))?;
884
885    let bytes = if write_gzip {
886        let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
887        encoder
888            .write_all(&encoded)
889            .with_context(|| format!("failed to gzip lore state {}", path.display()))?;
890        encoder
891            .finish()
892            .with_context(|| format!("failed to finalize gzip lore state {}", path.display()))?
893    } else {
894        encoded
895    };
896
897    std::fs::write(path, bytes)
898        .with_context(|| format!("failed to write lore state file {}", path.display()))?;
899
900    Ok(())
901}
902
903fn enforce_cli_write_guard(path: impl AsRef<std::path::Path>, operation: &str) -> Result<()> {
904    let service = McpService::new(path);
905    let snapshot = service.state_snapshot()?;
906    let report = service.memory_preflight(operation)?;
907
908    if snapshot.state_checksum != report.state_checksum {
909        anyhow::bail!(
910            "state-first guard failed: state drift detected during preflight (snapshot {}, preflight {})",
911            snapshot.state_checksum,
912            report.state_checksum
913        );
914    }
915
916    if report
917        .issues
918        .iter()
919        .any(|issue| issue.severity == PreflightSeverity::Block)
920    {
921        println!("Preflight issues:");
922        for issue in report.issues {
923            println!("- {:?} {}: {}", issue.severity, issue.code, issue.message);
924        }
925        anyhow::bail!(
926            "state-first preflight blocked {} operation; resolve issues and retry",
927            operation
928        );
929    }
930
931    for issue in report
932        .issues
933        .iter()
934        .filter(|issue| issue.severity != PreflightSeverity::Info)
935    {
936        println!("Preflight {:?} {}: {}", issue.severity, issue.code, issue.message);
937    }
938
939    Ok(())
940}
941fn resolve(args: ResolveArgs) -> Result<()> {
942    let workspace = Workspace::discover(&args.path)?;
943    enforce_cli_write_guard(workspace.root(), "edit")?;
944
945    let state = workspace.load_state()?;
946    let target_location = args.location.clone();
947    
948    let candidate_atoms: Vec<LoreAtom> = state.atoms.into_iter()
949        .filter(|atom| {
950            let p = atom.path.as_ref().map(|v| v.to_string_lossy().replace('\\', "/")).unwrap_or_else(|| "<no-path>".to_string());
951            let s = atom.scope.as_deref().unwrap_or("<no-scope>");
952            format!("{}::{}", p, s) == target_location && atom.state != AtomState::Deprecated
953        })
954        .collect();
955
956    if candidate_atoms.is_empty() {
957        println!("No active contradictions found at location {target_location}");
958        return Ok(());
959    }
960
961    let winner_id = if let Some(id) = args.winner_id {
962        if !candidate_atoms.iter().any(|a| a.id == id) {
963            anyhow::bail!("Atom ID {} not found among active atoms at location {}", id, target_location);
964        }
965        id
966    } else {
967        println!("Found {} active atoms at {}:", candidate_atoms.len(), target_location);
968        for (i, atom) in candidate_atoms.iter().enumerate() {
969            println!("[{}] {} ({:?}) - {}", i, atom.id, atom.state, atom.title);
970        }
971        
972        let idx = loop {
973            print!("Select the winning atom index (0-{}): ", candidate_atoms.len() - 1);
974            std::io::stdout().flush()?;
975            let mut input = String::new();
976            std::io::stdin().read_line(&mut input)?;
977            if let Ok(idx) = input.trim().parse::<usize>() {
978                if idx < candidate_atoms.len() {
979                    break idx;
980                }
981            }
982            println!("Invalid selection.");
983        };
984        candidate_atoms[idx].id.clone()
985    };
986
987    let actor = std::env::var("USER").ok();
988    
989    for atom in candidate_atoms {
990        if atom.id != winner_id {
991            // Note: workspace.transition_atom_state requires (id, target_state, reason, actor)
992            println!("Deprecating lost atom {}...", atom.id);
993            let res = workspace.transition_atom_state(
994                &atom.id,
995                AtomState::Deprecated,
996                args.reason.clone().unwrap_or_else(|| "Resolved via CLI".to_string()),
997                actor.clone(),
998            );
999            if let Err(e) = res {
1000                println!("Warning: failed to deprecate {}: {}", atom.id, e);
1001            }
1002        }
1003    }
1004    
1005    // Attempt to accept the winner if it isn't accepted yet
1006    let winner_atom = workspace.load_state()?.atoms.into_iter().find(|a| a.id == winner_id).unwrap();
1007    if winner_atom.state != AtomState::Accepted {
1008        println!("Accepting winning atom {}...", winner_id);
1009        let res = workspace.transition_atom_state(
1010            &winner_id,
1011            AtomState::Accepted,
1012            args.reason.clone().unwrap_or_else(|| "Resolved via CLI".to_string()),
1013            actor.clone(),
1014        );
1015        if let Err(e) = res {
1016            println!("Warning: failed to accept winner {}: {}", winner_id, e);
1017        }
1018    }
1019
1020    println!("Conflict at {} resolved successfully. [{}] is the winner.", target_location, winner_id);
1021    Ok(())
1022}