skillwiki 0.10.16 → 0.10.18

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.
@@ -4,8 +4,8 @@ import {
4
4
  UNMANAGED_START,
5
5
  renderRootIndex,
6
6
  writeRootIndexProjection
7
- } from "./chunk-NMUYMNNB.js";
8
- import "./chunk-C5OLZRRM.js";
7
+ } from "./chunk-IGCW3DR2.js";
8
+ import "./chunk-Y6KRDGI2.js";
9
9
  export {
10
10
  UNMANAGED_END,
11
11
  UNMANAGED_START,
@@ -2,9 +2,9 @@
2
2
  import {
3
3
  runManagedWritePreflight,
4
4
  runManagedWriteTransaction
5
- } from "./chunk-UJLSUJB6.js";
6
- import "./chunk-OWPQJOFG.js";
7
- import "./chunk-C5OLZRRM.js";
5
+ } from "./chunk-QJPCCW4N.js";
6
+ import "./chunk-GNS2ZV5P.js";
7
+ import "./chunk-Y6KRDGI2.js";
8
8
  export {
9
9
  runManagedWritePreflight,
10
10
  runManagedWriteTransaction
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runSkillwikiMcpStdio
4
- } from "./chunk-24MVNAFJ.js";
4
+ } from "./chunk-IAPD6YTC.js";
5
5
  import "./chunk-7I2TPIV5.js";
6
- import "./chunk-NMUYMNNB.js";
7
- import "./chunk-OWPQJOFG.js";
8
- import "./chunk-C5OLZRRM.js";
6
+ import "./chunk-IGCW3DR2.js";
7
+ import "./chunk-GNS2ZV5P.js";
8
+ import "./chunk-Y6KRDGI2.js";
9
9
 
10
10
  // src/mcp-entry.ts
11
11
  runSkillwikiMcpStdio().catch((error) => {
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, writeFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const scriptDir = dirname(fileURLToPath(import.meta.url));
8
+ const repoRoot = join(scriptDir, "..", "..", "..");
9
+ const catalogPath = join(
10
+ repoRoot,
11
+ "packages",
12
+ "shared",
13
+ "src",
14
+ "systemd-property-catalog.json",
15
+ );
16
+ const outputPath = join(
17
+ repoRoot,
18
+ "packages",
19
+ "vault-sync",
20
+ "skills",
21
+ "vault-sync-status",
22
+ "systemd-property-catalog.sh",
23
+ );
24
+ const checkOnly = process.argv.includes("--check");
25
+
26
+ const catalog = JSON.parse(readFileSync(catalogPath, "utf8"));
27
+ const entries = Object.entries(catalog);
28
+ const invalid = entries.find(([semantic, property]) =>
29
+ !/^[a-z][a-z0-9_]*$/.test(semantic)
30
+ || typeof property !== "string"
31
+ || !/^[A-Z][A-Za-z0-9]*$/.test(property)
32
+ );
33
+ if (invalid) {
34
+ throw new Error(`invalid systemd property catalog entry: ${JSON.stringify(invalid)}`);
35
+ }
36
+
37
+ const lines = [
38
+ "#!/usr/bin/env bash",
39
+ "# AUTO-GENERATED by packages/vault-sync/scripts/generate-systemd-property-catalog.mjs.",
40
+ "# Source: packages/shared/src/systemd-property-catalog.json",
41
+ "# Do not edit this file directly.",
42
+ "",
43
+ "snapshot_semantic_to_systemd_prop() {",
44
+ ' case "$1" in',
45
+ ...entries.map(([semantic, property]) =>
46
+ ` ${semantic}) printf '${property}\\n' ;;`
47
+ ),
48
+ " # Closed map: never pass unknown/snake_case keys through to systemctl.",
49
+ " *) return 1 ;;",
50
+ " esac",
51
+ "}",
52
+ "",
53
+ ].join("\n");
54
+
55
+ if (checkOnly) {
56
+ let current = "";
57
+ try {
58
+ current = readFileSync(outputPath, "utf8");
59
+ } catch {
60
+ // Report the same actionable drift error for a missing generated file.
61
+ }
62
+ if (current !== lines) {
63
+ console.error(
64
+ "systemd property catalog shell adapter is stale; run "
65
+ + "node packages/vault-sync/scripts/generate-systemd-property-catalog.mjs",
66
+ );
67
+ process.exit(1);
68
+ }
69
+ console.log("systemd property catalog shell adapter is current");
70
+ } else {
71
+ writeFileSync(outputPath, lines);
72
+ console.log(`wrote ${outputPath}`);
73
+ }
@@ -61,10 +61,22 @@ elif [ -f "$SCRIPT_DIR/../../scripts/lib/delete-intent.sh" ]; then
61
61
  source "$SCRIPT_DIR/../../scripts/lib/delete-intent.sh"
62
62
  fi
63
63
 
64
+ if [ -f "$SCRIPT_DIR/lib/runtime-manifest.sh" ]; then
65
+ source "$SCRIPT_DIR/lib/runtime-manifest.sh"
66
+ elif [ -f "$SCRIPT_DIR/scripts/lib/runtime-manifest.sh" ]; then
67
+ source "$SCRIPT_DIR/scripts/lib/runtime-manifest.sh"
68
+ elif [ -f "$SCRIPT_DIR/../../scripts/lib/runtime-manifest.sh" ]; then
69
+ source "$SCRIPT_DIR/../../scripts/lib/runtime-manifest.sh"
70
+ fi
71
+
64
72
  if ! command -v vault_sync_scan_conflict_markers >/dev/null 2>&1; then
65
73
  echo "[wiki-snapshot] ERROR: conflict-marker helper unavailable; refusing to run." >&2
66
74
  exit 1
67
75
  fi
76
+ if ! command -v vault_sync_sha256 >/dev/null 2>&1; then
77
+ echo "[wiki-snapshot] ERROR: SHA-256 helper unavailable; refusing to run." >&2
78
+ exit 1
79
+ fi
68
80
 
69
81
  # ── Guard: Linux-only operation ────────────────────────────
70
82
  platform_require linux
@@ -79,10 +91,26 @@ CLOUD_REMOTE="${CLOUD_REMOTE:-cloud:cloud/wiki}"
79
91
  REPAIR_SCRIPT="${WIKI_GIT_REPAIR_SCRIPT:-$SCRIPT_DIR/wiki-git-repair-v3.sh}"
80
92
  MAX_S3_ONLY_NOTES="${WIKI_SNAPSHOT_MAX_S3_ONLY_NOTES:-200}"
81
93
  MAX_TOMBSTONE_PRUNES="${WIKI_SNAPSHOT_MAX_TOMBSTONE_PRUNES:-10}"
94
+ PROJECTION_PARITY_TIMEOUT_SECONDS="${WIKI_SNAPSHOT_PROJECTION_PARITY_TIMEOUT_SECONDS:-120}"
95
+ PROJECTION_PARITY_POLL_SECONDS="${WIKI_SNAPSHOT_PROJECTION_PARITY_POLL_SECONDS:-2}"
96
+ PROJECTION_READ_TIMEOUT_SECONDS="${WIKI_SNAPSHOT_PROJECTION_READ_TIMEOUT_SECONDS:-15}"
82
97
  DATE=$(date +%Y%m%d_%H%M%S)
83
98
  RCLONE_LOG="/tmp/rclone-${DATE}.log"
84
99
  SNAPSHOT_DIRECT_S3_NOT_GIT_COUNT=0
85
100
  SNAPSHOT_REMOTE_INVENTORY_READY=0
101
+ PROJECTION_STATE_DIR=""
102
+ PROJECTION_EXPECTED_INDEX_SHA256=""
103
+ PROJECTION_EXPECTED_LOG_SHA256=""
104
+ SNAPSHOT_BASE_OID=""
105
+ SNAPSHOT_GIT_DIR=""
106
+ SNAPSHOT_GIT_COMMON_DIR=""
107
+
108
+ cleanup_projection_state() {
109
+ if [ -n "$PROJECTION_STATE_DIR" ] && [ -d "$PROJECTION_STATE_DIR" ]; then
110
+ rm -rf "$PROJECTION_STATE_DIR"
111
+ fi
112
+ }
113
+ trap cleanup_projection_state EXIT
86
114
 
87
115
  fetch_origin_main_ref() {
88
116
  # The snapshot guards read origin/main directly. Use an explicit destination
@@ -309,6 +337,174 @@ log() {
309
337
  echo "$(date '+%Y-%m-%d %H:%M:%S') $1" | tee -a "$LOG_FILE"
310
338
  }
311
339
 
340
+ snapshot_projection_hash_file() {
341
+ local path="${1:-}"
342
+ [ -f "$path" ] || return 1
343
+ local digest
344
+ digest="$(vault_sync_sha256 "$path")"
345
+ [ -n "$digest" ] || return 1
346
+ printf '%s\n' "$digest"
347
+ }
348
+
349
+ validate_projection_parity_config() {
350
+ case "$PROJECTION_PARITY_TIMEOUT_SECONDS" in
351
+ ''|*[!0-9]*)
352
+ log "ERROR: invalid WIKI_SNAPSHOT_PROJECTION_PARITY_TIMEOUT_SECONDS=$PROJECTION_PARITY_TIMEOUT_SECONDS; expected a non-negative integer"
353
+ return 1
354
+ ;;
355
+ esac
356
+ case "$PROJECTION_PARITY_POLL_SECONDS" in
357
+ ''|*[!0-9]*)
358
+ log "ERROR: invalid WIKI_SNAPSHOT_PROJECTION_PARITY_POLL_SECONDS=$PROJECTION_PARITY_POLL_SECONDS; expected a non-negative integer"
359
+ return 1
360
+ ;;
361
+ esac
362
+ case "$PROJECTION_READ_TIMEOUT_SECONDS" in
363
+ ''|*[!0-9]*|0)
364
+ log "ERROR: invalid WIKI_SNAPSHOT_PROJECTION_READ_TIMEOUT_SECONDS=$PROJECTION_READ_TIMEOUT_SECONDS; expected a positive integer"
365
+ return 1
366
+ ;;
367
+ esac
368
+ return 0
369
+ }
370
+
371
+ snapshot_freeze_projection_expectations() {
372
+ if [ "${WIKI_SNAPSHOT_TEST_SKIP_PROJECTION_PARITY:-0}" = "1" ] \
373
+ && [ -n "${SNAPSHOT_TEST_ROOT:-}" ]; then
374
+ log "projection parity guard skipped by test fixture"
375
+ return 0
376
+ fi
377
+
378
+ if [ ! -f "$WIKI_DIR/index.md" ] || [ ! -f "$WIKI_DIR/log.md" ]; then
379
+ log "ERROR: live projection files missing after materialization"
380
+ return 1
381
+ fi
382
+
383
+ PROJECTION_STATE_DIR="$(mktemp -d)" || {
384
+ log "ERROR: could not create private projection parity state"
385
+ return 1
386
+ }
387
+ if ! cp "$WIKI_DIR/index.md" "$PROJECTION_STATE_DIR/expected-index.md" \
388
+ || ! cp "$WIKI_DIR/log.md" "$PROJECTION_STATE_DIR/expected-log.md"; then
389
+ log "ERROR: could not freeze live projection bytes"
390
+ return 1
391
+ fi
392
+
393
+ PROJECTION_EXPECTED_INDEX_SHA256="$(snapshot_projection_hash_file "$PROJECTION_STATE_DIR/expected-index.md")" || return 1
394
+ PROJECTION_EXPECTED_LOG_SHA256="$(snapshot_projection_hash_file "$PROJECTION_STATE_DIR/expected-log.md")" || return 1
395
+ log "projection expected bytes frozen index_sha256=$PROJECTION_EXPECTED_INDEX_SHA256 log_sha256=$PROJECTION_EXPECTED_LOG_SHA256"
396
+ return 0
397
+ }
398
+
399
+ snapshot_live_projection_matches_frozen() {
400
+ local live_index_sha live_log_sha
401
+ if ! cmp -s "$PROJECTION_STATE_DIR/expected-index.md" "$WIKI_DIR/index.md" \
402
+ || ! cmp -s "$PROJECTION_STATE_DIR/expected-log.md" "$WIKI_DIR/log.md"; then
403
+ live_index_sha="$(snapshot_projection_hash_file "$WIKI_DIR/index.md" 2>/dev/null || echo unavailable)"
404
+ live_log_sha="$(snapshot_projection_hash_file "$WIKI_DIR/log.md" 2>/dev/null || echo unavailable)"
405
+ log "ERROR: live projection changed after freeze expected_index_sha256=$PROJECTION_EXPECTED_INDEX_SHA256 live_index_sha256=$live_index_sha expected_log_sha256=$PROJECTION_EXPECTED_LOG_SHA256 live_log_sha256=$live_log_sha"
406
+ return 1
407
+ fi
408
+ return 0
409
+ }
410
+
411
+ snapshot_wait_for_direct_projection_parity() {
412
+ if [ -z "$PROJECTION_STATE_DIR" ]; then
413
+ # Test-only skip path above. Production always has frozen state.
414
+ return 0
415
+ fi
416
+
417
+ local started deadline now attempts
418
+ local remote_index="$PROJECTION_STATE_DIR/remote-index.md"
419
+ local remote_log="$PROJECTION_STATE_DIR/remote-log.md"
420
+ local remote_index_sha="unavailable"
421
+ local remote_log_sha="unavailable"
422
+ local index_pid log_pid index_rc log_rc
423
+ started="$(date +%s)"
424
+ deadline=$((started + PROJECTION_PARITY_TIMEOUT_SECONDS))
425
+ attempts=0
426
+
427
+ while :; do
428
+ attempts=$((attempts + 1))
429
+ : > "$remote_index"
430
+ : > "$remote_log"
431
+ timeout "$PROJECTION_READ_TIMEOUT_SECONDS" \
432
+ rclone cat "${CLOUD_REMOTE%/}/index.md" --retries 1 >"$remote_index" 2>>"$LOG_FILE" &
433
+ index_pid=$!
434
+ timeout "$PROJECTION_READ_TIMEOUT_SECONDS" \
435
+ rclone cat "${CLOUD_REMOTE%/}/log.md" --retries 1 >"$remote_log" 2>>"$LOG_FILE" &
436
+ log_pid=$!
437
+ wait "$index_pid"
438
+ index_rc=$?
439
+ wait "$log_pid"
440
+ log_rc=$?
441
+ if [ "$index_rc" -eq 0 ] && [ "$log_rc" -eq 0 ]; then
442
+ if cmp -s "$PROJECTION_STATE_DIR/expected-index.md" "$remote_index" \
443
+ && cmp -s "$PROJECTION_STATE_DIR/expected-log.md" "$remote_log"; then
444
+ if ! snapshot_live_projection_matches_frozen; then
445
+ return 1
446
+ fi
447
+ remote_index_sha="$(snapshot_projection_hash_file "$remote_index" 2>/dev/null || echo unavailable)"
448
+ remote_log_sha="$(snapshot_projection_hash_file "$remote_log" 2>/dev/null || echo unavailable)"
449
+ log "projection direct-store parity confirmed attempts=$attempts expected_index_sha256=$PROJECTION_EXPECTED_INDEX_SHA256 live_index_sha256=$PROJECTION_EXPECTED_INDEX_SHA256 remote_index_sha256=$remote_index_sha expected_log_sha256=$PROJECTION_EXPECTED_LOG_SHA256 live_log_sha256=$PROJECTION_EXPECTED_LOG_SHA256 remote_log_sha256=$remote_log_sha"
450
+ return 0
451
+ fi
452
+ fi
453
+
454
+ now="$(date +%s)"
455
+ if [ "$now" -ge "$deadline" ]; then
456
+ if [ "$index_rc" -eq 0 ]; then
457
+ remote_index_sha="$(snapshot_projection_hash_file "$remote_index" 2>/dev/null || echo unavailable)"
458
+ fi
459
+ if [ "$log_rc" -eq 0 ]; then
460
+ remote_log_sha="$(snapshot_projection_hash_file "$remote_log" 2>/dev/null || echo unavailable)"
461
+ fi
462
+ log "ERROR: projection direct-store parity timeout attempts=$attempts expected_index_sha256=$PROJECTION_EXPECTED_INDEX_SHA256 remote_index_sha256=$remote_index_sha expected_log_sha256=$PROJECTION_EXPECTED_LOG_SHA256 remote_log_sha256=$remote_log_sha"
463
+ return 1
464
+ fi
465
+ sleep "$PROJECTION_PARITY_POLL_SECONDS"
466
+ done
467
+ }
468
+
469
+ snapshot_verify_worktree_projection_parity() {
470
+ if [ -z "$PROJECTION_STATE_DIR" ]; then
471
+ return 0
472
+ fi
473
+ local worktree_index_sha worktree_log_sha
474
+ worktree_index_sha="$(snapshot_projection_hash_file "$SNAPSHOT_WORKTREE/index.md" 2>/dev/null || echo unavailable)"
475
+ worktree_log_sha="$(snapshot_projection_hash_file "$SNAPSHOT_WORKTREE/log.md" 2>/dev/null || echo unavailable)"
476
+ if ! cmp -s "$PROJECTION_STATE_DIR/expected-index.md" "$SNAPSHOT_WORKTREE/index.md" \
477
+ || ! cmp -s "$PROJECTION_STATE_DIR/expected-log.md" "$SNAPSHOT_WORKTREE/log.md"; then
478
+ log "ERROR: projection worktree parity mismatch expected_index_sha256=$PROJECTION_EXPECTED_INDEX_SHA256 worktree_index_sha256=$worktree_index_sha expected_log_sha256=$PROJECTION_EXPECTED_LOG_SHA256 worktree_log_sha256=$worktree_log_sha"
479
+ return 1
480
+ fi
481
+ log "projection worktree parity confirmed expected_index_sha256=$PROJECTION_EXPECTED_INDEX_SHA256 worktree_index_sha256=$worktree_index_sha expected_log_sha256=$PROJECTION_EXPECTED_LOG_SHA256 worktree_log_sha256=$worktree_log_sha"
482
+ return 0
483
+ }
484
+
485
+ snapshot_verify_semantic_projection_preview() {
486
+ if [ -z "$PROJECTION_STATE_DIR" ]; then
487
+ return 0
488
+ fi
489
+ local preview="$PROJECTION_STATE_DIR/worktree-preview.json"
490
+ if ! "$SKILLWIKI_BIN" projections materialize "$SNAPSHOT_WORKTREE" >"$preview" 2>>"$LOG_FILE"; then
491
+ log "ERROR: projection semantic preview failed"
492
+ return 1
493
+ fi
494
+ if ! grep -Eq '"index_drift"[[:space:]]*:[[:space:]]*false' "$preview" \
495
+ || ! grep -Eq '"log_drift"[[:space:]]*:[[:space:]]*false' "$preview"; then
496
+ log "ERROR: projection semantic preview mismatch index_or_log_drift=true"
497
+ return 1
498
+ fi
499
+ log "projection semantic preview confirmed index_drift=false log_drift=false"
500
+ return 0
501
+ }
502
+
503
+ snapshot_verify_projection_candidate() {
504
+ snapshot_verify_worktree_projection_parity \
505
+ && snapshot_verify_semantic_projection_preview
506
+ }
507
+
312
508
  # Emit the canonical snapshot-completion terminal record (v0.10.14).
313
509
  # One stable machine-parseable line written for both pushed and no-change
314
510
  # success outcomes. Failure paths must never call this.
@@ -316,9 +512,43 @@ log() {
316
512
  emit_snapshot_complete() {
317
513
  local outcome="$1"
318
514
  local head_oid origin_oid
515
+ if ! snapshot_refresh_origin_main; then
516
+ log "ERROR: could not refresh origin/main for final snapshot proof"
517
+ return 1
518
+ fi
319
519
  head_oid="$(git -C "$SNAPSHOT_WORKTREE" rev-parse HEAD 2>/dev/null || echo unknown)"
320
520
  origin_oid="$(git -C "$SNAPSHOT_WORKTREE" rev-parse --verify -q origin/main 2>/dev/null || echo unknown)"
521
+ if [ "$head_oid" = "unknown" ] || [ "$origin_oid" = "unknown" ] || [ "$head_oid" != "$origin_oid" ]; then
522
+ log "ERROR: final snapshot proof failed head=$head_oid origin=$origin_oid"
523
+ return 1
524
+ fi
321
525
  log "SNAPSHOT_COMPLETE schema=v1 outcome=${outcome} result=success ts=$(date -u +%Y-%m-%dT%H:%M:%SZ) head=${head_oid} origin=${origin_oid}"
526
+ return 0
527
+ }
528
+
529
+ snapshot_freeze_git_receipt() {
530
+ local mutation_root convergence_root
531
+ mutation_root="$(cd "$WIKI_DIR" 2>/dev/null && pwd -P)" || return 1
532
+ convergence_root="$(cd "$SNAPSHOT_WORKTREE" 2>/dev/null && pwd -P)" || return 1
533
+ SNAPSHOT_GIT_DIR="$(git -C "$SNAPSHOT_WORKTREE" rev-parse --absolute-git-dir 2>/dev/null)" || return 1
534
+ SNAPSHOT_GIT_COMMON_DIR="$(git -C "$SNAPSHOT_WORKTREE" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)" || return 1
535
+ SNAPSHOT_BASE_OID="$(git -C "$SNAPSHOT_WORKTREE" rev-parse HEAD 2>/dev/null)" || return 1
536
+ log "snapshot transaction receipt mutation_root=$mutation_root convergence_root=$convergence_root git_dir=$SNAPSHOT_GIT_DIR git_common_dir=$SNAPSHOT_GIT_COMMON_DIR base_oid=$SNAPSHOT_BASE_OID write_mode=dual-path-snapshot"
537
+ return 0
538
+ }
539
+
540
+ snapshot_verify_git_receipt() {
541
+ local current_git_dir current_common_dir current_head
542
+ current_git_dir="$(git -C "$SNAPSHOT_WORKTREE" rev-parse --absolute-git-dir 2>/dev/null)" || return 1
543
+ current_common_dir="$(git -C "$SNAPSHOT_WORKTREE" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)" || return 1
544
+ current_head="$(git -C "$SNAPSHOT_WORKTREE" rev-parse HEAD 2>/dev/null)" || return 1
545
+ if [ "$current_git_dir" != "$SNAPSHOT_GIT_DIR" ] \
546
+ || [ "$current_common_dir" != "$SNAPSHOT_GIT_COMMON_DIR" ] \
547
+ || [ "$current_head" != "$SNAPSHOT_BASE_OID" ]; then
548
+ log "ERROR: snapshot transaction receipt drift expected_base_oid=$SNAPSHOT_BASE_OID actual_base_oid=$current_head"
549
+ return 1
550
+ fi
551
+ return 0
322
552
  }
323
553
 
324
554
  validate_tombstone_prune_cap() {
@@ -410,6 +640,9 @@ log "=== Wiki Snapshot: $DATE ==="
410
640
  if ! validate_tombstone_prune_cap; then
411
641
  exit 1
412
642
  fi
643
+ if ! validate_projection_parity_config; then
644
+ exit 1
645
+ fi
413
646
 
414
647
  # Check disk space (need at least 100MB free)
415
648
  AVAILABLE_KB=$(df -k "$SNAPSHOT_WORKTREE" | awk 'NR==2 {print $4}')
@@ -418,8 +651,9 @@ if [ "$AVAILABLE_KB" -lt 102400 ]; then
418
651
  exit 1
419
652
  fi
420
653
 
421
- # Check if git directory exists
422
- if [ ! -d "$SNAPSHOT_WORKTREE/.git" ]; then
654
+ # Check repository validity through Git so linked worktrees (.git text files)
655
+ # are accepted and Git-internal paths are resolved from the convergence root.
656
+ if [ "$(git -C "$SNAPSHOT_WORKTREE" rev-parse --is-inside-work-tree 2>/dev/null)" != "true" ]; then
423
657
  log "ERROR: Git worktree not found or not a git repo: $SNAPSHOT_WORKTREE"
424
658
  exit 1
425
659
  fi
@@ -432,6 +666,10 @@ if [ "$(cd "$WIKI_DIR" 2>/dev/null && pwd -P)" = "$(cd "$SNAPSHOT_WORKTREE" 2>/d
432
666
  log "ERROR: WIKI_DIR and WIKI_GIT_WORKTREE must be distinct paths for dual-path projection (got: $WIKI_DIR)"
433
667
  exit 1
434
668
  fi
669
+ if ! snapshot_freeze_git_receipt; then
670
+ log "ERROR: could not freeze snapshot transaction receipt"
671
+ exit 1
672
+ fi
435
673
 
436
674
  # Single-authority root projections before FUSE/S3 pull promotion.
437
675
  # Mutation target is the live vault ($WIKI_DIR); Git pull/base-OID use
@@ -459,6 +697,17 @@ if command -v "$SKILLWIKI_BIN" >/dev/null 2>&1; then
459
697
  exit 1
460
698
  fi
461
699
  log "OK projections materialize before snapshot sync"
700
+ if ! snapshot_freeze_projection_expectations; then
701
+ log "FAIL projection expectation freeze; snapshot promotion refused"
702
+ exit 1
703
+ fi
704
+ if ! snapshot_wait_for_direct_projection_parity; then
705
+ log "FAIL projection direct-store parity; snapshot promotion refused"
706
+ exit 1
707
+ fi
708
+ else
709
+ log "ERROR: skillwiki CLI unavailable; refusing snapshot without projection materialization and parity proof"
710
+ exit 1
462
711
  fi
463
712
 
464
713
  # Common rclone options
@@ -660,6 +909,10 @@ snapshot_reconcile_delete_intents() {
660
909
  if ! snapshot_reconcile_delete_intents; then
661
910
  exit 1
662
911
  fi
912
+ if ! snapshot_verify_projection_candidate; then
913
+ log "FAIL projection candidate verification; snapshot promotion refused"
914
+ exit 1
915
+ fi
663
916
 
664
917
  # Change to git dir for operations
665
918
  cd "$SNAPSHOT_WORKTREE" || { log "ERROR: Failed to cd to $SNAPSHOT_WORKTREE"; exit 1; }
@@ -730,6 +983,14 @@ if [ "$needs_repair" = true ]; then
730
983
  if ! snapshot_reconcile_delete_intents; then
731
984
  exit 1
732
985
  fi
986
+ if ! snapshot_verify_projection_candidate; then
987
+ log "FAIL post-repair projection candidate verification; snapshot promotion refused"
988
+ exit 1
989
+ fi
990
+ if ! snapshot_freeze_git_receipt; then
991
+ log "ERROR: could not refresh snapshot transaction receipt after repair"
992
+ exit 1
993
+ fi
733
994
  fi
734
995
 
735
996
  if ! raw_dedup_guard; then
@@ -743,7 +1004,13 @@ fi
743
1004
  # Check for changes
744
1005
  if [ -z "$(git status --porcelain)" ]; then
745
1006
  log "No changes to commit"
746
- emit_snapshot_complete "no-change"
1007
+ if ! snapshot_verify_git_receipt; then
1008
+ log "ERROR: snapshot transaction changed before no-change proof"
1009
+ exit 1
1010
+ fi
1011
+ if ! emit_snapshot_complete "no-change"; then
1012
+ exit 1
1013
+ fi
747
1014
  exit 0
748
1015
  fi
749
1016
 
@@ -757,6 +1024,13 @@ if command -v git_case_assert_clean >/dev/null 2>&1; then
757
1024
  fi
758
1025
  fi
759
1026
 
1027
+ # The commit candidate may modify worktree bytes, but it must not silently move
1028
+ # the convergence repository or HEAD away from the frozen transaction base.
1029
+ if ! snapshot_verify_git_receipt; then
1030
+ log "ERROR: snapshot transaction changed before staging"
1031
+ exit 1
1032
+ fi
1033
+
760
1034
  # Commit
761
1035
  echo "Committing changes..."
762
1036
  if ! git add -A; then
@@ -820,7 +1094,9 @@ done
820
1094
  if [ "$PUSH_SUCCESS" = true ]; then
821
1095
  log "Push successful"
822
1096
  log "Status: complete"
823
- emit_snapshot_complete "pushed"
1097
+ if ! emit_snapshot_complete "pushed"; then
1098
+ exit 1
1099
+ fi
824
1100
  exit 0
825
1101
  else
826
1102
  log "ERROR: Push failed after $PUSH_RETRIES attempts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.10.16",
3
+ "version": "0.10.18",
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.16",
3
+ "version": "0.10.18",
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.16",
3
+ "version": "0.10.18",
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.16",
3
+ "version": "0.10.18",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",