skillwiki 0.10.5 → 0.10.7
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-NJFCTMYZ.js → chunk-45SMQAWC.js} +15 -7
- package/dist/{chunk-GEKJG4F7.js → chunk-4KGCTQM3.js} +90 -5
- package/dist/cli.js +3 -3
- package/dist/{managed-write-preflight-DGIE2FE2.js → managed-write-preflight-57DQZI2N.js} +1 -1
- package/dist/skillwiki-mcp.js +1 -1
- package/dist/vault-sync/scripts/lib/managed-write-lock.sh +154 -11
- package/dist/vault-sync/scripts/wiki-pull-with-auto-resolve.sh +10 -1
- 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
|
@@ -5604,7 +5604,8 @@ function hasOriginMain(resolvedPath) {
|
|
|
5604
5604
|
execSync2("git rev-parse --verify --quiet origin/main", {
|
|
5605
5605
|
cwd: resolvedPath,
|
|
5606
5606
|
encoding: "utf8",
|
|
5607
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
5607
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
5608
|
+
timeout: 2e3
|
|
5608
5609
|
});
|
|
5609
5610
|
return true;
|
|
5610
5611
|
} catch {
|
|
@@ -5622,7 +5623,8 @@ function checkVaultGitDirty(resolvedPath) {
|
|
|
5622
5623
|
const lines = execSync2("git status --porcelain", {
|
|
5623
5624
|
cwd: resolvedPath,
|
|
5624
5625
|
encoding: "utf8",
|
|
5625
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
5626
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
5627
|
+
timeout: 5e3
|
|
5626
5628
|
}).trim().split("\n").filter(Boolean);
|
|
5627
5629
|
if (lines.length > 0) {
|
|
5628
5630
|
return check("warn", "vault_git_dirty", "Vault git dirty state", `${lines.length} dirty file(s) in vault worktree`);
|
|
@@ -5799,7 +5801,8 @@ function checkVaultGitComparison(resolvedPath, id, label, range, nonZeroSuffix,
|
|
|
5799
5801
|
const count = parseInt(execSync2(`git rev-list --count ${range}`, {
|
|
5800
5802
|
cwd: resolvedPath,
|
|
5801
5803
|
encoding: "utf8",
|
|
5802
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
5804
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
5805
|
+
timeout: 5e3
|
|
5803
5806
|
}).trim(), 10);
|
|
5804
5807
|
if (count > 0) {
|
|
5805
5808
|
return check("warn", id, label, `${count} commit(s) ${nonZeroSuffix}`);
|
|
@@ -6745,6 +6748,9 @@ function findSkillNames(dir) {
|
|
|
6745
6748
|
return results;
|
|
6746
6749
|
}
|
|
6747
6750
|
var METRIC_TYPES = ["entities", "concepts", "comparisons", "queries", "meta"];
|
|
6751
|
+
function doctorReadOnlyScanRoot(resolvedPath) {
|
|
6752
|
+
return resolveReadOnlyVaultRoot(resolvedPath).root;
|
|
6753
|
+
}
|
|
6748
6754
|
async function vaultMetrics(resolvedPath) {
|
|
6749
6755
|
const ids = [
|
|
6750
6756
|
["vault_metric_pages", "Vault pages by type"],
|
|
@@ -6755,7 +6761,8 @@ async function vaultMetrics(resolvedPath) {
|
|
|
6755
6761
|
];
|
|
6756
6762
|
const noVault = () => ids.map(([id, label]) => check("info", id, label, "no vault configured"));
|
|
6757
6763
|
if (!resolvedPath) return noVault();
|
|
6758
|
-
const
|
|
6764
|
+
const scanRoot = doctorReadOnlyScanRoot(resolvedPath);
|
|
6765
|
+
const scan = await scanVault(scanRoot);
|
|
6759
6766
|
if (!scan.ok) return noVault();
|
|
6760
6767
|
const tk = scan.data.typedKnowledge;
|
|
6761
6768
|
const perType = METRIC_TYPES.map((d) => `${d} ${tk.filter((p) => p.relPath.startsWith(d + "/")).length}`).join(", ");
|
|
@@ -6782,7 +6789,7 @@ async function vaultMetrics(resolvedPath) {
|
|
|
6782
6789
|
}
|
|
6783
6790
|
let logLines = 0;
|
|
6784
6791
|
try {
|
|
6785
|
-
logLines = readFileSync10(join24(
|
|
6792
|
+
logLines = readFileSync10(join24(scanRoot, "log.md"), "utf8").split("\n").length;
|
|
6786
6793
|
} catch {
|
|
6787
6794
|
}
|
|
6788
6795
|
return [
|
|
@@ -6844,8 +6851,9 @@ async function runDoctor(input) {
|
|
|
6844
6851
|
checks.push(checkVaultS3Remote(input.home, input.execProbe, input.env ?? process.env));
|
|
6845
6852
|
checks.push(checkVaultSnapshotterReachable(fleetLoad, input.checkSnapshotter, input.execProbe));
|
|
6846
6853
|
checks.push(checkVaultPromotionLag(gitCheckPath));
|
|
6847
|
-
|
|
6848
|
-
checks.push(
|
|
6854
|
+
const readOnlyScanRoot = resolvedPath ? doctorReadOnlyScanRoot(resolvedPath) : void 0;
|
|
6855
|
+
checks.push(checkDotStoreClean(readOnlyScanRoot));
|
|
6856
|
+
checks.push(checkVaultConflictMarkers(readOnlyScanRoot));
|
|
6849
6857
|
checks.push(checkS3MountPerf(resolvedPath));
|
|
6850
6858
|
checks.push(checkS3MountFreshness(resolvedPath));
|
|
6851
6859
|
checks.push(checkRcloneFlagAudit(resolvedPath));
|
|
@@ -16,20 +16,90 @@ import {
|
|
|
16
16
|
} from "./chunk-C5OLZRRM.js";
|
|
17
17
|
|
|
18
18
|
// src/utils/managed-write-preflight.ts
|
|
19
|
-
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
19
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
20
20
|
import { join as join2, resolve } from "path";
|
|
21
21
|
|
|
22
22
|
// src/utils/managed-write-lock.ts
|
|
23
23
|
import { randomBytes } from "crypto";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
existsSync,
|
|
26
|
+
mkdirSync,
|
|
27
|
+
readFileSync,
|
|
28
|
+
unlinkSync,
|
|
29
|
+
writeFileSync
|
|
30
|
+
} from "fs";
|
|
25
31
|
import { dirname, join } from "path";
|
|
26
32
|
function managedWriteLockPath(vault) {
|
|
27
33
|
const gitPath = git(vault, ["rev-parse", "--git-path", "vault-sync/managed-write.lock"]);
|
|
28
34
|
if (gitPath) return gitPath.startsWith("/") ? gitPath : join(vault, gitPath);
|
|
29
35
|
return join(vault, ".skillwiki", "managed-write.lock");
|
|
30
36
|
}
|
|
31
|
-
function
|
|
37
|
+
function readLockRecord(path) {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function isManagedWriteLockOwnerAlive(pid) {
|
|
45
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false;
|
|
46
|
+
try {
|
|
47
|
+
process.kill(pid, 0);
|
|
48
|
+
return true;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error.code === "EPERM") return true;
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function hasUnsafeGitState(vault) {
|
|
55
|
+
const gitDirRaw = git(vault, ["rev-parse", "--git-dir"]);
|
|
56
|
+
if (!gitDirRaw) return true;
|
|
57
|
+
const gitDir = gitDirRaw.startsWith("/") ? gitDirRaw : join(vault, gitDirRaw);
|
|
58
|
+
for (const rel of ["rebase-merge", "rebase-apply"]) {
|
|
59
|
+
if (existsSync(join(gitDir, rel))) return true;
|
|
60
|
+
}
|
|
61
|
+
for (const rel of ["MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD"]) {
|
|
62
|
+
if (existsSync(join(gitDir, rel))) return true;
|
|
63
|
+
}
|
|
64
|
+
const unmerged = git(vault, ["ls-files", "-u"]);
|
|
65
|
+
return Boolean(unmerged && unmerged.trim().length > 0);
|
|
66
|
+
}
|
|
67
|
+
function reclaimDeadManagedWriteLockOwner(vault) {
|
|
32
68
|
const path = managedWriteLockPath(vault);
|
|
69
|
+
if (!existsSync(path)) return ok({ reclaimed: false });
|
|
70
|
+
const record = readLockRecord(path);
|
|
71
|
+
if (!record) {
|
|
72
|
+
return err("SYNC_LOCK_HELD", { path, message: "managed-write lock unreadable" });
|
|
73
|
+
}
|
|
74
|
+
if (isManagedWriteLockOwnerAlive(record.pid)) {
|
|
75
|
+
return err("SYNC_LOCK_HELD", { path, message: "managed-write lock owner is alive" });
|
|
76
|
+
}
|
|
77
|
+
if (hasUnsafeGitState(vault)) {
|
|
78
|
+
return err("SYNC_LOCK_HELD", {
|
|
79
|
+
path,
|
|
80
|
+
message: "managed-write lock not reclaimed: unsafe git state"
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const recoveryDir = join(dirname(path), "recovery");
|
|
85
|
+
mkdirSync(recoveryDir, { recursive: true });
|
|
86
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
87
|
+
const recoveryPath = join(recoveryDir, `stale-managed-write-lock-${stamp}-${process.pid}.json`);
|
|
88
|
+
const meta = {
|
|
89
|
+
recovered_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
90
|
+
recovery_reason: "owner_pid_dead",
|
|
91
|
+
owner_pid_alive: false,
|
|
92
|
+
lock: record
|
|
93
|
+
};
|
|
94
|
+
writeFileSync(recoveryPath, `${JSON.stringify(meta, null, 2)}
|
|
95
|
+
`, { flag: "wx" });
|
|
96
|
+
unlinkSync(path);
|
|
97
|
+
return ok({ reclaimed: true, recoveryPath });
|
|
98
|
+
} catch (error) {
|
|
99
|
+
return err("WRITE_FAILED", { path, message: String(error) });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function tryCreateLock(path, command) {
|
|
33
103
|
const ownerToken = randomBytes(16).toString("hex");
|
|
34
104
|
const acquired = (/* @__PURE__ */ new Date()).toISOString();
|
|
35
105
|
try {
|
|
@@ -40,12 +110,27 @@ function acquireManagedWriteLock(vault, command) {
|
|
|
40
110
|
`,
|
|
41
111
|
{ flag: "wx" }
|
|
42
112
|
);
|
|
43
|
-
return ok({ vault, path, ownerToken, acquired });
|
|
113
|
+
return ok({ vault: "", path, ownerToken, acquired });
|
|
44
114
|
} catch (error) {
|
|
45
115
|
if (error.code === "EEXIST") return err("SYNC_LOCK_HELD", { path });
|
|
46
116
|
return err("WRITE_FAILED", { path, message: String(error) });
|
|
47
117
|
}
|
|
48
118
|
}
|
|
119
|
+
function acquireManagedWriteLock(vault, command) {
|
|
120
|
+
const path = managedWriteLockPath(vault);
|
|
121
|
+
const first = tryCreateLock(path, command);
|
|
122
|
+
if (first.ok) {
|
|
123
|
+
return ok({ ...first.data, vault });
|
|
124
|
+
}
|
|
125
|
+
if (first.error !== "SYNC_LOCK_HELD") return first;
|
|
126
|
+
const reclaimed = reclaimDeadManagedWriteLockOwner(vault);
|
|
127
|
+
if (!reclaimed.ok || !reclaimed.data.reclaimed) {
|
|
128
|
+
return err("SYNC_LOCK_HELD", { path });
|
|
129
|
+
}
|
|
130
|
+
const second = tryCreateLock(path, command);
|
|
131
|
+
if (second.ok) return ok({ ...second.data, vault });
|
|
132
|
+
return second.ok === false ? second : err("SYNC_LOCK_HELD", { path });
|
|
133
|
+
}
|
|
49
134
|
function releaseManagedWriteLock(handle) {
|
|
50
135
|
try {
|
|
51
136
|
const parsed = JSON.parse(readFileSync(handle.path, "utf8"));
|
|
@@ -85,7 +170,7 @@ function preflightBlocker(vault) {
|
|
|
85
170
|
}
|
|
86
171
|
function fleetManifestBytes(vault) {
|
|
87
172
|
const path = join2(vault, FLEET_REL_PATH);
|
|
88
|
-
if (!
|
|
173
|
+
if (!existsSync2(path)) return null;
|
|
89
174
|
return readFileSync2(path);
|
|
90
175
|
}
|
|
91
176
|
function isGitVault(vault) {
|
package/dist/cli.js
CHANGED
|
@@ -69,7 +69,7 @@ import {
|
|
|
69
69
|
satelliteLatestRunPath,
|
|
70
70
|
taxonomyCommentForPage,
|
|
71
71
|
upsertIndexEntry
|
|
72
|
-
} from "./chunk-
|
|
72
|
+
} from "./chunk-45SMQAWC.js";
|
|
73
73
|
import {
|
|
74
74
|
normalizeDistTag,
|
|
75
75
|
readCache,
|
|
@@ -95,7 +95,7 @@ import {
|
|
|
95
95
|
releaseManagedWriteLock,
|
|
96
96
|
runManagedWritePreflight,
|
|
97
97
|
runManagedWriteTransaction
|
|
98
|
-
} from "./chunk-
|
|
98
|
+
} from "./chunk-4KGCTQM3.js";
|
|
99
99
|
import {
|
|
100
100
|
FLEET_REL_PATH,
|
|
101
101
|
git,
|
|
@@ -6758,7 +6758,7 @@ async function emitManagedVaultWrite(vault, command, mutate, opts) {
|
|
|
6758
6758
|
if (guard.blocked) {
|
|
6759
6759
|
return emit({ exitCode: guard.exitCode, result: guard.result }, void 0, { postCommit: false });
|
|
6760
6760
|
}
|
|
6761
|
-
const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-
|
|
6761
|
+
const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-57DQZI2N.js");
|
|
6762
6762
|
const run = await runManagedWriteTransaction2({
|
|
6763
6763
|
vault,
|
|
6764
6764
|
command,
|
package/dist/skillwiki-mcp.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# managed-write-lock.sh — shared CLI/shell managed-write lock (Bash 3.2).
|
|
3
3
|
# Source from vault-sync scripts. Lock path: git --git-path vault-sync/managed-write.lock
|
|
4
|
+
#
|
|
5
|
+
# Lifecycle:
|
|
6
|
+
# - Acquire uses noclobber create so concurrent live owners fail closed.
|
|
7
|
+
# - Release only removes the lock when this shell owns the token.
|
|
8
|
+
# - On acquire contention, a lock whose owner PID is dead may be reclaimed after
|
|
9
|
+
# preserving the old lock record under vault-sync/recovery/ — never by age alone,
|
|
10
|
+
# never while rebase/unmerged paths exist, never while a live PID holds it.
|
|
4
11
|
|
|
5
12
|
VAULT_SYNC_MANAGED_LOCK_PATH=""
|
|
6
13
|
VAULT_SYNC_MANAGED_LOCK_TOKEN_OWNED=""
|
|
@@ -23,11 +30,134 @@ vault_sync_managed_lock_read_token() {
|
|
|
23
30
|
sed -n 's/.*"owner_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$path" | head -1
|
|
24
31
|
}
|
|
25
32
|
|
|
33
|
+
vault_sync_managed_lock_read_pid() {
|
|
34
|
+
local path="$1"
|
|
35
|
+
[ -f "$path" ] || return 1
|
|
36
|
+
sed -n 's/.*"pid"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$path" | head -1
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Returns 0 when PID appears alive, 1 when dead/unknown/missing.
|
|
40
|
+
vault_sync_managed_lock_pid_alive() {
|
|
41
|
+
local pid="$1"
|
|
42
|
+
case "$pid" in
|
|
43
|
+
""|*[!0-9]*) return 1 ;;
|
|
44
|
+
esac
|
|
45
|
+
# kill -0 succeeds if the process exists (or is not owned but present).
|
|
46
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
47
|
+
return 0
|
|
48
|
+
fi
|
|
49
|
+
return 1
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Returns 0 when it is safe to reclaim a dead-owner lock for this repo.
|
|
53
|
+
vault_sync_managed_lock_safe_to_reclaim() {
|
|
54
|
+
local repo="${1:-.}"
|
|
55
|
+
local git_dir unmerged review_op
|
|
56
|
+
|
|
57
|
+
git_dir="$(git -C "$repo" rev-parse --git-dir 2>/dev/null)" || return 1
|
|
58
|
+
case "$git_dir" in
|
|
59
|
+
/*) ;;
|
|
60
|
+
*) git_dir="$repo/$git_dir" ;;
|
|
61
|
+
esac
|
|
62
|
+
|
|
63
|
+
# Never reclaim during an active/leftover sequencer.
|
|
64
|
+
if [ -d "$git_dir/rebase-merge" ] || [ -d "$git_dir/rebase-apply" ] \
|
|
65
|
+
|| [ -d "$git_dir/MERGE_HEAD" ] || [ -f "$git_dir/CHERRY_PICK_HEAD" ] \
|
|
66
|
+
|| [ -f "$git_dir/REVERT_HEAD" ]; then
|
|
67
|
+
return 1
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
unmerged="$(git -C "$repo" ls-files -u 2>/dev/null | head -1 || true)"
|
|
71
|
+
if [ -n "$unmerged" ]; then
|
|
72
|
+
return 1
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# When journal helpers are loaded, refuse reclaim while review-required handoff
|
|
76
|
+
# journals exist. Standalone lock tests may not source the journal library.
|
|
77
|
+
if command -v vault_sync_op_find_review_required >/dev/null 2>&1; then
|
|
78
|
+
review_op="$(vault_sync_op_find_review_required "$repo" 2>/dev/null || true)"
|
|
79
|
+
if [ -n "$review_op" ]; then
|
|
80
|
+
return 1
|
|
81
|
+
fi
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
return 0
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# Preserve the current lock file under vault-sync/recovery/ then remove it.
|
|
88
|
+
# Returns 0 when the live lock path is gone (preserved or already absent).
|
|
89
|
+
vault_sync_managed_lock_preserve_and_clear() {
|
|
90
|
+
local path="$1"
|
|
91
|
+
local reason="${2:-owner_pid_dead}"
|
|
92
|
+
local rec_dir rec_path ts body
|
|
93
|
+
|
|
94
|
+
[ -n "$path" ] || return 1
|
|
95
|
+
if [ ! -f "$path" ]; then
|
|
96
|
+
return 0
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
rec_dir="$(dirname "$path")/recovery"
|
|
100
|
+
mkdir -p "$rec_dir" || return 1
|
|
101
|
+
ts="$(date -u +%Y%m%dT%H%MZ 2>/dev/null || date +%Y%m%dT%H%M)"
|
|
102
|
+
rec_path="$rec_dir/stale-managed-write-lock-${ts}-$$.json"
|
|
103
|
+
body="$(cat "$path" 2>/dev/null || true)"
|
|
104
|
+
if [ -n "$body" ]; then
|
|
105
|
+
# Best-effort structured recovery record; fall back to raw bytes.
|
|
106
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
107
|
+
REASON="$reason" BODY="$body" REC="$rec_path" python3 - <<'PY' 2>/dev/null || printf '%s\n' "$body" >"$rec_path"
|
|
108
|
+
import json, os, time
|
|
109
|
+
raw = os.environ.get("BODY", "").strip()
|
|
110
|
+
try:
|
|
111
|
+
lock = json.loads(raw)
|
|
112
|
+
except Exception:
|
|
113
|
+
lock = {"raw": raw}
|
|
114
|
+
meta = {
|
|
115
|
+
"recovered_at": time.strftime("%Y-%m-%dT%H:%MZ", time.gmtime()),
|
|
116
|
+
"recovery_reason": os.environ.get("REASON", "owner_pid_dead"),
|
|
117
|
+
"owner_pid_alive": False,
|
|
118
|
+
"lock": lock,
|
|
119
|
+
}
|
|
120
|
+
open(os.environ["REC"], "w", encoding="utf-8").write(json.dumps(meta, indent=2) + "\n")
|
|
121
|
+
PY
|
|
122
|
+
else
|
|
123
|
+
printf '%s\n' "$body" >"$rec_path"
|
|
124
|
+
fi
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
rm -f -- "$path"
|
|
128
|
+
[ ! -f "$path" ]
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# If the lock path exists with a dead owner and reclaim is safe, preserve+clear.
|
|
132
|
+
# Returns 0 when lock path is free (reclaimed or never present), 1 when still held.
|
|
133
|
+
vault_sync_managed_lock_reclaim_dead_owner() {
|
|
134
|
+
local repo="${1:-.}"
|
|
135
|
+
local path pid
|
|
136
|
+
|
|
137
|
+
path="$(vault_sync_managed_lock_path "$repo")" || return 1
|
|
138
|
+
if [ ! -f "$path" ]; then
|
|
139
|
+
return 0
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
pid="$(vault_sync_managed_lock_read_pid "$path" || true)"
|
|
143
|
+
if vault_sync_managed_lock_pid_alive "$pid"; then
|
|
144
|
+
return 1
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
if ! vault_sync_managed_lock_safe_to_reclaim "$repo"; then
|
|
148
|
+
return 1
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
vault_sync_managed_lock_preserve_and_clear "$path" "owner_pid_dead" || return 1
|
|
152
|
+
return 0
|
|
153
|
+
}
|
|
154
|
+
|
|
26
155
|
# Acquire or adopt managed-write lock. Returns 0 on success, 1 on contention/mismatch.
|
|
27
156
|
vault_sync_managed_lock_acquire() {
|
|
28
157
|
local repo="${1:-.}"
|
|
29
158
|
local command="${2:-wiki-pull}"
|
|
30
|
-
local path token now inherited
|
|
159
|
+
local path token now inherited attempt
|
|
160
|
+
|
|
31
161
|
path="$(vault_sync_managed_lock_path "$repo")" || return 1
|
|
32
162
|
VAULT_SYNC_MANAGED_LOCK_PATH="$path"
|
|
33
163
|
mkdir -p "$(dirname "$path")" || return 1
|
|
@@ -46,16 +176,29 @@ vault_sync_managed_lock_acquire() {
|
|
|
46
176
|
return 1
|
|
47
177
|
fi
|
|
48
178
|
|
|
49
|
-
|
|
50
|
-
[
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
179
|
+
attempt=0
|
|
180
|
+
while [ "$attempt" -lt 2 ]; do
|
|
181
|
+
attempt=$((attempt + 1))
|
|
182
|
+
token="$(od -An -N16 -tx1 /dev/urandom 2>/dev/null | tr -d ' \n')"
|
|
183
|
+
[ -n "$token" ] || token="$$-$(date +%s)"
|
|
184
|
+
now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
185
|
+
if ( set -o noclobber; printf '{"pid":%s,"owner_token":"%s","acquired":"%s","command":"%s"}\n' \
|
|
186
|
+
"$$" "$token" "$now" "$command" >"$path" ) 2>/dev/null; then
|
|
187
|
+
VAULT_SYNC_MANAGED_LOCK_TOKEN_OWNED="$token"
|
|
188
|
+
VAULT_SYNC_MANAGED_LOCK_ACQUIRED="$now"
|
|
189
|
+
VAULT_SYNC_MANAGED_LOCK_OWNS_RELEASE=1
|
|
190
|
+
return 0
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
# Contention: only the first pass may reclaim a dead owner, then retry once.
|
|
194
|
+
if [ "$attempt" -eq 1 ]; then
|
|
195
|
+
if vault_sync_managed_lock_reclaim_dead_owner "$repo"; then
|
|
196
|
+
continue
|
|
197
|
+
fi
|
|
198
|
+
fi
|
|
199
|
+
return 1
|
|
200
|
+
done
|
|
201
|
+
|
|
59
202
|
return 1
|
|
60
203
|
}
|
|
61
204
|
|
|
@@ -71,7 +71,16 @@ release_pull_lock() {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
# Always release cooperative lock and owned managed-write lock on exit.
|
|
74
|
-
|
|
74
|
+
# INT/TERM re-enter via EXIT so SIGKILL-only gaps are handled by dead-owner
|
|
75
|
+
# reclaim on the next acquire (see managed-write-lock.sh).
|
|
76
|
+
vault_sync_pull_cleanup() {
|
|
77
|
+
release_pull_lock
|
|
78
|
+
vault_sync_managed_lock_release || true
|
|
79
|
+
}
|
|
80
|
+
trap vault_sync_pull_cleanup EXIT
|
|
81
|
+
trap 'exit 130' INT
|
|
82
|
+
trap 'exit 143' TERM
|
|
83
|
+
trap 'exit 129' HUP
|
|
75
84
|
|
|
76
85
|
acquire_pull_lock() {
|
|
77
86
|
local lock_rc=0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillwiki",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.7",
|
|
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": {
|