@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.
- package/README.md +15 -7
- package/README.zh-CN.md +15 -7
- package/package.json +6 -1
- package/plugin/hooks/codex/classify-message.py +3 -5
- package/plugin/hooks/codex/session-start.py +20 -7
- package/plugin/hooks/codex/validate-write.py +79 -87
- package/vault/thinking/.gitkeep +0 -0
- package/plugin/hooks/session-start.sh +0 -221
- package/vault/.claude/settings.json +0 -39
- package/vault/.claude/skills/dump/SKILL.md +0 -29
- package/vault/.claude/skills/ingest/SKILL.md +0 -63
- package/vault/.claude/skills/recall/SKILL.md +0 -54
- package/vault/.claude/skills/wrap-up/SKILL.md +0 -35
- package/vault/.codex/config.toml +0 -2
- package/vault/.codex/hooks.json +0 -28
- package/vault/.codex/skills/dump/SKILL.md +0 -29
- package/vault/.codex/skills/ingest/SKILL.md +0 -63
- package/vault/.codex/skills/recall/SKILL.md +0 -54
- package/vault/.codex/skills/wrap-up/SKILL.md +0 -35
- package/vault/.codex-vault/hooks/claude/classify-message.py +0 -305
- package/vault/.codex-vault/hooks/claude/session-start.py +0 -383
- package/vault/.codex-vault/hooks/claude/validate-write.py +0 -123
- package/vault/.codex-vault/hooks/codex/classify-message.py +0 -305
- package/vault/.codex-vault/hooks/codex/session-start.py +0 -387
- package/vault/.codex-vault/hooks/codex/validate-write.py +0 -127
- package/vault/AGENTS.md +0 -118
|
@@ -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
|