@suwujs/codex-vault 0.6.0 → 0.7.0
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 +7 -7
- package/README.zh-CN.md +7 -7
- package/bin/codex-vault-run.sh +28 -0
- package/package.json +1 -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/.codex-vault/hooks/codex/classify-message.py +6 -10
- package/vault/.codex-vault/hooks/codex/session-start.py +15 -18
- package/vault/AGENTS.md +1 -118
- package/plugin/hooks/session-start.sh +0 -221
package/README.md
CHANGED
|
@@ -63,20 +63,20 @@ git commit (persistent)
|
|
|
63
63
|
Next session → session-start hook injects context back
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
Hooks power the loop:
|
|
67
67
|
|
|
68
|
-
| Hook | When | What |
|
|
69
|
-
|
|
70
|
-
| **session-start** | Agent starts | Injects North Star goals, recent git changes, active work, vault file listing |
|
|
71
|
-
| **classify-message** | Every message | Detects decisions, wins, project updates — hints the agent where to file them |
|
|
72
|
-
| **validate-write** | After writing `.md` | Checks frontmatter and wikilinks — catches mistakes before they stick |
|
|
68
|
+
| Hook | When | What | Claude Code | Codex CLI |
|
|
69
|
+
|------|------|------|-------------|-----------|
|
|
70
|
+
| **session-start** | Agent starts | Injects North Star goals, recent git changes, active work, vault file listing | SessionStart | SessionStart |
|
|
71
|
+
| **classify-message** | Every message | Detects decisions, wins, project updates — hints the agent where to file them | UserPromptSubmit | UserPromptSubmit |
|
|
72
|
+
| **validate-write** | After writing `.md` | Checks frontmatter and wikilinks — catches mistakes before they stick | PostToolUse (Write\|Edit) | N/A (Codex only supports Bash) |
|
|
73
73
|
|
|
74
74
|
## Supported Agents
|
|
75
75
|
|
|
76
76
|
| Agent | Hooks | Skills | Status |
|
|
77
77
|
|-------|-------|--------|--------|
|
|
78
78
|
| Claude Code | 3 hooks via `.claude/settings.json` | `/dump` `/recall` `/ingest` `/wrap-up` | Full support |
|
|
79
|
-
| Codex CLI |
|
|
79
|
+
| Codex CLI | 2 hooks via `.codex/hooks.json` | `$dump` `$recall` `$ingest` `$wrap-up` | Full support (PostToolUse limited to Bash by Codex) |
|
|
80
80
|
| Other | Write an adapter ([docs/adding-an-agent.md](docs/adding-an-agent.md)) | Depends on agent | Community |
|
|
81
81
|
|
|
82
82
|
## Vault Structure
|
package/README.zh-CN.md
CHANGED
|
@@ -95,20 +95,20 @@ git commit(持久化)
|
|
|
95
95
|
下次 session → session-start hook 注入上下文
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
Hook 驱动整个循环:
|
|
99
99
|
|
|
100
|
-
| Hook | 触发时机 | 做什么 |
|
|
101
|
-
|
|
102
|
-
| **session-start** | Agent 启动 | 注入 North Star 目标、近期 git 变更、活跃项目、vault 文件清单 |
|
|
103
|
-
| **classify-message** | 每条消息 | 检测决策、成果、项目更新 — 提示 agent 该归档到哪里 |
|
|
104
|
-
| **validate-write** | 写 `.md` 后 | 检查 frontmatter 和 wikilinks — 在落盘前纠错 |
|
|
100
|
+
| Hook | 触发时机 | 做什么 | Claude Code | Codex CLI |
|
|
101
|
+
|------|---------|--------|-------------|-----------|
|
|
102
|
+
| **session-start** | Agent 启动 | 注入 North Star 目标、近期 git 变更、活跃项目、vault 文件清单 | SessionStart | SessionStart |
|
|
103
|
+
| **classify-message** | 每条消息 | 检测决策、成果、项目更新 — 提示 agent 该归档到哪里 | UserPromptSubmit | UserPromptSubmit |
|
|
104
|
+
| **validate-write** | 写 `.md` 后 | 检查 frontmatter 和 wikilinks — 在落盘前纠错 | PostToolUse (Write\|Edit) | 不支持(Codex 仅支持 Bash) |
|
|
105
105
|
|
|
106
106
|
## 支持的 Agent
|
|
107
107
|
|
|
108
108
|
| Agent | Hooks | Skills | 状态 |
|
|
109
109
|
|-------|-------|--------|------|
|
|
110
110
|
| Claude Code | `.claude/settings.json` 3 hooks | `/dump` `/recall` `/ingest` `/wrap-up` | 完整支持 |
|
|
111
|
-
| Codex CLI | `.codex/hooks.json`
|
|
111
|
+
| Codex CLI | `.codex/hooks.json` 2 hooks | `$dump` `$recall` `$ingest` `$wrap-up` | 完整支持(PostToolUse 受 Codex 限制仅支持 Bash) |
|
|
112
112
|
| 其他 | 写适配器([指南](docs/adding-an-agent.md)) | 取决于 agent | 社区贡献 |
|
|
113
113
|
|
|
114
114
|
## Vault 结构
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Codex-Vault wrapper — shows vault banner then launches Codex CLI
|
|
3
|
+
# Usage: codex-vault-run [codex args...]
|
|
4
|
+
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
6
|
+
PROJECT_DIR="$(pwd)"
|
|
7
|
+
|
|
8
|
+
# Find session-start hook relative to project or plugin
|
|
9
|
+
HOOK=""
|
|
10
|
+
for candidate in \
|
|
11
|
+
"$PROJECT_DIR/plugin/hooks/codex/session-start.py" \
|
|
12
|
+
"$PROJECT_DIR/.codex-vault/hooks/codex/session-start.py" \
|
|
13
|
+
"$SCRIPT_DIR/../plugin/hooks/codex/session-start.py"; do
|
|
14
|
+
if [ -f "$candidate" ]; then
|
|
15
|
+
HOOK="$candidate"
|
|
16
|
+
break
|
|
17
|
+
fi
|
|
18
|
+
done
|
|
19
|
+
|
|
20
|
+
if [ -n "$HOOK" ]; then
|
|
21
|
+
SUMMARY=$(echo '{}' | python3 "$HOOK" 2>/dev/null \
|
|
22
|
+
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('systemMessage',''))" 2>/dev/null)
|
|
23
|
+
if [ -n "$SUMMARY" ]; then
|
|
24
|
+
echo " $SUMMARY"
|
|
25
|
+
fi
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
exec codex "$@"
|
package/package.json
CHANGED
|
@@ -283,16 +283,14 @@ def main():
|
|
|
283
283
|
icon = "\U0001f504" if mode == "auto" else "\U0001f4a1"
|
|
284
284
|
label = ", ".join(feedback_parts) if feedback_parts else "intent detected"
|
|
285
285
|
|
|
286
|
-
#
|
|
287
|
-
sys.stderr.write(f" {icon} vault: {label}\n")
|
|
288
|
-
|
|
286
|
+
# hookSpecificOutput + additionalContext → injected into LLM context
|
|
289
287
|
output = {
|
|
290
288
|
"hookSpecificOutput": {
|
|
291
289
|
"hookEventName": "UserPromptSubmit",
|
|
292
290
|
"additionalContext": context
|
|
293
|
-
}
|
|
291
|
+
}
|
|
294
292
|
}
|
|
295
|
-
json.
|
|
293
|
+
sys.stdout.write(json.dumps(output) + "\n")
|
|
296
294
|
sys.stdout.flush()
|
|
297
295
|
|
|
298
296
|
sys.exit(0)
|
|
@@ -351,7 +351,7 @@ def main():
|
|
|
351
351
|
"additionalContext": "## Session Context\n\n(No vault found)"
|
|
352
352
|
}
|
|
353
353
|
}
|
|
354
|
-
json.
|
|
354
|
+
sys.stdout.write(json.dumps(output) + "\n")
|
|
355
355
|
sys.exit(0)
|
|
356
356
|
|
|
357
357
|
# Read hook input for session metadata
|
|
@@ -361,20 +361,33 @@ def main():
|
|
|
361
361
|
event = {}
|
|
362
362
|
|
|
363
363
|
context = _build_context(vault_dir)
|
|
364
|
-
banner = _build_banner(vault_dir)
|
|
365
364
|
|
|
366
|
-
#
|
|
367
|
-
|
|
368
|
-
|
|
365
|
+
# Build a short summary for user-facing display
|
|
366
|
+
goal = _north_star_goal(vault_dir)
|
|
367
|
+
active_dir = os.path.join(vault_dir, "work", "active")
|
|
368
|
+
work_count = len([f for f in os.listdir(active_dir) if f.endswith(".md")]) if os.path.isdir(active_dir) else 0
|
|
369
|
+
changes = _git_status_short(vault_dir)
|
|
370
|
+
all_files = _find_md_files(vault_dir)
|
|
371
|
+
|
|
372
|
+
summary_parts = ["[Vault]"]
|
|
373
|
+
branch = _run_git(["rev-parse", "--abbrev-ref", "HEAD"], vault_dir)
|
|
374
|
+
if branch:
|
|
375
|
+
summary_parts.append(branch[0])
|
|
376
|
+
if goal:
|
|
377
|
+
summary_parts.append(f"goal: {goal}")
|
|
378
|
+
summary_parts.append(f"active:{work_count}")
|
|
379
|
+
summary_parts.append(f"changes:{len(changes)}")
|
|
380
|
+
summary_parts.append(f"{len(all_files)} notes")
|
|
381
|
+
summary = " | ".join(summary_parts)
|
|
369
382
|
|
|
370
383
|
output = {
|
|
371
384
|
"hookSpecificOutput": {
|
|
372
385
|
"hookEventName": "SessionStart",
|
|
373
386
|
"additionalContext": context
|
|
374
387
|
},
|
|
388
|
+
"systemMessage": summary
|
|
375
389
|
}
|
|
376
|
-
|
|
377
|
-
json.dump(output, sys.stdout)
|
|
390
|
+
sys.stdout.write(json.dumps(output) + "\n")
|
|
378
391
|
sys.stdout.flush()
|
|
379
392
|
sys.exit(0)
|
|
380
393
|
|
|
@@ -1,121 +1,113 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""PostToolUse hook for Codex CLI — Bash command validation.
|
|
3
3
|
|
|
4
|
-
Checks
|
|
5
|
-
|
|
6
|
-
|
|
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.
|
|
4
|
+
Checks Bash command output for hard failures (command not found,
|
|
5
|
+
permission denied, missing paths) and non-zero exit codes with
|
|
6
|
+
informative output. Modeled after oh-my-codex's native PostToolUse.
|
|
11
7
|
"""
|
|
12
8
|
import json
|
|
13
9
|
import re
|
|
14
10
|
import sys
|
|
15
|
-
import os
|
|
16
|
-
from pathlib import Path
|
|
17
11
|
|
|
18
12
|
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
13
|
+
HARD_FAILURE_PATTERNS = re.compile(
|
|
14
|
+
r"command not found|permission denied|no such file or directory",
|
|
15
|
+
re.IGNORECASE,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _safe_string(value):
|
|
20
|
+
return value if isinstance(value, str) else ""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _safe_int(value):
|
|
24
|
+
if isinstance(value, int):
|
|
25
|
+
return value
|
|
26
|
+
if isinstance(value, str) and value.strip().lstrip("-").isdigit():
|
|
27
|
+
return int(value.strip())
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _parse_tool_response(raw):
|
|
32
|
+
"""Try to parse tool_response as JSON dict."""
|
|
33
|
+
if isinstance(raw, dict):
|
|
34
|
+
return raw
|
|
35
|
+
if isinstance(raw, str):
|
|
36
|
+
try:
|
|
37
|
+
parsed = json.loads(raw)
|
|
38
|
+
if isinstance(parsed, dict):
|
|
39
|
+
return parsed
|
|
40
|
+
except (ValueError, TypeError):
|
|
41
|
+
pass
|
|
42
|
+
return None
|
|
31
43
|
|
|
32
44
|
|
|
33
45
|
def main():
|
|
34
46
|
try:
|
|
35
|
-
|
|
47
|
+
payload = json.load(sys.stdin)
|
|
36
48
|
except (ValueError, EOFError, OSError):
|
|
37
49
|
sys.exit(0)
|
|
38
50
|
|
|
39
|
-
|
|
40
|
-
if
|
|
51
|
+
tool_name = _safe_string(payload.get("tool_name", "")).strip()
|
|
52
|
+
if tool_name != "Bash":
|
|
41
53
|
sys.exit(0)
|
|
42
54
|
|
|
43
|
-
|
|
44
|
-
if
|
|
45
|
-
|
|
55
|
+
# Extract command and response
|
|
56
|
+
tool_input = payload.get("tool_input") if isinstance(payload.get("tool_input"), dict) else {}
|
|
57
|
+
command = _safe_string(tool_input.get("command", "")).strip()
|
|
46
58
|
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
raw_response = payload.get("tool_response")
|
|
60
|
+
parsed = _parse_tool_response(raw_response)
|
|
49
61
|
|
|
50
|
-
|
|
51
|
-
|
|
62
|
+
exit_code = None
|
|
63
|
+
stdout_text = ""
|
|
64
|
+
stderr_text = ""
|
|
52
65
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
if parsed:
|
|
67
|
+
exit_code = _safe_int(parsed.get("exit_code")) or _safe_int(parsed.get("exitCode"))
|
|
68
|
+
stdout_text = _safe_string(parsed.get("stdout", "")).strip()
|
|
69
|
+
stderr_text = _safe_string(parsed.get("stderr", "")).strip()
|
|
70
|
+
else:
|
|
71
|
+
stdout_text = _safe_string(raw_response).strip()
|
|
59
72
|
|
|
60
|
-
|
|
61
|
-
if
|
|
73
|
+
combined = f"{stderr_text}\n{stdout_text}".strip()
|
|
74
|
+
if not combined:
|
|
62
75
|
sys.exit(0)
|
|
63
76
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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:
|
|
77
|
+
# Check for hard failures
|
|
78
|
+
if HARD_FAILURE_PATTERNS.search(combined):
|
|
79
|
+
output = {
|
|
80
|
+
"decision": "block",
|
|
81
|
+
"reason": "Bash output indicates a command/setup failure that should be fixed before retrying.",
|
|
82
|
+
"hookSpecificOutput": {
|
|
83
|
+
"hookEventName": "PostToolUse",
|
|
84
|
+
"additionalContext": (
|
|
85
|
+
"Bash reported `command not found`, `permission denied`, or a missing file/path. "
|
|
86
|
+
"Verify the command, dependency installation, PATH, file permissions, "
|
|
87
|
+
"and referenced paths before retrying."
|
|
88
|
+
),
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
sys.stdout.write(json.dumps(output) + "\n")
|
|
92
|
+
sys.stdout.flush()
|
|
97
93
|
sys.exit(0)
|
|
98
94
|
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
95
|
+
# Check for non-zero exit code with informative output
|
|
96
|
+
if exit_code is not None and exit_code != 0 and len(combined) > 0:
|
|
111
97
|
output = {
|
|
98
|
+
"decision": "block",
|
|
99
|
+
"reason": "Bash command returned a non-zero exit code but produced useful output that should be reviewed before retrying.",
|
|
112
100
|
"hookSpecificOutput": {
|
|
113
101
|
"hookEventName": "PostToolUse",
|
|
114
|
-
"additionalContext":
|
|
102
|
+
"additionalContext": (
|
|
103
|
+
"The Bash output appears informative despite the non-zero exit code. "
|
|
104
|
+
"Review and report the output before retrying instead of assuming the command simply failed."
|
|
105
|
+
),
|
|
115
106
|
},
|
|
116
107
|
}
|
|
117
|
-
json.
|
|
108
|
+
sys.stdout.write(json.dumps(output) + "\n")
|
|
118
109
|
sys.stdout.flush()
|
|
110
|
+
sys.exit(0)
|
|
119
111
|
|
|
120
112
|
sys.exit(0)
|
|
121
113
|
|
|
@@ -283,16 +283,12 @@ def main():
|
|
|
283
283
|
icon = "\U0001f504" if mode == "auto" else "\U0001f4a1"
|
|
284
284
|
label = ", ".join(feedback_parts) if feedback_parts else "intent detected"
|
|
285
285
|
|
|
286
|
-
#
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
"additionalContext": context
|
|
293
|
-
},
|
|
294
|
-
}
|
|
295
|
-
json.dump(output, sys.stdout)
|
|
286
|
+
# stdout print → directly displayed to user
|
|
287
|
+
print(f" {icon} vault: {label}")
|
|
288
|
+
|
|
289
|
+
# systemMessage → injected into LLM context
|
|
290
|
+
result = {"systemMessage": context}
|
|
291
|
+
sys.stdout.write(json.dumps(result))
|
|
296
292
|
sys.stdout.flush()
|
|
297
293
|
|
|
298
294
|
sys.exit(0)
|
|
@@ -345,13 +345,9 @@ def _build_banner(vault_dir):
|
|
|
345
345
|
def main():
|
|
346
346
|
vault_dir = _find_vault_root()
|
|
347
347
|
if not vault_dir:
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
"additionalContext": "## Session Context\n\n(No vault found)"
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
json.dump(output, sys.stdout)
|
|
348
|
+
print("📚 Codex-Vault: No vault found")
|
|
349
|
+
result = {"systemMessage": "## Session Context\n\n(No vault found)"}
|
|
350
|
+
sys.stdout.write(json.dumps(result))
|
|
355
351
|
sys.exit(0)
|
|
356
352
|
|
|
357
353
|
# Read hook input for session metadata
|
|
@@ -361,20 +357,21 @@ def main():
|
|
|
361
357
|
event = {}
|
|
362
358
|
|
|
363
359
|
context = _build_context(vault_dir)
|
|
364
|
-
banner = _build_banner(vault_dir)
|
|
365
360
|
|
|
366
|
-
#
|
|
367
|
-
|
|
368
|
-
|
|
361
|
+
# stdout print → directly displayed to user
|
|
362
|
+
goal = _north_star_goal(vault_dir)
|
|
363
|
+
active_dir = os.path.join(vault_dir, "work", "active")
|
|
364
|
+
work_count = len([f for f in os.listdir(active_dir) if f.endswith(".md")]) if os.path.isdir(active_dir) else 0
|
|
365
|
+
changes = _git_status_short(vault_dir)
|
|
369
366
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
},
|
|
375
|
-
}
|
|
367
|
+
print("📚 Codex-Vault loaded!")
|
|
368
|
+
if goal:
|
|
369
|
+
print(f"🎯 {goal}")
|
|
370
|
+
print(f"📝 Active: {work_count} | Changes: {len(changes)}")
|
|
376
371
|
|
|
377
|
-
|
|
372
|
+
# systemMessage → injected into LLM context
|
|
373
|
+
result = {"systemMessage": context}
|
|
374
|
+
sys.stdout.write(json.dumps(result))
|
|
378
375
|
sys.stdout.flush()
|
|
379
376
|
sys.exit(0)
|
|
380
377
|
|
package/vault/AGENTS.md
CHANGED
|
@@ -1,118 +1 @@
|
|
|
1
|
-
|
|
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
|
|
1
|
+
@CLAUDE.md
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
set -eo pipefail
|
|
3
|
-
|
|
4
|
-
# Codex-Vault session-start hook
|
|
5
|
-
# Injects vault context into the agent's prompt at session start.
|
|
6
|
-
# Works with any agent that supports SessionStart hooks (Claude Code, Codex CLI).
|
|
7
|
-
#
|
|
8
|
-
# Output contract (matches claude-mem pattern):
|
|
9
|
-
# stdout → JSON with hookSpecificOutput.additionalContext (agent context)
|
|
10
|
-
# stderr → visible banner (user terminal)
|
|
11
|
-
#
|
|
12
|
-
# Dynamic context: adapts git log window, reads full North Star,
|
|
13
|
-
# shows all active work, and includes uncommitted changes.
|
|
14
|
-
|
|
15
|
-
VAULT_DIR="${CLAUDE_PROJECT_DIR:-${CODEX_PROJECT_DIR:-$(pwd)}}"
|
|
16
|
-
cd "$VAULT_DIR"
|
|
17
|
-
|
|
18
|
-
# Find vault root (look for Home.md or brain/ directory)
|
|
19
|
-
if [ ! -f "Home.md" ] && [ ! -d "brain/" ]; then
|
|
20
|
-
# Try vault/ subdirectory (integrated layout)
|
|
21
|
-
if [ -d "vault/" ]; then
|
|
22
|
-
VAULT_DIR="$VAULT_DIR/vault"
|
|
23
|
-
cd "$VAULT_DIR"
|
|
24
|
-
fi
|
|
25
|
-
fi
|
|
26
|
-
|
|
27
|
-
# --- Visible banner (stderr → user terminal) ---
|
|
28
|
-
{
|
|
29
|
-
echo ""
|
|
30
|
-
echo " 📚 Codex-Vault · Session Loaded"
|
|
31
|
-
|
|
32
|
-
# North Star preview
|
|
33
|
-
if [ -f "brain/North Star.md" ]; then
|
|
34
|
-
GOAL=$(sed -n '/^## Current Focus/,/^## /{/^- ./{s/^- //;p;q;};}' "brain/North Star.md" | cut -c1-40)
|
|
35
|
-
[ -n "$GOAL" ] && echo " 🎯 $GOAL" || echo " 🎯 (set goals in North Star.md)"
|
|
36
|
-
else
|
|
37
|
-
echo " 🎯 (create brain/North Star.md)"
|
|
38
|
-
fi
|
|
39
|
-
|
|
40
|
-
# Active work count
|
|
41
|
-
WORK_COUNT=$(find work/active -name "*.md" 2>/dev/null | wc -l | tr -d ' ')
|
|
42
|
-
echo " 📋 $WORK_COUNT active project(s)"
|
|
43
|
-
|
|
44
|
-
# Uncommitted changes count
|
|
45
|
-
CHANGE_COUNT=$(git status --short -- . 2>/dev/null | wc -l | tr -d ' ')
|
|
46
|
-
if [ "$CHANGE_COUNT" -gt 0 ]; then
|
|
47
|
-
echo " ✏️ $CHANGE_COUNT uncommitted change(s)"
|
|
48
|
-
else
|
|
49
|
-
echo " ✅ working tree clean"
|
|
50
|
-
fi
|
|
51
|
-
echo ""
|
|
52
|
-
} >&2
|
|
53
|
-
|
|
54
|
-
# --- Collect context into variable ---
|
|
55
|
-
CONTEXT=$(
|
|
56
|
-
cat <<'CONTEXT_HEADER'
|
|
57
|
-
## Session Context
|
|
58
|
-
|
|
59
|
-
CONTEXT_HEADER
|
|
60
|
-
|
|
61
|
-
echo "### Date"
|
|
62
|
-
echo "$(date +%Y-%m-%d) ($(date +%A))"
|
|
63
|
-
echo ""
|
|
64
|
-
|
|
65
|
-
# North Star — full file (should be concise by design)
|
|
66
|
-
echo "### North Star"
|
|
67
|
-
if [ -f "brain/North Star.md" ]; then
|
|
68
|
-
cat "brain/North Star.md"
|
|
69
|
-
else
|
|
70
|
-
echo "(No North Star found — create brain/North Star.md to set goals)"
|
|
71
|
-
fi
|
|
72
|
-
echo ""
|
|
73
|
-
|
|
74
|
-
# Recent changes — adaptive window
|
|
75
|
-
echo "### Recent Changes"
|
|
76
|
-
COMMITS_48H=$(git log --oneline --since="48 hours ago" --no-merges 2>/dev/null | wc -l | tr -d ' ')
|
|
77
|
-
if [ "$COMMITS_48H" -gt 0 ]; then
|
|
78
|
-
echo "(last 48 hours)"
|
|
79
|
-
git log --oneline --since="48 hours ago" --no-merges 2>/dev/null | head -15 || true
|
|
80
|
-
else
|
|
81
|
-
COMMITS_7D=$(git log --oneline --since="7 days ago" --no-merges 2>/dev/null | wc -l | tr -d ' ')
|
|
82
|
-
if [ "$COMMITS_7D" -gt 0 ]; then
|
|
83
|
-
echo "(nothing in 48h — showing last 7 days)"
|
|
84
|
-
git log --oneline --since="7 days ago" --no-merges 2>/dev/null | head -15 || true
|
|
85
|
-
else
|
|
86
|
-
echo "(nothing recent — showing last 5 commits)"
|
|
87
|
-
git log --oneline -5 --no-merges 2>/dev/null || echo "(no git history)"
|
|
88
|
-
fi
|
|
89
|
-
fi
|
|
90
|
-
echo ""
|
|
91
|
-
|
|
92
|
-
# Recent operations from log — adaptive
|
|
93
|
-
echo "### Recent Operations"
|
|
94
|
-
if [ -f "log.md" ]; then
|
|
95
|
-
ENTRY_COUNT=$(grep -c "^## \[" "log.md" 2>/dev/null || echo "0")
|
|
96
|
-
if [ "$ENTRY_COUNT" -gt 0 ]; then
|
|
97
|
-
# Show last 5 entries with full header line (includes date + type)
|
|
98
|
-
grep "^## \[" "log.md" | tail -5
|
|
99
|
-
else
|
|
100
|
-
echo "(no entries in log.md)"
|
|
101
|
-
fi
|
|
102
|
-
else
|
|
103
|
-
echo "(no log.md)"
|
|
104
|
-
fi
|
|
105
|
-
echo ""
|
|
106
|
-
|
|
107
|
-
# Active work — show all (this is the current focus, no truncation)
|
|
108
|
-
echo "### Active Work"
|
|
109
|
-
if [ -d "work/active" ]; then
|
|
110
|
-
WORK_FILES=$(ls work/active/*.md 2>/dev/null || true)
|
|
111
|
-
if [ -n "$WORK_FILES" ]; then
|
|
112
|
-
echo "$WORK_FILES" | sed 's|work/active/||;s|\.md$||'
|
|
113
|
-
else
|
|
114
|
-
echo "(none)"
|
|
115
|
-
fi
|
|
116
|
-
else
|
|
117
|
-
echo "(no work/active/ directory)"
|
|
118
|
-
fi
|
|
119
|
-
echo ""
|
|
120
|
-
|
|
121
|
-
# Uncommitted changes — shows agent what's in-flight
|
|
122
|
-
echo "### Uncommitted Changes"
|
|
123
|
-
CHANGES=$(git status --short -- . 2>/dev/null | head -20 || true)
|
|
124
|
-
if [ -n "$CHANGES" ]; then
|
|
125
|
-
echo "$CHANGES"
|
|
126
|
-
else
|
|
127
|
-
echo "(working tree clean)"
|
|
128
|
-
fi
|
|
129
|
-
echo ""
|
|
130
|
-
|
|
131
|
-
# Recently modified brain files — highlights memory that may need review
|
|
132
|
-
echo "### Recently Modified Brain Files"
|
|
133
|
-
if [ -d "brain" ]; then
|
|
134
|
-
GIT_DIR=$(git rev-parse --git-dir 2>/dev/null || echo "")
|
|
135
|
-
if [ -n "$GIT_DIR" ] && [ -f "$GIT_DIR/index" ]; then
|
|
136
|
-
RECENT_BRAIN=$(find brain/ -name "*.md" -newer "$GIT_DIR/index" 2>/dev/null || true)
|
|
137
|
-
else
|
|
138
|
-
RECENT_BRAIN=""
|
|
139
|
-
fi
|
|
140
|
-
if [ -n "$RECENT_BRAIN" ]; then
|
|
141
|
-
echo "$RECENT_BRAIN" | sed 's|brain/||;s|\.md$||'
|
|
142
|
-
else
|
|
143
|
-
# Fallback: show brain files modified in last 7 days
|
|
144
|
-
RECENT_BRAIN=$(find brain/ -name "*.md" -mtime -7 2>/dev/null || true)
|
|
145
|
-
if [ -n "$RECENT_BRAIN" ]; then
|
|
146
|
-
echo "(modified in last 7 days)"
|
|
147
|
-
echo "$RECENT_BRAIN" | sed 's|brain/||;s|\.md$||'
|
|
148
|
-
else
|
|
149
|
-
echo "(no recent changes)"
|
|
150
|
-
fi
|
|
151
|
-
fi
|
|
152
|
-
fi
|
|
153
|
-
echo ""
|
|
154
|
-
|
|
155
|
-
# Vault file listing — tiered to avoid flooding context in large vaults
|
|
156
|
-
echo "### Vault Files"
|
|
157
|
-
ALL_FILES=$(find . -name "*.md" -not -path "./.git/*" -not -path "./.obsidian/*" -not -path "./thinking/*" -not -path "./.claude/*" -not -path "./.codex/*" -not -path "./.codex-vault/*" -not -path "./node_modules/*" 2>/dev/null | sort)
|
|
158
|
-
FILE_COUNT=$(echo "$ALL_FILES" | grep -c . 2>/dev/null || echo "0")
|
|
159
|
-
|
|
160
|
-
_folder_summary() {
|
|
161
|
-
echo "$ALL_FILES" | sed 's|^\./||' | cut -d/ -f1 | sort | uniq -c | sort -rn | while read count dir; do
|
|
162
|
-
echo " $dir/ ($count files)"
|
|
163
|
-
done
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
_key_files() {
|
|
167
|
-
echo "$ALL_FILES" | grep -E "(Home|Index|North Star|Memories|Key Decisions|Patterns|log)\\.md$" || true
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if [ "$FILE_COUNT" -le 20 ]; then
|
|
171
|
-
echo "$ALL_FILES"
|
|
172
|
-
|
|
173
|
-
elif [ "$FILE_COUNT" -le 50 ]; then
|
|
174
|
-
HOT_FILES=$(echo "$ALL_FILES" | grep -v -E "^\./sources/|^\./work/archive/" || true)
|
|
175
|
-
COLD_COUNT=$(echo "$ALL_FILES" | grep -E "^\./sources/|^\./work/archive/" | grep -c . 2>/dev/null || echo "0")
|
|
176
|
-
|
|
177
|
-
if [ -n "$HOT_FILES" ]; then
|
|
178
|
-
echo "$HOT_FILES"
|
|
179
|
-
fi
|
|
180
|
-
if [ "$COLD_COUNT" -gt 0 ]; then
|
|
181
|
-
echo ""
|
|
182
|
-
echo "(+ $COLD_COUNT files in sources/ and work/archive/ — use /recall to search)"
|
|
183
|
-
fi
|
|
184
|
-
|
|
185
|
-
elif [ "$FILE_COUNT" -le 150 ]; then
|
|
186
|
-
echo "($FILE_COUNT files — showing summary)"
|
|
187
|
-
echo ""
|
|
188
|
-
_folder_summary
|
|
189
|
-
echo ""
|
|
190
|
-
echo "Recently modified (7 days):"
|
|
191
|
-
find . -name "*.md" -mtime -7 -not -path "./.git/*" -not -path "./.obsidian/*" -not -path "./thinking/*" -not -path "./.claude/*" -not -path "./.codex/*" -not -path "./.codex-vault/*" -not -path "./node_modules/*" 2>/dev/null | sort || echo " (none)"
|
|
192
|
-
echo ""
|
|
193
|
-
echo "Key files:"
|
|
194
|
-
_key_files
|
|
195
|
-
|
|
196
|
-
else
|
|
197
|
-
echo "($FILE_COUNT files — showing summary)"
|
|
198
|
-
echo ""
|
|
199
|
-
_folder_summary
|
|
200
|
-
echo ""
|
|
201
|
-
echo "Recently modified (3 days):"
|
|
202
|
-
find . -name "*.md" -mtime -3 -not -path "./.git/*" -not -path "./.obsidian/*" -not -path "./thinking/*" -not -path "./.claude/*" -not -path "./.codex/*" -not -path "./.codex-vault/*" -not -path "./node_modules/*" 2>/dev/null | sort || echo " (none)"
|
|
203
|
-
echo ""
|
|
204
|
-
echo "Key files:"
|
|
205
|
-
_key_files
|
|
206
|
-
echo ""
|
|
207
|
-
echo "Use /recall <topic> to search the vault."
|
|
208
|
-
fi
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
# --- Output structured JSON (stdout → agent via hookSpecificOutput) ---
|
|
212
|
-
python3 -c "
|
|
213
|
-
import json, sys
|
|
214
|
-
context = sys.stdin.read()
|
|
215
|
-
json.dump({
|
|
216
|
-
'hookSpecificOutput': {
|
|
217
|
-
'hookEventName': 'SessionStart',
|
|
218
|
-
'additionalContext': context
|
|
219
|
-
}
|
|
220
|
-
}, sys.stdout)
|
|
221
|
-
" <<< "$CONTEXT"
|