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,631 @@
1
+ #!/bin/bash
2
+ # wiki-pull-with-auto-resolve.sh — git pull --rebase with archive-commit conflict storm
3
+ # auto-resolution.
4
+ #
5
+ # When git pull --rebase hits content conflicts on archive-only commits
6
+ # (message matches "^archive: moved"), auto-resolve all conflicts with
7
+ # --ours (keep HEAD) and continue. Non-archive conflicts are left for
8
+ # manual resolution.
9
+ #
10
+ # Append-only log.md conflicts (root log.md or any */log.md) are union-merged
11
+ # via `git merge-file --union` so concurrent appends on both sides are
12
+ # preserved without wedging the unattended pull. This runs before the
13
+ # archive-commit check, so log.md conflicts on archive/snapshot commits are
14
+ # also union-merged (append-only logs benefit from union, not --ours). If any
15
+ # conflicted path is not a log.md, the union resolver declines and the
16
+ # archive/manual branch handles the full set. A rebase iteration cap
17
+ # (MAX_REBASE_ITERS) prevents a silent resolver failure from spinning forever.
18
+ #
19
+ # Before pull, stale-clean sequencer state is cleared with recovery-ref +
20
+ # `git rebase --quit` (never abort). Fully materialized local commits (content
21
+ # already present on the remote tip) may be dropped from the rebase todo.
22
+ #
23
+ # Convergence uses a frozen TARGET_OID from the post-fetch tip. On a non-archive
24
+ # manual conflict the helper may retry once if the remote advanced while the
25
+ # conflict identity is still owned/unmutated; otherwise it fails closed with
26
+ # handoff=1 (review-required). Later runs refuse to reclaim handoff rebases.
27
+ #
28
+ # Force-push guard: this helper never publishes to a remote. Do not add
29
+ # non-fast-forward push flags here. If a publish path is added later,
30
+ # require merge-base ancestry of the remote tip before a plain push.
31
+
32
+ #
33
+ # Usage:
34
+ # wiki-pull-with-auto-resolve.sh [--remote <name>] [--branch <name>]
35
+ # Defaults: origin, main
36
+
37
+ set -u
38
+
39
+ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]:-$0}" )" && pwd )"
40
+ . "$SCRIPT_DIR/lib/platform.sh"
41
+ . "$SCRIPT_DIR/lib/lockfile.sh"
42
+ . "$SCRIPT_DIR/lib/git-case.sh"
43
+ . "$SCRIPT_DIR/lib/conflict-markers.sh"
44
+ . "$SCRIPT_DIR/lib/git-rebase-state.sh"
45
+ . "$SCRIPT_DIR/lib/git-materialization.sh"
46
+ . "$SCRIPT_DIR/lib/git-operation-journal.sh"
47
+ . "$SCRIPT_DIR/lib/managed-write-lock.sh"
48
+ platform_detect_os
49
+
50
+ WIKI_DIR="${WIKI_DIR:-$HOME/wiki}"
51
+ REMOTE="${1:-origin}"
52
+ BRANCH="${2:-main}"
53
+ LOG_FILE="$(platform_log_dir)/wiki-pull.log"
54
+ # Vault-scoped cooperative lock so concurrent pulls on the same vault fail closed.
55
+ WIKI_DIR_HASH="$(printf '%s' "$WIKI_DIR" | shasum -a 256 2>/dev/null | cut -c1-16)"
56
+ if [ -z "$WIKI_DIR_HASH" ]; then
57
+ WIKI_DIR_HASH="$(printf '%s' "$WIKI_DIR" | cksum | awk '{print $1}')"
58
+ fi
59
+ LOCK_FILE="$(platform_cache_dir)/wiki-pull.${WIKI_DIR_HASH}.lock"
60
+ LOCK_HELD=0
61
+
62
+ mkdir -p "$(dirname "$LOG_FILE")" "$(dirname "$LOCK_FILE")"
63
+
64
+ log() { printf '%s %s\n' "$(date -u +%FT%TZ)" "$*" >>"$LOG_FILE"; }
65
+
66
+ release_pull_lock() {
67
+ if [ "${LOCK_HELD:-0}" = "1" ] && [ -n "${LOCK_FILE:-}" ]; then
68
+ lockfile_release "$LOCK_FILE"
69
+ LOCK_HELD=0
70
+ fi
71
+ }
72
+
73
+ # Always release cooperative lock and owned managed-write lock on exit.
74
+ trap 'release_pull_lock; vault_sync_managed_lock_release' EXIT
75
+
76
+ acquire_pull_lock() {
77
+ local lock_rc=0
78
+ lockfile_acquire "$LOCK_FILE" 600 || lock_rc=$?
79
+ if [ "$lock_rc" -eq 1 ]; then
80
+ log "FAIL pull lock contention path=$LOCK_FILE"
81
+ echo "FAIL: another wiki-pull is in flight for this vault (lock=$LOCK_FILE)" >&2
82
+ return 1
83
+ fi
84
+ if [ "$lock_rc" -eq 2 ]; then
85
+ log "stale pull lock reclaimed path=$LOCK_FILE"
86
+ fi
87
+ LOCK_HELD=1
88
+ return 0
89
+ }
90
+
91
+ drop_or_preserve_untracked_remote_overlaps() {
92
+ local removed=0
93
+ local preserved=0
94
+ local path remote_blob
95
+ local collision_root backup_path backup_dir
96
+
97
+ while IFS= read -r -d '' path; do
98
+ if ! git cat-file -e "$REMOTE/$BRANCH:$path" 2>/dev/null; then
99
+ continue
100
+ fi
101
+
102
+ remote_blob="$(mktemp)"
103
+ if git show "$REMOTE/$BRANCH:$path" >"$remote_blob" 2>/dev/null && cmp -s "$path" "$remote_blob"; then
104
+ if rm -f -- "$path"; then
105
+ removed=$((removed + 1))
106
+ else
107
+ rm -f "$remote_blob"
108
+ log "FAIL could not remove untracked remote duplicate before pull: $path"
109
+ return 1
110
+ fi
111
+ else
112
+ rm -f "$remote_blob"
113
+ if [ -z "${collision_root:-}" ]; then
114
+ collision_root="$(platform_cache_dir)/untracked-collisions/$(date -u +%Y%m%dT%H%M%SZ)-$$"
115
+ fi
116
+ backup_path="$collision_root/$path"
117
+ backup_dir="$(dirname "$backup_path")"
118
+ if ! mkdir -p "$backup_dir" || ! cp -p "$path" "$backup_path"; then
119
+ log "FAIL could not preserve divergent untracked remote overlap before pull: $path"
120
+ return 1
121
+ fi
122
+ if rm -f -- "$path"; then
123
+ preserved=$((preserved + 1))
124
+ log "PRESERVE divergent untracked remote overlap before pull: $path -> $backup_path"
125
+ else
126
+ log "FAIL could not remove preserved divergent untracked remote overlap before pull: $path"
127
+ return 1
128
+ fi
129
+ fi
130
+ rm -f "$remote_blob"
131
+ done < <(git ls-files --others --exclude-standard -z)
132
+
133
+ if [ "$removed" -gt 0 ]; then
134
+ log "DROP $removed identical untracked remote duplicate(s) before pull"
135
+ fi
136
+ if [ "$preserved" -gt 0 ]; then
137
+ log "PRESERVE $preserved divergent untracked remote overlap(s) before pull"
138
+ fi
139
+ return 0
140
+ }
141
+
142
+ is_log_conflict_path() {
143
+ case "$1" in
144
+ log.md|*/log.md) return 0 ;;
145
+ *) return 1 ;;
146
+ esac
147
+ }
148
+
149
+ # Roll back paths already staged by an in-progress union resolve so a later
150
+ # archive/manual resolve branch starts from a clean, fully-conflicted state.
151
+ # `git checkout --conflict=merge` re-creates the UU state (all 3 index stages)
152
+ # in both index and worktree, undoing a prior `git add`. Best-effort — errors
153
+ # are logged, not fatal.
154
+ rollback_resolved() {
155
+ local arr_name="$1[@]"
156
+ local p
157
+ for p in "${!arr_name}"; do
158
+ if ! git checkout --conflict=merge -- "$p" 2>/dev/null; then
159
+ git restore --staged -- "$p" 2>/dev/null || log "WARN rollback could not restore conflict state: $p"
160
+ fi
161
+ done
162
+ }
163
+
164
+ verify_no_tracked_conflict_markers() {
165
+ local matches
166
+ matches="$(mktemp)" || { log "FAIL could not create conflict-marker scan temp file"; return 1; }
167
+ if ! vault_sync_scan_conflict_markers "$WIKI_DIR" "$matches"; then
168
+ log "FAIL unresolved conflict marker blocks remain after pull"
169
+ vault_sync_log_conflict_marker_findings "$matches" "$LOG_FILE"
170
+ rm -f "$matches"
171
+ return 1
172
+ fi
173
+ rm -f "$matches"
174
+ return 0
175
+ }
176
+
177
+ # Composite derived-artifact resolver (log, root index, project index, taxonomy).
178
+ # Delegates the complete conflict set to skillwiki sync resolve-derived; no
179
+ # blanket checkout --ours for archive/snapshot mixed sets.
180
+ try_auto_resolve_derived_conflicts() {
181
+ local op_id="$1"
182
+ local cli_ts="${SCRIPT_DIR}/../../cli/src/cli.ts"
183
+ local cmd_rc=1
184
+ if [ -z "$op_id" ]; then
185
+ return 1
186
+ fi
187
+ # Prefer monorepo CLI when present (packaged install uses adjacent dist later).
188
+ if [ -f "$cli_ts" ] && command -v npx >/dev/null 2>&1; then
189
+ if VAULT_SYNC_OPERATION_ID="$op_id" \
190
+ npx --yes tsx "$cli_ts" sync resolve-derived "$WIKI_DIR" --operation-id "$op_id" >>"$LOG_FILE" 2>&1; then
191
+ log "AUTO-RESOLVE composite derived artifacts op=$op_id"
192
+ return 0
193
+ fi
194
+ cmd_rc=$?
195
+ log "WARN monorepo resolve-derived failed rc=$cmd_rc op=$op_id"
196
+ fi
197
+ if command -v skillwiki >/dev/null 2>&1; then
198
+ if VAULT_SYNC_OPERATION_ID="$op_id" \
199
+ skillwiki sync resolve-derived "$WIKI_DIR" --operation-id "$op_id" >>"$LOG_FILE" 2>&1; then
200
+ log "AUTO-RESOLVE composite derived artifacts op=$op_id (skillwiki)"
201
+ return 0
202
+ fi
203
+ fi
204
+ return 1
205
+ }
206
+
207
+ # Build a temporary sequence editor that drops fully materialized commits.
208
+ prepare_materialization_sequence_editor() {
209
+ local drop_list="$1"
210
+ local editor_script="$2"
211
+ cat > "$editor_script" <<EOF
212
+ #!/bin/bash
213
+ set -u
214
+ DROP_LIST="$drop_list"
215
+ LIB="$SCRIPT_DIR/lib/git-materialization.sh"
216
+ . "\$LIB"
217
+ vault_sync_sequence_editor_drop "\$DROP_LIST" "\$1"
218
+ EOF
219
+ chmod +x "$editor_script"
220
+ }
221
+
222
+ # Rebase onto an exact frozen OID (not a moving symbolic remote tip alone).
223
+ # Materialization drop list is computed against that same OID.
224
+ run_rebase_onto_target() {
225
+ local target="$1"
226
+ local drop_list editor_script merge_base rc
227
+
228
+ drop_list="$(mktemp)"
229
+ editor_script="$(mktemp)"
230
+ merge_base="$(git merge-base HEAD "$target" 2>/dev/null || true)"
231
+ export GIT_SEQUENCE_EDITOR=:
232
+ if [ -n "$merge_base" ]; then
233
+ vault_sync_list_materialized_commits "$merge_base" "$target" "$drop_list" "$WIKI_DIR"
234
+ if [ -s "$drop_list" ]; then
235
+ while IFS= read -r drop_sha; do
236
+ [ -n "$drop_sha" ] || continue
237
+ log "DROP materialized commit $drop_sha (proven on $target)"
238
+ done <"$drop_list"
239
+ prepare_materialization_sequence_editor "$drop_list" "$editor_script"
240
+ export GIT_SEQUENCE_EDITOR="$editor_script"
241
+ fi
242
+ fi
243
+ git -c rebase.reapplyCherryPicks=true rebase "$target" 2>>"$LOG_FILE"
244
+ rc=$?
245
+ rm -f "$drop_list" "$editor_script"
246
+ return $rc
247
+ }
248
+
249
+ # Surface a non-archive manual conflict: optional one owned stale-target retry,
250
+ # else review-required handoff. Returns 0 with REBASE_RC set when a retry was
251
+ # started (caller should continue the loop); exits 1 on handoff.
252
+ handle_manual_conflict() {
253
+ local commit_msg="$1"
254
+ local conflicts="$2"
255
+ local live_oid old_target
256
+
257
+ vault_sync_op_record_conflict_identity "$WIKI_DIR" "$OP_ID"
258
+
259
+ # Test-only hook (unset in production): inject remote advance / human mutation.
260
+ if [ -n "${VAULT_SYNC_TEST_ON_CONFLICT_HOOK:-}" ]; then
261
+ eval "$VAULT_SYNC_TEST_ON_CONFLICT_HOOK"
262
+ fi
263
+
264
+ git fetch --quiet "$REMOTE" "$BRANCH" 2>>"$LOG_FILE"
265
+ live_oid="$(git rev-parse "$REMOTE/$BRANCH")"
266
+
267
+ if vault_sync_op_may_retry "$WIKI_DIR" "$OP_ID" "$live_oid"; then
268
+ log "RETRY stale target $TARGET_OID -> $live_oid op=$OP_ID"
269
+ vault_sync_op_set_phase "$WIKI_DIR" "$OP_ID" "retrying"
270
+ vault_sync_op_set_field "$WIKI_DIR" "$OP_ID" "retry_count" "1"
271
+ git rebase --abort 2>>"$LOG_FILE" || true
272
+ old_target="$TARGET_OID"
273
+ TARGET_OID="$live_oid"
274
+ # CAS update recovery target ref (expected old = previous TARGET_OID).
275
+ if ! vault_sync_op_cas_recovery_target "$WIKI_DIR" "$OP_ID" "$TARGET_OID" "$old_target" 2>>"$LOG_FILE"; then
276
+ vault_sync_op_mark_review_required "$WIKI_DIR" "$OP_ID" "recovery-target-cas-failed"
277
+ log "FAIL recovery target CAS failed op=$OP_ID old=$old_target new=$TARGET_OID"
278
+ exit 1
279
+ fi
280
+ vault_sync_op_set_field "$WIKI_DIR" "$OP_ID" "target_oid" "$TARGET_OID"
281
+ vault_sync_op_set_phase "$WIKI_DIR" "$OP_ID" "rebasing"
282
+ run_rebase_onto_target "$TARGET_OID"
283
+ REBASE_RC=$?
284
+ return 0
285
+ fi
286
+
287
+ vault_sync_op_mark_review_required "$WIKI_DIR" "$OP_ID" "semantic-conflict-or-stale-exhausted"
288
+ log "MANUAL-RESOLVE-NEEDED ($commit_msg): $conflicts"
289
+ echo "=== CONFLICT on non-archive commit op=$OP_ID ==="
290
+ echo "Commit: $commit_msg"
291
+ echo "Conflicted files:"
292
+ echo "$conflicts"
293
+ echo ""
294
+ echo "Resolve manually, then run: git rebase --continue"
295
+ exit 1
296
+ }
297
+
298
+ # Log handoff only for review-required journals (not complete ops that also set
299
+ # handoff=1). Prefer matching sequencer onto/orig-head to journal fields.
300
+ log_review_required_handoff_if_present() {
301
+ local jdir jf op_id phase handoff j_target j_orig seq_onto seq_orig matched any_review
302
+ local git_dir rebase_dir
303
+ jdir="$(vault_sync_op_journal_dir "$WIKI_DIR" 2>/dev/null || true)"
304
+ [ -n "${jdir:-}" ] && [ -d "$jdir" ] || return 0
305
+
306
+ seq_onto=""
307
+ seq_orig=""
308
+ git_dir="$(git -C "$WIKI_DIR" rev-parse --git-dir 2>/dev/null || true)"
309
+ case "$git_dir" in
310
+ /*) ;;
311
+ "") git_dir="" ;;
312
+ *) git_dir="$WIKI_DIR/$git_dir" ;;
313
+ esac
314
+ rebase_dir=""
315
+ if [ -n "$git_dir" ]; then
316
+ if [ -d "$git_dir/rebase-merge" ]; then
317
+ rebase_dir="$git_dir/rebase-merge"
318
+ elif [ -d "$git_dir/rebase-apply" ]; then
319
+ rebase_dir="$git_dir/rebase-apply"
320
+ fi
321
+ fi
322
+ if [ -n "$rebase_dir" ]; then
323
+ [ -f "$rebase_dir/onto" ] && seq_onto="$(tr -d '[:space:]' <"$rebase_dir/onto")"
324
+ if [ -f "$rebase_dir/orig-head" ]; then
325
+ seq_orig="$(tr -d '[:space:]' <"$rebase_dir/orig-head")"
326
+ elif [ -f "$rebase_dir/orig_head" ]; then
327
+ seq_orig="$(tr -d '[:space:]' <"$rebase_dir/orig_head")"
328
+ fi
329
+ fi
330
+
331
+ matched=0
332
+ any_review=0
333
+ for jf in "$jdir"/*.env; do
334
+ [ -f "$jf" ] || continue
335
+ op_id="$(basename "$jf" .env)"
336
+ phase="$(vault_sync_op_get_field "$WIKI_DIR" "$op_id" phase 2>/dev/null || true)"
337
+ handoff="$(vault_sync_op_get_field "$WIKI_DIR" "$op_id" handoff 2>/dev/null || true)"
338
+ # Only review-required (ignore complete ops that also set handoff=1).
339
+ [ "$phase" = "review-required" ] || continue
340
+ [ "$handoff" = "1" ] || continue
341
+ any_review=1
342
+ j_target="$(vault_sync_op_get_field "$WIKI_DIR" "$op_id" target_oid 2>/dev/null || true)"
343
+ j_orig="$(vault_sync_op_get_field "$WIKI_DIR" "$op_id" original_head 2>/dev/null || true)"
344
+ if [ -n "$seq_onto" ] && [ -n "$j_target" ] && [ "$seq_onto" = "$j_target" ]; then
345
+ if [ -z "$seq_orig" ] || [ -z "$j_orig" ] || [ "$seq_orig" = "$j_orig" ]; then
346
+ log "handoff journal present; refusing auto-cleanup op=${op_id} (sequencer match)"
347
+ matched=1
348
+ break
349
+ fi
350
+ fi
351
+ done
352
+
353
+ if [ "$matched" -eq 0 ] && [ "$any_review" -eq 1 ]; then
354
+ # review-required handoff exists but sequencer fields did not uniquely match;
355
+ # still note handoff without claiming complete-op journals.
356
+ log "handoff journal present; refusing auto-cleanup"
357
+ fi
358
+ }
359
+
360
+ cd "$WIKI_DIR" || { log "ERROR: cd $WIKI_DIR failed"; exit 1; }
361
+
362
+ # Serialize with managed CLI writers (shared lock path). Fail closed on contention.
363
+ if ! vault_sync_managed_lock_acquire "$WIKI_DIR" "wiki-pull"; then
364
+ log "FAIL managed-write lock held — refusing concurrent pull"
365
+ exit 1
366
+ fi
367
+
368
+ # Stable handoff gate: never open a new journal/stash while unmerged paths,
369
+ # an in-progress non-rebase Git operation, or a review-required journal exist.
370
+ PREFLIGHT_BLOCKER="$(vault_sync_op_preflight_blocker "$WIKI_DIR" 2>/dev/null || true)"
371
+ if [ -n "$PREFLIGHT_BLOCKER" ]; then
372
+ PREFLIGHT_REASON="${PREFLIGHT_BLOCKER%% *}"
373
+ PREFLIGHT_OP="${PREFLIGHT_BLOCKER#* }"
374
+ [ "$PREFLIGHT_OP" = "$PREFLIGHT_BLOCKER" ] && PREFLIGHT_OP=""
375
+ log "HANDOFF existing reason=$PREFLIGHT_REASON op=${PREFLIGHT_OP:-none}; no new operation"
376
+ printf 'VAULT_SYNC_HANDOFF reason=%s op=%s\n' "$PREFLIGHT_REASON" "${PREFLIGHT_OP:-none}"
377
+ exit 2
378
+ fi
379
+
380
+ # Classify leftover rebase state. Active rebases fail closed; stale-clean
381
+ # state is cleared with recovery-ref + quit (never abort — abort would reset
382
+ # the branch tip to orig-head and discard newer authored work).
383
+ # Handoff immunity: never abort a rebase owned by a handoff journal.
384
+ # Active rebase with unmerged paths is handled by the preflight gate above
385
+ # (exit 2). Active rebase without unmerged paths still fails closed here.
386
+ if [ -d "$WIKI_DIR/.git/rebase-merge" ] || [ -d "$WIKI_DIR/.git/rebase-apply" ]; then
387
+ REBASE_STATE="$(vault_sync_rebase_state "$WIKI_DIR")"
388
+ log "REBASE-STATE $REBASE_STATE"
389
+ case "$REBASE_STATE" in
390
+ none)
391
+ ;;
392
+ stale-clean)
393
+ PRE_CLEANUP_HEAD="$(git rev-parse HEAD)"
394
+ if vault_sync_clear_stale_rebase "$WIKI_DIR"; then
395
+ POST_CLEANUP_HEAD="$(git rev-parse HEAD)"
396
+ if [ "$PRE_CLEANUP_HEAD" != "$POST_CLEANUP_HEAD" ]; then
397
+ log "FAIL stale rebase cleanup moved HEAD ($PRE_CLEANUP_HEAD -> $POST_CLEANUP_HEAD)"
398
+ exit 1
399
+ fi
400
+ log "CLEANUP stale-clean rebase via quit (tip preserved at $PRE_CLEANUP_HEAD)"
401
+ else
402
+ log "FAIL could not clear stale-clean rebase state safely"
403
+ exit 1
404
+ fi
405
+ ;;
406
+ active|*)
407
+ log "FAIL active rebase state present — refusing auto-cleanup"
408
+ log_review_required_handoff_if_present
409
+ exit 1
410
+ ;;
411
+ esac
412
+ fi
413
+
414
+ if ! CASE_CONFLICTS=$(git_case_conflicts); then
415
+ log "FAIL case-only path collision detected"
416
+ printf '%s\n' "$CASE_CONFLICTS" >>"$LOG_FILE"
417
+ exit 1
418
+ fi
419
+
420
+ # Do a git fetch first to be safe
421
+ git fetch --quiet "$REMOTE" "$BRANCH" 2>>"$LOG_FILE"
422
+ log "FETCH $REMOTE/$BRANCH"
423
+
424
+ # Check if we're behind
425
+ BEHIND=$(git rev-list --count "HEAD..$REMOTE/$BRANCH" 2>/dev/null || echo 0)
426
+ if [ "$BEHIND" -eq 0 ]; then
427
+ if ! verify_no_tracked_conflict_markers; then
428
+ exit 1
429
+ fi
430
+ log "UP-TO-DATE (0 behind)"
431
+ exit 0
432
+ fi
433
+
434
+ log "PULL --rebase ($BEHIND commits behind)"
435
+
436
+ # Cooperative lock before first mutation (untracked overlap handling, stash, rebase).
437
+ if ! acquire_pull_lock; then
438
+ exit 1
439
+ fi
440
+
441
+ # Begin journal + freeze target BEFORE untracked-overlap mutations so the
442
+ # operation owns the whole convergence transaction.
443
+ OP_ID="pull-$(hostname -s 2>/dev/null || echo host)-$(date -u +%Y%m%dT%H%M%SZ)-${$}-$(od -An -N4 -tx1 /dev/urandom | tr -d ' \n')"
444
+ ORIGINAL_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
445
+ ORIGINAL_HEAD="$(git rev-parse HEAD)"
446
+ # Freeze the convergence tip as an exact OID (not a moving symbolic name).
447
+ TARGET_OID="$(git rev-parse "$REMOTE/$BRANCH")"
448
+ HELPER_VERSION="${VAULT_SYNC_HELPER_VERSION:-unknown}"
449
+ RUNTIME_HASH="${VAULT_SYNC_RUNTIME_HASH:-}"
450
+ LOCK_IDENTITY="$LOCK_FILE"
451
+
452
+ if ! vault_sync_op_begin "$WIKI_DIR" "$OP_ID" "$ORIGINAL_BRANCH" "$ORIGINAL_HEAD" "$TARGET_OID" \
453
+ "$LOCK_IDENTITY" "$HELPER_VERSION" "$RUNTIME_HASH"; then
454
+ log "FAIL could not begin operation journal"
455
+ exit 1
456
+ fi
457
+
458
+ if ! drop_or_preserve_untracked_remote_overlaps; then
459
+ vault_sync_op_mark_review_required "$WIKI_DIR" "$OP_ID" "untracked-overlap-failed"
460
+ exit 1
461
+ fi
462
+
463
+ INV="$(mktemp)"
464
+ vault_sync_op_write_inventory "$WIKI_DIR" "$INV"
465
+ vault_sync_op_record_inventory "$WIKI_DIR" "$OP_ID" "$INV"
466
+ rm -f "$INV"
467
+
468
+ OWNED_STASH_OID=""
469
+ PRESERVE_SCOPE="none"
470
+ NEED_STASH=0
471
+ INCLUDE_UNTRACKED=0
472
+ if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then
473
+ NEED_STASH=1
474
+ PRESERVE_SCOPE="tracked"
475
+ fi
476
+ if [ -n "$(git ls-files --others --exclude-standard 2>/dev/null)" ]; then
477
+ # After drop_or_preserve_untracked_remote_overlaps, remaining untracked must be preserved.
478
+ NEED_STASH=1
479
+ INCLUDE_UNTRACKED=1
480
+ if [ "$PRESERVE_SCOPE" = "tracked" ]; then
481
+ PRESERVE_SCOPE="tracked+untracked"
482
+ else
483
+ PRESERVE_SCOPE="untracked"
484
+ fi
485
+ fi
486
+
487
+ if [ "$NEED_STASH" -eq 1 ]; then
488
+ STASH_MSG="vault-sync op=${OP_ID} $(date -u +%Y-%m-%dT%H:%MZ)"
489
+ if OWNED_STASH_OID="$(vault_sync_op_stash_push_owned "$WIKI_DIR" "$STASH_MSG" "$INCLUDE_UNTRACKED")"; then
490
+ vault_sync_op_record_stash "$WIKI_DIR" "$OP_ID" "$OWNED_STASH_OID" "$PRESERVE_SCOPE"
491
+ log "STASH oid=$OWNED_STASH_OID scope=$PRESERVE_SCOPE op=$OP_ID"
492
+ else
493
+ vault_sync_op_mark_review_required "$WIKI_DIR" "$OP_ID" "stash-failed"
494
+ log "FAIL stash before rebase"
495
+ exit 1
496
+ fi
497
+ fi
498
+
499
+ # Test-only hook (unset in production): crash after stash to prove journal retention.
500
+ if [ "${VAULT_SYNC_TEST_EXIT_AFTER_STASH:-0}" = "1" ]; then
501
+ log "TEST exit after stash"
502
+ exit 99
503
+ fi
504
+
505
+ vault_sync_op_set_phase "$WIKI_DIR" "$OP_ID" "rebasing"
506
+
507
+ # Run rebase onto frozen TARGET_OID with auto-resolve for archive conflict storms.
508
+ # --reapply-cherry-picks prevents git from skipping matching patch-ids and
509
+ # dirtying the working tree mid-rebase. Prefer `git rebase $TARGET_OID` over
510
+ # `pull --rebase` so the pin is explicit (fetch already done).
511
+ run_rebase_onto_target "$TARGET_OID"
512
+ REBASE_RC=$?
513
+
514
+ # Defense-in-depth: cap rebase iterations so a silent resolver failure cannot
515
+ # spin the unattended pull forever. A healthy conflict storm is bounded by the
516
+ # number of commits being replayed; 200 is well above any realistic rebase.
517
+ MAX_REBASE_ITERS=200
518
+ REBASE_ITERS=0
519
+
520
+ while [ $REBASE_RC -ne 0 ]; do
521
+ REBASE_ITERS=$((REBASE_ITERS + 1))
522
+ if [ $REBASE_ITERS -gt $MAX_REBASE_ITERS ]; then
523
+ log "FAIL rebase iteration cap ($MAX_REBASE_ITERS) exceeded — aborting to avoid spin"
524
+ vault_sync_op_mark_review_required "$WIKI_DIR" "$OP_ID" "rebase-iteration-cap"
525
+ git rebase --abort 2>/dev/null || true
526
+ exit 1
527
+ fi
528
+
529
+ # Check if we're in a rebase conflict state
530
+ if [ ! -d "$WIKI_DIR/.git/rebase-merge" ]; then
531
+ # Rebase failed before starting. Fall back to a normal merge so the
532
+ # unattended push loop does not wedge on recoverable non-rebase errors.
533
+ log "REBASE failed to start (rc=$REBASE_RC) — falling back to merge"
534
+ git rebase --abort 2>/dev/null || true
535
+ if git merge --no-edit "$TARGET_OID" 2>>"$LOG_FILE"; then
536
+ log "OK fallback merge succeeded"
537
+ REBASE_RC=0
538
+ else
539
+ vault_sync_op_mark_review_required "$WIKI_DIR" "$OP_ID" "fallback-merge-failed"
540
+ log "FAIL fallback merge also failed"
541
+ exit 1
542
+ fi
543
+ continue
544
+ fi
545
+
546
+ # Check if there are actual conflicts
547
+ CONFLICTS=$(git diff --name-only --diff-filter=U 2>/dev/null)
548
+ if [ -z "$CONFLICTS" ]; then
549
+ # No file-level conflicts — maybe the conflict was resolved externally
550
+ # or it's a different kind of failure. Try to continue.
551
+ if ! GIT_EDITOR=true git rebase --continue 2>>"$LOG_FILE"; then
552
+ vault_sync_op_mark_review_required "$WIKI_DIR" "$OP_ID" "rebase-continue-no-conflicts"
553
+ log "FAIL rebase continue (no conflicts but failed)"
554
+ exit 1
555
+ fi
556
+ REBASE_RC=$?
557
+ continue
558
+ fi
559
+
560
+ if try_auto_resolve_derived_conflicts "$OP_ID"; then
561
+ if ! GIT_EDITOR=true git rebase --continue 2>>"$LOG_FILE"; then
562
+ REBASE_RC=$?
563
+ else
564
+ REBASE_RC=0
565
+ fi
566
+ continue
567
+ fi
568
+
569
+ # Composite resolver declined (unknown paths or failure). Manual path:
570
+ # one owned stale-target retry, else handoff — no blanket --ours.
571
+ STOPPED_SHA=$(cat "$WIKI_DIR/.git/rebase-merge/stopped-sha" 2>/dev/null || echo "")
572
+ if [ -z "$STOPPED_SHA" ]; then
573
+ vault_sync_op_mark_review_required "$WIKI_DIR" "$OP_ID" "missing-stopped-sha"
574
+ log "WARN cannot determine stopped-sha — surfacing conflicts"
575
+ exit 1
576
+ fi
577
+
578
+ COMMIT_MSG=$(git log --format=%s -1 "$STOPPED_SHA" 2>/dev/null || echo "")
579
+ handle_manual_conflict "$COMMIT_MSG" "$CONFLICTS"
580
+ continue
581
+ done
582
+
583
+ if [ -n "$OWNED_STASH_OID" ]; then
584
+ if vault_sync_op_stash_apply_owned "$WIKI_DIR" "$OWNED_STASH_OID" 2>>"$LOG_FILE"; then
585
+ if ! vault_sync_op_verify_inventory "$WIKI_DIR" "$OP_ID" "$OWNED_STASH_OID" 2>>"$LOG_FILE"; then
586
+ vault_sync_op_mark_review_required "$WIKI_DIR" "$OP_ID" "inventory-verify-failed"
587
+ log "FAIL inventory verification after stash apply oid=$OWNED_STASH_OID"
588
+ exit 1
589
+ fi
590
+ if vault_sync_op_stash_drop_owned "$WIKI_DIR" "$OWNED_STASH_OID" 2>>"$LOG_FILE"; then
591
+ log "STASH apply+drop ok oid=$OWNED_STASH_OID"
592
+ else
593
+ log "WARN could not drop owned stash oid=$OWNED_STASH_OID"
594
+ fi
595
+ else
596
+ CONFLICTS=$(git diff --name-only --diff-filter=U 2>/dev/null)
597
+ vault_sync_op_set_phase "$WIKI_DIR" "$OP_ID" "restoring"
598
+ if [ -n "$CONFLICTS" ] && try_auto_resolve_derived_conflicts "$OP_ID"; then
599
+ if [ -n "$(git diff --name-only --diff-filter=U 2>/dev/null)" ]; then
600
+ vault_sync_op_mark_review_required "$WIKI_DIR" "$OP_ID" "stash-restore-conflicts"
601
+ log "FAIL stash apply derived auto-resolve left conflicts"
602
+ exit 1
603
+ fi
604
+ # Derived regeneration rewrites conflicted paths; verify presence for those
605
+ # paths and content for everything else.
606
+ SKIP_CONTENT="$(printf '%s\n' $CONFLICTS)"
607
+ if ! vault_sync_op_verify_inventory "$WIKI_DIR" "$OP_ID" "$OWNED_STASH_OID" "$SKIP_CONTENT" 2>>"$LOG_FILE"; then
608
+ vault_sync_op_mark_review_required "$WIKI_DIR" "$OP_ID" "inventory-verify-failed"
609
+ log "FAIL inventory verification after derived auto-resolve oid=$OWNED_STASH_OID"
610
+ exit 1
611
+ fi
612
+
613
+ vault_sync_op_stash_drop_owned "$WIKI_DIR" "$OWNED_STASH_OID" 2>>"$LOG_FILE" || true
614
+ log "STASH apply derived conflicts auto-resolved"
615
+ else
616
+ vault_sync_op_mark_review_required "$WIKI_DIR" "$OP_ID" "semantic-conflict"
617
+ log "FAIL stash apply after rebase oid=$OWNED_STASH_OID"
618
+ exit 1
619
+ fi
620
+ fi
621
+ fi
622
+
623
+ if ! verify_no_tracked_conflict_markers; then
624
+ vault_sync_op_mark_review_required "$WIKI_DIR" "$OP_ID" "conflict-markers-remain"
625
+ exit 1
626
+ fi
627
+
628
+ # Pull helper does not push; close the owned journal after successful convergence.
629
+ vault_sync_op_close_complete "$WIKI_DIR" "$OP_ID"
630
+ log "OK pull completed op=$OP_ID"
631
+ exit 0