skillwiki 0.10.39 → 0.10.40

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.
@@ -32,6 +32,21 @@ HANDOFF_NOTIFY_AFTER_SECONDS="${WIKI_FETCH_HANDOFF_NOTIFY_AFTER_SECONDS:-3600}"
32
32
  case "$HANDOFF_NOTIFY_AFTER_SECONDS" in
33
33
  ''|*[!0-9]*) HANDOFF_NOTIFY_AFTER_SECONDS=3600 ;;
34
34
  esac
35
+ # P2 handoff hard-pause (2026-08-12 design, see projects/llm-wiki/architecture/2026-08-12-wiki-push-fail-dedup-design.md)
36
+ # WIKI_FETCH_HANDOFF_HARD_PAUSE=1 enables the hard pause; default is 0 (opt-in
37
+ # per design Q3) so the legacy reminder-backoff behavior is preserved.
38
+ # WIKI_FETCH_HANDOFF_HARD_PAUSE_CYCLES controls the threshold of consecutive
39
+ # 5-min cycles of the same handoff identity before pausing the fetch job
40
+ # (default 13 cycles — one past the 1h HANDOFF_NOTIFY_AFTER_SECONDS mark at
41
+ # StartInterval=300, so the counter can reach the threshold before the
42
+ # notify resets it; the design's "1-hour hard pause" intent is preserved).
43
+ FETCH_HARD_PAUSE="${WIKI_FETCH_HANDOFF_HARD_PAUSE:-0}"
44
+ FETCH_HARD_PAUSE_CYCLES="${WIKI_FETCH_HANDOFF_HARD_PAUSE_CYCLES:-13}"
45
+ case "$FETCH_HARD_PAUSE_CYCLES" in
46
+ ''|*[!0-9]*) FETCH_HARD_PAUSE_CYCLES=13 ;;
47
+ esac
48
+ FETCH_HARD_PAUSE_COUNTER_FILE="$STATE_DIR/handoff-cycle-counter"
49
+ FETCH_HARD_PAUSE_MARKER="$STATE_DIR/wiki-fetch.paused"
35
50
  # Opt-in: when enabled, a positive delta triggers `git pull --rebase` so the
36
51
  # local working tree consumes sg01 Snapshot commits automatically. This
37
52
  # replaces the git pull that was previously bundled inside wiki-push.sh's
@@ -43,6 +58,15 @@ LOG_FILE="$(platform_log_dir)/wiki-fetch.log"
43
58
 
44
59
  mkdir -p "$STATE_DIR" "$(dirname "$LOG_FILE")"
45
60
 
61
+ # Terminal-state check: if the pause marker exists from a prior incident,
62
+ # the launchd job is already unloaded. Honor the pause — log once and exit
63
+ # without re-fetching or re-notifying. The user clears this by removing
64
+ # the marker and re-loading the plist (per design Q1, B).
65
+ if [ -f "$FETCH_HARD_PAUSE_MARKER" ]; then
66
+ log "PAUSED fetch job is paused; marker present at $FETCH_HARD_PAUSE_MARKER — exiting"
67
+ exit 0
68
+ fi
69
+
46
70
  log() {
47
71
  printf '%s %s\n' "$(date -u +%FT%TZ)" "$*" >>"$LOG_FILE"
48
72
  }
@@ -50,7 +74,7 @@ log() {
50
74
  handle_existing_handoff() {
51
75
  local blocker reason op identity previous_identity notified_at now
52
76
  blocker="$(vault_sync_op_preflight_blocker "$WIKI_DIR" 2>/dev/null || true)"
53
- [ -n "$blocker" ] || { rm -f "$HANDOFF_STATE_FILE" 2>/dev/null || true; return 1; }
77
+ [ -n "$blocker" ] || { rm -f "$HANDOFF_STATE_FILE" "$FETCH_HARD_PAUSE_COUNTER_FILE" 2>/dev/null || true; return 1; }
54
78
 
55
79
  reason="${blocker%% *}"
56
80
  op="${blocker#* }"
@@ -69,8 +93,34 @@ handle_existing_handoff() {
69
93
  platform_notify "wiki" "review-required handoff ${op:-none} still blocks sync"
70
94
  printf 'identity=%s\nnotified_at=%s\n' "$identity" "$now" > "$HANDOFF_STATE_FILE"
71
95
  log "NOTIFY handoff identity=$identity"
96
+ # P2: a new handoff identity (or a fresh notify) resets the cycle counter.
97
+ if [ "$FETCH_HARD_PAUSE" = "1" ]; then
98
+ printf '0\n' > "$FETCH_HARD_PAUSE_COUNTER_FILE" 2>/dev/null || true
99
+ fi
72
100
  else
73
101
  log "SKIP PULL handoff identity=$identity reminder-backoff"
102
+ # P2: increment the cycle counter for this identity. When the counter
103
+ # reaches FETCH_HARD_PAUSE_CYCLES, write the persistent-handoff log line
104
+ # and unload the launchd job (with marker-file fallback for non-macOS).
105
+ if [ "$FETCH_HARD_PAUSE" = "1" ]; then
106
+ local cycle_count
107
+ cycle_count=0
108
+ if [ -f "$FETCH_HARD_PAUSE_COUNTER_FILE" ]; then
109
+ cycle_count="$(cat "$FETCH_HARD_PAUSE_COUNTER_FILE" 2>/dev/null || echo 0)"
110
+ case "$cycle_count" in ''|*[!0-9]*) cycle_count=0 ;; esac
111
+ fi
112
+ cycle_count=$((cycle_count + 1))
113
+ printf '%s\n' "$cycle_count" > "$FETCH_HARD_PAUSE_COUNTER_FILE" 2>/dev/null || true
114
+ if [ "$cycle_count" -ge "$FETCH_HARD_PAUSE_CYCLES" ]; then
115
+ log "handoff persistent — pausing fetch (identity=$identity cycles=$cycle_count threshold=$FETCH_HARD_PAUSE_CYCLES)"
116
+ printf 'paused_at=%s\nidentity=%s\ncycles=%s\nthreshold=%s\nreason=handoff-persistent-past-threshold\n' \
117
+ "$(date -u +%FT%TZ)" "$identity" "$cycle_count" "$FETCH_HARD_PAUSE_CYCLES" \
118
+ > "$FETCH_HARD_PAUSE_MARKER" 2>/dev/null || true
119
+ if [ "$(platform_scheduler)" = "launchd" ]; then
120
+ launchctl unload "$HOME/Library/LaunchAgents/com.karlchow.wiki-fetch.plist" 2>/dev/null || true
121
+ fi
122
+ fi
123
+ fi
74
124
  fi
75
125
  return 0
76
126
  }
@@ -37,20 +37,122 @@ LOCK_FILE="$(platform_cache_dir)/wiki-push.lock"
37
37
  LOG_FILE="$(platform_log_dir)/wiki-push.log"
38
38
  LOG_MAX_SIZE=1048576 # 1 MB
39
39
  LOG_KEEP=5
40
+ # P1 conflict-marker dedup (2026-08-12 design, see projects/llm-wiki/architecture/2026-08-12-wiki-push-fail-dedup-design.md)
41
+ # WIKI_PUSH_FAIL_DEDUP_DISABLE=1 disables the dedup entirely (rollback to legacy behavior).
42
+ # WIKI_PUSH_FAIL_DEDUP_COOLDOWN_SECONDS controls the per-incident cooldown window (default 900s = 15min).
43
+ PUSH_DEDUP_DISABLE="${WIKI_PUSH_FAIL_DEDUP_DISABLE:-0}"
44
+ PUSH_DEDUP_COOLDOWN_SECONDS="${WIKI_PUSH_FAIL_DEDUP_COOLDOWN_SECONDS:-900}"
45
+ PUSH_DEDUP_STATE_FILE="$(platform_cache_dir)/wiki-push-fail-dedup.state"
46
+ PUSH_DEDUP_PAUSE_MARKER="$(platform_cache_dir)/wiki-push.paused"
40
47
 
41
48
  mkdir -p "$(dirname "$LOCK_FILE")" "$(dirname "$LOG_FILE")"
42
49
 
43
50
  log() { printf '%s %s\n' "$(date -u +%FT%TZ)" "$*" >>"$LOG_FILE"; }
44
51
 
52
+ # Terminal-state check: if the pause marker exists from a prior incident,
53
+ # the launchd job is already unloaded. Honor the pause — log once and exit
54
+ # without re-scanning or re-attempting the push. The user clears this by
55
+ # removing the marker and re-loading the plist (per design Q1, B).
56
+ if [ "$PUSH_DEDUP_DISABLE" != "1" ] && [ -f "$PUSH_DEDUP_PAUSE_MARKER" ]; then
57
+ log "PAUSED push job is paused; marker present at $PUSH_DEDUP_PAUSE_MARKER — exiting"
58
+ exit 0
59
+ fi
60
+
61
+ # P1 dedup state check: read the dedup state file's mtime and compare against
62
+ # the cooldown window. Echoes one of: "disabled" (dedup turned off, caller
63
+ # must not touch state), "first" (no prior incident, standard guard should
64
+ # run), "cooldown" (within window, suppress duplicate FAIL line + skip the
65
+ # new FAIL log), "expired" (past window, escalate to pause).
66
+ push_dedup_cooldown_check() {
67
+ if [ "$PUSH_DEDUP_DISABLE" = "1" ]; then
68
+ printf '%s\n' "disabled"
69
+ return 0
70
+ fi
71
+ if [ ! -f "$PUSH_DEDUP_STATE_FILE" ]; then
72
+ printf '%s\n' "first"
73
+ return 0
74
+ fi
75
+ local state_ctime now elapsed
76
+ state_ctime="$(platform_stat_ctime "$PUSH_DEDUP_STATE_FILE" 2>/dev/null || echo 0)"
77
+ now="$(date +%s)"
78
+ elapsed=$((now - state_ctime))
79
+ if [ "$elapsed" -lt "$PUSH_DEDUP_COOLDOWN_SECONDS" ]; then
80
+ printf '%s\n' "cooldown"
81
+ return 0
82
+ fi
83
+ printf '%s\n' "expired"
84
+ return 0
85
+ }
86
+
87
+ # Touch (or create) the dedup state file so subsequent invocations see the
88
+ # incident's first-detection timestamp. Uses a single `touch` to avoid races
89
+ # with parallel invocations (which are guarded by the lockfile above).
90
+ push_dedup_touch_state() {
91
+ touch "$PUSH_DEDUP_STATE_FILE" 2>/dev/null || true
92
+ }
93
+
94
+ # Clear the dedup state file (used on a successful push, when markers are gone).
95
+ push_dedup_clear_state() {
96
+ rm -f "$PUSH_DEDUP_STATE_FILE" 2>/dev/null || true
97
+ }
98
+
99
+ # Write a pause marker file as a portable stand-in for `launchctl unload`.
100
+ # On a real macOS host, the launchd plist is the actual unpause target; this
101
+ # marker is a portable test surface and a cross-platform indicator.
102
+ push_dedup_write_pause_marker() {
103
+ local now
104
+ now="$(date -u +%FT%TZ)"
105
+ printf 'paused_at=%s reason=conflict-markers-persist-past-cooldown cooldown_seconds=%s\n' \
106
+ "$now" "$PUSH_DEDUP_COOLDOWN_SECONDS" > "$PUSH_DEDUP_PAUSE_MARKER" 2>/dev/null || true
107
+ if [ "$(platform_scheduler)" = "launchd" ]; then
108
+ # Best-effort launchd unload. Failure is non-fatal — the marker file is
109
+ # the canonical pause signal. This matches the design doc's rollback
110
+ # semantics: the marker is the test surface, launchctl is the production
111
+ # surface, both can be present.
112
+ launchctl unload "$HOME/Library/LaunchAgents/com.karlchow.wiki-push.plist" 2>/dev/null || true
113
+ fi
114
+ }
115
+
45
116
  conflict_marker_guard() {
46
117
  local findings
47
118
  findings="$(mktemp)" || { log "FAIL could not create conflict-marker scan temp file"; return 1; }
48
119
  if ! vault_sync_scan_conflict_markers "$WIKI_DIR" "$findings"; then
49
- log "FAIL conflict marker blocks present; refusing S3 push"
50
- vault_sync_log_conflict_marker_findings "$findings" "$LOG_FILE"
120
+ local dedup_state
121
+ dedup_state="$(push_dedup_cooldown_check)"
122
+ case "$dedup_state" in
123
+ first)
124
+ # First detection: log the FAIL line, mark the state file so
125
+ # subsequent invocations within the cooldown window are silent.
126
+ log "FAIL conflict marker blocks present; refusing S3 push"
127
+ vault_sync_log_conflict_marker_findings "$findings" "$LOG_FILE"
128
+ push_dedup_touch_state
129
+ ;;
130
+ expired)
131
+ # Past the cooldown: log the FAIL line + escalation notice,
132
+ # write the pause marker, refresh the state file so the next
133
+ # cooldown starts from this moment.
134
+ log "FAIL conflict marker blocks present; refusing S3 push"
135
+ vault_sync_log_conflict_marker_findings "$findings" "$LOG_FILE"
136
+ log "cooldown expired — pausing push (markers persist past ${PUSH_DEDUP_COOLDOWN_SECONDS}s)"
137
+ push_dedup_write_pause_marker
138
+ push_dedup_touch_state
139
+ ;;
140
+ cooldown)
141
+ # Within cooldown: suppress the duplicate FAIL line. The first
142
+ # FAIL line in the incident (state=first) is already logged;
143
+ # subsequent invocations within the cooldown are silent.
144
+ ;;
145
+ disabled)
146
+ # Dedup is off: log the FAIL line (legacy behavior), do not
147
+ # touch any state file or pause marker.
148
+ log "FAIL conflict marker blocks present; refusing S3 push"
149
+ vault_sync_log_conflict_marker_findings "$findings" "$LOG_FILE"
150
+ ;;
151
+ esac
51
152
  rm -f "$findings"
52
153
  return 1
53
154
  fi
155
+ push_dedup_clear_state
54
156
  rm -f "$findings"
55
157
  return 0
56
158
  }
@@ -359,6 +461,11 @@ if [ "$RC" -eq 0 ]; then
359
461
  if ! remote_prune_tombstoned_paths; then
360
462
  log "FAIL tombstone prune failed after rclone copy"
361
463
  fi
464
+ # P1: a successful push proves markers are gone. conflict_marker_guard
465
+ # already cleared the dedup state on the scan-success path. The pause
466
+ # marker rm is defensive: it covers the case where the user manually
467
+ # unpaused the launchd job without removing the marker file.
468
+ rm -f "$PUSH_DEDUP_PAUSE_MARKER" 2>/dev/null || true
362
469
  fi
363
470
 
364
471
  exit 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.10.39",
3
+ "version": "0.10.40",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "skillwiki": "dist/cli.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.10.39",
3
+ "version": "0.10.40",
4
4
  "skills": "./",
5
5
  "description": "Project-aware Karpathy-style knowledge base for Claude Code: 19 prompt-only skills (wiki-*, proj-*, using-skillwiki) backed by the deterministic `skillwiki` CLI.",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.10.39",
3
+ "version": "0.10.40",
4
4
  "description": "Project-aware Karpathy-style knowledge base for Codex with 19 prompt-only skills backed by the deterministic skillwiki CLI.",
5
5
  "author": {
6
6
  "name": "karlorz",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillwiki/skills",
3
- "version": "0.10.39",
3
+ "version": "0.10.40",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",