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::{AtomEditRequest, 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    /// Start an operational lore session (signal + pre-write checkpoint)
42    SessionStart(SessionStartArgs),
43    /// Finish an operational lore session (validate + commit + sync + post-sync checkpoint + release)
44    SessionFinish(SessionFinishArgs),
45    /// Fetch active lore constraints and history for a file
46    Context(ContextArgs),
47    /// Propose a new Lore Atom
48    Propose(ProposeArgs),
49    /// Spawn the Model Context Protocol (MCP) server
50    Mcp(McpArgs),
51    /// Explain the rationale of code based on bordered lore
52    Explain(ExplainArgs),
53    /// Run CI logic checks over the workspace vs canon Lore
54    Validate(ValidateArgs),
55    /// Synchronize the hot-workspace with cold-storage
56    Sync(SyncArgs),
57    /// Strap Git-Lore into local Git hooks
58    Install(InstallArgs),
59    /// The underlying reconciliator invoked automatically by Git
60    Merge(MergeArgs),
61    /// Alter the lifecycle state of an existing Lore Atom
62    SetState(SetStateArgs),
63    /// Edit an existing Lore Atom in-place (metadata/trace)
64    EditAtom(EditAtomArgs),
65    /// Generates LLM integration instructions/skills (e.g. for GitHub Copilot)
66    Generate(GenerateArgs),
67    /// Interactively resolve active content contradictions at a specific location
68    Resolve(ResolveArgs),
69}
70
71#[derive(Args, Debug)]
72struct ResolveArgs {
73    /// The conflict location key (e.g. "path/to/file.rs::my_scope")
74    #[arg(value_name = "LOCATION")]
75    location: String,
76    /// Workspace path
77    #[arg(long, default_value = ".")]
78    path: PathBuf,
79    /// Pre-select the winning atom ID (bypasses interactive prompt)
80    #[arg(long)]
81    winner_id: Option<String>,
82    /// Optional reason for this resolution
83    #[arg(long)]
84    reason: Option<String>,
85}
86
87#[derive(Args, Debug)]
88struct GenerateArgs {
89    /// Target file to write integration skill/instruction to
90    #[arg(default_value = ".github/git-lore-skills.md")]
91    output: PathBuf,
92}
93
94#[derive(Args, Debug)]
95struct InitArgs {
96    /// Workspace path
97    #[arg(default_value = ".")]
98    path: PathBuf,
99}
100
101#[derive(Args, Debug)]
102struct MarkArgs {
103    /// The brief identifier or name of the rule
104    #[arg(long)]
105    title: String,
106    /// Explanatory text that provides context (the "Why")
107    #[arg(long)]
108    body: Option<String>,
109    /// The scope boundary, like a function name or class
110    #[arg(long)]
111    scope: Option<String>,
112    /// The target directory or file this rule binds to
113    #[arg(long)]
114    path: Option<PathBuf>,
115    /// A literal shell command that validates the atom when preflight runs
116    #[arg(long = "validation-script")]
117    validation_script: Option<String>,
118    /// The typology of the lore
119    #[arg(long, value_enum, default_value = "decision")]
120    kind: LoreKindArg,
121}
122
123#[derive(Args, Debug)]
124struct StatusArgs {
125    /// Workspace path
126    #[arg(default_value = ".")]
127    path: PathBuf,
128}
129
130#[derive(Args, Debug)]
131struct CheckpointArgs {
132    /// Workspace path
133    #[arg(default_value = ".")]
134    path: PathBuf,
135    /// Optional message outlining the checkpoint reason
136    #[arg(long)]
137    message: Option<String>,
138}
139
140#[derive(Args, Debug)]
141struct CommitArgs {
142    /// Workspace path
143    #[arg(default_value = ".")]
144    path: PathBuf,
145    /// Your commit message subject
146    #[arg(long)]
147    message: String,
148    /// Allow committing even if no files changed (lore changes only)
149    #[arg(long, default_value_t = true)]
150    allow_empty: bool,
151}
152
153#[derive(Args, Debug)]
154struct SignalArgs {
155    /// Workspace path
156    #[arg(default_value = ".")]
157    workspace: PathBuf,
158    /// Identifier for the active AI session (auto-generated if missing)
159    #[arg(long)]
160    session_id: Option<String>,
161    /// Release an existing PRISM signal for a session and exit
162    #[arg(long, alias = "clear", default_value_t = false)]
163    release: bool,
164    /// The name/identity of the emitting agent
165    #[arg(long)]
166    agent: Option<String>,
167    /// Target code scope
168    #[arg(long)]
169    scope: Option<String>,
170    /// The affected file(s) or directories globs
171    #[arg(long = "path", value_name = "GLOB")]
172    paths: Vec<String>,
173    /// The temporary assumptions running in memory
174    #[arg(long = "assumption")]
175    assumptions: Vec<String>,
176    /// A tentative brief goal or decision
177    #[arg(long)]
178    decision: Option<String>,
179}
180
181#[derive(Args, Debug)]
182struct SessionStartArgs {
183    /// Workspace path
184    #[arg(default_value = ".")]
185    workspace: PathBuf,
186    /// Identifier for the active session (auto-generated if missing)
187    #[arg(long)]
188    session_id: Option<String>,
189    /// The name/identity of the emitting agent
190    #[arg(long)]
191    agent: Option<String>,
192    /// Target code scope
193    #[arg(long)]
194    scope: Option<String>,
195    /// The affected file(s) or directories globs
196    #[arg(long = "path", value_name = "GLOB")]
197    paths: Vec<String>,
198    /// The temporary assumptions running in memory
199    #[arg(long = "assumption")]
200    assumptions: Vec<String>,
201    /// A tentative brief goal or decision
202    #[arg(long)]
203    decision: Option<String>,
204    /// Optional reason to include in the pre-write checkpoint message
205    #[arg(long)]
206    reason: Option<String>,
207    /// Optional explicit checkpoint message
208    #[arg(long = "checkpoint-message")]
209    checkpoint_message: Option<String>,
210}
211
212#[derive(Args, Debug)]
213struct SessionFinishArgs {
214    /// Workspace path
215    #[arg(default_value = ".")]
216    workspace: PathBuf,
217    /// Session identifier emitted by session-start
218    #[arg(long)]
219    session_id: String,
220    /// Your commit message subject
221    #[arg(long)]
222    message: String,
223    /// Allow committing even if no files changed (lore changes only)
224    #[arg(long, default_value_t = true)]
225    allow_empty: bool,
226    /// Optional owner name for the post-sync checkpoint message
227    #[arg(long)]
228    agent: Option<String>,
229    /// Optional reason to include in the post-sync checkpoint message
230    #[arg(long)]
231    reason: Option<String>,
232    /// Optional explicit checkpoint message
233    #[arg(long = "checkpoint-message")]
234    checkpoint_message: Option<String>,
235}
236
237#[derive(Args, Debug)]
238struct ContextArgs {
239    /// Workspace path
240    #[arg(default_value = ".")]
241    path: PathBuf,
242    /// The target script/file
243    #[arg(long)]
244    file: PathBuf,
245    /// Specific line number for tree-sitter drilling
246    #[arg(long)]
247    cursor_line: Option<usize>,
248}
249
250#[derive(Args, Debug)]
251struct ProposeArgs {
252    /// Workspace path
253    #[arg(default_value = ".")]
254    path: PathBuf,
255    /// The target script/file
256    #[arg(long)]
257    file: PathBuf,
258    /// The headline of the new rule
259    #[arg(long)]
260    title: String,
261    /// The context and reasoning body
262    #[arg(long)]
263    body: Option<String>,
264    /// A literal shell command that validates the atom when preflight runs
265    #[arg(long = "validation-script")]
266    validation_script: Option<String>,
267    /// Targeted line number
268    #[arg(long)]
269    cursor_line: Option<usize>,
270    /// Type of the proposed element
271    #[arg(long, value_enum, default_value = "decision")]
272    kind: LoreKindArg,
273}
274
275#[derive(Args, Debug)]
276struct McpArgs {
277    /// Workspace path
278    #[arg(default_value = ".")]
279    path: PathBuf,
280}
281
282#[derive(Args, Debug)]
283struct ExplainArgs {
284    /// Workspace path
285    #[arg(default_value = ".")]
286    path: PathBuf,
287    /// The target file
288    #[arg(long)]
289    file: PathBuf,
290    /// Specific line number for tree-sitter drilling
291    #[arg(long)]
292    cursor_line: Option<usize>,
293}
294
295#[derive(Args, Debug)]
296struct ValidateArgs {
297    /// Workspace path
298    #[arg(default_value = ".")]
299    path: PathBuf,
300}
301
302#[derive(Args, Debug)]
303struct SyncArgs {
304    /// Workspace path
305    #[arg(default_value = ".")]
306    path: PathBuf,
307}
308
309#[derive(Args, Debug)]
310struct InstallArgs {
311    /// Workspace path
312    #[arg(default_value = ".")]
313    path: PathBuf,
314}
315
316#[derive(Args, Debug)]
317struct MergeArgs {
318    /// Base commit/lore state
319    base: PathBuf,
320    /// Current commit/lore state
321    current: PathBuf,
322    /// Other branch commit/lore state
323    other: PathBuf,
324}
325
326#[derive(Args, Debug)]
327struct SetStateArgs {
328    /// Workspace path
329    #[arg(default_value = ".")]
330    path: PathBuf,
331    /// The ID of the atom to update
332    #[arg(long = "atom-id")]
333    atom_id: String,
334    /// The new state for the atom
335    #[arg(long, value_enum)]
336    state: AtomStateArg,
337    /// The reason for changing the state
338    #[arg(long)]
339    reason: String,
340    /// The actor making the change
341    #[arg(long)]
342    actor: Option<String>,
343}
344
345#[derive(Args, Debug)]
346struct EditAtomArgs {
347    /// Workspace path
348    #[arg(default_value = ".")]
349    path: PathBuf,
350    /// The ID of the atom to edit
351    #[arg(long = "atom-id")]
352    atom_id: String,
353    /// Optional new lore kind
354    #[arg(long, value_enum)]
355    kind: Option<LoreKindArg>,
356    /// Optional new title
357    #[arg(long)]
358    title: Option<String>,
359    /// Optional new body
360    #[arg(long)]
361    body: Option<String>,
362    /// Clear existing body
363    #[arg(long, default_value_t = false)]
364    clear_body: bool,
365    /// Optional new scope
366    #[arg(long)]
367    scope: Option<String>,
368    /// Clear existing scope
369    #[arg(long, default_value_t = false)]
370    clear_scope: bool,
371    /// Optional new atom path anchor
372    #[arg(long = "atom-path")]
373    atom_path: Option<PathBuf>,
374    /// Clear existing atom path anchor
375    #[arg(long, default_value_t = false)]
376    clear_atom_path: bool,
377    /// Optional new validation script command
378    #[arg(long = "validation-script")]
379    validation_script: Option<String>,
380    /// Clear existing validation script
381    #[arg(long, default_value_t = false)]
382    clear_validation_script: bool,
383    /// Set accepted trace commit SHA
384    #[arg(long = "trace-commit-sha")]
385    trace_commit_sha: Option<String>,
386    /// Clear accepted trace commit SHA
387    #[arg(long, default_value_t = false)]
388    clear_trace_commit: bool,
389    /// Required reason for audit logging
390    #[arg(long)]
391    reason: String,
392    /// The actor making the edit
393    #[arg(long)]
394    actor: Option<String>,
395}
396
397#[derive(Clone, Copy, Debug, ValueEnum)]
398enum LoreKindArg {
399    Decision,
400    Assumption,
401    OpenQuestion,
402    Signal,
403}
404
405#[derive(Clone, Copy, Debug, ValueEnum)]
406enum AtomStateArg {
407    Draft,
408    Proposed,
409    Accepted,
410    Deprecated,
411}
412
413pub fn run() -> Result<()> {
414    let cli = Cli::parse();
415
416    match cli.command {
417        Commands::Init(args) => init(args),
418        Commands::Mark(args) => mark(args),
419        Commands::Status(args) => status(args),
420        Commands::Checkpoint(args) => checkpoint(args),
421        Commands::Commit(args) => commit(args),
422        Commands::Signal(args) => signal(args),
423        Commands::SessionStart(args) => session_start(args),
424        Commands::SessionFinish(args) => session_finish(args),
425        Commands::Context(args) => context(args),
426        Commands::Propose(args) => propose(args),
427        Commands::Mcp(args) => mcp(args),
428        Commands::Explain(args) => explain(args),
429        Commands::Validate(args) => validate(args),
430        Commands::Sync(args) => sync(args),
431        Commands::Install(args) => install(args),
432        Commands::Merge(args) => merge(args),
433        Commands::SetState(args) => set_state(args),
434        Commands::EditAtom(args) => edit_atom(args),
435        Commands::Generate(args) => generate(args),
436        Commands::Resolve(args) => resolve(args),
437    }
438}
439
440fn generate(args: GenerateArgs) -> Result<()> {
441    if let Some(parent) = args.output.parent() {
442        if !parent.exists() {
443            std::fs::create_dir_all(parent).with_context(|| format!("failed to create dir: {}", parent.display()))?;
444        }
445    }
446    
447    let content = r#"---
448description: >-
449  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.
450---
451
452# Git-Lore Skills
453
454Keep architectural decisions and knowledge strongly bound to codebase states.
455
456**When to Use:**
457- When adding notes/assumptions explicitly requested by the user.
458- When a user asks "document this pattern for later", "mark this assumption", or "save this rule".
459- Upon discovering a consistent convention not currently documented in `.lore`.
460
461## Instructions
462
463<instructions>
464You 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.
465
466### 1. Discovering Lore (Context)
467When you navigate to a new file or need to understand how it should be implemented, read the context using:
468- **MCP Tool:** `git_lore_context` (pass the file path) or `git_lore_memory_search` (pass a query).
469- **CLI Alternative:** Tell the user to run `git-lore context --file <file>` or `git-lore explain --file <file>`.
470
471### 2. Recording Lore (Propose / Mark)
472When the user and you make an important architectural decision, or establish a convention that other AI agents should know:
473- **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.
474- **CLI Alternative:** Suggest the user run:
475  `git-lore mark --title "Your concise rule constraint" --body "The reason why this exists" --path "<relative_file_path>"`
476
477### 3. Git Workflows
478When 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"`.
479
480# Flujo de Trabajo: Git-Lore + Git
481
482**¿En qué etapa de desarrollo te encuentras?**
483
484## 1. Modificando código críptico o legado
485Necesitas arreglar un bug o extender un módulo, pero el código es confuso y no sabes qué romperás si lo cambias.
486
487**Flujo:**
488*   **Git:** Crear rama: `git checkout -b fix/module`
489*   **Git-Lore:** Obtener reglas: `git-lore context --file module.rs`
490
491> **¿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.
492
493## 2. Tomando decisiones arquitectónicas clave
494Estás liderando un nuevo feature y has decidido usar un patrón de diseño o herramienta específica para este módulo.
495
496**Flujo:**
497*   **Git:** Programar la lógica principal y hacer `git add .`
498*   **Git-Lore:** Marcar: `git-lore mark --kind decision --title "Usar Patrón Builder..."`
499*   **Integración:** Confirmar: `git-lore commit -m "feat: xyz"`
500
501> **¿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ó.
502
503## 3. Delegando código complejo a una IA (Copilot, Agentes)
504Le estás pidiendo a un Agente IA que genere un refactor masivo o construya un nuevo servicio desde tu editor (VS Code).
505
506**Flujo:**
507*   **MCP Server:** La IA pide contexto silenciosamente: `git_lore_context(scope)`
508*   **Desarrollo:** La IA genera código respetando las restricciones inyectadas.
509*   **Evolución:** La IA sugiere reglas: `git_lore_propose(...)`
510
511> **¿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.
512
513## 4. Revisión de un Pull Request
514Un colega sube su código para que lo apruebes y se funda con la rama principal.
515
516**Flujo:**
517*   **Git / CI:** Se levanta la Pull Request en GitHub/GitLab.
518*   **Git-Lore:** CI verifica o el humano ejecuta `git-lore validate`.
519
520> **¿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.
521
522## 5. Explorando la Memoria del Proyecto (Discovery)
523No 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.
524
525**Flujo:**
526*   **MCP Server:** La IA busca intenciones difusas: `git_lore_memory_search("auth architecture")`
527*   **Git-Lore:** Obtener justificación detallada: `git-lore explain --file src/auth.rs`
528
529> **¿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.
530
531## 6. Evolución del Conocimiento (Estado y Ciclo de Vida)
532El 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.
533
534**Flujo:**
535*   **MCP Server:** La IA sugiere cambios: `git_lore_propose(target_state="Proposed")`
536*   **Git-Lore:** El humano formaliza: `git-lore set-state --state accepted`
537*   **Git-Lore:** Las reglas viejas se retiran: `git-lore set-state --state deprecated`
538
539> **¿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.
540
541## 7. Flujos Activos Autoriales (Signals & Preflight)
542Agentes 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.
543
544**Flujo:**
545*   **Git-Lore:** Crear instantánea segura: `git-lore checkpoint / git-lore status`
546*   **MCP Server:** Validaciones de estado: `git_lore_memory_preflight("commit")`
547*   **Git-Lore:** Agentes emiten eventos cortos: `git-lore signal --agent "Codegen"`
548
549> **¿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.
550
551## 8. Congelando el Conocimiento (Checkpoints)
552Está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.
553
554**Flujo:**
555*   **Git-Lore:** Congelar el estado base: `git-lore checkpoint --message "Pre-refactor de auth"`
556*   **MCP Server:** Agentes IA validan checksums: `git_lore_state_snapshot()`
557*   **Integración:** Fallo rápido (Fail-fast) preventivo en caso de discrepancias temporales.
558
559> **¿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).
560
561## 9. Fusión y Reconciliación de Conocimiento (Merge)
562Trabajas 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.
563
564**Flujo:**
565*   **Git:** Comienza la fusión de archivos: `git merge feature/branch`
566*   **Git-Lore:** Git dispara el merge driver: `git-lore merge <base> <current> <other>`
567*   **Git-Lore:** Reconciliación: deduplica IDs, verifica estados (Ej. "Accepted" vence a "Proposed").
568
569> **¿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.
570
571## 10. Proposiciones y Señales Contextuales (Propose & Signal)
572Durante 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.
573
574**Flujo:**
575*   **Git-Lore:** Crear señal efímera: `git-lore signal --assumption "Asumo que la API devuelve XML" --path src/`
576*   **MCP Server:** Subagentes leen la señal: `git_lore_memory_search()` expone la suposición fresca.
577*   **Git-Lore:** Validación: `git-lore propose --title "API responde JSON" --kind decision` reemplaza la suposición.
578
579> **¿Cómo funciona el salto de Señal a Decisión internamente?**
580>
581> 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"*.
582> 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).
583> 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.
584</instructions>
585"#;
586    let mut file = std::fs::File::create(&args.output).context("failed to create output file")?;
587    file.write_all(content.as_bytes()).context("failed to write content")?;
588    
589    println!("Successfully generated Git-Lore skill at: {}", args.output.display());
590    Ok(())
591}
592
593fn init(args: InitArgs) -> Result<()> {
594    let workspace = Workspace::init(&args.path)
595        .with_context(|| format!("failed to initialize workspace at {}", args.path.display()))?;
596
597    println!(
598        "Initialized Git-Lore workspace at {}",
599        workspace.root().display()
600    );
601    Ok(())
602}
603
604fn mark(args: MarkArgs) -> Result<()> {
605    let workspace = Workspace::discover(".")?;
606    enforce_cli_write_guard(workspace.root(), "edit")?;
607
608    let atom = LoreAtom::new(
609        args.kind.into(),
610        AtomState::Proposed,
611        args.title,
612        args.body,
613        args.scope,
614        args.path,
615    )
616    .with_validation_script(args.validation_script);
617    let atom_id = atom.id.clone();
618
619    workspace.record_atom(atom)?;
620    println!("Recorded proposed lore atom {atom_id}");
621    Ok(())
622}
623
624fn status(args: StatusArgs) -> Result<()> {
625    let workspace = Workspace::discover(&args.path)?;
626    let state = workspace.load_state()?;
627    let report = workspace.entropy_report()?;
628
629    println!("Workspace: {}", workspace.root().display());
630    println!("Total atoms: {}", state.atoms.len());
631    println!("Entropy score: {}/100", report.score);
632
633    for atom in state.atoms.iter().rev().take(5) {
634        println!(
635            "- [{}] {:?} {:?}: {}",
636            atom.id, atom.kind, atom.state, atom.title
637        );
638    }
639
640    if report.contradictions.is_empty() {
641        println!("Contradictions: none");
642    } else {
643        println!("Contradictions:");
644        for contradiction in report.contradictions.iter().take(5) {
645            println!("- {:?} {}: {}", contradiction.kind, contradiction.key, contradiction.message);
646        }
647    }
648
649    if !report.notes.is_empty() {
650        println!("Entropy notes:");
651        for note in report.notes {
652            println!("- {note}");
653        }
654    }
655
656    Ok(())
657}
658
659fn checkpoint(args: CheckpointArgs) -> Result<()> {
660    let workspace = Workspace::discover(&args.path)?;
661    enforce_cli_write_guard(workspace.root(), "commit")?;
662
663    let checkpoint = workspace.write_checkpoint(args.message)?;
664    let subject = checkpoint
665        .message
666        .as_deref()
667        .unwrap_or("git-lore checkpoint");
668    let commit_message = git::build_commit_message(subject, &checkpoint.atoms);
669
670    if let Ok(repository) = git::discover_repository(&workspace.root()) {
671        println!("Git repository: {}", git::repository_root(&repository).display());
672    }
673
674    println!("Checkpoint {} written", checkpoint.id);
675    if !commit_message.is_empty() {
676        println!();
677        println!("{commit_message}");
678    }
679
680    Ok(())
681}
682
683fn commit(args: CommitArgs) -> Result<()> {
684    let workspace = Workspace::discover(&args.path)?;
685    enforce_cli_write_guard(workspace.root(), "commit")?;
686
687    let hash = run_lore_commit(&workspace, &args.message, args.allow_empty)?;
688
689    println!("Committed lore checkpoint {}", hash);
690    Ok(())
691}
692
693fn run_lore_commit(workspace: &Workspace, message: impl AsRef<str>, allow_empty: bool) -> Result<String> {
694    let repository_root = git::discover_repository(workspace.root())?;
695    let state = workspace.load_state()?;
696
697    let validation_issues = git::validate_workspace_against_git(&repository_root, workspace)?;
698    if !validation_issues.is_empty() {
699        anyhow::bail!(
700            "validation failed: {}",
701            validation_issues.join("; ")
702        );
703    }
704
705    let commit_message = git::build_commit_message(message, &state.atoms);
706
707    let hash = git::commit_lore_message(&repository_root, commit_message, allow_empty)?;
708    workspace.accept_active_atoms(Some(&hash))?;
709
710    for atom in state.atoms.iter().filter(|atom| atom.state != AtomState::Deprecated) {
711        git::write_lore_ref(&repository_root, atom, &hash)?;
712    }
713
714    Ok(hash)
715}
716
717fn session_start(args: SessionStartArgs) -> Result<()> {
718    let workspace = Workspace::discover(&args.workspace)?;
719    enforce_cli_signal_guard(workspace.root())?;
720
721    let pruned_stale = workspace.prune_stale_prism_signals(PRISM_STALE_TTL_SECONDS)?;
722    if pruned_stale > 0 {
723        println!("Pruned {pruned_stale} stale PRISM signal(s) before starting session");
724    }
725
726    let mut paths = args.paths;
727    if paths.is_empty() {
728        paths.push(".".to_string());
729    }
730
731    let signal = PrismSignal::new(
732        args.session_id.unwrap_or_else(|| Uuid::new_v4().to_string()),
733        args.agent,
734        args.scope,
735        paths,
736        args.assumptions,
737        args.decision,
738    );
739
740    workspace.write_prism_signal(&signal)?;
741    let conflicts = workspace.scan_prism_conflicts(&signal)?;
742
743    enforce_cli_write_guard(workspace.root(), "commit")?;
744    let checkpoint_message = args.checkpoint_message.unwrap_or_else(|| {
745        build_session_checkpoint_message(
746            "pre-write",
747            &signal.session_id,
748            signal.agent.as_deref(),
749            args.reason.as_deref(),
750            None,
751        )
752    });
753    let checkpoint = workspace.write_checkpoint(Some(checkpoint_message))?;
754
755    println!("Session started: {}", signal.session_id);
756    println!("Pre-write checkpoint: {}", checkpoint.id);
757
758    if conflicts.is_empty() {
759        println!("No soft-lock conflicts detected.");
760    } else {
761        println!("Soft-lock warnings:");
762        for conflict in conflicts {
763            let agent = conflict.agent.as_deref().unwrap_or("unknown-agent");
764            let scope = conflict.scope.as_deref().unwrap_or("unknown-scope");
765            let decision = conflict.decision.as_deref().unwrap_or("no decision recorded");
766            println!(
767                "- session {} ({agent}, {scope}) overlaps on {}: {decision}",
768                conflict.session_id,
769                conflict.overlapping_paths.join(", "),
770            );
771        }
772    }
773
774    println!(
775        "Next: run propose/mark updates, then session-finish with --session-id {}",
776        signal.session_id
777    );
778
779    Ok(())
780}
781
782fn session_finish(args: SessionFinishArgs) -> Result<()> {
783    let workspace = Workspace::discover(&args.workspace)?;
784    enforce_cli_write_guard(workspace.root(), "commit")?;
785
786    let signal_owner = workspace
787        .load_prism_signals()?
788        .into_iter()
789        .find(|signal| signal.session_id == args.session_id)
790        .and_then(|signal| signal.agent);
791
792    let hash = run_lore_commit(&workspace, &args.message, args.allow_empty)?;
793    let repository_root = git::discover_repository(workspace.root())?;
794
795    enforce_cli_write_guard(workspace.root(), "sync")?;
796    let atoms = git::sync_workspace_from_git_history(&repository_root, &workspace)?;
797
798    let checkpoint_owner = args.agent.or(signal_owner);
799    let post_checkpoint_message = args.checkpoint_message.unwrap_or_else(|| {
800        build_session_checkpoint_message(
801            "post-sync",
802            &args.session_id,
803            checkpoint_owner.as_deref(),
804            args.reason.as_deref(),
805            Some(&hash),
806        )
807    });
808    let checkpoint = workspace.write_checkpoint(Some(post_checkpoint_message))?;
809
810    let released = workspace.remove_prism_signal(&args.session_id)?;
811
812    println!("Committed lore checkpoint {}", hash);
813    println!("Synchronized {} lore atoms from Git history", atoms.len());
814    println!("Post-sync checkpoint: {}", checkpoint.id);
815    if released {
816        println!("Released PRISM signal {}", args.session_id);
817    } else {
818        println!("No PRISM signal found for session {}", args.session_id);
819    }
820
821    Ok(())
822}
823
824fn build_session_checkpoint_message(
825    stage: &str,
826    session_id: &str,
827    owner: Option<&str>,
828    reason: Option<&str>,
829    commit_sha: Option<&str>,
830) -> String {
831    let owner = owner
832        .map(str::trim)
833        .filter(|value| !value.is_empty())
834        .unwrap_or("unknown");
835
836    let mut parts = vec![
837        format!("owner={owner}"),
838        format!("session={session_id}"),
839        format!("stage={stage}"),
840    ];
841
842    if let Some(reason) = reason.map(str::trim).filter(|value| !value.is_empty()) {
843        parts.push(format!("reason={reason}"));
844    }
845
846    if let Some(commit_sha) = commit_sha
847        .map(str::trim)
848        .filter(|value| !value.is_empty())
849    {
850        parts.push(format!("commit={commit_sha}"));
851    }
852
853    parts.join("; ")
854}
855fn signal(args: SignalArgs) -> Result<()> {
856    let workspace = Workspace::discover(&args.workspace)?;
857
858    if args.release {
859        let session_id = args
860            .session_id
861            .as_deref()
862            .ok_or_else(|| anyhow::anyhow!("--session-id is required when using --release"))?;
863
864        let removed = workspace.remove_prism_signal(session_id)?;
865        if removed {
866            println!("Released PRISM signal {session_id}");
867        } else {
868            println!("No PRISM signal found for session {session_id}");
869        }
870        return Ok(());
871    }
872
873    if args.paths.is_empty() {
874        anyhow::bail!("at least one --path glob is required to broadcast a PRISM signal");
875    }
876
877    enforce_cli_signal_guard(workspace.root())?;
878
879    let pruned_stale = workspace.prune_stale_prism_signals(PRISM_STALE_TTL_SECONDS)?;
880    if pruned_stale > 0 {
881        println!("Pruned {pruned_stale} stale PRISM signal(s) before broadcasting");
882    }
883
884    let signal = PrismSignal::new(
885        args.session_id.unwrap_or_else(|| Uuid::new_v4().to_string()),
886        args.agent,
887        args.scope,
888        args.paths,
889        args.assumptions,
890        args.decision,
891    );
892
893    workspace.write_prism_signal(&signal)?;
894    let conflicts = workspace.scan_prism_conflicts(&signal)?;
895
896    println!("Broadcast PRISM signal {}", signal.session_id);
897
898    if conflicts.is_empty() {
899        println!("No soft-lock conflicts detected.");
900        return Ok(());
901    }
902
903    println!("Soft-lock warnings:");
904    for conflict in conflicts {
905        let agent = conflict.agent.as_deref().unwrap_or("unknown-agent");
906        let scope = conflict.scope.as_deref().unwrap_or("unknown-scope");
907        let decision = conflict.decision.as_deref().unwrap_or("no decision recorded");
908        println!(
909            "- session {} ({agent}, {scope}) overlaps on {}: {decision}",
910            conflict.session_id,
911            conflict.overlapping_paths.join(", "),
912        );
913    }
914
915    Ok(())
916}
917
918fn context(args: ContextArgs) -> Result<()> {
919    let service = McpService::new(&args.path);
920    let snapshot = service.context(&args.file, args.cursor_line)?;
921
922    println!("Workspace: {}", snapshot.workspace_root.display());
923    println!("File: {}", snapshot.file_path.display());
924
925    if let Some(scope) = snapshot.scope {
926        println!("Scope: {} {} ({}-{})", scope.kind_label(), scope.name, scope.start_line, scope.end_line);
927    }
928
929    if snapshot.constraints.is_empty() {
930        println!("No matching lore constraints.");
931    } else {
932        println!("Constraints:");
933        for constraint in snapshot.constraints {
934            println!("- {constraint}");
935        }
936    }
937
938    Ok(())
939}
940
941fn propose(args: ProposeArgs) -> Result<()> {
942    let service = McpService::new(&args.path);
943    enforce_cli_write_guard(&args.path, "edit")?;
944
945    let result = service.propose(ProposalRequest {
946        file_path: args.file,
947        cursor_line: args.cursor_line,
948        kind: args.kind.into(),
949        title: args.title,
950        body: args.body,
951        scope: None,
952        validation_script: args.validation_script,
953    })?;
954
955    println!("Proposed lore atom {}", result.atom.id);
956    if let Some(scope) = result.scope {
957        println!("Scope: {} {} ({}-{})", scope.kind_label(), scope.name, scope.start_line, scope.end_line);
958    }
959
960    Ok(())
961}
962
963fn mcp(args: McpArgs) -> Result<()> {
964    let server = crate::mcp::McpServer::new(&args.path);
965    server.run_stdio()
966}
967
968fn explain(args: ExplainArgs) -> Result<()> {
969    let service = McpService::new(&args.path);
970    let snapshot = service.context(&args.file, args.cursor_line)?;
971
972    println!("Workspace: {}", snapshot.workspace_root.display());
973    println!("File: {}", snapshot.file_path.display());
974
975    if let Some(scope) = snapshot.scope {
976        println!("Scope: {} {} ({}-{})", scope.kind_label(), scope.name, scope.start_line, scope.end_line);
977    }
978
979    if snapshot.historical_decisions.is_empty() {
980        println!("Historical decisions: none");
981    } else {
982        println!("Historical decisions:");
983        for decision in snapshot.historical_decisions {
984            println!("- {} {}", decision.commit_hash, decision.trailer_value);
985        }
986    }
987
988    if snapshot.constraints.is_empty() {
989        println!("No matching constraints.");
990    } else {
991        println!("Constraints:");
992        for constraint in snapshot.constraints {
993            println!("- {constraint}");
994        }
995    }
996
997    Ok(())
998}
999
1000fn validate(args: ValidateArgs) -> Result<()> {
1001    let workspace = Workspace::discover(&args.path)?;
1002    let repository_root = git::discover_repository(workspace.root())?;
1003    let issues = git::validate_workspace_against_git(&repository_root, &workspace)?;
1004
1005    if issues.is_empty() {
1006        println!("Validation passed");
1007        return Ok(());
1008    }
1009
1010    println!("Validation issues:");
1011    for issue in issues {
1012        println!("- {issue}");
1013    }
1014
1015    anyhow::bail!("validation failed");
1016}
1017
1018fn sync(args: SyncArgs) -> Result<()> {
1019    let workspace = Workspace::discover(&args.path)?;
1020    enforce_cli_write_guard(workspace.root(), "sync")?;
1021
1022    let repository_root = git::discover_repository(workspace.root())?;
1023    let atoms = git::sync_workspace_from_git_history(&repository_root, &workspace)?;
1024
1025    #[cfg(feature = "semantic-search")]
1026    {
1027        let state = workspace.load_state()?;
1028        let accepted = workspace.load_accepted_atoms()?;
1029        crate::mcp::semantic::rebuild_index(workspace.root(), &state.atoms, &accepted)?;
1030        println!("Rebuilt Memvid semantic local index.");
1031    }
1032
1033    println!("Synchronized {} lore atoms from Git history", atoms.len());
1034    Ok(())
1035}
1036
1037fn install(args: InstallArgs) -> Result<()> {
1038    let workspace = Workspace::discover(&args.path)?;
1039    let repository_root = git::discover_repository(workspace.root())?;
1040    git::install_git_lore_integration(&repository_root)?;
1041
1042    println!("Installed Git-Lore hooks and merge driver in {}", repository_root.display());
1043    Ok(())
1044}
1045
1046fn merge(args: MergeArgs) -> Result<()> {
1047    let base = read_workspace_state_file(&args.base)
1048        .with_context(|| format!("failed to read base merge file {}", args.base.display()))?;
1049    let current = read_workspace_state_file(&args.current)
1050        .with_context(|| format!("failed to read current merge file {}", args.current.display()))?;
1051    let other = read_workspace_state_file(&args.other)
1052        .with_context(|| format!("failed to read other merge file {}", args.other.display()))?;
1053
1054    let merged_version = base
1055        .state
1056        .version
1057        .max(current.state.version)
1058        .max(other.state.version);
1059    let outcome = crate::lore::merge::reconcile_lore(&base.state, &current.state, &other.state);
1060    let merged_state = WorkspaceState {
1061        version: merged_version,
1062        atoms: outcome.merged,
1063    };
1064
1065    let write_gzip = base.was_gzip || current.was_gzip || other.was_gzip;
1066    write_workspace_state_file(&args.current, &merged_state, write_gzip)
1067        .with_context(|| format!("failed to write merged lore file {}", args.current.display()))?;
1068
1069    if outcome.conflicts.is_empty() {
1070        println!("Merged lore state with {} atom(s)", merged_state.atoms.len());
1071        return Ok(());
1072    }
1073
1074    eprintln!(
1075        "Lore merge produced {} conflict(s); manual review required",
1076        outcome.conflicts.len()
1077    );
1078    for conflict in outcome.conflicts {
1079        eprintln!("- {:?} {}: {}", conflict.kind, conflict.key, conflict.message);
1080    }
1081
1082    anyhow::bail!("lore merge requires manual resolution")
1083}
1084
1085fn set_state(args: SetStateArgs) -> Result<()> {
1086    let workspace = Workspace::discover(&args.path)?;
1087    enforce_cli_write_guard(workspace.root(), "edit")?;
1088
1089    let actor = args.actor.or_else(|| std::env::var("USER").ok());
1090    let updated = workspace.transition_atom_state(
1091        &args.atom_id,
1092        args.state.into(),
1093        args.reason,
1094        actor,
1095    )?;
1096
1097    println!(
1098        "Transitioned lore atom {} to {:?}",
1099        updated.id, updated.state
1100    );
1101    Ok(())
1102}
1103
1104fn edit_atom(args: EditAtomArgs) -> Result<()> {
1105    let workspace = Workspace::discover(&args.path)?;
1106    enforce_cli_write_guard(workspace.root(), "edit")?;
1107
1108    if args.body.is_some() && args.clear_body {
1109        anyhow::bail!("cannot use --body and --clear-body together");
1110    }
1111    if args.scope.is_some() && args.clear_scope {
1112        anyhow::bail!("cannot use --scope and --clear-scope together");
1113    }
1114    if args.atom_path.is_some() && args.clear_atom_path {
1115        anyhow::bail!("cannot use --atom-path and --clear-atom-path together");
1116    }
1117    if args.validation_script.is_some() && args.clear_validation_script {
1118        anyhow::bail!("cannot use --validation-script and --clear-validation-script together");
1119    }
1120    if args.trace_commit_sha.is_some() && args.clear_trace_commit {
1121        anyhow::bail!("cannot use --trace-commit-sha and --clear-trace-commit together");
1122    }
1123
1124    let body_update = if args.clear_body {
1125        Some(None)
1126    } else {
1127        args.body.map(Some)
1128    };
1129    let scope_update = if args.clear_scope {
1130        Some(None)
1131    } else {
1132        args.scope.map(Some)
1133    };
1134    let path_update = if args.clear_atom_path {
1135        Some(None)
1136    } else {
1137        args.atom_path.map(Some)
1138    };
1139    let validation_script_update = if args.clear_validation_script {
1140        Some(None)
1141    } else {
1142        args.validation_script.map(Some)
1143    };
1144    let trace_commit_update = if args.clear_trace_commit {
1145        Some(None)
1146    } else {
1147        args.trace_commit_sha.map(Some)
1148    };
1149
1150    let actor = args.actor.or_else(|| std::env::var("USER").ok());
1151    let result = workspace.edit_atom(
1152        &args.atom_id,
1153        AtomEditRequest {
1154            kind: args.kind.map(Into::into),
1155            title: args.title,
1156            body: body_update,
1157            scope: scope_update,
1158            path: path_update,
1159            validation_script: validation_script_update,
1160            trace_commit_sha: trace_commit_update,
1161        },
1162        args.reason,
1163        actor,
1164    )?;
1165
1166    if result.changed_fields.is_empty() {
1167        println!("No changes applied to lore atom {}", result.atom.id);
1168        return Ok(());
1169    }
1170
1171    println!(
1172        "Edited lore atom {} in-place ({})",
1173        result.atom.id,
1174        result.changed_fields.join(", "),
1175    );
1176    if let Some(source_commit) = result.source_commit {
1177        println!("Trace commit: {source_commit}");
1178    }
1179
1180    Ok(())
1181}
1182
1183impl From<LoreKindArg> for LoreKind {
1184    fn from(value: LoreKindArg) -> Self {
1185        match value {
1186            LoreKindArg::Decision => LoreKind::Decision,
1187            LoreKindArg::Assumption => LoreKind::Assumption,
1188            LoreKindArg::OpenQuestion => LoreKind::OpenQuestion,
1189            LoreKindArg::Signal => LoreKind::Signal,
1190        }
1191    }
1192}
1193
1194impl From<AtomStateArg> for AtomState {
1195    fn from(value: AtomStateArg) -> Self {
1196        match value {
1197            AtomStateArg::Draft => AtomState::Draft,
1198            AtomStateArg::Proposed => AtomState::Proposed,
1199            AtomStateArg::Accepted => AtomState::Accepted,
1200            AtomStateArg::Deprecated => AtomState::Deprecated,
1201        }
1202    }
1203}
1204
1205#[derive(Clone, Debug)]
1206struct EncodedWorkspaceState {
1207    state: WorkspaceState,
1208    was_gzip: bool,
1209}
1210
1211fn read_workspace_state_file(path: &std::path::Path) -> Result<EncodedWorkspaceState> {
1212    let bytes = std::fs::read(path)
1213        .with_context(|| format!("failed to read lore state file {}", path.display()))?;
1214    let is_gzip = bytes.starts_with(&[0x1f, 0x8b]);
1215
1216    let content = if is_gzip {
1217        let mut decoder = GzDecoder::new(bytes.as_slice());
1218        let mut decoded = Vec::new();
1219        decoder
1220            .read_to_end(&mut decoded)
1221            .with_context(|| format!("failed to decompress lore state file {}", path.display()))?;
1222        decoded
1223    } else {
1224        bytes
1225    };
1226
1227    let state: WorkspaceState = serde_json::from_slice(&content)
1228        .with_context(|| format!("failed to parse lore state file {}", path.display()))?;
1229
1230    Ok(EncodedWorkspaceState {
1231        state,
1232        was_gzip: is_gzip,
1233    })
1234}
1235
1236fn write_workspace_state_file(
1237    path: &std::path::Path,
1238    state: &WorkspaceState,
1239    write_gzip: bool,
1240) -> Result<()> {
1241    let encoded = serde_json::to_vec_pretty(state)
1242        .with_context(|| format!("failed to encode merged lore state {}", path.display()))?;
1243
1244    let bytes = if write_gzip {
1245        let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
1246        encoder
1247            .write_all(&encoded)
1248            .with_context(|| format!("failed to gzip lore state {}", path.display()))?;
1249        encoder
1250            .finish()
1251            .with_context(|| format!("failed to finalize gzip lore state {}", path.display()))?
1252    } else {
1253        encoded
1254    };
1255
1256    std::fs::write(path, bytes)
1257        .with_context(|| format!("failed to write lore state file {}", path.display()))?;
1258
1259    Ok(())
1260}
1261
1262fn enforce_cli_write_guard(path: impl AsRef<std::path::Path>, operation: &str) -> Result<()> {
1263    let service = McpService::new(path);
1264    let snapshot = service.state_snapshot()?;
1265    let report = service.memory_preflight(operation)?;
1266
1267    if snapshot.state_checksum != report.state_checksum {
1268        anyhow::bail!(
1269            "state-first guard failed: state drift detected during preflight (snapshot {}, preflight {})",
1270            snapshot.state_checksum,
1271            report.state_checksum
1272        );
1273    }
1274
1275    if report
1276        .issues
1277        .iter()
1278        .any(|issue| issue.severity == PreflightSeverity::Block)
1279    {
1280        println!("Preflight issues:");
1281        for issue in report.issues {
1282            println!("- {:?} {}: {}", issue.severity, issue.code, issue.message);
1283        }
1284        anyhow::bail!(
1285            "state-first preflight blocked {} operation; resolve issues and retry",
1286            operation
1287        );
1288    }
1289
1290    for issue in report
1291        .issues
1292        .iter()
1293        .filter(|issue| issue.severity != PreflightSeverity::Info)
1294    {
1295        println!("Preflight {:?} {}: {}", issue.severity, issue.code, issue.message);
1296    }
1297
1298    Ok(())
1299}
1300
1301fn enforce_cli_signal_guard(path: impl AsRef<std::path::Path>) -> Result<()> {
1302    let service = McpService::new(path);
1303    let snapshot = service.state_snapshot()?;
1304    let report = service.memory_preflight("edit")?;
1305
1306    if snapshot.state_checksum != report.state_checksum {
1307        anyhow::bail!(
1308            "state-first guard failed: state drift detected during preflight (snapshot {}, preflight {})",
1309            snapshot.state_checksum,
1310            report.state_checksum
1311        );
1312    }
1313
1314    let blocking_issues = report
1315        .issues
1316        .iter()
1317        .filter(|issue| issue.severity == PreflightSeverity::Block)
1318        .filter(|issue| issue.code != "prism_hard_lock")
1319        .collect::<Vec<_>>();
1320
1321    if !blocking_issues.is_empty() {
1322        println!("Preflight issues:");
1323        for issue in report.issues {
1324            println!("- {:?} {}: {}", issue.severity, issue.code, issue.message);
1325        }
1326        anyhow::bail!(
1327            "state-first preflight blocked signal operation; resolve issues and retry"
1328        );
1329    }
1330
1331    for issue in report
1332        .issues
1333        .iter()
1334        .filter(|issue| issue.code != "prism_hard_lock")
1335        .filter(|issue| issue.severity != PreflightSeverity::Info)
1336    {
1337        println!("Preflight {:?} {}: {}", issue.severity, issue.code, issue.message);
1338    }
1339
1340    Ok(())
1341}
1342
1343fn resolve(args: ResolveArgs) -> Result<()> {
1344    let workspace = Workspace::discover(&args.path)?;
1345    enforce_cli_write_guard(workspace.root(), "edit")?;
1346
1347    let state = workspace.load_state()?;
1348    let target_location = args.location.clone();
1349    
1350    let candidate_atoms: Vec<LoreAtom> = state.atoms.into_iter()
1351        .filter(|atom| {
1352            let p = atom.path.as_ref().map(|v| v.to_string_lossy().replace('\\', "/")).unwrap_or_else(|| "<no-path>".to_string());
1353            let s = atom.scope.as_deref().unwrap_or("<no-scope>");
1354            format!("{}::{}", p, s) == target_location && atom.state != AtomState::Deprecated
1355        })
1356        .collect();
1357
1358    if candidate_atoms.is_empty() {
1359        println!("No active contradictions found at location {target_location}");
1360        return Ok(());
1361    }
1362
1363    let winner_id = if let Some(id) = args.winner_id {
1364        if !candidate_atoms.iter().any(|a| a.id == id) {
1365            anyhow::bail!("Atom ID {} not found among active atoms at location {}", id, target_location);
1366        }
1367        id
1368    } else {
1369        println!("Found {} active atoms at {}:", candidate_atoms.len(), target_location);
1370        for (i, atom) in candidate_atoms.iter().enumerate() {
1371            println!("[{}] {} ({:?}) - {}", i, atom.id, atom.state, atom.title);
1372        }
1373        
1374        let idx = loop {
1375            print!("Select the winning atom index (0-{}): ", candidate_atoms.len() - 1);
1376            std::io::stdout().flush()?;
1377            let mut input = String::new();
1378            std::io::stdin().read_line(&mut input)?;
1379            if let Ok(idx) = input.trim().parse::<usize>() {
1380                if idx < candidate_atoms.len() {
1381                    break idx;
1382                }
1383            }
1384            println!("Invalid selection.");
1385        };
1386        candidate_atoms[idx].id.clone()
1387    };
1388
1389    let actor = std::env::var("USER").ok();
1390    
1391    for atom in candidate_atoms {
1392        if atom.id != winner_id {
1393            // Note: workspace.transition_atom_state requires (id, target_state, reason, actor)
1394            println!("Deprecating lost atom {}...", atom.id);
1395            let res = workspace.transition_atom_state(
1396                &atom.id,
1397                AtomState::Deprecated,
1398                args.reason.clone().unwrap_or_else(|| "Resolved via CLI".to_string()),
1399                actor.clone(),
1400            );
1401            if let Err(e) = res {
1402                println!("Warning: failed to deprecate {}: {}", atom.id, e);
1403            }
1404        }
1405    }
1406    
1407    // Attempt to accept the winner if it isn't accepted yet
1408    let winner_atom = workspace.load_state()?.atoms.into_iter().find(|a| a.id == winner_id).unwrap();
1409    if winner_atom.state != AtomState::Accepted {
1410        println!("Accepting winning atom {}...", winner_id);
1411        let res = workspace.transition_atom_state(
1412            &winner_id,
1413            AtomState::Accepted,
1414            args.reason.clone().unwrap_or_else(|| "Resolved via CLI".to_string()),
1415            actor.clone(),
1416        );
1417        if let Err(e) = res {
1418            println!("Warning: failed to accept winner {}: {}", winner_id, e);
1419        }
1420    }
1421
1422    println!("Conflict at {} resolved successfully. [{}] is the winner.", target_location, winner_id);
1423    Ok(())
1424}
1425
1426#[cfg(test)]
1427mod tests {
1428    use super::build_session_checkpoint_message;
1429
1430    #[test]
1431    fn session_checkpoint_message_includes_required_fields() {
1432        let message = build_session_checkpoint_message(
1433            "pre-write",
1434            "session-123",
1435            Some("agent-x"),
1436            Some("capture evidence"),
1437            None,
1438        );
1439
1440        assert!(message.contains("owner=agent-x"));
1441        assert!(message.contains("session=session-123"));
1442        assert!(message.contains("stage=pre-write"));
1443        assert!(message.contains("reason=capture evidence"));
1444        assert!(!message.contains("commit="));
1445    }
1446
1447    #[test]
1448    fn session_checkpoint_message_includes_commit_when_present() {
1449        let message = build_session_checkpoint_message(
1450            "post-sync",
1451            "session-123",
1452            None,
1453            None,
1454            Some("abc123"),
1455        );
1456
1457        assert!(message.contains("owner=unknown"));
1458        assert!(message.contains("stage=post-sync"));
1459        assert!(message.contains("commit=abc123"));
1460    }
1461}