Install
$ agentstack add skill-humanerd-drew-opencode-drewgent-vault-naming-convention ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ● Filesystem access Used
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Vault Naming Convention
Every file in the {{AGENT_NAME}} vault is a node in the knowledge graph. Filename uniqueness ensures wikilinks resolve deterministically. P-layer prefix preserves priority identity. Exclusion boundaries keep operational noise out of the graph.
Why This Matters (The SEO of Knowledge)
Duplicate filenames waste "resolution budget" — the same way duplicate content wastes search engine crawl budget:
[[SOUL]]with 4SOUL.mdfiles → Obsidian guesses, possibly wrong[[SCHEMA]]with 3SCHEMA.mdfiles → ambiguous reference- Agent reading the vault must resolve ambiguity → compute waste
From NeuronFS: the P-layer prefix (P0-P6) is structural IDENTITY, not just a folder. Losing it in a short-name link loses priority information.
Rule: Unique Basenames in Structural Zones
Structural zones = paths that form the knowledge graph:
P0-brainstem/throughP6-prefrontal/skills/(each skill has its ownSKILL.md)YOUR_SITE/
Non-structural zones (excluded from Obsidian, can have duplicates):
source/— {{AGENTNAMELOWER}}-agent upstream codecron/output/— operational logs.trash/— recovery staging_agent/— runtime statenode_modules/,archive/
Enforcement
Every .md file in structural zones must have a unique basename (case-sensitive):
find ~/.{{AGENT_NAME_LOWER}} -name '*.md' \
-not -path '*/cron/*' -not -path '*/.trash/*' \
-not -path '*/source/*' -not -path '*/_agent/*' \
-not -path '*/node_modules/*' -not -path '*/archive/*' \
2>/dev/null | while read f; do basename "$f"; done \
| sort | uniq -c | sort -rn | awk '$1 > 1 {print $1, $2}'
Wikilink Convention by Scope
| Scope | Recommended Style | Example | |-------|------------------|---------| | Unique filename | Short name [[Slug]] | [[SELF_MODEL]], [[rules]] | | Non-unique filename | Full path [[P-layer/path/Slug]] | [[@identity/persona/SOUL]] (not bare [[SOUL]]) | | Skill file | [[skills/category/name/SKILL]] | [[skills/devops/kanban-worker/SKILL]] | | Neuron file | [[@identity/禁/slug]] | [[@identity/禁/禁task_qa_gate.neuron]] |
Obsidian Exclusion Configuration
~/.{{AGENT_NAME_LOWER}}/.obsidian/app.json controls what Obsidian indexes:
{
"extensionOverrides": [".neuron"],
"userIgnoreFilters": [
"cron/output",
"source",
".trash",
"_agent"
]
}
When to Exclude a Directory
A directory should be excluded from Obsidian when it contains:
- Operational logs (cron outputs, telemetry) — write-only by design, create fake graph edges
- Source code ({{AGENTNAMELOWER}}-agent upstream) — not vault content, 725MB of noise
- Recovery staging (.trash) — temporary, should not be in the graph
- Runtime state (_agent) — ephemeral, changes every session
Graph Quality Metrics
A healthy vault graph has:
| Metric | Target | Current (2026-06-14) | |--------|--------|----------------------| | Files with ≥1 wikilink | >90% | 76% | | Average wikilinks per file | 5-10 | 3.2 | | Bidirectional pairs | >60% | unknown | | Dominant star cluster size | /dev/null
Orphan files (0 inbound, 0 outbound)
Use recall("orphans") or scan manually
## Philosophy: Build It Right Once
The vault should require **zero maintenance** for naming hygiene. Not a monitoring cron job, not a report — just the right conventions enforced at creation time.
**User's explicit correction (2026-06-14)**: When investigating a vault problem, do NOT default to "let's set up a cron job that reports X." Instead, fix the root cause so there is nothing to report. "리포트를 보내면 끝? 관리를 해야지... 더 중요한건 관리할 일이 안생기게 애초에 잘하던가."
This applies to every vault-hygiene task: prefer structural fixes (rename, exclude, solve at source) over detection/reporting loops.
### Zero-Maintenance Principle
| Instead of this | Do this |
|----------------|---------|
| Cron job that reports duplicates | Ensure unique names at file creation |
| Monthly cleanup of noise files | Exclude operational dirs from Obsidian at vault setup |
| Script to fix broken wikilinks | Always use full-path `[[P-layer/path/Slug]]` for ambiguous names |
| Monitoring dashboard | One-time structural fix (the current state) |
**Decision tree**: when you spot a recurring vault issue, ask "can I make it impossible for this to happen again?" If yes, do that. A report/cron/alert is the fallback only when the root cause genuinely cannot be eliminated.
**When to exclude from Obsidian** (not "when to add a cleanup job"):
- `cron/output/` — operational logs, creates fake graph edges
- `source/` — upstream code, not vault content
- `.trash/` — recovery staging
- `_agent/` — runtime state, ephemeral
### Dual-Namespace Reality
`~/.{{AGENT_NAME_LOWER}}/` is shared by two systems:
| Namespace | Owner | Convention | Example files |
|-----------|-------|-----------|--------------|
| Root level | **Hermes Agent** runtime | No vault wikilinks to root files | `SOUL.md`, `config.yaml`, `.env`, `CHANGELOG.md` |
| P-layer dirs | **{{AGENT_NAME}} vault** knowledge | Always use full-path wikilinks | `P1-limbic/persona/SOUL.md`, `P0-brainstem/brain/rules.md` |
Hermes Agent's `_ensure_default_soul_md()` in `hermes_cli/config.py` seeds `~/SOUL.md` if missing — this is correct behavior. The root `SOUL.md` is the **Hermes system prompt**, not {{AGENT_NAME}}'s identity document. {{AGENT_NAME}}'s identity lives at `P1-limbic/persona/SOUL.md`.
**Rule**: Never create a vault wikilink to a root-level file. Root = Hermes runtime. P-layers = {{AGENT_NAME}} knowledge.
## Organic Mesh Connectivity
### Priority Order
When improving graph connectivity, follow this order (per user preference, 2026-06-14):
1. **P-layer core** — rules.md, SOUL.md, SELF_MODEL.md, SCHEMA.md, SKILL-INDEX.md, INTEGRATION_PROTOCOL.md, NEURONFS_RULES.md — add missing bidirectional, downward, and sideways links
2. **Skills** — related-skill cross-links between SKILL.md files in the same category
Content — SEO articles topic-based cross-links (lowest priority; see `references/seo-article-crosslinking-20260614.md` for full pipeline)
### Reference: Skill Cross-Linking Session
See `references/skill-cross-linking-20260614.md` for the detailed session log: which skills were linked, broken links found, and the `related_skills → wikilinks` conversion technique.
## Broken Link Audit Methodology
When auditing vault wikilinks, **most "broken" links found by naive grep are false positives**. The real broken count is far smaller. Always apply these filters:
### False Positive Categories
| Source of false positive | Pattern | Why it's OK |
|--------------------------|---------|-------------|
| **Template variables** | `[[{{project_id}}]]`, `[[projects/{{slug}}]]` | In `.episodes-template.md` and similar — never meant to resolve |
| **SEO article short-names** | `[[10 ChatGPT SEO Tools That Help You Rank Higher]]` | Obsidian resolves by basename — file is in `2025/` subdir |
| **Code block content** | `[[elapsedTime]]`, `[[totalTime]]`, `[[count]]` | Inside code fences — grep finds them, Obsidian ignores |
| **Script references** | `[[scripts/run_kanban_worker.py]]` | Not .md files — referenced as documentation, not graph nodes |
### Audit Protocol
```python
import re, os
vault_root = "~/.{{AGENT_NAME_LOWER}}"
all_md = []
for root, dirs, files in os.walk(vault_root):
dirs[:] = [d for d in dirs if d not in EXCLUDED]
for f in files:
if f.endswith('.md'):
all_md.append(os.path.join(root, f))
# Build basename resolution map (Obsidian's actual behavior)
basename_map = {}
for fp in all_md:
bn = os.path.basename(fp).replace('.md', '')
basename_map.setdefault(bn, []).append(fp)
def resolves(target):
t = target if target.endswith('.md') else target + '.md'
if os.path.exists(os.path.join(vault_root, t)):
return True
bn = target.replace('.md', '')
if bn in basename_map:
return True # Obsidian resolves by basename
if '.neuron' in target:
for r, d, f in os.walk(vault_root):
if os.path.basename(target) in f:
return True
return False
# True audit with code block filtering
for fp in all_md:
with open(fp, errors='replace') as f:
content = f.read()
# Strip code blocks to avoid false positives
clean = re.sub(r'```.*?```', '', content, flags=re.DOTALL)
clean = re.sub(r'`[^`]+`', '', clean)
found = re.findall(r'\[\[([^\]]+?)(?:\|[^\]]+)?\]\]', clean)
for link in found:
target = link.strip()
if target.endswith(('.png', '.jpg', '.svg', '.gif', '.py')):
continue
if not resolves(target):
TRULY_BROKEN.append((target, fp))
Common Fix Patterns
| Detected broken link | Cause | Fix | |----------------------|-------|-----| | [[@identity/.../禁/禁xxx]] | Missing .neuron extension | Add .neuron → [[@identity/.../禁/禁xxx.neuron]] | | [[skills/content-pipeline]] | SKILL.md nested under directory | Change to [[skills/content-pipeline/SKILL]] | | [[skills/seo-article-harvester]] | Same pattern | Change to [[skills/seo-article-harvester/SKILL]] |
Orphan & Passive File Detection
Orphans: 0 inbound + 0 outbound links. Usually archive, template, or write-only telemetry.
Passive files: inbound > 0 but outbound = 0. These receive links but never reciprocate — they're dead-ends in graph traversal.
Passive File Fix (bulk)
DESCRIPTION.md files in skills/ categories are the most common passive files:
- They're linked from SKILL-INDEX (inbound > 0)
- They have no
links:section (outbound = 0)
Fix: add outbound links to SKILL-INDEX and rules:
fixed = 0
for fp in glob.glob("skills/*/DESCRIPTION.md"):
content = read(fp)
if 'links:' in content[:500]:
continue
# Insert links before closing ---
content = content.replace(
'\n---\n\n',
'\nlinks:\n - "[[@action/skills/SKILL-INDEX]]"\n - "[[@identity/brain/rules]]"\n---\n\n'
)
write(fp, content)
Detection Script
# Build inbound/outbound maps
for fp in all_md:
with open(fp) as f:
content = f.read()
clean = re.sub(r'```.*?```', '', content, flags=re.DOTALL)
links = re.findall(r'\[\[([^\]]+?)(?:\|[^\]]+)?\]\]', clean)
for link in links:
target = link.strip().replace('.md', '')
if resolves(target):
inbound[target].add(slug)
outbound[slug].add(target)
# Find passive files
passive = [(s, len(inbound[s])) for s in all_slugs
if len(outbound.get(s, set())) == 0 and len(inbound.get(s, set())) > 0]
Bulk Cross-Linking Technique (for SEO articles)
When facing 2,500+ articles that need cross-links, use a 3-phase approach:
Phase 1: Convert Article → Hub (plain text → wikilink)
Many articles reference hub pages as plain text in their links: section:
links:
- SEO_ai_llm_search_Hub # Plain text — NOT a graph edge
- "[[@memory/.../index-by-topic]]" # Proper wikilink
Fix: replace SEO_X_Hub with [[SEO_X_Hub]] in the links list. Creates bidirectional edges since hubs already link to articles.
Phase 2: Hub → Hub Related Links
Hub pages (one per topic cluster) link to articles but not to each other. Add related-topic links:
hub_relations = {
'SEO_ai_llm_search_Hub': ['SEO_general_Hub', 'SEO_algorithm_updates_Hub'],
'SEO_technical_seo_Hub': ['SEO_onpage_seo_Hub', 'SEO_analytics_tools_Hub'],
# ... one pair per hub
}
Phase 3: Article ↔ Article within Cluster
For articles sharing the same cluster field, add cross-links:
for cluster, articles in cluster_groups.items():
all_fnames = [fn for _, fn in articles]
for fpath, fname, content in articles:
candidates = [fn for fn in all_fnames if fn != fname and fn not in existing_links]
targets = random.sample(candidates, min(3, len(candidates)))
# Add [[target]] to links section
Source-Domain Clustering (when no cluster field)
For articles that only have source-domain tags (e.g., tags: [seo, ahrefs.com]):
source_groups = defaultdict(list)
for fpath, content in articles:
tags = extract_tags(content)
source = [t for t in tags if '.' in t and t not in ('resource', 'seo')]
if source:
source_groups[source[0]].append((fpath, fname))
# Link articles sharing the same source domain
Inline Wikilink Injection (See Also Sections)
Frontmatter links: creates graph edges, but body wikilinks carry more weight in Obsidian Graph View. For articles with keyword/topic metadata, inject a ## See also section at the end:
# For SEO articles with keyword: field
keyword_to_topic = {
'온페이지 SEO': 'SEO_onpage_seo_Hub',
'콘텐츠 전략': 'SEO_content_strategy_Hub',
# ... map keywords to hub pages
}
for fp in article_files:
kw_match = re.search(r'^keyword:\s*(.+)', content, re.MULTILINE)
if not kw_match:
continue
topics = {keyword_to_topic[k.strip()] for k in kw_match.group(1).split(',')
if k.strip() in keyword_to_topic}
if not topics:
continue
# Add at end:
topic_links = '\n'.join([f'- [[{t}]]' for t in sorted(topics)])
content += f'\n\n## See also\n{topic_links}\n'
This is safe for automation: it never modifies frontmatter, never breaks existing formatting, and only adds content at the very end of the file.
Mesh Linking Strategy
A healthy knowledge graph has cycles (A↔B↔C) not just stars (hub→leaves). Add links in three directions:
UPWARD: child → parent concept (P2-memory → P5-ego/SELF_MODEL)
DOWNWARD: parent → child detail (rules.md → specific neuron file)
SIDEWAYS: peer → peer related (P2-SCHEMA ↔ P4-INTEGRATION_PROTOCOL)
For each core file, check:
- ✅ Upward: Does it link to SELF_MODEL, rules, or its parent layer?
- ❌ Downward: Does the parent link back to its children?
- ❌ Sideways: Does it link to related files in other P-layers?
Bidirectional Completeness
If A → B exists, B → A should too, unless B is a pure index/hub page (SKILL-INDEX, index-by-topic). Reasonable exception: SEO article hub pages don't need backlinks from every article.
Cross-Linking Technique: Systematic Audit
When adding cross-links to a cluster (P-layer, skill category, etc.), use this 4-step process:
# Step 1: Identify the cluster
# Scan all files in a category/directory for current link counts
for f in ~/.{{AGENT_NAME_LOWER}}/path/to/cluster/*/SKILL.md; do
name=$(basename "$(dirname "$f")")
count=$(sed -n '/^links:/,/^---/p' "$f" | grep -c '\[\[\|')
echo "$count $name"
done | sort -rn
# Step 2: Identify natural relationships
# Check for `related_skills` in metadata that are NOT yet in `links:`
grep -A2 'related_skills:' ~/.{{AGENT_NAME_LOWER}}/path/to/cluster/*/SKILL.md
# Step 3: For each skill with related_skills in metadata,
# add those as actual `links:` wikilinks (metadata doesn't create graph edges)
# Then add bidirectional links between sibling skills
# Step 4: Verify all new wikilinks resolve
for link in "list/of/newly/added/links"; do
find ~/.{{AGENT_NAME_LOWER}} -path "*/${link}.md" | grep -q . && echo "OK" || echo "BROKEN"
done
Converting related_skills (Metadata) to links (Graph Edges)
Many SKILL.md files have related_skills in their YAML metadata (metadata.hermes.related_skills) but no actual links: in frontmatter. Metadata is machine-readable but does NOT create Obsidian graph edges.
Pattern found (2026-06-14): ~15 skills across software-development/ and brain/ had related_skills arrays that were never converted to wikilinks. Example:
python-debugpyhadrelated_skills: [systematic-debugging, node-inspect-debugger]in metadata but nolinks:sectionsimplify-codehadrelated_skills: [requesting-code-review, test-driven-development, plan]in metadata
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: humanerd-drew
- Source: humanerd-drew/opencode-drewgent
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.