@suwujs/codex-vault 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,387 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Session-start hook for Codex CLI — injects vault context into the agent's prompt.
3
-
4
- Outputs structured JSON: additionalContext for LLM.
5
- Banner is written to stderr (Codex CLI does not render systemMessage).
6
-
7
- Dynamic context: adapts git log window, reads full North Star,
8
- shows all active work, and includes uncommitted changes.
9
- """
10
- import json
11
- import os
12
- import re
13
- import subprocess
14
- import sys
15
- from collections import Counter
16
- from datetime import datetime
17
- from pathlib import Path
18
-
19
-
20
- def _find_vault_root():
21
- """Find vault root from CWD — check for Home.md/brain/, then vault/ subdir."""
22
- cwd = os.environ.get("CODEX_PROJECT_DIR", os.getcwd())
23
- if os.path.isfile(os.path.join(cwd, "Home.md")) or os.path.isdir(os.path.join(cwd, "brain")):
24
- return cwd
25
- vault_sub = os.path.join(cwd, "vault")
26
- if os.path.isdir(vault_sub) and (
27
- os.path.isfile(os.path.join(vault_sub, "Home.md")) or
28
- os.path.isdir(os.path.join(vault_sub, "brain"))
29
- ):
30
- return vault_sub
31
- return None
32
-
33
-
34
- def _run_git(args, cwd, timeout=5):
35
- """Run git command and return stdout lines."""
36
- try:
37
- result = subprocess.run(
38
- ["git"] + args,
39
- capture_output=True, text=True, cwd=cwd, timeout=timeout,
40
- )
41
- return result.stdout.strip().splitlines() if result.stdout.strip() else []
42
- except Exception:
43
- return []
44
-
45
-
46
- def _git_log_oneline(cwd, since=None, max_count=None):
47
- """Get git log --oneline entries."""
48
- args = ["log", "--oneline", "--no-merges"]
49
- if since:
50
- args.append(f"--since={since}")
51
- if max_count:
52
- args.extend(["-n", str(max_count)])
53
- return _run_git(args, cwd)
54
-
55
-
56
- def _git_status_short(cwd):
57
- """Get git status --short output."""
58
- return _run_git(["status", "--short", "--", "."], cwd)
59
-
60
-
61
- def _read_file(path):
62
- """Read file content, return empty string on error."""
63
- try:
64
- return Path(path).read_text(encoding="utf-8")
65
- except Exception:
66
- return ""
67
-
68
-
69
- def _find_md_files(vault_dir):
70
- """Find all .md files in vault, excluding non-vault directories."""
71
- exclude = {".git", ".obsidian", "thinking", ".claude", ".codex",
72
- ".codex-vault", ".codex-mem", "node_modules"}
73
- files = []
74
- for root, dirs, filenames in os.walk(vault_dir):
75
- dirs[:] = [d for d in dirs if d not in exclude]
76
- for f in filenames:
77
- if f.endswith(".md"):
78
- rel = os.path.relpath(os.path.join(root, f), vault_dir)
79
- files.append(f"./{rel}")
80
- return sorted(files)
81
-
82
-
83
- def _folder_summary(all_files):
84
- """Generate folder summary with file counts."""
85
- folders = Counter()
86
- for f in all_files:
87
- parts = f[2:].split("/") # strip ./
88
- folders[parts[0] if len(parts) > 1 else "."] += 1
89
- return [f" {folder}/ ({count} files)"
90
- for folder, count in folders.most_common()]
91
-
92
-
93
- def _key_files(all_files):
94
- """Filter for key vault files."""
95
- pattern = re.compile(
96
- r"(Home|Index|North Star|Memories|Key Decisions|Patterns|log)\.md$")
97
- return [f for f in all_files if pattern.search(f)]
98
-
99
-
100
- def _mtime_ok(vault_dir, rel_path, cutoff):
101
- """Check if file was modified after cutoff timestamp."""
102
- try:
103
- return os.path.getmtime(os.path.join(vault_dir, rel_path.lstrip("./"))) > cutoff
104
- except Exception:
105
- return False
106
-
107
-
108
- def _north_star_goal(vault_dir):
109
- """Extract first goal from North Star for banner display."""
110
- ns_path = os.path.join(vault_dir, "brain", "North Star.md")
111
- if not os.path.isfile(ns_path):
112
- return None
113
- content = _read_file(ns_path)
114
- in_focus = False
115
- for line in content.splitlines():
116
- if re.match(r"^## Current Focus", line):
117
- in_focus = True
118
- continue
119
- if in_focus and line.startswith("## "):
120
- break
121
- if in_focus and re.match(r"^- .+", line):
122
- goal = line[2:].strip()
123
- return goal[:40] if goal else None
124
- return None
125
-
126
-
127
- # ── Context builder (→ additionalContext for LLM) ──────────────────────
128
-
129
-
130
- def _build_context(vault_dir):
131
- lines = []
132
- lines.append("## Session Context")
133
- lines.append("")
134
-
135
- # Date
136
- lines.append("### Date")
137
- now = datetime.now()
138
- lines.append(f"{now.strftime('%Y-%m-%d')} ({now.strftime('%A')})")
139
- lines.append("")
140
-
141
- # North Star — full file
142
- lines.append("### North Star")
143
- ns_path = os.path.join(vault_dir, "brain", "North Star.md")
144
- if os.path.isfile(ns_path):
145
- lines.append(_read_file(ns_path))
146
- else:
147
- lines.append("(No North Star found — create brain/North Star.md to set goals)")
148
- lines.append("")
149
-
150
- # Recent changes — adaptive window
151
- lines.append("### Recent Changes")
152
- commits_48h = _git_log_oneline(vault_dir, since="48 hours ago")
153
- if commits_48h:
154
- lines.append("(last 48 hours)")
155
- lines.extend(commits_48h[:15])
156
- else:
157
- commits_7d = _git_log_oneline(vault_dir, since="7 days ago")
158
- if commits_7d:
159
- lines.append("(nothing in 48h — showing last 7 days)")
160
- lines.extend(commits_7d[:15])
161
- else:
162
- lines.append("(nothing recent — showing last 5 commits)")
163
- commits = _git_log_oneline(vault_dir, max_count=5)
164
- lines.extend(commits if commits else ["(no git history)"])
165
- lines.append("")
166
-
167
- # Recent operations from log.md
168
- lines.append("### Recent Operations")
169
- log_path = os.path.join(vault_dir, "log.md")
170
- if os.path.isfile(log_path):
171
- entries = [l for l in _read_file(log_path).splitlines()
172
- if l.startswith("## [")]
173
- lines.extend(entries[-5:] if entries else ["(no entries in log.md)"])
174
- else:
175
- lines.append("(no log.md)")
176
- lines.append("")
177
-
178
- # Active work
179
- lines.append("### Active Work")
180
- active_dir = os.path.join(vault_dir, "work", "active")
181
- if os.path.isdir(active_dir):
182
- work_files = sorted(f for f in os.listdir(active_dir) if f.endswith(".md"))
183
- if work_files:
184
- for f in work_files:
185
- lines.append(f.replace(".md", ""))
186
- else:
187
- lines.append("(none)")
188
- else:
189
- lines.append("(no work/active/ directory)")
190
- lines.append("")
191
-
192
- # Uncommitted changes
193
- lines.append("### Uncommitted Changes")
194
- changes = _git_status_short(vault_dir)
195
- lines.extend(changes[:20] if changes else ["(working tree clean)"])
196
- lines.append("")
197
-
198
- # Recently modified brain files
199
- lines.append("### Recently Modified Brain Files")
200
- brain_dir = os.path.join(vault_dir, "brain")
201
- if os.path.isdir(brain_dir):
202
- cutoff = datetime.now().timestamp() - 7 * 86400
203
- recent = sorted(
204
- f.replace(".md", "")
205
- for f in os.listdir(brain_dir)
206
- if f.endswith(".md") and os.path.getmtime(os.path.join(brain_dir, f)) > cutoff
207
- )
208
- if recent:
209
- lines.append("(modified in last 7 days)")
210
- lines.extend(recent)
211
- else:
212
- lines.append("(no recent changes)")
213
- lines.append("")
214
-
215
- # Vault file listing — tiered
216
- lines.append("### Vault Files")
217
- all_files = _find_md_files(vault_dir)
218
- n = len(all_files)
219
-
220
- if n <= 20:
221
- lines.extend(all_files)
222
-
223
- elif n <= 50:
224
- hot = [f for f in all_files
225
- if not re.match(r"\./sources/|\./work/archive/", f)]
226
- cold = n - len(hot)
227
- lines.extend(hot)
228
- if cold > 0:
229
- lines.append("")
230
- lines.append(f"(+ {cold} files in sources/ and work/archive/ — use /recall to search)")
231
-
232
- elif n <= 150:
233
- lines.append(f"({n} files — showing summary)")
234
- lines.append("")
235
- lines.extend(_folder_summary(all_files))
236
- lines.append("")
237
- lines.append("Recently modified (7 days):")
238
- cutoff = datetime.now().timestamp() - 7 * 86400
239
- recent = [f for f in all_files if _mtime_ok(vault_dir, f, cutoff)]
240
- lines.extend(recent if recent else [" (none)"])
241
- lines.append("")
242
- lines.append("Key files:")
243
- lines.extend(_key_files(all_files))
244
-
245
- else:
246
- lines.append(f"({n} files — showing summary)")
247
- lines.append("")
248
- lines.extend(_folder_summary(all_files))
249
- lines.append("")
250
- lines.append("Recently modified (3 days):")
251
- cutoff = datetime.now().timestamp() - 3 * 86400
252
- recent = [f for f in all_files if _mtime_ok(vault_dir, f, cutoff)]
253
- lines.extend(recent if recent else [" (none)"])
254
- lines.append("")
255
- lines.append("Key files:")
256
- lines.extend(_key_files(all_files))
257
- lines.append("")
258
- lines.append("Use /recall <topic> to search the vault.")
259
-
260
- return "\n".join(lines)
261
-
262
-
263
- # ── ANSI colors (oh-my-codex style) ──────────────────────────────────
264
-
265
- RESET = "\x1b[0m"
266
- BOLD = "\x1b[1m"
267
- DIM = "\x1b[2m"
268
- CYAN = "\x1b[36m"
269
- GREEN = "\x1b[32m"
270
- YELLOW = "\x1b[33m"
271
- SEP = f" {DIM}|{RESET} "
272
-
273
-
274
- def _c(code, text):
275
- return f"{code}{text}{RESET}"
276
-
277
-
278
- # ── Banner builder (→ stderr for terminal) ────────────────────────────
279
-
280
-
281
- def _build_banner(vault_dir):
282
- # --- Line 1: statusline ---
283
- elements = []
284
-
285
- # Git branch
286
- branch = _run_git(["rev-parse", "--abbrev-ref", "HEAD"], vault_dir)
287
- if branch:
288
- elements.append(_c(CYAN, branch[0]))
289
-
290
- # North Star goal
291
- goal = _north_star_goal(vault_dir)
292
- if goal:
293
- elements.append(f"\U0001f3af {goal}")
294
- else:
295
- elements.append(_c(DIM, "\U0001f3af no goal set"))
296
-
297
- # Active work
298
- active_dir = os.path.join(vault_dir, "work", "active")
299
- work_files = sorted(
300
- f.replace(".md", "")
301
- for f in os.listdir(active_dir) if f.endswith(".md")
302
- ) if os.path.isdir(active_dir) else []
303
- if work_files:
304
- names = ", ".join(work_files[:3])
305
- suffix = f" +{len(work_files) - 3}" if len(work_files) > 3 else ""
306
- elements.append(_c(GREEN, f"active:{len(work_files)}") + _c(DIM, f" {names}{suffix}"))
307
- else:
308
- elements.append(_c(DIM, "active:0"))
309
-
310
- # Changes
311
- changes = _git_status_short(vault_dir)
312
- if changes:
313
- elements.append(_c(YELLOW, f"changes:{len(changes)}"))
314
- else:
315
- elements.append(_c(GREEN, "clean"))
316
-
317
- label = _c(BOLD, "[Vault]")
318
- statusline = label + " " + SEP.join(elements)
319
-
320
- # --- Line 2+: recent commits ---
321
- lines = ["", statusline]
322
- commits = _git_log_oneline(vault_dir, since="7 days ago")
323
- if not commits:
324
- commits = _git_log_oneline(vault_dir, max_count=3)
325
- if commits:
326
- for c in commits[:3]:
327
- # hash in cyan, message in dim
328
- parts = c.split(" ", 1)
329
- if len(parts) == 2:
330
- lines.append(f" {_c(DIM, parts[0])} {_c(DIM, parts[1])}")
331
- else:
332
- lines.append(f" {_c(DIM, c)}")
333
-
334
- # --- Line: vault file count ---
335
- all_files = _find_md_files(vault_dir)
336
- lines.append(_c(DIM, f" {len(all_files)} notes"))
337
- lines.append("")
338
-
339
- return "\n".join(lines)
340
-
341
-
342
- # ── Main ───────────────────────────────────────────────────────────────
343
-
344
-
345
- def main():
346
- vault_dir = _find_vault_root()
347
- if not vault_dir:
348
- output = {
349
- "hookSpecificOutput": {
350
- "hookEventName": "SessionStart",
351
- "additionalContext": "## Session Context\n\n(No vault found)"
352
- }
353
- }
354
- json.dump(output, sys.stdout)
355
- sys.exit(0)
356
-
357
- # Read hook input for session metadata
358
- try:
359
- event = json.load(sys.stdin)
360
- except Exception:
361
- event = {}
362
-
363
- context = _build_context(vault_dir)
364
- banner = _build_banner(vault_dir)
365
-
366
- # Codex CLI: systemMessage not rendered by TUI,
367
- # use stderr for terminal visibility (best effort)
368
- sys.stderr.write(banner + "\n")
369
-
370
- output = {
371
- "hookSpecificOutput": {
372
- "hookEventName": "SessionStart",
373
- "additionalContext": context
374
- },
375
- }
376
-
377
- json.dump(output, sys.stdout)
378
- sys.stdout.flush()
379
- sys.exit(0)
380
-
381
-
382
- if __name__ == "__main__":
383
- try:
384
- main()
385
- except Exception:
386
- # Never block session start
387
- sys.exit(0)
@@ -1,127 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Post-write validation for vault notes — Codex CLI version.
3
-
4
- Checks frontmatter and wikilinks on any .md file written to the vault.
5
- Outputs hookSpecificOutput for Codex CLI. Feedback via stderr
6
- (Codex CLI does not render systemMessage).
7
-
8
- Note: Codex CLI PostToolUse only supports Bash matcher, so this script
9
- will only run when triggered by a Bash tool writing .md files. The
10
- validation logic is identical to the Claude Code version.
11
- """
12
- import json
13
- import re
14
- import sys
15
- import os
16
- from pathlib import Path
17
-
18
-
19
- def _check_log_format(content):
20
- """Validate log.md entry format: ## [YYYY-MM-DD] <type> | <title>"""
21
- warnings = []
22
- for i, line in enumerate(content.splitlines(), 1):
23
- if line.startswith("## ") and not line.startswith("## ["):
24
- # Heading that looks like a log entry but missing date brackets
25
- if any(t in line.lower() for t in ["ingest", "session", "query", "maintenance", "decision", "archive"]):
26
- warnings.append(f"Line {i}: log entry missing date format — expected `## [YYYY-MM-DD] <type> | <title>`")
27
- elif line.startswith("## ["):
28
- if not re.match(r"^## \[\d{4}-\d{2}-\d{2}\] \w+", line):
29
- warnings.append(f"Line {i}: malformed log entry — expected `## [YYYY-MM-DD] <type> | <title>`")
30
- return warnings
31
-
32
-
33
- def main():
34
- try:
35
- input_data = json.load(sys.stdin)
36
- except (ValueError, EOFError, OSError):
37
- sys.exit(0)
38
-
39
- tool_input = input_data.get("tool_input")
40
- if not isinstance(tool_input, dict):
41
- sys.exit(0)
42
-
43
- file_path = tool_input.get("file_path", "")
44
- if not isinstance(file_path, str) or not file_path:
45
- sys.exit(0)
46
-
47
- if not file_path.endswith(".md"):
48
- sys.exit(0)
49
-
50
- normalized = file_path.replace("\\", "/")
51
- basename = os.path.basename(normalized)
52
-
53
- # Skip non-vault files
54
- skip_names = {"README.md", "CHANGELOG.md", "CONTRIBUTING.md", "CLAUDE.md", "AGENTS.md", "LICENSE"}
55
- if basename in skip_names:
56
- sys.exit(0)
57
- if basename.startswith("README.") and basename.endswith(".md"):
58
- sys.exit(0)
59
-
60
- skip_paths = [".claude/", ".codex/", ".codex-vault/", ".mind/", "templates/", "thinking/", "node_modules/", "plugin/", "docs/"]
61
- if any(skip in normalized for skip in skip_paths):
62
- sys.exit(0)
63
-
64
- warnings = []
65
-
66
- try:
67
- content = Path(file_path).read_text(encoding="utf-8")
68
-
69
- if not content.startswith("---"):
70
- warnings.append("Missing YAML frontmatter")
71
- else:
72
- parts = content.split("---", 2)
73
- if len(parts) >= 3:
74
- fm = parts[1]
75
- if "date:" not in fm and basename != "log.md":
76
- warnings.append("Missing `date` in frontmatter")
77
- if "tags:" not in fm:
78
- warnings.append("Missing `tags` in frontmatter")
79
- if "description:" not in fm:
80
- warnings.append("Missing `description` in frontmatter (~150 chars)")
81
-
82
- if len(content) > 300 and "[[" not in content:
83
- warnings.append("No [[wikilinks]] found — every note should link to at least one other note")
84
-
85
- # Check for unfilled template placeholders
86
- placeholders = re.findall(r"\{\{[^}]+\}\}", content)
87
- if placeholders:
88
- examples = ", ".join(placeholders[:3])
89
- warnings.append(f"Unfilled template placeholders found: {examples}")
90
-
91
- # Validate log.md format
92
- if basename == "log.md":
93
- log_warnings = _check_log_format(content)
94
- warnings.extend(log_warnings)
95
-
96
- except Exception:
97
- sys.exit(0)
98
-
99
- if warnings:
100
- hint_list = "\n".join(f" - {w}" for w in warnings)
101
- count = len(warnings)
102
- first = warnings[0]
103
- if count == 1:
104
- feedback = f"\u26a0\ufe0f vault: {basename} — {first}"
105
- else:
106
- feedback = f"\u26a0\ufe0f vault: {basename} — {first} (+{count - 1} more)"
107
-
108
- # Codex CLI: use stderr for feedback (no systemMessage rendering)
109
- sys.stderr.write(f" {feedback}\n")
110
-
111
- output = {
112
- "hookSpecificOutput": {
113
- "hookEventName": "PostToolUse",
114
- "additionalContext": f"Vault warnings for `{basename}`:\n{hint_list}\nFix these before moving on."
115
- },
116
- }
117
- json.dump(output, sys.stdout)
118
- sys.stdout.flush()
119
-
120
- sys.exit(0)
121
-
122
-
123
- if __name__ == "__main__":
124
- try:
125
- main()
126
- except Exception:
127
- sys.exit(0)
package/vault/AGENTS.md DELETED
@@ -1,118 +0,0 @@
1
- # Codex-Vault — Core Instructions
2
-
3
- A structured knowledge vault maintained by an LLM agent. You write notes, maintain links, and keep indexes current. The human curates sources, directs analysis, and asks questions.
4
-
5
- ## Vault Structure
6
-
7
- | Folder | Purpose |
8
- |--------|---------|
9
- | `Home.md` | Vault entry point — quick links, current focus |
10
- | `brain/` | Persistent memory — goals, decisions, patterns |
11
- | `work/` | Work notes index (`Index.md`) |
12
- | `work/active/` | Current projects (move to archive when done) |
13
- | `work/archive/` | Completed work |
14
- | `templates/` | Note templates with YAML frontmatter |
15
- | `sources/` | Raw source documents — immutable, LLM reads only |
16
- | `thinking/` | Scratchpad — promote findings, then delete |
17
- | `reference/` | Saved answers and analyses from query writeback |
18
-
19
- ## Session Lifecycle
20
-
21
- ### Start
22
-
23
- The SessionStart hook injects: North Star goals, recent git changes, active work, vault file listing. You start with context, not a blank slate.
24
-
25
- ### Work
26
-
27
- 1. The classify hook detects intent and suggests skills — **do not auto-execute**. Suggest the skill to the user and let them decide.
28
- 2. Available skills: `/dump`, `/recall`, `/ingest`, `/wrap-up`
29
- 3. Search before creating — check if a related note exists (use `/recall <topic>` for targeted vault search)
30
- 4. Update `work/Index.md` if a new note was created
31
-
32
- ### End
33
-
34
- When the user says "wrap up" or similar:
35
- 1. Verify new notes have frontmatter and wikilinks
36
- 2. Update `work/Index.md` with any new or completed notes
37
- 3. Archive completed projects: move from `work/active/` to `work/archive/`
38
- 4. Check if `brain/` notes need updating with new decisions or patterns
39
-
40
- ## Creating Notes
41
-
42
- 1. **Always use YAML frontmatter**: `date`, `description` (~150 chars), `tags`
43
- 2. **Use templates** from `templates/`
44
- 3. **Place files correctly**: active work in `work/active/`, completed in `work/archive/`, source summaries in `work/active/` (tag: `source-summary`), drafts in `thinking/`
45
- 4. **Name files descriptively** — use the note title as filename
46
-
47
- ## Linking — Critical
48
-
49
- **Graph-first.** Folders group by purpose, links group by meaning. A note lives in one folder but links to many notes.
50
-
51
- **A note without links is a bug.** Every new note must link to at least one existing note via `[[wikilinks]]`.
52
-
53
- Link syntax:
54
- - `[[Note Title]]` — standard wikilink
55
- - `[[Note Title|display text]]` — aliased
56
- - `[[Note Title#Heading]]` — deep link
57
-
58
- ### When to Link
59
-
60
- - Work note ↔ Decision Record (bidirectional)
61
- - Index → all work notes
62
- - North Star → active projects
63
- - Memories → source notes
64
-
65
- ## Memory System
66
-
67
- All persistent memory lives in `brain/`:
68
-
69
- | File | Stores |
70
- |------|--------|
71
- | `North Star.md` | Goals and focus areas — read every session |
72
- | `Memories.md` | Index of memory topics |
73
- | `Key Decisions.md` | Decisions worth recalling across sessions |
74
- | `Patterns.md` | Recurring patterns discovered across work |
75
-
76
- When asked to "remember" something: write to the appropriate `brain/` file with a wikilink to context.
77
-
78
- ## Sources & Ingest
79
-
80
- `sources/` holds raw source documents (articles, papers, web clips). This is the immutable layer — the agent reads from it but never modifies source files.
81
-
82
- - Drop raw files into `sources/` (markdown preferred) or use `/ingest` with a URL
83
- - `/ingest` reads the source, discusses key takeaways, then creates a **Source Summary** in `work/active/` with tag `source-summary`
84
- - The summary uses the Source Summary template: Key Takeaways, Summary, Connections, Quotes/Data Points
85
- - Every ingest updates `work/Index.md` (Sources section) and checks for cross-links to existing notes
86
- - If the source contains decisions or patterns, update the relevant `brain/` notes too
87
- - Source summaries link back to the raw source via the `source` frontmatter field
88
-
89
- ## Operation Log
90
-
91
- Append to `log.md` after significant operations: ingests, decisions, project archives, maintenance passes.
92
-
93
- - Format: `## [YYYY-MM-DD] <type> | <title>` followed by bullet points
94
- - Types: `ingest`, `session`, `query`, `maintenance`, `decision`, `archive`
95
- - Don't log every small edit — only operations that change the vault's knowledge state
96
- - Entries are append-only; never edit or delete previous entries
97
-
98
- ## Query Writeback
99
-
100
- When answering a substantial question that synthesizes multiple vault notes:
101
-
102
- 1. Offer: "This answer could be useful later — want me to save it as a reference note?"
103
- 2. If yes, create a Reference Note in `reference/` using the template
104
- 3. Link the reference note from related work notes in `## Related`
105
- 4. Add the reference note to `work/Index.md` under `## Reference`
106
- 5. Don't prompt for trivial questions — only for answers that synthesize, compare, or analyze
107
-
108
- ## Vault Location
109
-
110
- The vault may live at the project root or in a `vault/` subdirectory. Use the SessionStart context to determine the actual path. All folder references above (e.g. `brain/`, `work/active/`) are relative to the vault root.
111
-
112
- ## Rules
113
-
114
- - Preserve existing frontmatter when editing notes
115
- - Always check for and suggest connections between notes
116
- - Every note must have a `description` field (~150 chars)
117
- - When reorganizing, never delete without user confirmation
118
- - Use `[[wikilinks]]` not markdown links