skillwiki 0.10.37 → 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.
@@ -5968,6 +5968,7 @@ export {
5968
5968
  isFailedRunStatus,
5969
5969
  readSatelliteLatestRunFromText,
5970
5970
  evaluateSatelliteRunHealth,
5971
+ detectFuseMount,
5971
5972
  snapshotterHealthChecks,
5972
5973
  runDoctor,
5973
5974
  readCliPackageJson,
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  GateError,
8
8
  SATELLITE_STALE_MS,
9
9
  configPath,
10
+ detectFuseMount,
10
11
  evaluateDirtyVolumeGate,
11
12
  evaluateSatelliteRunHealth,
12
13
  findPlugin,
@@ -38,7 +39,7 @@ import {
38
39
  scanConflictMarkerBlocksInText,
39
40
  snapshotterHealthChecks,
40
41
  upsertIndexEntry
41
- } from "./chunk-WJVG3DD5.js";
42
+ } from "./chunk-NDR2OY4K.js";
42
43
  import {
43
44
  normalizeDistTag,
44
45
  readCache,
@@ -168,6 +169,7 @@ import {
168
169
  ok,
169
170
  readPage,
170
171
  redactSensitiveContent,
172
+ resolveReadOnlyVaultRoot,
171
173
  scanSensitiveContent,
172
174
  scanVault,
173
175
  splitFrontmatter
@@ -4471,7 +4473,8 @@ import { mkdir as mkdir8, readFile as readFile11, writeFile as writeFile7 } from
4471
4473
  import { join as join24, relative, sep } from "path";
4472
4474
  var MAX_WORDS = 900;
4473
4475
  async function runSessionBrief(input) {
4474
- const scan = await scanVault(input.vault);
4476
+ const { root: scanRoot } = resolveReadOnlyVaultRoot(input.vault);
4477
+ const scan = await scanVault(scanRoot);
4475
4478
  if (!scan.ok) {
4476
4479
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
4477
4480
  }
@@ -4497,7 +4500,7 @@ async function runSessionBrief(input) {
4497
4500
  loadSatelliteHealth(input.vault),
4498
4501
  loadSessionPins(input.vault, project),
4499
4502
  project ? loadMemoryTopics(input.vault, project) : Promise.resolve([]),
4500
- loadPendingSources(input.vault, today)
4503
+ loadPendingSources(scanRoot, today)
4501
4504
  ]);
4502
4505
  const healthWarnings = [
4503
4506
  ...baseHealthWarnings,
@@ -8404,11 +8407,23 @@ async function runBackupRestore(input) {
8404
8407
  import { existsSync as existsSync10, statSync as statSync4 } from "fs";
8405
8408
  import { readFile as readFile16 } from "fs/promises";
8406
8409
  import { join as join32 } from "path";
8410
+ var FUSE_NO_MIRROR_NOTE = "FUSE vault with no read mirror - status scan may be slow";
8407
8411
  async function runStatus(input) {
8408
8412
  if (!existsSync10(input.vault)) {
8409
8413
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
8410
8414
  }
8411
- const scan = await scanVault(input.vault);
8415
+ const { root: scanRoot, mirrored } = resolveReadOnlyVaultRoot(input.vault);
8416
+ let readSource = "live";
8417
+ if (mirrored) {
8418
+ readSource = "mirror";
8419
+ } else if (detectFuseMount(input.vault)) {
8420
+ readSource = "fuse-no-mirror";
8421
+ }
8422
+ if (readSource === "fuse-no-mirror") {
8423
+ process.stderr.write(`warning: ${FUSE_NO_MIRROR_NOTE}
8424
+ `);
8425
+ }
8426
+ const scan = await scanVault(scanRoot);
8412
8427
  if (!scan.ok) {
8413
8428
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
8414
8429
  }
@@ -8430,7 +8445,7 @@ async function runStatus(input) {
8430
8445
  const compound = scan.data.compound.length;
8431
8446
  let schemaVersion = "v1";
8432
8447
  try {
8433
- const schemaContent = await readFile16(join32(input.vault, "SCHEMA.md"), "utf8");
8448
+ const schemaContent = await readFile16(join32(scanRoot, "SCHEMA.md"), "utf8");
8434
8449
  const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
8435
8450
  if (versionMatch) schemaVersion = versionMatch[1];
8436
8451
  } catch {
@@ -8467,14 +8482,20 @@ async function runStatus(input) {
8467
8482
  };
8468
8483
  const totalPages = Object.values(pageCounts).reduce((a, b) => a + b, 0);
8469
8484
  const rawTotal = rawArticles + rawTranscripts;
8470
- const humanHint = [
8485
+ const hintLines = [
8471
8486
  `vault: ${input.vault}`,
8472
8487
  `lang: ${langResult.value}`,
8473
8488
  `total: ${totalPages} pages`,
8474
8489
  ` entities: ${pageCounts.entities} concepts: ${pageCounts.concepts} comparisons: ${pageCounts.comparisons} queries: ${pageCounts.queries} meta: ${pageCounts.meta}`,
8475
8490
  ` raw: ${rawTotal} work_items: ${workItems} compound: ${compound}`,
8476
8491
  `last modified: ${lastModified.slice(0, 10)}`
8477
- ].join("\n");
8492
+ ];
8493
+ if (readSource === "mirror") {
8494
+ hintLines.push(`read source: mirror (${scanRoot}) - page counts may lag up to 30m`);
8495
+ } else if (readSource === "fuse-no-mirror") {
8496
+ hintLines.push(FUSE_NO_MIRROR_NOTE);
8497
+ }
8498
+ const humanHint = hintLines.join("\n");
8478
8499
  return {
8479
8500
  exitCode: ExitCode.OK,
8480
8501
  result: ok({
@@ -8484,6 +8505,7 @@ async function runStatus(input) {
8484
8505
  page_counts: pageCounts,
8485
8506
  total_pages: totalPages,
8486
8507
  last_modified: lastModified,
8508
+ read_source: readSource,
8487
8509
  humanHint
8488
8510
  })
8489
8511
  };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runSkillwikiMcpStdio
4
- } from "./chunk-WJVG3DD5.js";
4
+ } from "./chunk-NDR2OY4K.js";
5
5
  import "./chunk-7I2TPIV5.js";
6
6
  import "./chunk-QVO332CT.js";
7
7
  import "./chunk-XRBUABGE.js";
@@ -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.37",
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.37",
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.37",
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.37",
3
+ "version": "0.10.40",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",