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 Init(InitArgs),
31 Mark(MarkArgs),
33 Status(StatusArgs),
35 Checkpoint(CheckpointArgs),
37 Commit(CommitArgs),
39 Signal(SignalArgs),
41 Context(ContextArgs),
43 Propose(ProposeArgs),
45 Mcp(McpArgs),
47 Explain(ExplainArgs),
49 Validate(ValidateArgs),
51 Sync(SyncArgs),
53 Install(InstallArgs),
55 Merge(MergeArgs),
57 SetState(SetStateArgs),
59 Generate(GenerateArgs),
61 Resolve(ResolveArgs),
63}
64
65#[derive(Args, Debug)]
66struct ResolveArgs {
67 #[arg(value_name = "LOCATION")]
69 location: String,
70 #[arg(long, default_value = ".")]
72 path: PathBuf,
73 #[arg(long)]
75 winner_id: Option<String>,
76 #[arg(long)]
78 reason: Option<String>,
79}
80
81#[derive(Args, Debug)]
82struct GenerateArgs {
83 #[arg(default_value = ".github/git-lore-skills.md")]
85 output: PathBuf,
86}
87
88#[derive(Args, Debug)]
89struct InitArgs {
90 #[arg(default_value = ".")]
92 path: PathBuf,
93}
94
95#[derive(Args, Debug)]
96struct MarkArgs {
97 #[arg(long)]
99 title: String,
100 #[arg(long)]
102 body: Option<String>,
103 #[arg(long)]
105 scope: Option<String>,
106 #[arg(long)]
108 path: Option<PathBuf>,
109 #[arg(long = "validation-script")]
111 validation_script: Option<String>,
112 #[arg(long, value_enum, default_value = "decision")]
114 kind: LoreKindArg,
115}
116
117#[derive(Args, Debug)]
118struct StatusArgs {
119 #[arg(default_value = ".")]
121 path: PathBuf,
122}
123
124#[derive(Args, Debug)]
125struct CheckpointArgs {
126 #[arg(default_value = ".")]
128 path: PathBuf,
129 #[arg(long)]
131 message: Option<String>,
132}
133
134#[derive(Args, Debug)]
135struct CommitArgs {
136 #[arg(default_value = ".")]
138 path: PathBuf,
139 #[arg(long)]
141 message: String,
142 #[arg(long, default_value_t = true)]
144 allow_empty: bool,
145}
146
147#[derive(Args, Debug)]
148struct SignalArgs {
149 #[arg(default_value = ".")]
151 workspace: PathBuf,
152 #[arg(long)]
154 session_id: Option<String>,
155 #[arg(long)]
157 agent: Option<String>,
158 #[arg(long)]
160 scope: Option<String>,
161 #[arg(long = "path", value_name = "GLOB")]
163 paths: Vec<String>,
164 #[arg(long = "assumption")]
166 assumptions: Vec<String>,
167 #[arg(long)]
169 decision: Option<String>,
170}
171
172#[derive(Args, Debug)]
173struct ContextArgs {
174 #[arg(default_value = ".")]
176 path: PathBuf,
177 #[arg(long)]
179 file: PathBuf,
180 #[arg(long)]
182 cursor_line: Option<usize>,
183}
184
185#[derive(Args, Debug)]
186struct ProposeArgs {
187 #[arg(default_value = ".")]
189 path: PathBuf,
190 #[arg(long)]
192 file: PathBuf,
193 #[arg(long)]
195 title: String,
196 #[arg(long)]
198 body: Option<String>,
199 #[arg(long = "validation-script")]
201 validation_script: Option<String>,
202 #[arg(long)]
204 cursor_line: Option<usize>,
205 #[arg(long, value_enum, default_value = "decision")]
207 kind: LoreKindArg,
208}
209
210#[derive(Args, Debug)]
211struct McpArgs {
212 #[arg(default_value = ".")]
214 path: PathBuf,
215}
216
217#[derive(Args, Debug)]
218struct ExplainArgs {
219 #[arg(default_value = ".")]
221 path: PathBuf,
222 #[arg(long)]
224 file: PathBuf,
225 #[arg(long)]
227 cursor_line: Option<usize>,
228}
229
230#[derive(Args, Debug)]
231struct ValidateArgs {
232 #[arg(default_value = ".")]
234 path: PathBuf,
235}
236
237#[derive(Args, Debug)]
238struct SyncArgs {
239 #[arg(default_value = ".")]
241 path: PathBuf,
242}
243
244#[derive(Args, Debug)]
245struct InstallArgs {
246 #[arg(default_value = ".")]
248 path: PathBuf,
249}
250
251#[derive(Args, Debug)]
252struct MergeArgs {
253 base: PathBuf,
255 current: PathBuf,
257 other: PathBuf,
259}
260
261#[derive(Args, Debug)]
262struct SetStateArgs {
263 #[arg(default_value = ".")]
265 path: PathBuf,
266 #[arg(long = "atom-id")]
268 atom_id: String,
269 #[arg(long, value_enum)]
271 state: AtomStateArg,
272 #[arg(long)]
274 reason: String,
275 #[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, ¤t.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 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 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}