skillwiki 0.9.63 → 0.10.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.
Files changed (36) hide show
  1. package/dist/chunk-5TBIMLTZ.js +343 -0
  2. package/dist/chunk-C5OLZRRM.js +357 -0
  3. package/dist/{chunk-TUFQZ5K4.js → chunk-DR7KFHNH.js} +792 -1983
  4. package/dist/chunk-IZABIE44.js +647 -0
  5. package/dist/chunk-S5ABQCXQ.js +580 -0
  6. package/dist/cli.js +1540 -550
  7. package/dist/index-projection-ERFX76U5.js +10 -0
  8. package/dist/managed-write-preflight-SILEUQEV.js +11 -0
  9. package/dist/skillwiki-mcp.js +4 -1
  10. package/dist/vault-sync/scripts/lib/conflict-markers.sh +69 -0
  11. package/dist/vault-sync/scripts/lib/delete-intent.sh +74 -0
  12. package/dist/vault-sync/scripts/lib/fleet.sh +103 -0
  13. package/dist/vault-sync/scripts/lib/git-case.sh +71 -0
  14. package/dist/vault-sync/scripts/lib/git-materialization.sh +264 -0
  15. package/dist/vault-sync/scripts/lib/git-operation-journal.sh +469 -0
  16. package/dist/vault-sync/scripts/lib/git-rebase-state.sh +180 -0
  17. package/dist/vault-sync/scripts/lib/lockfile.sh +70 -0
  18. package/dist/vault-sync/scripts/lib/managed-write-lock.sh +80 -0
  19. package/dist/vault-sync/scripts/lib/platform.sh +184 -0
  20. package/dist/vault-sync/scripts/lib/runtime-manifest.sh +223 -0
  21. package/dist/vault-sync/scripts/wiki-fetch-notify.sh +207 -0
  22. package/dist/vault-sync/scripts/wiki-fuse-refresh.sh +405 -0
  23. package/dist/vault-sync/scripts/wiki-pull-with-auto-resolve.sh +631 -0
  24. package/dist/vault-sync/scripts/wiki-push.sh +364 -0
  25. package/dist/vault-sync/scripts/wiki-snapshot.sh +587 -0
  26. package/package.json +2 -2
  27. package/skills/.claude-plugin/plugin.json +1 -1
  28. package/skills/.codex-plugin/plugin.json +1 -1
  29. package/skills/README.md +13 -0
  30. package/skills/package.json +1 -1
  31. package/skills/proj-work/SKILL.md +3 -0
  32. package/skills/skills/proj-work/SKILL.md +3 -0
  33. package/skills/skills/using-skillwiki/SKILL.md +13 -0
  34. package/skills/skills/wiki-crystallize/SKILL.md +3 -0
  35. package/skills/using-skillwiki/SKILL.md +13 -0
  36. package/skills/wiki-crystallize/SKILL.md +3 -0
@@ -0,0 +1,587 @@
1
+ #!/bin/bash
2
+ # wiki-snapshot.sh — Linux-only S3 → git snapshot/promotion job.
3
+ #
4
+ # Source-of-truth: this is the canonical vault-sync copy. The deployed copy on
5
+ # sg01 lives under /root/.local/share/vault-sync/bin; Hermes is not part of the
6
+ # production snapshot path. See work item:
7
+ # projects/llm-wiki/work/2026-05-25-vault-sync-plugin-scaffold/
8
+ #
9
+ # Origin: migrated 2026-05-25 from sg01 wiki-snapshot-v3.sh (sha256:
10
+ # 037b05ddb6b47e377c3ef493e69730bc3ff3e3ccaa51d56edb7027091582f383).
11
+ #
12
+ # Hard Rule (NON-NEGOTIABLE):
13
+ # The --max-delete 10 guard in RCLONE_OPTS MUST be preserved.
14
+ # Without it, momentary S3 inconsistency mass-deletes files from GitHub.
15
+ # Reference: raw/transcripts/2026-05-23-bug-sg01-snapshot-destructive-rclone-sync.md
16
+ #
17
+ # Single-writer-git invariant: only ONE host per vault remote may run this.
18
+ # sg01 is the current snapshotter (role: snapshotter in fleet.yaml).
19
+ # Running on a second host violates the authority model.
20
+
21
+ set -o pipefail
22
+
23
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
24
+
25
+ if [ -n "${GIT_DIR:-}" ]; then
26
+ echo "[wiki-snapshot] WARNING: ignoring exported GIT_DIR; use WIKI_GIT_WORKTREE for the snapshot worktree" >&2
27
+ unset GIT_DIR
28
+ fi
29
+
30
+ # Source platform.sh — handles both dev (scripts/lib/) and deployed (lib/) layouts
31
+ if [ -f "$SCRIPT_DIR/lib/platform.sh" ]; then
32
+ source "$SCRIPT_DIR/lib/platform.sh"
33
+ elif [ -f "$SCRIPT_DIR/scripts/lib/platform.sh" ]; then
34
+ source "$SCRIPT_DIR/scripts/lib/platform.sh"
35
+ elif [ -f "$SCRIPT_DIR/../../scripts/lib/platform.sh" ]; then
36
+ source "$SCRIPT_DIR/../../scripts/lib/platform.sh"
37
+ fi
38
+ platform_detect_os
39
+
40
+ if [ -f "$SCRIPT_DIR/lib/git-case.sh" ]; then
41
+ source "$SCRIPT_DIR/lib/git-case.sh"
42
+ elif [ -f "$SCRIPT_DIR/scripts/lib/git-case.sh" ]; then
43
+ source "$SCRIPT_DIR/scripts/lib/git-case.sh"
44
+ elif [ -f "$SCRIPT_DIR/../../scripts/lib/git-case.sh" ]; then
45
+ source "$SCRIPT_DIR/../../scripts/lib/git-case.sh"
46
+ fi
47
+
48
+ if [ -f "$SCRIPT_DIR/lib/conflict-markers.sh" ]; then
49
+ source "$SCRIPT_DIR/lib/conflict-markers.sh"
50
+ elif [ -f "$SCRIPT_DIR/scripts/lib/conflict-markers.sh" ]; then
51
+ source "$SCRIPT_DIR/scripts/lib/conflict-markers.sh"
52
+ elif [ -f "$SCRIPT_DIR/../../scripts/lib/conflict-markers.sh" ]; then
53
+ source "$SCRIPT_DIR/../../scripts/lib/conflict-markers.sh"
54
+ fi
55
+
56
+ if [ -f "$SCRIPT_DIR/lib/delete-intent.sh" ]; then
57
+ source "$SCRIPT_DIR/lib/delete-intent.sh"
58
+ elif [ -f "$SCRIPT_DIR/scripts/lib/delete-intent.sh" ]; then
59
+ source "$SCRIPT_DIR/scripts/lib/delete-intent.sh"
60
+ elif [ -f "$SCRIPT_DIR/../../scripts/lib/delete-intent.sh" ]; then
61
+ source "$SCRIPT_DIR/../../scripts/lib/delete-intent.sh"
62
+ fi
63
+
64
+ if ! command -v vault_sync_scan_conflict_markers >/dev/null 2>&1; then
65
+ echo "[wiki-snapshot] ERROR: conflict-marker helper unavailable; refusing to run." >&2
66
+ exit 1
67
+ fi
68
+
69
+ # ── Guard: Linux-only operation ────────────────────────────
70
+ platform_require linux
71
+
72
+ # ── Path configuration (env-overridable for plugin portability) ─
73
+ WIKI_DIR="${WIKI_DIR:-/root/wiki}"
74
+ SNAPSHOT_WORKTREE="${WIKI_GIT_WORKTREE:-${SNAPSHOT_WORKTREE:-/root/wiki-git}}"
75
+ LOCK_FILE="${WIKI_SNAPSHOT_LOCK:-/var/lock/wiki-snapshot.lock}"
76
+ DEFAULT_LOG_DIR="$(platform_log_dir)"
77
+ LOG_FILE="${WIKI_SNAPSHOT_LOG:-$DEFAULT_LOG_DIR/wiki-snapshot.log}"
78
+ CLOUD_REMOTE="${CLOUD_REMOTE:-cloud:cloud/wiki}"
79
+ REPAIR_SCRIPT="${WIKI_GIT_REPAIR_SCRIPT:-$SCRIPT_DIR/wiki-git-repair-v3.sh}"
80
+ MAX_S3_ONLY_NOTES="${WIKI_SNAPSHOT_MAX_S3_ONLY_NOTES:-200}"
81
+ DATE=$(date +%Y%m%d_%H%M%S)
82
+ RCLONE_LOG="/tmp/rclone-${DATE}.log"
83
+ SNAPSHOT_DIRECT_S3_NOT_GIT_COUNT=0
84
+
85
+ fetch_origin_main_ref() {
86
+ # The snapshot guards read origin/main directly. Use an explicit destination
87
+ # refspec so a damaged/minimal remote.fetch config cannot leave it stale.
88
+ git fetch --quiet origin +refs/heads/main:refs/remotes/origin/main
89
+ }
90
+
91
+ snapshot_direct_s3_preflight() {
92
+ local tmp_dir
93
+ tmp_dir="$(mktemp -d)"
94
+ # Ensure tmp_dir is cleaned up on any return path or interrupt.
95
+ trap 'rm -rf "$tmp_dir"' RETURN
96
+ local direct_paths="$tmp_dir/direct-s3.paths"
97
+ local direct_notes="$tmp_dir/direct-s3-notes.paths"
98
+ local git_paths="$tmp_dir/git.paths"
99
+ local direct_not_git="$tmp_dir/direct-s3-not-git.paths"
100
+
101
+ if ! rclone lsf "$CLOUD_REMOTE" --recursive --files-only 2>/dev/null | LC_ALL=C sort > "$direct_paths"; then
102
+ echo "[wiki-snapshot] WARN: direct-S3 preflight could not list $CLOUD_REMOTE"
103
+ return 0
104
+ fi
105
+
106
+ grep -vE '^(\.skillwiki/|\.claude/|\.obsidian/|\.antigravitycli/|\.playwright-cli/|raw/\._\.DS_Store$|\._\.DS_Store$)' "$direct_paths" | LC_ALL=C sort -u > "$direct_notes" || true
107
+ (
108
+ cd "$SNAPSHOT_WORKTREE" || exit 1
109
+ fetch_origin_main_ref 2>/dev/null || true
110
+ if git rev-parse --verify --quiet origin/main >/dev/null 2>&1; then
111
+ git -c core.quotePath=false ls-tree -r --name-only origin/main
112
+ else
113
+ git -c core.quotePath=false ls-files
114
+ fi | LC_ALL=C sort -u
115
+ ) > "$git_paths" 2>/dev/null || {
116
+ echo "[wiki-snapshot] WARN: direct-S3 preflight could not list tracked files in snapshot worktree $SNAPSHOT_WORKTREE"
117
+ return 0
118
+ }
119
+
120
+ LC_ALL=C comm -23 "$direct_notes" "$git_paths" > "$direct_not_git"
121
+ SNAPSHOT_DIRECT_S3_NOT_GIT_COUNT="$(wc -l < "$direct_not_git" | tr -d ' ')"
122
+ if [ "$SNAPSHOT_DIRECT_S3_NOT_GIT_COUNT" != "0" ]; then
123
+ echo "[wiki-snapshot] WARN: direct-S3-not-git warning: $SNAPSHOT_DIRECT_S3_NOT_GIT_COUNT note path(s) exist in direct S3 but not in $SNAPSHOT_WORKTREE"
124
+ sed -n '1,20p' "$direct_not_git" | sed 's/^/[wiki-snapshot] WARN: direct-S3-not-git: /'
125
+ return 2
126
+ fi
127
+
128
+ return 0
129
+ }
130
+
131
+ handle_direct_s3_preflight_before_sync() {
132
+ snapshot_direct_s3_preflight
133
+ local rc=$?
134
+ if [ "$rc" -eq 0 ]; then
135
+ return 0
136
+ fi
137
+
138
+ if [ "${WIKI_SNAPSHOT_ALLOW_S3_ONLY_NOTES:-0}" = "1" ]; then
139
+ log "WARNING: direct-S3 preflight found note paths missing from Git; WIKI_SNAPSHOT_ALLOW_S3_ONLY_NOTES=1 allows this live snapshot"
140
+ return 0
141
+ fi
142
+
143
+ case "$MAX_S3_ONLY_NOTES" in
144
+ ''|*[!0-9]*)
145
+ log "ERROR: invalid WIKI_SNAPSHOT_MAX_S3_ONLY_NOTES=$MAX_S3_ONLY_NOTES; refusing live snapshot"
146
+ return 1
147
+ ;;
148
+ esac
149
+
150
+ if [ "$SNAPSHOT_DIRECT_S3_NOT_GIT_COUNT" -le "$MAX_S3_ONLY_NOTES" ]; then
151
+ log "WARNING: direct-S3 preflight found $SNAPSHOT_DIRECT_S3_NOT_GIT_COUNT note path(s) missing from Git; within limit $MAX_S3_ONLY_NOTES, allowing live snapshot"
152
+ return 0
153
+ fi
154
+
155
+ log "ERROR: direct-S3-not-git count exceeds limit: $SNAPSHOT_DIRECT_S3_NOT_GIT_COUNT > $MAX_S3_ONLY_NOTES"
156
+ log "ERROR: Review and promote/delete the S3-only note paths first, raise WIKI_SNAPSHOT_MAX_S3_ONLY_NOTES, or set WIKI_SNAPSHOT_ALLOW_S3_ONLY_NOTES=1 for an explicitly approved promotion run"
157
+ return 1
158
+ }
159
+
160
+ # ── Guard: --max-delete verification ───────────────────────
161
+ # Verify that this script (or a target script) has the --max-delete guard.
162
+ # Returns 0 if guard is present, 1 if missing.
163
+ wiki_snapshot_assert_guards() {
164
+ local script_path="${1:-$0}"
165
+
166
+ if [ ! -f "$script_path" ]; then
167
+ echo "FATAL: snapshot script not found: $script_path" >&2
168
+ return 1
169
+ fi
170
+
171
+ if grep -q -- '--max-delete' "$script_path" 2>/dev/null; then
172
+ return 0
173
+ else
174
+ echo "FATAL: --max-delete guard MISSING from $script_path" >&2
175
+ echo "Do NOT run this script on production until the guard is added." >&2
176
+ echo "Reference: raw/transcripts/2026-05-23-bug-sg01-snapshot-destructive-rclone-sync.md" >&2
177
+ return 1
178
+ fi
179
+ }
180
+
181
+ # Self-check: ensure THIS script contains its own --max-delete guard.
182
+ if ! wiki_snapshot_assert_guards "$0"; then
183
+ echo "[wiki-snapshot] Self-guard check failed — refusing to run." >&2
184
+ exit 1
185
+ fi
186
+
187
+ # ── Dry-run mode ────────────────────────────────────────────
188
+ DRY_RUN=false
189
+ [[ "${1:-}" == "--dry-run" ]] && DRY_RUN=true
190
+
191
+ if [ "$DRY_RUN" = true ]; then
192
+ echo "[wiki-snapshot] DRY RUN"
193
+ echo " WIKI_DIR = $WIKI_DIR"
194
+ echo " SNAPSHOT_WORKTREE = $SNAPSHOT_WORKTREE"
195
+ echo " LOCK_FILE = $LOCK_FILE"
196
+ echo " LOG_FILE = $LOG_FILE"
197
+ echo " CLOUD_REMOTE = $CLOUD_REMOTE"
198
+ echo " REPAIR_SCRIPT = $REPAIR_SCRIPT"
199
+ echo "[wiki-snapshot] DRY RUN: --max-delete guard verified (present in $0)"
200
+ snapshot_direct_s3_preflight || true
201
+ echo "[wiki-snapshot] DRY RUN: would acquire $LOCK_FILE, rclone sync, git commit, push."
202
+ echo "[wiki-snapshot] DRY RUN: Complete. No changes made."
203
+ exit 0
204
+ fi
205
+
206
+ mkdir -p "$(dirname "$LOG_FILE")"
207
+
208
+ # ── Lock file to prevent concurrent execution ──────────────
209
+ exec 200>"$LOCK_FILE"
210
+ if ! flock -n 200; then
211
+ echo "$(date '+%Y-%m-%d %H:%M:%S') ERROR: Another instance of wiki-snapshot is running. Exiting."
212
+ exit 1
213
+ fi
214
+
215
+ # Logging helper
216
+ log() {
217
+ echo "$(date '+%Y-%m-%d %H:%M:%S') $1" | tee -a "$LOG_FILE"
218
+ }
219
+
220
+ raw_dedup_guard() {
221
+ if [ "${WIKI_SNAPSHOT_RAW_DEDUP_GUARD:-1}" = "0" ]; then
222
+ log "raw_dedup guard skipped by WIKI_SNAPSHOT_RAW_DEDUP_GUARD=0"
223
+ return 0
224
+ fi
225
+
226
+ SKILLWIKI_BIN="${WIKI_SNAPSHOT_SKILLWIKI_BIN:-skillwiki}"
227
+ if ! command -v "$SKILLWIKI_BIN" >/dev/null 2>&1; then
228
+ log "ERROR: skillwiki CLI unavailable; refusing to commit snapshot without raw_dedup guard"
229
+ return 1
230
+ fi
231
+
232
+ GUARD_LOG="${RCLONE_LOG}.raw-dedup-guard"
233
+ if ! "$SKILLWIKI_BIN" lint "$SNAPSHOT_WORKTREE" --only raw_dedup --summary >"$GUARD_LOG" 2>&1; then
234
+ log "ERROR: raw_dedup guard failed after cloud sync; refusing to commit snapshot"
235
+ cat "$GUARD_LOG" >>"$LOG_FILE" 2>/dev/null || true
236
+ rm -f "$GUARD_LOG"
237
+ return 1
238
+ fi
239
+
240
+ rm -f "$GUARD_LOG"
241
+ log "raw_dedup guard passed"
242
+ return 0
243
+ }
244
+
245
+ conflict_marker_guard() {
246
+ if [ "${WIKI_SNAPSHOT_CONFLICT_MARKER_GUARD:-1}" = "0" ]; then
247
+ log "conflict-marker guard skipped by WIKI_SNAPSHOT_CONFLICT_MARKER_GUARD=0"
248
+ return 0
249
+ fi
250
+
251
+ local findings
252
+ findings="$(mktemp)" || { log "ERROR: could not create conflict-marker scan temp file"; return 1; }
253
+ if ! vault_sync_scan_conflict_markers "$SNAPSHOT_WORKTREE" "$findings"; then
254
+ log "ERROR: conflict marker blocks found after cloud sync; refusing to commit snapshot"
255
+ vault_sync_log_conflict_marker_findings "$findings" "$LOG_FILE"
256
+ rm -f "$findings"
257
+ return 1
258
+ fi
259
+ rm -f "$findings"
260
+ log "conflict-marker guard passed"
261
+ return 0
262
+ }
263
+
264
+ refresh_git_baseline() {
265
+ (
266
+ cd "$SNAPSHOT_WORKTREE" || exit 1
267
+ fetch_origin_main_ref 2>/dev/null || exit 2
268
+
269
+ if [ -n "$(git status --porcelain)" ]; then
270
+ exit 3
271
+ fi
272
+
273
+ if git merge-base --is-ancestor HEAD origin/main 2>/dev/null; then
274
+ if [ "$(git rev-parse HEAD)" != "$(git rev-parse origin/main)" ]; then
275
+ git merge --ff-only origin/main >/dev/null
276
+ exit 4
277
+ fi
278
+ exit 0
279
+ fi
280
+
281
+ exit 5
282
+ )
283
+ local rc=$?
284
+ case "$rc" in
285
+ 0) return 0 ;;
286
+ 4) log "Git baseline fast-forwarded to origin/main before S3 sync"; return 0 ;;
287
+ 2) log "WARNING: Could not fetch origin/main before S3 sync; continuing with existing refs"; return 0 ;;
288
+ 3) log "WARNING: Snapshot worktree dirty before S3 sync; skipping baseline fast-forward"; return 0 ;;
289
+ 5) log "WARNING: Snapshot worktree is not an ancestor of origin/main; repair step will handle it after S3 sync"; return 0 ;;
290
+ *) log "WARNING: Git baseline refresh failed rc=$rc; continuing"; return 0 ;;
291
+ esac
292
+ }
293
+
294
+ log "=== Wiki Snapshot: $DATE ==="
295
+
296
+ # Check disk space (need at least 100MB free)
297
+ AVAILABLE_KB=$(df -k "$SNAPSHOT_WORKTREE" | awk 'NR==2 {print $4}')
298
+ if [ "$AVAILABLE_KB" -lt 102400 ]; then
299
+ log "ERROR: Insufficient disk space. Only ${AVAILABLE_KB}KB available, need 100MB."
300
+ exit 1
301
+ fi
302
+
303
+ # Check if git directory exists
304
+ if [ ! -d "$SNAPSHOT_WORKTREE/.git" ]; then
305
+ log "ERROR: Git worktree not found or not a git repo: $SNAPSHOT_WORKTREE"
306
+ exit 1
307
+ fi
308
+
309
+ refresh_git_baseline
310
+
311
+ # Single-authority root projections before FUSE/S3 pull promotion.
312
+ if command -v skillwiki >/dev/null 2>&1; then
313
+ if ! skillwiki projections materialize "$WIKI_DIR" --write >>"$LOG_FILE" 2>&1; then
314
+ log "FAIL root projection materialization; snapshot promotion refused"
315
+ exit 1
316
+ fi
317
+ log "OK projections materialize before snapshot sync"
318
+ fi
319
+
320
+ # Common rclone options
321
+ # NOTE: rclone sync already deletes by default; exclusions prevent .git deletion
322
+ RCLONE_OPTS=(
323
+ --exclude ".snapshots/**"
324
+ --exclude ".git/**"
325
+ --exclude ".obsidian/**"
326
+ --exclude ".skillwiki/**"
327
+ --exclude ".claude/**"
328
+ --exclude ".antigravitycli/**"
329
+ --exclude ".playwright-cli/**"
330
+ --exclude "._*"
331
+ --exclude ".conflict*"
332
+ --exclude "*.conflict-*"
333
+ --checksum
334
+ --transfers 8
335
+ --checkers 16
336
+ --fast-list
337
+ --timeout 5m
338
+ --contimeout 1m
339
+ --max-delete 10
340
+ )
341
+
342
+ if ! handle_direct_s3_preflight_before_sync; then
343
+ exit 1
344
+ fi
345
+
346
+ # Sync from cloud directly to git dir using rclone
347
+ echo "Syncing from cloud to git repo (via rclone)..."
348
+
349
+ if ! rclone sync "$CLOUD_REMOTE" "$SNAPSHOT_WORKTREE" "${RCLONE_OPTS[@]}" --stats 10s 2>&1 | tee "$RCLONE_LOG"; then
350
+ RCLONE_EXIT=$?
351
+ log "ERROR: Rclone sync failed with exit code $RCLONE_EXIT"
352
+ log "Last 50 lines of output:"
353
+ tail -50 "$RCLONE_LOG" >> "$LOG_FILE" 2>/dev/null || true
354
+ rm -f "$RCLONE_LOG"
355
+ exit 1
356
+ fi
357
+
358
+ rm -f "$RCLONE_LOG"
359
+
360
+ # Verify sync actually happened
361
+ if [ ! -f "$SNAPSHOT_WORKTREE/index.md" ]; then
362
+ log "ERROR: Sync verification failed - index.md not found in git dir"
363
+ exit 1
364
+ fi
365
+
366
+ # --- Delete-intent no-resurrect ---
367
+ # Git is SSOT for intentional absences. After S3→worktree sync, strip any path
368
+ # that has an active tombstone on origin/main and optionally prune S3.
369
+ snapshot_apply_delete_intents() {
370
+ if ! command -v delete_intent_list_active_paths_from_git >/dev/null 2>&1 \
371
+ && ! command -v delete_intent_list_active_paths >/dev/null 2>&1; then
372
+ log "WARNING: delete-intent helpers unavailable; skipping tombstone no-resurrect"
373
+ return 0
374
+ fi
375
+
376
+ # Re-materialize ledger from git so rclone sync cannot drop git-only intents,
377
+ # then list from the worktree (single parse — avoid re-git-show of every blob).
378
+ (
379
+ cd "$SNAPSHOT_WORKTREE" || exit 0
380
+ fetch_origin_main_ref 2>/dev/null || true
381
+ if git rev-parse --verify --quiet origin/main >/dev/null 2>&1; then
382
+ git checkout origin/main -- meta/delete-intents/ 2>/dev/null || true
383
+ fi
384
+ )
385
+
386
+ local paths_file pruned=0
387
+ paths_file="$(mktemp)"
388
+ if command -v delete_intent_list_active_paths >/dev/null 2>&1; then
389
+ delete_intent_list_active_paths "$SNAPSHOT_WORKTREE" > "$paths_file" 2>/dev/null || true
390
+ elif command -v delete_intent_list_active_paths_from_git >/dev/null 2>&1; then
391
+ delete_intent_list_active_paths_from_git "$SNAPSHOT_WORKTREE" origin/main > "$paths_file" 2>/dev/null || true
392
+ fi
393
+
394
+ if [ ! -s "$paths_file" ]; then
395
+ rm -f "$paths_file"
396
+ log "delete-intent: no active tombstones"
397
+ return 0
398
+ fi
399
+
400
+ local rel_path
401
+ while IFS= read -r rel_path; do
402
+ [ -z "$rel_path" ] && continue
403
+ if [ -e "$SNAPSHOT_WORKTREE/$rel_path" ]; then
404
+ rm -f "$SNAPSHOT_WORKTREE/$rel_path"
405
+ log "delete-intent: stripped resurrected path from worktree: $rel_path"
406
+ fi
407
+ # Bounded S3 prune for tombstoned paths still present on remote.
408
+ if [ "$pruned" -lt 10 ]; then
409
+ if rclone deletefile "${CLOUD_REMOTE%/}/$rel_path" >>"$LOG_FILE" 2>&1; then
410
+ log "delete-intent: pruned S3 object $rel_path"
411
+ pruned=$((pruned + 1))
412
+ fi
413
+ fi
414
+ done < "$paths_file"
415
+
416
+ rm -f "$paths_file"
417
+ log "delete-intent: no-resurrect pass complete (s3_pruned=$pruned)"
418
+ return 0
419
+ }
420
+
421
+ snapshot_apply_delete_intents
422
+
423
+ snapshot_direct_s3_preflight || true
424
+
425
+ # Change to git dir for operations
426
+ cd "$SNAPSHOT_WORKTREE" || { log "ERROR: Failed to cd to $SNAPSHOT_WORKTREE"; exit 1; }
427
+
428
+ # Configure git if not already set
429
+ git config user.email "snapshot@vault-sync.local" 2>/dev/null || true
430
+ git config user.name "Vault Sync Snapshot" 2>/dev/null || true
431
+
432
+ if command -v git_case_assert_clean >/dev/null 2>&1; then
433
+ if ! CASE_CONFLICTS=$(git_case_conflicts); then
434
+ log "ERROR: Case-only path collision detected; refusing to commit snapshot"
435
+ printf '%s\n' "$CASE_CONFLICTS" | tee -a "$LOG_FILE"
436
+ exit 1
437
+ fi
438
+ else
439
+ log "ERROR: git-case helper unavailable; refusing to commit snapshot"
440
+ exit 1
441
+ fi
442
+
443
+ # Check if git is in broken state (rebase in progress, merge conflicts, diverged)
444
+ needs_repair=false
445
+
446
+ # Check for rebase/merge in progress
447
+ if [ -d ".git/rebase-merge" ] || [ -d ".git/rebase-apply" ] || [ -f ".git/MERGE_HEAD" ]; then
448
+ log "WARNING: Git rebase/merge in progress - needs repair"
449
+ needs_repair=true
450
+ fi
451
+
452
+ # Check for unmerged files
453
+ if git diff --name-only --diff-filter=U | grep -q . 2>/dev/null; then
454
+ log "WARNING: Merge conflicts detected - needs repair"
455
+ needs_repair=true
456
+ fi
457
+
458
+ # Check if we're in detached HEAD state
459
+ if ! git symbolic-ref -q HEAD >/dev/null 2>&1; then
460
+ log "WARNING: Detached HEAD state - needs repair"
461
+ needs_repair=true
462
+ fi
463
+
464
+ # Check if we can fast-forward to origin
465
+ DEFAULT_BRANCH=$(git remote show origin 2>/dev/null | grep 'HEAD branch' | awk '{print $3}')
466
+ DEFAULT_BRANCH=${DEFAULT_BRANCH:-main}
467
+
468
+ if ! git merge-base --is-ancestor HEAD "origin/$DEFAULT_BRANCH" 2>/dev/null; then
469
+ # Local HEAD is not an ancestor of origin - diverged
470
+ log "WARNING: Diverged from origin/$DEFAULT_BRANCH - needs repair"
471
+ needs_repair=true
472
+ fi
473
+
474
+ # Repair if needed
475
+ if [ "$needs_repair" = true ]; then
476
+ log "Running git repair..."
477
+ if ! bash "$REPAIR_SCRIPT" 2>&1 | tee -a "$LOG_FILE"; then
478
+ log "ERROR: Git repair failed"
479
+ exit 1
480
+ fi
481
+ cd "$SNAPSHOT_WORKTREE" || { log "ERROR: Failed to cd to $SNAPSHOT_WORKTREE after repair"; exit 1; }
482
+
483
+ # Re-run rclone sync after repair to ensure we have latest
484
+ if ! rclone sync "$CLOUD_REMOTE" "$SNAPSHOT_WORKTREE" "${RCLONE_OPTS[@]}" --stats 10s 2>&1 | tee "$RCLONE_LOG"; then
485
+ log "ERROR: Post-repair rclone sync failed"
486
+ tail -50 "$RCLONE_LOG" >> "$LOG_FILE" 2>/dev/null || true
487
+ rm -f "$RCLONE_LOG"
488
+ exit 1
489
+ fi
490
+ rm -f "$RCLONE_LOG"
491
+ snapshot_apply_delete_intents
492
+ fi
493
+
494
+ if ! raw_dedup_guard; then
495
+ exit 1
496
+ fi
497
+
498
+ if ! conflict_marker_guard; then
499
+ exit 1
500
+ fi
501
+
502
+ # Check for changes
503
+ if [ -z "$(git status --porcelain)" ]; then
504
+ log "No changes to commit"
505
+ exit 0
506
+ fi
507
+
508
+ # Re-check immediately before staging because rclone/repair may have changed the
509
+ # tree after the first guard.
510
+ if command -v git_case_assert_clean >/dev/null 2>&1; then
511
+ if ! CASE_CONFLICTS=$(git_case_conflicts); then
512
+ log "ERROR: Case-only path collision detected before git add; refusing to commit snapshot"
513
+ printf '%s\n' "$CASE_CONFLICTS" | tee -a "$LOG_FILE"
514
+ exit 1
515
+ fi
516
+ fi
517
+
518
+ # Commit
519
+ echo "Committing changes..."
520
+ if ! git add -A; then
521
+ log "ERROR: git add failed"
522
+ exit 1
523
+ fi
524
+
525
+ if ! git commit -m "Snapshot $DATE"; then
526
+ log "ERROR: git commit failed"
527
+ exit 1
528
+ fi
529
+
530
+ # Pull with rebase to get any remote changes (with retry)
531
+ echo "Pulling from origin..."
532
+ PULL_RETRIES=3
533
+ PULL_SUCCESS=false
534
+
535
+ for i in $(seq 1 $PULL_RETRIES); do
536
+ if git pull --rebase origin "$DEFAULT_BRANCH" 2>&1 | tee -a "$LOG_FILE"; then
537
+ PULL_SUCCESS=true
538
+ break
539
+ fi
540
+ log "WARNING: Pull attempt $i failed, retrying..."
541
+ sleep 5
542
+ done
543
+
544
+ if [ "$PULL_SUCCESS" = false ]; then
545
+ log "ERROR: Pull failed after $PULL_RETRIES attempts - attempting repair"
546
+ if bash "$REPAIR_SCRIPT" 2>&1 | tee -a "$LOG_FILE"; then
547
+ # After repair, commit timestamp will be slightly different
548
+ DATE=$(date +%Y%m%d_%H%M%S)
549
+ cd "$SNAPSHOT_WORKTREE" || { log "ERROR: Failed to cd after repair"; exit 1; }
550
+
551
+ # Re-sync after repair
552
+ rclone sync "$CLOUD_REMOTE" "$SNAPSHOT_WORKTREE" "${RCLONE_OPTS[@]}" 2>&1 | tee -a "$LOG_FILE" || true
553
+ if ! raw_dedup_guard || ! conflict_marker_guard; then
554
+ exit 1
555
+ fi
556
+ git add -A || true
557
+ git commit -m "Snapshot $DATE (post-repair)" || true
558
+ else
559
+ log "ERROR: Repair failed after pull failure"
560
+ exit 1
561
+ fi
562
+ fi
563
+
564
+ # Push (with retry)
565
+ echo "Pushing to origin..."
566
+ PUSH_RETRIES=3
567
+ PUSH_SUCCESS=false
568
+
569
+ for i in $(seq 1 $PUSH_RETRIES); do
570
+ if git push origin "$DEFAULT_BRANCH" 2>&1 | tee -a "$LOG_FILE"; then
571
+ PUSH_SUCCESS=true
572
+ break
573
+ fi
574
+ log "WARNING: Push attempt $i failed, retrying..."
575
+ sleep 5
576
+ done
577
+
578
+ if [ "$PUSH_SUCCESS" = true ]; then
579
+ log "Push successful"
580
+ log "Status: complete"
581
+ exit 0
582
+ else
583
+ log "ERROR: Push failed after $PUSH_RETRIES attempts"
584
+ # Run repair for next attempt
585
+ bash "$REPAIR_SCRIPT" 2>&1 | tee -a "$LOG_FILE" || true
586
+ exit 1
587
+ fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.9.63",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "skillwiki": "dist/cli.js",
@@ -13,7 +13,7 @@
13
13
  "README.md"
14
14
  ],
15
15
  "scripts": {
16
- "build": "tsup && rm -rf ./skills && cp -r ../skills ./skills",
16
+ "build": "tsup && node ./scripts/copy-build-assets.mjs",
17
17
  "test": "vitest run",
18
18
  "test:watch": "vitest",
19
19
  "typecheck": "tsc --noEmit",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.9.63",
3
+ "version": "0.10.0",
4
4
  "skills": "./",
5
5
  "description": "Project-aware Karpathy-style knowledge base for Claude Code: 19 prompt-only skills (wiki-*, proj-*, using-skillwiki) backed by the deterministic `skillwiki` CLI.",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.9.63",
3
+ "version": "0.10.0",
4
4
  "description": "Project-aware Karpathy-style knowledge base for Codex with 19 prompt-only skills backed by the deterministic skillwiki CLI.",
5
5
  "author": {
6
6
  "name": "karlorz",
package/skills/README.md CHANGED
@@ -40,3 +40,16 @@ canonical skill, agent, or hook assets. Run
40
40
 
41
41
  The sibling `vault-sync` plugin ships six additional operational skills under
42
42
  `packages/vault-sync/skills/` and is packaged separately from this skill set.
43
+
44
+
45
+ ## Managed Vault Mutation Contract
46
+
47
+ Before a managed vault mutation, invoke the managed SkillWiki command while the draft remains outside the authoritative target path. The command resolves fleet authority, refuses existing unmerged/review-required state, converges an authorized Git writer, freezes the base OID, and only then applies the write. Do not run `git pull --rebase --autostash` after placing the authoritative change in the live worktree. Do not edit root `index.md` or root `log.md` directly; projection and log commands own those compatibility files.
48
+
49
+ - typed pages: `skillwiki page publish <draft> <vault> --target <path>` then the same command with `--write`
50
+ - archive: `skillwiki archive <path> <vault>`
51
+ - ad-hoc structural log: `skillwiki log-append <vault> --content '<entry>'` (Release A dual-write) or event materialization (Release B)
52
+ - project/root index: `skillwiki project-index <slug> <vault> --apply` and `skillwiki index rebuild <vault> --write` only through managed commands
53
+ - log projection: `skillwiki log materialize <vault> [--write]`
54
+ - paired projections: `skillwiki projections materialize <vault> [--write]`
55
+
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillwiki/skills",
3
- "version": "0.9.63",
3
+ "version": "0.10.0",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",
@@ -67,3 +67,6 @@ Rules:
67
67
  - Writing spec/plan files outside the work folder.
68
68
  - Marking `status: completed` without a `completed:` date.
69
69
  - Accepting tasks.md status labels without independent disk verification.
70
+ ## Managed writes
71
+
72
+ Use managed SkillWiki commands (`skillwiki page publish`, `skillwiki archive`) rather than editing root index.md/log.md directly.
@@ -67,3 +67,6 @@ Rules:
67
67
  - Writing spec/plan files outside the work folder.
68
68
  - Marking `status: completed` without a `completed:` date.
69
69
  - Accepting tasks.md status labels without independent disk verification.
70
+ ## Managed writes
71
+
72
+ Use managed SkillWiki commands (`skillwiki page publish`, `skillwiki archive`) rather than editing root index.md/log.md directly.
@@ -75,6 +75,19 @@ path, run publisher dry-run, then run the same command with `--write`.
75
75
  - Non-typed project work items and immutable raw sources keep their existing
76
76
  workflows.
77
77
 
78
+
79
+ ## Managed Vault Mutation Contract
80
+
81
+ Before a managed vault mutation, invoke the managed SkillWiki command while the draft remains outside the authoritative target path. The command resolves fleet authority, refuses existing unmerged/review-required state, converges an authorized Git writer, freezes the base OID, and only then applies the write. Do not run `git pull --rebase --autostash` after placing the authoritative change in the live worktree. Do not edit root `index.md` or root `log.md` directly; projection and log commands own those compatibility files.
82
+
83
+ - typed pages: `skillwiki page publish <draft> <vault> --target <path>` then the same command with `--write`
84
+ - archive: `skillwiki archive <path> <vault>`
85
+ - ad-hoc structural log: `skillwiki log-append <vault> --content '<entry>'` (Release A dual-write) or event materialization (Release B)
86
+ - project/root index: `skillwiki project-index <slug> <vault> --apply` and `skillwiki index rebuild <vault> --write` only through managed commands
87
+ - log projection: `skillwiki log materialize <vault> [--write]`
88
+ - paired projections: `skillwiki projections materialize <vault> [--write]`
89
+
90
+
78
91
  ## CLI probe and failsafe (vault-mutating skills)
79
92
 
80
93
  Vault fleets that combine S3 + GitHub need an explicit delete-intent path. Prefer the skillwiki CLI; when it is missing, agents that still have **git/gh access to the private vault remote** use **FAILSAFE-GIT**.