skillwiki 0.9.63 → 0.10.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.
Files changed (36) hide show
  1. package/dist/chunk-C5OLZRRM.js +357 -0
  2. package/dist/chunk-IZABIE44.js +647 -0
  3. package/dist/chunk-R6BKJWVC.js +890 -0
  4. package/dist/chunk-SCGC7YNM.js +213 -0
  5. package/dist/{chunk-TUFQZ5K4.js → chunk-XSTXPA34.js} +834 -1981
  6. package/dist/cli.js +1780 -697
  7. package/dist/index-projection-ERFX76U5.js +10 -0
  8. package/dist/managed-write-preflight-PW4OOOMV.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 +24 -0
  34. package/skills/skills/wiki-crystallize/SKILL.md +3 -0
  35. package/skills/using-skillwiki/SKILL.md +24 -0
  36. package/skills/wiki-crystallize/SKILL.md +3 -0
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ renderRootIndex,
4
+ writeRootIndexProjection
5
+ } from "./chunk-IZABIE44.js";
6
+ import "./chunk-C5OLZRRM.js";
7
+ export {
8
+ renderRootIndex,
9
+ writeRootIndexProjection
10
+ };
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ runManagedWritePreflight,
4
+ runManagedWriteTransaction
5
+ } from "./chunk-SCGC7YNM.js";
6
+ import "./chunk-R6BKJWVC.js";
7
+ import "./chunk-C5OLZRRM.js";
8
+ export {
9
+ runManagedWritePreflight,
10
+ runManagedWriteTransaction
11
+ };
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runSkillwikiMcpStdio
4
- } from "./chunk-TUFQZ5K4.js";
4
+ } from "./chunk-XSTXPA34.js";
5
5
  import "./chunk-7I2TPIV5.js";
6
+ import "./chunk-IZABIE44.js";
7
+ import "./chunk-R6BKJWVC.js";
8
+ import "./chunk-C5OLZRRM.js";
6
9
 
7
10
  // src/mcp-entry.ts
8
11
  runSkillwikiMcpStdio().catch((error) => {
@@ -0,0 +1,69 @@
1
+ #!/bin/sh
2
+ # conflict-markers.sh — Detect complete Git conflict-marker blocks in Markdown.
3
+ #
4
+ # Scans *.md under a vault root, ignoring fenced code blocks. Writes findings
5
+ # as path:line: conflict marker block starts here. No ripgrep dependency.
6
+
7
+ vault_sync_scan_conflict_markers() {
8
+ local root="$1"
9
+ local out_file="$2"
10
+ local rel file
11
+
12
+ : >"$out_file" || return 1
13
+ (
14
+ cd "$root" || exit 2
15
+ find . \
16
+ -path './.git' -prune -o \
17
+ -path './.obsidian' -prune -o \
18
+ -path './.skillwiki' -prune -o \
19
+ -path './.claude' -prune -o \
20
+ -path './.antigravitycli' -prune -o \
21
+ -path './.playwright-cli' -prune -o \
22
+ -type f -name '*.md' -print
23
+ ) | while IFS= read -r file; do
24
+ rel="${file#./}"
25
+ awk -v path="$rel" '
26
+ BEGIN {
27
+ in_fence = 0
28
+ open_line = 0
29
+ sep_line = 0
30
+ }
31
+ /^```/ || /^~~~/ {
32
+ in_fence = !in_fence
33
+ next
34
+ }
35
+ in_fence {
36
+ next
37
+ }
38
+ /^<<<<<<< / {
39
+ open_line = NR
40
+ sep_line = 0
41
+ next
42
+ }
43
+ /^=======$/ {
44
+ if (open_line > 0) {
45
+ sep_line = NR
46
+ }
47
+ next
48
+ }
49
+ /^>>>>>>> / {
50
+ if (open_line > 0 && sep_line > 0) {
51
+ printf "%s:%d: conflict marker block starts here\n", path, open_line
52
+ }
53
+ open_line = 0
54
+ sep_line = 0
55
+ next
56
+ }
57
+ ' "$root/$rel" >>"$out_file"
58
+ done
59
+
60
+ [ ! -s "$out_file" ]
61
+ }
62
+
63
+ vault_sync_log_conflict_marker_findings() {
64
+ local findings_file="$1"
65
+ local log_file="$2"
66
+ if [ -s "$findings_file" ]; then
67
+ cat "$findings_file" >>"$log_file"
68
+ fi
69
+ }
@@ -0,0 +1,74 @@
1
+ #!/bin/bash
2
+ # delete-intent.sh — helpers for vault-delete-intent/v1 tombstones under
3
+ # meta/delete-intents/. Used by wiki-push (S3 prune) and wiki-snapshot
4
+ # (no-resurrect). Prefer reading from a vault worktree directory.
5
+
6
+ # Print active delete-intent paths (one per line) from VAULT/meta/delete-intents.
7
+ # Requires python3 for JSON. Silent empty on missing dir.
8
+ delete_intent_list_active_paths() {
9
+ local vault="${1:-}"
10
+ local dir
11
+ [ -n "$vault" ] || return 0
12
+ dir="$vault/meta/delete-intents"
13
+ [ -d "$dir" ] || return 0
14
+
15
+ python3 - "$dir" <<'PY'
16
+ import json, os, sys
17
+ from datetime import datetime, timezone
18
+
19
+ d = sys.argv[1]
20
+ now = datetime.now(timezone.utc)
21
+ paths = []
22
+ for name in sorted(os.listdir(d)):
23
+ if not name.endswith(".json"):
24
+ continue
25
+ path = os.path.join(d, name)
26
+ try:
27
+ with open(path, "r", encoding="utf-8") as f:
28
+ data = json.load(f)
29
+ except Exception:
30
+ continue
31
+ if data.get("schema") != "vault-delete-intent/v1":
32
+ continue
33
+ p = data.get("path")
34
+ if not p or not data.get("action"):
35
+ continue
36
+ exp = data.get("expires")
37
+ if exp not in (None, ""):
38
+ try:
39
+ # support Z suffix
40
+ exp_s = str(exp).replace("Z", "+00:00")
41
+ exp_dt = datetime.fromisoformat(exp_s)
42
+ if exp_dt.tzinfo is None:
43
+ exp_dt = exp_dt.replace(tzinfo=timezone.utc)
44
+ if exp_dt <= now:
45
+ continue
46
+ except Exception:
47
+ continue
48
+ paths.append(p)
49
+ for p in sorted(set(paths)):
50
+ print(p)
51
+ PY
52
+ }
53
+
54
+ # List active paths by reading JSON blobs from git ref (default origin/main).
55
+ # WORKTREE must be a git checkout; REF defaults to origin/main.
56
+ delete_intent_list_active_paths_from_git() {
57
+ local worktree="${1:-}"
58
+ local ref="${2:-origin/main}"
59
+ [ -n "$worktree" ] || return 0
60
+ [ -d "$worktree/.git" ] || [ -f "$worktree/.git" ] || return 0
61
+
62
+ local tmp
63
+ tmp="$(mktemp -d)" || return 0
64
+ (
65
+ cd "$worktree" || exit 0
66
+ git ls-tree -r --name-only "$ref" -- meta/delete-intents/ 2>/dev/null | while IFS= read -r rel; do
67
+ [ -z "$rel" ] && continue
68
+ mkdir -p "$tmp/$(dirname "$rel")"
69
+ git show "$ref:$rel" > "$tmp/$rel" 2>/dev/null || true
70
+ done
71
+ )
72
+ delete_intent_list_active_paths "$tmp"
73
+ rm -rf "$tmp"
74
+ }
@@ -0,0 +1,103 @@
1
+ #!/bin/sh
2
+ # fleet.sh — Fleet manifest parsing for vault-sync.
3
+ #
4
+ # Reads fleet.yaml from the vault and provides role enforcement.
5
+ # Uses yq if available, awk fallback for the limited schema.
6
+
7
+ # Populate VS_FLEET_* env from YAML.
8
+ # Uses yq if available, awk fallback otherwise.
9
+ fleet_load() {
10
+ _fleet_file=""
11
+ if command -v skillwiki >/dev/null 2>&1; then
12
+ _vault_path=$(skillwiki path 2>/dev/null | python3 -c "import json,sys; print(json.load(sys.stdin)['data']['path'])" 2>/dev/null) || true
13
+ if [ -n "${_vault_path:-}" ]; then
14
+ _fleet_file="$_vault_path/projects/llm-wiki/architecture/fleet.yaml"
15
+ fi
16
+ fi
17
+
18
+ # Fallback: guess from WIKI_DIR
19
+ if [ -z "${_fleet_file:-}" ] || [ ! -f "$_fleet_file" ]; then
20
+ _wiki_dir="${WIKI_DIR:-$HOME/wiki}"
21
+ _fleet_file="$_wiki_dir/projects/llm-wiki/architecture/fleet.yaml"
22
+ fi
23
+
24
+ if [ ! -f "$_fleet_file" ]; then
25
+ VS_FLEET_LOADED=false
26
+ export VS_FLEET_LOADED
27
+ return 0
28
+ fi
29
+
30
+ VS_FLEET_FILE="$_fleet_file"
31
+ VS_FLEET_LOADED=true
32
+ export VS_FLEET_FILE VS_FLEET_LOADED
33
+ }
34
+
35
+ # Echo: hostname of current snapshotter, or "" if none.
36
+ fleet_get_snapshotter() {
37
+ if [ "${VS_FLEET_LOADED:-false}" != "true" ]; then
38
+ fleet_load
39
+ fi
40
+ if [ "${VS_FLEET_LOADED:-false}" != "true" ]; then
41
+ echo ""
42
+ return 0
43
+ fi
44
+
45
+ if command -v yq >/dev/null 2>&1; then
46
+ yq '.hosts | to_entries[] | select(.value.role == "snapshotter") | .key' "$VS_FLEET_FILE" 2>/dev/null
47
+ else
48
+ # awk fallback: find the host with role: snapshotter
49
+ awk '/^ [a-z0-9_-]+:/{host=$1; gsub(/:/,"",host)} /role: snapshotter/{print host; exit}' "$VS_FLEET_FILE" 2>/dev/null
50
+ fi
51
+ }
52
+
53
+ # Exit 0 if host is marked protected: true, 1 otherwise.
54
+ fleet_is_protected() {
55
+ _host="$1"
56
+ if [ "${VS_FLEET_LOADED:-false}" != "true" ]; then
57
+ fleet_load
58
+ fi
59
+ if [ "${VS_FLEET_LOADED:-false}" != "true" ]; then
60
+ return 1
61
+ fi
62
+
63
+ if command -v yq >/dev/null 2>&1; then
64
+ _prot=$(yq ".hosts.$_host.protected // false" "$VS_FLEET_FILE" 2>/dev/null)
65
+ [ "$_prot" = "true" ]
66
+ else
67
+ # awk fallback: find protected: true under the host key
68
+ awk -v h="$_host" '
69
+ $0 ~ "^ " h ":" { in_host=1; next }
70
+ /^ [a-z]/ { in_host=0 }
71
+ in_host && /protected: true/ { found=1; exit }
72
+ END { exit (found ? 0 : 1) }
73
+ ' "$VS_FLEET_FILE" 2>/dev/null
74
+ fi
75
+ }
76
+
77
+ # Exit 0 if install is safe, non-zero with reason if not.
78
+ fleet_validate_install() {
79
+ _host="$1"
80
+ _role="$2"
81
+ _override="${3:-false}"
82
+
83
+ if [ "${VS_FLEET_LOADED:-false}" != "true" ]; then
84
+ fleet_load
85
+ fi
86
+
87
+ # If role is not snapshotter, install is always safe
88
+ if [ "$_role" != "snapshotter" ]; then
89
+ return 0
90
+ fi
91
+
92
+ # Check if a snapshotter already exists
93
+ _current=$(fleet_get_snapshotter)
94
+ if [ -n "$_current" ] && [ "$_current" != "$_host" ]; then
95
+ if [ "$_override" != "true" ]; then
96
+ echo "FATAL: host '$_current' is already the snapshotter. Use --override-snapshotter to force." >&2
97
+ return 1
98
+ fi
99
+ echo "WARNING: overriding snapshotter from '$_current' to '$_host'" >&2
100
+ fi
101
+
102
+ return 0
103
+ }
@@ -0,0 +1,71 @@
1
+ #!/bin/sh
2
+ # git-case.sh — Detect case-only path collisions before unattended git writes.
3
+ #
4
+ # Case-insensitive filesystems cannot represent two paths that differ only by
5
+ # letter case. If Linux commits such a tree, macOS checkouts can become
6
+ # permanently dirty and unable to rebase. These helpers return nonzero and print
7
+ # conflicting pairs when a repo contains such paths.
8
+
9
+ git_case_index_conflicts() {
10
+ git ls-files 2>/dev/null | awk '
11
+ {
12
+ key = tolower($0)
13
+ if (seen[key] && seen[key] != $0) {
14
+ print seen[key] " <-> " $0
15
+ bad = 1
16
+ } else if (!seen[key]) {
17
+ seen[key] = $0
18
+ }
19
+ }
20
+ END { exit bad ? 1 : 0 }
21
+ '
22
+ }
23
+
24
+ git_case_worktree_conflicts() {
25
+ find . -type f \
26
+ ! -path './.git/*' \
27
+ ! -path './.snapshots/*' \
28
+ -print 2>/dev/null |
29
+ sed 's#^\./##' |
30
+ awk '
31
+ {
32
+ key = tolower($0)
33
+ if (seen[key] && seen[key] != $0) {
34
+ print seen[key] " <-> " $0
35
+ bad = 1
36
+ } else if (!seen[key]) {
37
+ seen[key] = $0
38
+ }
39
+ }
40
+ END { exit bad ? 1 : 0 }
41
+ '
42
+ }
43
+
44
+ git_case_conflicts() {
45
+ _git_case_tmp="${TMPDIR:-/tmp}/vault-sync-case-conflicts.$$"
46
+ : >"$_git_case_tmp"
47
+
48
+ git_case_index_conflicts >>"$_git_case_tmp" || true
49
+ git_case_worktree_conflicts >>"$_git_case_tmp" || true
50
+
51
+ if [ -s "$_git_case_tmp" ]; then
52
+ sort -u "$_git_case_tmp"
53
+ rm -f "$_git_case_tmp"
54
+ return 1
55
+ fi
56
+
57
+ rm -f "$_git_case_tmp"
58
+ return 0
59
+ }
60
+
61
+ git_case_assert_clean() {
62
+ _git_case_label="${1:-repository}"
63
+ _git_case_conflicts="$(git_case_conflicts)"
64
+ _git_case_rc=$?
65
+ if [ "$_git_case_rc" -ne 0 ]; then
66
+ printf 'FATAL: case-only path collision in %s\n' "$_git_case_label" >&2
67
+ printf '%s\n' "$_git_case_conflicts" >&2
68
+ return 1
69
+ fi
70
+ return 0
71
+ }
@@ -0,0 +1,264 @@
1
+ #!/bin/bash
2
+ # git-materialization.sh — prove a local commit's content is already present on a target ref.
3
+ #
4
+ # Bash 3.2 compatible. Source from vault-sync scripts.
5
+ #
6
+ # vault_sync_commit_materialized <commit> <target-ref> [repo]
7
+ # Exit 0 only when every changed path is proven present in the target tree:
8
+ # - ordinary add/modify: target blob == commit blob
9
+ # - delete: target path absent
10
+ # - log.md / */log.md: every added ## section body occurs byte-for-byte in target log
11
+ # Fail closed (exit 1) on rename, binary mismatch, missing target, unsupported status,
12
+ # partial log section match, or any unprovable change.
13
+
14
+ vault_sync_is_log_path() {
15
+ case "$1" in
16
+ log.md|*/log.md) return 0 ;;
17
+ *) return 1 ;;
18
+ esac
19
+ }
20
+
21
+ # Return 0 if file contains at least one NUL byte (binary). Do NOT use
22
+ # `grep -q $'\0'` — on macOS BSD grep that matches every non-empty file.
23
+ vault_sync_file_has_nul() {
24
+ local f="$1"
25
+ [ -f "$f" ] || return 1
26
+ # Compare original to a NUL-stripped copy; differ ⇒ had NUL.
27
+ if ! tr -d '\0' <"$f" | cmp -s - "$f"; then
28
+ return 0
29
+ fi
30
+ return 1
31
+ }
32
+
33
+ # Extract Markdown sections starting at ^## from stdin; print each section
34
+ # separated by a form-feed so callers can compare complete bodies.
35
+ vault_sync_extract_h2_sections() {
36
+ awk '
37
+ BEGIN { sec = ""; insec = 0 }
38
+ /^## / {
39
+ if (insec && sec != "") {
40
+ printf "%s\f", sec
41
+ }
42
+ sec = $0 "\n"
43
+ insec = 1
44
+ next
45
+ }
46
+ insec {
47
+ sec = sec $0 "\n"
48
+ }
49
+ END {
50
+ if (insec && sec != "") {
51
+ printf "%s\f", sec
52
+ }
53
+ }
54
+ '
55
+ }
56
+
57
+ # Return 0 if every added ## section in new_file appears byte-for-byte in target_file.
58
+ vault_sync_log_sections_materialized() {
59
+ local new_file="$1"
60
+ local target_file="$2"
61
+ local old_file="${3:-}"
62
+ local new_secs target_text sec old_secs sec_file rc
63
+
64
+ if [ ! -f "$new_file" ] || [ ! -f "$target_file" ]; then
65
+ return 1
66
+ fi
67
+
68
+ new_secs="$(vault_sync_extract_h2_sections < "$new_file")"
69
+ target_text="$(cat "$target_file")"
70
+ old_secs=""
71
+ if [ -n "$old_file" ] && [ -f "$old_file" ]; then
72
+ old_secs="$(vault_sync_extract_h2_sections < "$old_file")"
73
+ fi
74
+
75
+ # Write sections to a temp file and iterate without a pipeline subshell
76
+ # so a missing section can fail the function itself (Bash 3.2-safe).
77
+ sec_file="$(mktemp)" || return 1
78
+ printf '%s' "$new_secs" > "$sec_file"
79
+ rc=0
80
+ while IFS= read -r -d $'\f' sec || [ -n "${sec:-}" ]; do
81
+ [ -n "${sec:-}" ] || continue
82
+ if [ -n "$old_secs" ]; then
83
+ case "$old_secs" in
84
+ *"$sec"*) continue ;;
85
+ esac
86
+ fi
87
+ case "$target_text" in
88
+ *"$sec"*) ;;
89
+ *) rc=1; break ;;
90
+ esac
91
+ done < "$sec_file"
92
+ rm -f "$sec_file"
93
+ return $rc
94
+ }
95
+
96
+ # Prove commit is fully materialized on target-ref.
97
+ vault_sync_commit_materialized() {
98
+ local commit="$1"
99
+ local target_ref="$2"
100
+ local repo="${3:-.}"
101
+ local parent status path tmpdir old_blob new_blob target_blob
102
+ local commit_sha target_sha
103
+ local changed=0
104
+
105
+ if [ -z "$commit" ] || [ -z "$target_ref" ]; then
106
+ return 1
107
+ fi
108
+ if ! git -C "$repo" rev-parse -q --verify "$commit^{commit}" >/dev/null 2>&1; then
109
+ return 1
110
+ fi
111
+ if ! git -C "$repo" rev-parse -q --verify "$target_ref^{commit}" >/dev/null 2>&1; then
112
+ return 1
113
+ fi
114
+
115
+ parent="$(git -C "$repo" rev-parse "${commit}^" 2>/dev/null || true)"
116
+ if [ -z "$parent" ]; then
117
+ return 1
118
+ fi
119
+
120
+ while IFS= read -r line; do
121
+ [ -n "$line" ] || continue
122
+ changed=1
123
+ status="${line%% *}"
124
+ path="${line#* }"
125
+ case "$status" in
126
+ R*|C*)
127
+ return 1
128
+ ;;
129
+ A|M|D|T)
130
+ ;;
131
+ *)
132
+ return 1
133
+ ;;
134
+ esac
135
+
136
+ case "$path" in
137
+ *$'\t'*) return 1 ;;
138
+ esac
139
+
140
+ if vault_sync_is_log_path "$path"; then
141
+ if [ "$status" = "D" ]; then
142
+ if git -C "$repo" cat-file -e "$target_ref:$path" 2>/dev/null; then
143
+ return 1
144
+ fi
145
+ continue
146
+ fi
147
+ tmpdir="$(mktemp -d)" || return 1
148
+ old_blob="$tmpdir/old"
149
+ new_blob="$tmpdir/new"
150
+ target_blob="$tmpdir/target"
151
+ if [ "$status" = "A" ]; then
152
+ : > "$old_blob"
153
+ else
154
+ if ! git -C "$repo" show "$parent:$path" >"$old_blob" 2>/dev/null; then
155
+ rm -rf "$tmpdir"
156
+ return 1
157
+ fi
158
+ fi
159
+ if ! git -C "$repo" show "$commit:$path" >"$new_blob" 2>/dev/null; then
160
+ rm -rf "$tmpdir"
161
+ return 1
162
+ fi
163
+ if ! git -C "$repo" show "$target_ref:$path" >"$target_blob" 2>/dev/null; then
164
+ rm -rf "$tmpdir"
165
+ return 1
166
+ fi
167
+ # Real binary/NUL check (not BSD-grep $'\0', which false-positives on text).
168
+ if vault_sync_file_has_nul "$new_blob" || vault_sync_file_has_nul "$target_blob"; then
169
+ rm -rf "$tmpdir"
170
+ return 1
171
+ fi
172
+ if ! vault_sync_log_sections_materialized "$new_blob" "$target_blob" "$old_blob"; then
173
+ rm -rf "$tmpdir"
174
+ return 1
175
+ fi
176
+ rm -rf "$tmpdir"
177
+ continue
178
+ fi
179
+
180
+ case "$status" in
181
+ D)
182
+ if git -C "$repo" cat-file -e "$target_ref:$path" 2>/dev/null; then
183
+ return 1
184
+ fi
185
+ ;;
186
+ A|M|T)
187
+ if ! git -C "$repo" cat-file -e "$commit:$path" 2>/dev/null; then
188
+ return 1
189
+ fi
190
+ if ! git -C "$repo" cat-file -e "$target_ref:$path" 2>/dev/null; then
191
+ return 1
192
+ fi
193
+ commit_sha="$(git -C "$repo" rev-parse "$commit:$path" 2>/dev/null)" || return 1
194
+ target_sha="$(git -C "$repo" rev-parse "$target_ref:$path" 2>/dev/null)" || return 1
195
+ if [ "$commit_sha" != "$target_sha" ]; then
196
+ return 1
197
+ fi
198
+ ;;
199
+ esac
200
+ done < <(git -C "$repo" diff-tree --no-commit-id --name-status -r "$parent" "$commit" 2>/dev/null)
201
+
202
+ # Empty commit (no path changes): nothing to replay, treat as materialized.
203
+ return 0
204
+ }
205
+
206
+ vault_sync_list_materialized_commits() {
207
+ local base_ref="$1"
208
+ local target_ref="$2"
209
+ local out_file="$3"
210
+ local repo="${4:-.}"
211
+ local sha
212
+
213
+ : > "$out_file"
214
+ while IFS= read -r sha; do
215
+ [ -n "$sha" ] || continue
216
+ if vault_sync_commit_materialized "$sha" "$target_ref" "$repo"; then
217
+ # Store full SHA for robust matching in the sequence editor.
218
+ git -C "$repo" rev-parse "$sha" >> "$out_file" 2>/dev/null || printf '%s\n' "$sha" >> "$out_file"
219
+ fi
220
+ done < <(git -C "$repo" rev-list --reverse "${base_ref}..HEAD" 2>/dev/null)
221
+ return 0
222
+ }
223
+
224
+ # Drop matching pick lines entirely (portable; avoids relying on `drop` verb).
225
+ # Usage: vault_sync_sequence_editor_drop <drop-list-file> <todo-file>
226
+ vault_sync_sequence_editor_drop() {
227
+ local drop_list="$1"
228
+ local todo_file="$2"
229
+ local tmp line rest sha full
230
+
231
+ tmp="$(mktemp)" || return 1
232
+ while IFS= read -r line || [ -n "$line" ]; do
233
+ case "$line" in
234
+ "pick "*|"p "*)
235
+ rest="${line#* }"
236
+ sha="${rest%% *}"
237
+ full=""
238
+ # Match if any full SHA in drop list starts with the todo short/long sha,
239
+ # or the todo sha starts with a drop-list entry.
240
+ if [ -s "$drop_list" ]; then
241
+ while IFS= read -r full || [ -n "$full" ]; do
242
+ [ -n "$full" ] || continue
243
+ case "$full" in
244
+ "$sha"*)
245
+ # omit this pick line (drop)
246
+ continue 2
247
+ ;;
248
+ esac
249
+ case "$sha" in
250
+ "$full"*)
251
+ continue 2
252
+ ;;
253
+ esac
254
+ done < "$drop_list"
255
+ fi
256
+ printf '%s\n' "$line" >> "$tmp"
257
+ ;;
258
+ *)
259
+ printf '%s\n' "$line" >> "$tmp"
260
+ ;;
261
+ esac
262
+ done < "$todo_file"
263
+ mv "$tmp" "$todo_file"
264
+ }