feat: a skill creating skill as default
This commit is contained in:
@@ -40,7 +40,7 @@ identifier = "de.rfc1437.ds4server"
|
||||
description = "A native macOS coding-agent GUI for DwarfStar"
|
||||
binaries = [{ path = "ds4-server", main = true }]
|
||||
icons = ["assets/DS4Server.icns", "assets/app-icon.png"]
|
||||
resources = ["metal"]
|
||||
resources = ["metal", "assets/dev-brain"]
|
||||
|
||||
[package.metadata.packager.macos]
|
||||
minimum-system-version = "13.0"
|
||||
|
||||
15
assets/dev-brain/purpose.md
Normal file
15
assets/dev-brain/purpose.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Dev Brain purpose
|
||||
|
||||
Dev Brain compiles durable, source-backed knowledge from the registered projects.
|
||||
|
||||
## Priorities
|
||||
|
||||
- Architecture, behavior, decisions, invariants, workflows, and relationships.
|
||||
- Small topic pages that answer recurring development questions.
|
||||
- Visible uncertainty and exact project provenance.
|
||||
|
||||
## Recurring questions
|
||||
|
||||
- Where does a behavior live, and what must remain invariant when it changes?
|
||||
- Which decisions constrain the current implementation?
|
||||
- Which project sources must be rechecked before relying on this page?
|
||||
67
assets/dev-brain/schema.md
Normal file
67
assets/dev-brain/schema.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Dev Brain schema
|
||||
|
||||
The registered projects are authoritative. This vault is a derived, human-readable wiki.
|
||||
|
||||
## Managed paths
|
||||
|
||||
DS4Server manages `purpose.md`, `schema.md`, `index.md`, `skills.md`, `log.md`, and topic pages below `projects/`, `subsystems/`, `concepts/`, `decisions/`, `invariants/`, `workflows/`, and `skills/`. Topic pages must opt in with `dev_brain: true`. Other notes, hidden files, Obsidian settings, attachments, and trash are never modified.
|
||||
|
||||
## Topic frontmatter
|
||||
|
||||
```yaml
|
||||
---
|
||||
dev_brain: true
|
||||
type: subsystem # project, subsystem, concept, decision, invariant, workflow, or skill
|
||||
project: Registered project name
|
||||
status: verified # verified, stale, or needs-review
|
||||
verified_at: 2026-07-27T12:00:00Z
|
||||
sources:
|
||||
- project: Registered project name
|
||||
path: src/example.rs
|
||||
symbol: optional_symbol
|
||||
revision: latest-commit-containing-this-file-content
|
||||
# For a clean tracked file, use `git log -1 --format=%H -- src/example.rs`,
|
||||
# or a unique lowercase hex prefix of at least 7 characters. Use hash when
|
||||
# this file differs from HEAD, is untracked, or the registered project is not Git.
|
||||
---
|
||||
```
|
||||
|
||||
`project` names a registered project, not this Dev Brain vault; `dev_brain_info` lists the exact registered names and folders. Each source `path` is relative to that registered project's folder; never resolve it inside the vault. Cite only files that support the page's claims, not every dirty file in the project. Each source has exactly one evidence version: `revision` or a lowercase SHA-256 `hash`. A revision is the newest commit that changed that source file, returned by `git log -1 --format=%H -- path`; validation requires that per-file revision and compares its file content with the current clean file. Every source therefore has its own revision; never stamp all sources with the repository's overall HEAD. Use a hash when that specific file differs from HEAD, is untracked, or its registered project is not Git.
|
||||
|
||||
## Skills
|
||||
|
||||
Skills are on-demand instructions stored as one Markdown file per skill below `skills/`. They use the same provenance and status fields as topic pages, set `type: skill`, and additionally require `name` and `description` frontmatter. Names contain 1–64 lowercase letters, digits, or hyphens, with no leading, trailing, or consecutive hyphens. Descriptions contain 1–1024 characters and explain both what the skill does and when to use it. Only skills whose status and current evidence are both `verified` appear in generated `skills.md` and the session system prompt. The prompt exposes each verified skill's name, description, and Markdown path; read the complete matching file on demand before following it. `index.md` lists all managed topic pages, while `skills.md` lists verified skills only. Do not hand-edit either generated index.
|
||||
|
||||
Files with `built_in: true` are app-managed skills restored from the DS4Server bundle. Do not create or edit them by hand.
|
||||
|
||||
```yaml
|
||||
---
|
||||
dev_brain: true
|
||||
type: skill
|
||||
name: review-release
|
||||
description: Verify a release candidate against the project checklist. Use before publishing a release.
|
||||
project: Registered project name
|
||||
status: verified
|
||||
verified_at: 2026-07-27T12:00:00Z
|
||||
sources:
|
||||
- project: Registered project name
|
||||
path: docs/releasing.md
|
||||
revision: full-or-unique-short-clean-git-revision
|
||||
---
|
||||
```
|
||||
|
||||
## Compilation
|
||||
|
||||
Read the purpose first. Examine high-signal manifests, documentation, schemas, entry points, public interfaces, and tests. Create the smallest coherent topic set that answers the purpose; do not mirror every source file. Use ordinary Obsidian wikilinks and embeds to connect topics.
|
||||
|
||||
## Querying
|
||||
|
||||
Start with `index.md` or ranked search, then follow links and backlinks. A `verified` page is authoritative only while all recorded evidence is current. Treat `stale` pages as navigation leads and `needs-review` pages as explicit uncertainty; check project sources before making claims from either.
|
||||
|
||||
## Refresh and semantic validation
|
||||
|
||||
When validation reports drifted sources, handle each listed file independently: re-read it, compare its current behavior with the page's documented findings, update, split, merge, or retire affected knowledge, then update only that source's revision or hash. For a large revision-backed file, `git diff <recorded-revision> -- path` shows the change from the recorded evidence to the current working-tree version; use it to focus the review, then read enough current context to validate the affected findings. Inspect the commits affecting that path since the recorded revision as well. When code was removed, inspect the full commits and search the current project, callers, and tests before concluding the behavior disappeared: it may have been renamed, replaced, or moved into another file that should replace or join the page's cited sources. Repeat for every listed file and re-run validation. Keep `index.md` exact, and append a material update entry to `log.md` with source revisions. Only mark a page `verified` after its claims have been checked against its current evidence. Unsupported conclusions stay `needs-review`.
|
||||
|
||||
## Publication
|
||||
|
||||
Call `dev_brain_info`, then maintain these pages with the ordinary file tools. Append material updates to `log.md`; `index.md` and `skills.md` are generated and should not be hand-maintained. Finish with `dev_brain_validate`. Structural or provenance errors must be repaired, while broken links are warnings so valid work remains editable instead of being discarded.
|
||||
58
assets/dev-brain/skills/create-dev-brain-skill.md
Normal file
58
assets/dev-brain/skills/create-dev-brain-skill.md
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
dev_brain: true
|
||||
type: skill
|
||||
built_in: true
|
||||
name: create-dev-brain-skill
|
||||
description: Create or update a source-backed skill in Dev Brain. Use when adding reusable instructions that should activate on demand in future development tasks.
|
||||
project: DS4Server built-in
|
||||
status: verified
|
||||
verified_at: 2026-07-30T00:00:00Z
|
||||
sources: []
|
||||
---
|
||||
|
||||
# Create a Dev Brain skill
|
||||
|
||||
Create a skill when a repeatable development task needs instructions beyond the model's general knowledge. Keep it concise and source-backed.
|
||||
|
||||
## File structure
|
||||
|
||||
A Dev Brain skill is one Markdown file, not a directory:
|
||||
|
||||
```text
|
||||
skills/
|
||||
└── skill-name.md
|
||||
```
|
||||
|
||||
Use this structure:
|
||||
|
||||
```yaml
|
||||
---
|
||||
dev_brain: true
|
||||
type: skill
|
||||
name: skill-name
|
||||
description: State what the skill does and when to use it.
|
||||
project: Exact registered project name
|
||||
status: needs-review # change to verified only after checking every claim
|
||||
verified_at: 2026-07-30T12:00:00Z
|
||||
sources:
|
||||
- project: Exact registered project name
|
||||
path: path/to/authoritative-source.md
|
||||
revision: latest-commit-that-changed-this-file
|
||||
---
|
||||
|
||||
# Human-readable skill title
|
||||
|
||||
Write direct instructions for completing the task. Link to related Dev Brain pages when useful.
|
||||
```
|
||||
|
||||
Use lowercase letters, digits, and hyphens for `name`. Put all trigger conditions in `description`. Cite at least one registered project source, using the exact per-file revision for a clean Git file or its lowercase SHA-256 hash when required by `schema.md`.
|
||||
|
||||
## Create and activate
|
||||
|
||||
1. Call `dev_brain_info` and read `purpose.md` and `schema.md`.
|
||||
2. Read the authoritative project sources for the instructions.
|
||||
3. Write `skills/<name>.md` with current evidence and `status: verified`; use `needs-review` until every claim is checked.
|
||||
4. Append the material change to `log.md`.
|
||||
5. Run `dev_brain_validate`.
|
||||
|
||||
Validation is the activation step. It verifies the file, rebuilds generated `skills.md`, and makes the skill's name, description, and path available to the agent. Never edit `skills.md` directly.
|
||||
@@ -633,9 +633,15 @@ impl App {
|
||||
}
|
||||
Message::ConfirmRestoreDevBrainDefaultGuides => {
|
||||
self.restore_dev_brain_confirmation = false;
|
||||
self.preference_error = crate::dev_brain::restore_default_guides(Path::new(
|
||||
&self.preference_draft.dev_brain_vault_path,
|
||||
))
|
||||
let projects = self
|
||||
.projects
|
||||
.iter()
|
||||
.map(|project| project.project.clone())
|
||||
.collect::<Vec<_>>();
|
||||
self.preference_error = crate::dev_brain::restore_default_guides(
|
||||
Path::new(&self.preference_draft.dev_brain_vault_path),
|
||||
&projects,
|
||||
)
|
||||
.err();
|
||||
}
|
||||
Message::CancelRestoreDevBrainDefaultGuides => {
|
||||
|
||||
@@ -199,9 +199,9 @@ impl App {
|
||||
.size(12)
|
||||
.width(Length::Fill),
|
||||
hint(
|
||||
action_button("Recreate purpose.md and schema.md")
|
||||
action_button("Recreate guidance and skills")
|
||||
.on_press(Message::RestoreDevBrainDefaultGuides),
|
||||
"Overwrites only purpose.md and schema.md with this version's defaults. Topic pages, generated indexes, and log.md are preserved.",
|
||||
"Overwrites purpose.md, schema.md, and included skills with this version's defaults. Other skill pages, generated indexes, topic pages, and log.md are preserved.",
|
||||
),
|
||||
]
|
||||
.spacing(12)
|
||||
@@ -752,7 +752,7 @@ impl App {
|
||||
let confirmation = container(
|
||||
column![
|
||||
text("Recreate Dev Brain guidance?").size(22),
|
||||
text("This replaces purpose.md and schema.md in the selected vault with this version's defaults. Topic pages, generated indexes, skills, and log.md are preserved.")
|
||||
text("This replaces purpose.md, schema.md, and included skills in the selected vault with this version's defaults. Other skill pages, generated indexes, topic pages, and log.md are preserved.")
|
||||
.size(13),
|
||||
row![
|
||||
Space::new().width(Length::Fill),
|
||||
|
||||
270
src/dev_brain.rs
270
src/dev_brain.rs
@@ -38,87 +38,11 @@ pub(crate) const TOOL_SCHEMAS: &str = r#"{"type":"function","function":{"name":"
|
||||
{"type":"function","function":{"name":"dev_brain_search","description":"Search the validated Dev Brain index with freshness and project evidence. Use ordinary search for literal or regex file search.","parameters":{"type":"object","properties":{"query":{"type":"string"},"limit":{"type":"number"},"authoritative":{"type":"boolean"}},"required":["query"]}}}
|
||||
{"type":"function","function":{"name":"dev_brain_validate","description":"Validate managed pages after ordinary file edits, report every drifted project source for semantic reinspection and per-source revision updates, deterministically rebuild index.md and skills.md, refresh search, and report repairable link warnings.","parameters":{"type":"object","properties":{}}}}"#;
|
||||
|
||||
const DEFAULT_PURPOSE: &str = "# Dev Brain purpose\n\n\
|
||||
Dev Brain compiles durable, source-backed knowledge from the registered projects.\n\n\
|
||||
## Priorities\n\n\
|
||||
- Architecture, behavior, decisions, invariants, workflows, and relationships.\n\
|
||||
- Small topic pages that answer recurring development questions.\n\
|
||||
- Visible uncertainty and exact project provenance.\n\n\
|
||||
## Recurring questions\n\n\
|
||||
- Where does a behavior live, and what must remain invariant when it changes?\n\
|
||||
- Which decisions constrain the current implementation?\n\
|
||||
- Which project sources must be rechecked before relying on this page?\n";
|
||||
|
||||
const DEFAULT_INDEX: &str = "# Dev Brain index\n\nNo topic pages have been compiled yet.\n";
|
||||
const DEFAULT_SKILLS: &str = "# Dev Brain skills\n\nNo verified skills are available.\n";
|
||||
const DEFAULT_LOG: &str =
|
||||
"# Dev Brain log\n\n<!-- Append material wiki updates and their source revisions below. -->\n";
|
||||
const DEFAULT_SCHEMA: &str = r#"# Dev Brain schema
|
||||
|
||||
The registered projects are authoritative. This vault is a derived, human-readable wiki.
|
||||
|
||||
## Managed paths
|
||||
|
||||
DS4Server manages `purpose.md`, `schema.md`, `index.md`, `skills.md`, `log.md`, and topic pages below `projects/`, `subsystems/`, `concepts/`, `decisions/`, `invariants/`, `workflows/`, and `skills/`. Topic pages must opt in with `dev_brain: true`. Other notes, hidden files, Obsidian settings, attachments, and trash are never modified.
|
||||
|
||||
## Topic frontmatter
|
||||
|
||||
```yaml
|
||||
---
|
||||
dev_brain: true
|
||||
type: subsystem # project, subsystem, concept, decision, invariant, workflow, or skill
|
||||
project: Registered project name
|
||||
status: verified # verified, stale, or needs-review
|
||||
verified_at: 2026-07-27T12:00:00Z
|
||||
sources:
|
||||
- project: Registered project name
|
||||
path: src/example.rs
|
||||
symbol: optional_symbol
|
||||
revision: latest-commit-containing-this-file-content
|
||||
# For a clean tracked file, use `git log -1 --format=%H -- src/example.rs`,
|
||||
# or a unique lowercase hex prefix of at least 7 characters. Use hash when
|
||||
# this file differs from HEAD, is untracked, or the registered project is not Git.
|
||||
---
|
||||
```
|
||||
|
||||
`project` names a registered project, not this Dev Brain vault; `dev_brain_info` lists the exact registered names and folders. Each source `path` is relative to that registered project's folder; never resolve it inside the vault. Cite only files that support the page's claims, not every dirty file in the project. Each source has exactly one evidence version: `revision` or a lowercase SHA-256 `hash`. A revision is the newest commit that changed that source file, returned by `git log -1 --format=%H -- path`; validation requires that per-file revision and compares its file content with the current clean file. Every source therefore has its own revision; never stamp all sources with the repository's overall HEAD. Use a hash when that specific file differs from HEAD, is untracked, or its registered project is not Git.
|
||||
|
||||
## Skills
|
||||
|
||||
Skills are on-demand instructions stored as Markdown below `skills/`. They use the same provenance and status fields as topic pages, set `type: skill`, and additionally require `name` and `description` frontmatter. Names contain 1–64 lowercase letters, digits, or hyphens, with no leading, trailing, or consecutive hyphens. Descriptions contain 1–1024 characters and explain both what the skill does and when to use it. Only skills whose status and current evidence are both `verified` appear in generated `skills.md` and the session system prompt. The prompt exposes each verified skill's name, description, and Markdown path; read the complete matching file on demand before following it. `index.md` lists all managed topic pages, while `skills.md` lists verified skills only. Do not hand-edit either generated index.
|
||||
|
||||
```yaml
|
||||
---
|
||||
dev_brain: true
|
||||
type: skill
|
||||
name: review-release
|
||||
description: Verify a release candidate against the project checklist. Use before publishing a release.
|
||||
project: Registered project name
|
||||
status: verified
|
||||
verified_at: 2026-07-27T12:00:00Z
|
||||
sources:
|
||||
- project: Registered project name
|
||||
path: docs/releasing.md
|
||||
revision: full-or-unique-short-clean-git-revision
|
||||
---
|
||||
```
|
||||
|
||||
## Compilation
|
||||
|
||||
Read the purpose first. Examine high-signal manifests, documentation, schemas, entry points, public interfaces, and tests. Create the smallest coherent topic set that answers the purpose; do not mirror every source file. Use ordinary Obsidian wikilinks and embeds to connect topics.
|
||||
|
||||
## Querying
|
||||
|
||||
Start with `index.md` or ranked search, then follow links and backlinks. A `verified` page is authoritative only while all recorded evidence is current. Treat `stale` pages as navigation leads and `needs-review` pages as explicit uncertainty; check project sources before making claims from either.
|
||||
|
||||
## Refresh and semantic validation
|
||||
|
||||
When validation reports drifted sources, handle each listed file independently: re-read it, compare its current behavior with the page's documented findings, update, split, merge, or retire affected knowledge, then update only that source's revision or hash. For a large revision-backed file, `git diff <recorded-revision> -- path` shows the change from the recorded evidence to the current working-tree version; use it to focus the review, then read enough current context to validate the affected findings. Inspect the commits affecting that path since the recorded revision as well. When code was removed, inspect the full commits and search the current project, callers, and tests before concluding the behavior disappeared: it may have been renamed, replaced, or moved into another file that should replace or join the page's cited sources. Repeat for every listed file and re-run validation. Keep `index.md` exact, and append a material update entry to `log.md` with source revisions. Only mark a page `verified` after its claims have been checked against its current evidence. Unsupported conclusions stay `needs-review`.
|
||||
|
||||
## Publication
|
||||
|
||||
Call `dev_brain_info`, then maintain these pages with the ordinary file tools. Append material updates to `log.md`; `index.md` and `skills.md` are generated and should not be hand-maintained. Finish with `dev_brain_validate`. Structural or provenance errors must be repaired, while broken links are warnings so valid work remains editable instead of being discarded.
|
||||
"#;
|
||||
const INCLUDED_SKILLS: [&str; 1] = ["skills/create-dev-brain-skill.md"];
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RegisteredProject {
|
||||
@@ -145,6 +69,8 @@ struct TopicFrontmatter {
|
||||
dev_brain: bool,
|
||||
#[serde(rename = "type")]
|
||||
page_type: String,
|
||||
#[serde(default)]
|
||||
built_in: bool,
|
||||
project: String,
|
||||
status: String,
|
||||
verified_at: String,
|
||||
@@ -692,11 +618,7 @@ pub(crate) fn skills_prompt(
|
||||
let vault = config.vault()?;
|
||||
ensure_contract(&vault)?;
|
||||
let projects = registered_projects(projects)?;
|
||||
let pages = load_and_validate_pages(&vault, &vault, &projects, false)?;
|
||||
let skills = render_skills(&pages);
|
||||
if fs::read_to_string(vault.join("skills.md")).map_or(true, |current| current != skills) {
|
||||
publish_files(&vault, "skills", &[("skills.md", &skills)])?;
|
||||
}
|
||||
let skills = publish_skills_index(&vault, &projects)?;
|
||||
Ok(format!(
|
||||
"# Available Dev Brain skills\n\nThe following generated index contains verified skills only. Paths are relative to {}. When a task matches, read the complete Markdown file before acting.\n\n{}",
|
||||
vault.display(),
|
||||
@@ -732,7 +654,7 @@ fn registered_projects(projects: &[Project]) -> Result<Vec<RegisteredProject>, S
|
||||
Ok(projects)
|
||||
}
|
||||
|
||||
pub(crate) fn restore_default_guides(vault: &Path) -> Result<(), String> {
|
||||
pub(crate) fn restore_default_guides(vault: &Path, projects: &[Project]) -> Result<(), String> {
|
||||
let _guard = VAULT_LOCK
|
||||
.write()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
@@ -742,13 +664,22 @@ pub(crate) fn restore_default_guides(vault: &Path) -> Result<(), String> {
|
||||
"The selected vault has no Dev Brain index.md and log.md contract to restore.".into(),
|
||||
);
|
||||
}
|
||||
publish_defaults(
|
||||
&vault,
|
||||
&[
|
||||
("purpose.md", DEFAULT_PURPOSE),
|
||||
("schema.md", DEFAULT_SCHEMA),
|
||||
],
|
||||
)
|
||||
let purpose = read_default_asset("purpose.md")?;
|
||||
let schema = read_default_asset("schema.md")?;
|
||||
let skills = read_included_skills()?;
|
||||
let mut files = vec![
|
||||
("purpose.md", purpose.as_str()),
|
||||
("schema.md", schema.as_str()),
|
||||
];
|
||||
files.extend(
|
||||
skills
|
||||
.iter()
|
||||
.map(|(path, content)| (*path, content.as_str())),
|
||||
);
|
||||
publish_defaults(&vault, &files)?;
|
||||
let projects = registered_projects(projects)?;
|
||||
publish_skills_index(&vault, &projects)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_contract(vault: &Path) -> Result<(), String> {
|
||||
@@ -773,16 +704,22 @@ fn ensure_contract(vault: &Path) -> Result<(), String> {
|
||||
);
|
||||
}
|
||||
if present == 0 {
|
||||
publish_defaults(
|
||||
vault,
|
||||
&[
|
||||
("purpose.md", DEFAULT_PURPOSE),
|
||||
("schema.md", DEFAULT_SCHEMA),
|
||||
let purpose = read_default_asset("purpose.md")?;
|
||||
let schema = read_default_asset("schema.md")?;
|
||||
let skills = read_included_skills()?;
|
||||
let mut files = vec![
|
||||
("purpose.md", purpose.as_str()),
|
||||
("schema.md", schema.as_str()),
|
||||
("index.md", DEFAULT_INDEX),
|
||||
("skills.md", DEFAULT_SKILLS),
|
||||
("log.md", DEFAULT_LOG),
|
||||
],
|
||||
)?;
|
||||
];
|
||||
files.extend(
|
||||
skills
|
||||
.iter()
|
||||
.map(|(path, content)| (*path, content.as_str())),
|
||||
);
|
||||
publish_defaults(vault, &files)?;
|
||||
} else if !skills_path.is_file() {
|
||||
publish_defaults(vault, &[("skills.md", DEFAULT_SKILLS)])?;
|
||||
}
|
||||
@@ -809,7 +746,11 @@ fn publish_files(vault: &Path, label: &str, files: &[(&str, &str)]) -> Result<()
|
||||
let result = (|| {
|
||||
let mut changed = BTreeSet::new();
|
||||
for (path, content) in files {
|
||||
fs::write(staging.join(path), content).map_err(|error| error.to_string())?;
|
||||
let destination = staging.join(path);
|
||||
if let Some(parent) = destination.parent() {
|
||||
fs::create_dir_all(parent).map_err(|error| error.to_string())?;
|
||||
}
|
||||
fs::write(destination, content).map_err(|error| error.to_string())?;
|
||||
changed.insert(PathBuf::from(path));
|
||||
}
|
||||
commit_batch(vault, &staging, &changed)
|
||||
@@ -818,6 +759,41 @@ fn publish_files(vault: &Path, label: &str, files: &[(&str, &str)]) -> Result<()
|
||||
result
|
||||
}
|
||||
|
||||
fn read_default_asset(path: &str) -> Result<String, String> {
|
||||
let bundled = std::env::current_exe().ok().and_then(|path| {
|
||||
path.parent()?
|
||||
.parent()
|
||||
.map(|path| path.join("Resources/dev-brain"))
|
||||
});
|
||||
let checkout = Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/dev-brain");
|
||||
let directory = bundled
|
||||
.filter(|path| path.join("purpose.md").is_file())
|
||||
.unwrap_or(checkout);
|
||||
let asset = directory.join(path);
|
||||
fs::read_to_string(&asset).map_err(|error| {
|
||||
format!(
|
||||
"Could not read bundled Dev Brain asset {}: {error}",
|
||||
asset.display()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn read_included_skills() -> Result<Vec<(&'static str, String)>, String> {
|
||||
INCLUDED_SKILLS
|
||||
.iter()
|
||||
.map(|path| read_default_asset(path).map(|content| (*path, content)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn publish_skills_index(vault: &Path, projects: &[RegisteredProject]) -> Result<String, String> {
|
||||
let pages = load_and_validate_pages(vault, vault, projects, false)?;
|
||||
let skills = render_skills(&pages);
|
||||
if fs::read_to_string(vault.join("skills.md")).map_or(true, |current| current != skills) {
|
||||
publish_files(vault, "skills", &[("skills.md", &skills)])?;
|
||||
}
|
||||
Ok(skills)
|
||||
}
|
||||
|
||||
pub(crate) fn validate_vault(path: &Path) -> Result<PathBuf, String> {
|
||||
let vault = path
|
||||
.canonicalize()
|
||||
@@ -916,6 +892,7 @@ fn load_and_validate_pages(
|
||||
}
|
||||
validate_page_type(&path, &frontmatter.page_type)?;
|
||||
validate_skill_metadata(&frontmatter, &relative)?;
|
||||
validate_built_in_skill(&frontmatter, &relative, &content)?;
|
||||
if !matches!(
|
||||
frontmatter.status.as_str(),
|
||||
"verified" | "stale" | "needs-review"
|
||||
@@ -927,6 +904,9 @@ fn load_and_validate_pages(
|
||||
}
|
||||
OffsetDateTime::parse(&frontmatter.verified_at, &Rfc3339)
|
||||
.map_err(|_| format!("{relative} verified_at must be an RFC 3339 timestamp."))?;
|
||||
let drifted = if frontmatter.built_in {
|
||||
Vec::new()
|
||||
} else {
|
||||
if frontmatter.sources.is_empty() {
|
||||
return Err(format!("{relative} must cite at least one project source."));
|
||||
}
|
||||
@@ -949,17 +929,19 @@ fn load_and_validate_pages(
|
||||
frontmatter.project
|
||||
));
|
||||
}
|
||||
let drifted = frontmatter
|
||||
frontmatter
|
||||
.sources
|
||||
.iter()
|
||||
.map(|source| {
|
||||
validate_source(source, projects)
|
||||
.map(|fresh| (!fresh).then(|| format!("{}:{}", source.project, source.path)))
|
||||
validate_source(source, projects).map(|fresh| {
|
||||
(!fresh).then(|| format!("{}:{}", source.project, source.path))
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
if publishing && frontmatter.status == "verified" && !drifted.is_empty() {
|
||||
return Err(format!(
|
||||
"{relative} cannot be published as verified because its evidence has drifted:\n- {}\nRe-read each listed source and check whether its changes alter the page's documented findings. For a large revision-backed source, use `git diff <recorded-revision> -- path` to focus on what changed, then inspect the necessary current context and the commits affecting that path since the recorded revision. If code disappeared, inspect the full change commits and search the current project, callers, and tests for a rename, replacement, or move to another file before concluding the behavior was removed; update the cited source list when evidence moved. Update the knowledge when needed, then set only that source's revision to `git log -1 --format=%H -- path` (not the repository's overall HEAD), or its hash when required. Re-run dev_brain_validate after every listed source is current.",
|
||||
@@ -1408,9 +1390,9 @@ fn validate_page_type(path: &Path, page_type: &str) -> Result<(), String> {
|
||||
|
||||
fn validate_skill_metadata(frontmatter: &TopicFrontmatter, path: &str) -> Result<(), String> {
|
||||
if frontmatter.page_type != "skill" {
|
||||
if frontmatter.name.is_some() || frontmatter.description.is_some() {
|
||||
if frontmatter.built_in || frontmatter.name.is_some() || frontmatter.description.is_some() {
|
||||
return Err(format!(
|
||||
"{path} may use name and description only when type is skill."
|
||||
"{path} may use built_in, name, and description only when type is skill."
|
||||
));
|
||||
}
|
||||
return Ok(());
|
||||
@@ -1443,6 +1425,25 @@ fn validate_skill_metadata(frontmatter: &TopicFrontmatter, path: &str) -> Result
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_built_in_skill(
|
||||
frontmatter: &TopicFrontmatter,
|
||||
path: &str,
|
||||
content: &str,
|
||||
) -> Result<(), String> {
|
||||
if !frontmatter.built_in {
|
||||
return Ok(());
|
||||
}
|
||||
if !INCLUDED_SKILLS.contains(&path) {
|
||||
return Err(format!("{path} is not an included Dev Brain skill."));
|
||||
}
|
||||
if read_default_asset(path)? != content {
|
||||
return Err(format!(
|
||||
"{path} differs from the included Dev Brain skill. Restore the built-in guidance from Preferences."
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn normalize_managed_path(value: &str) -> Result<PathBuf, String> {
|
||||
let path = normalize_relative_path(value)?;
|
||||
if path.extension().is_none_or(|extension| extension != "md") {
|
||||
@@ -1776,9 +1777,11 @@ mod tests {
|
||||
#[test]
|
||||
fn skill_index_migration_does_not_overwrite_an_unrelated_root_note() {
|
||||
let fixture = Fixture::new();
|
||||
let purpose = read_default_asset("purpose.md").unwrap();
|
||||
let schema = read_default_asset("schema.md").unwrap();
|
||||
for (path, content) in [
|
||||
("purpose.md", DEFAULT_PURPOSE),
|
||||
("schema.md", DEFAULT_SCHEMA),
|
||||
("purpose.md", purpose.as_str()),
|
||||
("schema.md", schema.as_str()),
|
||||
("index.md", DEFAULT_INDEX),
|
||||
("log.md", DEFAULT_LOG),
|
||||
] {
|
||||
@@ -1804,6 +1807,10 @@ mod tests {
|
||||
fs::read_to_string(fixture.vault.join("skills.md")).unwrap(),
|
||||
DEFAULT_SKILLS
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read_to_string(fixture.vault.join(INCLUDED_SKILLS[0])).unwrap(),
|
||||
read_default_asset(INCLUDED_SKILLS[0]).unwrap()
|
||||
);
|
||||
drop(brain);
|
||||
fs::remove_file(fixture.vault.join("skills.md")).unwrap();
|
||||
let mut brain = fixture.brain();
|
||||
@@ -1847,9 +1854,11 @@ mod tests {
|
||||
fs::write(fixture.project.join("source.rs"), "changed\n").unwrap();
|
||||
let prompt = skills_prompt(&fixture.config(), &fixture.projects()).unwrap();
|
||||
assert!(!prompt.contains("review-release"));
|
||||
assert_eq!(
|
||||
fs::read_to_string(fixture.vault.join("skills.md")).unwrap(),
|
||||
DEFAULT_SKILLS
|
||||
assert!(prompt.contains("create-dev-brain-skill"));
|
||||
assert!(
|
||||
fs::read_to_string(fixture.vault.join("skills.md"))
|
||||
.unwrap()
|
||||
.contains("create-dev-brain-skill")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1872,19 +1881,56 @@ mod tests {
|
||||
assert!(brain.validate().unwrap_err().contains("skill name"));
|
||||
fs::remove_file(fixture.vault.join("skills/invalid.md")).unwrap();
|
||||
|
||||
fs::write(
|
||||
fixture.vault.join(INCLUDED_SKILLS[0]),
|
||||
format!("{}\n", read_default_asset(INCLUDED_SKILLS[0]).unwrap()),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
brain
|
||||
.validate()
|
||||
.unwrap_err()
|
||||
.contains("differs from the included Dev Brain skill")
|
||||
);
|
||||
|
||||
fs::write(fixture.vault.join("purpose.md"), "custom purpose\n").unwrap();
|
||||
fs::write(fixture.vault.join("schema.md"), "custom schema\n").unwrap();
|
||||
fs::write(
|
||||
fixture.vault.join(INCLUDED_SKILLS[0]),
|
||||
"custom included skill\n",
|
||||
)
|
||||
.unwrap();
|
||||
let custom_skill = fixture.skill(
|
||||
"verified",
|
||||
&hash,
|
||||
"custom-skill",
|
||||
"Keep custom instructions. Use for custom work.",
|
||||
"Custom skill",
|
||||
);
|
||||
fs::write(fixture.vault.join("skills/custom.md"), &custom_skill).unwrap();
|
||||
fs::write(fixture.vault.join("log.md"), "preserved log\n").unwrap();
|
||||
fs::write(fixture.vault.join("index.md"), "preserved index\n").unwrap();
|
||||
restore_default_guides(&fixture.vault).unwrap();
|
||||
fs::write(fixture.vault.join("skills.md"), "stale skill index\n").unwrap();
|
||||
restore_default_guides(&fixture.vault, &fixture.projects()).unwrap();
|
||||
assert_eq!(
|
||||
fs::read_to_string(fixture.vault.join("purpose.md")).unwrap(),
|
||||
DEFAULT_PURPOSE
|
||||
read_default_asset("purpose.md").unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read_to_string(fixture.vault.join("schema.md")).unwrap(),
|
||||
DEFAULT_SCHEMA
|
||||
read_default_asset("schema.md").unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read_to_string(fixture.vault.join(INCLUDED_SKILLS[0])).unwrap(),
|
||||
read_default_asset(INCLUDED_SKILLS[0]).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read_to_string(fixture.vault.join("skills/custom.md")).unwrap(),
|
||||
custom_skill
|
||||
);
|
||||
let skills = fs::read_to_string(fixture.vault.join("skills.md")).unwrap();
|
||||
assert!(skills.contains("create-dev-brain-skill"));
|
||||
assert!(skills.contains("custom-skill"));
|
||||
assert_eq!(
|
||||
fs::read_to_string(fixture.vault.join("log.md")).unwrap(),
|
||||
"preserved log\n"
|
||||
|
||||
Reference in New Issue
Block a user