fix: preserve edited vault guidance status
This commit is contained in:
@@ -806,6 +806,7 @@ Tolaria tracks managed vault-level AI guidance separately from normal note conte
|
||||
- `GEMINI.md` is an optional Gemini CLI compatibility shim that points Gemini back to `AGENTS.md`
|
||||
- `useVaultAiGuidanceStatus` reads `get_vault_ai_guidance_status` and normalizes the backend state into four UI cases: `managed`, `missing`, `broken`, and `custom`
|
||||
- `restore_vault_ai_guidance` repairs only Tolaria-managed files and creates the optional Gemini shim on explicit request; user-authored custom `AGENTS.md` / `CLAUDE.md` / `GEMINI.md` files are surfaced as custom and left untouched
|
||||
- Editing a usable `AGENTS.md`, including changing its frontmatter `type`, makes the file custom rather than broken; broken is reserved for missing, empty, frontmatter-only, unreadable, or exact replaceable managed templates/stubs
|
||||
- The status bar AI badge and command palette consume that abstraction to expose restore actions only when the managed guidance is missing or broken
|
||||
|
||||
Vault guidance is intentionally short and vault-specific. General Tolaria product behavior is delivered through the bundled agent docs resource instead:
|
||||
|
||||
@@ -544,7 +544,7 @@ Once a vault is ready, `useAiAgentsOnboarding` can show a one-time `AiAgentsOnbo
|
||||
|
||||
`useGettingStartedClone` reuses the same parent-folder semantics for the status-bar / command-palette clone action, and `Toast` is rendered through the AI-agents onboarding gate so the resolved destination path stays visible right after a successful clone.
|
||||
|
||||
The starter content no longer lives in the app repo. `src-tauri/src/vault/getting_started.rs` holds the public starter repo URL (`refactoringhq/tolaria-getting-started`), delegates the clone to the git backend, then normalizes Tolaria-managed root guidance and type scaffolding (`AGENTS.md`, `CLAUDE.md`, `type.md`, `note.md`) so fresh starter vaults pick up the current defaults even when the remote starter repo still carries a legacy copy or an older pre-`type:` `is_a`-era template. `AGENTS.md` stays the canonical vault guidance file; `CLAUDE.md` is a compatibility shim that imports it for Claude Code without duplicating the instructions, and Tolaria seeds it as an organized `Note` so it stays out of the way in a fresh vault. Optional `GEMINI.md` guidance is created only by the explicit AI guidance restore action. The clone helper still accepts the legacy `LAPUTA_GETTING_STARTED_REPO_URL` environment override so older automation can continue to redirect the starter source during the transition.
|
||||
The starter content no longer lives in the app repo. `src-tauri/src/vault/getting_started.rs` holds the public starter repo URL (`refactoringhq/tolaria-getting-started`), delegates the clone to the git backend, then normalizes Tolaria-managed root guidance and type scaffolding (`AGENTS.md`, `CLAUDE.md`, `type.md`, `note.md`) so fresh starter vaults pick up the current defaults even when the remote starter repo still carries a legacy copy or an older pre-`type:` `is_a`-era template. `AGENTS.md` stays the canonical vault guidance file; `CLAUDE.md` is a compatibility shim that imports it for Claude Code without duplicating the instructions, and Tolaria seeds it as an organized `Note` so it stays out of the way in a fresh vault. Optional `GEMINI.md` guidance is created only by the explicit AI guidance restore action. Once a user edits a usable `AGENTS.md`, including changing its frontmatter `type`, the status command treats it as custom guidance instead of broken; repair remains reserved for missing, empty, frontmatter-only, unreadable, or exact replaceable managed templates/stubs. The clone helper still accepts the legacy `LAPUTA_GETTING_STARTED_REPO_URL` environment override so older automation can continue to redirect the starter source during the transition.
|
||||
|
||||
After the clone completes, Tolaria removes every configured git remote from the new starter vault. Getting Started vaults therefore open as local-only by default, and users opt into a remote later with the explicit Add Remote flow.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use serde::Serialize;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::getting_started::{agents_content_can_be_refreshed, AGENTS_MD};
|
||||
use super::getting_started::{agents_content_is_known_managed_template, AGENTS_MD};
|
||||
|
||||
/// Content for `type.md` — describes the generic Type metamodel for the vault.
|
||||
const TYPE_TYPE_DEFINITION: &str = "\
|
||||
@@ -111,7 +111,9 @@ fn read_file_or_empty(path: &Path) -> String {
|
||||
}
|
||||
|
||||
fn agents_content_can_be_replaced(content: &str) -> bool {
|
||||
content.contains("See config/agents.md") || agents_content_can_be_refreshed(content)
|
||||
content.trim().is_empty()
|
||||
|| content.contains("See config/agents.md")
|
||||
|| agents_content_is_known_managed_template(content)
|
||||
}
|
||||
|
||||
fn root_agents_can_be_replaced(path: &Path) -> bool {
|
||||
@@ -157,6 +159,30 @@ fn sync_managed_file(
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn strip_markdown_frontmatter(content: &str) -> &str {
|
||||
let Some(line_ending) = resolve_frontmatter_line_ending(content) else {
|
||||
return content;
|
||||
};
|
||||
let after_open = &content[3 + line_ending.len()..];
|
||||
let close_marker = format!("{line_ending}---");
|
||||
let Some(close_index) = after_open.find(&close_marker) else {
|
||||
return content;
|
||||
};
|
||||
let after_close = &after_open[close_index + close_marker.len()..];
|
||||
after_close.strip_prefix(line_ending).unwrap_or(after_close)
|
||||
}
|
||||
|
||||
fn resolve_frontmatter_line_ending(content: &str) -> Option<&'static str> {
|
||||
if content.starts_with("---\r\n") {
|
||||
return Some("\r\n");
|
||||
}
|
||||
content.starts_with("---\n").then_some("\n")
|
||||
}
|
||||
|
||||
fn guidance_content_has_body(content: &str) -> bool {
|
||||
!strip_markdown_frontmatter(content).trim().is_empty()
|
||||
}
|
||||
|
||||
fn classify_guidance_file(
|
||||
path: &Path,
|
||||
matches_managed: fn(&str) -> bool,
|
||||
@@ -171,6 +197,10 @@ fn classify_guidance_file(
|
||||
return AiGuidanceFileState::Managed;
|
||||
}
|
||||
|
||||
if !guidance_content_has_body(&content) {
|
||||
return AiGuidanceFileState::Broken;
|
||||
}
|
||||
|
||||
if can_replace(path) {
|
||||
return AiGuidanceFileState::Broken;
|
||||
}
|
||||
@@ -468,17 +498,17 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_refreshes_outdated_managed_agents(run: VaultOperation) {
|
||||
let outdated_agents = AGENTS_MD.replacen(
|
||||
fn assert_preserves_edited_default_agents(run: VaultOperation) {
|
||||
let edited_agents = AGENTS_MD.replacen(
|
||||
"Store note type in the `type:` frontmatter field.",
|
||||
"`type:` is the preferred type field. Tolaria still understands legacy aliases such as `Is A`.",
|
||||
1,
|
||||
);
|
||||
let (_dir, vault) = run_with_agents(run, Some(&outdated_agents), None);
|
||||
let (_dir, vault) = run_with_agents(run, Some(&edited_agents), None);
|
||||
|
||||
let content = read_root_agents(&vault);
|
||||
assert!(content.contains("Store note type in the `type:` frontmatter field."));
|
||||
assert!(!content.contains("Tolaria still understands legacy aliases such as `Is A`."));
|
||||
assert!(content.contains("Tolaria still understands legacy aliases such as `Is A`."));
|
||||
assert!(!content.contains("Store note type in the `type:` frontmatter field."));
|
||||
}
|
||||
|
||||
fn assert_legacy_agents_move_to_root(
|
||||
@@ -584,7 +614,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_seed_config_files_refreshes_stale_default_agents() {
|
||||
fn test_seed_config_files_preserves_edited_stale_default_agents() {
|
||||
let (_dir, vault) = create_vault();
|
||||
write_root_agents(
|
||||
&vault,
|
||||
@@ -594,15 +624,13 @@ mod tests {
|
||||
seed_config_files(vault.to_str().unwrap());
|
||||
|
||||
let content = read_root_agents(&vault);
|
||||
assert!(content.contains("Use the first H1 as the note title."));
|
||||
assert!(content.contains("Tolaria reads notes recursively from all folders"));
|
||||
assert!(content.contains("views/*.yml"));
|
||||
assert!(content.contains("Belongs to:"));
|
||||
assert!(content.contains("Do not add `title:` frontmatter."));
|
||||
assert!(!content.contains("Use the first H1 as the note title."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_seed_config_files_refreshes_outdated_managed_agents() {
|
||||
assert_refreshes_outdated_managed_agents(run_seed);
|
||||
fn test_seed_config_files_preserves_edited_default_agents() {
|
||||
assert_preserves_edited_default_agents(run_seed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -724,8 +752,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_repair_config_files_refreshes_outdated_managed_agents() {
|
||||
assert_refreshes_outdated_managed_agents(run_repair);
|
||||
fn test_repair_config_files_preserves_edited_default_agents() {
|
||||
assert_preserves_edited_default_agents(run_repair);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -746,6 +774,48 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_ai_guidance_status_treats_edited_agents_as_custom() {
|
||||
let (_dir, vault) = create_vault();
|
||||
let edited_agents = AGENTS_MD.replacen("type: Note", "type: Vault Guidance", 1)
|
||||
+ "\n\n- Prefer the user's vault-specific naming conventions.\n";
|
||||
write_root_agents(&vault, &edited_agents);
|
||||
write_root_claude(&vault, CLAUDE_MD_SHIM);
|
||||
write_root_gemini(&vault, GEMINI_MD_SHIM);
|
||||
|
||||
let status = get_ai_guidance_status(vault.to_str().unwrap()).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
status,
|
||||
VaultAiGuidanceStatus {
|
||||
agents_state: AiGuidanceFileState::Custom,
|
||||
claude_state: AiGuidanceFileState::Managed,
|
||||
gemini_state: AiGuidanceFileState::Managed,
|
||||
can_restore: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_ai_guidance_status_reports_frontmatter_only_agents_as_broken() {
|
||||
let (_dir, vault) = create_vault();
|
||||
write_root_agents(&vault, "---\ntype: Vault Guidance\n---\n");
|
||||
write_root_claude(&vault, CLAUDE_MD_SHIM);
|
||||
write_root_gemini(&vault, GEMINI_MD_SHIM);
|
||||
|
||||
let status = get_ai_guidance_status(vault.to_str().unwrap()).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
status,
|
||||
VaultAiGuidanceStatus {
|
||||
agents_state: AiGuidanceFileState::Broken,
|
||||
claude_state: AiGuidanceFileState::Managed,
|
||||
gemini_state: AiGuidanceFileState::Managed,
|
||||
can_restore: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_restore_ai_guidance_files_repairs_without_overwriting_custom_agents() {
|
||||
let (_dir, vault) = create_vault();
|
||||
|
||||
@@ -292,6 +292,10 @@ pub(super) fn agents_content_can_be_refreshed(content: &str) -> bool {
|
||||
AgentsContent::new(content).can_be_refreshed()
|
||||
}
|
||||
|
||||
pub(super) fn agents_content_is_known_managed_template(content: &str) -> bool {
|
||||
AgentsContent::new(content).is_known_legacy_template()
|
||||
}
|
||||
|
||||
/// Default AGENTS.md content — vault instructions for AI agents.
|
||||
/// Describes Tolaria vault mechanics only; no user-specific structure.
|
||||
/// The vault scanner will pick this up as a regular entry.
|
||||
|
||||
Reference in New Issue
Block a user