@neikyun/ciel 6.8.0 → 6.9.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/assets/.claude/hooks/memory-bootstrap.sh +287 -0
- package/assets/.claude/hooks/memory-engine.py +718 -0
- package/assets/.claude/hooks/pre-agent-gate.sh +55 -0
- package/assets/.claude/hooks/pre-tool-write.sh +83 -0
- package/assets/.claude/hooks/session-start.sh +132 -0
- package/assets/.claude/hooks/session-version-check.sh +14 -0
- package/assets/.claude/hooks/user-prompt-submit.sh +112 -0
- package/assets/.claude/settings.json +4 -4
- package/assets/commands/ciel-audit.md +78 -17
- package/assets/commands/ciel-memory-bootstrap.md +160 -0
- package/assets/commands/ciel-status.md +1 -1
- package/assets/platforms/opencode/.opencode/agents/ciel-explorer.md +1 -1
- package/assets/platforms/opencode/.opencode/agents/ciel-improver.md +2 -2
- package/assets/platforms/opencode/.opencode/agents/ciel-researcher.md +1 -1
- package/assets/platforms/opencode/.opencode/commands/ciel-audit.md +40 -22
- package/dist/cli/claude.d.ts.map +1 -1
- package/dist/cli/claude.js +28 -13
- package/dist/cli/claude.js.map +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.cjs +11 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Ciel — Memory Bootstrap (cued-recall)
|
|
3
|
+
# Trigger: invoked manually by /ciel-memory-bootstrap or programmatically
|
|
4
|
+
# Purpose: scan project for ingestable tribal docs and report findings
|
|
5
|
+
# Usage:
|
|
6
|
+
# memory-bootstrap.sh scan → report what would be ingested (dry-run, default)
|
|
7
|
+
# memory-bootstrap.sh ingest → actually create memories from sources
|
|
8
|
+
# memory-bootstrap.sh status → report current memory corpus stats
|
|
9
|
+
# memory-bootstrap.sh github-scan → scan GitHub issues/PRs for tribal knowledge
|
|
10
|
+
#
|
|
11
|
+
# Never blocks (exit 0 on success, exit 1 only on hard errors). Stdout is the
|
|
12
|
+
# user-facing report. See ADR-0001 and skill `memoire`.
|
|
13
|
+
|
|
14
|
+
set -e
|
|
15
|
+
|
|
16
|
+
CMD="${1:-scan}"
|
|
17
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-${PWD}}"
|
|
18
|
+
MEMORY_DIR="$PROJECT_DIR/.ciel/memory"
|
|
19
|
+
INDEX_FILE="$MEMORY_DIR/index.json"
|
|
20
|
+
|
|
21
|
+
# ─── Source candidates (priority order) ──────────────────────────────────────
|
|
22
|
+
declare -a SOURCES=(
|
|
23
|
+
"$PROJECT_DIR/.ciel/learnings.md"
|
|
24
|
+
"$PROJECT_DIR/.claude/learnings.md"
|
|
25
|
+
"$PROJECT_DIR/.claude/lessons.md"
|
|
26
|
+
"$PROJECT_DIR/lessons.md"
|
|
27
|
+
"$PROJECT_DIR/LESSONS.md"
|
|
28
|
+
"$PROJECT_DIR/ciel-overlay.md"
|
|
29
|
+
"$PROJECT_DIR/AGENTS.md"
|
|
30
|
+
"$PROJECT_DIR/CLAUDE.md"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# .claude/rules/*.md detected separately (variable count)
|
|
34
|
+
RULES_DIR="$PROJECT_DIR/.claude/rules"
|
|
35
|
+
|
|
36
|
+
# ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
scan_sources() {
|
|
39
|
+
local found=0
|
|
40
|
+
echo "Scanning $PROJECT_DIR for ingestable tribal docs..."
|
|
41
|
+
echo ""
|
|
42
|
+
for src in "${SOURCES[@]}"; do
|
|
43
|
+
if [[ -f "$src" ]]; then
|
|
44
|
+
local size lines
|
|
45
|
+
size=$(wc -c < "$src" | tr -d ' ')
|
|
46
|
+
lines=$(wc -l < "$src" | tr -d ' ')
|
|
47
|
+
echo " ✓ $src ($lines lines, $size bytes)"
|
|
48
|
+
found=$((found + 1))
|
|
49
|
+
fi
|
|
50
|
+
done
|
|
51
|
+
|
|
52
|
+
if [[ -d "$RULES_DIR" ]]; then
|
|
53
|
+
local rules_count
|
|
54
|
+
rules_count=$(find "$RULES_DIR" -maxdepth 2 -name "*.md" -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
55
|
+
if [[ "$rules_count" -gt 0 ]]; then
|
|
56
|
+
echo " ✓ $RULES_DIR/ ($rules_count rule files)"
|
|
57
|
+
found=$((found + rules_count))
|
|
58
|
+
fi
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
echo ""
|
|
62
|
+
if [[ "$found" -eq 0 ]]; then
|
|
63
|
+
echo "No ingestable sources found. The cued-recall memory will populate"
|
|
64
|
+
echo "organically as you intervene with the model. No action needed."
|
|
65
|
+
return 0
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
echo "Found $found source(s). Run with 'ingest' to convert to cued-recall memories:"
|
|
69
|
+
echo " bash hooks/memory-bootstrap.sh ingest"
|
|
70
|
+
return 0
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
ingest_sources() {
|
|
74
|
+
mkdir -p "$MEMORY_DIR/episodes" "$MEMORY_DIR/concepts" "$MEMORY_DIR/guards"
|
|
75
|
+
|
|
76
|
+
# Initialize index if absent
|
|
77
|
+
if [[ ! -f "$INDEX_FILE" ]]; then
|
|
78
|
+
cat > "$INDEX_FILE" <<'EOF'
|
|
79
|
+
{
|
|
80
|
+
"version": 2,
|
|
81
|
+
"memories": {},
|
|
82
|
+
"by_path": {},
|
|
83
|
+
"by_symbol": {},
|
|
84
|
+
"by_intent": {},
|
|
85
|
+
"by_language": {}
|
|
86
|
+
}
|
|
87
|
+
EOF
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
echo "Bootstrap ingestion is intentionally deferred to the model."
|
|
91
|
+
echo ""
|
|
92
|
+
echo "The cued-recall design (ADR-0001) requires that captures be validated"
|
|
93
|
+
echo "by the user. Auto-ingestion would defeat that filter and create"
|
|
94
|
+
echo "cargo-cult memories from docs that may already be stale."
|
|
95
|
+
echo ""
|
|
96
|
+
echo "Workflow:"
|
|
97
|
+
echo " 1. The model reads the sources listed by 'scan' above."
|
|
98
|
+
echo " 2. For each candidate entry (e.g. each [date] line in lessons.md,"
|
|
99
|
+
echo " each rule in .claude/rules/), the model proposes a memory:"
|
|
100
|
+
echo " - title (1 line)"
|
|
101
|
+
echo " - tags: paths, symbols, intents, language"
|
|
102
|
+
echo " - content (the lesson itself)"
|
|
103
|
+
echo " 3. The user validates or skips per entry."
|
|
104
|
+
echo " 4. Validated entries are written to .ciel/memory/episodes/"
|
|
105
|
+
echo " with frontmatter, and index.json is updated."
|
|
106
|
+
echo ""
|
|
107
|
+
echo "The model uses the skill 'memoire' to perform this. Initialized"
|
|
108
|
+
echo "structure at $MEMORY_DIR/."
|
|
109
|
+
return 0
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
github_scan() {
|
|
113
|
+
local repo
|
|
114
|
+
repo=$(gh repo view --json name,owner --jq '"\(.owner.login)/\(.name)"' 2>/dev/null) || {
|
|
115
|
+
echo "GitHub CLI (gh) not authenticated or no remote found."
|
|
116
|
+
echo "Run 'gh auth login' first or configure a remote."
|
|
117
|
+
return 0
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
echo "Scanning GitHub repository $repo for tribal knowledge..."
|
|
121
|
+
echo ""
|
|
122
|
+
|
|
123
|
+
# ─── Issues ────────────────────────────────────────────────────────────────
|
|
124
|
+
echo "=== Issues ==="
|
|
125
|
+
echo ""
|
|
126
|
+
|
|
127
|
+
# Fetch last 50 closed+open issues with comments
|
|
128
|
+
gh issue list --repo "$repo" --limit 50 --state all --json number,title,body,labels,state,comments,url 2>/dev/null | \
|
|
129
|
+
python3 -c "
|
|
130
|
+
import json, sys
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
issues = json.load(sys.stdin)
|
|
134
|
+
except Exception as e:
|
|
135
|
+
print(f' Error parsing issues: {e}')
|
|
136
|
+
sys.exit(0)
|
|
137
|
+
|
|
138
|
+
found = 0
|
|
139
|
+
for issue in issues:
|
|
140
|
+
body = issue.get('body') or ''
|
|
141
|
+
title = issue.get('title', '')
|
|
142
|
+
url = issue.get('url', '')
|
|
143
|
+
labels = ', '.join(l.get('name','') for l in issue.get('labels',[]))
|
|
144
|
+
comments = issue.get('comments') or []
|
|
145
|
+
state = issue.get('state', '')
|
|
146
|
+
all_text = body + ' ' + ' '.join(c.get('body','') for c in comments)
|
|
147
|
+
|
|
148
|
+
signals = []
|
|
149
|
+
for keyword in ['MISTAKE', 'RULE:', 'lesson learned', 'never do', 'we decided', 'chose', 'opted for', 'considered.*but', 'TODO', 'REASON:']:
|
|
150
|
+
if keyword.lower() in all_text.lower():
|
|
151
|
+
signals.append(keyword)
|
|
152
|
+
|
|
153
|
+
if signals or len(all_text) > 200:
|
|
154
|
+
found += 1
|
|
155
|
+
print(f' [{state}] #{issue.get(\"number\")} - {title}')
|
|
156
|
+
print(f' URL: {url}')
|
|
157
|
+
if labels:
|
|
158
|
+
print(f' Labels: {labels}')
|
|
159
|
+
if signals:
|
|
160
|
+
print(f' Signals: {', '.join(set(signals))}')
|
|
161
|
+
body_preview = body[:300].replace(chr(10), ' ') if body else '(empty)'
|
|
162
|
+
print(f' Body: {body_preview}...' if len(body) > 300 else f' Body: {body_preview}')
|
|
163
|
+
if comments:
|
|
164
|
+
for i, c in enumerate(comments[:3]):
|
|
165
|
+
cbody = (c.get('body') or '')[:200].replace(chr(10), ' ')
|
|
166
|
+
print(f' Comment {i+1}: {cbody}...' if len((c.get('body') or '')) > 200 else f' Comment {i+1}: {cbody}')
|
|
167
|
+
if len(comments) > 3:
|
|
168
|
+
print(f' ... +{len(comments)-3} more comments')
|
|
169
|
+
print()
|
|
170
|
+
|
|
171
|
+
if found == 0:
|
|
172
|
+
print(' No issues with detectable tribal knowledge found.')
|
|
173
|
+
else:
|
|
174
|
+
print(f' Found {found} issue(s) with potential tribal knowledge.')
|
|
175
|
+
" 2>/dev/null || echo " Error fetching issues. Is 'gh' installed?"
|
|
176
|
+
|
|
177
|
+
echo ""
|
|
178
|
+
echo "=== Pull Requests ==="
|
|
179
|
+
echo ""
|
|
180
|
+
|
|
181
|
+
# Fetch last 50 merged+open PRs
|
|
182
|
+
gh pr list --repo "$repo" --limit 50 --state all --json number,title,body,state,mergedAt,comments,url,additions,deletions 2>/dev/null | \
|
|
183
|
+
python3 -c "
|
|
184
|
+
import json, sys
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
prs = json.load(sys.stdin)
|
|
188
|
+
except Exception as e:
|
|
189
|
+
print(f' Error parsing PRs: {e}')
|
|
190
|
+
sys.exit(0)
|
|
191
|
+
|
|
192
|
+
found = 0
|
|
193
|
+
for pr in prs:
|
|
194
|
+
body = pr.get('body') or ''
|
|
195
|
+
title = pr.get('title', '')
|
|
196
|
+
url = pr.get('url', '')
|
|
197
|
+
comments = pr.get('comments') or []
|
|
198
|
+
state = 'MERGED' if pr.get('mergedAt') else pr.get('state', '').upper()
|
|
199
|
+
all_text = body + ' ' + ' '.join(c.get('body','') for c in comments)
|
|
200
|
+
|
|
201
|
+
signals = []
|
|
202
|
+
for keyword in ['MISTAKE', 'RULE:', 'lesson learned', 'never do', 'we decided', 'chose', 'opted for', 'considered.*but', 'TODO', 'REASON:', 'trade-off', 'alternative']:
|
|
203
|
+
if keyword.lower() in all_text.lower():
|
|
204
|
+
signals.append(keyword)
|
|
205
|
+
|
|
206
|
+
if signals or len(all_text) > 200:
|
|
207
|
+
found += 1
|
|
208
|
+
print(f' [{state}] #{pr.get(\"number\")} - {title}')
|
|
209
|
+
print(f' URL: {url}')
|
|
210
|
+
size = pr.get('additions',0)+pr.get('deletions',0)
|
|
211
|
+
print(f' Size: +{pr.get(\"additions\",0)}/-{pr.get(\"deletions\",0)} ({size} lines)')
|
|
212
|
+
if signals:
|
|
213
|
+
print(f' Signals: {', '.join(set(signals))}')
|
|
214
|
+
body_preview = body[:300].replace(chr(10), ' ') if body else '(empty)'
|
|
215
|
+
print(f' Description: {body_preview}...' if len(body) > 300 else f' Description: {body_preview}')
|
|
216
|
+
if comments:
|
|
217
|
+
for i, c in enumerate(comments[:3]):
|
|
218
|
+
cbody = (c.get('body') or '')[:200].replace(chr(10), ' ')
|
|
219
|
+
print(f' Comment {i+1}: {cbody}...' if len((c.get('body') or '')) > 200 else f' Comment {i+1}: {cbody}')
|
|
220
|
+
if len(comments) > 3:
|
|
221
|
+
print(f' ... +{len(comments)-3} more comments')
|
|
222
|
+
print()
|
|
223
|
+
|
|
224
|
+
if found == 0:
|
|
225
|
+
print(' No PRs with detectable tribal knowledge found.')
|
|
226
|
+
else:
|
|
227
|
+
print(f' Found {found} PR(s) with potential tribal knowledge.')
|
|
228
|
+
" 2>/dev/null || echo " Error fetching PRs."
|
|
229
|
+
|
|
230
|
+
echo "---"
|
|
231
|
+
echo "GitHub scan complete. To ingest these into cued-recall memory,"
|
|
232
|
+
echo "the model will read each candidate and propose entries for validation."
|
|
233
|
+
return 0
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
status_corpus() {
|
|
237
|
+
if [[ ! -f "$INDEX_FILE" ]]; then
|
|
238
|
+
echo "No memory corpus yet at $MEMORY_DIR/."
|
|
239
|
+
echo "Run: bash hooks/memory-bootstrap.sh scan"
|
|
240
|
+
return 0
|
|
241
|
+
fi
|
|
242
|
+
|
|
243
|
+
INDEX_FILE="$INDEX_FILE" python3 - <<'PYEOF'
|
|
244
|
+
import json, os
|
|
245
|
+
from datetime import datetime, timezone
|
|
246
|
+
try:
|
|
247
|
+
with open(os.environ['INDEX_FILE']) as f:
|
|
248
|
+
idx = json.load(f)
|
|
249
|
+
mems = idx.get('memories', {})
|
|
250
|
+
total = len(mems)
|
|
251
|
+
stale = sum(1 for m in mems.values() if m.get('stale'))
|
|
252
|
+
active = total - stale
|
|
253
|
+
triggered = sum(m.get('trigger_count', 0) for m in mems.values())
|
|
254
|
+
by_lang = {}
|
|
255
|
+
for m in mems.values():
|
|
256
|
+
for lang in m.get('languages', []) or ['unknown']:
|
|
257
|
+
by_lang[lang] = by_lang.get(lang, 0) + 1
|
|
258
|
+
print(f"Cued-recall memory corpus")
|
|
259
|
+
print(f" Total memories : {total}")
|
|
260
|
+
print(f" Active : {active}")
|
|
261
|
+
print(f" Stale : {stale}")
|
|
262
|
+
print(f" Total triggers : {triggered}")
|
|
263
|
+
if by_lang:
|
|
264
|
+
print(f" By language : " + ", ".join(f"{k}={v}" for k, v in sorted(by_lang.items())))
|
|
265
|
+
print(f" Index version : {idx.get('version', '?')}")
|
|
266
|
+
except Exception as e:
|
|
267
|
+
print(f"Could not read index: {e}")
|
|
268
|
+
PYEOF
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
case "$CMD" in
|
|
272
|
+
scan) scan_sources ;;
|
|
273
|
+
ingest) ingest_sources ;;
|
|
274
|
+
status) status_corpus ;;
|
|
275
|
+
github-scan|github) github_scan ;;
|
|
276
|
+
*)
|
|
277
|
+
echo "Usage: $0 {scan|ingest|status|github-scan}"
|
|
278
|
+
echo ""
|
|
279
|
+
echo " scan — Report what would be ingested (default, dry-run)"
|
|
280
|
+
echo " ingest — Initialize .ciel/memory/ structure and instruct model to ingest"
|
|
281
|
+
echo " status — Report current corpus stats"
|
|
282
|
+
echo " github-scan — Scan GitHub issues/PRs for tribal knowledge"
|
|
283
|
+
exit 1
|
|
284
|
+
;;
|
|
285
|
+
esac
|
|
286
|
+
|
|
287
|
+
exit 0
|