@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.
@@ -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