skillwiki 0.10.11 → 0.10.13
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-TYN2IHBY.js → chunk-E3PMAHS3.js} +12 -11
- package/dist/{chunk-3ZVZ2YEE.js → chunk-IIUMTKKA.js} +5 -2
- package/dist/{chunk-U34B2XQJ.js → chunk-NMUYMNNB.js} +2 -0
- package/dist/{chunk-P2FCRGKU.js → chunk-X2CPXF4T.js} +6 -2
- package/dist/cli.js +465 -165
- package/dist/{index-projection-VNJ3TLEK.js → index-projection-MHXG7QO2.js} +5 -1
- package/dist/{managed-write-preflight-7TWSEZJZ.js → managed-write-preflight-4SWWFT75.js} +2 -2
- package/dist/skillwiki-mcp.js +3 -3
- package/dist/vault-sync/scripts/lib/delete-intent.sh +27 -0
- package/dist/vault-sync/scripts/lib/git-operation-journal.sh +106 -13
- package/dist/vault-sync/scripts/lib/managed-write-lock.sh +10 -7
- package/dist/vault-sync/scripts/wiki-snapshot.sh +246 -43
- package/package.json +1 -1
- package/skills/.claude-plugin/plugin.json +1 -1
- package/skills/.codex-plugin/plugin.json +1 -1
- package/skills/package.json +1 -1
- package/skills/skills/using-skillwiki/SKILL.md +1 -1
- package/skills/using-skillwiki/SKILL.md +1 -1
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
UNMANAGED_END,
|
|
4
|
+
UNMANAGED_START,
|
|
3
5
|
renderRootIndex,
|
|
4
6
|
writeRootIndexProjection
|
|
5
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-NMUYMNNB.js";
|
|
6
8
|
import "./chunk-C5OLZRRM.js";
|
|
7
9
|
export {
|
|
10
|
+
UNMANAGED_END,
|
|
11
|
+
UNMANAGED_START,
|
|
8
12
|
renderRootIndex,
|
|
9
13
|
writeRootIndexProjection
|
|
10
14
|
};
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import {
|
|
3
3
|
runManagedWritePreflight,
|
|
4
4
|
runManagedWriteTransaction
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-IIUMTKKA.js";
|
|
6
|
+
import "./chunk-E3PMAHS3.js";
|
|
7
7
|
import "./chunk-C5OLZRRM.js";
|
|
8
8
|
export {
|
|
9
9
|
runManagedWritePreflight,
|
package/dist/skillwiki-mcp.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
runSkillwikiMcpStdio
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-X2CPXF4T.js";
|
|
5
5
|
import "./chunk-7I2TPIV5.js";
|
|
6
|
-
import "./chunk-
|
|
7
|
-
import "./chunk-
|
|
6
|
+
import "./chunk-NMUYMNNB.js";
|
|
7
|
+
import "./chunk-E3PMAHS3.js";
|
|
8
8
|
import "./chunk-C5OLZRRM.js";
|
|
9
9
|
|
|
10
10
|
// src/mcp-entry.ts
|
|
@@ -72,3 +72,30 @@ delete_intent_list_active_paths_from_git() {
|
|
|
72
72
|
delete_intent_list_active_paths "$tmp"
|
|
73
73
|
rm -rf "$tmp"
|
|
74
74
|
}
|
|
75
|
+
|
|
76
|
+
# Print the sorted unique exact intersection between active tombstone paths and
|
|
77
|
+
# a successful remote inventory. Both inputs are newline-delimited
|
|
78
|
+
# vault-relative path files. This helper is pure: no network, logging, or
|
|
79
|
+
# mutation outside its temporary sort files.
|
|
80
|
+
delete_intent_plan_remote_paths() {
|
|
81
|
+
local active_paths_file="${1:-}"
|
|
82
|
+
local remote_paths_file="${2:-}"
|
|
83
|
+
[ -f "$active_paths_file" ] || return 1
|
|
84
|
+
[ -f "$remote_paths_file" ] || return 1
|
|
85
|
+
|
|
86
|
+
local tmp_dir active_sorted remote_sorted rc
|
|
87
|
+
tmp_dir="$(mktemp -d)" || return 1
|
|
88
|
+
active_sorted="$tmp_dir/active.paths"
|
|
89
|
+
remote_sorted="$tmp_dir/remote.paths"
|
|
90
|
+
|
|
91
|
+
if ! LC_ALL=C sort -u "$active_paths_file" > "$active_sorted" \
|
|
92
|
+
|| ! LC_ALL=C sort -u "$remote_paths_file" > "$remote_sorted"; then
|
|
93
|
+
rm -rf "$tmp_dir"
|
|
94
|
+
return 1
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
LC_ALL=C comm -12 "$active_sorted" "$remote_sorted"
|
|
98
|
+
rc=$?
|
|
99
|
+
rm -rf "$tmp_dir"
|
|
100
|
+
return "$rc"
|
|
101
|
+
}
|
|
@@ -38,24 +38,40 @@ vault_sync_op_set_field() {
|
|
|
38
38
|
vault_sync_op_set_fields() {
|
|
39
39
|
local repo="$1" op_id="$2"
|
|
40
40
|
shift 2
|
|
41
|
-
local file tmp
|
|
41
|
+
local file tmp updates source
|
|
42
42
|
file="$(vault_sync_op_journal_path "$repo" "$op_id")" || return 1
|
|
43
43
|
tmp="$(mktemp)" || return 1
|
|
44
|
-
|
|
45
|
-
cp "$file" "$tmp" || { rm -f "$tmp"; return 1; }
|
|
46
|
-
else
|
|
47
|
-
: >"$tmp"
|
|
48
|
-
fi
|
|
44
|
+
updates="$(mktemp)" || { rm -f "$tmp"; return 1; }
|
|
49
45
|
while [ "$#" -ge 2 ]; do
|
|
50
|
-
|
|
51
|
-
BEGIN{found=0}
|
|
52
|
-
$1==k {print k"="v; found=1; next}
|
|
53
|
-
{print}
|
|
54
|
-
END{if(!found) print k"="v}
|
|
55
|
-
' "$tmp" >"${tmp}.new" || { rm -f "$tmp" "${tmp}.new"; return 1; }
|
|
56
|
-
mv "${tmp}.new" "$tmp"
|
|
46
|
+
printf '%s=%s\n' "$1" "$2" >>"$updates" || { rm -f "$tmp" "$updates"; return 1; }
|
|
57
47
|
shift 2
|
|
58
48
|
done
|
|
49
|
+
source="$file"
|
|
50
|
+
[ -f "$source" ] || source=/dev/null
|
|
51
|
+
awk -F= '
|
|
52
|
+
FNR==NR {
|
|
53
|
+
key=$1
|
|
54
|
+
if (!(key in replacement)) order[++count]=key
|
|
55
|
+
replacement[key]=substr($0, index($0,"=")+1)
|
|
56
|
+
next
|
|
57
|
+
}
|
|
58
|
+
{
|
|
59
|
+
key=$1
|
|
60
|
+
if (key in replacement) {
|
|
61
|
+
print key"="replacement[key]
|
|
62
|
+
seen[key]=1
|
|
63
|
+
next
|
|
64
|
+
}
|
|
65
|
+
print
|
|
66
|
+
}
|
|
67
|
+
END {
|
|
68
|
+
for (i=1; i<=count; i++) {
|
|
69
|
+
key=order[i]
|
|
70
|
+
if (!(key in seen)) print key"="replacement[key]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
' "$updates" "$source" >"$tmp" || { rm -f "$tmp" "$updates"; return 1; }
|
|
74
|
+
rm -f "$updates"
|
|
59
75
|
mv "$tmp" "$file"
|
|
60
76
|
}
|
|
61
77
|
|
|
@@ -160,6 +176,83 @@ vault_sync_op_find_review_required() {
|
|
|
160
176
|
return 1
|
|
161
177
|
}
|
|
162
178
|
|
|
179
|
+
# Mark one obsolete review-required handoff complete while preserving its audit
|
|
180
|
+
# trail. Caller must establish that no live owner is mutating the journal.
|
|
181
|
+
vault_sync_op_mark_superseded_stale_review_required() {
|
|
182
|
+
local repo="$1" op_id="$2" by="${3:-vault-sync-managed-preflight}"
|
|
183
|
+
local reason="$4" prior_reason="$5" now
|
|
184
|
+
if [ -z "$prior_reason" ] && [ -n "$reason" ] && [ "$reason" != "superseded-stale-review-required" ]; then
|
|
185
|
+
prior_reason="$reason"
|
|
186
|
+
fi
|
|
187
|
+
now="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date +%Y-%m-%dT%H:%M:%S)"
|
|
188
|
+
vault_sync_op_set_fields "$repo" "$op_id" \
|
|
189
|
+
"phase" "complete" \
|
|
190
|
+
"handoff" "1" \
|
|
191
|
+
"reason" "superseded-stale-review-required" \
|
|
192
|
+
"prior_reason" "$prior_reason" \
|
|
193
|
+
"superseded_at" "$now" \
|
|
194
|
+
"cleared_by" "$by" \
|
|
195
|
+
"cleared_reason" "operator-or-preflight $now"
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# Auto-supersede review-required handoffs whose target is already an ancestor
|
|
199
|
+
# of HEAD. Unrelated dirty WIP is preserved; active sequencers and unmerged
|
|
200
|
+
# paths fail closed. Returns nonzero while any applicable handoff remains open.
|
|
201
|
+
vault_sync_op_supersede_stale_review_required() {
|
|
202
|
+
local repo="$1" by="${2:-vault-sync-managed-preflight}"
|
|
203
|
+
local current_git_dir git_dir unmerged jdir jf op_id phase handoff journal_git_dir target unresolved
|
|
204
|
+
local reason prior_reason key value
|
|
205
|
+
|
|
206
|
+
current_git_dir="$(git -C "$repo" rev-parse --absolute-git-dir 2>/dev/null)" || return 1
|
|
207
|
+
git_dir="$current_git_dir"
|
|
208
|
+
if [ -d "$git_dir/rebase-merge" ] || [ -d "$git_dir/rebase-apply" ] \
|
|
209
|
+
|| [ -f "$git_dir/MERGE_HEAD" ] || [ -f "$git_dir/CHERRY_PICK_HEAD" ] \
|
|
210
|
+
|| [ -f "$git_dir/REVERT_HEAD" ]; then
|
|
211
|
+
return 1
|
|
212
|
+
fi
|
|
213
|
+
unmerged="$(git -C "$repo" ls-files -u 2>/dev/null | head -1 || true)"
|
|
214
|
+
[ -z "$unmerged" ] || return 1
|
|
215
|
+
|
|
216
|
+
jdir="$(vault_sync_op_journal_dir "$repo")" || return 1
|
|
217
|
+
[ -d "$jdir" ] || return 0
|
|
218
|
+
unresolved=""
|
|
219
|
+
for jf in "$jdir"/*.env; do
|
|
220
|
+
[ -f "$jf" ] || continue
|
|
221
|
+
op_id="$(basename "$jf" .env)"
|
|
222
|
+
phase=""
|
|
223
|
+
handoff=""
|
|
224
|
+
journal_git_dir=""
|
|
225
|
+
target=""
|
|
226
|
+
reason=""
|
|
227
|
+
prior_reason=""
|
|
228
|
+
while IFS='=' read -r key value; do
|
|
229
|
+
case "$key" in
|
|
230
|
+
phase) phase="$value" ;;
|
|
231
|
+
handoff) handoff="$value" ;;
|
|
232
|
+
worktree_git_dir) journal_git_dir="$value" ;;
|
|
233
|
+
target_oid) target="$value" ;;
|
|
234
|
+
reason) reason="$value" ;;
|
|
235
|
+
prior_reason) prior_reason="$value" ;;
|
|
236
|
+
esac
|
|
237
|
+
done <"$jf"
|
|
238
|
+
[ "$phase" = "review-required" ] && [ "$handoff" = "1" ] || continue
|
|
239
|
+
if [ -n "$journal_git_dir" ] && [ "$journal_git_dir" != "$current_git_dir" ]; then
|
|
240
|
+
continue
|
|
241
|
+
fi
|
|
242
|
+
if [ -z "$target" ]; then
|
|
243
|
+
[ -n "$unresolved" ] || unresolved="$op_id"
|
|
244
|
+
continue
|
|
245
|
+
fi
|
|
246
|
+
if git -C "$repo" merge-base --is-ancestor "$target" HEAD 2>/dev/null; then
|
|
247
|
+
vault_sync_op_mark_superseded_stale_review_required \
|
|
248
|
+
"$repo" "$op_id" "$by" "$reason" "$prior_reason" || return 1
|
|
249
|
+
else
|
|
250
|
+
[ -n "$unresolved" ] || unresolved="$op_id"
|
|
251
|
+
fi
|
|
252
|
+
done
|
|
253
|
+
[ -z "$unresolved" ]
|
|
254
|
+
}
|
|
255
|
+
|
|
163
256
|
vault_sync_op_preflight_blocker() {
|
|
164
257
|
local repo="$1" unmerged op_id git_dir
|
|
165
258
|
unmerged="$(git -C "$repo" diff --name-only --diff-filter=U 2>/dev/null || true)"
|
|
@@ -62,7 +62,7 @@ vault_sync_managed_lock_safe_to_reclaim() {
|
|
|
62
62
|
|
|
63
63
|
# Never reclaim during an active/leftover sequencer.
|
|
64
64
|
if [ -d "$git_dir/rebase-merge" ] || [ -d "$git_dir/rebase-apply" ] \
|
|
65
|
-
|| [ -
|
|
65
|
+
|| [ -f "$git_dir/MERGE_HEAD" ] || [ -f "$git_dir/CHERRY_PICK_HEAD" ] \
|
|
66
66
|
|| [ -f "$git_dir/REVERT_HEAD" ]; then
|
|
67
67
|
return 1
|
|
68
68
|
fi
|
|
@@ -72,13 +72,16 @@ vault_sync_managed_lock_safe_to_reclaim() {
|
|
|
72
72
|
return 1
|
|
73
73
|
fi
|
|
74
74
|
|
|
75
|
-
# When journal helpers are loaded,
|
|
76
|
-
#
|
|
77
|
-
|
|
75
|
+
# When journal helpers are loaded, a dead owner may close handoffs whose
|
|
76
|
+
# target is already in HEAD, then continue reclaim in this same acquire.
|
|
77
|
+
# Live owners never reach this function; active sequencers/unmerged paths
|
|
78
|
+
# already failed closed above. Any unresolved handoff still blocks reclaim.
|
|
79
|
+
if command -v vault_sync_op_supersede_stale_review_required >/dev/null 2>&1; then
|
|
80
|
+
vault_sync_op_supersede_stale_review_required \
|
|
81
|
+
"$repo" "vault-sync-managed-lock-reclaim" 2>/dev/null || return 1
|
|
82
|
+
elif command -v vault_sync_op_find_review_required >/dev/null 2>&1; then
|
|
78
83
|
review_op="$(vault_sync_op_find_review_required "$repo" 2>/dev/null || true)"
|
|
79
|
-
|
|
80
|
-
return 1
|
|
81
|
-
fi
|
|
84
|
+
[ -z "$review_op" ] || return 1
|
|
82
85
|
fi
|
|
83
86
|
|
|
84
87
|
return 0
|
|
@@ -78,9 +78,11 @@ LOG_FILE="${WIKI_SNAPSHOT_LOG:-$DEFAULT_LOG_DIR/wiki-snapshot.log}"
|
|
|
78
78
|
CLOUD_REMOTE="${CLOUD_REMOTE:-cloud:cloud/wiki}"
|
|
79
79
|
REPAIR_SCRIPT="${WIKI_GIT_REPAIR_SCRIPT:-$SCRIPT_DIR/wiki-git-repair-v3.sh}"
|
|
80
80
|
MAX_S3_ONLY_NOTES="${WIKI_SNAPSHOT_MAX_S3_ONLY_NOTES:-200}"
|
|
81
|
+
MAX_TOMBSTONE_PRUNES="${WIKI_SNAPSHOT_MAX_TOMBSTONE_PRUNES:-10}"
|
|
81
82
|
DATE=$(date +%Y%m%d_%H%M%S)
|
|
82
83
|
RCLONE_LOG="/tmp/rclone-${DATE}.log"
|
|
83
84
|
SNAPSHOT_DIRECT_S3_NOT_GIT_COUNT=0
|
|
85
|
+
SNAPSHOT_REMOTE_INVENTORY_READY=0
|
|
84
86
|
|
|
85
87
|
fetch_origin_main_ref() {
|
|
86
88
|
# The snapshot guards read origin/main directly. Use an explicit destination
|
|
@@ -88,25 +90,63 @@ fetch_origin_main_ref() {
|
|
|
88
90
|
git fetch --quiet origin +refs/heads/main:refs/remotes/origin/main
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
snapshot_refresh_origin_main() {
|
|
94
|
+
(
|
|
95
|
+
cd "$SNAPSHOT_WORKTREE" || exit 1
|
|
96
|
+
fetch_origin_main_ref
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
snapshot_load_active_delete_intent_paths() {
|
|
101
|
+
local output_file="${1:-}"
|
|
102
|
+
[ -n "$output_file" ] || return 1
|
|
103
|
+
: > "$output_file" || return 1
|
|
104
|
+
|
|
105
|
+
if command -v delete_intent_list_active_paths_from_git >/dev/null 2>&1 \
|
|
106
|
+
&& git -C "$SNAPSHOT_WORKTREE" rev-parse --verify --quiet origin/main >/dev/null 2>&1; then
|
|
107
|
+
delete_intent_list_active_paths_from_git "$SNAPSHOT_WORKTREE" origin/main > "$output_file"
|
|
108
|
+
return $?
|
|
109
|
+
fi
|
|
110
|
+
if command -v delete_intent_list_active_paths >/dev/null 2>&1; then
|
|
111
|
+
delete_intent_list_active_paths "$SNAPSHOT_WORKTREE" > "$output_file"
|
|
112
|
+
return $?
|
|
113
|
+
fi
|
|
114
|
+
return 1
|
|
115
|
+
}
|
|
116
|
+
|
|
91
117
|
snapshot_direct_s3_preflight() {
|
|
118
|
+
local inventory_output="${1:-}"
|
|
119
|
+
local active_paths_file="${2:-}"
|
|
92
120
|
local tmp_dir
|
|
93
|
-
tmp_dir="$(mktemp -d)"
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
121
|
+
tmp_dir="$(mktemp -d)" || {
|
|
122
|
+
echo "[wiki-snapshot] WARN: direct-S3 preflight could not create temporary state"
|
|
123
|
+
SNAPSHOT_REMOTE_INVENTORY_READY=0
|
|
124
|
+
return 0
|
|
125
|
+
}
|
|
126
|
+
local direct_paths
|
|
127
|
+
if [ -n "$inventory_output" ]; then
|
|
128
|
+
direct_paths="$inventory_output"
|
|
129
|
+
else
|
|
130
|
+
direct_paths="$tmp_dir/direct-s3.paths"
|
|
131
|
+
fi
|
|
97
132
|
local direct_notes="$tmp_dir/direct-s3-notes.paths"
|
|
98
133
|
local git_paths="$tmp_dir/git.paths"
|
|
134
|
+
local direct_not_git_candidates="$tmp_dir/direct-s3-not-git-candidates.paths"
|
|
99
135
|
local direct_not_git="$tmp_dir/direct-s3-not-git.paths"
|
|
136
|
+
local active_sorted="$tmp_dir/active.paths"
|
|
137
|
+
|
|
138
|
+
SNAPSHOT_REMOTE_INVENTORY_READY=0
|
|
100
139
|
|
|
101
|
-
if ! rclone lsf "$CLOUD_REMOTE" --recursive --files-only 2>/dev/null | LC_ALL=C sort > "$direct_paths"; then
|
|
140
|
+
if ! rclone lsf "$CLOUD_REMOTE" --recursive --files-only 2>/dev/null | LC_ALL=C sort -u > "$direct_paths"; then
|
|
102
141
|
echo "[wiki-snapshot] WARN: direct-S3 preflight could not list $CLOUD_REMOTE"
|
|
142
|
+
rm -rf "$tmp_dir"
|
|
103
143
|
return 0
|
|
104
144
|
fi
|
|
145
|
+
SNAPSHOT_REMOTE_INVENTORY_READY=1
|
|
105
146
|
|
|
106
147
|
grep -vE '^(\.skillwiki/|\.claude/|\.obsidian/|\.antigravitycli/|\.playwright-cli/|raw/\._\.DS_Store$|\._\.DS_Store$)' "$direct_paths" | LC_ALL=C sort -u > "$direct_notes" || true
|
|
107
148
|
(
|
|
108
149
|
cd "$SNAPSHOT_WORKTREE" || exit 1
|
|
109
|
-
fetch_origin_main_ref 2>/dev/null || true
|
|
110
150
|
if git rev-parse --verify --quiet origin/main >/dev/null 2>&1; then
|
|
111
151
|
git -c core.quotePath=false ls-tree -r --name-only origin/main
|
|
112
152
|
else
|
|
@@ -114,23 +154,48 @@ snapshot_direct_s3_preflight() {
|
|
|
114
154
|
fi | LC_ALL=C sort -u
|
|
115
155
|
) > "$git_paths" 2>/dev/null || {
|
|
116
156
|
echo "[wiki-snapshot] WARN: direct-S3 preflight could not list tracked files in snapshot worktree $SNAPSHOT_WORKTREE"
|
|
157
|
+
rm -rf "$tmp_dir"
|
|
117
158
|
return 0
|
|
118
159
|
}
|
|
119
160
|
|
|
120
|
-
LC_ALL=C comm -23 "$direct_notes" "$git_paths" > "$
|
|
161
|
+
LC_ALL=C comm -23 "$direct_notes" "$git_paths" > "$direct_not_git_candidates"
|
|
162
|
+
if [ -n "$active_paths_file" ] && [ -f "$active_paths_file" ]; then
|
|
163
|
+
LC_ALL=C sort -u "$active_paths_file" > "$active_sorted"
|
|
164
|
+
LC_ALL=C comm -23 "$direct_not_git_candidates" "$active_sorted" > "$direct_not_git"
|
|
165
|
+
else
|
|
166
|
+
LC_ALL=C sort -u "$direct_not_git_candidates" > "$direct_not_git"
|
|
167
|
+
fi
|
|
121
168
|
SNAPSHOT_DIRECT_S3_NOT_GIT_COUNT="$(wc -l < "$direct_not_git" | tr -d ' ')"
|
|
122
169
|
if [ "$SNAPSHOT_DIRECT_S3_NOT_GIT_COUNT" != "0" ]; then
|
|
123
170
|
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
171
|
sed -n '1,20p' "$direct_not_git" | sed 's/^/[wiki-snapshot] WARN: direct-S3-not-git: /'
|
|
172
|
+
rm -rf "$tmp_dir"
|
|
125
173
|
return 2
|
|
126
174
|
fi
|
|
127
175
|
|
|
176
|
+
rm -rf "$tmp_dir"
|
|
128
177
|
return 0
|
|
129
178
|
}
|
|
130
179
|
|
|
131
180
|
handle_direct_s3_preflight_before_sync() {
|
|
132
|
-
|
|
181
|
+
local tmp_dir active_paths
|
|
182
|
+
tmp_dir="$(mktemp -d)" || {
|
|
183
|
+
log "ERROR: could not create direct-S3 preflight state"
|
|
184
|
+
return 1
|
|
185
|
+
}
|
|
186
|
+
active_paths="$tmp_dir/active.paths"
|
|
187
|
+
if ! snapshot_refresh_origin_main 2>/dev/null; then
|
|
188
|
+
log "WARNING: could not refresh origin/main before direct-S3 preflight; using the existing ref"
|
|
189
|
+
fi
|
|
190
|
+
if ! snapshot_load_active_delete_intent_paths "$active_paths"; then
|
|
191
|
+
rm -rf "$tmp_dir"
|
|
192
|
+
log "ERROR: active delete-intent inventory unavailable before direct-S3 preflight"
|
|
193
|
+
return 1
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
snapshot_direct_s3_preflight "" "$active_paths"
|
|
133
197
|
local rc=$?
|
|
198
|
+
rm -rf "$tmp_dir"
|
|
134
199
|
if [ "$rc" -eq 0 ]; then
|
|
135
200
|
return 0
|
|
136
201
|
fi
|
|
@@ -197,7 +262,34 @@ if [ "$DRY_RUN" = true ]; then
|
|
|
197
262
|
echo " CLOUD_REMOTE = $CLOUD_REMOTE"
|
|
198
263
|
echo " REPAIR_SCRIPT = $REPAIR_SCRIPT"
|
|
199
264
|
echo "[wiki-snapshot] DRY RUN: --max-delete guard verified (present in $0)"
|
|
200
|
-
|
|
265
|
+
dry_run_tmp="$(mktemp -d)" || exit 1
|
|
266
|
+
dry_run_active="$dry_run_tmp/active.paths"
|
|
267
|
+
dry_run_remote="$dry_run_tmp/remote.paths"
|
|
268
|
+
dry_run_plan="$dry_run_tmp/remote-present.paths"
|
|
269
|
+
case "$MAX_TOMBSTONE_PRUNES" in
|
|
270
|
+
''|*[!0-9]*)
|
|
271
|
+
echo "[wiki-snapshot] ERROR: invalid WIKI_SNAPSHOT_MAX_TOMBSTONE_PRUNES=$MAX_TOMBSTONE_PRUNES; expected a non-negative integer" >&2
|
|
272
|
+
rm -rf "$dry_run_tmp"
|
|
273
|
+
exit 1
|
|
274
|
+
;;
|
|
275
|
+
esac
|
|
276
|
+
snapshot_refresh_origin_main 2>/dev/null || true
|
|
277
|
+
snapshot_load_active_delete_intent_paths "$dry_run_active" || : > "$dry_run_active"
|
|
278
|
+
snapshot_direct_s3_preflight "$dry_run_remote" "$dry_run_active" || true
|
|
279
|
+
if [ "$SNAPSHOT_REMOTE_INVENTORY_READY" = "1" ] \
|
|
280
|
+
&& delete_intent_plan_remote_paths "$dry_run_active" "$dry_run_remote" > "$dry_run_plan"; then
|
|
281
|
+
dry_run_active_count="$(wc -l < "$dry_run_active" | tr -d ' ')"
|
|
282
|
+
dry_run_remote_count="$(wc -l < "$dry_run_plan" | tr -d ' ')"
|
|
283
|
+
dry_run_absent_count=$((dry_run_active_count - dry_run_remote_count))
|
|
284
|
+
dry_run_deferred_count=0
|
|
285
|
+
if [ "$dry_run_remote_count" -gt "$MAX_TOMBSTONE_PRUNES" ] 2>/dev/null; then
|
|
286
|
+
dry_run_deferred_count=$((dry_run_remote_count - MAX_TOMBSTONE_PRUNES))
|
|
287
|
+
fi
|
|
288
|
+
echo "[wiki-snapshot] DRY RUN: delete-intent active=$dry_run_active_count remote_present=$dry_run_remote_count already_absent=$dry_run_absent_count deferred=$dry_run_deferred_count"
|
|
289
|
+
else
|
|
290
|
+
echo "[wiki-snapshot] DRY RUN: delete-intent remote inventory unavailable; optional pruning would be skipped"
|
|
291
|
+
fi
|
|
292
|
+
rm -rf "$dry_run_tmp"
|
|
201
293
|
echo "[wiki-snapshot] DRY RUN: would acquire $LOCK_FILE, rclone sync, git commit, push."
|
|
202
294
|
echo "[wiki-snapshot] DRY RUN: Complete. No changes made."
|
|
203
295
|
exit 0
|
|
@@ -217,6 +309,16 @@ log() {
|
|
|
217
309
|
echo "$(date '+%Y-%m-%d %H:%M:%S') $1" | tee -a "$LOG_FILE"
|
|
218
310
|
}
|
|
219
311
|
|
|
312
|
+
validate_tombstone_prune_cap() {
|
|
313
|
+
case "$MAX_TOMBSTONE_PRUNES" in
|
|
314
|
+
''|*[!0-9]*)
|
|
315
|
+
log "ERROR: invalid WIKI_SNAPSHOT_MAX_TOMBSTONE_PRUNES=$MAX_TOMBSTONE_PRUNES; expected a non-negative integer"
|
|
316
|
+
return 1
|
|
317
|
+
;;
|
|
318
|
+
esac
|
|
319
|
+
return 0
|
|
320
|
+
}
|
|
321
|
+
|
|
220
322
|
raw_dedup_guard() {
|
|
221
323
|
if [ "${WIKI_SNAPSHOT_RAW_DEDUP_GUARD:-1}" = "0" ]; then
|
|
222
324
|
log "raw_dedup guard skipped by WIKI_SNAPSHOT_RAW_DEDUP_GUARD=0"
|
|
@@ -293,6 +395,10 @@ refresh_git_baseline() {
|
|
|
293
395
|
|
|
294
396
|
log "=== Wiki Snapshot: $DATE ==="
|
|
295
397
|
|
|
398
|
+
if ! validate_tombstone_prune_cap; then
|
|
399
|
+
exit 1
|
|
400
|
+
fi
|
|
401
|
+
|
|
296
402
|
# Check disk space (need at least 100MB free)
|
|
297
403
|
AVAILABLE_KB=$(df -k "$SNAPSHOT_WORKTREE" | awk 'NR==2 {print $4}')
|
|
298
404
|
if [ "$AVAILABLE_KB" -lt 102400 ]; then
|
|
@@ -392,61 +498,156 @@ fi
|
|
|
392
498
|
# --- Delete-intent no-resurrect ---
|
|
393
499
|
# Git is SSOT for intentional absences. After S3→worktree sync, strip any path
|
|
394
500
|
# that has an active tombstone on origin/main and optionally prune S3.
|
|
501
|
+
snapshot_remote_path_exists_exact() {
|
|
502
|
+
local rel_path="${1:-}"
|
|
503
|
+
local parent_path base_name remote_parent output_file rc
|
|
504
|
+
[ -n "$rel_path" ] || return 2
|
|
505
|
+
|
|
506
|
+
base_name="${rel_path##*/}"
|
|
507
|
+
if [ "$rel_path" = "$base_name" ]; then
|
|
508
|
+
remote_parent="${CLOUD_REMOTE%/}"
|
|
509
|
+
else
|
|
510
|
+
parent_path="${rel_path%/*}"
|
|
511
|
+
remote_parent="${CLOUD_REMOTE%/}/$parent_path"
|
|
512
|
+
fi
|
|
513
|
+
|
|
514
|
+
output_file="$(mktemp)" || return 2
|
|
515
|
+
if ! rclone lsf "$remote_parent" --files-only --max-depth 1 --retries 1 > "$output_file" 2>>"$LOG_FILE"; then
|
|
516
|
+
rm -f "$output_file"
|
|
517
|
+
return 2
|
|
518
|
+
fi
|
|
519
|
+
if grep -Fxq -- "$base_name" "$output_file"; then
|
|
520
|
+
rc=0
|
|
521
|
+
else
|
|
522
|
+
rc=1
|
|
523
|
+
fi
|
|
524
|
+
rm -f "$output_file"
|
|
525
|
+
return "$rc"
|
|
526
|
+
}
|
|
527
|
+
|
|
395
528
|
snapshot_apply_delete_intents() {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
529
|
+
local paths_file="${1:-}"
|
|
530
|
+
local remote_inventory_file="${2:-}"
|
|
531
|
+
local inventory_ready="${3:-0}"
|
|
532
|
+
if [ ! -f "$paths_file" ]; then
|
|
533
|
+
log "ERROR: active delete-intent inventory file missing; refusing snapshot promotion"
|
|
534
|
+
return 1
|
|
535
|
+
fi
|
|
536
|
+
|
|
537
|
+
if [ ! -s "$paths_file" ]; then
|
|
538
|
+
log "delete-intent: no active tombstones"
|
|
399
539
|
return 0
|
|
400
540
|
fi
|
|
401
541
|
|
|
402
542
|
# Re-materialize ledger from git so rclone sync cannot drop git-only intents,
|
|
403
|
-
#
|
|
404
|
-
(
|
|
405
|
-
cd "$SNAPSHOT_WORKTREE" || exit
|
|
406
|
-
fetch_origin_main_ref 2>/dev/null || true
|
|
543
|
+
# and fail closed if the authoritative ledger cannot be restored.
|
|
544
|
+
if ! (
|
|
545
|
+
cd "$SNAPSHOT_WORKTREE" || exit 1
|
|
407
546
|
if git rev-parse --verify --quiet origin/main >/dev/null 2>&1; then
|
|
408
|
-
git checkout origin/main -- meta/delete-intents/
|
|
547
|
+
git checkout origin/main -- meta/delete-intents/
|
|
409
548
|
fi
|
|
410
|
-
)
|
|
549
|
+
) 2>>"$LOG_FILE"; then
|
|
550
|
+
log "ERROR: could not restore authoritative delete-intent ledger from origin/main"
|
|
551
|
+
return 1
|
|
552
|
+
fi
|
|
411
553
|
|
|
412
|
-
local
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
554
|
+
local active_count remote_present_count attempted pruned already_absent failed deferred
|
|
555
|
+
local rel_path plan_file recheck_rc
|
|
556
|
+
active_count="$(wc -l < "$paths_file" | tr -d ' ')"
|
|
557
|
+
remote_present_count=0
|
|
558
|
+
attempted=0
|
|
559
|
+
pruned=0
|
|
560
|
+
already_absent=0
|
|
561
|
+
failed=0
|
|
562
|
+
deferred=0
|
|
563
|
+
|
|
564
|
+
while IFS= read -r rel_path; do
|
|
565
|
+
[ -z "$rel_path" ] && continue
|
|
566
|
+
if [ -e "$SNAPSHOT_WORKTREE/$rel_path" ]; then
|
|
567
|
+
rm -f -- "$SNAPSHOT_WORKTREE/$rel_path"
|
|
568
|
+
log "delete-intent: stripped resurrected path from worktree: $rel_path"
|
|
569
|
+
fi
|
|
570
|
+
done < "$paths_file"
|
|
571
|
+
|
|
572
|
+
if [ "$inventory_ready" != "1" ] || [ ! -f "$remote_inventory_file" ]; then
|
|
573
|
+
log "WARNING: delete-intent remote inventory unavailable; optional S3 pruning skipped"
|
|
574
|
+
log "delete-intent: no-resurrect pass complete active=$active_count inventory_ready=0 remote_present=0 attempted=0 pruned=0 already_absent=0 failed=0 deferred=0"
|
|
575
|
+
return 0
|
|
418
576
|
fi
|
|
419
577
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
578
|
+
plan_file="$(mktemp)" || {
|
|
579
|
+
log "WARNING: could not create delete-intent prune plan; optional S3 pruning skipped"
|
|
580
|
+
return 0
|
|
581
|
+
}
|
|
582
|
+
if ! delete_intent_plan_remote_paths "$paths_file" "$remote_inventory_file" > "$plan_file"; then
|
|
583
|
+
rm -f "$plan_file"
|
|
584
|
+
log "WARNING: could not plan delete-intent remote intersection; optional S3 pruning skipped"
|
|
423
585
|
return 0
|
|
424
586
|
fi
|
|
425
587
|
|
|
426
|
-
|
|
588
|
+
remote_present_count="$(wc -l < "$plan_file" | tr -d ' ')"
|
|
589
|
+
already_absent=$((active_count - remote_present_count))
|
|
590
|
+
|
|
427
591
|
while IFS= read -r rel_path; do
|
|
428
592
|
[ -z "$rel_path" ] && continue
|
|
429
|
-
if [ -
|
|
430
|
-
|
|
431
|
-
log "delete-intent: stripped resurrected path from worktree: $rel_path"
|
|
593
|
+
if [ "$attempted" -ge "$MAX_TOMBSTONE_PRUNES" ]; then
|
|
594
|
+
break
|
|
432
595
|
fi
|
|
433
|
-
|
|
434
|
-
if
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
fi
|
|
596
|
+
attempted=$((attempted + 1))
|
|
597
|
+
if rclone deletefile "${CLOUD_REMOTE%/}/$rel_path" --retries 1 >>"$LOG_FILE" 2>&1; then
|
|
598
|
+
pruned=$((pruned + 1))
|
|
599
|
+
log "delete-intent: pruned S3 object $rel_path"
|
|
600
|
+
continue
|
|
439
601
|
fi
|
|
440
|
-
done < "$paths_file"
|
|
441
602
|
|
|
442
|
-
|
|
443
|
-
|
|
603
|
+
snapshot_remote_path_exists_exact "$rel_path"
|
|
604
|
+
recheck_rc=$?
|
|
605
|
+
if [ "$recheck_rc" -eq 1 ]; then
|
|
606
|
+
already_absent=$((already_absent + 1))
|
|
607
|
+
log "delete-intent: delete race resolved as already absent: $rel_path"
|
|
608
|
+
elif [ "$recheck_rc" -eq 0 ]; then
|
|
609
|
+
failed=$((failed + 1))
|
|
610
|
+
log "WARNING: delete-intent prune failed and object remains present: $rel_path"
|
|
611
|
+
else
|
|
612
|
+
failed=$((failed + 1))
|
|
613
|
+
log "WARNING: delete-intent prune failed and exact recheck was unavailable: $rel_path"
|
|
614
|
+
fi
|
|
615
|
+
done < "$plan_file"
|
|
616
|
+
|
|
617
|
+
deferred=$((remote_present_count - attempted))
|
|
618
|
+
rm -f "$plan_file"
|
|
619
|
+
log "delete-intent: no-resurrect pass complete active=$active_count inventory_ready=1 remote_present=$remote_present_count attempted=$attempted pruned=$pruned already_absent=$already_absent failed=$failed deferred=$deferred"
|
|
444
620
|
return 0
|
|
445
621
|
}
|
|
446
622
|
|
|
447
|
-
|
|
623
|
+
snapshot_reconcile_delete_intents() {
|
|
624
|
+
local tmp_dir active_paths remote_inventory rc
|
|
625
|
+
tmp_dir="$(mktemp -d)" || {
|
|
626
|
+
log "ERROR: could not create delete-intent reconciliation state"
|
|
627
|
+
return 1
|
|
628
|
+
}
|
|
629
|
+
active_paths="$tmp_dir/active.paths"
|
|
630
|
+
remote_inventory="$tmp_dir/remote.paths"
|
|
631
|
+
|
|
632
|
+
if ! snapshot_refresh_origin_main 2>/dev/null; then
|
|
633
|
+
log "WARNING: could not refresh origin/main before delete-intent reconciliation; using the existing ref"
|
|
634
|
+
fi
|
|
635
|
+
if ! snapshot_load_active_delete_intent_paths "$active_paths"; then
|
|
636
|
+
rm -rf "$tmp_dir"
|
|
637
|
+
log "ERROR: could not load active delete-intent paths; refusing snapshot promotion"
|
|
638
|
+
return 1
|
|
639
|
+
fi
|
|
448
640
|
|
|
449
|
-
snapshot_direct_s3_preflight || true
|
|
641
|
+
snapshot_direct_s3_preflight "$remote_inventory" "$active_paths" || true
|
|
642
|
+
snapshot_apply_delete_intents "$active_paths" "$remote_inventory" "$SNAPSHOT_REMOTE_INVENTORY_READY"
|
|
643
|
+
rc=$?
|
|
644
|
+
rm -rf "$tmp_dir"
|
|
645
|
+
return "$rc"
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if ! snapshot_reconcile_delete_intents; then
|
|
649
|
+
exit 1
|
|
650
|
+
fi
|
|
450
651
|
|
|
451
652
|
# Change to git dir for operations
|
|
452
653
|
cd "$SNAPSHOT_WORKTREE" || { log "ERROR: Failed to cd to $SNAPSHOT_WORKTREE"; exit 1; }
|
|
@@ -514,7 +715,9 @@ if [ "$needs_repair" = true ]; then
|
|
|
514
715
|
exit 1
|
|
515
716
|
fi
|
|
516
717
|
rm -f "$RCLONE_LOG"
|
|
517
|
-
|
|
718
|
+
if ! snapshot_reconcile_delete_intents; then
|
|
719
|
+
exit 1
|
|
720
|
+
fi
|
|
518
721
|
fi
|
|
519
722
|
|
|
520
723
|
if ! raw_dedup_guard; then
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillwiki",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.13",
|
|
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": {
|