skillwiki 0.10.4 → 0.10.6
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-GEKJG4F7.js → chunk-4KGCTQM3.js} +90 -5
- package/dist/{chunk-Z3WS45PL.js → chunk-NJFCTMYZ.js} +81 -7
- package/dist/cli.js +19 -10
- 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
|
@@ -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) {
|
|
@@ -6999,6 +6999,61 @@ ${input.text.trim()}
|
|
|
6999
6999
|
import { createHash as createHash7 } from "crypto";
|
|
7000
7000
|
import { mkdir as mkdir6, readFile as readFile16, readdir as readdir4, stat as stat5, writeFile as writeFile5 } from "fs/promises";
|
|
7001
7001
|
import { basename as basename2, extname, join as join26, relative as relative3, sep as sep3 } from "path";
|
|
7002
|
+
|
|
7003
|
+
// src/utils/memory-authority.ts
|
|
7004
|
+
var TIER_RANK = {
|
|
7005
|
+
"accepted-decision": 0,
|
|
7006
|
+
"operational-guidance": 1,
|
|
7007
|
+
proposed: 2,
|
|
7008
|
+
exploratory: 3,
|
|
7009
|
+
unclassified: 4
|
|
7010
|
+
};
|
|
7011
|
+
function memoryAuthorityTiersRank(tier) {
|
|
7012
|
+
if (!tier) return TIER_RANK.unclassified;
|
|
7013
|
+
const key = String(tier).toLowerCase();
|
|
7014
|
+
return TIER_RANK[key] ?? TIER_RANK.unclassified;
|
|
7015
|
+
}
|
|
7016
|
+
function classifyMemoryAuthority(source) {
|
|
7017
|
+
const kind = (source.memory_kind ?? "").toLowerCase();
|
|
7018
|
+
const policy = (source.memory_policy ?? "").toLowerCase();
|
|
7019
|
+
const status = (source.memory_status ?? "").toLowerCase();
|
|
7020
|
+
if (kind === "decision-context" && policy === "operational" && status === "active") {
|
|
7021
|
+
return "accepted-decision";
|
|
7022
|
+
}
|
|
7023
|
+
if (policy === "operational" && status === "active") {
|
|
7024
|
+
return "operational-guidance";
|
|
7025
|
+
}
|
|
7026
|
+
if (["proposed", "draft", "pending"].includes(status) || ["proposed", "hypothesis", "research"].includes(policy)) {
|
|
7027
|
+
return "proposed";
|
|
7028
|
+
}
|
|
7029
|
+
if (policy === "exploratory") {
|
|
7030
|
+
return "exploratory";
|
|
7031
|
+
}
|
|
7032
|
+
return "unclassified";
|
|
7033
|
+
}
|
|
7034
|
+
function isProjectLocal(source, project) {
|
|
7035
|
+
if (!project) return false;
|
|
7036
|
+
if (typeof source.path === "string" && source.path.startsWith(`projects/${project}/`)) {
|
|
7037
|
+
return true;
|
|
7038
|
+
}
|
|
7039
|
+
const scope = (source.memory_scope || "project").toLowerCase();
|
|
7040
|
+
if (scope !== "project") return false;
|
|
7041
|
+
return !source.project || source.project === project;
|
|
7042
|
+
}
|
|
7043
|
+
function compareMemoryAuthority(a, b, options = {}) {
|
|
7044
|
+
const tierDiff = memoryAuthorityTiersRank(classifyMemoryAuthority(a)) - memoryAuthorityTiersRank(classifyMemoryAuthority(b));
|
|
7045
|
+
if (tierDiff !== 0) return tierDiff;
|
|
7046
|
+
if (options.preferProjectWithinTiers && options.project) {
|
|
7047
|
+
const aLocal = Number(isProjectLocal(a, options.project));
|
|
7048
|
+
const bLocal = Number(isProjectLocal(b, options.project));
|
|
7049
|
+
if (aLocal !== bLocal) return bLocal - aLocal;
|
|
7050
|
+
}
|
|
7051
|
+
const updatedDiff = b.updated.localeCompare(a.updated);
|
|
7052
|
+
if (updatedDiff !== 0) return updatedDiff;
|
|
7053
|
+
return a.path.localeCompare(b.path);
|
|
7054
|
+
}
|
|
7055
|
+
|
|
7056
|
+
// src/commands/memory.ts
|
|
7002
7057
|
async function runMemoryTopics(input) {
|
|
7003
7058
|
const scan = await scanVault(input.vault);
|
|
7004
7059
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
@@ -7825,12 +7880,16 @@ function normalizeTopics(value) {
|
|
|
7825
7880
|
const paths = stringArray(item.paths);
|
|
7826
7881
|
if (!name || !summary || !updated || paths.length === 0) continue;
|
|
7827
7882
|
const project = stringField(item.project);
|
|
7883
|
+
const authority_tier = stringField(item.authority_tier);
|
|
7884
|
+
const lead_path = stringField(item.lead_path);
|
|
7828
7885
|
topics.push({
|
|
7829
7886
|
name,
|
|
7830
7887
|
summary,
|
|
7831
7888
|
...project ? { project } : {},
|
|
7832
7889
|
updated,
|
|
7833
|
-
paths
|
|
7890
|
+
paths,
|
|
7891
|
+
...authority_tier ? { authority_tier } : {},
|
|
7892
|
+
...lead_path ? { lead_path } : {}
|
|
7834
7893
|
});
|
|
7835
7894
|
}
|
|
7836
7895
|
return topics;
|
|
@@ -7870,10 +7929,14 @@ function normalizeLimit(value) {
|
|
|
7870
7929
|
return Math.floor(value);
|
|
7871
7930
|
}
|
|
7872
7931
|
function compareTopics(a, b) {
|
|
7932
|
+
if (a.authority_tier || b.authority_tier) {
|
|
7933
|
+
const tr = memoryAuthorityTiersRank(a.authority_tier) - memoryAuthorityTiersRank(b.authority_tier);
|
|
7934
|
+
if (tr !== 0) return tr;
|
|
7935
|
+
}
|
|
7873
7936
|
return b.updated.localeCompare(a.updated) || a.name.localeCompare(b.name);
|
|
7874
7937
|
}
|
|
7875
7938
|
function compareSources(a, b) {
|
|
7876
|
-
return
|
|
7939
|
+
return compareMemoryAuthority(a, b);
|
|
7877
7940
|
}
|
|
7878
7941
|
function normalizeRecallScope(value) {
|
|
7879
7942
|
if (!value) return void 0;
|
|
@@ -7891,8 +7954,13 @@ function recallScopeMatches(source, project, scope) {
|
|
|
7891
7954
|
return source.memory_scope === scope;
|
|
7892
7955
|
}
|
|
7893
7956
|
function compareRecallSources(a, b, project, scope) {
|
|
7894
|
-
if (scope
|
|
7895
|
-
|
|
7957
|
+
if (scope === "all") {
|
|
7958
|
+
return compareMemoryAuthority(a, b, {
|
|
7959
|
+
project,
|
|
7960
|
+
preferProjectWithinTiers: true
|
|
7961
|
+
});
|
|
7962
|
+
}
|
|
7963
|
+
return compareSources(a, b);
|
|
7896
7964
|
}
|
|
7897
7965
|
function isProjectMemorySource(source, project) {
|
|
7898
7966
|
const scope = source.memory_scope || "project";
|
|
@@ -7972,12 +8040,17 @@ function buildTopics(project, sources) {
|
|
|
7972
8040
|
}
|
|
7973
8041
|
return [...byTopic.entries()].map(([name, topicSources]) => {
|
|
7974
8042
|
const sorted = [...topicSources].sort(compareSources);
|
|
8043
|
+
const lead = sorted[0];
|
|
7975
8044
|
return {
|
|
7976
8045
|
name,
|
|
7977
8046
|
project,
|
|
7978
|
-
summary:
|
|
7979
|
-
updated:
|
|
7980
|
-
paths: sorted.map((source) => source.path)
|
|
8047
|
+
summary: lead?.summary ?? "",
|
|
8048
|
+
updated: lead?.updated ?? "",
|
|
8049
|
+
paths: sorted.map((source) => source.path),
|
|
8050
|
+
...lead ? {
|
|
8051
|
+
authority_tier: classifyMemoryAuthority(lead),
|
|
8052
|
+
lead_path: lead.path
|
|
8053
|
+
} : {}
|
|
7981
8054
|
};
|
|
7982
8055
|
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
7983
8056
|
}
|
|
@@ -9213,6 +9286,7 @@ export {
|
|
|
9213
9286
|
runDoctor,
|
|
9214
9287
|
readCliPackageJson,
|
|
9215
9288
|
runObserve,
|
|
9289
|
+
memoryAuthorityTiersRank,
|
|
9216
9290
|
runMemoryTopics,
|
|
9217
9291
|
runMemoryIndex,
|
|
9218
9292
|
runMemoryRecall,
|
package/dist/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
getSessionId,
|
|
19
19
|
isFailedRunStatus,
|
|
20
20
|
isValidRemoteDeleteCap,
|
|
21
|
+
memoryAuthorityTiersRank,
|
|
21
22
|
mergeTaxonomyConflict,
|
|
22
23
|
normalizeRemoteRoot,
|
|
23
24
|
planAndMaybePruneRemoteObjects,
|
|
@@ -68,7 +69,7 @@ import {
|
|
|
68
69
|
satelliteLatestRunPath,
|
|
69
70
|
taxonomyCommentForPage,
|
|
70
71
|
upsertIndexEntry
|
|
71
|
-
} from "./chunk-
|
|
72
|
+
} from "./chunk-NJFCTMYZ.js";
|
|
72
73
|
import {
|
|
73
74
|
normalizeDistTag,
|
|
74
75
|
readCache,
|
|
@@ -94,7 +95,7 @@ import {
|
|
|
94
95
|
releaseManagedWriteLock,
|
|
95
96
|
runManagedWritePreflight,
|
|
96
97
|
runManagedWriteTransaction
|
|
97
|
-
} from "./chunk-
|
|
98
|
+
} from "./chunk-4KGCTQM3.js";
|
|
98
99
|
import {
|
|
99
100
|
FLEET_REL_PATH,
|
|
100
101
|
git,
|
|
@@ -3909,13 +3910,21 @@ async function loadMemoryTopics(vault, project) {
|
|
|
3909
3910
|
try {
|
|
3910
3911
|
const parsed = JSON.parse(text);
|
|
3911
3912
|
if (!Array.isArray(parsed.topics)) return [];
|
|
3912
|
-
return parsed.topics.filter((topic) => typeof topic === "object" && topic !== null && !Array.isArray(topic)).map((topic) =>
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3913
|
+
return parsed.topics.filter((topic) => typeof topic === "object" && topic !== null && !Array.isArray(topic)).map((topic) => {
|
|
3914
|
+
const authority_tier = stringField(topic.authority_tier);
|
|
3915
|
+
const lead_path = stringField(topic.lead_path);
|
|
3916
|
+
return {
|
|
3917
|
+
name: stringField(topic.name),
|
|
3918
|
+
summary: stringField(topic.summary),
|
|
3919
|
+
project: stringField(topic.project) || void 0,
|
|
3920
|
+
updated: stringField(topic.updated),
|
|
3921
|
+
paths: Array.isArray(topic.paths) ? topic.paths.filter((path) => typeof path === "string") : [],
|
|
3922
|
+
...authority_tier ? { authority_tier } : {},
|
|
3923
|
+
...lead_path ? { lead_path } : {}
|
|
3924
|
+
};
|
|
3925
|
+
}).filter((topic) => topic.name && topic.summary && topic.updated && topic.paths.length > 0).sort(
|
|
3926
|
+
(a, b) => memoryAuthorityTiersRank(a.authority_tier) - memoryAuthorityTiersRank(b.authority_tier) || b.updated.localeCompare(a.updated) || a.name.localeCompare(b.name)
|
|
3927
|
+
).slice(0, 5);
|
|
3919
3928
|
} catch {
|
|
3920
3929
|
return [];
|
|
3921
3930
|
}
|
|
@@ -6749,7 +6758,7 @@ async function emitManagedVaultWrite(vault, command, mutate, opts) {
|
|
|
6749
6758
|
if (guard.blocked) {
|
|
6750
6759
|
return emit({ exitCode: guard.exitCode, result: guard.result }, void 0, { postCommit: false });
|
|
6751
6760
|
}
|
|
6752
|
-
const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-
|
|
6761
|
+
const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-57DQZI2N.js");
|
|
6753
6762
|
const run = await runManagedWriteTransaction2({
|
|
6754
6763
|
vault,
|
|
6755
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.6",
|
|
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": {
|