Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions crates/terraphim_agent/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,11 @@ enum Command {
#[arg(long, default_value_t = false)]
pinned: bool,
},
/// Manage knowledge graph entries
Kg {
#[command(subcommand)]
sub: KgSub,
},
/// Chat with the AI using a specific role
#[cfg(feature = "llm")]
Chat {
Expand Down Expand Up @@ -1204,6 +1209,20 @@ enum ConfigSub {
Reload,
}

#[derive(Subcommand, Debug)]
enum KgSub {
/// List knowledge graph entries
List {
#[arg(long)]
role: Option<String>,
#[arg(long, default_value_t = 50)]
top_k: usize,
/// Show only pinned entries
#[arg(long, default_value_t = false)]
pinned: bool,
},
}

/// Get the session cache file path
#[cfg(feature = "repl-sessions")]
fn get_session_cache_path() -> std::path::PathBuf {
Expand Down Expand Up @@ -2264,6 +2283,28 @@ async fn run_offline_command(
}
Ok(())
}
Command::Kg { sub } => match sub {
KgSub::List {
role,
top_k,
pinned,
} => {
let role_name = service.resolve_role(role.as_deref()).await?;

if pinned {
let pinned_concepts = service.get_role_graph_pinned(&role_name).await?;
for concept in pinned_concepts {
println!("{}", concept);
}
} else {
let concepts = service.get_role_graph_top_k(&role_name, top_k).await?;
for concept in concepts {
println!("{}", concept);
}
}
Ok(())
}
},
#[cfg(feature = "llm")]
Command::Chat {
role,
Expand Down Expand Up @@ -4261,6 +4302,39 @@ async fn run_server_command(
}
Ok(())
}
Command::Kg { sub } => match sub {
KgSub::List {
role,
top_k,
pinned,
} => {
let role_name = if let Some(role) = role {
role
} else {
let config_res = api.get_config().await?;
config_res.config.selected_role.to_string()
};

let graph_res = api.rolegraph(Some(&role_name)).await?;
if pinned {
let pinned_ids: std::collections::HashSet<u64> =
graph_res.pinned_node_ids.iter().copied().collect();
for node in graph_res.nodes {
if pinned_ids.contains(&node.id) {
println!("{}", node.label);
}
}
} else {
let mut nodes_sorted = graph_res.nodes;
#[allow(clippy::unnecessary_sort_by)]
nodes_sorted.sort_by(|a, b| b.rank.cmp(&a.rank));
for node in nodes_sorted.into_iter().take(top_k) {
println!("{}", node.label);
}
}
Ok(())
}
},
#[cfg(feature = "llm")]
Command::Chat {
role,
Expand Down
103 changes: 95 additions & 8 deletions crates/terraphim_config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::{path::PathBuf, sync::Arc};
use terraphim_automata::{
AutomataPath,
builder::{Logseq, ThesaurusBuilder},
load_thesaurus,
load_thesaurus, parse_markdown_directives_dir,
};
use terraphim_persistence::Persistable;
use terraphim_rolegraph::{RoleGraph, RoleGraphSync};
Expand Down Expand Up @@ -945,6 +945,53 @@ impl Persistable for Config {
}

/// ConfigState for the Terraphim (Actor)
/// Extract trigger and pinned directives from KG markdown files.
///
/// Parses markdown directives from the given directory, looks up each concept
/// in the thesaurus to obtain its node ID, and returns a map of node IDs to
/// trigger text plus a list of pinned node IDs.
fn extract_triggers_from_kg(
kg_path: &PathBuf,
thesaurus: &terraphim_types::Thesaurus,
) -> (ahash::AHashMap<u64, String>, Vec<u64>) {
let mut triggers = ahash::AHashMap::new();
let mut pinned = Vec::new();

let parsed = match parse_markdown_directives_dir(kg_path.as_path()) {
Ok(result) => result,
Err(err) => {
log::warn!(
"Failed to parse markdown directives from {:?}: {}",
kg_path,
err
);
return (triggers, pinned);
}
};

for (concept_name, directives) in parsed.directives {
let normalized_value = terraphim_types::NormalizedTermValue::new(concept_name.clone());
if let Some(term) = thesaurus.get(&normalized_value) {
let node_id = term.id;
if let Some(trigger_text) = directives.trigger {
if !trigger_text.trim().is_empty() {
triggers.insert(node_id, trigger_text.trim().to_string());
}
}
if directives.pinned {
pinned.push(node_id);
}
} else {
log::debug!(
"Concept '{}' not found in thesaurus for trigger extraction",
concept_name
);
}
}

(triggers, pinned)
}

/// Config state can be updated using the API or Atomic Server
///
/// Holds the Terraphim Config and the RoleGraphs
Expand Down Expand Up @@ -979,8 +1026,22 @@ impl ConfigState {
match load_thesaurus(automata_path).await {
Ok(thesaurus) => {
log::info!("Successfully loaded thesaurus from automata path");
let rolegraph =
RoleGraph::new(role_name.clone(), thesaurus).await?;
let mut rolegraph =
RoleGraph::new(role_name.clone(), thesaurus.clone()).await?;
// Load trigger/pinned directives from local KG if available
if let Some(kg_local) = &kg.knowledge_graph_local {
let (triggers, pinned) =
extract_triggers_from_kg(&kg_local.path, &thesaurus);
if !triggers.is_empty() || !pinned.is_empty() {
log::info!(
"Loading {} triggers and {} pinned entries for role {} from local KG",
triggers.len(),
pinned.len(),
role_name
);
rolegraph.load_trigger_index(triggers, pinned, 0.3);
}
}
roles.insert(role_name.clone(), RoleGraphSync::from(rolegraph));
}
Err(e) => {
Expand All @@ -1004,9 +1065,24 @@ impl ConfigState {
"Successfully built thesaurus from local KG fallback for role {}",
role_name
);
let rolegraph =
RoleGraph::new(role_name.clone(), thesaurus)
.await?;
let mut rolegraph = RoleGraph::new(
role_name.clone(),
thesaurus.clone(),
)
.await?;
let (triggers, pinned) = extract_triggers_from_kg(
&kg_local.path,
&thesaurus,
);
if !triggers.is_empty() || !pinned.is_empty() {
log::info!(
"Loading {} triggers and {} pinned entries for role {} from local KG fallback",
triggers.len(),
pinned.len(),
role_name
);
rolegraph.load_trigger_index(triggers, pinned, 0.3);
}
roles.insert(
role_name.clone(),
RoleGraphSync::from(rolegraph),
Expand Down Expand Up @@ -1040,8 +1116,19 @@ impl ConfigState {
"Successfully built thesaurus from local KG for role {}",
role_name
);
let rolegraph =
RoleGraph::new(role_name.clone(), thesaurus).await?;
let mut rolegraph =
RoleGraph::new(role_name.clone(), thesaurus.clone()).await?;
let (triggers, pinned) =
extract_triggers_from_kg(&kg_local.path, &thesaurus);
if !triggers.is_empty() || !pinned.is_empty() {
log::info!(
"Loading {} triggers and {} pinned entries for role {} from local KG",
triggers.len(),
pinned.len(),
role_name
);
rolegraph.load_trigger_index(triggers, pinned, 0.3);
}
roles.insert(role_name.clone(), RoleGraphSync::from(rolegraph));
}
Err(e) => {
Expand Down
2 changes: 1 addition & 1 deletion crates/terraphim_service/src/auto_route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub const JMAP_MISSING_TOKEN_PENALTY: i64 = 1;
/// no document indexing). Returns 0 when the thesaurus is empty or no concept
/// matches.
fn score_distinct_concepts(rg: &RoleGraph, query: &str) -> usize {
let ids = rg.find_matching_node_ids(query);
let ids = rg.find_matching_node_ids_with_fallback(query, false);
let unique: AHashSet<u64> = ids.into_iter().collect();
unique.len()
}
Expand Down
95 changes: 95 additions & 0 deletions reports/spec-validation-20260505.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Spec Validation Report

**Agent:** Carthos (Domain Architect / spec-validator)
**Date:** 2026-05-05
**Report:** reports/spec-validation-20260505.md

---

## Summary

| Spec | Status | Notes |
|------|--------|-------|
| design-gitea82-correction-event.md | **PASS** | All types, CLI, handlers, and tests implemented and wired |
| d3-session-auto-capture-plan.md | **PASS** | from_session, extraction, filtering, CLI all implemented |
| learning-correction-system-plan.md | **PARTIAL** | Phases A-H, J implemented; Phase I (agent evolution) not wired |
| design-gitea84-trigger-based-retrieval.md | **FAIL** | Parsing and index implemented; production wiring incomplete |
| design-single-agent-listener.md | **OPERATIONAL** | Config exists; tmux session not currently running |

---

## Detailed Findings

### 1. design-gitea82-correction-event.md -- PASS

- `CorrectionType` enum with all 7 variants: `capture.rs:42`
- `CorrectionEvent` struct with full fields: `capture.rs:134`
- `to_markdown()` / `from_markdown()` serialization: `capture.rs:210-320`
- CLI `learn correction` subcommand: `main.rs:965`
- Handler wired in main dispatch: `main.rs:3480`

### 2. d3-session-auto-capture-plan.md -- PASS

- `from_session_commands()`: `procedure.rs:470`
- `extract_bash_commands_from_session()`: `procedure.rs:520`
- Trivial command filtering (cd, ls, echo, etc.): `procedure.rs:490`
- Auto-title generation: `procedure.rs:510`
- CLI `learn procedure from-session`: `main.rs:1179`
- Feature-gated behind `repl-sessions`: `main.rs:1179`

### 3. learning-correction-system-plan.md -- PARTIAL

**IMPLEMENTED (PASS):**
- Phase A (#480 redaction): `redaction.rs` wired in capture pipeline
- Phase B (#693 procedure memory): `procedure.rs` ungated, CLI commands: list, show, record, add-step, success, failure, replay, health, enable, disable, from-session
- Phase C (#703 entity annotation): `annotate_with_entities()` in `capture.rs`, `--semantic` flag on `learn query`
- Phase D (#694 replay): `replay.rs` with `StepOutcome`, `ReplayResult`, dry-run support
- Phase E (#599 multi-hook): `LearnHookType` enum: PreToolUse, PostToolUse, UserPromptSubmit
- Phase F (#695 health monitoring): `ProcedureHealthReport`, `health_check()`, auto-disable on critical
- Phase G (#727 shared learning): `SharedLearningSub` CLI: List, Promote, Import, Stats, Inject
- Phase H (#704 guard): `guard_patterns.rs` with GuardDecision: Allow, Block, Sandbox
- Phase J (#515-517 validation): `terraphim_hooks::ValidationService` exists with Aho-Corasick pattern matching

**NOT IMPLEMENTED (FAIL):**
- Phase I (#727-730 agent evolution): `terraphim_agent_evolution` crate exists but is **NOT a workspace member** and **NOT wired into `terraphim_agent` main binary**

### 4. design-gitea84-trigger-based-retrieval.md -- FAIL

**IMPLEMENTED:**
- `trigger::` and `pinned::` directive parsing: `markdown_directives.rs:180`
- `TriggerIndex` with TF-IDF: `terraphim_rolegraph/src/lib.rs:380`
- `find_matching_node_ids_with_fallback()`: `terraphim_rolegraph/src/lib.rs:450`
- `query_graph_with_trigger_fallback()`: `terraphim_rolegraph/src/lib.rs:470`
- `include_pinned` parameter on search query: `terraphim_config/src/lib.rs:1153`
- `graph --pinned` CLI flag: `main.rs:738`

**CRITICAL GAP:**
- `load_trigger_index()` is **never called in production code**. It is only invoked in unit tests (`terraphim_rolegraph/src/lib.rs:2205`).
- Result: The `trigger_index` field in `RoleGraph` remains empty in production. `query_graph_with_trigger_fallback()` falls back to an empty index, making the TF-IDF fallback path a no-op.
- Pinned entries DO work via `include_pinned` because `pinned_node_ids` is populated through the SerializableRoleGraph roundtrip, but trigger-based retrieval is dead code.

**MINOR GAP:**
- Spec requests `kg list --pinned` command. Implementation provides `graph --pinned` instead. Functionality exists but command naming diverges from spec.

### 5. design-single-agent-listener.md -- OPERATIONAL

- Config file `listener-worker.json` exists
- Launch script `start-listener.sh` exists
- Binary `~/.cargo/bin/terraphim-agent` exists
- **Gap:** tmux session `terraphim-worker` is not currently running

---

## Recommendations

1. **Wire `load_trigger_index()` in production**: After building the RoleGraph from KG markdown, extract `trigger::` and `pinned::` directives and call `load_trigger_index()`. This likely belongs in `terraphim_config/src/lib.rs` during role graph construction.

2. **Wire agent evolution crate**: Add `terraphim_agent_evolution` to workspace members and wire `EvolutionWorkflowManager` into the main binary behind a feature flag.

3. **Start listener tmux session**: Run `start-listener.sh` to activate the Gitea listener.

4. **Align CLI naming**: Consider adding `kg list --pinned` as an alias for `graph --pinned` to match spec.

---

Theme-ID: spec-gap
Loading