skillwiki 0.10.114 → 0.10.116

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.
@@ -48,7 +48,7 @@ import {
48
48
  satelliteGateFromFleetLoad,
49
49
  snapshotterAliasForLocalHost,
50
50
  toUndirectedWeighted
51
- } from "./chunk-FQERKMD7.js";
51
+ } from "./chunk-CNELLIZ2.js";
52
52
  import {
53
53
  eventPathFor,
54
54
  writeLogEvent
@@ -71,12 +71,6 @@ import {
71
71
  mergeMcpAuthIntoEnv,
72
72
  redactMcpSecret
73
73
  } from "./chunk-OGEAFXON.js";
74
- import {
75
- xml
76
- } from "./chunk-XJ5VEIH7.js";
77
- import {
78
- atomicWriteText
79
- } from "./chunk-6VUK2FTX.js";
80
74
  import {
81
75
  CLIP_NOT_CONFIGURED,
82
76
  PRODUCTION_MIRROR_LABEL,
@@ -94,6 +88,9 @@ import {
94
88
  resolveSkillwikiHome,
95
89
  writeDotenv
96
90
  } from "./chunk-IJKJGQKL.js";
91
+ import {
92
+ atomicWriteText
93
+ } from "./chunk-6VUK2FTX.js";
97
94
  import {
98
95
  CompoundSchema,
99
96
  ExitCode,
@@ -4423,8 +4420,8 @@ var mcpHandshakeProbe = {
4423
4420
  };
4424
4421
 
4425
4422
  // src/doctor/probes/clip-outbox.ts
4426
- import { existsSync as existsSync18, readFileSync as readFileSync12 } from "fs";
4427
- import { execSync as execSync7 } from "child_process";
4423
+ import { existsSync as existsSync18 } from "fs";
4424
+ import { execSync as execSync7, execFileSync } from "child_process";
4428
4425
  import { platform as platform6 } from "os";
4429
4426
  import { join as join21 } from "path";
4430
4427
  var CLIP_CHECK_IDS = [
@@ -4477,6 +4474,27 @@ function launchdLoaded(label) {
4477
4474
  return false;
4478
4475
  }
4479
4476
  }
4477
+ function validateClipLaunchdPlist(value, label, command, root) {
4478
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
4479
+ const plist = value;
4480
+ const args = plist.ProgramArguments;
4481
+ if (plist.Label !== label || !Array.isArray(args) || args.length < 4 || !args.every((arg) => typeof arg === "string") || !args[0] || args[2] !== "clip-outbox" || args[3] !== command) return false;
4482
+ return root === void 0 ? plist.WatchPaths === void 0 : Array.isArray(plist.WatchPaths) && plist.WatchPaths.length === 1 && plist.WatchPaths[0] === root;
4483
+ }
4484
+ function launchdExplicitlyDisabled(text, label) {
4485
+ return text.split("\n").some((line) => {
4486
+ const match = line.trim().match(/^"([^"]+)"\s*=>\s*(true|false|enabled|disabled)\s*;?$/);
4487
+ return match?.[1] === label && (match[2] === "true" || match[2] === "disabled");
4488
+ });
4489
+ }
4490
+ function validInstalledPlist(path, label, command, root) {
4491
+ const decoded = JSON.parse(execFileSync("/usr/bin/plutil", ["-convert", "json", "-o", "-", path], {
4492
+ encoding: "utf8",
4493
+ timeout: 2e3,
4494
+ stdio: ["ignore", "pipe", "ignore"]
4495
+ }));
4496
+ return validateClipLaunchdPlist(decoded, label, command, root);
4497
+ }
4480
4498
  function watcherStatus(env, label, home, root) {
4481
4499
  const fixture = env.CLIP_WATCHER_FIXTURE;
4482
4500
  if (fixture === "loaded-enabled") return { loaded: true, enabled: true, kind: "production" };
@@ -4491,11 +4509,9 @@ function watcherStatus(env, label, home, root) {
4491
4509
  }
4492
4510
  const plist = join21(home, "Library", "LaunchAgents", `${label}.plist`);
4493
4511
  try {
4494
- const text = readFileSync12(plist, "utf8");
4495
- const xmlRoot = xml(root);
4496
- const matchesRoot = text.includes(`<key>WatchPaths</key><array><string>${xmlRoot}</string></array>`);
4512
+ const matchesRoot = validInstalledPlist(plist, label, "watch", root);
4497
4513
  const disabled = execSync7(`launchctl print-disabled gui/${process.getuid()}`, { encoding: "utf8", timeout: 2e3, stdio: ["ignore", "pipe", "ignore"] });
4498
- const explicitlyDisabled = disabled.split("\n").some((line) => line.includes(`"${label}"`) && line.includes("true"));
4514
+ const explicitlyDisabled = launchdExplicitlyDisabled(disabled, label);
4499
4515
  return { loaded: true, enabled: matchesRoot && !explicitlyDisabled, kind: "production" };
4500
4516
  } catch {
4501
4517
  return { loaded: true, enabled: false, kind: "unknown" };
@@ -4518,11 +4534,9 @@ function listenerOk(env, home) {
4518
4534
  if (!launchdLoaded(PRODUCTION_MIRROR_LABEL)) return false;
4519
4535
  const plist = join21(home, "Library", "LaunchAgents", `${PRODUCTION_MIRROR_LABEL}.plist`);
4520
4536
  try {
4521
- const text = readFileSync12(plist, "utf8");
4522
- if (text.includes("<key>WatchPaths</key>")) return false;
4537
+ const valid = validInstalledPlist(plist, PRODUCTION_MIRROR_LABEL, "mirror-watch");
4523
4538
  const disabled = execSync7(`launchctl print-disabled gui/${process.getuid()}`, { encoding: "utf8", timeout: 2e3, stdio: ["ignore", "pipe", "ignore"] });
4524
- const explicitlyDisabled = disabled.split("\n").some((line) => line.includes(`"${PRODUCTION_MIRROR_LABEL}"`) && line.includes("true"));
4525
- return text.includes("mirror-watch") && text.includes(PRODUCTION_MIRROR_LABEL) && !explicitlyDisabled;
4539
+ return valid && !launchdExplicitlyDisabled(disabled, PRODUCTION_MIRROR_LABEL);
4526
4540
  } catch {
4527
4541
  return false;
4528
4542
  }
@@ -4701,7 +4715,7 @@ async function runDoctor(input) {
4701
4715
  }
4702
4716
 
4703
4717
  // src/utils/package-info.ts
4704
- import { readFileSync as readFileSync13 } from "fs";
4718
+ import { readFileSync as readFileSync12 } from "fs";
4705
4719
  function packageJsonCandidateUrls(baseUrl = import.meta.url) {
4706
4720
  return [
4707
4721
  new URL("../package.json", baseUrl),
@@ -4711,7 +4725,7 @@ function packageJsonCandidateUrls(baseUrl = import.meta.url) {
4711
4725
  function readCliPackageJson(baseUrl = import.meta.url) {
4712
4726
  for (const url of packageJsonCandidateUrls(baseUrl)) {
4713
4727
  try {
4714
- const pkg = JSON.parse(readFileSync13(url, "utf8"));
4728
+ const pkg = JSON.parse(readFileSync12(url, "utf8"));
4715
4729
  if (typeof pkg.version === "string") {
4716
4730
  return { ...pkg, version: pkg.version };
4717
4731
  }
@@ -4722,8 +4736,8 @@ function readCliPackageJson(baseUrl = import.meta.url) {
4722
4736
  }
4723
4737
 
4724
4738
  // src/utils/vault-write-gates.ts
4725
- import { execFileSync } from "child_process";
4726
- import { existsSync as existsSync20, readdirSync as readdirSync7, readFileSync as readFileSync14, statSync as statSync3 } from "fs";
4739
+ import { execFileSync as execFileSync2 } from "child_process";
4740
+ import { existsSync as existsSync20, readdirSync as readdirSync7, readFileSync as readFileSync13, statSync as statSync3 } from "fs";
4727
4741
  import { join as join22, relative as relative2 } from "path";
4728
4742
  var DEFAULT_DIRTY_VOLUME_THRESHOLD = 50;
4729
4743
  var DEFAULT_CAPTURE_BUDGET = 20;
@@ -4805,7 +4819,7 @@ function measureDirtyVolume(vault) {
4805
4819
  }
4806
4820
  let porcelain = "";
4807
4821
  try {
4808
- porcelain = execFileSync("git", ["status", "--porcelain"], {
4822
+ porcelain = execFileSync2("git", ["status", "--porcelain"], {
4809
4823
  cwd: vault,
4810
4824
  encoding: "utf8",
4811
4825
  stdio: ["pipe", "pipe", "pipe"]
@@ -4996,7 +5010,7 @@ function listProjectDayCaptures(vault, project, day) {
4996
5010
  }
4997
5011
  if (norm.startsWith("raw/transcripts/")) {
4998
5012
  try {
4999
- const body = readFileSync14(abs, "utf8");
5013
+ const body = readFileSync13(abs, "utf8");
5000
5014
  if (body.includes(`project: ${slug}`) || body.includes(`project: "[[${slug}]]"`) || body.includes(`project: [[${slug}]]`)) {
5001
5015
  found.push(norm);
5002
5016
  } else if (base.includes(slug)) {
package/dist/cli.js CHANGED
@@ -1,4 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ CLIP_MIGRATION_HINT,
4
+ refreshInstalledClipPlist
5
+ } from "./chunk-4ZKCGX4V.js";
2
6
  import {
3
7
  buildDeleteIntent,
4
8
  normalizeVaultRelPath,
@@ -64,7 +68,7 @@ import {
64
68
  upsertIndexEntry,
65
69
  vectorIndexStatus,
66
70
  withMcpClient
67
- } from "./chunk-5RHR4FEK.js";
71
+ } from "./chunk-WV7NLVCD.js";
68
72
  import {
69
73
  applySourceDisposition,
70
74
  decodeSourceActionApproval,
@@ -174,7 +178,7 @@ import {
174
178
  stageVaultContentChanges,
175
179
  supersedeStaleReviewRequiredJournals,
176
180
  taxonomyCommentForPage
177
- } from "./chunk-FQERKMD7.js";
181
+ } from "./chunk-CNELLIZ2.js";
178
182
  import {
179
183
  authorizeRawOperation,
180
184
  buildSourceReferenceIndex,
@@ -217,13 +221,6 @@ import {
217
221
  nonEmptyEnvValue,
218
222
  redactMcpSecret
219
223
  } from "./chunk-OGEAFXON.js";
220
- import {
221
- CLIP_MIGRATION_HINT,
222
- refreshInstalledClipPlist
223
- } from "./chunk-XJ5VEIH7.js";
224
- import {
225
- atomicWriteText
226
- } from "./chunk-6VUK2FTX.js";
227
224
  import {
228
225
  CLIP_NOT_CONFIGURED,
229
226
  loadClipOutboxConfig
@@ -239,6 +236,9 @@ import {
239
236
  resolveSkillwikiHome,
240
237
  writeDotenv
241
238
  } from "./chunk-IJKJGQKL.js";
239
+ import {
240
+ atomicWriteText
241
+ } from "./chunk-6VUK2FTX.js";
242
242
  import {
243
243
  ExitCode,
244
244
  MetaSchema,
@@ -3608,7 +3608,7 @@ async function runClipOutboxStatus(home) {
3608
3608
  }
3609
3609
  async function runClipOutboxInstall(input) {
3610
3610
  try {
3611
- const { migrateClipOutbox } = await import("./migrate-SRRCXI2K.js");
3611
+ const { migrateClipOutbox } = await import("./migrate-4K6SXESR.js");
3612
3612
  return { exitCode: ExitCode.OK, result: ok(await migrateClipOutbox(input)) };
3613
3613
  } catch (e) {
3614
3614
  return { exitCode: ExitCode.PREFLIGHT_FAILED, result: err("PREFLIGHT_FAILED", { message: e instanceof Error ? e.message : "clip migration failed" }) };
@@ -3624,7 +3624,7 @@ async function runClipOutboxWatch(home) {
3624
3624
  const { loadClipCredentials, validateRuntime } = await import("./runtime-LQZIG3XN.js");
3625
3625
  const { createS3CreateOnlyPut, parseDestination } = await import("./upload-HTE62TOV.js");
3626
3626
  const { postReadyHint } = await import("./ready-S7F53Q2D.js");
3627
- const { startClipWorker } = await import("./watcher-VJ3TOU6J.js");
3627
+ const { startClipWorker } = await import("./watcher-2PBZQLKL.js");
3628
3628
  const cfg = resolved.config;
3629
3629
  const credentials = await loadClipCredentials(home, cfg);
3630
3630
  await validateRuntime(home, cfg, credentials.s3.endpoint);
@@ -11715,7 +11715,7 @@ async function emitManagedVaultWrite(vault, command, mutate, opts) {
11715
11715
  if (dirty) {
11716
11716
  return emit(dirty, void 0, { postCommit: false });
11717
11717
  }
11718
- const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-2BMHZWVJ.js");
11718
+ const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-KCQHTIHI.js");
11719
11719
  const run = await runManagedWriteTransaction2({
11720
11720
  vault,
11721
11721
  command,
@@ -6,12 +6,12 @@ import {
6
6
  runManagedWritePeerGate,
7
7
  runManagedWritePreflight,
8
8
  runManagedWriteTransaction
9
- } from "./chunk-FQERKMD7.js";
9
+ } from "./chunk-CNELLIZ2.js";
10
10
  import "./chunk-K3YZESND.js";
11
11
  import "./chunk-3TKNAOJH.js";
12
12
  import "./chunk-JL2SCLXT.js";
13
- import "./chunk-6VUK2FTX.js";
14
13
  import "./chunk-IJKJGQKL.js";
14
+ import "./chunk-6VUK2FTX.js";
15
15
  import "./chunk-VNSZCK5V.js";
16
16
  export {
17
17
  DEFAULT_MANAGED_WRITE_WAIT_MS,
@@ -6,11 +6,11 @@ import {
6
6
  refreshInstalledClipPlist,
7
7
  stableCliBinary,
8
8
  xml
9
- } from "./chunk-XJ5VEIH7.js";
10
- import "./chunk-6VUK2FTX.js";
9
+ } from "./chunk-4ZKCGX4V.js";
11
10
  import "./chunk-OWWWO2XS.js";
12
11
  import "./chunk-5D5HFQ76.js";
13
12
  import "./chunk-IJKJGQKL.js";
13
+ import "./chunk-6VUK2FTX.js";
14
14
  import "./chunk-VNSZCK5V.js";
15
15
  export {
16
16
  CLIP_MIGRATION_HINT,
@@ -1,20 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runSkillwikiMcpStdio
4
- } from "./chunk-5RHR4FEK.js";
4
+ } from "./chunk-WV7NLVCD.js";
5
5
  import "./chunk-CBT35FAW.js";
6
6
  import "./chunk-7I2TPIV5.js";
7
7
  import "./chunk-IJWZDQDH.js";
8
- import "./chunk-FQERKMD7.js";
8
+ import "./chunk-CNELLIZ2.js";
9
9
  import "./chunk-K3YZESND.js";
10
10
  import "./chunk-3TKNAOJH.js";
11
11
  import "./chunk-JL2SCLXT.js";
12
12
  import "./chunk-OGEAFXON.js";
13
- import "./chunk-XJ5VEIH7.js";
14
- import "./chunk-6VUK2FTX.js";
15
13
  import "./chunk-OWWWO2XS.js";
16
14
  import "./chunk-5D5HFQ76.js";
17
15
  import "./chunk-IJKJGQKL.js";
16
+ import "./chunk-6VUK2FTX.js";
18
17
  import "./chunk-VNSZCK5V.js";
19
18
 
20
19
  // src/mcp-entry.ts
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env python3
2
+ """Upgrade installed helpers without touching configuration or scheduler state.
3
+
4
+ Each entry point execs an immutable payload path, so an in-flight helper and
5
+ its late-loaded libraries stay on the same release across subsequent upgrades.
6
+ Legacy directory migration refuses while an installed helper is running.
7
+ """
8
+ import datetime
9
+ import hashlib
10
+ import json
11
+ import os
12
+ from pathlib import Path
13
+ import shutil
14
+ import signal
15
+ import subprocess
16
+ import sys
17
+ import tempfile
18
+
19
+
20
+ def deploy(root, share, role, version, commit, host, dry_run=False):
21
+ root, share = Path(root).resolve(), Path(share).resolve()
22
+ current = share / "bin"
23
+ if not current.is_dir() or not (share / "runtime-manifest.json").is_file():
24
+ raise ValueError("runtime-only requires an existing installation and manifest")
25
+ if dry_run:
26
+ print("runtime-only: stage immutable payload, validate, back up and switch bin; preserve configuration and scheduler")
27
+ return
28
+ lock = share / ".runtime-upgrade.lock"
29
+ lock.mkdir() # No age-based reclamation of another deployer's lock.
30
+ release = None
31
+ backup = None
32
+ swapped = False
33
+ old_link = os.readlink(current) if current.is_symlink() else None
34
+ manifest = share / "runtime-manifest.json"
35
+ old_manifest = manifest.read_bytes()
36
+ try:
37
+ # Existing old-style entry points cannot pin their late-loaded libs.
38
+ # Refuse migration until they finish; never stop or start a content job.
39
+ processes = subprocess.check_output(["ps", "-axww", "-o", "args="], text=True)
40
+ if not current.is_symlink() and any(str(current) + "/" in line for line in processes.splitlines()):
41
+ raise ValueError("installed helper is active; retry runtime upgrade after it exits")
42
+ releases = share / "runtime-releases"
43
+ releases.mkdir(exist_ok=True)
44
+ release = Path(tempfile.mkdtemp(prefix="runtime-", dir=releases))
45
+ payload = release / "payload"
46
+ shutil.copytree(root / "scripts", payload, ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
47
+ shutil.copy2(root / "skills/vault-presync/wiki-sync.sh", payload / "wiki-sync.sh")
48
+ # Keep auxiliary installed files while replacing package-owned helpers.
49
+ for source in current.iterdir():
50
+ if source.name in ("lib", "payload", "runtime-manifest.json", "previous-runtime-manifest.json"):
51
+ continue
52
+ if source.is_file():
53
+ shutil.copy2(source, release / source.name)
54
+ shutil.copytree(payload / "lib", release / "lib")
55
+ for file in payload.rglob("*"):
56
+ if file.suffix == ".sh":
57
+ subprocess.run(["bash", "-n", str(file)], check=True)
58
+ elif file.suffix == ".py":
59
+ compile(file.read_bytes(), str(file), "exec")
60
+ for file in payload.glob("*.sh"):
61
+ file.chmod(0o755)
62
+ # Quote absolute immutable path using shell single-quote escaping.
63
+ quoted = "'" + str(file).replace("'", "'\\''") + "'"
64
+ launcher = release / file.name
65
+ launcher.write_text("#!/bin/sh\nexec /bin/bash " + quoted + ' "$@"\n')
66
+ launcher.chmod(0o755)
67
+ files = {"bin/" + str(p.relative_to(release)): hashlib.sha256(p.read_bytes()).hexdigest()
68
+ for p in sorted(release.rglob("*")) if p.is_file()}
69
+ agents = Path.home() / "Library/LaunchAgents"
70
+ for p in sorted(agents.glob("com.karlchow.wiki-*.plist")):
71
+ files["LaunchAgents/" + p.name] = hashlib.sha256(p.read_bytes()).hexdigest()
72
+ new_manifest = dict(schema_version=1, package_version=version, package_commit=commit,
73
+ installer_version=version, installed_at=datetime.datetime.now(datetime.timezone.utc).isoformat(),
74
+ role=role, host_id=host, files=files)
75
+ (release / "runtime-manifest.json").write_text(json.dumps(new_manifest, indent=2) + "\n")
76
+ (release / "previous-runtime-manifest.json").write_bytes(old_manifest)
77
+ if os.environ.get("VS_RUNTIME_TEST_FAIL") == "before-switch":
78
+ raise ValueError("injected pre-switch failure")
79
+ candidate = share / ".bin-next"
80
+ candidate.symlink_to(release)
81
+ if old_link is None:
82
+ backup = share / ("bin-backup-" + release.name)
83
+ current.rename(backup)
84
+ # A legacy runner that opened its script during staging must finish
85
+ # against the old libraries; refuse before exposing new libraries.
86
+ processes = subprocess.check_output(["ps", "-axww", "-o", "args="], text=True)
87
+ if any(str(current) + "/" in line for line in processes.splitlines()):
88
+ raise ValueError("legacy helper started during staging; runtime restored, retry later")
89
+ os.replace(candidate, current)
90
+ swapped = True
91
+ if os.environ.get("VS_RUNTIME_TEST_FAIL") == "after-switch":
92
+ raise ValueError("injected post-switch failure")
93
+ staged_manifest = share / ".runtime-manifest-next.json"
94
+ staged_manifest.write_bytes((release / "runtime-manifest.json").read_bytes())
95
+ os.replace(staged_manifest, manifest)
96
+ for relative, digest in files.items():
97
+ if relative.startswith("bin/") and hashlib.sha256((share / relative).read_bytes()).hexdigest() != digest:
98
+ raise ValueError("installed runtime hash mismatch")
99
+ print("runtime-only installed " + version + "; rollback=" + str(backup or old_link))
100
+ except BaseException:
101
+ if swapped:
102
+ current.unlink()
103
+ if backup is not None and backup.exists():
104
+ backup.rename(current)
105
+ elif swapped and old_link is not None:
106
+ current.symlink_to(old_link)
107
+ manifest.write_bytes(old_manifest)
108
+ if release is not None:
109
+ shutil.rmtree(release)
110
+ raise
111
+ finally:
112
+ (share / ".bin-next").unlink(missing_ok=True)
113
+ (share / ".runtime-manifest-next.json").unlink(missing_ok=True)
114
+ lock.rmdir()
115
+
116
+
117
+ if __name__ == "__main__":
118
+ def interrupted(_signal, _frame):
119
+ raise KeyboardInterrupt("runtime upgrade interrupted")
120
+ signal.signal(signal.SIGTERM, interrupted)
121
+ signal.signal(signal.SIGINT, interrupted)
122
+ try:
123
+ deploy(*sys.argv[1:7], dry_run=sys.argv[7:] == ["--dry-run"])
124
+ except (OSError, ValueError, subprocess.SubprocessError) as error:
125
+ print("runtime-only refused: " + str(error), file=sys.stderr)
126
+ sys.exit(1)
@@ -456,67 +456,13 @@ vault_sync_op_write_inventory() {
456
456
  # Fail closed on any mismatch.
457
457
  vault_sync_op_verify_inventory() {
458
458
  local repo="$1" op_id="$2" stash_oid="${3:-}" skip_content_paths="${4:-}"
459
- local inv section path expected_hash actual_hash unmerged skip has_u3
460
-
459
+ local inv restore_head helper
461
460
  inv="$(vault_sync_op_get_field "$repo" "$op_id" inventory_path)" || return 1
462
- [ -f "$inv" ] || return 1
463
-
464
- unmerged="$(git -C "$repo" diff --name-only --diff-filter=U 2>/dev/null || true)"
465
- has_u3=0
466
- if [ -n "$stash_oid" ] && git -C "$repo" rev-parse -q --verify "${stash_oid}^3" >/dev/null 2>&1; then
467
- has_u3=1
468
- fi
469
- section=""
470
- while IFS= read -r path || [ -n "$path" ]; do
471
- case "$path" in
472
- "# staged") section="staged"; continue ;;
473
- "# tracked") section="tracked"; continue ;;
474
- "# untracked") section="untracked"; continue ;;
475
- ""|\#*) continue ;;
476
- esac
477
- [ -n "$path" ] || continue
478
- [ -n "$section" ] || continue
479
-
480
- # Skip content checks for currently-unmerged conflict paths.
481
- if printf '%s\n' "$unmerged" | grep -Fxq -- "$path"; then
482
- continue
483
- fi
484
-
485
- skip=0
486
- if [ -n "$skip_content_paths" ] && printf '%s\n' "$skip_content_paths" | grep -Fxq -- "$path"; then
487
- skip=1
488
- fi
489
-
490
- case "$section" in
491
- staged|tracked)
492
- if [ -n "$stash_oid" ] && git -C "$repo" cat-file -e "$stash_oid:$path" 2>/dev/null; then
493
- if [ ! -f "$repo/$path" ] && [ ! -L "$repo/$path" ]; then
494
- return 1
495
- fi
496
- if [ "$skip" -eq 0 ]; then
497
- expected_hash="$(git -C "$repo" rev-parse "$stash_oid:$path" 2>/dev/null || echo missing)"
498
- actual_hash="$(git -C "$repo" hash-object -- "$repo/$path" 2>/dev/null || echo missing)"
499
- [ "$expected_hash" = "$actual_hash" ] || return 1
500
- fi
501
- fi
502
- ;;
503
- untracked)
504
- if [ ! -e "$repo/$path" ] && [ ! -L "$repo/$path" ]; then
505
- return 1
506
- fi
507
- if [ "$skip" -eq 0 ] && [ "$has_u3" -eq 1 ]; then
508
- if git -C "$repo" cat-file -e "${stash_oid}^3:$path" 2>/dev/null; then
509
- expected_hash="$(git -C "$repo" rev-parse "${stash_oid}^3:$path" 2>/dev/null || echo missing)"
510
- if [ -f "$repo/$path" ]; then
511
- actual_hash="$(git -C "$repo" hash-object -- "$repo/$path" 2>/dev/null || echo missing)"
512
- [ "$expected_hash" = "$actual_hash" ] || return 1
513
- fi
514
- fi
515
- fi
516
- ;;
517
- esac
518
- done <"$inv"
519
- return 0
461
+ [ -f "$inv" ] && [ -n "$stash_oid" ] || return 1
462
+ restore_head="$(vault_sync_op_get_field "$repo" "$op_id" restore_head)"
463
+ [ -n "$restore_head" ] || restore_head="$(git -C "$repo" rev-parse HEAD)" || return 1
464
+ helper="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/verify-stash-inventory.py"
465
+ python3 "$helper" "$repo" "$inv" "$stash_oid" "$restore_head" "$skip_content_paths"
520
466
  }
521
467
 
522
468
  vault_sync_op_stash_push_owned() {
@@ -99,6 +99,14 @@ vault_sync_package_commit() {
99
99
  printf '%s\n' "$_override"
100
100
  return 0
101
101
  fi
102
+ # Exported deployment bundles have no .git; the bundle pin is the exact commit.
103
+ if [ -f "$_root/../../.deploy-pin" ]; then
104
+ _pin="$(cat "$_root/../../.deploy-pin")"
105
+ if printf '%s\n' "$_pin" | LC_ALL=C grep -Eq '^[0-9a-f]{40,64}$'; then
106
+ printf '%s\n' "$_pin"
107
+ return 0
108
+ fi
109
+ fi
102
110
  if command -v git >/dev/null 2>&1 && [ -d "$_root/../../.git" -o -f "$_root/../../.git" ] 2>/dev/null; then
103
111
  git -C "$_root/../.." rev-parse HEAD 2>/dev/null || echo ""
104
112
  return 0
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env python3
2
+ """Verify an owned stash restore against its frozen post-rebase baseline."""
3
+ import os
4
+ import pathlib
5
+ import subprocess
6
+ import sys
7
+ import tempfile
8
+
9
+
10
+ def verify(repo, inventory, stash, head, skipped):
11
+ def git(*args, data=None, stream=None):
12
+ return subprocess.run(["git", "--literal-pathspecs", "-C", repo, *args], input=data, stdin=stream,
13
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True).stdout
14
+
15
+ def entry(tree, path):
16
+ raw = git("ls-tree", "-z", tree, "--", path)
17
+ if not raw:
18
+ return None
19
+ meta, name = raw.rstrip(b"\0").split(b"\t", 1)
20
+ mode, kind, oid = meta.decode().split()
21
+ if name.decode() != path or kind != "blob":
22
+ raise ValueError("unsupported inventory entry")
23
+ return mode, oid
24
+
25
+ def expected(path):
26
+ base, local, upstream = (entry(tree, path) for tree in (stash + "^1", stash, head))
27
+ if local == base:
28
+ return upstream
29
+ if upstream == base or local == upstream:
30
+ return local
31
+ if not base or not local or not upstream:
32
+ raise ValueError("ambiguous add/delete restore")
33
+ if any(mode not in ("100644", "100755") for mode, _ in (base, local, upstream)):
34
+ raise ValueError("ambiguous file-type restore")
35
+ mode = local[0] if upstream[0] == base[0] else upstream[0]
36
+ with tempfile.TemporaryDirectory(prefix="stash-merge-") as directory:
37
+ files = [pathlib.Path(directory, name) for name in ("upstream", "base", "local")]
38
+ for file, value in zip(files, (upstream, base, local)):
39
+ if int(git("cat-file", "-s", value[1])) > 16 * 1024 * 1024:
40
+ raise ValueError("merge verification exceeds bounded file size")
41
+ file.write_bytes(git("cat-file", "blob", value[1]))
42
+ merged = subprocess.run(["git", "merge-file", "-p", *map(str, files)],
43
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
44
+ if merged.returncode:
45
+ raise ValueError("ambiguous content restore")
46
+ return mode, git("hash-object", "--stdin", data=merged.stdout).decode().strip()
47
+
48
+ if git("rev-parse", "HEAD").strip() != git("rev-parse", head).strip():
49
+ raise ValueError("restore baseline moved")
50
+ section = None
51
+ for path in pathlib.Path(inventory).read_text().splitlines():
52
+ if path in ("# staged", "# tracked", "# untracked"):
53
+ section = path[2:]
54
+ continue
55
+ if not section or not path or path.startswith("#"):
56
+ continue
57
+ if pathlib.PurePosixPath(path).is_absolute() or ".." in pathlib.PurePosixPath(path).parts:
58
+ raise ValueError("invalid inventory path")
59
+ file = pathlib.Path(repo, path)
60
+ if any(parent.is_symlink() for parent in file.parents if parent != pathlib.Path(repo) and pathlib.Path(repo) in parent.parents):
61
+ raise ValueError("inventory parent changed type")
62
+ # Unknown conflicts must never be treated as verified restoration.
63
+ if git("ls-files", "-u", "--", path):
64
+ raise ValueError("unresolved inventory path")
65
+ if path in skipped:
66
+ derived = path in ("index.md", "SCHEMA.md", "log.md") or path.endswith("/log.md") or (path.startswith("projects/") and path.count("/") == 2 and path.endswith("/knowledge.md"))
67
+ if not derived:
68
+ raise ValueError("non-derived inventory exemption")
69
+ want = entry(stash, path) or entry(head, path)
70
+ else:
71
+ want = entry(stash + "^3", path) if section == "untracked" else expected(path)
72
+ if want is None:
73
+ if os.path.lexists(file):
74
+ raise ValueError("deleted inventory path reappeared")
75
+ continue
76
+ mode, oid = want
77
+ if mode == "120000":
78
+ if not file.is_symlink():
79
+ raise ValueError("inventory symlink type changed")
80
+ data = os.fsencode(os.readlink(file))
81
+ else:
82
+ if file.is_symlink() or not file.is_file():
83
+ raise ValueError("inventory regular file missing or type changed")
84
+ if mode not in ("100644", "100755"):
85
+ raise ValueError("unsupported inventory mode")
86
+ if bool(file.stat().st_mode & 0o111) != (mode == "100755"):
87
+ raise ValueError("inventory executable mode changed")
88
+ data = None
89
+ if path not in skipped:
90
+ if data is None:
91
+ with file.open("rb") as stream:
92
+ actual = git("hash-object", "--stdin", stream=stream)
93
+ else:
94
+ actual = git("hash-object", "--stdin", data=data)
95
+ if actual.decode().strip() != oid:
96
+ raise ValueError("inventory content mismatch")
97
+
98
+
99
+ if __name__ == "__main__":
100
+ try:
101
+ repo, inventory, stash, head, skip = sys.argv[1:]
102
+ verify(repo, inventory, stash, head, set(skip.splitlines()))
103
+ except (OSError, ValueError, subprocess.SubprocessError):
104
+ print("inventory verification refused: missing, changed or ambiguous restore", file=sys.stderr)
105
+ sys.exit(1)
@@ -220,6 +220,7 @@ try_auto_resolve_derived_conflicts() {
220
220
  cmd_rc=$?
221
221
  printf '%s\n' "$resolver_out" >>"$LOG_FILE"
222
222
  if [ "$cmd_rc" -eq 0 ]; then
223
+ RESOLVED_PATHS="$(printf '%s\n' "$resolver_out" | python3 -c 'import json,sys; print("\n".join(json.load(sys.stdin)["data"]["resolved_paths"]))')" || return 1
223
224
  log "AUTO-RESOLVE composite derived artifacts op=$op_id"
224
225
  return 0
225
226
  elif [ "$cmd_rc" -eq 60 ]; then
@@ -243,6 +244,7 @@ except: pass
243
244
  cmd_rc=$?
244
245
  printf '%s\n' "$resolver_out" >>"$LOG_FILE"
245
246
  if [ "$cmd_rc" -eq 0 ]; then
247
+ RESOLVED_PATHS="$(printf '%s\n' "$resolver_out" | python3 -c 'import json,sys; print("\n".join(json.load(sys.stdin)["data"]["resolved_paths"]))')" || return 1
246
248
  log "AUTO-RESOLVE composite derived artifacts op=$op_id (skillwiki)"
247
249
  return 0
248
250
  elif [ "$cmd_rc" -eq 60 ]; then
@@ -615,12 +617,11 @@ while [ $REBASE_RC -ne 0 ]; do
615
617
  continue
616
618
  fi
617
619
 
618
- if try_auto_resolve_derived_conflicts "$OP_ID"; then
619
- if ! GIT_EDITOR=true git rebase --continue 2>>"$LOG_FILE"; then
620
- REBASE_RC=$?
621
- else
622
- REBASE_RC=0
623
- fi
620
+ RESOLVER_RC=0
621
+ try_auto_resolve_derived_conflicts "$OP_ID" || RESOLVER_RC=$?
622
+ if [ "$RESOLVER_RC" -eq 0 ]; then
623
+ GIT_EDITOR=true git rebase --continue 2>>"$LOG_FILE"
624
+ REBASE_RC=$?
624
625
  continue
625
626
  fi
626
627
 
@@ -628,8 +629,7 @@ while [ $REBASE_RC -ne 0 ]; do
628
629
  # The unknown paths must go through the manual-conflict path (retry/handoff).
629
630
  # The derived paths are already git-added; rebase --continue will fail on
630
631
  # the remaining unknowns, so we go straight to handle_manual_conflict.
631
- if [ "$?" -eq 60 ]; then
632
- local unknown_conflicts
632
+ if [ "$RESOLVER_RC" -eq 60 ]; then
633
633
  unknown_conflicts="$(git diff --name-only --diff-filter=U 2>/dev/null)"
634
634
  if [ -n "$unknown_conflicts" ]; then
635
635
  STOPPED_SHA=$(cat "$WIKI_DIR/.git/rebase-merge/stopped-sha" 2>/dev/null || echo "")
@@ -659,6 +659,7 @@ while [ $REBASE_RC -ne 0 ]; do
659
659
  done
660
660
 
661
661
  if [ -n "$OWNED_STASH_OID" ]; then
662
+ vault_sync_op_set_field "$WIKI_DIR" "$OP_ID" restore_head "$(git rev-parse HEAD)" || exit 1
662
663
  if vault_sync_op_stash_apply_owned "$WIKI_DIR" "$OWNED_STASH_OID" 2>>"$LOG_FILE"; then
663
664
  if ! vault_sync_op_verify_inventory "$WIKI_DIR" "$OP_ID" "$OWNED_STASH_OID" 2>>"$LOG_FILE"; then
664
665
  vault_sync_op_mark_review_required "$WIKI_DIR" "$OP_ID" "inventory-verify-failed"
@@ -673,7 +674,12 @@ if [ -n "$OWNED_STASH_OID" ]; then
673
674
  else
674
675
  CONFLICTS=$(git diff --name-only --diff-filter=U 2>/dev/null)
675
676
  vault_sync_op_set_phase "$WIKI_DIR" "$OP_ID" "restoring"
676
- if [ -n "$CONFLICTS" ] && try_auto_resolve_derived_conflicts "$OP_ID"; then
677
+ RESOLVER_RC=1
678
+ if [ -n "$CONFLICTS" ]; then
679
+ RESOLVER_RC=0
680
+ try_auto_resolve_derived_conflicts "$OP_ID" || RESOLVER_RC=$?
681
+ fi
682
+ if [ "$RESOLVER_RC" -eq 0 ]; then
677
683
  if [ -n "$(git diff --name-only --diff-filter=U 2>/dev/null)" ]; then
678
684
  vault_sync_op_mark_review_required "$WIKI_DIR" "$OP_ID" "stash-restore-conflicts"
679
685
  log "FAIL stash apply derived auto-resolve left conflicts"
@@ -681,7 +687,7 @@ if [ -n "$OWNED_STASH_OID" ]; then
681
687
  fi
682
688
  # Derived regeneration rewrites conflicted paths; verify presence for those
683
689
  # paths and content for everything else.
684
- SKIP_CONTENT="$(printf '%s\n' $CONFLICTS)"
690
+ SKIP_CONTENT="${RESOLVED_PATHS:-}"
685
691
  if ! vault_sync_op_verify_inventory "$WIKI_DIR" "$OP_ID" "$OWNED_STASH_OID" "$SKIP_CONTENT" 2>>"$LOG_FILE"; then
686
692
  vault_sync_op_mark_review_required "$WIKI_DIR" "$OP_ID" "inventory-verify-failed"
687
693
  log "FAIL inventory verification after derived auto-resolve oid=$OWNED_STASH_OID"
@@ -690,7 +696,7 @@ if [ -n "$OWNED_STASH_OID" ]; then
690
696
 
691
697
  vault_sync_op_stash_drop_owned "$WIKI_DIR" "$OWNED_STASH_OID" 2>>"$LOG_FILE" || true
692
698
  log "STASH apply derived conflicts auto-resolved"
693
- elif [ -n "$CONFLICTS" ] && [ "$?" -eq 60 ]; then
699
+ elif [ "$RESOLVER_RC" -eq 60 ]; then
694
700
  # M4: Partial-decline during stash-apply. The resolver resolved the
695
701
  # derived subset and returned resolved_paths. Unknown paths remain
696
702
  # conflicted — surface as review-required.
@@ -704,11 +710,7 @@ if [ -n "$OWNED_STASH_OID" ]; then
704
710
  # derived paths. The conflict set alone is insufficient because the
705
711
  # resolver regenerates derived files that may not have been in the
706
712
  # stash-apply conflict set (derived-conflicts.ts:100-134).
707
- SKIP_CONTENT="$(printf '%s\n' $CONFLICTS)"
708
- if [ -n "${RESOLVED_PATHS:-}" ]; then
709
- SKIP_CONTENT="${SKIP_CONTENT}
710
- ${RESOLVED_PATHS}"
711
- fi
713
+ SKIP_CONTENT="${RESOLVED_PATHS:-}"
712
714
  if ! vault_sync_op_verify_inventory "$WIKI_DIR" "$OP_ID" "$OWNED_STASH_OID" "$SKIP_CONTENT" 2>>"$LOG_FILE"; then
713
715
  vault_sync_op_mark_review_required "$WIKI_DIR" "$OP_ID" "inventory-verify-failed-partial"
714
716
  log "FAIL inventory verification after partial-decline oid=$OWNED_STASH_OID"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.10.114",
3
+ "version": "0.10.116",
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.114",
3
+ "version": "0.10.116",
4
4
  "skills": "./",
5
5
  "description": "Project-aware Karpathy-style knowledge base for Claude Code: 22 prompt-only skills (wiki-*, proj-*, using-skillwiki, skillwiki-mcp, skillwiki-connect) backed by the deterministic `skillwiki` CLI.",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.10.114",
3
+ "version": "0.10.116",
4
4
  "description": "Project-aware Karpathy-style knowledge base for Codex with 22 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",
3
- "version": "0.10.114",
3
+ "version": "0.10.116",
4
4
  "description": "Project-aware Karpathy-style knowledge base: 22 prompt-only skills (wiki-*, proj-*, using-skillwiki, skillwiki-mcp, skillwiki-connect) backed by HTTP MCP wiki_capture.",
5
5
  "author": {
6
6
  "name": "karlorz"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillwiki/skills",
3
- "version": "0.10.114",
3
+ "version": "0.10.116",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",
@@ -1,7 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- atomicWriteText
4
- } from "./chunk-6VUK2FTX.js";
5
2
  import {
6
3
  PRODUCTION_MIRROR_LABEL,
7
4
  PRODUCTION_WATCHER_LABEL,
@@ -15,6 +12,9 @@ import {
15
12
  import {
16
13
  parseDotenvFile
17
14
  } from "./chunk-IJKJGQKL.js";
15
+ import {
16
+ atomicWriteText
17
+ } from "./chunk-6VUK2FTX.js";
18
18
 
19
19
  // src/clip-outbox/migrate.ts
20
20
  import { access, mkdir, readFile } from "fs/promises";
@@ -35,15 +35,15 @@ import {
35
35
  splitFrontmatter,
36
36
  vaultIoConcurrency
37
37
  } from "./chunk-JL2SCLXT.js";
38
- import {
39
- atomicWriteText
40
- } from "./chunk-6VUK2FTX.js";
41
38
  import {
42
39
  parseDotenvFile,
43
40
  parseDotenvText,
44
41
  profileKey,
45
42
  resolveSkillwikiHome
46
43
  } from "./chunk-IJKJGQKL.js";
44
+ import {
45
+ atomicWriteText
46
+ } from "./chunk-6VUK2FTX.js";
47
47
  import {
48
48
  ExitCode,
49
49
  FleetManifestSchema,
@@ -1,9 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- readJournal,
4
- upsertJournal,
5
- writeJournal
6
- } from "./chunk-SEY4N4CT.js";
7
2
  import {
8
3
  buildReadyHint
9
4
  } from "./chunk-OSL4J7GF.js";
@@ -14,10 +9,15 @@ import {
14
9
  sha256Hex
15
10
  } from "./chunk-ZRGZBAVS.js";
16
11
  import "./chunk-QESNTTVY.js";
17
- import "./chunk-6VUK2FTX.js";
18
12
  import {
19
13
  assertPublicHttpsUrl
20
14
  } from "./chunk-5D5HFQ76.js";
15
+ import {
16
+ readJournal,
17
+ upsertJournal,
18
+ writeJournal
19
+ } from "./chunk-SEY4N4CT.js";
20
+ import "./chunk-6VUK2FTX.js";
21
21
  import "./chunk-VNSZCK5V.js";
22
22
 
23
23
  // src/clip-outbox/watcher.ts