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.
- package/dist/chunk-5TBIMLTZ.js +343 -0
- package/dist/chunk-C5OLZRRM.js +357 -0
- package/dist/{chunk-TUFQZ5K4.js → chunk-DR7KFHNH.js} +792 -1983
- package/dist/chunk-IZABIE44.js +647 -0
- package/dist/chunk-S5ABQCXQ.js +580 -0
- package/dist/cli.js +1540 -550
- package/dist/index-projection-ERFX76U5.js +10 -0
- package/dist/managed-write-preflight-SILEUQEV.js +11 -0
- package/dist/skillwiki-mcp.js +4 -1
- package/dist/vault-sync/scripts/lib/conflict-markers.sh +69 -0
- package/dist/vault-sync/scripts/lib/delete-intent.sh +74 -0
- package/dist/vault-sync/scripts/lib/fleet.sh +103 -0
- package/dist/vault-sync/scripts/lib/git-case.sh +71 -0
- package/dist/vault-sync/scripts/lib/git-materialization.sh +264 -0
- package/dist/vault-sync/scripts/lib/git-operation-journal.sh +469 -0
- package/dist/vault-sync/scripts/lib/git-rebase-state.sh +180 -0
- package/dist/vault-sync/scripts/lib/lockfile.sh +70 -0
- package/dist/vault-sync/scripts/lib/managed-write-lock.sh +80 -0
- package/dist/vault-sync/scripts/lib/platform.sh +184 -0
- package/dist/vault-sync/scripts/lib/runtime-manifest.sh +223 -0
- package/dist/vault-sync/scripts/wiki-fetch-notify.sh +207 -0
- package/dist/vault-sync/scripts/wiki-fuse-refresh.sh +405 -0
- package/dist/vault-sync/scripts/wiki-pull-with-auto-resolve.sh +631 -0
- package/dist/vault-sync/scripts/wiki-push.sh +364 -0
- package/dist/vault-sync/scripts/wiki-snapshot.sh +587 -0
- package/package.json +2 -2
- package/skills/.claude-plugin/plugin.json +1 -1
- package/skills/.codex-plugin/plugin.json +1 -1
- package/skills/README.md +13 -0
- package/skills/package.json +1 -1
- package/skills/proj-work/SKILL.md +3 -0
- package/skills/skills/proj-work/SKILL.md +3 -0
- package/skills/skills/using-skillwiki/SKILL.md +13 -0
- package/skills/skills/wiki-crystallize/SKILL.md +3 -0
- package/skills/using-skillwiki/SKILL.md +13 -0
- 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
|