skillwiki 0.10.48 → 0.10.50

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.
@@ -6,7 +6,7 @@ import {
6
6
  import {
7
7
  renderRootIndex,
8
8
  writeRootIndexProjection
9
- } from "./chunk-GAU4D25O.js";
9
+ } from "./chunk-O3HCB7R2.js";
10
10
  import {
11
11
  CONFIG_KEYS,
12
12
  acquireOwnedSyncLock,
@@ -35,15 +35,28 @@ import {
35
35
  snapshotterAliasForLocalHost,
36
36
  toUndirectedWeighted,
37
37
  writeDotenv
38
- } from "./chunk-FXASPB3C.js";
38
+ } from "./chunk-EUTNN4RN.js";
39
39
  import {
40
40
  atomicWriteText,
41
41
  prepareTypedPage
42
- } from "./chunk-UHZBOOMK.js";
42
+ } from "./chunk-74OSLCXE.js";
43
+ import {
44
+ applySourceCompileClaim,
45
+ applySourceCompilePublished,
46
+ applySourceCompileRelease,
47
+ applySourceReview,
48
+ listCompileStatus,
49
+ listSourceReviews,
50
+ planSourceCompileClaim,
51
+ planSourceCompilePublished,
52
+ planSourceCompileRelease,
53
+ planSourceReview,
54
+ runSourcesPending
55
+ } from "./chunk-HBQTTYXZ.js";
43
56
  import {
44
57
  eventPathFor,
45
58
  writeLogEvent
46
- } from "./chunk-PLUHIOCQ.js";
59
+ } from "./chunk-GAHMWLWU.js";
47
60
  import {
48
61
  CompoundSchema,
49
62
  ExitCode,
@@ -64,7 +77,7 @@ import {
64
77
  scanVault,
65
78
  splitFrontmatter,
66
79
  systemdPropertyFor
67
- } from "./chunk-EVQASYII.js";
80
+ } from "./chunk-IJ7DD7QZ.js";
68
81
 
69
82
  // src/commands/log-append.ts
70
83
  import { readFile, stat } from "fs/promises";
@@ -1056,11 +1069,13 @@ async function runConfigPath(input) {
1056
1069
  return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync2(filePath), humanHint: filePath }) };
1057
1070
  }
1058
1071
 
1059
- // src/commands/doctor.ts
1060
- import { existsSync as existsSync7, lstatSync, readlinkSync, readdirSync as readdirSync3, statSync as statSync2, readFileSync as readFileSync6 } from "fs";
1061
- import { join as join10, resolve as resolve2 } from "path";
1062
- import { execSync as execSync2 } from "child_process";
1063
- import { platform as platform2 } from "os";
1072
+ // src/doctor/runner.ts
1073
+ import { existsSync as existsSync17 } from "fs";
1074
+
1075
+ // src/doctor/probes/environment.ts
1076
+ import { existsSync as existsSync4, lstatSync, readlinkSync } from "fs";
1077
+ import { resolve as resolve2, join as join7 } from "path";
1078
+ import { execSync } from "child_process";
1064
1079
 
1065
1080
  // src/utils/plugin-registry.ts
1066
1081
  import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2 } from "fs";
@@ -1180,792 +1195,396 @@ function parseTomlScalar(rawValue) {
1180
1195
  return value;
1181
1196
  }
1182
1197
 
1183
- // src/utils/conflict-markers.ts
1184
- import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
1185
- import { join as join7 } from "path";
1186
- function scanConflictMarkerBlocksInText(relPath, text) {
1187
- const findings = [];
1188
- const lines = text.split(/\r?\n/);
1189
- let inFence = false;
1190
- let openLine = 0;
1191
- let sawSeparator = false;
1192
- for (let i = 0; i < lines.length; i += 1) {
1193
- const line = lines[i];
1194
- if (line.startsWith("```") || line.startsWith("~~~")) {
1195
- inFence = !inFence;
1196
- continue;
1197
- }
1198
- if (inFence) continue;
1199
- if (line.startsWith("<<<<<<< ")) {
1200
- openLine = i + 1;
1201
- sawSeparator = false;
1202
- continue;
1203
- }
1204
- if (line === "=======" && openLine > 0) {
1205
- sawSeparator = true;
1206
- continue;
1207
- }
1208
- if (line.startsWith(">>>>>>> ")) {
1209
- if (openLine > 0 && sawSeparator) {
1210
- findings.push({ path: relPath, line: openLine });
1211
- }
1212
- openLine = 0;
1213
- sawSeparator = false;
1214
- }
1198
+ // src/doctor/probes/helpers.ts
1199
+ function check(status, id, label, detail) {
1200
+ return { id, label, status, detail };
1201
+ }
1202
+
1203
+ // src/doctor/probes/environment.ts
1204
+ function checkNodeVersion() {
1205
+ const major = parseInt(process.version.slice(1).split(".")[0], 10);
1206
+ if (major >= 20) {
1207
+ return check("pass", "node_version", "Node.js version", `v${major} >= 20`);
1215
1208
  }
1216
- return findings;
1209
+ return check("error", "node_version", "Node.js version", `Node.js v${major} is below minimum v20`);
1217
1210
  }
1218
- var PRUNE_DIRS = /* @__PURE__ */ new Set([
1219
- ".git",
1220
- ".obsidian",
1221
- ".skillwiki",
1222
- ".claude",
1223
- ".antigravitycli",
1224
- ".playwright-cli"
1225
- ]);
1226
- function walkMarkdownFiles(root, dir, rel, out) {
1227
- let entries;
1211
+ function detectCliChannels(argv, home) {
1212
+ const channels = [];
1213
+ if (argv.length >= 2 && argv[1].endsWith("cli.js")) {
1214
+ const devPath = resolve2(argv[1]);
1215
+ channels.push({ name: "dev", path: devPath, isDevLink: true });
1216
+ }
1228
1217
  try {
1229
- entries = readdirSync2(dir, { withFileTypes: true });
1218
+ const whichOut = execSync("which skillwiki 2>/dev/null", { encoding: "utf8" }).trim();
1219
+ if (whichOut) {
1220
+ const isDev = isDevSymlink(whichOut);
1221
+ if (!channels.some((c) => c.path === resolve2(whichOut))) {
1222
+ channels.push({ name: "npm", path: whichOut, isDevLink: isDev });
1223
+ }
1224
+ }
1230
1225
  } catch {
1231
- return;
1232
1226
  }
1233
- for (const entry of entries) {
1234
- if (entry.isDirectory()) {
1235
- if (PRUNE_DIRS.has(entry.name)) continue;
1236
- walkMarkdownFiles(root, join7(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name, out);
1237
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
1238
- out.push(rel ? `${rel}/${entry.name}` : entry.name);
1227
+ const plugin = findPlugin(home);
1228
+ if (plugin) {
1229
+ const pluginBin = join7(plugin.installPath, "bin", "skillwiki");
1230
+ if (existsSync4(pluginBin)) {
1231
+ channels.push({ name: "plugin", path: pluginBin, isDevLink: false });
1239
1232
  }
1240
1233
  }
1241
- }
1242
- function scanVaultConflictMarkers(vaultRoot) {
1243
- if (!existsSync4(vaultRoot)) return [];
1244
- const relPaths = [];
1245
- walkMarkdownFiles(vaultRoot, vaultRoot, "", relPaths);
1246
- const all = [];
1247
- for (const rel of relPaths) {
1248
- let text;
1249
- try {
1250
- text = readFileSync3(join7(vaultRoot, rel), "utf8");
1251
- } catch {
1252
- continue;
1253
- }
1254
- all.push(...scanConflictMarkerBlocksInText(rel, text));
1234
+ const installBin = join7(home, ".claude", "skills", "bin", "skillwiki");
1235
+ if (existsSync4(installBin)) {
1236
+ channels.push({ name: "install", path: installBin, isDevLink: false });
1255
1237
  }
1256
- return all;
1257
- }
1258
-
1259
- // src/utils/satellite-run-health.ts
1260
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1261
- import { join as join8 } from "path";
1262
- var SATELLITE_STALE_MS = 26 * 60 * 60 * 1e3;
1263
- function satelliteLatestRunPath(vault) {
1264
- return join8(vault, ".skillwiki", "agent-memory-trends", "latest-run.json");
1265
- }
1266
- function isFailedRunStatus(status) {
1267
- return status === "fail" || status === "failure";
1238
+ return channels;
1268
1239
  }
1269
- function parseLatestRunFile(text) {
1240
+ function isDevSymlink(binPath) {
1270
1241
  try {
1271
- const parsed = JSON.parse(text);
1272
- const status = typeof parsed.status === "string" ? parsed.status : "";
1273
- if (!status) return null;
1274
- const finishedAt = typeof parsed.finished_at === "string" && parsed.finished_at.length > 0 ? parsed.finished_at : void 0;
1275
- const failureClass = parsed.failure_class != null && String(parsed.failure_class).length > 0 ? String(parsed.failure_class) : void 0;
1276
- return { status, finishedAt, failureClass };
1242
+ const st = lstatSync(binPath);
1243
+ if (st.isSymbolicLink()) {
1244
+ const target = resolve2(binPath, "..", readlinkSync(binPath));
1245
+ return target.includes("packages/cli") || target.includes("packages\\cli");
1246
+ }
1277
1247
  } catch {
1278
- return null;
1279
1248
  }
1249
+ return false;
1280
1250
  }
1281
- function readSatelliteLatestRunFromText(text) {
1282
- return parseLatestRunFile(text);
1283
- }
1284
- function readSatelliteLatestRun(vault) {
1285
- const latestPath = satelliteLatestRunPath(vault);
1286
- if (!existsSync5(latestPath)) return null;
1287
- try {
1288
- return parseLatestRunFile(readFileSync4(latestPath, "utf8"));
1289
- } catch {
1290
- return null;
1251
+ function checkCliChannels(argv, home) {
1252
+ const channels = detectCliChannels(argv, home);
1253
+ if (channels.length === 0) {
1254
+ return check("warn", "cli_channels", "CLI channels", "skillwiki not found on any channel");
1291
1255
  }
1292
- }
1293
- function evaluateSatelliteRunHealth(vault, now) {
1294
- const run = readSatelliteLatestRun(vault);
1295
- if (!run) {
1296
- return { failed: false, stale: false };
1256
+ if (channels.length === 1) {
1257
+ const ch = channels[0];
1258
+ const label = ch.isDevLink ? `${ch.name} (dev source)` : ch.name;
1259
+ return check("pass", "cli_channels", "CLI channels", `Single channel: ${label}`);
1297
1260
  }
1298
- const failed = isFailedRunStatus(run.status);
1299
- let stale = false;
1300
- if (!failed && run.finishedAt) {
1301
- const ts = Date.parse(run.finishedAt);
1302
- if (Number.isFinite(ts) && now.getTime() - ts > SATELLITE_STALE_MS) {
1303
- stale = true;
1261
+ const devChannels = channels.filter((c) => c.isDevLink);
1262
+ const prodChannels = channels.filter((c) => !c.isDevLink);
1263
+ if (devChannels.length > 0 && prodChannels.length > 0) {
1264
+ const hasInstall2 = prodChannels.some((c) => c.name === "install");
1265
+ if (!hasInstall2) {
1266
+ const devNames2 = devChannels.map((c) => `${c.name}(dev)`);
1267
+ const prodNames2 = prodChannels.map((c) => c.name);
1268
+ return check("pass", "cli_channels", "CLI channels", `${channels.length} channels: ${[...devNames2, ...prodNames2].join(", ")} \u2014 dev source with installed production channels`);
1304
1269
  }
1270
+ const devNames = devChannels.map((c) => `${c.name}(dev)`);
1271
+ const prodNames = prodChannels.map((c) => c.name);
1272
+ return check(
1273
+ "warn",
1274
+ "cli_channels",
1275
+ "CLI channels",
1276
+ `${channels.length} channels: ${[...devNames, ...prodNames].join(", ")} \u2014 dev and prod binaries overlap; dev repo should use project-local settings only`
1277
+ );
1305
1278
  }
1306
- return {
1307
- failed,
1308
- stale,
1309
- failureClass: run.failureClass,
1310
- finishedAt: run.finishedAt
1311
- };
1279
+ const names = channels.map((c) => c.name);
1280
+ const hasInstall = channels.some((c) => c.name === "install");
1281
+ if (hasInstall) {
1282
+ return check(
1283
+ "warn",
1284
+ "cli_channels",
1285
+ "CLI channels",
1286
+ `${channels.length} channels: ${names.join(", ")} \u2014 remove unused install with: rm ~/.claude/skills/bin/skillwiki`
1287
+ );
1288
+ }
1289
+ return check("pass", "cli_channels", "CLI channels", `${channels.length} channels: ${names.join(", ")}`);
1312
1290
  }
1313
-
1314
- // src/utils/s3-mount-health.ts
1315
- import { execSync } from "child_process";
1316
- import { platform } from "os";
1317
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, readFileSync as readFile6 } from "fs";
1318
- import { join as join9 } from "path";
1319
- var OS = platform();
1320
- function findRcloneMountPid() {
1291
+ async function checkConfigFile(home) {
1292
+ const cfgPath = configPath(home);
1293
+ if (!existsSync4(cfgPath)) {
1294
+ return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
1295
+ }
1321
1296
  try {
1322
- const out = execSync("pgrep -f 'rclone.*mount'", {
1323
- encoding: "utf8",
1324
- timeout: 2e3,
1325
- stdio: ["pipe", "pipe", "pipe"]
1326
- }).trim();
1327
- const pids = out.split("\n").filter(Boolean);
1328
- if (pids.length === 0) return null;
1329
- return parseInt(pids[0], 10);
1330
- } catch {
1331
- try {
1332
- const out = execSync("ps aux", { encoding: "utf8", timeout: 2e3, stdio: ["pipe", "pipe", "pipe"] });
1333
- for (const line of out.split("\n")) {
1334
- if (line.includes("rclone") && line.includes("mount") && !line.includes("grep")) {
1335
- const parts = line.trim().split(/\s+/);
1336
- if (parts.length >= 2) return parseInt(parts[1], 10);
1337
- }
1338
- }
1339
- } catch {
1340
- }
1341
- return null;
1297
+ const map = await parseDotenvFile(cfgPath);
1298
+ const keys = Object.keys(map);
1299
+ return check("pass", "config_file", "Config file exists", `Found with keys: ${keys.length > 0 ? keys.join(", ") : "(none set)"}`);
1300
+ } catch (e) {
1301
+ return check("warn", "config_file", "Config file exists", `Failed to parse ${cfgPath}: ${String(e)}`);
1342
1302
  }
1343
1303
  }
1344
- function parseRcloneFlags(pid) {
1345
- const flags = /* @__PURE__ */ new Map();
1346
- try {
1347
- const args = getRcloneArgs(pid);
1348
- for (let i = 0; i < args.length; i++) {
1349
- const arg = args[i];
1350
- if (arg.startsWith("--") && arg.includes("=")) {
1351
- const eq = arg.indexOf("=");
1352
- flags.set(arg.slice(0, eq), arg.slice(eq + 1));
1353
- } else if (arg.startsWith("--")) {
1354
- const next = args[i + 1];
1355
- if (next && !next.startsWith("-")) {
1356
- flags.set(arg, next);
1357
- i++;
1358
- } else {
1359
- flags.set(arg, "");
1360
- }
1361
- }
1304
+ async function checkProfiles(home) {
1305
+ const map = await parseDotenvFile(configPath(home));
1306
+ const profiles = [];
1307
+ for (const key of Object.keys(map)) {
1308
+ if (key.startsWith("WIKI_") && key.endsWith("_PATH") && key !== "WIKI_PATH") {
1309
+ const name = key.slice(5, -5).toLowerCase().replace(/_/g, "-");
1310
+ profiles.push(name);
1362
1311
  }
1363
- } catch {
1364
1312
  }
1365
- return flags;
1366
- }
1367
- function getRcloneVersion() {
1368
- try {
1369
- const out = execSync("rclone version", {
1370
- encoding: "utf8",
1371
- timeout: 3e3,
1372
- stdio: ["pipe", "pipe", "pipe"]
1373
- });
1374
- const match = out.match(/rclone\s+v(\d+)\.(\d+)\.(\d+)/i);
1375
- if (!match) return null;
1376
- return {
1377
- major: parseInt(match[1], 10),
1378
- minor: parseInt(match[2], 10),
1379
- patch: parseInt(match[3], 10),
1380
- raw: out.split("\n")[0].trim()
1381
- };
1382
- } catch {
1383
- return null;
1313
+ if (profiles.length === 0) {
1314
+ return check("pass", "wiki_profiles", "Wiki profiles", "No named profiles configured");
1384
1315
  }
1316
+ const defaultProfile = map["WIKI_DEFAULT"] ?? "(none)";
1317
+ return check(
1318
+ "pass",
1319
+ "wiki_profiles",
1320
+ "Wiki profiles",
1321
+ `${profiles.length} profile(s): ${profiles.join(", ")}; default: ${defaultProfile}`
1322
+ );
1385
1323
  }
1386
- function extractRcloneFs(args) {
1387
- let foundMount = false;
1388
- for (const arg of args) {
1389
- if (arg === "mount") {
1390
- foundMount = true;
1391
- continue;
1392
- }
1393
- if (foundMount && arg.includes(":") && !arg.startsWith("-") && !arg.startsWith("/")) {
1394
- return arg;
1395
- }
1324
+ async function checkProjectLocalOverride(cwd) {
1325
+ const dir = cwd ?? process.cwd();
1326
+ const envPath = join7(dir, ".skillwiki", ".env");
1327
+ if (existsSync4(envPath)) {
1328
+ return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
1396
1329
  }
1397
- return null;
1330
+ return check("pass", "project_local", "Project-local config", "None");
1398
1331
  }
1399
- function getRcloneArgs(pid) {
1400
- try {
1401
- if (OS === "linux") {
1402
- const raw = readFileSync5(`/proc/${pid}/cmdline`);
1403
- return new TextDecoder().decode(raw).split("\0").filter(Boolean);
1404
- } else {
1405
- const out = execSync(`ps -o args= -p ${pid}`, {
1406
- encoding: "utf8",
1407
- timeout: 2e3,
1408
- stdio: ["pipe", "pipe", "pipe"]
1409
- }).trim();
1410
- return out.split(/\s+/);
1411
- }
1412
- } catch {
1413
- return [];
1332
+ function checkWikiPathSet(ctx) {
1333
+ if (ctx.resolvedPath) {
1334
+ return check("pass", "wiki_path_set", "WIKI_PATH configured", `Resolved via ${ctx.wikiPathSource ?? "unknown"}: ${ctx.resolvedPath}`);
1414
1335
  }
1336
+ return check("error", "wiki_path_set", "WIKI_PATH configured", "No vault configured. Run `skillwiki init` or pass --vault.");
1415
1337
  }
1416
- function queryRcloneRC(rcAddr, fs) {
1417
- try {
1418
- const payload = JSON.stringify({ fs });
1419
- const out = execSync(
1420
- `curl -s --max-time 3 -X POST "http://${rcAddr}/vfs/stats" -H "Content-Type: application/json" -d '${payload}' 2>/dev/null`,
1421
- { encoding: "utf8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }
1422
- );
1423
- if (!out.trim()) return null;
1424
- const data = JSON.parse(out);
1425
- if (data.status && data.status >= 400) {
1426
- return { error: data.error || `RC error (status ${data.status})`, erroredFiles: 0, uploadsInProgress: 0, uploadsQueued: 0, outOfSpace: false, bytesUsed: 0, files: 0, totalSize: "unknown" };
1427
- }
1428
- const dc = data.diskCache || {};
1429
- return {
1430
- erroredFiles: dc.erroredFiles ?? 0,
1431
- uploadsInProgress: dc.uploadsInProgress ?? 0,
1432
- uploadsQueued: dc.uploadsQueued ?? 0,
1433
- outOfSpace: dc.outOfSpace ?? false,
1434
- bytesUsed: dc.bytesUsed ?? 0,
1435
- files: dc.files ?? 0,
1436
- totalSize: data.totalSize || "unknown"
1437
- };
1438
- } catch {
1439
- return { error: "RC endpoint unreachable", erroredFiles: 0, uploadsInProgress: 0, uploadsQueued: 0, outOfSpace: false, bytesUsed: 0, files: 0, totalSize: "unknown" };
1338
+ var environmentProbe = {
1339
+ id: "environment",
1340
+ async run(ctx) {
1341
+ return [
1342
+ checkNodeVersion(),
1343
+ checkCliChannels(ctx.input.argv, ctx.input.home),
1344
+ await checkConfigFile(ctx.input.home),
1345
+ await checkProfiles(ctx.input.home),
1346
+ await checkProjectLocalOverride(ctx.input.cwd),
1347
+ checkWikiPathSet(ctx)
1348
+ ];
1440
1349
  }
1441
- }
1442
- function detectFuseMount(vaultPath) {
1443
- try {
1444
- if (OS === "linux") {
1445
- const mounts = readFileSync5("/proc/mounts", "utf8");
1446
- let best = null;
1447
- for (const line of mounts.split("\n")) {
1448
- const parts = line.split(" ");
1449
- if (parts.length < 3) continue;
1450
- const point = parts[1];
1451
- const fs = parts[2];
1452
- if (vaultPath.startsWith(point) && (!best || point.length > best.point.length)) {
1453
- best = { point, fs };
1454
- }
1455
- }
1456
- if (best && best.fs.includes("fuse")) return { mountPoint: best.point, fsType: best.fs };
1457
- } else if (OS === "darwin") {
1458
- const out = execSync("mount", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
1459
- let best = null;
1460
- for (const line of out.split("\n")) {
1461
- const match = line.match(/^(\S+) on (\S+) \((.*?)\)/);
1462
- if (!match) continue;
1463
- const point = match[2];
1464
- const opts = match[3];
1465
- if (opts.includes("fuse") && vaultPath.startsWith(point) && (!best || point.length > best.point.length)) {
1466
- best = { point, fsType: `fuse.${match[1].split(":")[0] || "unknown"}` };
1467
- }
1468
- }
1469
- if (best) return { mountPoint: best.point, fsType: best.fsType };
1470
- }
1471
- } catch {
1350
+ };
1351
+
1352
+ // src/doctor/probes/vault-structure.ts
1353
+ import { existsSync as existsSync5, statSync as statSync2 } from "fs";
1354
+ import { join as join8 } from "path";
1355
+ function checkWikiPathExists(resolvedPath) {
1356
+ if (resolvedPath === void 0) {
1357
+ return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
1472
1358
  }
1473
- return null;
1359
+ if (existsSync5(resolvedPath) && statSync2(resolvedPath).isDirectory()) {
1360
+ return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
1361
+ }
1362
+ return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
1474
1363
  }
1475
- function writeTest(dir) {
1476
- const testFile = join9(dir, `.doctor-write-test-${process.pid}.tmp`);
1477
- const payload = `skillwiki doctor write test \u2014 ${Date.now()} \u2014 ${Math.random().toString(36).slice(2)}`;
1478
- const start = Date.now();
1479
- try {
1480
- writeFileSync2(testFile, payload, "utf8");
1481
- } catch (e) {
1482
- return { success: false, writeMs: Date.now() - start, readMs: 0, size: 0, error: `write failed: ${e.message}` };
1364
+ function checkVaultStructure(resolvedPath) {
1365
+ if (resolvedPath === void 0) {
1366
+ return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
1483
1367
  }
1484
- const writeMs = Date.now() - start;
1485
- const readStart = Date.now();
1486
- try {
1487
- const back = readFile6(testFile, "utf8");
1488
- const readMs = Date.now() - readStart;
1489
- if (back !== payload) {
1490
- try {
1491
- unlinkSync2(testFile);
1492
- } catch {
1493
- }
1494
- return { success: false, writeMs, readMs, size: Buffer.byteLength(payload, "utf8"), error: "content mismatch \u2014 wrote and read-back differ" };
1495
- }
1496
- } catch (e) {
1497
- try {
1498
- unlinkSync2(testFile);
1499
- } catch {
1500
- }
1501
- return { success: false, writeMs, readMs: Date.now() - readStart, size: 0, error: `read failed: ${e.message}` };
1368
+ if (!existsSync5(resolvedPath)) {
1369
+ return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
1502
1370
  }
1503
- try {
1504
- unlinkSync2(testFile);
1505
- } catch {
1371
+ const missing = [];
1372
+ if (!existsSync5(join8(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
1373
+ for (const dir of ["raw", "entities", "concepts", "meta"]) {
1374
+ if (!existsSync5(join8(resolvedPath, dir))) missing.push(dir + "/");
1506
1375
  }
1507
- return { success: true, writeMs, readMs: Date.now() - readStart, size: Buffer.byteLength(payload, "utf8") };
1376
+ if (missing.length === 0) {
1377
+ return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
1378
+ }
1379
+ return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
1508
1380
  }
1509
- var DURATION_UNIT_SECONDS = {
1510
- ms: 1 / 1e3,
1511
- s: 1,
1512
- m: 60,
1513
- h: 3600,
1514
- d: 86400,
1515
- w: 604800
1516
- };
1517
- function parseDurationSeconds(raw) {
1518
- const input = raw.trim().toLowerCase();
1519
- if (!input) return null;
1520
- if (/^\d+(?:\.\d+)?$/.test(input)) {
1521
- const num = parseFloat(input);
1522
- return Number.isFinite(num) ? num : null;
1381
+ function checkObsidianTemplates(resolvedPath) {
1382
+ if (resolvedPath === void 0) {
1383
+ return check("error", "obsidian_templates", "Obsidian templates", "Cannot check \u2014 WIKI_PATH not resolved");
1523
1384
  }
1524
- const re = /(\d+(?:\.\d+)?)(ms|s|m|h|d|w)/g;
1525
- let total = 0;
1526
- let consumed = 0;
1527
- for (const match of input.matchAll(re)) {
1528
- const full = match[0];
1529
- const value = parseFloat(match[1]);
1530
- const unit = match[2];
1531
- if (!Number.isFinite(value)) return null;
1532
- const factor = DURATION_UNIT_SECONDS[unit];
1533
- if (factor === void 0) return null;
1534
- total += value * factor;
1535
- consumed += full.length;
1385
+ const missing = [];
1386
+ if (!existsSync5(join8(resolvedPath, "_Templates"))) missing.push("_Templates/");
1387
+ if (!existsSync5(join8(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
1388
+ if (!existsSync5(join8(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
1389
+ if (missing.length === 0) {
1390
+ return check("pass", "obsidian_templates", "Obsidian templates", "Template folder and config present");
1536
1391
  }
1537
- if (consumed !== input.length) return null;
1538
- return total;
1392
+ return check("warn", "obsidian_templates", "Obsidian templates", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to create`);
1539
1393
  }
1540
- var FLAG_THRESHOLDS = {
1541
- "--vfs-write-back": { min: 15, unit: "s", label: "VFS write-back window" },
1542
- "--vfs-write-wait": { min: 10, unit: "s", label: "VFS write-wait" },
1543
- "--vfs-cache-max-age": { min: 24, unit: "h", label: "VFS cache max age" }
1394
+ var vaultStructureProbe = {
1395
+ id: "vault_structure",
1396
+ run(ctx) {
1397
+ return [
1398
+ checkWikiPathExists(ctx.resolvedPath),
1399
+ checkVaultStructure(ctx.resolvedPath),
1400
+ checkObsidianTemplates(ctx.resolvedPath)
1401
+ ];
1402
+ }
1544
1403
  };
1545
- var MIN_RCLONE_VERSION = { major: 1, minor: 65, patch: 0 };
1546
1404
 
1547
- // src/commands/doctor.ts
1548
- function check(status, id, label, detail) {
1549
- return { id, label, status, detail };
1550
- }
1551
- function checkNodeVersion() {
1552
- const major = parseInt(process.version.slice(1).split(".")[0], 10);
1553
- if (major >= 20) {
1554
- return check("pass", "node_version", "Node.js version", `v${major} >= 20`);
1405
+ // src/doctor/probes/git-fleet.ts
1406
+ import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
1407
+ import { join as join9 } from "path";
1408
+ import { execSync as execSync2 } from "child_process";
1409
+ import { platform } from "os";
1410
+ function checkVaultGitRemote(resolvedPath) {
1411
+ if (resolvedPath === void 0) {
1412
+ return check("error", "vault_git_remote", "Vault git remote", "Cannot check \u2014 WIKI_PATH not resolved");
1555
1413
  }
1556
- return check("error", "node_version", "Node.js version", `Node.js v${major} is below minimum v20`);
1557
- }
1558
- function detectCliChannels(argv, home) {
1559
- const channels = [];
1560
- if (argv.length >= 2 && argv[1].endsWith("cli.js")) {
1561
- const devPath = resolve2(argv[1]);
1562
- channels.push({ name: "dev", path: devPath, isDevLink: true });
1414
+ if (!existsSync6(join9(resolvedPath, ".git"))) {
1415
+ return check("warn", "vault_git_remote", "Vault git remote", "Vault is not a git repository \u2014 sync features unavailable");
1563
1416
  }
1564
1417
  try {
1565
- const whichOut = execSync2("which skillwiki 2>/dev/null", { encoding: "utf8" }).trim();
1566
- if (whichOut) {
1567
- const isDev = isDevSymlink(whichOut);
1568
- if (!channels.some((c) => c.path === resolve2(whichOut))) {
1569
- channels.push({ name: "npm", path: whichOut, isDevLink: isDev });
1570
- }
1418
+ const remote = execSync2("git remote", { cwd: resolvedPath, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
1419
+ if (!remote) {
1420
+ return check("warn", "vault_git_remote", "Vault git remote", "No remote configured \u2014 push/pull unavailable");
1421
+ }
1422
+ let branch = "(no commits yet)";
1423
+ try {
1424
+ branch = execSync2("git rev-parse --abbrev-ref HEAD", { cwd: resolvedPath, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
1425
+ } catch {
1571
1426
  }
1427
+ return check("pass", "vault_git_remote", "Vault git remote", `Remote: ${remote.split("\n")[0]}, branch: ${branch}`);
1572
1428
  } catch {
1429
+ return check("warn", "vault_git_remote", "Vault git remote", "Could not read git remote info");
1573
1430
  }
1574
- const plugin = findPlugin(home);
1575
- if (plugin) {
1576
- const pluginBin = join10(plugin.installPath, "bin", "skillwiki");
1577
- if (existsSync7(pluginBin)) {
1578
- channels.push({ name: "plugin", path: pluginBin, isDevLink: false });
1579
- }
1431
+ }
1432
+ async function checkFleetIdentity(input) {
1433
+ if (!input.vaultPath) {
1434
+ return check("pass", "fleet_identity", "Fleet identity", "No vault path \u2014 check skipped");
1580
1435
  }
1581
- const installBin = join10(home, ".claude", "skills", "bin", "skillwiki");
1582
- if (existsSync7(installBin)) {
1583
- channels.push({ name: "install", path: installBin, isDevLink: false });
1436
+ const load = input.fleetLoad !== void 0 ? input.fleetLoad : await loadFleetManifestAndHost({
1437
+ vault: input.vaultPath,
1438
+ env: { ...process.env, WIKI_PATH: input.envValue ?? input.vaultPath },
1439
+ home: input.home,
1440
+ cwd: input.cwd ?? process.cwd(),
1441
+ osHostname: process.env.HOSTNAME,
1442
+ user: process.env.USER
1443
+ });
1444
+ if (!load) {
1445
+ return check("pass", "fleet_identity", "Fleet identity", "Fleet manifest unavailable \u2014 check skipped");
1584
1446
  }
1585
- return channels;
1586
- }
1587
- function isDevSymlink(binPath) {
1588
- try {
1589
- const st = lstatSync(binPath);
1590
- if (st.isSymbolicLink()) {
1591
- const target = resolve2(binPath, "..", readlinkSync(binPath));
1592
- return target.includes("packages/cli") || target.includes("packages\\cli");
1593
- }
1594
- } catch {
1447
+ if (load.identityStatus === "known") {
1448
+ return check("pass", "fleet_identity", "Fleet identity", `Resolved ${load.hostId ?? "unknown"} via ${load.source ?? "unknown"}`);
1595
1449
  }
1596
- return false;
1450
+ const detail = load.warnings.length > 0 ? load.warnings.join("; ") : "Fleet identity is unresolved";
1451
+ return check("warn", "fleet_identity", "Fleet identity", detail);
1597
1452
  }
1598
- function checkCliChannels(argv, home) {
1599
- const channels = detectCliChannels(argv, home);
1600
- if (channels.length === 0) {
1601
- return check("warn", "cli_channels", "CLI channels", "skillwiki not found on any channel");
1453
+ function checkSyncLastPush(resolvedPath) {
1454
+ if (resolvedPath === void 0) {
1455
+ return check("error", "sync_last_push", "Vault sync recency", "Cannot check \u2014 WIKI_PATH not resolved");
1602
1456
  }
1603
- if (channels.length === 1) {
1604
- const ch = channels[0];
1605
- const label = ch.isDevLink ? `${ch.name} (dev source)` : ch.name;
1606
- return check("pass", "cli_channels", "CLI channels", `Single channel: ${label}`);
1457
+ if (!existsSync6(join9(resolvedPath, ".git"))) {
1458
+ return check("pass", "sync_last_push", "Vault sync recency", "No git repo \u2014 sync check skipped");
1607
1459
  }
1608
- const devChannels = channels.filter((c) => c.isDevLink);
1609
- const prodChannels = channels.filter((c) => !c.isDevLink);
1610
- if (devChannels.length > 0 && prodChannels.length > 0) {
1611
- const hasInstall2 = prodChannels.some((c) => c.name === "install");
1612
- if (!hasInstall2) {
1613
- const devNames2 = devChannels.map((c) => `${c.name}(dev)`);
1614
- const prodNames2 = prodChannels.map((c) => c.name);
1615
- return check("pass", "cli_channels", "CLI channels", `${channels.length} channels: ${[...devNames2, ...prodNames2].join(", ")} \u2014 dev source with installed production channels`);
1460
+ let timestamp;
1461
+ try {
1462
+ const out = execSync2("git log -1 --format=%ct origin/HEAD", {
1463
+ cwd: resolvedPath,
1464
+ encoding: "utf8",
1465
+ stdio: ["pipe", "pipe", "pipe"]
1466
+ }).trim();
1467
+ timestamp = parseInt(out, 10);
1468
+ } catch {
1469
+ try {
1470
+ const out = execSync2("git log -1 --format=%ct HEAD", {
1471
+ cwd: resolvedPath,
1472
+ encoding: "utf8",
1473
+ stdio: ["pipe", "pipe", "pipe"]
1474
+ }).trim();
1475
+ timestamp = parseInt(out, 10);
1476
+ } catch {
1616
1477
  }
1617
- const devNames = devChannels.map((c) => `${c.name}(dev)`);
1618
- const prodNames = prodChannels.map((c) => c.name);
1619
- return check(
1620
- "warn",
1621
- "cli_channels",
1622
- "CLI channels",
1623
- `${channels.length} channels: ${[...devNames, ...prodNames].join(", ")} \u2014 dev and prod binaries overlap; dev repo should use project-local settings only`
1624
- );
1625
- }
1626
- const names = channels.map((c) => c.name);
1627
- const hasInstall = channels.some((c) => c.name === "install");
1628
- if (hasInstall) {
1629
- return check(
1630
- "warn",
1631
- "cli_channels",
1632
- "CLI channels",
1633
- `${channels.length} channels: ${names.join(", ")} \u2014 remove unused install with: rm ~/.claude/skills/bin/skillwiki`
1634
- );
1635
1478
  }
1636
- return check("pass", "cli_channels", "CLI channels", `${channels.length} channels: ${names.join(", ")}`);
1637
- }
1638
- function isDevSourceRun(argv) {
1639
- return argv.length >= 2 && argv[1].endsWith("cli.js");
1640
- }
1641
- async function checkConfigFile(home) {
1642
- const cfgPath = configPath(home);
1643
- if (!existsSync7(cfgPath)) {
1644
- return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
1479
+ if (timestamp === void 0 || isNaN(timestamp)) {
1480
+ return check("warn", "sync_last_push", "Vault sync recency", "No commits found \u2014 consider running `skillwiki sync status`");
1645
1481
  }
1646
- try {
1647
- const map = await parseDotenvFile(cfgPath);
1648
- const keys = Object.keys(map);
1649
- return check("pass", "config_file", "Config file exists", `Found with keys: ${keys.length > 0 ? keys.join(", ") : "(none set)"}`);
1650
- } catch (e) {
1651
- return check("warn", "config_file", "Config file exists", `Failed to parse ${cfgPath}: ${String(e)}`);
1482
+ const daysSince = Math.floor((Date.now() / 1e3 - timestamp) / 86400);
1483
+ const dateStr = new Date(timestamp * 1e3).toISOString().slice(0, 10);
1484
+ if (daysSince > 7) {
1485
+ return check("warn", "sync_last_push", "Vault sync recency", `Last push was ${daysSince} days ago \u2014 consider running \`skillwiki sync status\``);
1652
1486
  }
1487
+ return check("pass", "sync_last_push", "Vault sync recency", `Last push: ${dateStr} (${daysSince} day(s) ago)`);
1653
1488
  }
1654
- function checkWikiPathExists(resolvedPath) {
1655
- if (resolvedPath === void 0) {
1656
- return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
1657
- }
1658
- if (existsSync7(resolvedPath) && statSync2(resolvedPath).isDirectory()) {
1659
- return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
1489
+ function hasOriginMain(resolvedPath) {
1490
+ try {
1491
+ execSync2("git rev-parse --verify --quiet origin/main", {
1492
+ cwd: resolvedPath,
1493
+ encoding: "utf8",
1494
+ stdio: ["pipe", "pipe", "pipe"],
1495
+ timeout: 2e3
1496
+ });
1497
+ return true;
1498
+ } catch {
1499
+ return false;
1660
1500
  }
1661
- return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
1662
1501
  }
1663
- function checkVaultStructure(resolvedPath) {
1502
+ function checkVaultGitDirty(resolvedPath) {
1664
1503
  if (resolvedPath === void 0) {
1665
- return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
1666
- }
1667
- if (!existsSync7(resolvedPath)) {
1668
- return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
1669
- }
1670
- const missing = [];
1671
- if (!existsSync7(join10(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
1672
- for (const dir of ["raw", "entities", "concepts", "meta"]) {
1673
- if (!existsSync7(join10(resolvedPath, dir))) missing.push(dir + "/");
1674
- }
1675
- if (missing.length === 0) {
1676
- return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
1677
- }
1678
- return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
1679
- }
1680
- function checkSkillsInstalled(home, cwd) {
1681
- const srcDir = cwd ? join10(cwd, "packages", "skills") : void 0;
1682
- if (srcDir && existsSync7(srcDir)) {
1683
- const found = findInstalledSkillMd(srcDir);
1684
- if (found.length > 0) {
1685
- return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (source)`);
1686
- }
1504
+ return check("pass", "vault_git_dirty", "Vault git dirty state", "No vault path \u2014 check skipped");
1687
1505
  }
1688
- const plugin = findPlugin(home);
1689
- if (plugin) {
1690
- const found = findInstalledSkillMd(plugin.installPath);
1691
- if (found.length > 0) {
1692
- return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (plugin v${plugin.version})`);
1693
- }
1506
+ if (!existsSync6(join9(resolvedPath, ".git"))) {
1507
+ return check("pass", "vault_git_dirty", "Vault git dirty state", "No git repo \u2014 check skipped");
1694
1508
  }
1695
- const skillsDir = join10(home, ".claude", "skills");
1696
- if (existsSync7(skillsDir)) {
1697
- const found = findInstalledSkillMd(skillsDir);
1698
- if (found.length > 0) {
1699
- return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (CLI install)`);
1509
+ try {
1510
+ const lines = execSync2("git status --porcelain", {
1511
+ cwd: resolvedPath,
1512
+ encoding: "utf8",
1513
+ stdio: ["pipe", "pipe", "pipe"],
1514
+ timeout: 5e3
1515
+ }).trim().split("\n").filter(Boolean);
1516
+ if (lines.length > 0) {
1517
+ return check("warn", "vault_git_dirty", "Vault git dirty state", `${lines.length} dirty file(s) in vault worktree`);
1700
1518
  }
1519
+ return check("pass", "vault_git_dirty", "Vault git dirty state", "Clean worktree");
1520
+ } catch {
1521
+ return check("warn", "vault_git_dirty", "Vault git dirty state", "Could not read git status");
1701
1522
  }
1702
- return check("warn", "skills_installed", "Skills installed", "No SKILL.md files found");
1703
1523
  }
1704
- function checkDuplicateSkills(home) {
1705
- const plugin = findPlugin(home);
1706
- const skillsDir = join10(home, ".claude", "skills");
1707
- const agentSkillDirs = [
1708
- { label: "~/.codex/skills/", path: join10(home, ".codex", "skills") },
1709
- { label: "~/.agents/skills/", path: join10(home, ".agents", "skills") }
1710
- ];
1711
- if (!plugin) {
1712
- return check("pass", "skills_duplicate", "Skills not duplicated", "Single install channel");
1713
- }
1714
- const pluginSkills = findSkillNames(plugin.installPath);
1715
- const cliSkills = findSkillNames(skillsDir);
1716
- const cliDuplicates = cliSkills.filter((name) => pluginSkills.includes(name));
1717
- const agentDuplicates = [];
1718
- for (const { label, path } of agentSkillDirs) {
1719
- const overlap = findSkillNames(path).filter((name) => pluginSkills.includes(name));
1720
- if (overlap.length > 0) {
1721
- agentDuplicates.push({ dir: label, names: overlap });
1722
- }
1723
- }
1724
- if (cliDuplicates.length === 0 && agentDuplicates.length === 0) {
1725
- return check("pass", "skills_duplicate", "Skills not duplicated", "No overlap between plugin and other channels");
1726
- }
1727
- const parts = [];
1728
- if (cliDuplicates.length > 0) {
1729
- parts.push(`${cliDuplicates.length} skill(s) in both plugin and ~/.claude/skills/ \u2014 remove CLI copies: rm -r ~/.claude/skills/{${cliDuplicates.slice(0, 3).join(",")}${cliDuplicates.length > 3 ? ",\u2026" : ""}}`);
1730
- }
1731
- for (const { dir, names } of agentDuplicates) {
1732
- parts.push(`${names.length} stale skill(s) in ${dir} \u2014 plugin provides: ${names.slice(0, 3).join(", ")}${names.length > 3 ? ", \u2026" : ""}`);
1524
+ function gitRefHash(resolvedPath, ref) {
1525
+ try {
1526
+ const out = execSync2(`git rev-parse --verify ${ref}`, {
1527
+ cwd: resolvedPath,
1528
+ encoding: "utf8",
1529
+ stdio: ["pipe", "pipe", "pipe"],
1530
+ timeout: 2e3
1531
+ }).trim();
1532
+ return out || void 0;
1533
+ } catch {
1534
+ return void 0;
1733
1535
  }
1734
- const status = cliDuplicates.length > 0 ? "warn" : "info";
1735
- return check(status, "skills_duplicate", "Skills not duplicated", parts.join("; "));
1736
1536
  }
1737
- function checkNpmUpdate(home, currentVersion) {
1738
- const { hasUpdate, latest, distTag } = latestFromCache(home, currentVersion);
1739
- if (!latest) {
1740
- return check("pass", "npm_update", "npm CLI version", `v${currentVersion} (${distTag}: no cache yet)`);
1741
- }
1742
- if (hasUpdate) {
1743
- return check("warn", "npm_update", "npm CLI version", `v${currentVersion} \u2014 ${distTag} update available: v${latest}. Run \`skillwiki update --tag ${distTag}\`.`);
1537
+ function remoteMainHash(resolvedPath) {
1538
+ try {
1539
+ const out = execSync2("git ls-remote origin refs/heads/main", {
1540
+ cwd: resolvedPath,
1541
+ encoding: "utf8",
1542
+ stdio: ["pipe", "pipe", "pipe"],
1543
+ timeout: 3e3
1544
+ }).trim();
1545
+ const hash = out.split(/\s+/)[0];
1546
+ return /^[0-9a-f]{40}$/i.test(hash) ? hash : void 0;
1547
+ } catch {
1548
+ return void 0;
1744
1549
  }
1745
- return check("pass", "npm_update", "npm CLI version", `v${currentVersion} (${distTag}: v${latest})`);
1746
1550
  }
1747
- function checkPluginVersionDrift(home, currentVersion, devSourceRun) {
1748
- const plugins = findPluginInstallations(home);
1749
- if (plugins.length === 0) {
1750
- return check("pass", "plugin_version_drift", "Plugin/CLI version", "Plugin not installed \u2014 CLI only");
1751
- }
1752
- const drifted = plugins.filter((plugin) => plugin.version !== currentVersion);
1753
- if (drifted.length === 0) {
1754
- if (plugins.length === 1 && plugins[0].channel === "claude") {
1755
- return check("pass", "plugin_version_drift", "Plugin/CLI version", `Both at v${currentVersion}`);
1756
- }
1757
- if (plugins.length === 1) {
1758
- return check("pass", "plugin_version_drift", "Plugin/CLI version", `${plugins[0].label} plugin and CLI both at v${currentVersion}`);
1759
- }
1760
- const labels = plugins.map((plugin) => `${plugin.label} plugin`).join(", ");
1761
- return check("pass", "plugin_version_drift", "Plugin/CLI version", `${labels}, and CLI all at v${currentVersion}`);
1762
- }
1763
- if (devSourceRun && drifted.every((plugin) => semverGt(currentVersion, plugin.version))) {
1764
- const details2 = drifted.map((plugin) => `${plugin.label} plugin v${plugin.version}`).join(", ");
1765
- return check("info", "plugin_version_drift", "Plugin/CLI version", `Dev source v${currentVersion} is ahead of installed ${details2}`);
1766
- }
1767
- const details = drifted.map((plugin) => {
1768
- const updateCmd = pluginUpdateCommand(plugin, currentVersion);
1769
- return `${plugin.label} plugin v${plugin.version} \u2260 CLI v${currentVersion} \u2014 run \`${updateCmd}\``;
1770
- });
1551
+ function checkStaleRemoteMain(resolvedPath) {
1552
+ if (resolvedPath === void 0) return void 0;
1553
+ if (!existsSync6(join9(resolvedPath, ".git"))) return void 0;
1554
+ const localOrigin = gitRefHash(resolvedPath, "origin/main");
1555
+ if (!localOrigin) return void 0;
1556
+ const remoteMain = remoteMainHash(resolvedPath);
1557
+ if (!remoteMain || remoteMain === localOrigin) return void 0;
1771
1558
  return check(
1772
1559
  "warn",
1773
- "plugin_version_drift",
1774
- "Plugin/CLI version",
1775
- details.join("; ")
1560
+ "vault_git_behind",
1561
+ "Vault commits behind",
1562
+ `Remote main differs from local origin/main (${remoteMain.slice(0, 8)} != ${localOrigin.slice(0, 8)}) \u2014 run git fetch before trusting behind count`
1776
1563
  );
1777
1564
  }
1778
- function pluginUpdateCommand(plugin, currentVersion) {
1779
- if (semverGt(plugin.version, currentVersion)) {
1780
- return "npm install -g skillwiki@latest";
1565
+ function checkVaultGitComparison(resolvedPath, id, label, range, nonZeroSuffix, zeroDetail) {
1566
+ if (resolvedPath === void 0) {
1567
+ return check("pass", id, label, "No vault path \u2014 check skipped");
1781
1568
  }
1782
- if (plugin.channel === "claude") {
1783
- return "claude plugin update skillwiki@llm-wiki";
1569
+ if (!existsSync6(join9(resolvedPath, ".git"))) {
1570
+ return check("pass", id, label, "No git repo \u2014 check skipped");
1784
1571
  }
1785
- if (plugin.sourceType === "git") {
1786
- return "codex plugin marketplace upgrade llm-wiki && codex plugin remove skillwiki@llm-wiki && codex plugin add skillwiki@llm-wiki";
1787
- }
1788
- return "codex plugin remove skillwiki@llm-wiki && codex plugin add skillwiki@llm-wiki";
1789
- }
1790
- async function checkProfiles(home) {
1791
- const map = await parseDotenvFile(configPath(home));
1792
- const profiles = [];
1793
- for (const key of Object.keys(map)) {
1794
- if (key.startsWith("WIKI_") && key.endsWith("_PATH") && key !== "WIKI_PATH") {
1795
- const name = key.slice(5, -5).toLowerCase().replace(/_/g, "-");
1796
- profiles.push(name);
1797
- }
1798
- }
1799
- if (profiles.length === 0) {
1800
- return check("pass", "wiki_profiles", "Wiki profiles", "No named profiles configured");
1801
- }
1802
- const defaultProfile = map["WIKI_DEFAULT"] ?? "(none)";
1803
- return check(
1804
- "pass",
1805
- "wiki_profiles",
1806
- "Wiki profiles",
1807
- `${profiles.length} profile(s): ${profiles.join(", ")}; default: ${defaultProfile}`
1808
- );
1809
- }
1810
- async function checkProjectLocalOverride(cwd) {
1811
- const dir = cwd ?? process.cwd();
1812
- const envPath = join10(dir, ".skillwiki", ".env");
1813
- if (existsSync7(envPath)) {
1814
- return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
1815
- }
1816
- return check("pass", "project_local", "Project-local config", "None");
1817
- }
1818
- function checkVaultGitRemote(resolvedPath) {
1819
- if (resolvedPath === void 0) {
1820
- return check("error", "vault_git_remote", "Vault git remote", "Cannot check \u2014 WIKI_PATH not resolved");
1821
- }
1822
- if (!existsSync7(join10(resolvedPath, ".git"))) {
1823
- return check("warn", "vault_git_remote", "Vault git remote", "Vault is not a git repository \u2014 sync features unavailable");
1824
- }
1825
- try {
1826
- const remote = execSync2("git remote", { cwd: resolvedPath, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
1827
- if (!remote) {
1828
- return check("warn", "vault_git_remote", "Vault git remote", "No remote configured \u2014 push/pull unavailable");
1829
- }
1830
- let branch = "(no commits yet)";
1831
- try {
1832
- branch = execSync2("git rev-parse --abbrev-ref HEAD", { cwd: resolvedPath, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
1833
- } catch {
1834
- }
1835
- return check("pass", "vault_git_remote", "Vault git remote", `Remote: ${remote.split("\n")[0]}, branch: ${branch}`);
1836
- } catch {
1837
- return check("warn", "vault_git_remote", "Vault git remote", "Could not read git remote info");
1838
- }
1839
- }
1840
- function checkObsidianTemplates(resolvedPath) {
1841
- if (resolvedPath === void 0) {
1842
- return check("error", "obsidian_templates", "Obsidian templates", "Cannot check \u2014 WIKI_PATH not resolved");
1843
- }
1844
- const missing = [];
1845
- if (!existsSync7(join10(resolvedPath, "_Templates"))) missing.push("_Templates/");
1846
- if (!existsSync7(join10(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
1847
- if (!existsSync7(join10(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
1848
- if (missing.length === 0) {
1849
- return check("pass", "obsidian_templates", "Obsidian templates", "Template folder and config present");
1850
- }
1851
- return check("warn", "obsidian_templates", "Obsidian templates", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to create`);
1852
- }
1853
- function checkDotStoreClean(resolvedPath) {
1854
- if (resolvedPath === void 0) {
1855
- return check("error", "dsstore_clean", "No .DS_Store in raw/", "Cannot check \u2014 WIKI_PATH not resolved");
1856
- }
1857
- const rawDir = join10(resolvedPath, "raw");
1858
- if (!existsSync7(rawDir)) {
1859
- return check("pass", "dsstore_clean", "No .DS_Store in raw/", "raw/ directory not found \u2014 check skipped");
1860
- }
1861
- const found = [];
1862
- (function walk(dir, rel) {
1863
- let entries;
1864
- try {
1865
- entries = readdirSync3(dir, { withFileTypes: true });
1866
- } catch {
1867
- return;
1868
- }
1869
- for (const entry of entries) {
1870
- if (entry.name === ".DS_Store") {
1871
- found.push(rel ? `${rel}/.DS_Store` : ".DS_Store");
1872
- } else if (entry.isDirectory()) {
1873
- walk(join10(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
1874
- }
1875
- }
1876
- })(rawDir, "");
1877
- if (found.length === 0) {
1878
- return check("pass", "dsstore_clean", "No .DS_Store in raw/", "No .DS_Store files found");
1879
- }
1880
- return check("info", "dsstore_clean", "No .DS_Store in raw/", `${found.length} .DS_Store file(s) found \u2014 remove with: find ${rawDir} -name .DS_Store -delete`);
1881
- }
1882
- function checkVaultConflictMarkers(resolvedPath) {
1883
- if (resolvedPath === void 0) {
1884
- return check("pass", "vault_conflict_markers", "Vault conflict markers", "No vault path \u2014 check skipped");
1885
- }
1886
- const findings = scanVaultConflictMarkers(resolvedPath);
1887
- if (findings.length === 0) {
1888
- return check("pass", "vault_conflict_markers", "Vault conflict markers", "No complete conflict-marker blocks");
1889
- }
1890
- const first = findings[0];
1891
- const n = findings.length;
1892
- const fileWord = n === 1 ? "file" : "files";
1893
- return check(
1894
- "error",
1895
- "vault_conflict_markers",
1896
- "Vault conflict markers",
1897
- `${n} ${fileWord}, first: ${first.path}:${first.line}`
1898
- );
1899
- }
1900
- function checkSyncLastPush(resolvedPath) {
1901
- if (resolvedPath === void 0) {
1902
- return check("error", "sync_last_push", "Vault sync recency", "Cannot check \u2014 WIKI_PATH not resolved");
1903
- }
1904
- if (!existsSync7(join10(resolvedPath, ".git"))) {
1905
- return check("pass", "sync_last_push", "Vault sync recency", "No git repo \u2014 sync check skipped");
1906
- }
1907
- let timestamp;
1908
- try {
1909
- const out = execSync2("git log -1 --format=%ct origin/HEAD", {
1910
- cwd: resolvedPath,
1911
- encoding: "utf8",
1912
- stdio: ["pipe", "pipe", "pipe"]
1913
- }).trim();
1914
- timestamp = parseInt(out, 10);
1915
- } catch {
1916
- try {
1917
- const out = execSync2("git log -1 --format=%ct HEAD", {
1918
- cwd: resolvedPath,
1919
- encoding: "utf8",
1920
- stdio: ["pipe", "pipe", "pipe"]
1921
- }).trim();
1922
- timestamp = parseInt(out, 10);
1923
- } catch {
1924
- }
1925
- }
1926
- if (timestamp === void 0 || isNaN(timestamp)) {
1927
- return check("warn", "sync_last_push", "Vault sync recency", "No commits found \u2014 consider running `skillwiki sync status`");
1928
- }
1929
- const daysSince = Math.floor((Date.now() / 1e3 - timestamp) / 86400);
1930
- const dateStr = new Date(timestamp * 1e3).toISOString().slice(0, 10);
1931
- if (daysSince > 7) {
1932
- return check("warn", "sync_last_push", "Vault sync recency", `Last push was ${daysSince} days ago \u2014 consider running \`skillwiki sync status\``);
1933
- }
1934
- return check("pass", "sync_last_push", "Vault sync recency", `Last push: ${dateStr} (${daysSince} day(s) ago)`);
1935
- }
1936
- function hasOriginMain(resolvedPath) {
1937
- try {
1938
- execSync2("git rev-parse --verify --quiet origin/main", {
1939
- cwd: resolvedPath,
1940
- encoding: "utf8",
1941
- stdio: ["pipe", "pipe", "pipe"],
1942
- timeout: 2e3
1943
- });
1944
- return true;
1945
- } catch {
1946
- return false;
1947
- }
1948
- }
1949
- function checkVaultGitDirty(resolvedPath) {
1950
- if (resolvedPath === void 0) {
1951
- return check("pass", "vault_git_dirty", "Vault git dirty state", "No vault path \u2014 check skipped");
1952
- }
1953
- if (!existsSync7(join10(resolvedPath, ".git"))) {
1954
- return check("pass", "vault_git_dirty", "Vault git dirty state", "No git repo \u2014 check skipped");
1955
- }
1956
- try {
1957
- const lines = execSync2("git status --porcelain", {
1958
- cwd: resolvedPath,
1959
- encoding: "utf8",
1960
- stdio: ["pipe", "pipe", "pipe"],
1961
- timeout: 5e3
1962
- }).trim().split("\n").filter(Boolean);
1963
- if (lines.length > 0) {
1964
- return check("warn", "vault_git_dirty", "Vault git dirty state", `${lines.length} dirty file(s) in vault worktree`);
1965
- }
1966
- return check("pass", "vault_git_dirty", "Vault git dirty state", "Clean worktree");
1967
- } catch {
1968
- return check("warn", "vault_git_dirty", "Vault git dirty state", "Could not read git status");
1572
+ if (!hasOriginMain(resolvedPath)) {
1573
+ return check("pass", id, label, "origin/main unavailable \u2014 check skipped");
1574
+ }
1575
+ try {
1576
+ const count = parseInt(execSync2(`git rev-list --count ${range}`, {
1577
+ cwd: resolvedPath,
1578
+ encoding: "utf8",
1579
+ stdio: ["pipe", "pipe", "pipe"],
1580
+ timeout: 5e3
1581
+ }).trim(), 10);
1582
+ if (count > 0) {
1583
+ return check("warn", id, label, `${count} commit(s) ${nonZeroSuffix}`);
1584
+ }
1585
+ return check("pass", id, label, zeroDetail);
1586
+ } catch {
1587
+ return check("warn", id, label, "Could not compare HEAD with origin/main");
1969
1588
  }
1970
1589
  }
1971
1590
  function checkVaultGitAhead(resolvedPath) {
@@ -1990,52 +1609,48 @@ function checkVaultGitBehind(resolvedPath) {
1990
1609
  "0 commits behind origin/main"
1991
1610
  );
1992
1611
  }
1993
- function gitRefHash(resolvedPath, ref) {
1994
- try {
1995
- const out = execSync2(`git rev-parse --verify ${ref}`, {
1996
- cwd: resolvedPath,
1997
- encoding: "utf8",
1998
- stdio: ["pipe", "pipe", "pipe"],
1999
- timeout: 2e3
2000
- }).trim();
2001
- return out || void 0;
2002
- } catch {
2003
- return void 0;
2004
- }
1612
+ function pullLogPaths(home) {
1613
+ const paths = platform() === "darwin" ? [
1614
+ join9(home, "Library", "Logs", "wiki-pull.log"),
1615
+ join9(home, ".local", "state", "vault-sync", "log", "wiki-pull.log")
1616
+ ] : [
1617
+ join9(home, ".local", "state", "vault-sync", "log", "wiki-pull.log"),
1618
+ join9(home, "Library", "Logs", "wiki-pull.log")
1619
+ ];
1620
+ return [...new Set(paths)];
2005
1621
  }
2006
- function remoteMainHash(resolvedPath) {
1622
+ function isRecentLogLine(line, nowMs) {
1623
+ const match = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)/);
1624
+ if (!match) return true;
1625
+ const ts = Date.parse(match[1]);
1626
+ if (!Number.isFinite(ts)) return true;
1627
+ return nowMs - ts <= 24 * 60 * 60 * 1e3;
1628
+ }
1629
+ function checkVaultGitPullFailures(home) {
1630
+ const path = pullLogPaths(home).find((p) => existsSync6(p));
1631
+ if (!path) {
1632
+ return check("pass", "vault_git_pull_failures", "Vault pull failures", "No wiki-pull.log found \u2014 check skipped");
1633
+ }
2007
1634
  try {
2008
- const out = execSync2("git ls-remote origin refs/heads/main", {
2009
- cwd: resolvedPath,
2010
- encoding: "utf8",
2011
- stdio: ["pipe", "pipe", "pipe"],
2012
- timeout: 3e3
2013
- }).trim();
2014
- const hash = out.split(/\s+/)[0];
2015
- return /^[0-9a-f]{40}$/i.test(hash) ? hash : void 0;
1635
+ const lines = readFileSync3(path, "utf8").split(/\r?\n/).filter(Boolean);
1636
+ const now = Date.now();
1637
+ const failures = lines.filter(
1638
+ (line) => isRecentLogLine(line, now) && /(pre-push pull failed|FAIL .*pull|FAIL .*rebase|cannot pull with rebase|unstaged changes)/i.test(line)
1639
+ );
1640
+ if (failures.length > 0) {
1641
+ const sample = failures.slice(-2).map((line) => line.slice(0, 100)).join(" | ");
1642
+ return check("warn", "vault_git_pull_failures", "Vault pull failures", `${failures.length} recent pull failure(s): ${sample}`);
1643
+ }
1644
+ return check("pass", "vault_git_pull_failures", "Vault pull failures", "No recent pull failures logged");
2016
1645
  } catch {
2017
- return void 0;
1646
+ return check("warn", "vault_git_pull_failures", "Vault pull failures", `Could not read ${path}`);
2018
1647
  }
2019
1648
  }
2020
- function checkStaleRemoteMain(resolvedPath) {
2021
- if (resolvedPath === void 0) return void 0;
2022
- if (!existsSync7(join10(resolvedPath, ".git"))) return void 0;
2023
- const localOrigin = gitRefHash(resolvedPath, "origin/main");
2024
- if (!localOrigin) return void 0;
2025
- const remoteMain = remoteMainHash(resolvedPath);
2026
- if (!remoteMain || remoteMain === localOrigin) return void 0;
2027
- return check(
2028
- "warn",
2029
- "vault_git_behind",
2030
- "Vault commits behind",
2031
- `Remote main differs from local origin/main (${remoteMain.slice(0, 8)} != ${localOrigin.slice(0, 8)}) \u2014 run git fetch before trusting behind count`
2032
- );
2033
- }
2034
1649
  function checkVaultLocalGit(resolvedPath) {
2035
1650
  if (resolvedPath === void 0) {
2036
1651
  return check("warn", "vault_local_git", "Vault local git", "Cannot check \u2014 WIKI_PATH not resolved");
2037
1652
  }
2038
- if (!existsSync7(join10(resolvedPath, ".git"))) {
1653
+ if (!existsSync6(join9(resolvedPath, ".git"))) {
2039
1654
  return check("warn", "vault_local_git", "Vault local git", "Not a git repository - sync features unavailable");
2040
1655
  }
2041
1656
  try {
@@ -2054,7 +1669,7 @@ function checkVaultGithubRemote(resolvedPath, exec) {
2054
1669
  if (resolvedPath === void 0) {
2055
1670
  return check("pass", "vault_github_remote", "Vault GitHub remote", "No vault path \u2014 check skipped");
2056
1671
  }
2057
- if (!existsSync7(join10(resolvedPath, ".git"))) {
1672
+ if (!existsSync6(join9(resolvedPath, ".git"))) {
2058
1673
  return check("pass", "vault_github_remote", "Vault GitHub remote", "No git repo \u2014 check skipped");
2059
1674
  }
2060
1675
  const state = probeGithubReachability(resolvedPath, exec);
@@ -2098,7 +1713,7 @@ function checkVaultPromotionLag(resolvedPath) {
2098
1713
  if (resolvedPath === void 0) {
2099
1714
  return check("pass", "vault_promotion_lag", "Vault promotion lag", "No vault path \u2014 check skipped");
2100
1715
  }
2101
- if (!existsSync7(join10(resolvedPath, ".git"))) {
1716
+ if (!existsSync6(join9(resolvedPath, ".git"))) {
2102
1717
  return check("pass", "vault_promotion_lag", "Vault promotion lag", "No git repo \u2014 check skipped");
2103
1718
  }
2104
1719
  try {
@@ -2121,162 +1736,409 @@ function checkVaultPromotionLag(resolvedPath) {
2121
1736
  return check("pass", "vault_promotion_lag", "Vault promotion lag", "Could not read origin/main \u2014 check skipped");
2122
1737
  }
2123
1738
  }
2124
- function checkVaultGitComparison(resolvedPath, id, label, range, nonZeroSuffix, zeroDetail) {
2125
- if (resolvedPath === void 0) {
2126
- return check("pass", id, label, "No vault path \u2014 check skipped");
2127
- }
2128
- if (!existsSync7(join10(resolvedPath, ".git"))) {
2129
- return check("pass", id, label, "No git repo \u2014 check skipped");
2130
- }
2131
- if (!hasOriginMain(resolvedPath)) {
2132
- return check("pass", id, label, "origin/main unavailable \u2014 check skipped");
2133
- }
2134
- try {
2135
- const count = parseInt(execSync2(`git rev-list --count ${range}`, {
2136
- cwd: resolvedPath,
2137
- encoding: "utf8",
2138
- stdio: ["pipe", "pipe", "pipe"],
2139
- timeout: 5e3
2140
- }).trim(), 10);
2141
- if (count > 0) {
2142
- return check("warn", id, label, `${count} commit(s) ${nonZeroSuffix}`);
2143
- }
2144
- return check("pass", id, label, zeroDetail);
2145
- } catch {
2146
- return check("warn", id, label, "Could not compare HEAD with origin/main");
1739
+ var gitFleetProbe = {
1740
+ id: "git_fleet",
1741
+ async run(ctx) {
1742
+ return [
1743
+ checkVaultGitRemote(ctx.gitCheckPath),
1744
+ await checkFleetIdentity({
1745
+ vaultPath: ctx.resolvedPath,
1746
+ home: ctx.input.home,
1747
+ cwd: ctx.input.cwd,
1748
+ envValue: ctx.input.envValue,
1749
+ fleetLoad: ctx.fleetLoad
1750
+ }),
1751
+ checkSyncLastPush(ctx.gitCheckPath),
1752
+ checkVaultGitDirty(ctx.gitCheckPath),
1753
+ checkVaultGitAhead(ctx.gitCheckPath),
1754
+ checkVaultGitBehind(ctx.gitCheckPath),
1755
+ checkVaultGitPullFailures(ctx.input.home),
1756
+ checkVaultLocalGit(ctx.gitCheckPath),
1757
+ checkVaultGithubRemote(ctx.gitCheckPath, ctx.input.execProbe),
1758
+ checkVaultS3Remote(ctx.input.home, ctx.input.execProbe, ctx.input.env ?? process.env),
1759
+ checkVaultSnapshotterReachable(ctx.fleetLoad, ctx.input.checkSnapshotter, ctx.input.execProbe),
1760
+ checkVaultPromotionLag(ctx.gitCheckPath)
1761
+ ];
2147
1762
  }
2148
- }
2149
- function checkSatelliteLastRun(vaultPath, satelliteExpected) {
2150
- if (!satelliteExpected) {
2151
- return check("pass", "satellite_job_last_run", "Satellite job last run", "Satellite job not expected on this host");
2152
- }
2153
- if (vaultPath === void 0) {
2154
- return check("pass", "satellite_job_last_run", "Satellite job last run", "No vault path \u2014 check skipped");
2155
- }
2156
- const latestPath = satelliteLatestRunPath(vaultPath);
2157
- if (!existsSync7(latestPath)) {
2158
- return check("pass", "satellite_job_last_run", "Satellite job last run", "No latest-run.json \u2014 satellite has not run yet");
2159
- }
2160
- try {
2161
- const health = evaluateSatelliteRunHealth(vaultPath, /* @__PURE__ */ new Date());
2162
- if (health.failed) {
2163
- const fc = health.failureClass;
2164
- const detail = fc ? `Last satellite run failed (failure_class: ${fc})` : "Last satellite run failed";
2165
- return check("error", "satellite_job_last_run", "Satellite job last run", detail);
1763
+ };
1764
+
1765
+ // src/doctor/probes/hygiene.ts
1766
+ import { existsSync as existsSync8, readdirSync as readdirSync3 } from "fs";
1767
+ import { join as join11 } from "path";
1768
+
1769
+ // src/utils/conflict-markers.ts
1770
+ import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
1771
+ import { join as join10 } from "path";
1772
+ function scanConflictMarkerBlocksInText(relPath, text) {
1773
+ const findings = [];
1774
+ const lines = text.split(/\r?\n/);
1775
+ let inFence = false;
1776
+ let openLine = 0;
1777
+ let sawSeparator = false;
1778
+ for (let i = 0; i < lines.length; i += 1) {
1779
+ const line = lines[i];
1780
+ if (line.startsWith("```") || line.startsWith("~~~")) {
1781
+ inFence = !inFence;
1782
+ continue;
2166
1783
  }
2167
- if (health.stale && health.finishedAt) {
2168
- return check(
2169
- "warn",
2170
- "satellite_job_last_run",
2171
- "Satellite job last run",
2172
- `Last run finished_at is older than 26h (${health.finishedAt})`
2173
- );
1784
+ if (inFence) continue;
1785
+ if (line.startsWith("<<<<<<< ")) {
1786
+ openLine = i + 1;
1787
+ sawSeparator = false;
1788
+ continue;
2174
1789
  }
2175
- return check(
2176
- "pass",
2177
- "satellite_job_last_run",
2178
- "Satellite job last run",
2179
- health.finishedAt ? `Last run ok (finished_at ${health.finishedAt})` : "Last run ok"
2180
- );
1790
+ if (line === "=======" && openLine > 0) {
1791
+ sawSeparator = true;
1792
+ continue;
1793
+ }
1794
+ if (line.startsWith(">>>>>>> ")) {
1795
+ if (openLine > 0 && sawSeparator) {
1796
+ findings.push({ path: relPath, line: openLine });
1797
+ }
1798
+ openLine = 0;
1799
+ sawSeparator = false;
1800
+ }
1801
+ }
1802
+ return findings;
1803
+ }
1804
+ var PRUNE_DIRS = /* @__PURE__ */ new Set([
1805
+ ".git",
1806
+ ".obsidian",
1807
+ ".skillwiki",
1808
+ ".claude",
1809
+ ".antigravitycli",
1810
+ ".playwright-cli"
1811
+ ]);
1812
+ function walkMarkdownFiles(root, dir, rel, out) {
1813
+ let entries;
1814
+ try {
1815
+ entries = readdirSync2(dir, { withFileTypes: true });
2181
1816
  } catch {
2182
- return check("warn", "satellite_job_last_run", "Satellite job last run", `Could not read ${latestPath}`);
1817
+ return;
1818
+ }
1819
+ for (const entry of entries) {
1820
+ if (entry.isDirectory()) {
1821
+ if (PRUNE_DIRS.has(entry.name)) continue;
1822
+ walkMarkdownFiles(root, join10(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name, out);
1823
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
1824
+ out.push(rel ? `${rel}/${entry.name}` : entry.name);
1825
+ }
2183
1826
  }
2184
1827
  }
2185
- function defaultSatelliteTimerDeps() {
2186
- return {
2187
- platform: () => platform2(),
2188
- systemctlIsActive: (unit) => {
2189
- try {
2190
- return execSync2(`systemctl is-active ${unit}`, {
2191
- encoding: "utf8",
2192
- timeout: 2e3,
2193
- stdio: ["pipe", "pipe", "pipe"]
2194
- }).trim();
2195
- } catch {
2196
- return void 0;
2197
- }
1828
+ function scanVaultConflictMarkers(vaultRoot) {
1829
+ if (!existsSync7(vaultRoot)) return [];
1830
+ const relPaths = [];
1831
+ walkMarkdownFiles(vaultRoot, vaultRoot, "", relPaths);
1832
+ const all = [];
1833
+ for (const rel of relPaths) {
1834
+ let text;
1835
+ try {
1836
+ text = readFileSync4(join10(vaultRoot, rel), "utf8");
1837
+ } catch {
1838
+ continue;
2198
1839
  }
2199
- };
1840
+ all.push(...scanConflictMarkerBlocksInText(rel, text));
1841
+ }
1842
+ return all;
2200
1843
  }
2201
- function checkSatelliteTimer(satelliteExpected, deps = defaultSatelliteTimerDeps()) {
2202
- if (!satelliteExpected) {
2203
- return check("pass", "satellite_job_timer", "Satellite job timer", "Satellite job not expected on this host");
1844
+
1845
+ // src/doctor/probes/hygiene.ts
1846
+ function checkDotStoreClean(resolvedPath) {
1847
+ if (resolvedPath === void 0) {
1848
+ return check("error", "dsstore_clean", "No .DS_Store in raw/", "Cannot check \u2014 WIKI_PATH not resolved");
2204
1849
  }
2205
- if (deps.platform() !== "linux") {
2206
- return check("pass", "satellite_job_timer", "Satellite job timer", "Timer check skipped \u2014 Linux only");
1850
+ const rawDir = join11(resolvedPath, "raw");
1851
+ if (!existsSync8(rawDir)) {
1852
+ return check("pass", "dsstore_clean", "No .DS_Store in raw/", "raw/ directory not found \u2014 check skipped");
2207
1853
  }
2208
- const out = deps.systemctlIsActive("agent-memory-trends.timer");
2209
- if (out === void 0) {
2210
- return check("pass", "satellite_job_timer", "Satellite job timer", "systemctl unavailable");
1854
+ const found = [];
1855
+ (function walk(dir, rel) {
1856
+ let entries;
1857
+ try {
1858
+ entries = readdirSync3(dir, { withFileTypes: true });
1859
+ } catch {
1860
+ return;
1861
+ }
1862
+ for (const entry of entries) {
1863
+ if (entry.name === ".DS_Store") {
1864
+ found.push(rel ? `${rel}/.DS_Store` : ".DS_Store");
1865
+ } else if (entry.isDirectory()) {
1866
+ walk(join11(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
1867
+ }
1868
+ }
1869
+ })(rawDir, "");
1870
+ if (found.length === 0) {
1871
+ return check("pass", "dsstore_clean", "No .DS_Store in raw/", "No .DS_Store files found");
2211
1872
  }
2212
- if (out === "active") {
2213
- return check("pass", "satellite_job_timer", "Satellite job timer", "systemd: agent-memory-trends.timer active");
1873
+ return check("info", "dsstore_clean", "No .DS_Store in raw/", `${found.length} .DS_Store file(s) found \u2014 remove with: find ${rawDir} -name .DS_Store -delete`);
1874
+ }
1875
+ function checkVaultConflictMarkers(resolvedPath) {
1876
+ if (resolvedPath === void 0) {
1877
+ return check("pass", "vault_conflict_markers", "Vault conflict markers", "No vault path \u2014 check skipped");
1878
+ }
1879
+ const findings = scanVaultConflictMarkers(resolvedPath);
1880
+ if (findings.length === 0) {
1881
+ return check("pass", "vault_conflict_markers", "Vault conflict markers", "No complete conflict-marker blocks");
2214
1882
  }
1883
+ const first = findings[0];
1884
+ const n = findings.length;
1885
+ const fileWord = n === 1 ? "file" : "files";
2215
1886
  return check(
2216
1887
  "error",
2217
- "satellite_job_timer",
2218
- "Satellite job timer",
2219
- `systemd: agent-memory-trends.timer is ${out || "not active"}`
1888
+ "vault_conflict_markers",
1889
+ "Vault conflict markers",
1890
+ `${n} ${fileWord}, first: ${first.path}:${first.line}`
2220
1891
  );
2221
1892
  }
2222
- async function checkFleetIdentity(input) {
2223
- if (!input.vaultPath) {
2224
- return check("pass", "fleet_identity", "Fleet identity", "No vault path \u2014 check skipped");
1893
+ var hygieneProbe = {
1894
+ id: "hygiene",
1895
+ run(ctx) {
1896
+ return [
1897
+ checkDotStoreClean(ctx.readOnlyScanRoot),
1898
+ checkVaultConflictMarkers(ctx.readOnlyScanRoot)
1899
+ ];
2225
1900
  }
2226
- const load = input.fleetLoad !== void 0 ? input.fleetLoad : await loadFleetManifestAndHost({
2227
- vault: input.vaultPath,
2228
- env: { ...process.env, WIKI_PATH: input.envValue ?? input.vaultPath },
2229
- home: input.home,
2230
- cwd: input.cwd ?? process.cwd(),
2231
- osHostname: process.env.HOSTNAME,
2232
- user: process.env.USER
2233
- });
2234
- if (!load) {
2235
- return check("pass", "fleet_identity", "Fleet identity", "Fleet manifest unavailable \u2014 check skipped");
1901
+ };
1902
+
1903
+ // src/doctor/probes/s3-mount-health.ts
1904
+ import { existsSync as existsSync10 } from "fs";
1905
+ import { join as join13 } from "path";
1906
+ import { execSync as execSync4 } from "child_process";
1907
+
1908
+ // src/utils/s3-mount-health.ts
1909
+ import { execSync as execSync3 } from "child_process";
1910
+ import { platform as platform2 } from "os";
1911
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, readFileSync as readFile6 } from "fs";
1912
+ import { join as join12 } from "path";
1913
+ var OS = platform2();
1914
+ function findRcloneMountPid() {
1915
+ try {
1916
+ const out = execSync3("pgrep -f 'rclone.*mount'", {
1917
+ encoding: "utf8",
1918
+ timeout: 2e3,
1919
+ stdio: ["pipe", "pipe", "pipe"]
1920
+ }).trim();
1921
+ const pids = out.split("\n").filter(Boolean);
1922
+ if (pids.length === 0) return null;
1923
+ return parseInt(pids[0], 10);
1924
+ } catch {
1925
+ try {
1926
+ const out = execSync3("ps aux", { encoding: "utf8", timeout: 2e3, stdio: ["pipe", "pipe", "pipe"] });
1927
+ for (const line of out.split("\n")) {
1928
+ if (line.includes("rclone") && line.includes("mount") && !line.includes("grep")) {
1929
+ const parts = line.trim().split(/\s+/);
1930
+ if (parts.length >= 2) return parseInt(parts[1], 10);
1931
+ }
1932
+ }
1933
+ } catch {
1934
+ }
1935
+ return null;
2236
1936
  }
2237
- if (load.identityStatus === "known") {
2238
- return check("pass", "fleet_identity", "Fleet identity", `Resolved ${load.hostId ?? "unknown"} via ${load.source ?? "unknown"}`);
1937
+ }
1938
+ function parseRcloneFlags(pid) {
1939
+ const flags = /* @__PURE__ */ new Map();
1940
+ try {
1941
+ const args = getRcloneArgs(pid);
1942
+ for (let i = 0; i < args.length; i++) {
1943
+ const arg = args[i];
1944
+ if (arg.startsWith("--") && arg.includes("=")) {
1945
+ const eq = arg.indexOf("=");
1946
+ flags.set(arg.slice(0, eq), arg.slice(eq + 1));
1947
+ } else if (arg.startsWith("--")) {
1948
+ const next = args[i + 1];
1949
+ if (next && !next.startsWith("-")) {
1950
+ flags.set(arg, next);
1951
+ i++;
1952
+ } else {
1953
+ flags.set(arg, "");
1954
+ }
1955
+ }
1956
+ }
1957
+ } catch {
2239
1958
  }
2240
- const detail = load.warnings.length > 0 ? load.warnings.join("; ") : "Fleet identity is unresolved";
2241
- return check("warn", "fleet_identity", "Fleet identity", detail);
1959
+ return flags;
2242
1960
  }
2243
- function pullLogPaths(home) {
2244
- const paths = platform2() === "darwin" ? [
2245
- join10(home, "Library", "Logs", "wiki-pull.log"),
2246
- join10(home, ".local", "state", "vault-sync", "log", "wiki-pull.log")
2247
- ] : [
2248
- join10(home, ".local", "state", "vault-sync", "log", "wiki-pull.log"),
2249
- join10(home, "Library", "Logs", "wiki-pull.log")
2250
- ];
2251
- return [...new Set(paths)];
1961
+ function getRcloneVersion() {
1962
+ try {
1963
+ const out = execSync3("rclone version", {
1964
+ encoding: "utf8",
1965
+ timeout: 3e3,
1966
+ stdio: ["pipe", "pipe", "pipe"]
1967
+ });
1968
+ const match = out.match(/rclone\s+v(\d+)\.(\d+)\.(\d+)/i);
1969
+ if (!match) return null;
1970
+ return {
1971
+ major: parseInt(match[1], 10),
1972
+ minor: parseInt(match[2], 10),
1973
+ patch: parseInt(match[3], 10),
1974
+ raw: out.split("\n")[0].trim()
1975
+ };
1976
+ } catch {
1977
+ return null;
1978
+ }
2252
1979
  }
2253
- function isRecentLogLine(line, nowMs) {
2254
- const match = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)/);
2255
- if (!match) return true;
2256
- const ts = Date.parse(match[1]);
2257
- if (!Number.isFinite(ts)) return true;
2258
- return nowMs - ts <= 24 * 60 * 60 * 1e3;
1980
+ function extractRcloneFs(args) {
1981
+ let foundMount = false;
1982
+ for (const arg of args) {
1983
+ if (arg === "mount") {
1984
+ foundMount = true;
1985
+ continue;
1986
+ }
1987
+ if (foundMount && arg.includes(":") && !arg.startsWith("-") && !arg.startsWith("/")) {
1988
+ return arg;
1989
+ }
1990
+ }
1991
+ return null;
1992
+ }
1993
+ function getRcloneArgs(pid) {
1994
+ try {
1995
+ if (OS === "linux") {
1996
+ const raw = readFileSync5(`/proc/${pid}/cmdline`);
1997
+ return new TextDecoder().decode(raw).split("\0").filter(Boolean);
1998
+ } else {
1999
+ const out = execSync3(`ps -o args= -p ${pid}`, {
2000
+ encoding: "utf8",
2001
+ timeout: 2e3,
2002
+ stdio: ["pipe", "pipe", "pipe"]
2003
+ }).trim();
2004
+ return out.split(/\s+/);
2005
+ }
2006
+ } catch {
2007
+ return [];
2008
+ }
2009
+ }
2010
+ function queryRcloneRC(rcAddr, fs) {
2011
+ try {
2012
+ const payload = JSON.stringify({ fs });
2013
+ const out = execSync3(
2014
+ `curl -s --max-time 3 -X POST "http://${rcAddr}/vfs/stats" -H "Content-Type: application/json" -d '${payload}' 2>/dev/null`,
2015
+ { encoding: "utf8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }
2016
+ );
2017
+ if (!out.trim()) return null;
2018
+ const data = JSON.parse(out);
2019
+ if (data.status && data.status >= 400) {
2020
+ return { error: data.error || `RC error (status ${data.status})`, erroredFiles: 0, uploadsInProgress: 0, uploadsQueued: 0, outOfSpace: false, bytesUsed: 0, files: 0, totalSize: "unknown" };
2021
+ }
2022
+ const dc = data.diskCache || {};
2023
+ return {
2024
+ erroredFiles: dc.erroredFiles ?? 0,
2025
+ uploadsInProgress: dc.uploadsInProgress ?? 0,
2026
+ uploadsQueued: dc.uploadsQueued ?? 0,
2027
+ outOfSpace: dc.outOfSpace ?? false,
2028
+ bytesUsed: dc.bytesUsed ?? 0,
2029
+ files: dc.files ?? 0,
2030
+ totalSize: data.totalSize || "unknown"
2031
+ };
2032
+ } catch {
2033
+ return { error: "RC endpoint unreachable", erroredFiles: 0, uploadsInProgress: 0, uploadsQueued: 0, outOfSpace: false, bytesUsed: 0, files: 0, totalSize: "unknown" };
2034
+ }
2035
+ }
2036
+ function detectFuseMount(vaultPath) {
2037
+ try {
2038
+ if (OS === "linux") {
2039
+ const mounts = readFileSync5("/proc/mounts", "utf8");
2040
+ let best = null;
2041
+ for (const line of mounts.split("\n")) {
2042
+ const parts = line.split(" ");
2043
+ if (parts.length < 3) continue;
2044
+ const point = parts[1];
2045
+ const fs = parts[2];
2046
+ if (vaultPath.startsWith(point) && (!best || point.length > best.point.length)) {
2047
+ best = { point, fs };
2048
+ }
2049
+ }
2050
+ if (best && best.fs.includes("fuse")) return { mountPoint: best.point, fsType: best.fs };
2051
+ } else if (OS === "darwin") {
2052
+ const out = execSync3("mount", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
2053
+ let best = null;
2054
+ for (const line of out.split("\n")) {
2055
+ const match = line.match(/^(\S+) on (\S+) \((.*?)\)/);
2056
+ if (!match) continue;
2057
+ const point = match[2];
2058
+ const opts = match[3];
2059
+ if (opts.includes("fuse") && vaultPath.startsWith(point) && (!best || point.length > best.point.length)) {
2060
+ best = { point, fsType: `fuse.${match[1].split(":")[0] || "unknown"}` };
2061
+ }
2062
+ }
2063
+ if (best) return { mountPoint: best.point, fsType: best.fsType };
2064
+ }
2065
+ } catch {
2066
+ }
2067
+ return null;
2259
2068
  }
2260
- function checkVaultGitPullFailures(home) {
2261
- const path = pullLogPaths(home).find((p) => existsSync7(p));
2262
- if (!path) {
2263
- return check("pass", "vault_git_pull_failures", "Vault pull failures", "No wiki-pull.log found \u2014 check skipped");
2069
+ function writeTest(dir) {
2070
+ const testFile = join12(dir, `.doctor-write-test-${process.pid}.tmp`);
2071
+ const payload = `skillwiki doctor write test \u2014 ${Date.now()} \u2014 ${Math.random().toString(36).slice(2)}`;
2072
+ const start = Date.now();
2073
+ try {
2074
+ writeFileSync2(testFile, payload, "utf8");
2075
+ } catch (e) {
2076
+ return { success: false, writeMs: Date.now() - start, readMs: 0, size: 0, error: `write failed: ${e.message}` };
2264
2077
  }
2078
+ const writeMs = Date.now() - start;
2079
+ const readStart = Date.now();
2265
2080
  try {
2266
- const lines = readFileSync6(path, "utf8").split(/\r?\n/).filter(Boolean);
2267
- const now = Date.now();
2268
- const failures = lines.filter(
2269
- (line) => isRecentLogLine(line, now) && /(pre-push pull failed|FAIL .*pull|FAIL .*rebase|cannot pull with rebase|unstaged changes)/i.test(line)
2270
- );
2271
- if (failures.length > 0) {
2272
- const sample = failures.slice(-2).map((line) => line.slice(0, 100)).join(" | ");
2273
- return check("warn", "vault_git_pull_failures", "Vault pull failures", `${failures.length} recent pull failure(s): ${sample}`);
2081
+ const back = readFile6(testFile, "utf8");
2082
+ const readMs = Date.now() - readStart;
2083
+ if (back !== payload) {
2084
+ try {
2085
+ unlinkSync2(testFile);
2086
+ } catch {
2087
+ }
2088
+ return { success: false, writeMs, readMs, size: Buffer.byteLength(payload, "utf8"), error: "content mismatch \u2014 wrote and read-back differ" };
2274
2089
  }
2275
- return check("pass", "vault_git_pull_failures", "Vault pull failures", "No recent pull failures logged");
2090
+ } catch (e) {
2091
+ try {
2092
+ unlinkSync2(testFile);
2093
+ } catch {
2094
+ }
2095
+ return { success: false, writeMs, readMs: Date.now() - readStart, size: 0, error: `read failed: ${e.message}` };
2096
+ }
2097
+ try {
2098
+ unlinkSync2(testFile);
2276
2099
  } catch {
2277
- return check("warn", "vault_git_pull_failures", "Vault pull failures", `Could not read ${path}`);
2278
2100
  }
2101
+ return { success: true, writeMs, readMs: Date.now() - readStart, size: Buffer.byteLength(payload, "utf8") };
2102
+ }
2103
+ var DURATION_UNIT_SECONDS = {
2104
+ ms: 1 / 1e3,
2105
+ s: 1,
2106
+ m: 60,
2107
+ h: 3600,
2108
+ d: 86400,
2109
+ w: 604800
2110
+ };
2111
+ function parseDurationSeconds(raw) {
2112
+ const input = raw.trim().toLowerCase();
2113
+ if (!input) return null;
2114
+ if (/^\d+(?:\.\d+)?$/.test(input)) {
2115
+ const num = parseFloat(input);
2116
+ return Number.isFinite(num) ? num : null;
2117
+ }
2118
+ const re = /(\d+(?:\.\d+)?)(ms|s|m|h|d|w)/g;
2119
+ let total = 0;
2120
+ let consumed = 0;
2121
+ for (const match of input.matchAll(re)) {
2122
+ const full = match[0];
2123
+ const value = parseFloat(match[1]);
2124
+ const unit = match[2];
2125
+ if (!Number.isFinite(value)) return null;
2126
+ const factor = DURATION_UNIT_SECONDS[unit];
2127
+ if (factor === void 0) return null;
2128
+ total += value * factor;
2129
+ consumed += full.length;
2130
+ }
2131
+ if (consumed !== input.length) return null;
2132
+ return total;
2279
2133
  }
2134
+ var FLAG_THRESHOLDS = {
2135
+ "--vfs-write-back": { min: 15, unit: "s", label: "VFS write-back window" },
2136
+ "--vfs-write-wait": { min: 10, unit: "s", label: "VFS write-wait" },
2137
+ "--vfs-cache-max-age": { min: 24, unit: "h", label: "VFS cache max age" }
2138
+ };
2139
+ var MIN_RCLONE_VERSION = { major: 1, minor: 65, patch: 0 };
2140
+
2141
+ // src/doctor/probes/s3-mount-health.ts
2280
2142
  function checkS3MountPerf(resolvedPath) {
2281
2143
  if (resolvedPath === void 0) {
2282
2144
  return check("pass", "s3_mount_perf", "S3 mount performance", "No vault path \u2014 check skipped");
@@ -2286,14 +2148,14 @@ function checkS3MountPerf(resolvedPath) {
2286
2148
  return check("pass", "s3_mount_perf", "S3 mount performance", "local disk");
2287
2149
  }
2288
2150
  const mountPoint = fuse.mountPoint;
2289
- const conceptsDir = join10(resolvedPath, "concepts");
2290
- if (!existsSync7(conceptsDir)) {
2151
+ const conceptsDir = join13(resolvedPath, "concepts");
2152
+ if (!existsSync10(conceptsDir)) {
2291
2153
  return check("pass", "s3_mount_perf", "S3 mount performance", `S3 FUSE mount (${mountPoint}), no concepts/ to benchmark`);
2292
2154
  }
2293
2155
  const start = Date.now();
2294
2156
  let timedOut = false;
2295
2157
  try {
2296
- execSync2(`rg -l "." "${conceptsDir}"`, {
2158
+ execSync4(`rg -l "." "${conceptsDir}"`, {
2297
2159
  timeout: 5e3,
2298
2160
  encoding: "utf8",
2299
2161
  stdio: ["pipe", "pipe", "pipe"]
@@ -2469,8 +2331,8 @@ function checkWriteTest(resolvedPath) {
2469
2331
  if (!fuse) {
2470
2332
  return check("pass", "s3_write_test", "S3 write test", "local disk \u2014 check skipped");
2471
2333
  }
2472
- const conceptsDir = join10(resolvedPath, "concepts");
2473
- if (!existsSync7(conceptsDir)) {
2334
+ const conceptsDir = join13(resolvedPath, "concepts");
2335
+ if (!existsSync10(conceptsDir)) {
2474
2336
  return check("pass", "s3_write_test", "S3 write test", "no concepts/ dir to test \u2014 check skipped");
2475
2337
  }
2476
2338
  const result = writeTest(conceptsDir);
@@ -2554,45 +2416,266 @@ function checkVfsCacheHealth(resolvedPath) {
2554
2416
  `${stats.files} files, ${(stats.bytesUsed / 1024 / 1024).toFixed(1)}MB \u2014 clean (0 errored, 0 pending)`
2555
2417
  );
2556
2418
  }
2557
- function checkVaultSyncPullHelper(home, env) {
2558
- const path = resolveVaultSyncPullHelper({
2559
- vault: "",
2560
- home,
2561
- env
2562
- });
2563
- if (path) {
2564
- return check("pass", "vault_sync_pull_helper", "Vault-sync pull helper", `Resolved: ${path}`);
2419
+ var s3MountHealthProbe = {
2420
+ id: "s3_mount_health",
2421
+ run(ctx) {
2422
+ return [
2423
+ checkS3MountPerf(ctx.resolvedPath),
2424
+ checkS3MountFreshness(ctx.resolvedPath),
2425
+ checkRcloneFlagAudit(ctx.resolvedPath),
2426
+ checkRcloneVersion(ctx.resolvedPath, ctx.vsConfig.installed),
2427
+ checkWriteTest(ctx.resolvedPath),
2428
+ checkVfsCacheHealth(ctx.resolvedPath)
2429
+ ];
2565
2430
  }
2566
- return check(
2567
- "error",
2568
- "vault_sync_pull_helper",
2569
- "Vault-sync pull helper",
2570
- "Not found \u2014 install skillwiki@0.10.1+, redeploy vault-sync, or set SKILLWIKI_VAULT_SYNC_PULL_HELPER"
2571
- );
2431
+ };
2432
+
2433
+ // src/doctor/probes/skills-plugins.ts
2434
+ import { existsSync as existsSync11, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "fs";
2435
+ import { join as join14 } from "path";
2436
+ function findSkillMd(dir) {
2437
+ const results = [];
2438
+ let entries;
2439
+ try {
2440
+ entries = readdirSync4(dir, { withFileTypes: true });
2441
+ } catch {
2442
+ return results;
2443
+ }
2444
+ for (const entry of entries) {
2445
+ if (entry.isFile() && entry.name === "SKILL.md") {
2446
+ results.push(join14(dir, entry.name));
2447
+ } else if (entry.isDirectory()) {
2448
+ results.push(...findSkillMd(join14(dir, entry.name)));
2449
+ }
2450
+ }
2451
+ return results;
2572
2452
  }
2573
- function checkVaultSyncReviewRequiredJournals(vaultPath) {
2574
- if (!vaultPath || !existsSync7(join10(vaultPath, ".git"))) {
2575
- return check("pass", "vault_sync_review_required_journals", "Review-required journals", "No git vault \u2014 check skipped");
2453
+ function findSkillNames(dir) {
2454
+ const results = [];
2455
+ let entries;
2456
+ try {
2457
+ entries = readdirSync4(dir, { withFileTypes: true });
2458
+ } catch {
2459
+ return results;
2460
+ }
2461
+ for (const entry of entries) {
2462
+ if (entry.isDirectory() && existsSync11(join14(dir, entry.name, "SKILL.md"))) {
2463
+ results.push(entry.name);
2464
+ }
2465
+ }
2466
+ return results;
2467
+ }
2468
+ function findInstalledSkillMd(dir) {
2469
+ const directSkills = findSkillNames(dir).map((name) => join14(dir, name, "SKILL.md"));
2470
+ return directSkills.length > 0 ? directSkills : findSkillMd(dir);
2471
+ }
2472
+ function checkSkillsInstalled(home, cwd) {
2473
+ const srcDir = cwd ? join14(cwd, "packages", "skills") : void 0;
2474
+ if (srcDir && existsSync11(srcDir)) {
2475
+ const found = findInstalledSkillMd(srcDir);
2476
+ if (found.length > 0) {
2477
+ return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (source)`);
2478
+ }
2479
+ }
2480
+ const plugin = findPlugin(home);
2481
+ if (plugin) {
2482
+ const found = findInstalledSkillMd(plugin.installPath);
2483
+ if (found.length > 0) {
2484
+ return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (plugin v${plugin.version})`);
2485
+ }
2486
+ }
2487
+ const skillsDir = join14(home, ".claude", "skills");
2488
+ if (existsSync11(skillsDir)) {
2489
+ const found = findInstalledSkillMd(skillsDir);
2490
+ if (found.length > 0) {
2491
+ return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (CLI install)`);
2492
+ }
2493
+ }
2494
+ return check("warn", "skills_installed", "Skills installed", "No SKILL.md files found");
2495
+ }
2496
+ function checkDuplicateSkills(home) {
2497
+ const plugin = findPlugin(home);
2498
+ const skillsDir = join14(home, ".claude", "skills");
2499
+ const agentSkillDirs = [
2500
+ { label: "~/.codex/skills/", path: join14(home, ".codex", "skills") },
2501
+ { label: "~/.agents/skills/", path: join14(home, ".agents", "skills") }
2502
+ ];
2503
+ if (!plugin) {
2504
+ return check("pass", "skills_duplicate", "Skills not duplicated", "Single install channel");
2505
+ }
2506
+ const pluginSkills = findSkillNames(plugin.installPath);
2507
+ const cliSkills = findSkillNames(skillsDir);
2508
+ const cliDuplicates = cliSkills.filter((name) => pluginSkills.includes(name));
2509
+ const agentDuplicates = [];
2510
+ for (const { label, path } of agentSkillDirs) {
2511
+ const overlap = findSkillNames(path).filter((name) => pluginSkills.includes(name));
2512
+ if (overlap.length > 0) {
2513
+ agentDuplicates.push({ dir: label, names: overlap });
2514
+ }
2515
+ }
2516
+ if (cliDuplicates.length === 0 && agentDuplicates.length === 0) {
2517
+ return check("pass", "skills_duplicate", "Skills not duplicated", "No overlap between plugin and other channels");
2518
+ }
2519
+ const parts = [];
2520
+ if (cliDuplicates.length > 0) {
2521
+ parts.push(`${cliDuplicates.length} skill(s) in both plugin and ~/.claude/skills/ \u2014 remove CLI copies: rm -r ~/.claude/skills/{${cliDuplicates.slice(0, 3).join(",")}${cliDuplicates.length > 3 ? ",\u2026" : ""}}`);
2522
+ }
2523
+ for (const { dir, names } of agentDuplicates) {
2524
+ parts.push(`${names.length} stale skill(s) in ${dir} \u2014 plugin provides: ${names.slice(0, 3).join(", ")}${names.length > 3 ? ", \u2026" : ""}`);
2525
+ }
2526
+ const status = cliDuplicates.length > 0 ? "warn" : "info";
2527
+ return check(status, "skills_duplicate", "Skills not duplicated", parts.join("; "));
2528
+ }
2529
+ var GROK_ACTIVATION_REFERENCE = "Read @~/.grok/skillwiki.md for SkillWiki activation context.";
2530
+ var STALE_GROK_ACTIVATION_REFERENCE = "Read @skillwiki.md";
2531
+ var ACTIVATION_FIX_HINT = "run `npm run install:activation` from the llm-wiki repo";
2532
+ function findGrokActivationTemplate(home, cwd) {
2533
+ if (cwd) {
2534
+ const src = join14(cwd, "packages", "skills", "using-skillwiki", "activation.md");
2535
+ if (existsSync11(src)) return src;
2576
2536
  }
2537
+ const pluginsRoot = join14(home, ".grok", "installed-plugins");
2538
+ if (!existsSync11(pluginsRoot)) return void 0;
2539
+ let entries;
2577
2540
  try {
2578
- const ops = listReviewRequiredOps(vaultPath);
2579
- if (ops.length === 0) {
2580
- return check("pass", "vault_sync_review_required_journals", "Review-required journals", "None");
2541
+ entries = readdirSync4(pluginsRoot, { withFileTypes: true });
2542
+ } catch {
2543
+ return void 0;
2544
+ }
2545
+ for (const entry of entries) {
2546
+ if (!entry.isDirectory()) continue;
2547
+ const root = join14(pluginsRoot, entry.name);
2548
+ for (const rel of ["using-skillwiki/activation.md", "skills/using-skillwiki/activation.md"]) {
2549
+ const candidate = join14(root, rel);
2550
+ if (existsSync11(candidate)) return candidate;
2581
2551
  }
2582
- const sample = ops[0]?.opId ?? "?";
2552
+ }
2553
+ return void 0;
2554
+ }
2555
+ function checkGrokActivation(home, cwd) {
2556
+ const grokDir = join14(home, ".grok");
2557
+ if (!existsSync11(grokDir)) {
2558
+ return check("pass", "activation_grok", "Grok activation", "Not a Grok host");
2559
+ }
2560
+ const activationPath = join14(grokDir, "skillwiki.md");
2561
+ const agentsPath = join14(grokDir, "AGENTS.md");
2562
+ const hasActivation = existsSync11(activationPath);
2563
+ const issues = [];
2564
+ if (!hasActivation) {
2565
+ issues.push("~/.grok/skillwiki.md missing");
2566
+ }
2567
+ if (!existsSync11(agentsPath)) {
2568
+ issues.push("~/.grok/AGENTS.md missing");
2569
+ } else {
2570
+ const agents = readFileSync6(agentsPath, "utf8");
2571
+ const hasBegin = agents.includes("<!-- skillwiki:begin -->");
2572
+ const hasExpected = agents.includes(GROK_ACTIVATION_REFERENCE);
2573
+ const hasStale = agents.includes(STALE_GROK_ACTIVATION_REFERENCE);
2574
+ if (!hasBegin) {
2575
+ issues.push("AGENTS.md marker missing");
2576
+ } else if (hasStale && !hasExpected) {
2577
+ issues.push("AGENTS.md marker is stale (@skillwiki.md)");
2578
+ } else if (!hasExpected) {
2579
+ issues.push("AGENTS.md marker is stale");
2580
+ }
2581
+ }
2582
+ if (hasActivation) {
2583
+ const template = findGrokActivationTemplate(home, cwd);
2584
+ if (template) {
2585
+ try {
2586
+ const installed = readFileSync6(activationPath, "utf8");
2587
+ const expected = readFileSync6(template, "utf8");
2588
+ if (installed !== expected) {
2589
+ issues.push("~/.grok/skillwiki.md differs from template");
2590
+ }
2591
+ } catch {
2592
+ }
2593
+ }
2594
+ }
2595
+ if (issues.length > 0) {
2583
2596
  return check(
2584
2597
  "warn",
2585
- "vault_sync_review_required_journals",
2586
- "Review-required journals",
2587
- `${ops.length} handoff(s); oldest/sample: ${sample} \u2014 if worktree clean: skillwiki sync journal clear-stale --dry-run`
2598
+ "activation_grok",
2599
+ "Grok activation",
2600
+ `${issues.join("; ")} \u2014 ${ACTIVATION_FIX_HINT}`
2588
2601
  );
2589
- } catch {
2590
- return check("pass", "vault_sync_review_required_journals", "Review-required journals", "Could not read journals \u2014 check skipped");
2591
2602
  }
2603
+ return check("pass", "activation_grok", "Grok activation", "Marker and compact file are current");
2604
+ }
2605
+ function checkNpmUpdate(home, currentVersion) {
2606
+ const { hasUpdate, latest, distTag } = latestFromCache(home, currentVersion);
2607
+ if (!latest) {
2608
+ return check("pass", "npm_update", "npm CLI version", `v${currentVersion} (${distTag}: no cache yet)`);
2609
+ }
2610
+ if (hasUpdate) {
2611
+ return check("warn", "npm_update", "npm CLI version", `v${currentVersion} \u2014 ${distTag} update available: v${latest}. Run \`skillwiki update --tag ${distTag}\`.`);
2612
+ }
2613
+ return check("pass", "npm_update", "npm CLI version", `v${currentVersion} (${distTag}: v${latest})`);
2614
+ }
2615
+ function pluginUpdateCommand(plugin, currentVersion) {
2616
+ if (semverGt(plugin.version, currentVersion)) {
2617
+ return "npm install -g skillwiki@latest";
2618
+ }
2619
+ if (plugin.channel === "claude") {
2620
+ return "claude plugin update skillwiki@llm-wiki";
2621
+ }
2622
+ if (plugin.sourceType === "git") {
2623
+ return "codex plugin marketplace upgrade llm-wiki && codex plugin remove skillwiki@llm-wiki && codex plugin add skillwiki@llm-wiki";
2624
+ }
2625
+ return "codex plugin remove skillwiki@llm-wiki && codex plugin add skillwiki@llm-wiki";
2626
+ }
2627
+ function checkPluginVersionDrift(home, currentVersion, devSourceRun) {
2628
+ const plugins = findPluginInstallations(home);
2629
+ if (plugins.length === 0) {
2630
+ return check("pass", "plugin_version_drift", "Plugin/CLI version", "Plugin not installed \u2014 CLI only");
2631
+ }
2632
+ const drifted = plugins.filter((plugin) => plugin.version !== currentVersion);
2633
+ if (drifted.length === 0) {
2634
+ if (plugins.length === 1 && plugins[0].channel === "claude") {
2635
+ return check("pass", "plugin_version_drift", "Plugin/CLI version", `Both at v${currentVersion}`);
2636
+ }
2637
+ if (plugins.length === 1) {
2638
+ return check("pass", "plugin_version_drift", "Plugin/CLI version", `${plugins[0].label} plugin and CLI both at v${currentVersion}`);
2639
+ }
2640
+ const labels = plugins.map((plugin) => `${plugin.label} plugin`).join(", ");
2641
+ return check("pass", "plugin_version_drift", "Plugin/CLI version", `${labels}, and CLI all at v${currentVersion}`);
2642
+ }
2643
+ if (devSourceRun && drifted.every((plugin) => semverGt(currentVersion, plugin.version))) {
2644
+ const details2 = drifted.map((plugin) => `${plugin.label} plugin v${plugin.version}`).join(", ");
2645
+ return check("info", "plugin_version_drift", "Plugin/CLI version", `Dev source v${currentVersion} is ahead of installed ${details2}`);
2646
+ }
2647
+ const details = drifted.map((plugin) => {
2648
+ const updateCmd = pluginUpdateCommand(plugin, currentVersion);
2649
+ return `${plugin.label} plugin v${plugin.version} \u2260 CLI v${currentVersion} \u2014 run \`${updateCmd}\``;
2650
+ });
2651
+ return check(
2652
+ "warn",
2653
+ "plugin_version_drift",
2654
+ "Plugin/CLI version",
2655
+ details.join("; ")
2656
+ );
2592
2657
  }
2658
+ var skillsPluginsProbe = {
2659
+ id: "skills_plugins",
2660
+ run(ctx) {
2661
+ return [
2662
+ checkSkillsInstalled(ctx.input.home, ctx.input.cwd),
2663
+ checkDuplicateSkills(ctx.input.home),
2664
+ checkGrokActivation(ctx.input.home, ctx.input.cwd),
2665
+ checkNpmUpdate(ctx.input.home, ctx.input.currentVersion),
2666
+ checkPluginVersionDrift(ctx.input.home, ctx.input.currentVersion, ctx.devSourceRun)
2667
+ ];
2668
+ }
2669
+ };
2670
+
2671
+ // src/doctor/probes/vault-sync.ts
2672
+ import { existsSync as existsSync12, readFileSync as readFileSync7 } from "fs";
2673
+ import { join as join15 } from "path";
2674
+ import { execSync as execSync5 } from "child_process";
2675
+ import { platform as platform3 } from "os";
2593
2676
  function readVaultSyncConfig(home) {
2594
2677
  try {
2595
- const content = readFileSync6(join10(home, ".skillwiki", ".env"), "utf8");
2678
+ const content = readFileSync7(join15(home, ".skillwiki", ".env"), "utf8");
2596
2679
  let installed = false;
2597
2680
  let role;
2598
2681
  let serviceScope;
@@ -2615,12 +2698,6 @@ function readVaultSyncConfig(home) {
2615
2698
  return { installed: false };
2616
2699
  }
2617
2700
  }
2618
- function resolveSnapshotGitWorktree(home) {
2619
- const configured = resolveConfiguredSnapshotWorktree(home);
2620
- if (configured) return configured;
2621
- const defaultPath = "/root/wiki-git";
2622
- return existsSync7(defaultPath) ? defaultPath : void 0;
2623
- }
2624
2701
  function normalizeSystemdValue(raw) {
2625
2702
  if (raw == null) return void 0;
2626
2703
  const v = raw.trim();
@@ -2632,9 +2709,9 @@ function hasCompletedRunEvidence(...timestamps) {
2632
2709
  }
2633
2710
  function loadSnapshotFixture(env) {
2634
2711
  const path = env.VS_SNAPSHOT_HEALTH_FIXTURE;
2635
- if (!path || !existsSync7(path)) return null;
2712
+ if (!path || !existsSync12(path)) return null;
2636
2713
  try {
2637
- return JSON.parse(readFileSync6(path, "utf8"));
2714
+ return JSON.parse(readFileSync7(path, "utf8"));
2638
2715
  } catch {
2639
2716
  return null;
2640
2717
  }
@@ -2642,7 +2719,7 @@ function loadSnapshotFixture(env) {
2642
2719
  function systemctlShowProperty(scope, unit, prop) {
2643
2720
  try {
2644
2721
  const cmd = scope === "system" ? `systemctl show ${unit} --property=${prop} --value` : `systemctl --user show ${unit} --property=${prop} --value`;
2645
- const out = execSync2(cmd, {
2722
+ const out = execSync5(cmd, {
2646
2723
  encoding: "utf8",
2647
2724
  timeout: 2e3,
2648
2725
  stdio: ["pipe", "pipe", "pipe"]
@@ -2721,7 +2798,7 @@ function snapshotterHealthChecks(scope, logDir, env) {
2721
2798
  let completionOutcome = "unknown";
2722
2799
  const logRecords = fixture ? fixture.log_records : (() => {
2723
2800
  try {
2724
- const content = readFileSync6(join10(logDir, "wiki-snapshot.log"), "utf8");
2801
+ const content = readFileSync7(join15(logDir, "wiki-snapshot.log"), "utf8");
2725
2802
  return content.split(/\r?\n/).filter(Boolean);
2726
2803
  } catch {
2727
2804
  return [];
@@ -2775,7 +2852,7 @@ function snapshotterHealthChecks(scope, logDir, env) {
2775
2852
  return [jobs, serviceResult, freshness, consecutiveFailures];
2776
2853
  }
2777
2854
  function vaultSyncChecks(input) {
2778
- const os = input.os ?? platform2();
2855
+ const os = input.os ?? platform3();
2779
2856
  const home = input.home;
2780
2857
  if (!input.vaultSyncInstalled) {
2781
2858
  const skip = (id, label) => check("pass", id, label, "vault-sync not installed \u2014 check skipped");
@@ -2791,14 +2868,14 @@ function vaultSyncChecks(input) {
2791
2868
  ];
2792
2869
  }
2793
2870
  const isMac = os === "darwin";
2794
- const logDir = input.logDir ?? (isMac ? join10(home, "Library", "Logs") : join10(home, ".local", "state", "vault-sync", "log"));
2795
- const shareDir = input.shareDir ?? (isMac ? join10(home, "Library", "Application Support", "vault-sync", "bin") : join10(home, ".local", "share", "vault-sync", "bin"));
2796
- const filterPath = input.filterPath ?? join10(home, ".config", "rclone", "wiki-push-filters.txt");
2797
- const packagedSnapshotPath = join10(shareDir, "wiki-snapshot.sh");
2871
+ const logDir = input.logDir ?? (isMac ? join15(home, "Library", "Logs") : join15(home, ".local", "state", "vault-sync", "log"));
2872
+ const shareDir = input.shareDir ?? (isMac ? join15(home, "Library", "Application Support", "vault-sync", "bin") : join15(home, ".local", "share", "vault-sync", "bin"));
2873
+ const filterPath = input.filterPath ?? join15(home, ".config", "rclone", "wiki-push-filters.txt");
2874
+ const packagedSnapshotPath = join15(shareDir, "wiki-snapshot.sh");
2798
2875
  const legacySnapshotPath = "/root/.hermes/scripts/wiki-snapshot-v3.sh";
2799
- const snapshotPath = input.snapshotScriptPath ?? (existsSync7(packagedSnapshotPath) ? packagedSnapshotPath : legacySnapshotPath);
2876
+ const snapshotPath = input.snapshotScriptPath ?? (existsSync12(packagedSnapshotPath) ? packagedSnapshotPath : legacySnapshotPath);
2800
2877
  if (input.vaultSyncRole === "snapshotter") {
2801
- const c12 = existsSync7(snapshotPath) ? check("pass", "vault_sync_installed", "Vault sync installed", `Found snapshot script: ${snapshotPath}`) : check("error", "vault_sync_installed", "Vault sync installed", `Snapshot script not found at ${snapshotPath}`);
2878
+ const c12 = existsSync12(snapshotPath) ? check("pass", "vault_sync_installed", "Vault sync installed", `Found snapshot script: ${snapshotPath}`) : check("error", "vault_sync_installed", "Vault sync installed", `Snapshot script not found at ${snapshotPath}`);
2802
2879
  const serviceScope = input.vaultSyncServiceScope ?? "user";
2803
2880
  const healthChecks = snapshotterHealthChecks(serviceScope, logDir, input.env ?? process.env);
2804
2881
  const cFetch2 = check(
@@ -2815,7 +2892,7 @@ function vaultSyncChecks(input) {
2815
2892
  );
2816
2893
  let c52;
2817
2894
  try {
2818
- if (!existsSync7(snapshotPath)) {
2895
+ if (!existsSync12(snapshotPath)) {
2819
2896
  c52 = check(
2820
2897
  "error",
2821
2898
  "vault_sync_snapshot_guard",
@@ -2823,7 +2900,7 @@ function vaultSyncChecks(input) {
2823
2900
  `Snapshot script not found at ${snapshotPath}`
2824
2901
  );
2825
2902
  } else {
2826
- const content = readFileSync6(snapshotPath, "utf8");
2903
+ const content = readFileSync7(snapshotPath, "utf8");
2827
2904
  if (!content.includes("--max-delete")) {
2828
2905
  c52 = check(
2829
2906
  "error",
@@ -2832,7 +2909,263 @@ function vaultSyncChecks(input) {
2832
2909
  `${snapshotPath} is missing --max-delete guard \u2014 dangerous without it`
2833
2910
  );
2834
2911
  } else {
2835
- c52 = check(
2912
+ c52 = check(
2913
+ "pass",
2914
+ "vault_sync_snapshot_guard",
2915
+ "Snapshot script guard",
2916
+ `--max-delete present in ${snapshotPath}`
2917
+ );
2918
+ }
2919
+ }
2920
+ } catch {
2921
+ c52 = check(
2922
+ "error",
2923
+ "vault_sync_snapshot_guard",
2924
+ "Snapshot script guard",
2925
+ `Cannot read ${snapshotPath}`
2926
+ );
2927
+ }
2928
+ return [c12, ...healthChecks, cFetch2, c42, c52];
2929
+ }
2930
+ const pushScriptPath = join15(shareDir, "wiki-push.sh");
2931
+ const c1 = existsSync12(pushScriptPath) ? check("pass", "vault_sync_installed", "Vault sync installed", `Found: ${pushScriptPath}`) : check("error", "vault_sync_installed", "Vault sync installed", `Script not found at ${pushScriptPath} \u2014 run vault-sync-install`);
2932
+ let c2;
2933
+ try {
2934
+ if (isMac) {
2935
+ const uidStr = execSync5("id -u", {
2936
+ encoding: "utf8",
2937
+ timeout: 2e3,
2938
+ stdio: ["pipe", "pipe", "pipe"]
2939
+ }).trim();
2940
+ const uid = parseInt(uidStr, 10);
2941
+ execSync5(`launchctl print gui/${uid}/com.karlchow.wiki-push`, {
2942
+ encoding: "utf8",
2943
+ timeout: 2e3,
2944
+ stdio: ["pipe", "ignore", "ignore"]
2945
+ });
2946
+ c2 = check(
2947
+ "pass",
2948
+ "vault_sync_jobs_enabled",
2949
+ "Vault sync jobs enabled",
2950
+ "launchd: com.karlchow.wiki-push loaded"
2951
+ );
2952
+ } else {
2953
+ const out = execSync5("systemctl --user is-enabled wiki-push.timer", {
2954
+ encoding: "utf8",
2955
+ timeout: 2e3,
2956
+ stdio: ["pipe", "pipe", "pipe"]
2957
+ }).trim();
2958
+ if (out === "enabled") {
2959
+ c2 = check(
2960
+ "pass",
2961
+ "vault_sync_jobs_enabled",
2962
+ "Vault sync jobs enabled",
2963
+ "systemd: wiki-push.timer enabled"
2964
+ );
2965
+ } else {
2966
+ c2 = check(
2967
+ "error",
2968
+ "vault_sync_jobs_enabled",
2969
+ "Vault sync jobs enabled",
2970
+ `systemd: wiki-push.timer is ${out} \u2014 run vault-sync-install`
2971
+ );
2972
+ }
2973
+ }
2974
+ } catch {
2975
+ c2 = check(
2976
+ "error",
2977
+ "vault_sync_jobs_enabled",
2978
+ "Vault sync jobs enabled",
2979
+ "Scheduler check failed \u2014 run vault-sync-install"
2980
+ );
2981
+ }
2982
+ const logFile = join15(logDir, "wiki-push.log");
2983
+ let c3;
2984
+ try {
2985
+ const logContent = readFileSync7(logFile, "utf8");
2986
+ const lines = logContent.trim().split("\n").filter(Boolean);
2987
+ if (lines.length === 0) {
2988
+ c3 = check(
2989
+ "warn",
2990
+ "vault_sync_last_push_age",
2991
+ "Vault sync last push recency",
2992
+ "Log file is empty"
2993
+ );
2994
+ } else {
2995
+ const lastLine = [...lines].reverse().find((line) => /FAIL|OK push/.test(line)) ?? lines[lines.length - 1];
2996
+ if (/FAIL/.test(lastLine)) {
2997
+ c3 = check(
2998
+ "error",
2999
+ "vault_sync_last_push_age",
3000
+ "Vault sync last push recency",
3001
+ `Last push failed: ${lastLine}`
3002
+ );
3003
+ } else if (/OK push/.test(lastLine)) {
3004
+ const tsMatch = lastLine.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)/);
3005
+ if (tsMatch) {
3006
+ const lastPush = new Date(tsMatch[1]).getTime();
3007
+ const ageSec = (Date.now() - lastPush) / 1e3;
3008
+ if (ageSec <= 180) {
3009
+ c3 = check(
3010
+ "pass",
3011
+ "vault_sync_last_push_age",
3012
+ "Vault sync last push recency",
3013
+ `Last push ${ageSec.toFixed(0)}s ago`
3014
+ );
3015
+ } else {
3016
+ c3 = check(
3017
+ "warn",
3018
+ "vault_sync_last_push_age",
3019
+ "Vault sync last push recency",
3020
+ `Last push ${Math.round(ageSec)}s ago (>3 min)`
3021
+ );
3022
+ }
3023
+ } else {
3024
+ c3 = check(
3025
+ "warn",
3026
+ "vault_sync_last_push_age",
3027
+ "Vault sync last push recency",
3028
+ `Unparseable push line: ${lastLine.slice(0, 80)}`
3029
+ );
3030
+ }
3031
+ } else {
3032
+ c3 = check(
3033
+ "warn",
3034
+ "vault_sync_last_push_age",
3035
+ "Vault sync last push recency",
3036
+ `Last log entry: ${lastLine.slice(0, 80)}`
3037
+ );
3038
+ }
3039
+ }
3040
+ } catch {
3041
+ c3 = existsSync12(logDir) ? check(
3042
+ "warn",
3043
+ "vault_sync_last_push_age",
3044
+ "Vault sync last push recency",
3045
+ `Log file not found at ${logFile}`
3046
+ ) : check(
3047
+ "error",
3048
+ "vault_sync_last_push_age",
3049
+ "Vault sync last push recency",
3050
+ `Log directory not found at ${logDir}`
3051
+ );
3052
+ }
3053
+ const fetchLogFile = join15(logDir, "wiki-fetch.log");
3054
+ let cFetch;
3055
+ try {
3056
+ const logContent = readFileSync7(fetchLogFile, "utf8");
3057
+ const lines = logContent.trim().split("\n").filter(Boolean);
3058
+ if (lines.length === 0) {
3059
+ cFetch = check(
3060
+ "warn",
3061
+ "vault_sync_last_fetch_status",
3062
+ "Vault sync last fetch status",
3063
+ "Fetch log file is empty"
3064
+ );
3065
+ } else {
3066
+ const lastLine = lines[lines.length - 1];
3067
+ if (/fetch failed/i.test(lastLine)) {
3068
+ cFetch = check(
3069
+ "error",
3070
+ "vault_sync_last_fetch_status",
3071
+ "Vault sync last fetch status",
3072
+ `Last fetch failed: ${lastLine.slice(0, 100)}`
3073
+ );
3074
+ } else if (/OK/.test(lastLine)) {
3075
+ cFetch = check(
3076
+ "pass",
3077
+ "vault_sync_last_fetch_status",
3078
+ "Vault sync last fetch status",
3079
+ lastLine.slice(0, 100)
3080
+ );
3081
+ } else {
3082
+ cFetch = check(
3083
+ "warn",
3084
+ "vault_sync_last_fetch_status",
3085
+ "Vault sync last fetch status",
3086
+ `Last fetch log entry: ${lastLine.slice(0, 80)}`
3087
+ );
3088
+ }
3089
+ }
3090
+ } catch {
3091
+ cFetch = check(
3092
+ "warn",
3093
+ "vault_sync_last_fetch_status",
3094
+ "Vault sync last fetch status",
3095
+ `Fetch log not found at ${fetchLogFile}`
3096
+ );
3097
+ }
3098
+ let c4;
3099
+ try {
3100
+ if (!existsSync12(filterPath)) {
3101
+ c4 = check(
3102
+ "error",
3103
+ "vault_sync_filter_present",
3104
+ "Vault sync filter file present",
3105
+ `Filter file not found at ${filterPath}`
3106
+ );
3107
+ } else {
3108
+ const content = readFileSync7(filterPath, "utf8");
3109
+ const requiredExcludes = [
3110
+ "remotely-save/data.json",
3111
+ ".skillwiki/sync.lock",
3112
+ ".skillwiki/memory/",
3113
+ ".skillwiki/memory-topics.json",
3114
+ ".claude/settings.local.json"
3115
+ ];
3116
+ const missing = requiredExcludes.filter((ex) => !content.includes(ex));
3117
+ if (missing.length > 0) {
3118
+ c4 = check(
3119
+ "warn",
3120
+ "vault_sync_filter_present",
3121
+ "Vault sync filter file present",
3122
+ `Missing required excludes: ${missing.join(", ")}`
3123
+ );
3124
+ } else {
3125
+ c4 = check(
3126
+ "pass",
3127
+ "vault_sync_filter_present",
3128
+ "Vault sync filter file present",
3129
+ `Found with required excludes at ${filterPath}`
3130
+ );
3131
+ }
3132
+ }
3133
+ } catch {
3134
+ c4 = check(
3135
+ "error",
3136
+ "vault_sync_filter_present",
3137
+ "Vault sync filter file present",
3138
+ `Cannot read filter file at ${filterPath}`
3139
+ );
3140
+ }
3141
+ let c5;
3142
+ if (input.vaultSyncRole !== "snapshotter") {
3143
+ c5 = check(
3144
+ "pass",
3145
+ "vault_sync_snapshot_guard",
3146
+ "Snapshot script guard",
3147
+ "Not a snapshotter host \u2014 check skipped"
3148
+ );
3149
+ } else {
3150
+ try {
3151
+ if (!existsSync12(snapshotPath)) {
3152
+ c5 = check(
3153
+ "error",
3154
+ "vault_sync_snapshot_guard",
3155
+ "Snapshot script guard",
3156
+ `Snapshot script not found at ${snapshotPath}`
3157
+ );
3158
+ } else {
3159
+ const content = readFileSync7(snapshotPath, "utf8");
3160
+ if (!content.includes("--max-delete")) {
3161
+ c5 = check(
3162
+ "error",
3163
+ "vault_sync_snapshot_guard",
3164
+ "Snapshot script guard",
3165
+ `${snapshotPath} is missing --max-delete guard \u2014 dangerous without it`
3166
+ );
3167
+ } else {
3168
+ c5 = check(
2836
3169
  "pass",
2837
3170
  "vault_sync_snapshot_guard",
2838
3171
  "Snapshot script guard",
@@ -2841,464 +3174,643 @@ function vaultSyncChecks(input) {
2841
3174
  }
2842
3175
  }
2843
3176
  } catch {
2844
- c52 = check(
3177
+ c5 = check(
2845
3178
  "error",
2846
3179
  "vault_sync_snapshot_guard",
2847
3180
  "Snapshot script guard",
2848
3181
  `Cannot read ${snapshotPath}`
2849
3182
  );
2850
3183
  }
2851
- return [c12, ...healthChecks, cFetch2, c42, c52];
2852
3184
  }
2853
- const pushScriptPath = join10(shareDir, "wiki-push.sh");
2854
- const c1 = existsSync7(pushScriptPath) ? check("pass", "vault_sync_installed", "Vault sync installed", `Found: ${pushScriptPath}`) : check("error", "vault_sync_installed", "Vault sync installed", `Script not found at ${pushScriptPath} \u2014 run vault-sync-install`);
2855
- let c2;
3185
+ return [c1, c2, c3, cFetch, c4, c5];
3186
+ }
3187
+ function checkVaultSyncPullHelper(home, env) {
3188
+ const path = resolveVaultSyncPullHelper({
3189
+ vault: "",
3190
+ home,
3191
+ env
3192
+ });
3193
+ if (path) {
3194
+ return check("pass", "vault_sync_pull_helper", "Vault-sync pull helper", `Resolved: ${path}`);
3195
+ }
3196
+ return check(
3197
+ "error",
3198
+ "vault_sync_pull_helper",
3199
+ "Vault-sync pull helper",
3200
+ "Not found \u2014 install skillwiki@0.10.1+, redeploy vault-sync, or set SKILLWIKI_VAULT_SYNC_PULL_HELPER"
3201
+ );
3202
+ }
3203
+ function checkVaultSyncReviewRequiredJournals(vaultPath) {
3204
+ if (!vaultPath || !existsSync12(join15(vaultPath, ".git"))) {
3205
+ return check("pass", "vault_sync_review_required_journals", "Review-required journals", "No git vault \u2014 check skipped");
3206
+ }
2856
3207
  try {
2857
- if (isMac) {
2858
- const uidStr = execSync2("id -u", {
2859
- encoding: "utf8",
2860
- timeout: 2e3,
2861
- stdio: ["pipe", "pipe", "pipe"]
2862
- }).trim();
2863
- const uid = parseInt(uidStr, 10);
2864
- execSync2(`launchctl print gui/${uid}/com.karlchow.wiki-push`, {
2865
- encoding: "utf8",
2866
- timeout: 2e3,
2867
- stdio: ["pipe", "ignore", "ignore"]
2868
- });
2869
- c2 = check(
2870
- "pass",
2871
- "vault_sync_jobs_enabled",
2872
- "Vault sync jobs enabled",
2873
- "launchd: com.karlchow.wiki-push loaded"
3208
+ const ops = listReviewRequiredOps(vaultPath);
3209
+ if (ops.length === 0) {
3210
+ return check("pass", "vault_sync_review_required_journals", "Review-required journals", "None");
3211
+ }
3212
+ const sample = ops[0]?.opId ?? "?";
3213
+ return check(
3214
+ "warn",
3215
+ "vault_sync_review_required_journals",
3216
+ "Review-required journals",
3217
+ `${ops.length} handoff(s); oldest/sample: ${sample} \u2014 if worktree clean: skillwiki sync journal clear-stale --dry-run`
3218
+ );
3219
+ } catch {
3220
+ return check("pass", "vault_sync_review_required_journals", "Review-required journals", "Could not read journals \u2014 check skipped");
3221
+ }
3222
+ }
3223
+ var vaultSyncProbe = {
3224
+ id: "vault_sync",
3225
+ run(ctx) {
3226
+ const checks = [];
3227
+ checks.push(...vaultSyncChecks({
3228
+ home: ctx.input.home,
3229
+ vaultSyncInstalled: ctx.vsConfig.installed,
3230
+ vaultSyncRole: ctx.vsConfig.role,
3231
+ vaultSyncServiceScope: ctx.vsConfig.serviceScope,
3232
+ snapshotScriptPath: ctx.vsConfig.snapshotScript,
3233
+ env: ctx.input.env ?? process.env
3234
+ }));
3235
+ checks.push(checkVaultSyncPullHelper(ctx.input.home, ctx.input.env ?? process.env));
3236
+ checks.push(checkVaultSyncReviewRequiredJournals(ctx.resolvedPath));
3237
+ return checks;
3238
+ }
3239
+ };
3240
+
3241
+ // src/doctor/probes/satellite.ts
3242
+ import { existsSync as existsSync14 } from "fs";
3243
+ import { execSync as execSync6 } from "child_process";
3244
+ import { platform as platform4 } from "os";
3245
+
3246
+ // src/utils/satellite-run-health.ts
3247
+ import { existsSync as existsSync13, readFileSync as readFileSync8 } from "fs";
3248
+ import { join as join16 } from "path";
3249
+ var SATELLITE_STALE_MS = 26 * 60 * 60 * 1e3;
3250
+ function satelliteLatestRunPath(vault) {
3251
+ return join16(vault, ".skillwiki", "agent-memory-trends", "latest-run.json");
3252
+ }
3253
+ function isFailedRunStatus(status) {
3254
+ return status === "fail" || status === "failure";
3255
+ }
3256
+ function parseLatestRunFile(text) {
3257
+ try {
3258
+ const parsed = JSON.parse(text);
3259
+ const status = typeof parsed.status === "string" ? parsed.status : "";
3260
+ if (!status) return null;
3261
+ const finishedAt = typeof parsed.finished_at === "string" && parsed.finished_at.length > 0 ? parsed.finished_at : void 0;
3262
+ const failureClass = parsed.failure_class != null && String(parsed.failure_class).length > 0 ? String(parsed.failure_class) : void 0;
3263
+ return { status, finishedAt, failureClass };
3264
+ } catch {
3265
+ return null;
3266
+ }
3267
+ }
3268
+ function readSatelliteLatestRunFromText(text) {
3269
+ return parseLatestRunFile(text);
3270
+ }
3271
+ function readSatelliteLatestRun(vault) {
3272
+ const latestPath = satelliteLatestRunPath(vault);
3273
+ if (!existsSync13(latestPath)) return null;
3274
+ try {
3275
+ return parseLatestRunFile(readFileSync8(latestPath, "utf8"));
3276
+ } catch {
3277
+ return null;
3278
+ }
3279
+ }
3280
+ function evaluateSatelliteRunHealth(vault, now) {
3281
+ const run = readSatelliteLatestRun(vault);
3282
+ if (!run) {
3283
+ return { failed: false, stale: false };
3284
+ }
3285
+ const failed = isFailedRunStatus(run.status);
3286
+ let stale = false;
3287
+ if (!failed && run.finishedAt) {
3288
+ const ts = Date.parse(run.finishedAt);
3289
+ if (Number.isFinite(ts) && now.getTime() - ts > SATELLITE_STALE_MS) {
3290
+ stale = true;
3291
+ }
3292
+ }
3293
+ return {
3294
+ failed,
3295
+ stale,
3296
+ failureClass: run.failureClass,
3297
+ finishedAt: run.finishedAt
3298
+ };
3299
+ }
3300
+
3301
+ // src/doctor/probes/satellite.ts
3302
+ function checkSatelliteLastRun(vaultPath, satelliteExpected) {
3303
+ if (!satelliteExpected) {
3304
+ return check("pass", "satellite_job_last_run", "Satellite job last run", "Satellite job not expected on this host");
3305
+ }
3306
+ if (vaultPath === void 0) {
3307
+ return check("pass", "satellite_job_last_run", "Satellite job last run", "No vault path \u2014 check skipped");
3308
+ }
3309
+ const latestPath = satelliteLatestRunPath(vaultPath);
3310
+ if (!existsSync14(latestPath)) {
3311
+ return check("pass", "satellite_job_last_run", "Satellite job last run", "No latest-run.json \u2014 satellite has not run yet");
3312
+ }
3313
+ try {
3314
+ const health = evaluateSatelliteRunHealth(vaultPath, /* @__PURE__ */ new Date());
3315
+ if (health.failed) {
3316
+ const fc = health.failureClass;
3317
+ const detail = fc ? `Last satellite run failed (failure_class: ${fc})` : "Last satellite run failed";
3318
+ return check("error", "satellite_job_last_run", "Satellite job last run", detail);
3319
+ }
3320
+ if (health.stale && health.finishedAt) {
3321
+ return check(
3322
+ "warn",
3323
+ "satellite_job_last_run",
3324
+ "Satellite job last run",
3325
+ `Last run finished_at is older than 26h (${health.finishedAt})`
2874
3326
  );
2875
- } else {
2876
- const out = execSync2("systemctl --user is-enabled wiki-push.timer", {
2877
- encoding: "utf8",
2878
- timeout: 2e3,
2879
- stdio: ["pipe", "pipe", "pipe"]
2880
- }).trim();
2881
- if (out === "enabled") {
2882
- c2 = check(
2883
- "pass",
2884
- "vault_sync_jobs_enabled",
2885
- "Vault sync jobs enabled",
2886
- "systemd: wiki-push.timer enabled"
2887
- );
2888
- } else {
2889
- c2 = check(
2890
- "error",
2891
- "vault_sync_jobs_enabled",
2892
- "Vault sync jobs enabled",
2893
- `systemd: wiki-push.timer is ${out} \u2014 run vault-sync-install`
2894
- );
3327
+ }
3328
+ return check(
3329
+ "pass",
3330
+ "satellite_job_last_run",
3331
+ "Satellite job last run",
3332
+ health.finishedAt ? `Last run ok (finished_at ${health.finishedAt})` : "Last run ok"
3333
+ );
3334
+ } catch {
3335
+ return check("warn", "satellite_job_last_run", "Satellite job last run", `Could not read ${latestPath}`);
3336
+ }
3337
+ }
3338
+ function defaultSatelliteTimerDeps() {
3339
+ return {
3340
+ platform: () => platform4(),
3341
+ systemctlIsActive: (unit) => {
3342
+ try {
3343
+ return execSync6(`systemctl is-active ${unit}`, {
3344
+ encoding: "utf8",
3345
+ timeout: 2e3,
3346
+ stdio: ["pipe", "pipe", "pipe"]
3347
+ }).trim();
3348
+ } catch {
3349
+ return void 0;
2895
3350
  }
2896
3351
  }
3352
+ };
3353
+ }
3354
+ function checkSatelliteTimer(satelliteExpected, deps = defaultSatelliteTimerDeps()) {
3355
+ if (!satelliteExpected) {
3356
+ return check("pass", "satellite_job_timer", "Satellite job timer", "Satellite job not expected on this host");
3357
+ }
3358
+ if (deps.platform() !== "linux") {
3359
+ return check("pass", "satellite_job_timer", "Satellite job timer", "Timer check skipped \u2014 Linux only");
3360
+ }
3361
+ const out = deps.systemctlIsActive("agent-memory-trends.timer");
3362
+ if (out === void 0) {
3363
+ return check("pass", "satellite_job_timer", "Satellite job timer", "systemctl unavailable");
3364
+ }
3365
+ if (out === "active") {
3366
+ return check("pass", "satellite_job_timer", "Satellite job timer", "systemd: agent-memory-trends.timer active");
3367
+ }
3368
+ return check(
3369
+ "error",
3370
+ "satellite_job_timer",
3371
+ "Satellite job timer",
3372
+ `systemd: agent-memory-trends.timer is ${out || "not active"}`
3373
+ );
3374
+ }
3375
+ var satelliteProbe = {
3376
+ id: "satellite",
3377
+ run(ctx) {
3378
+ return [
3379
+ checkSatelliteLastRun(ctx.resolvedPath, ctx.satelliteGate.satelliteExpected),
3380
+ checkSatelliteTimer(ctx.satelliteGate.satelliteExpected)
3381
+ ];
3382
+ }
3383
+ };
3384
+
3385
+ // src/doctor/probes/metrics.ts
3386
+ import { readFileSync as readFileSync9 } from "fs";
3387
+ import { join as join17 } from "path";
3388
+ var METRIC_TYPES = ["entities", "concepts", "comparisons", "queries", "meta"];
3389
+ function doctorReadOnlyScanRoot(resolvedPath) {
3390
+ return resolveReadOnlyVaultRoot(resolvedPath).root;
3391
+ }
3392
+ async function vaultMetrics(resolvedPath) {
3393
+ const ids = [
3394
+ ["vault_metric_pages", "Vault pages by type"],
3395
+ ["vault_metric_orphans", "Vault orphan rate"],
3396
+ ["vault_metric_bridges", "Vault bridge count"],
3397
+ ["vault_metric_cohesion", "Mean community cohesion"],
3398
+ ["vault_metric_log_size", "Vault log size"]
3399
+ ];
3400
+ const noVault = () => ids.map(([id, label]) => check("info", id, label, "no vault configured"));
3401
+ if (!resolvedPath) return noVault();
3402
+ const scanRoot = doctorReadOnlyScanRoot(resolvedPath);
3403
+ const scan = await scanVault(scanRoot);
3404
+ if (!scan.ok) return noVault();
3405
+ const tk = scan.data.typedKnowledge;
3406
+ const typedCount = tk.length;
3407
+ const perType = METRIC_TYPES.map((d) => `${d} ${tk.filter((p) => p.relPath.startsWith(d + "/")).length}`).join(", ");
3408
+ const adj = await buildWikilinkAdjacency(tk, void 0, scan.data.allMarkdown);
3409
+ const g = toUndirectedWeighted(adj);
3410
+ const nodes = [...g.keys()];
3411
+ const total = nodes.length;
3412
+ const orphanCount = nodes.filter((n) => g.get(n).size === 0).length;
3413
+ const orphanRate = total > 0 ? Math.round(orphanCount / total * 1e3) / 10 : 0;
3414
+ const comm = louvain(g);
3415
+ const groups = /* @__PURE__ */ new Map();
3416
+ for (const [node, c] of comm) {
3417
+ const arr = groups.get(c);
3418
+ if (arr) arr.push(node);
3419
+ else groups.set(c, [node]);
3420
+ }
3421
+ const cohesions = [...groups.values()].filter((m) => m.length >= 2).map((m) => communityCohesion(m, g));
3422
+ const meanCohesion = cohesions.length > 0 ? Math.round(cohesions.reduce((a, b) => a + b, 0) / cohesions.length * 1e3) / 1e3 : 0;
3423
+ let bridges = 0;
3424
+ for (const n of nodes) {
3425
+ const nbrComms = /* @__PURE__ */ new Set();
3426
+ for (const nb of g.get(n).keys()) nbrComms.add(comm.get(nb));
3427
+ if (nbrComms.size >= 3) bridges++;
3428
+ }
3429
+ let logLines = 0;
3430
+ try {
3431
+ logLines = readFileSync9(join17(scanRoot, "log.md"), "utf8").split("\n").length;
2897
3432
  } catch {
2898
- c2 = check(
2899
- "error",
2900
- "vault_sync_jobs_enabled",
2901
- "Vault sync jobs enabled",
2902
- "Scheduler check failed \u2014 run vault-sync-install"
3433
+ }
3434
+ return [
3435
+ check("info", "vault_metric_pages", "Vault pages by type", `${total} graph node(s) (${typedCount} typed; ${perType})`),
3436
+ check("info", "vault_metric_orphans", "Vault orphan rate", `${orphanRate}% (${orphanCount}/${total} degree-0)`),
3437
+ check("info", "vault_metric_bridges", "Vault bridge count", `${bridges} page(s) link >= 3 communities`),
3438
+ check("info", "vault_metric_cohesion", "Mean community cohesion", `${meanCohesion} across ${cohesions.length} communities (size >= 2)`),
3439
+ check("info", "vault_metric_log_size", "Vault log size", `${logLines} lines`)
3440
+ ];
3441
+ }
3442
+ var metricsProbe = {
3443
+ id: "metrics",
3444
+ async run(ctx) {
3445
+ return await vaultMetrics(ctx.resolvedPath);
3446
+ }
3447
+ };
3448
+
3449
+ // src/doctor/probes/fuse-staleness.ts
3450
+ import { platform as platform5 } from "os";
3451
+ var MAX_DIR_CACHE_TIME_SECONDS2 = 15 * 60;
3452
+ function defaultFuseStalenessDeps() {
3453
+ return {
3454
+ platform: () => platform5(),
3455
+ detectFuseMount: (vp) => detectFuseMount(vp),
3456
+ findRcloneMountPid: () => findRcloneMountPid(),
3457
+ parseRcloneFlags: (pid) => parseRcloneFlags(pid),
3458
+ getRcloneArgs: (pid) => getRcloneArgs(pid),
3459
+ queryRcloneRC: (rcAddr, fs) => queryRcloneRC(rcAddr, fs)
3460
+ };
3461
+ }
3462
+ function formatDurationForHumans2(seconds) {
3463
+ if (!Number.isFinite(seconds)) return `${seconds}s`;
3464
+ if (seconds >= 3600) return `${(seconds / 3600).toFixed(1)}h`;
3465
+ if (seconds >= 60) return `${(seconds / 60).toFixed(1)}m`;
3466
+ if (seconds >= 1) return `${seconds.toFixed(1)}s`;
3467
+ return `${Math.round(seconds * 1e3)}ms`;
3468
+ }
3469
+ function checkFuseStaleness(resolvedPath, deps = {}) {
3470
+ const resolvedDeps = { ...defaultFuseStalenessDeps(), ...deps };
3471
+ const os = resolvedDeps.platform();
3472
+ if (os !== "linux") {
3473
+ return check(
3474
+ "pass",
3475
+ "fuse_staleness",
3476
+ "FUSE visibility freshness",
3477
+ `Non-Linux host (${os}) \u2014 check skipped`
2903
3478
  );
2904
3479
  }
2905
- const logFile = join10(logDir, "wiki-push.log");
2906
- let c3;
2907
- try {
2908
- const logContent = readFileSync6(logFile, "utf8");
2909
- const lines = logContent.trim().split("\n").filter(Boolean);
2910
- if (lines.length === 0) {
2911
- c3 = check(
2912
- "warn",
2913
- "vault_sync_last_push_age",
2914
- "Vault sync last push recency",
2915
- "Log file is empty"
2916
- );
2917
- } else {
2918
- const lastLine = [...lines].reverse().find((line) => /FAIL|OK push/.test(line)) ?? lines[lines.length - 1];
2919
- if (/FAIL/.test(lastLine)) {
2920
- c3 = check(
2921
- "error",
2922
- "vault_sync_last_push_age",
2923
- "Vault sync last push recency",
2924
- `Last push failed: ${lastLine}`
2925
- );
2926
- } else if (/OK push/.test(lastLine)) {
2927
- const tsMatch = lastLine.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)/);
2928
- if (tsMatch) {
2929
- const lastPush = new Date(tsMatch[1]).getTime();
2930
- const ageSec = (Date.now() - lastPush) / 1e3;
2931
- if (ageSec <= 180) {
2932
- c3 = check(
2933
- "pass",
2934
- "vault_sync_last_push_age",
2935
- "Vault sync last push recency",
2936
- `Last push ${ageSec.toFixed(0)}s ago`
2937
- );
2938
- } else {
2939
- c3 = check(
2940
- "warn",
2941
- "vault_sync_last_push_age",
2942
- "Vault sync last push recency",
2943
- `Last push ${Math.round(ageSec)}s ago (>3 min)`
2944
- );
2945
- }
2946
- } else {
2947
- c3 = check(
2948
- "warn",
2949
- "vault_sync_last_push_age",
2950
- "Vault sync last push recency",
2951
- `Unparseable push line: ${lastLine.slice(0, 80)}`
2952
- );
2953
- }
2954
- } else {
2955
- c3 = check(
2956
- "warn",
2957
- "vault_sync_last_push_age",
2958
- "Vault sync last push recency",
2959
- `Last log entry: ${lastLine.slice(0, 80)}`
2960
- );
2961
- }
2962
- }
2963
- } catch {
2964
- c3 = existsSync7(logDir) ? check(
3480
+ if (!resolvedPath) {
3481
+ return check(
3482
+ "pass",
3483
+ "fuse_staleness",
3484
+ "FUSE visibility freshness",
3485
+ "No vault path \u2014 check skipped"
3486
+ );
3487
+ }
3488
+ const fuse = resolvedDeps.detectFuseMount(resolvedPath);
3489
+ if (!fuse) {
3490
+ return check(
3491
+ "pass",
3492
+ "fuse_staleness",
3493
+ "FUSE visibility freshness",
3494
+ "local disk (non-FUSE) \u2014 check skipped"
3495
+ );
3496
+ }
3497
+ const pid = resolvedDeps.findRcloneMountPid();
3498
+ if (pid === null) {
3499
+ return check(
2965
3500
  "warn",
2966
- "vault_sync_last_push_age",
2967
- "Vault sync last push recency",
2968
- `Log file not found at ${logFile}`
2969
- ) : check(
2970
- "error",
2971
- "vault_sync_last_push_age",
2972
- "Vault sync last push recency",
2973
- `Log directory not found at ${logDir}`
3501
+ "fuse_staleness",
3502
+ "FUSE visibility freshness",
3503
+ `S3 FUSE mount (${fuse.mountPoint}) but no rclone process found \u2014 cannot audit dir-cache freshness`
2974
3504
  );
2975
3505
  }
2976
- const fetchLogFile = join10(logDir, "wiki-fetch.log");
2977
- let cFetch;
2978
- try {
2979
- const logContent = readFileSync6(fetchLogFile, "utf8");
2980
- const lines = logContent.trim().split("\n").filter(Boolean);
2981
- if (lines.length === 0) {
2982
- cFetch = check(
3506
+ const flags = resolvedDeps.parseRcloneFlags(pid);
3507
+ const rawDirCache = flags.get("--dir-cache-time");
3508
+ let dirCachePassed = true;
3509
+ let dirCacheDetail = "";
3510
+ if (!rawDirCache) {
3511
+ dirCacheDetail = "PID " + pid + ": --dir-cache-time not set (rclone default 5m, within <=15m SLA)";
3512
+ } else {
3513
+ const seconds = parseDurationSeconds(rawDirCache);
3514
+ if (seconds === null) {
3515
+ return check(
2983
3516
  "warn",
2984
- "vault_sync_last_fetch_status",
2985
- "Vault sync last fetch status",
2986
- "Fetch log file is empty"
3517
+ "fuse_staleness",
3518
+ "FUSE visibility freshness",
3519
+ `PID ${pid}: could not parse --dir-cache-time=${rawDirCache}`
2987
3520
  );
3521
+ }
3522
+ if (seconds > MAX_DIR_CACHE_TIME_SECONDS2) {
3523
+ dirCachePassed = false;
3524
+ dirCacheDetail = `PID ${pid}: --dir-cache-time=${rawDirCache} (${formatDurationForHumans2(seconds)}) exceeds 15m SLA \u2014 external changes may remain invisible`;
2988
3525
  } else {
2989
- const lastLine = lines[lines.length - 1];
2990
- if (/fetch failed/i.test(lastLine)) {
2991
- cFetch = check(
2992
- "error",
2993
- "vault_sync_last_fetch_status",
2994
- "Vault sync last fetch status",
2995
- `Last fetch failed: ${lastLine.slice(0, 100)}`
2996
- );
2997
- } else if (/OK/.test(lastLine)) {
2998
- cFetch = check(
2999
- "pass",
3000
- "vault_sync_last_fetch_status",
3001
- "Vault sync last fetch status",
3002
- lastLine.slice(0, 100)
3003
- );
3004
- } else {
3005
- cFetch = check(
3006
- "warn",
3007
- "vault_sync_last_fetch_status",
3008
- "Vault sync last fetch status",
3009
- `Last fetch log entry: ${lastLine.slice(0, 80)}`
3010
- );
3011
- }
3526
+ dirCacheDetail = `PID ${pid}: --dir-cache-time=${rawDirCache} (${formatDurationForHumans2(seconds)}), within <=15m SLA`;
3012
3527
  }
3013
- } catch {
3014
- cFetch = check(
3015
- "warn",
3016
- "vault_sync_last_fetch_status",
3017
- "Vault sync last fetch status",
3018
- `Fetch log not found at ${fetchLogFile}`
3019
- );
3020
3528
  }
3021
- let c4;
3022
- try {
3023
- if (!existsSync7(filterPath)) {
3024
- c4 = check(
3025
- "error",
3026
- "vault_sync_filter_present",
3027
- "Vault sync filter file present",
3028
- `Filter file not found at ${filterPath}`
3029
- );
3030
- } else {
3031
- const content = readFileSync6(filterPath, "utf8");
3032
- const requiredExcludes = [
3033
- "remotely-save/data.json",
3034
- ".skillwiki/sync.lock",
3035
- ".skillwiki/memory/",
3036
- ".skillwiki/memory-topics.json",
3037
- ".claude/settings.local.json"
3038
- ];
3039
- const missing = requiredExcludes.filter((ex) => !content.includes(ex));
3040
- if (missing.length > 0) {
3041
- c4 = check(
3529
+ if (!dirCachePassed) {
3530
+ return check("warn", "fuse_staleness", "FUSE visibility freshness", dirCacheDetail);
3531
+ }
3532
+ if (flags.has("--rc")) {
3533
+ const rcAddr = flags.get("--rc-addr") || "127.0.0.1:5572";
3534
+ const args = resolvedDeps.getRcloneArgs(pid);
3535
+ const fs = extractRcloneFs(args) || "unknown:";
3536
+ const stats = resolvedDeps.queryRcloneRC(rcAddr, fs);
3537
+ if (stats) {
3538
+ if (stats.error) {
3539
+ return check("warn", "fuse_staleness", "FUSE visibility freshness", `${dirCacheDetail}; RC query error: ${stats.error}`);
3540
+ }
3541
+ const issues = [];
3542
+ if (stats.uploadsInProgress > 0) issues.push(`${stats.uploadsInProgress} upload(s) in progress`);
3543
+ if (stats.uploadsQueued > 10) issues.push(`${stats.uploadsQueued} upload(s) queued (backlog)`);
3544
+ if (stats.erroredFiles > 0) issues.push(`${stats.erroredFiles} errored file(s)`);
3545
+ if (stats.outOfSpace) issues.push("cache disk full");
3546
+ if (issues.length > 0) {
3547
+ return check(
3042
3548
  "warn",
3043
- "vault_sync_filter_present",
3044
- "Vault sync filter file present",
3045
- `Missing required excludes: ${missing.join(", ")}`
3046
- );
3047
- } else {
3048
- c4 = check(
3049
- "pass",
3050
- "vault_sync_filter_present",
3051
- "Vault sync filter file present",
3052
- `Found with required excludes at ${filterPath}`
3549
+ "fuse_staleness",
3550
+ "FUSE visibility freshness",
3551
+ `${dirCacheDetail}; VFS cache degradation: ${issues.join(", ")}`
3053
3552
  );
3054
3553
  }
3055
3554
  }
3555
+ }
3556
+ return check("pass", "fuse_staleness", "FUSE visibility freshness", dirCacheDetail);
3557
+ }
3558
+ var fuseStalenessProbe = {
3559
+ id: "fuse_staleness",
3560
+ run(ctx) {
3561
+ return [checkFuseStaleness(ctx.resolvedPath)];
3562
+ }
3563
+ };
3564
+
3565
+ // src/doctor/probes/activation-marker.ts
3566
+ import { existsSync as existsSync15, readFileSync as readFileSync10, readdirSync as readdirSync5 } from "fs";
3567
+ import { join as join18 } from "path";
3568
+ var GROK_ACTIVATION_REFERENCE2 = "Read @~/.grok/skillwiki.md for SkillWiki activation context.";
3569
+ var STALE_GROK_ACTIVATION_REFERENCE2 = "Read @skillwiki.md";
3570
+ var ACTIVATION_FIX_HINT2 = "run `npm run install:activation` from the llm-wiki repo";
3571
+ function findGrokActivationTemplate2(home, cwd) {
3572
+ if (cwd) {
3573
+ const src = join18(cwd, "packages", "skills", "using-skillwiki", "activation.md");
3574
+ if (existsSync15(src)) return src;
3575
+ const directSrc = join18(cwd, "skills", "using-skillwiki", "activation.md");
3576
+ if (existsSync15(directSrc)) return directSrc;
3577
+ }
3578
+ const pluginsRoot = join18(home, ".grok", "installed-plugins");
3579
+ if (!existsSync15(pluginsRoot)) return void 0;
3580
+ let entries;
3581
+ try {
3582
+ entries = readdirSync5(pluginsRoot, { withFileTypes: true });
3056
3583
  } catch {
3057
- c4 = check(
3058
- "error",
3059
- "vault_sync_filter_present",
3060
- "Vault sync filter file present",
3061
- `Cannot read filter file at ${filterPath}`
3062
- );
3584
+ return void 0;
3063
3585
  }
3064
- let c5;
3065
- if (input.vaultSyncRole !== "snapshotter") {
3066
- c5 = check(
3067
- "pass",
3068
- "vault_sync_snapshot_guard",
3069
- "Snapshot script guard",
3070
- "Not a snapshotter host \u2014 check skipped"
3071
- );
3586
+ for (const entry of entries) {
3587
+ if (!entry.isDirectory()) continue;
3588
+ const root = join18(pluginsRoot, entry.name);
3589
+ for (const rel of ["using-skillwiki/activation.md", "skills/using-skillwiki/activation.md"]) {
3590
+ const candidate = join18(root, rel);
3591
+ if (existsSync15(candidate)) return candidate;
3592
+ }
3593
+ }
3594
+ return void 0;
3595
+ }
3596
+ function checkActivationMarker(home, cwd) {
3597
+ const grokDir = join18(home, ".grok");
3598
+ if (!existsSync15(grokDir)) {
3599
+ return check("pass", "activation_marker", "Activation marker", "Not a Grok host \u2014 check skipped");
3600
+ }
3601
+ const activationPath = join18(grokDir, "skillwiki.md");
3602
+ const agentsPath = join18(grokDir, "AGENTS.md");
3603
+ const hasActivation = existsSync15(activationPath);
3604
+ const issues = [];
3605
+ if (!hasActivation) {
3606
+ issues.push("~/.grok/skillwiki.md missing");
3607
+ }
3608
+ if (!existsSync15(agentsPath)) {
3609
+ issues.push("~/.grok/AGENTS.md missing");
3072
3610
  } else {
3073
3611
  try {
3074
- if (!existsSync7(snapshotPath)) {
3075
- c5 = check(
3076
- "error",
3077
- "vault_sync_snapshot_guard",
3078
- "Snapshot script guard",
3079
- `Snapshot script not found at ${snapshotPath}`
3080
- );
3081
- } else {
3082
- const content = readFileSync6(snapshotPath, "utf8");
3083
- if (!content.includes("--max-delete")) {
3084
- c5 = check(
3085
- "error",
3086
- "vault_sync_snapshot_guard",
3087
- "Snapshot script guard",
3088
- `${snapshotPath} is missing --max-delete guard \u2014 dangerous without it`
3089
- );
3090
- } else {
3091
- c5 = check(
3092
- "pass",
3093
- "vault_sync_snapshot_guard",
3094
- "Snapshot script guard",
3095
- `--max-delete present in ${snapshotPath}`
3096
- );
3097
- }
3612
+ const agents = readFileSync10(agentsPath, "utf8");
3613
+ const hasBegin = agents.includes("<!-- skillwiki:begin -->");
3614
+ const hasExpected = agents.includes(GROK_ACTIVATION_REFERENCE2);
3615
+ const hasStale = agents.includes(STALE_GROK_ACTIVATION_REFERENCE2);
3616
+ if (!hasBegin) {
3617
+ issues.push("AGENTS.md marker missing");
3618
+ } else if (hasStale && !hasExpected) {
3619
+ issues.push("AGENTS.md marker is stale (@skillwiki.md)");
3620
+ } else if (!hasExpected) {
3621
+ issues.push("AGENTS.md marker is stale");
3098
3622
  }
3099
3623
  } catch {
3100
- c5 = check(
3101
- "error",
3102
- "vault_sync_snapshot_guard",
3103
- "Snapshot script guard",
3104
- `Cannot read ${snapshotPath}`
3105
- );
3624
+ issues.push("could not read ~/.grok/AGENTS.md");
3106
3625
  }
3107
3626
  }
3108
- return [c1, c2, c3, cFetch, c4, c5];
3627
+ if (hasActivation) {
3628
+ const template = findGrokActivationTemplate2(home, cwd);
3629
+ if (template) {
3630
+ try {
3631
+ const installed = readFileSync10(activationPath, "utf8");
3632
+ const expected = readFileSync10(template, "utf8");
3633
+ if (installed !== expected) {
3634
+ issues.push("~/.grok/skillwiki.md differs from template");
3635
+ }
3636
+ } catch {
3637
+ }
3638
+ }
3639
+ }
3640
+ if (issues.length > 0) {
3641
+ return check(
3642
+ "warn",
3643
+ "activation_marker",
3644
+ "Activation marker",
3645
+ `${issues.join("; ")} \u2014 ${ACTIVATION_FIX_HINT2}`
3646
+ );
3647
+ }
3648
+ return check("pass", "activation_marker", "Activation marker", "Marker and compact file match home-path contract");
3109
3649
  }
3110
- function findSkillMd(dir) {
3111
- const results = [];
3112
- let entries;
3113
- try {
3114
- entries = readdirSync3(dir, { withFileTypes: true });
3115
- } catch {
3116
- return results;
3650
+ var activationMarkerProbe = {
3651
+ id: "activation_marker",
3652
+ run(ctx) {
3653
+ return [checkActivationMarker(ctx.input.home, ctx.input.cwd)];
3654
+ }
3655
+ };
3656
+
3657
+ // src/doctor/probes/ds-store-noise.ts
3658
+ import { existsSync as existsSync16, readdirSync as readdirSync6 } from "fs";
3659
+ import { join as join19 } from "path";
3660
+ var VAULT_TRACKED_DIRS = ["raw", "entities", "concepts", "comparisons", "queries", "meta", "_archive", "_Templates"];
3661
+ function checkDsStoreNoise(resolvedPath) {
3662
+ if (resolvedPath === void 0) {
3663
+ return check("pass", "ds_store_noise", "No .DS_Store noise", "No vault path \u2014 check skipped");
3664
+ }
3665
+ if (!existsSync16(resolvedPath)) {
3666
+ return check("pass", "ds_store_noise", "No .DS_Store noise", "Vault directory does not exist \u2014 check skipped");
3667
+ }
3668
+ const found = [];
3669
+ function walk(dir, rel) {
3670
+ let entries;
3671
+ try {
3672
+ entries = readdirSync6(dir, { withFileTypes: true });
3673
+ } catch {
3674
+ return;
3675
+ }
3676
+ for (const entry of entries) {
3677
+ if (entry.name === ".DS_Store") {
3678
+ found.push(rel ? `${rel}/.DS_Store` : ".DS_Store");
3679
+ } else if (entry.isDirectory()) {
3680
+ if (entry.name === ".git" || entry.name === "node_modules") continue;
3681
+ walk(join19(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
3682
+ }
3683
+ }
3117
3684
  }
3118
- for (const entry of entries) {
3119
- if (entry.isFile() && entry.name === "SKILL.md") {
3120
- results.push(join10(dir, entry.name));
3121
- } else if (entry.isDirectory()) {
3122
- results.push(...findSkillMd(join10(dir, entry.name)));
3685
+ let scannedAny = false;
3686
+ for (const sub of VAULT_TRACKED_DIRS) {
3687
+ const subDir = join19(resolvedPath, sub);
3688
+ if (existsSync16(subDir)) {
3689
+ scannedAny = true;
3690
+ walk(subDir, sub);
3123
3691
  }
3124
3692
  }
3125
- return results;
3126
- }
3127
- function findInstalledSkillMd(dir) {
3128
- const directSkills = findSkillNames(dir).map((name) => join10(dir, name, "SKILL.md"));
3129
- return directSkills.length > 0 ? directSkills : findSkillMd(dir);
3130
- }
3131
- function findSkillNames(dir) {
3132
- const results = [];
3133
- let entries;
3134
- try {
3135
- entries = readdirSync3(dir, { withFileTypes: true });
3136
- } catch {
3137
- return results;
3693
+ if (!scannedAny) {
3694
+ walk(resolvedPath, "");
3138
3695
  }
3139
- for (const entry of entries) {
3140
- if (entry.isDirectory() && existsSync7(join10(dir, entry.name, "SKILL.md"))) {
3141
- results.push(entry.name);
3142
- }
3696
+ if (found.length === 0) {
3697
+ return check("pass", "ds_store_noise", "No .DS_Store noise", "No .DS_Store files found in vault");
3143
3698
  }
3144
- return results;
3699
+ const examples = found.slice(0, 3).join(", ");
3700
+ const more = found.length > 3 ? ", \u2026" : "";
3701
+ return check(
3702
+ "warn",
3703
+ "ds_store_noise",
3704
+ "No .DS_Store noise",
3705
+ `${found.length} .DS_Store file(s) found (${examples}${more}) \u2014 remove with: find ${resolvedPath} -name .DS_Store -delete`
3706
+ );
3145
3707
  }
3146
- var METRIC_TYPES = ["entities", "concepts", "comparisons", "queries", "meta"];
3147
- function doctorReadOnlyScanRoot(resolvedPath) {
3148
- return resolveReadOnlyVaultRoot(resolvedPath).root;
3708
+ var dsStoreNoiseProbe = {
3709
+ id: "ds_store_noise",
3710
+ run(ctx) {
3711
+ return [checkDsStoreNoise(ctx.readOnlyScanRoot ?? ctx.resolvedPath)];
3712
+ }
3713
+ };
3714
+
3715
+ // src/doctor/probes/index.ts
3716
+ var DOCTOR_PROBES = [
3717
+ environmentProbe,
3718
+ vaultStructureProbe,
3719
+ gitFleetProbe,
3720
+ hygieneProbe,
3721
+ s3MountHealthProbe,
3722
+ skillsPluginsProbe,
3723
+ vaultSyncProbe,
3724
+ satelliteProbe,
3725
+ metricsProbe,
3726
+ fuseStalenessProbe,
3727
+ activationMarkerProbe,
3728
+ dsStoreNoiseProbe
3729
+ ];
3730
+
3731
+ // src/doctor/runner.ts
3732
+ function isDevSourceRun(argv) {
3733
+ return argv.length >= 2 && argv[1].endsWith("cli.js");
3149
3734
  }
3150
- async function vaultMetrics(resolvedPath) {
3151
- const ids = [
3152
- ["vault_metric_pages", "Vault pages by type"],
3153
- ["vault_metric_orphans", "Vault orphan rate"],
3154
- ["vault_metric_bridges", "Vault bridge count"],
3155
- ["vault_metric_cohesion", "Mean community cohesion"],
3156
- ["vault_metric_log_size", "Vault log size"]
3157
- ];
3158
- const noVault = () => ids.map(([id, label]) => check("info", id, label, "no vault configured"));
3159
- if (!resolvedPath) return noVault();
3160
- const scanRoot = doctorReadOnlyScanRoot(resolvedPath);
3161
- const scan = await scanVault(scanRoot);
3162
- if (!scan.ok) return noVault();
3163
- const tk = scan.data.typedKnowledge;
3164
- const typedCount = tk.length;
3165
- const perType = METRIC_TYPES.map((d) => `${d} ${tk.filter((p) => p.relPath.startsWith(d + "/")).length}`).join(", ");
3166
- const adj = await buildWikilinkAdjacency(tk, void 0, scan.data.allMarkdown);
3167
- const g = toUndirectedWeighted(adj);
3168
- const nodes = [...g.keys()];
3169
- const total = nodes.length;
3170
- const orphanCount = nodes.filter((n) => g.get(n).size === 0).length;
3171
- const orphanRate = total > 0 ? Math.round(orphanCount / total * 1e3) / 10 : 0;
3172
- const comm = louvain(g);
3173
- const groups = /* @__PURE__ */ new Map();
3174
- for (const [node, c] of comm) {
3175
- const arr = groups.get(c);
3176
- if (arr) arr.push(node);
3177
- else groups.set(c, [node]);
3735
+ function resolveSnapshotGitWorktree(home) {
3736
+ const configured = resolveConfiguredSnapshotWorktree(home);
3737
+ if (configured) return configured;
3738
+ const defaultPath = "/root/wiki-git";
3739
+ return existsSync17(defaultPath) ? defaultPath : void 0;
3740
+ }
3741
+ var DoctorRunner = class {
3742
+ probes;
3743
+ constructor(probes = DOCTOR_PROBES) {
3744
+ this.probes = probes;
3178
3745
  }
3179
- const cohesions = [...groups.values()].filter((m) => m.length >= 2).map((m) => communityCohesion(m, g));
3180
- const meanCohesion = cohesions.length > 0 ? Math.round(cohesions.reduce((a, b) => a + b, 0) / cohesions.length * 1e3) / 1e3 : 0;
3181
- let bridges = 0;
3182
- for (const n of nodes) {
3183
- const nbrComms = /* @__PURE__ */ new Set();
3184
- for (const nb of g.get(n).keys()) nbrComms.add(comm.get(nb));
3185
- if (nbrComms.size >= 3) bridges++;
3746
+ getRegisteredProbes() {
3747
+ return this.probes;
3186
3748
  }
3187
- let logLines = 0;
3188
- try {
3189
- logLines = readFileSync6(join10(scanRoot, "log.md"), "utf8").split("\n").length;
3190
- } catch {
3749
+ async run(input) {
3750
+ const devSourceRun = isDevSourceRun(input.argv);
3751
+ const vsConfig = readVaultSyncConfig(input.home);
3752
+ const resolved = await resolveRuntimePath({
3753
+ flag: void 0,
3754
+ envValue: input.envValue,
3755
+ home: input.home,
3756
+ cwd: input.cwd
3757
+ });
3758
+ const resolvedPath = resolved.ok ? resolved.data.path : void 0;
3759
+ const gitCheckPath = vsConfig.role === "snapshotter" ? resolveSnapshotGitWorktree(input.home) ?? resolvedPath : resolvedPath;
3760
+ const fleetLoad = resolvedPath ? await loadFleetManifestAndHost({
3761
+ vault: resolvedPath,
3762
+ env: { ...process.env, WIKI_PATH: input.envValue ?? resolvedPath },
3763
+ home: input.home,
3764
+ cwd: input.cwd,
3765
+ osHostname: process.env.HOSTNAME,
3766
+ user: process.env.USER
3767
+ }) : null;
3768
+ const satelliteGate = satelliteGateFromFleetLoad(fleetLoad);
3769
+ const readOnlyScanRoot = resolvedPath ? doctorReadOnlyScanRoot(resolvedPath) : void 0;
3770
+ const ctx = {
3771
+ input,
3772
+ devSourceRun,
3773
+ vsConfig,
3774
+ resolvedPath,
3775
+ wikiPathSource: resolved.ok ? resolved.data.source : void 0,
3776
+ gitCheckPath,
3777
+ fleetLoad,
3778
+ readOnlyScanRoot,
3779
+ satelliteGate
3780
+ };
3781
+ const checks = [];
3782
+ for (const probe of this.probes) {
3783
+ const probeChecks = await probe.run(ctx);
3784
+ checks.push(...probeChecks);
3785
+ }
3786
+ const summary = {
3787
+ pass: checks.filter((c) => c.status === "pass").length,
3788
+ info: checks.filter((c) => c.status === "info").length,
3789
+ warn: checks.filter((c) => c.status === "warn").length,
3790
+ error: checks.filter((c) => c.status === "error").length
3791
+ };
3792
+ const exitCode = summary.error > 0 ? ExitCode.DOCTOR_HAS_ERRORS : summary.warn > 0 ? ExitCode.DOCTOR_HAS_WARNINGS : ExitCode.OK;
3793
+ const statusIcon = { pass: "\u2713", info: "i", warn: "\u26A0", error: "\u2717" };
3794
+ const lines = checks.map((c) => {
3795
+ const icon = statusIcon[c.status];
3796
+ const padded = c.label.padEnd(24);
3797
+ return ` ${icon} ${padded} ${c.detail}`;
3798
+ });
3799
+ lines.push("");
3800
+ const summaryParts = [`${summary.pass} pass`];
3801
+ if (summary.info > 0) summaryParts.push(`${summary.info} info`);
3802
+ summaryParts.push(`${summary.warn} warn`, `${summary.error} error`);
3803
+ lines.push(summaryParts.join(" \xB7 "));
3804
+ const humanHint = lines.join("\n");
3805
+ return { exitCode, result: ok({ checks, summary, humanHint }) };
3191
3806
  }
3192
- return [
3193
- check("info", "vault_metric_pages", "Vault pages by type", `${total} graph node(s) (${typedCount} typed; ${perType})`),
3194
- check("info", "vault_metric_orphans", "Vault orphan rate", `${orphanRate}% (${orphanCount}/${total} degree-0)`),
3195
- check("info", "vault_metric_bridges", "Vault bridge count", `${bridges} page(s) link >= 3 communities`),
3196
- check("info", "vault_metric_cohesion", "Mean community cohesion", `${meanCohesion} across ${cohesions.length} communities (size >= 2)`),
3197
- check("info", "vault_metric_log_size", "Vault log size", `${logLines} lines`)
3198
- ];
3199
- }
3807
+ };
3200
3808
  async function runDoctor(input) {
3201
- const checks = [];
3202
- const devSourceRun = isDevSourceRun(input.argv);
3203
- const vsConfig = readVaultSyncConfig(input.home);
3204
- checks.push(checkNodeVersion());
3205
- checks.push(checkCliChannels(input.argv, input.home));
3206
- checks.push(await checkConfigFile(input.home));
3207
- checks.push(await checkProfiles(input.home));
3208
- checks.push(await checkProjectLocalOverride(input.cwd));
3209
- const resolved = await resolveRuntimePath({
3210
- flag: void 0,
3211
- envValue: input.envValue,
3212
- home: input.home,
3213
- cwd: input.cwd
3214
- });
3215
- if (resolved.ok) {
3216
- checks.push(check("pass", "wiki_path_set", "WIKI_PATH configured", `Resolved via ${resolved.data.source}: ${resolved.data.path}`));
3217
- } else {
3218
- checks.push(check("error", "wiki_path_set", "WIKI_PATH configured", "No vault configured. Run `skillwiki init` or pass --vault."));
3219
- }
3220
- const resolvedPath = resolved.ok ? resolved.data.path : void 0;
3221
- const gitCheckPath = vsConfig.role === "snapshotter" ? resolveSnapshotGitWorktree(input.home) ?? resolvedPath : resolvedPath;
3222
- checks.push(checkWikiPathExists(resolvedPath));
3223
- checks.push(checkVaultStructure(resolvedPath));
3224
- checks.push(checkObsidianTemplates(resolvedPath));
3225
- checks.push(checkVaultGitRemote(gitCheckPath));
3226
- const fleetLoad = resolvedPath ? await loadFleetManifestAndHost({
3227
- vault: resolvedPath,
3228
- env: { ...process.env, WIKI_PATH: input.envValue ?? resolvedPath },
3229
- home: input.home,
3230
- cwd: input.cwd,
3231
- osHostname: process.env.HOSTNAME,
3232
- user: process.env.USER
3233
- }) : null;
3234
- checks.push(await checkFleetIdentity({
3235
- vaultPath: resolvedPath,
3236
- home: input.home,
3237
- cwd: input.cwd,
3238
- envValue: input.envValue,
3239
- fleetLoad
3240
- }));
3241
- checks.push(checkSyncLastPush(gitCheckPath));
3242
- checks.push(checkVaultGitDirty(gitCheckPath));
3243
- checks.push(checkVaultGitAhead(gitCheckPath));
3244
- checks.push(checkVaultGitBehind(gitCheckPath));
3245
- checks.push(checkVaultGitPullFailures(input.home));
3246
- checks.push(checkVaultLocalGit(gitCheckPath));
3247
- checks.push(checkVaultGithubRemote(gitCheckPath, input.execProbe));
3248
- checks.push(checkVaultS3Remote(input.home, input.execProbe, input.env ?? process.env));
3249
- checks.push(checkVaultSnapshotterReachable(fleetLoad, input.checkSnapshotter, input.execProbe));
3250
- checks.push(checkVaultPromotionLag(gitCheckPath));
3251
- const readOnlyScanRoot = resolvedPath ? doctorReadOnlyScanRoot(resolvedPath) : void 0;
3252
- checks.push(checkDotStoreClean(readOnlyScanRoot));
3253
- checks.push(checkVaultConflictMarkers(readOnlyScanRoot));
3254
- checks.push(checkS3MountPerf(resolvedPath));
3255
- checks.push(checkS3MountFreshness(resolvedPath));
3256
- checks.push(checkRcloneFlagAudit(resolvedPath));
3257
- checks.push(checkRcloneVersion(resolvedPath, vsConfig.installed));
3258
- checks.push(checkWriteTest(resolvedPath));
3259
- checks.push(checkVfsCacheHealth(resolvedPath));
3260
- checks.push(checkSkillsInstalled(input.home, input.cwd));
3261
- checks.push(checkDuplicateSkills(input.home));
3262
- checks.push(checkNpmUpdate(input.home, input.currentVersion));
3263
- checks.push(checkPluginVersionDrift(input.home, input.currentVersion, devSourceRun));
3264
- checks.push(...vaultSyncChecks({
3265
- home: input.home,
3266
- vaultSyncInstalled: vsConfig.installed,
3267
- vaultSyncRole: vsConfig.role,
3268
- vaultSyncServiceScope: vsConfig.serviceScope,
3269
- snapshotScriptPath: vsConfig.snapshotScript,
3270
- env: input.env ?? process.env
3271
- }));
3272
- checks.push(checkVaultSyncPullHelper(input.home, input.env ?? process.env));
3273
- checks.push(checkVaultSyncReviewRequiredJournals(resolvedPath));
3274
- const satelliteGate = satelliteGateFromFleetLoad(fleetLoad);
3275
- checks.push(checkSatelliteLastRun(resolvedPath, satelliteGate.satelliteExpected));
3276
- checks.push(checkSatelliteTimer(satelliteGate.satelliteExpected));
3277
- checks.push(...await vaultMetrics(resolvedPath));
3278
- const summary = {
3279
- pass: checks.filter((c) => c.status === "pass").length,
3280
- info: checks.filter((c) => c.status === "info").length,
3281
- warn: checks.filter((c) => c.status === "warn").length,
3282
- error: checks.filter((c) => c.status === "error").length
3283
- };
3284
- const exitCode = summary.error > 0 ? ExitCode.DOCTOR_HAS_ERRORS : summary.warn > 0 ? ExitCode.DOCTOR_HAS_WARNINGS : ExitCode.OK;
3285
- const statusIcon = { pass: "\u2713", info: "i", warn: "\u26A0", error: "\u2717" };
3286
- const lines = checks.map((c) => {
3287
- const icon = statusIcon[c.status];
3288
- const padded = c.label.padEnd(24);
3289
- return ` ${icon} ${padded} ${c.detail}`;
3290
- });
3291
- lines.push("");
3292
- const summaryParts = [`${summary.pass} pass`];
3293
- if (summary.info > 0) summaryParts.push(`${summary.info} info`);
3294
- summaryParts.push(`${summary.warn} warn`, `${summary.error} error`);
3295
- lines.push(summaryParts.join(" \xB7 "));
3296
- const humanHint = lines.join("\n");
3297
- return { exitCode, result: ok({ checks, summary, humanHint }) };
3809
+ return new DoctorRunner().run(input);
3298
3810
  }
3299
3811
 
3300
3812
  // src/utils/package-info.ts
3301
- import { readFileSync as readFileSync7 } from "fs";
3813
+ import { readFileSync as readFileSync11 } from "fs";
3302
3814
  function packageJsonCandidateUrls(baseUrl = import.meta.url) {
3303
3815
  return [
3304
3816
  new URL("../package.json", baseUrl),
@@ -3308,7 +3820,7 @@ function packageJsonCandidateUrls(baseUrl = import.meta.url) {
3308
3820
  function readCliPackageJson(baseUrl = import.meta.url) {
3309
3821
  for (const url of packageJsonCandidateUrls(baseUrl)) {
3310
3822
  try {
3311
- const pkg = JSON.parse(readFileSync7(url, "utf8"));
3823
+ const pkg = JSON.parse(readFileSync11(url, "utf8"));
3312
3824
  if (typeof pkg.version === "string") {
3313
3825
  return { ...pkg, version: pkg.version };
3314
3826
  }
@@ -3320,8 +3832,8 @@ function readCliPackageJson(baseUrl = import.meta.url) {
3320
3832
 
3321
3833
  // src/utils/vault-write-gates.ts
3322
3834
  import { execFileSync } from "child_process";
3323
- import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
3324
- import { join as join11, relative as relative2 } from "path";
3835
+ import { existsSync as existsSync18, readdirSync as readdirSync7, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
3836
+ import { join as join20, relative as relative2 } from "path";
3325
3837
  var DEFAULT_DIRTY_VOLUME_THRESHOLD = 50;
3326
3838
  var DEFAULT_CAPTURE_BUDGET = 20;
3327
3839
  var DEFAULT_NO_DECISION_STREAK = 3;
@@ -3383,7 +3895,7 @@ function measureDirtyVolume(vault) {
3383
3895
  is_git_repo: false,
3384
3896
  ...extra
3385
3897
  });
3386
- if (!existsSync8(vault) || !statSync3(vault).isDirectory()) {
3898
+ if (!existsSync18(vault) || !statSync3(vault).isDirectory()) {
3387
3899
  return empty();
3388
3900
  }
3389
3901
  const gitDir = git(vault, ["rev-parse", "--absolute-git-dir"]);
@@ -3421,8 +3933,8 @@ function measureDirtyVolume(vault) {
3421
3933
  if (!rel) continue;
3422
3934
  if (code === "??") {
3423
3935
  untracked += 1;
3424
- const abs = join11(vault, rel);
3425
- if (existsSync8(abs) && statSync3(abs).isDirectory()) {
3936
+ const abs = join20(vault, rel);
3937
+ if (existsSync18(abs) && statSync3(abs).isDirectory()) {
3426
3938
  const files = listFilesRecursive(abs);
3427
3939
  expanded += files.length;
3428
3940
  addBucket(rel, files.length);
@@ -3452,13 +3964,13 @@ function listFilesRecursive(dir) {
3452
3964
  const out = [];
3453
3965
  let entries;
3454
3966
  try {
3455
- entries = readdirSync4(dir);
3967
+ entries = readdirSync7(dir);
3456
3968
  } catch {
3457
3969
  return out;
3458
3970
  }
3459
3971
  for (const name of entries) {
3460
3972
  if (name === ".git") continue;
3461
- const p = join11(dir, name);
3973
+ const p = join20(dir, name);
3462
3974
  try {
3463
3975
  const st = statSync3(p);
3464
3976
  if (st.isDirectory()) out.push(...listFilesRecursive(p));
@@ -3553,7 +4065,7 @@ var CAPTURE_HYGIENE_CONTRACT = `After each productive cycle (or when daily captu
3553
4065
  function listProjectDayCaptures(vault, project, day) {
3554
4066
  const found = [];
3555
4067
  const slug = project.replace(/^\[\[/, "").replace(/\]\]$/, "").trim();
3556
- if (!slug || !existsSync8(vault)) return found;
4068
+ if (!slug || !existsSync18(vault)) return found;
3557
4069
  const consider = (abs, rel) => {
3558
4070
  if (!rel.endsWith(".md")) return;
3559
4071
  const base = rel.split(/[/\\]/).pop() ?? "";
@@ -3570,7 +4082,7 @@ function listProjectDayCaptures(vault, project, day) {
3570
4082
  }
3571
4083
  if (norm.startsWith("raw/transcripts/")) {
3572
4084
  try {
3573
- const body = readFileSync8(abs, "utf8");
4085
+ const body = readFileSync12(abs, "utf8");
3574
4086
  if (body.includes(`project: ${slug}`) || body.includes(`project: "[[${slug}]]"`) || body.includes(`project: [[${slug}]]`)) {
3575
4087
  found.push(norm);
3576
4088
  } else if (base.includes(slug)) {
@@ -3581,15 +4093,15 @@ function listProjectDayCaptures(vault, project, day) {
3581
4093
  }
3582
4094
  };
3583
4095
  const walk = (dir, relBase) => {
3584
- if (!existsSync8(dir)) return;
4096
+ if (!existsSync18(dir)) return;
3585
4097
  let entries;
3586
4098
  try {
3587
- entries = readdirSync4(dir);
4099
+ entries = readdirSync7(dir);
3588
4100
  } catch {
3589
4101
  return;
3590
4102
  }
3591
4103
  for (const name of entries) {
3592
- const abs = join11(dir, name);
4104
+ const abs = join20(dir, name);
3593
4105
  const rel = relBase ? `${relBase}/${name}` : name;
3594
4106
  try {
3595
4107
  const st = statSync3(abs);
@@ -3599,16 +4111,16 @@ function listProjectDayCaptures(vault, project, day) {
3599
4111
  }
3600
4112
  }
3601
4113
  };
3602
- walk(join11(vault, "raw", "transcripts"), "raw/transcripts");
3603
- walk(join11(vault, "projects", slug, "raw", "transcripts"), `projects/${slug}/raw/transcripts`);
3604
- walk(join11(vault, "projects", slug, "requirements"), `projects/${slug}/requirements`);
3605
- const workRoot = join11(vault, "projects", slug, "work");
3606
- if (existsSync8(workRoot)) {
4114
+ walk(join20(vault, "raw", "transcripts"), "raw/transcripts");
4115
+ walk(join20(vault, "projects", slug, "raw", "transcripts"), `projects/${slug}/raw/transcripts`);
4116
+ walk(join20(vault, "projects", slug, "requirements"), `projects/${slug}/requirements`);
4117
+ const workRoot = join20(vault, "projects", slug, "work");
4118
+ if (existsSync18(workRoot)) {
3607
4119
  try {
3608
- for (const name of readdirSync4(workRoot)) {
4120
+ for (const name of readdirSync7(workRoot)) {
3609
4121
  if (!name.startsWith(day)) continue;
3610
4122
  if (!/investigate|pilot-q|research|cycle|dev-loop/.test(name)) continue;
3611
- const abs = join11(workRoot, name);
4123
+ const abs = join20(workRoot, name);
3612
4124
  if (statSync3(abs).isDirectory()) {
3613
4125
  for (const f of listFilesRecursive(abs)) {
3614
4126
  const rel = relative2(vault, f).replace(/\\/g, "/");
@@ -3653,7 +4165,7 @@ function evaluateCaptureBudget(input) {
3653
4165
  return { allowed: true, reason: "under_budget", report };
3654
4166
  }
3655
4167
  function runWritePreflight(input) {
3656
- if (!input.vault || !existsSync8(input.vault)) {
4168
+ if (!input.vault || !existsSync18(input.vault)) {
3657
4169
  return err(GateError.VAULT_PATH_INVALID, { path: input.vault });
3658
4170
  }
3659
4171
  const want = new Set(input.checks ?? ["all"]);
@@ -3713,8 +4225,8 @@ function runWritePreflight(input) {
3713
4225
 
3714
4226
  // src/commands/observe.ts
3715
4227
  import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
3716
- import { existsSync as existsSync9, statSync as statSync4 } from "fs";
3717
- import { join as join12 } from "path";
4228
+ import { existsSync as existsSync19, statSync as statSync4 } from "fs";
4229
+ import { join as join21 } from "path";
3718
4230
  import { createHash as createHash2 } from "crypto";
3719
4231
  var ALLOWED_KINDS = /* @__PURE__ */ new Set(["note", "bug", "task", "idea", "session-log"]);
3720
4232
  function slugify(text) {
@@ -3737,7 +4249,7 @@ async function runObserve(input) {
3737
4249
  result: err("SCHEME_REJECTED", { message: "Text must not be empty" })
3738
4250
  };
3739
4251
  }
3740
- if (!existsSync9(input.vault) || !statSync4(input.vault).isDirectory()) {
4252
+ if (!existsSync19(input.vault) || !statSync4(input.vault).isDirectory()) {
3741
4253
  return {
3742
4254
  exitCode: ExitCode.VAULT_PATH_INVALID,
3743
4255
  result: err("VAULT_PATH_INVALID", { path: input.vault })
@@ -3763,7 +4275,7 @@ async function runObserve(input) {
3763
4275
  };
3764
4276
  }
3765
4277
  }
3766
- const transcriptsDir = join12(input.vault, "raw", "transcripts");
4278
+ const transcriptsDir = join21(input.vault, "raw", "transcripts");
3767
4279
  try {
3768
4280
  await mkdir3(transcriptsDir, { recursive: true });
3769
4281
  } catch {
@@ -3775,7 +4287,7 @@ async function runObserve(input) {
3775
4287
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3776
4288
  const slug = slugify(input.text);
3777
4289
  const fileName = `${today}-observation-${slug}.md`;
3778
- const filePath = join12(transcriptsDir, fileName);
4290
+ const filePath = join21(transcriptsDir, fileName);
3779
4291
  const body = `
3780
4292
  ${input.text.trim()}
3781
4293
  `;
@@ -3817,7 +4329,7 @@ ${input.text.trim()}
3817
4329
  // src/commands/memory.ts
3818
4330
  import { createHash as createHash3 } from "crypto";
3819
4331
  import { mkdir as mkdir4, readFile as readFile7, readdir as readdir2, stat as stat2, writeFile as writeFile3 } from "fs/promises";
3820
- import { basename as basename2, extname, join as join13, relative as relative3, sep as sep2 } from "path";
4332
+ import { basename as basename2, extname, join as join22, relative as relative3, sep as sep2 } from "path";
3821
4333
 
3822
4334
  // src/utils/memory-authority.ts
3823
4335
  var TIER_RANK = {
@@ -3936,8 +4448,8 @@ async function runMemoryIndex(input) {
3936
4448
  }
3937
4449
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
3938
4450
  const relCachePath = memoryCacheRelPath(input.project);
3939
- const absCachePath = join13(input.vault, relCachePath);
3940
- await mkdir4(join13(input.vault, ".skillwiki", "memory", input.project), { recursive: true });
4451
+ const absCachePath = join22(input.vault, relCachePath);
4452
+ await mkdir4(join22(input.vault, ".skillwiki", "memory", input.project), { recursive: true });
3941
4453
  await writeFile3(absCachePath, `${JSON.stringify({
3942
4454
  generated_at: generatedAt,
3943
4455
  project: input.project,
@@ -4129,7 +4641,7 @@ async function buildMemoryIndexState(pages, project) {
4129
4641
  }
4130
4642
  async function checkMemoryIndex(vault, project, current) {
4131
4643
  const relCachePath = memoryCacheRelPath(project);
4132
- const cacheText = await readIfExists(join13(vault, relCachePath));
4644
+ const cacheText = await readIfExists(join22(vault, relCachePath));
4133
4645
  if (!cacheText) {
4134
4646
  return {
4135
4647
  ok: true,
@@ -4538,7 +5050,7 @@ async function walkImportFiles(dir, out) {
4538
5050
  const entries = await readdir2(dir, { withFileTypes: true });
4539
5051
  for (const entry of entries) {
4540
5052
  if (entry.name === ".git" || entry.name === "node_modules") continue;
4541
- const path = join13(dir, entry.name);
5053
+ const path = join22(dir, entry.name);
4542
5054
  if (entry.isDirectory()) {
4543
5055
  await walkImportFiles(path, out);
4544
5056
  } else if (entry.isFile() && isImportCandidate(path)) {
@@ -4605,8 +5117,8 @@ async function writeImportCapture(vault, entry, today) {
4605
5117
  const content = hiddenString(entry, "__content");
4606
5118
  const project = hiddenString(entry, "__project");
4607
5119
  const relPath = await availableImportPath(vault, entry.proposed_path);
4608
- const absPath = join13(vault, relPath);
4609
- await mkdir4(join13(vault, "raw", "transcripts"), { recursive: true });
5120
+ const absPath = join22(vault, relPath);
5121
+ await mkdir4(join22(vault, "raw", "transcripts"), { recursive: true });
4610
5122
  await writeFile3(absPath, renderImportCapture(entry, content, project, today), "utf8");
4611
5123
  const validation = await runValidate({ file: absPath });
4612
5124
  return {
@@ -4622,7 +5134,7 @@ async function availableImportPath(vault, proposed) {
4622
5134
  const stem = proposed.slice(0, -ext.length);
4623
5135
  let candidate = proposed;
4624
5136
  let i = 2;
4625
- while (await readIfExists(join13(vault, candidate))) {
5137
+ while (await readIfExists(join22(vault, candidate))) {
4626
5138
  candidate = `${stem}-${i}${ext}`;
4627
5139
  i++;
4628
5140
  }
@@ -4807,10 +5319,10 @@ function memoryCacheRelPath(project) {
4807
5319
  }
4808
5320
  async function readMemoryCache(vault, project) {
4809
5321
  if (project) {
4810
- const projectCache = await readIfExists(join13(vault, memoryCacheRelPath(project)));
5322
+ const projectCache = await readIfExists(join22(vault, memoryCacheRelPath(project)));
4811
5323
  if (projectCache) return projectCache;
4812
5324
  }
4813
- return readIfExists(join13(vault, ".skillwiki", "memory-topics.json"));
5325
+ return readIfExists(join22(vault, ".skillwiki", "memory-topics.json"));
4814
5326
  }
4815
5327
  function dedupePages(pages) {
4816
5328
  const seen = /* @__PURE__ */ new Set();
@@ -4901,8 +5413,310 @@ function slugify2(value) {
4901
5413
  }
4902
5414
 
4903
5415
  // src/commands/query.ts
4904
- import { readFile as readFile8, stat as stat3 } from "fs/promises";
4905
- import { join as join14 } from "path";
5416
+ import { readFile as readFile9, stat as stat4 } from "fs/promises";
5417
+ import { join as join24 } from "path";
5418
+
5419
+ // src/utils/rrf.ts
5420
+ var RRF_K = 60;
5421
+ function fuseRankings(lists, k = RRF_K) {
5422
+ const scores = /* @__PURE__ */ new Map();
5423
+ for (const list of lists) {
5424
+ list.forEach((id, index) => {
5425
+ scores.set(id, (scores.get(id) ?? 0) + 1 / (k + index + 1));
5426
+ });
5427
+ }
5428
+ return [...scores.entries()].map(([id, score]) => ({ id, score })).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
5429
+ }
5430
+
5431
+ // src/utils/vector-index.ts
5432
+ import { mkdir as mkdir5, readFile as readFile8, stat as stat3 } from "fs/promises";
5433
+ import { join as join23 } from "path";
5434
+ import { existsSync as existsSync20 } from "fs";
5435
+ var VECTOR_INDEX_REL = ".skillwiki/vectors/index.json";
5436
+ var VECTOR_INDEX_SCHEMA = "skillwiki-tfidf-index/v1";
5437
+ function tokenize(text) {
5438
+ return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1);
5439
+ }
5440
+ function extractPageTokens(text) {
5441
+ const fm = extractFrontmatter(text);
5442
+ const split = splitFrontmatter(text);
5443
+ const title = fm.ok ? String(fm.data.title ?? "") : "";
5444
+ const body = split.ok ? split.data.body : text;
5445
+ return tokenize(`${title} ${body}`);
5446
+ }
5447
+ function vectorIndexPath(vault) {
5448
+ return join23(vault, ...VECTOR_INDEX_REL.split("/"));
5449
+ }
5450
+ function termFreq(tokens) {
5451
+ const tf = /* @__PURE__ */ new Map();
5452
+ for (const token of tokens) tf.set(token, (tf.get(token) ?? 0) + 1);
5453
+ return tf;
5454
+ }
5455
+ function toTfIdf(tf, df, docs) {
5456
+ const out = {};
5457
+ for (const [term, count] of tf) {
5458
+ const appears = df[term] ?? 0;
5459
+ if (appears === 0) continue;
5460
+ out[term] = count / Math.max(1, tf.size) * Math.log((docs + 1) / appears);
5461
+ }
5462
+ return out;
5463
+ }
5464
+ function cosine(a, b) {
5465
+ let dot = 0;
5466
+ let na = 0;
5467
+ let nb = 0;
5468
+ for (const value of Object.values(a)) na += value * value;
5469
+ for (const value of Object.values(b)) nb += value * value;
5470
+ if (na === 0 || nb === 0) return 0;
5471
+ const shorter = Object.keys(a).length < Object.keys(b).length ? a : b;
5472
+ const longer = shorter === a ? b : a;
5473
+ for (const [term, value] of Object.entries(shorter)) {
5474
+ const other = longer[term];
5475
+ if (other) dot += value * other;
5476
+ }
5477
+ return dot / Math.sqrt(na * nb);
5478
+ }
5479
+ async function buildVectorIndex(vault, now = (/* @__PURE__ */ new Date()).toISOString()) {
5480
+ const scan = await scanVault(vault);
5481
+ if (!scan.ok) return scan;
5482
+ const tokenized = [];
5483
+ for (const page of scan.data.typedKnowledge) {
5484
+ const text = await readPage(page);
5485
+ tokenized.push({ path: page.relPath, tokens: extractPageTokens(text) });
5486
+ }
5487
+ const df = {};
5488
+ for (const doc of tokenized) {
5489
+ for (const term of new Set(doc.tokens)) df[term] = (df[term] ?? 0) + 1;
5490
+ }
5491
+ const docs = {};
5492
+ for (const doc of tokenized) {
5493
+ docs[doc.path] = toTfIdf(termFreq(doc.tokens), df, tokenized.length);
5494
+ }
5495
+ const index = {
5496
+ schema: VECTOR_INDEX_SCHEMA,
5497
+ built_at: now,
5498
+ page_count: tokenized.length,
5499
+ df,
5500
+ docs
5501
+ };
5502
+ const dest = vectorIndexPath(vault);
5503
+ await mkdir5(join23(vault, ".skillwiki", "vectors"), { recursive: true });
5504
+ await atomicWriteText(dest, `${JSON.stringify(index)}
5505
+ `);
5506
+ return ok(index);
5507
+ }
5508
+ async function loadVectorIndex(vault) {
5509
+ try {
5510
+ const raw = await readFile8(vectorIndexPath(vault), "utf8");
5511
+ const parsed = JSON.parse(raw);
5512
+ if (parsed.schema !== VECTOR_INDEX_SCHEMA || typeof parsed.docs !== "object" || parsed.docs === null) {
5513
+ return err("HYBRID_INDEX_INVALID", { path: VECTOR_INDEX_REL });
5514
+ }
5515
+ return ok(parsed);
5516
+ } catch {
5517
+ return err("HYBRID_INDEX_MISSING", { path: VECTOR_INDEX_REL, message: "run skillwiki vectors rebuild" });
5518
+ }
5519
+ }
5520
+ async function vectorIndexStatus(vault) {
5521
+ const path = vectorIndexPath(vault);
5522
+ try {
5523
+ const fileStat = await stat3(path);
5524
+ const loaded = await loadVectorIndex(vault);
5525
+ if (!loaded.ok) return ok({ path: VECTOR_INDEX_REL, present: false });
5526
+ return ok({
5527
+ path: VECTOR_INDEX_REL,
5528
+ present: true,
5529
+ page_count: loaded.data.page_count,
5530
+ built_at: loaded.data.built_at,
5531
+ age_hours: (Date.now() - fileStat.mtimeMs) / 36e5
5532
+ });
5533
+ } catch {
5534
+ return ok({ path: VECTOR_INDEX_REL, present: false });
5535
+ }
5536
+ }
5537
+ function rankVectorIndex(index, query) {
5538
+ const q = toTfIdf(termFreq(tokenize(query)), index.df, Math.max(1, index.page_count));
5539
+ return Object.entries(index.docs).map(([path, vector]) => ({ path, score: cosine(q, vector) })).filter((row) => row.score > 0).sort((a, b) => b.score - a.score || a.path.localeCompare(b.path)).map((row) => row.path);
5540
+ }
5541
+ async function reindexPageInVectorIndex(vault, pageRelPath, now = (/* @__PURE__ */ new Date()).toISOString()) {
5542
+ const normalizedPage = pageRelPath.split(/\\|\//).join("/");
5543
+ if (!normalizedPage.endsWith(".md")) {
5544
+ return err("USAGE", { message: `Page must be a .md file: ${pageRelPath}` });
5545
+ }
5546
+ const absPath = join23(vault, ...normalizedPage.split("/"));
5547
+ let text;
5548
+ try {
5549
+ text = await readFile8(absPath, "utf8");
5550
+ } catch {
5551
+ return err("FILE_NOT_FOUND", { path: normalizedPage });
5552
+ }
5553
+ const loaded = await loadVectorIndex(vault);
5554
+ if (!loaded.ok) return loaded;
5555
+ const index = loaded.data;
5556
+ const tokens = extractPageTokens(text);
5557
+ const newTf = termFreq(tokens);
5558
+ const newTerms = new Set(newTf.keys());
5559
+ const oldDoc = index.docs[normalizedPage];
5560
+ const oldTerms = oldDoc ? new Set(Object.keys(oldDoc)) : /* @__PURE__ */ new Set();
5561
+ const isNewPage = !oldDoc;
5562
+ const newPageCount = isNewPage ? index.page_count + 1 : index.page_count;
5563
+ const termsAdded = [];
5564
+ const termsRemoved = [];
5565
+ for (const t of newTerms) {
5566
+ if (!oldTerms.has(t)) termsAdded.push(t);
5567
+ }
5568
+ for (const t of oldTerms) {
5569
+ if (!newTerms.has(t)) termsRemoved.push(t);
5570
+ }
5571
+ const dfChangedTerms = /* @__PURE__ */ new Set();
5572
+ for (const t of termsAdded) {
5573
+ index.df[t] = (index.df[t] ?? 0) + 1;
5574
+ dfChangedTerms.add(t);
5575
+ }
5576
+ for (const t of termsRemoved) {
5577
+ const nextDf = (index.df[t] ?? 1) - 1;
5578
+ if (nextDf <= 0) {
5579
+ delete index.df[t];
5580
+ } else {
5581
+ index.df[t] = nextDf;
5582
+ }
5583
+ dfChangedTerms.add(t);
5584
+ }
5585
+ index.page_count = newPageCount;
5586
+ index.built_at = now;
5587
+ index.docs[normalizedPage] = toTfIdf(newTf, index.df, index.page_count);
5588
+ if (isNewPage) {
5589
+ const oldDocsTotal = newPageCount - 1;
5590
+ for (const [docPath, docVector] of Object.entries(index.docs)) {
5591
+ if (docPath === normalizedPage) continue;
5592
+ const updatedVector = {};
5593
+ for (const [term, oldScore] of Object.entries(docVector)) {
5594
+ const dfVal = index.df[term];
5595
+ if (!dfVal || dfVal <= 0) continue;
5596
+ const oldDf = termsAdded.includes(term) ? index.df[term] - 1 : index.df[term];
5597
+ const oldIdf = Math.log((oldDocsTotal + 1) / oldDf);
5598
+ const newIdf = Math.log((newPageCount + 1) / dfVal);
5599
+ updatedVector[term] = oldIdf !== 0 ? oldScore * (newIdf / oldIdf) : 0;
5600
+ }
5601
+ index.docs[docPath] = updatedVector;
5602
+ }
5603
+ } else if (dfChangedTerms.size > 0) {
5604
+ for (const [docPath, docVector] of Object.entries(index.docs)) {
5605
+ if (docPath === normalizedPage) continue;
5606
+ let touched = false;
5607
+ for (const t of Object.keys(docVector)) {
5608
+ if (dfChangedTerms.has(t)) {
5609
+ touched = true;
5610
+ break;
5611
+ }
5612
+ }
5613
+ if (!touched) continue;
5614
+ const updatedVector = {};
5615
+ for (const [term, oldScore] of Object.entries(docVector)) {
5616
+ const dfVal = index.df[term];
5617
+ if (!dfVal || dfVal <= 0) continue;
5618
+ if (dfChangedTerms.has(term)) {
5619
+ const oldDf = termsAdded.includes(term) ? dfVal - 1 : termsRemoved.includes(term) ? dfVal + 1 : dfVal;
5620
+ const oldIdf = Math.log((newPageCount + 1) / oldDf);
5621
+ const newIdf = Math.log((newPageCount + 1) / dfVal);
5622
+ updatedVector[term] = oldIdf !== 0 ? oldScore * (newIdf / oldIdf) : 0;
5623
+ } else {
5624
+ updatedVector[term] = oldScore;
5625
+ }
5626
+ }
5627
+ index.docs[docPath] = updatedVector;
5628
+ }
5629
+ }
5630
+ const dest = vectorIndexPath(vault);
5631
+ await mkdir5(join23(vault, ".skillwiki", "vectors"), { recursive: true });
5632
+ await atomicWriteText(dest, `${JSON.stringify(index)}
5633
+ `);
5634
+ return ok({
5635
+ path: VECTOR_INDEX_REL,
5636
+ page: normalizedPage,
5637
+ terms_added: termsAdded.length,
5638
+ terms_removed: termsRemoved.length,
5639
+ page_count: index.page_count
5640
+ });
5641
+ }
5642
+ async function pruneVectorIndex(vault, opts) {
5643
+ const loaded = await loadVectorIndex(vault);
5644
+ if (!loaded.ok) return loaded;
5645
+ const index = loaded.data;
5646
+ const orphans = [];
5647
+ for (const docKey of Object.keys(index.docs)) {
5648
+ const normalizedKey = docKey.split(/\\|\//).join("/");
5649
+ const absPath = join23(vault, ...normalizedKey.split("/"));
5650
+ if (!existsSync20(absPath)) {
5651
+ orphans.push(normalizedKey);
5652
+ }
5653
+ }
5654
+ if (orphans.length === 0) {
5655
+ return ok({
5656
+ path: VECTOR_INDEX_REL,
5657
+ orphans: [],
5658
+ removed: 0,
5659
+ page_count: index.page_count,
5660
+ terms_pruned: 0
5661
+ });
5662
+ }
5663
+ const oldPageCount = index.page_count;
5664
+ const newPageCount = Math.max(0, oldPageCount - orphans.length);
5665
+ let termsPrunedCount = 0;
5666
+ for (const orphanKey of orphans) {
5667
+ const orphanDoc = index.docs[orphanKey] ?? {};
5668
+ for (const term of Object.keys(orphanDoc)) {
5669
+ const nextDf = (index.df[term] ?? 1) - 1;
5670
+ if (nextDf <= 0) {
5671
+ delete index.df[term];
5672
+ termsPrunedCount++;
5673
+ } else {
5674
+ index.df[term] = nextDf;
5675
+ }
5676
+ }
5677
+ delete index.docs[orphanKey];
5678
+ }
5679
+ index.page_count = newPageCount;
5680
+ const now = opts?.now ?? (/* @__PURE__ */ new Date()).toISOString();
5681
+ index.built_at = now;
5682
+ const scan = await scanVault(vault);
5683
+ if (!scan.ok) return scan;
5684
+ const remainingPagesByRel = /* @__PURE__ */ new Map();
5685
+ for (const p of scan.data.typedKnowledge) {
5686
+ remainingPagesByRel.set(p.relPath, p);
5687
+ }
5688
+ for (const docKey of Object.keys(index.docs)) {
5689
+ const page = remainingPagesByRel.get(docKey);
5690
+ if (page) {
5691
+ const text = await readPage(page);
5692
+ const tokens = extractPageTokens(text);
5693
+ index.docs[docKey] = toTfIdf(termFreq(tokens), index.df, newPageCount);
5694
+ } else {
5695
+ try {
5696
+ const text = await readFile8(join23(vault, ...docKey.split("/")), "utf8");
5697
+ const tokens = extractPageTokens(text);
5698
+ index.docs[docKey] = toTfIdf(termFreq(tokens), index.df, newPageCount);
5699
+ } catch {
5700
+ delete index.docs[docKey];
5701
+ }
5702
+ }
5703
+ }
5704
+ if (!opts?.dryRun) {
5705
+ const dest = vectorIndexPath(vault);
5706
+ await mkdir5(join23(vault, ".skillwiki", "vectors"), { recursive: true });
5707
+ await atomicWriteText(dest, `${JSON.stringify(index)}
5708
+ `);
5709
+ }
5710
+ return ok({
5711
+ path: VECTOR_INDEX_REL,
5712
+ orphans,
5713
+ removed: orphans.length,
5714
+ page_count: newPageCount,
5715
+ terms_pruned: termsPrunedCount
5716
+ });
5717
+ }
5718
+
5719
+ // src/commands/query.ts
4906
5720
  var W_KEYWORD = 2;
4907
5721
  var W_SOURCE_OVERLAP = 4;
4908
5722
  var W_WIKILINK = 3;
@@ -4930,7 +5744,7 @@ async function runQuery(input) {
4930
5744
  const scan = await scanVault(input.vault);
4931
5745
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
4932
5746
  const limit = input.limit ?? 10;
4933
- const queryTerms = tokenize(input.text);
5747
+ const queryTerms = tokenize2(input.text);
4934
5748
  if (queryTerms.length === 0) {
4935
5749
  return {
4936
5750
  exitCode: ExitCode.OK,
@@ -4975,7 +5789,7 @@ async function runQuery(input) {
4975
5789
  }
4976
5790
  const suppressRepetitiveHistoricalCycles = historicalCyclePageCount >= 3 && hasDirectOperationalSeed;
4977
5791
  const structuralSeedPaths = suppressRepetitiveHistoricalCycles ? operationalSeedPaths : seedPaths;
4978
- const results = pages.map((page) => {
5792
+ const structural = pages.map((page) => {
4979
5793
  const sourceOverlap = scoreSourceOverlap(page, pages, structuralSeedPaths);
4980
5794
  const wikilink = scoreWikilink(page.relPath, structuralSeedPaths, graph);
4981
5795
  const aa = scoreAdamicAdar(page.relPath, structuralSeedPaths, graph);
@@ -4990,11 +5804,31 @@ async function runQuery(input) {
4990
5804
  title: page.title,
4991
5805
  type: page.type
4992
5806
  };
4993
- }).filter((r) => r.score > 0).sort((a, b) => b.score - a.score || a.path.localeCompare(b.path)).slice(0, limit);
5807
+ }).filter((r) => r.score > 0).sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
5808
+ let results = structural.slice(0, limit);
5809
+ let hybridMeta;
5810
+ if (input.hybrid) {
5811
+ const index = await loadVectorIndex(input.vault);
5812
+ if (!index.ok) return { exitCode: ExitCode.USAGE, result: index };
5813
+ const byPath = new Map(pages.map((page) => [page.relPath, page]));
5814
+ const fused = fuseRankings([structural.map((row) => row.path), rankVectorIndex(index.data, input.text)]);
5815
+ results = fused.slice(0, limit).map((row) => {
5816
+ const existing = structural.find((item) => item.path === row.id);
5817
+ if (existing) return { ...existing, score: Math.round(row.score * 1e3) / 1e3 };
5818
+ const page = byPath.get(row.id);
5819
+ return {
5820
+ path: row.id,
5821
+ score: Math.round(row.score * 1e3) / 1e3,
5822
+ title: page?.title ?? "",
5823
+ type: page?.type ?? ""
5824
+ };
5825
+ });
5826
+ hybridMeta = { used: true, rrf_k: RRF_K };
5827
+ }
4994
5828
  let pendingSources;
4995
5829
  if (input.includePending) {
4996
- const { runSourcesPending } = await import("./sources-CMV5DKS5.js");
4997
- const pending = await runSourcesPending({
5830
+ const { runSourcesPending: runSourcesPending2 } = await import("./sources-BFYEFTS4.js");
5831
+ const pending = await runSourcesPending2({
4998
5832
  vault: input.vault,
4999
5833
  match: input.text,
5000
5834
  limit: input.limit ?? 10
@@ -5013,6 +5847,7 @@ ${pendingSources.length} matching pending source(s)` : "no matching pages found"
5013
5847
  results,
5014
5848
  ...pendingSources ? { pending_sources: pendingSources } : {},
5015
5849
  ...rankingGuardrails ? { ranking_guardrails: rankingGuardrails } : {},
5850
+ ...hybridMeta ? { hybrid: hybridMeta } : {},
5016
5851
  humanHint
5017
5852
  })
5018
5853
  };
@@ -5056,7 +5891,7 @@ function scoreTypeAffinity(pageType, queryTerms) {
5056
5891
  function isHistoricalCyclePage(relPath, title) {
5057
5892
  return HISTORICAL_CYCLE_RE.test(`${relPath} ${title}`);
5058
5893
  }
5059
- function tokenize(text) {
5894
+ function tokenize2(text) {
5060
5895
  return text.toLowerCase().split(/\s+/).filter((t) => t.length > 0);
5061
5896
  }
5062
5897
  function computeKeywordScore(terms, title, tags, body) {
@@ -5072,10 +5907,10 @@ function computeKeywordScore(terms, title, tags, body) {
5072
5907
  return score;
5073
5908
  }
5074
5909
  async function loadOrBuildGraph(vault) {
5075
- const graphPath = join14(vault, ".skillwiki", "graph.json");
5910
+ const graphPath = join24(vault, ".skillwiki", "graph.json");
5076
5911
  let needsBuild = false;
5077
5912
  try {
5078
- const fileStat = await stat3(graphPath);
5913
+ const fileStat = await stat4(graphPath);
5079
5914
  const ageHours = (Date.now() - fileStat.mtimeMs) / (1e3 * 60 * 60);
5080
5915
  if (ageHours > 24) needsBuild = true;
5081
5916
  } catch {
@@ -5086,13 +5921,73 @@ async function loadOrBuildGraph(vault) {
5086
5921
  if (buildResult.exitCode !== 0) return null;
5087
5922
  }
5088
5923
  try {
5089
- const raw = await readFile8(graphPath, "utf8");
5924
+ const raw = await readFile9(graphPath, "utf8");
5090
5925
  return JSON.parse(raw);
5091
5926
  } catch {
5092
5927
  return null;
5093
5928
  }
5094
5929
  }
5095
5930
 
5931
+ // src/commands/source-compile.ts
5932
+ function writeGuard(approve) {
5933
+ if (!approve) return { ok: false, error: "APPROVAL_INVALID", detail: { message: "--write requires --approve" } };
5934
+ return null;
5935
+ }
5936
+ async function runSourceCompileClaim(input) {
5937
+ if (!input.write) {
5938
+ const plan = await planSourceCompileClaim(input);
5939
+ return { exitCode: plan.ok ? ExitCode.OK : ExitCode.USAGE, result: plan };
5940
+ }
5941
+ const missing = writeGuard(input.approve);
5942
+ if (missing) return { exitCode: ExitCode.USAGE, result: missing };
5943
+ const applied = await applySourceCompileClaim({ ...input, approve: input.approve });
5944
+ if (!applied.ok) return { exitCode: ExitCode.USAGE, result: applied };
5945
+ return { exitCode: ExitCode.OK, result: ok({ ...applied.data, humanHint: `claimed ${input.rawPath}` }) };
5946
+ }
5947
+ async function runSourceCompileRelease(input) {
5948
+ if (!input.write) {
5949
+ const plan = await planSourceCompileRelease(input);
5950
+ return { exitCode: plan.ok ? ExitCode.OK : ExitCode.USAGE, result: plan };
5951
+ }
5952
+ const missing = writeGuard(input.approve);
5953
+ if (missing) return { exitCode: ExitCode.USAGE, result: missing };
5954
+ const applied = await applySourceCompileRelease({ ...input, approve: input.approve });
5955
+ if (!applied.ok) return { exitCode: ExitCode.USAGE, result: applied };
5956
+ return { exitCode: ExitCode.OK, result: ok({ ...applied.data, humanHint: `released ${input.rawPath}` }) };
5957
+ }
5958
+ async function runSourceCompilePublished(input) {
5959
+ if (!input.write) {
5960
+ const plan = await planSourceCompilePublished(input);
5961
+ return { exitCode: plan.ok ? ExitCode.OK : ExitCode.USAGE, result: plan };
5962
+ }
5963
+ const missing = writeGuard(input.approve);
5964
+ if (missing) return { exitCode: ExitCode.USAGE, result: missing };
5965
+ const applied = await applySourceCompilePublished({ ...input, approve: input.approve });
5966
+ if (!applied.ok) return { exitCode: ExitCode.USAGE, result: applied };
5967
+ return { exitCode: ExitCode.OK, result: ok({ ...applied.data, humanHint: `published compile turn for ${input.rawPath}` }) };
5968
+ }
5969
+ async function runSourceReview(input) {
5970
+ if (!input.write) {
5971
+ const plan = await planSourceReview(input);
5972
+ return { exitCode: plan.ok ? ExitCode.OK : ExitCode.USAGE, result: plan };
5973
+ }
5974
+ const missing = writeGuard(input.approve);
5975
+ if (missing) return { exitCode: ExitCode.USAGE, result: missing };
5976
+ const applied = await applySourceReview({ ...input, approve: input.approve });
5977
+ if (!applied.ok) return { exitCode: ExitCode.USAGE, result: applied };
5978
+ return { exitCode: ExitCode.OK, result: ok({ ...applied.data, humanHint: `${input.status} review for ${input.rawPath}` }) };
5979
+ }
5980
+ async function runSourceCompileStatus(input) {
5981
+ const listed = await listCompileStatus(input);
5982
+ if (!listed.ok) return { exitCode: ExitCode.USAGE, result: listed };
5983
+ return { exitCode: ExitCode.OK, result: ok({ ...listed.data, humanHint: listed.data.items.length ? `${listed.data.items.length} compile turns` : "no active compile turns" }) };
5984
+ }
5985
+ async function runSourceReviews(input) {
5986
+ const listed = await listSourceReviews(input);
5987
+ if (!listed.ok) return { exitCode: ExitCode.USAGE, result: listed };
5988
+ return { exitCode: ExitCode.OK, result: ok({ ...listed.data, humanHint: listed.data.items.length ? `${listed.data.items.length} open reviews` : "no open reviews" }) };
5989
+ }
5990
+
5096
5991
  // src/mcp/server.ts
5097
5992
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5098
5993
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -5101,7 +5996,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
5101
5996
  import { z } from "zod";
5102
5997
 
5103
5998
  // src/mcp/vault-resolve.ts
5104
- import { join as join15, resolve as resolve4 } from "path";
5999
+ import { join as join25, resolve as resolve4 } from "path";
5105
6000
 
5106
6001
  // src/mcp/allowlist.ts
5107
6002
  import { resolve as resolve3, sep as sep3 } from "path";
@@ -5163,7 +6058,7 @@ async function resolveMcpVault(input) {
5163
6058
  return ok({ vault: vaultPath, source });
5164
6059
  }
5165
6060
  function defaultGraphOut(vault) {
5166
- return join15(vault, ".skillwiki", "graph.json");
6061
+ return join25(vault, ".skillwiki", "graph.json");
5167
6062
  }
5168
6063
 
5169
6064
  // src/mcp/result-format.ts
@@ -5180,7 +6075,7 @@ function formatToolResult(payload) {
5180
6075
  // src/mcp/audit-log.ts
5181
6076
  import { appendFileSync, mkdirSync as mkdirSync2 } from "fs";
5182
6077
  import { homedir } from "os";
5183
- import { join as join16 } from "path";
6078
+ import { join as join26 } from "path";
5184
6079
  function auditEnabled() {
5185
6080
  const v = process.env.SKILLWIKI_MCP_AUDIT;
5186
6081
  if (v === "0" || v === "false") return false;
@@ -5192,7 +6087,7 @@ function auditSink() {
5192
6087
  function auditFilePath() {
5193
6088
  const custom = process.env.SKILLWIKI_MCP_AUDIT_FILE;
5194
6089
  if (custom && custom.length > 0) return custom;
5195
- return join16(homedir(), ".skillwiki", "mcp-audit.jsonl");
6090
+ return join26(homedir(), ".skillwiki", "mcp-audit.jsonl");
5196
6091
  }
5197
6092
  function auditMcpToolCall(entry) {
5198
6093
  if (!auditEnabled()) return;
@@ -5202,7 +6097,7 @@ function auditMcpToolCall(entry) {
5202
6097
  return;
5203
6098
  }
5204
6099
  const path = auditFilePath();
5205
- mkdirSync2(join16(path, ".."), { recursive: true });
6100
+ mkdirSync2(join26(path, ".."), { recursive: true });
5206
6101
  appendFileSync(path, line, "utf8");
5207
6102
  }
5208
6103
  async function runMcpToolHandler(tool, input, fn) {
@@ -5371,6 +6266,59 @@ function registerMcpTools(server) {
5371
6266
  return formatToolResult(r);
5372
6267
  })
5373
6268
  );
6269
+ server.registerTool(
6270
+ "skillwiki.sources_pending",
6271
+ {
6272
+ description: "List captured raw articles/papers awaiting typed integration (read-only).",
6273
+ inputSchema: z.object({
6274
+ ...vaultFields,
6275
+ match: z.string().optional().describe("Literal title, URL, or path match"),
6276
+ scope: z.enum(["articles", "papers", "all"]).optional(),
6277
+ limit: z.number().int().positive().optional().describe("Max items (default 50)"),
6278
+ includeIntegrated: z.boolean().optional()
6279
+ })
6280
+ },
6281
+ async ({ vault, wiki, match, scope, limit, includeIntegrated }) => runMcpToolHandler("skillwiki.sources_pending", { vault, wiki }, async () => {
6282
+ const v = await resolveMcpVault({ vault, wiki });
6283
+ if (!v.ok) return formatToolResult({ exitCode: 25, result: v });
6284
+ const r = await runSourcesPending({
6285
+ vault: v.data.vault,
6286
+ match,
6287
+ scope,
6288
+ limit,
6289
+ includeIntegrated
6290
+ });
6291
+ return formatToolResult(r);
6292
+ })
6293
+ );
6294
+ server.registerTool(
6295
+ "skillwiki.compile_status",
6296
+ {
6297
+ description: "List compiling and review-open pending compile-turns (read-only).",
6298
+ inputSchema: z.object({
6299
+ ...vaultFields
6300
+ })
6301
+ },
6302
+ async ({ vault, wiki }) => runMcpToolHandler("skillwiki.compile_status", { vault, wiki }, async () => {
6303
+ const v = await resolveMcpVault({ vault, wiki });
6304
+ if (!v.ok) return formatToolResult({ exitCode: 25, result: v });
6305
+ return formatToolResult(await runSourceCompileStatus({ vault: v.data.vault }));
6306
+ })
6307
+ );
6308
+ server.registerTool(
6309
+ "skillwiki.reviews",
6310
+ {
6311
+ description: "List open or needs-fix post-compile reviews (read-only).",
6312
+ inputSchema: z.object({
6313
+ ...vaultFields
6314
+ })
6315
+ },
6316
+ async ({ vault, wiki }) => runMcpToolHandler("skillwiki.reviews", { vault, wiki }, async () => {
6317
+ const v = await resolveMcpVault({ vault, wiki });
6318
+ if (!v.ok) return formatToolResult({ exitCode: 25, result: v });
6319
+ return formatToolResult(await runSourceReviews({ vault: v.data.vault }));
6320
+ })
6321
+ );
5374
6322
  }
5375
6323
 
5376
6324
  // src/mcp/mutating-tools.ts
@@ -5422,8 +6370,8 @@ function registerMcpMutatingTools(server) {
5422
6370
  }
5423
6371
 
5424
6372
  // src/mcp/resources.ts
5425
- import { readFile as readFile10 } from "fs/promises";
5426
- import { join as join18 } from "path";
6373
+ import { readFile as readFile11 } from "fs/promises";
6374
+ import { join as join28 } from "path";
5427
6375
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
5428
6376
 
5429
6377
  // src/mcp/lint-bucket.ts
@@ -5454,7 +6402,8 @@ var WARNING_KINDS = /* @__PURE__ */ new Set([
5454
6402
  "work_item_health",
5455
6403
  "orphaned_project_pages",
5456
6404
  "missing_overview",
5457
- "missing_diagram"
6405
+ "missing_diagram",
6406
+ "cycle_traps"
5458
6407
  ]);
5459
6408
  function bucketSeverity(kind) {
5460
6409
  if (ERROR_KINDS.has(kind)) return "error";
@@ -5551,9 +6500,9 @@ async function fetchQueryPreview(input) {
5551
6500
  }
5552
6501
 
5553
6502
  // src/mcp/graph-html.ts
5554
- import { readFile as readFile9 } from "fs/promises";
5555
- import { join as join17 } from "path";
5556
- import { existsSync as existsSync10 } from "fs";
6503
+ import { readFile as readFile10 } from "fs/promises";
6504
+ import { join as join27 } from "path";
6505
+ import { existsSync as existsSync21 } from "fs";
5557
6506
  var TYPE_COLORS = {
5558
6507
  entities: "#e74c3c",
5559
6508
  concepts: "#27ae60",
@@ -5621,9 +6570,9 @@ ${nodeSvg}
5621
6570
  return { html, node_count: nodes.length, edge_count: edges.length, truncated };
5622
6571
  }
5623
6572
  async function fetchGraphHtmlReport(input) {
5624
- const graphPath = input.graphPath ?? join17(input.vault, ".skillwiki", "graph.json");
6573
+ const graphPath = input.graphPath ?? join27(input.vault, ".skillwiki", "graph.json");
5625
6574
  const maxNodes = Math.min(Math.max(10, input.maxNodes ?? 120), 500);
5626
- if (!existsSync10(graphPath)) {
6575
+ if (!existsSync21(graphPath)) {
5627
6576
  return {
5628
6577
  exitCode: ExitCode.FILE_NOT_FOUND,
5629
6578
  result: err("GRAPH_MISSING", { path: graphPath, hint: "Run skillwiki.graph_build first." })
@@ -5631,7 +6580,7 @@ async function fetchGraphHtmlReport(input) {
5631
6580
  }
5632
6581
  let raw;
5633
6582
  try {
5634
- raw = await readFile9(graphPath, "utf8");
6583
+ raw = await readFile10(graphPath, "utf8");
5635
6584
  } catch (e) {
5636
6585
  return {
5637
6586
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -5695,7 +6644,7 @@ async function fetchStaleSummary(input) {
5695
6644
 
5696
6645
  // src/mcp/resources.ts
5697
6646
  async function readVaultFile(vault, rel) {
5698
- return readFile10(join18(vault, rel), "utf8");
6647
+ return readFile11(join28(vault, rel), "utf8");
5699
6648
  }
5700
6649
  async function tailLines(text, lines) {
5701
6650
  const parts = text.split(/\r?\n/);
@@ -5781,9 +6730,9 @@ function registerMcpResources(server) {
5781
6730
  if (!v.ok) {
5782
6731
  return { contents: [{ uri: uri.href, mimeType: "text/plain", text: JSON.stringify(v) }] };
5783
6732
  }
5784
- const path = join18(v.data.vault, ".skillwiki", "graph.json");
6733
+ const path = join28(v.data.vault, ".skillwiki", "graph.json");
5785
6734
  try {
5786
- const raw = await readFile10(path, "utf8");
6735
+ const raw = await readFile11(path, "utf8");
5787
6736
  const graph = JSON.parse(raw);
5788
6737
  const adjacency = graph.adjacency ?? {};
5789
6738
  const nodes = Object.keys(adjacency);
@@ -6071,6 +7020,35 @@ function registerMcpPrompts(server) {
6071
7020
  ]
6072
7021
  })
6073
7022
  );
7023
+ server.registerPrompt(
7024
+ "skillwiki-pending-review",
7025
+ {
7026
+ description: "Inspect pending sources and compile reviews without mutating the vault",
7027
+ argsSchema: {
7028
+ match: z3.string().optional().describe("Optional title/URL/path filter")
7029
+ }
7030
+ },
7031
+ ({ match }) => ({
7032
+ messages: [
7033
+ {
7034
+ role: "user",
7035
+ content: {
7036
+ type: "text",
7037
+ text: [
7038
+ "List pending raw sources and open compile reviews.",
7039
+ match ? `Filter: ${match}` : "No extra filter.",
7040
+ "",
7041
+ "1. Call skillwiki.sources_pending (read-only).",
7042
+ "2. Call skillwiki.compile_status and skillwiki.reviews.",
7043
+ "3. Summarize what an attended session should compile next.",
7044
+ "",
7045
+ "Do not call mutating tools. Compile claim/publish stays on the interactive CLI."
7046
+ ].join("\n")
7047
+ }
7048
+ }
7049
+ ]
7050
+ })
7051
+ );
6074
7052
  }
6075
7053
 
6076
7054
  // src/mcp/server.ts
@@ -6108,13 +7086,13 @@ export {
6108
7086
  runConfigSet,
6109
7087
  runConfigList,
6110
7088
  runConfigPath,
7089
+ detectFuseMount,
7090
+ snapshotterHealthChecks,
6111
7091
  SATELLITE_STALE_MS,
6112
7092
  satelliteLatestRunPath,
6113
7093
  isFailedRunStatus,
6114
7094
  readSatelliteLatestRunFromText,
6115
7095
  evaluateSatelliteRunHealth,
6116
- detectFuseMount,
6117
- snapshotterHealthChecks,
6118
7096
  runDoctor,
6119
7097
  readCliPackageJson,
6120
7098
  DEFAULT_DIRTY_VOLUME_THRESHOLD,
@@ -6131,6 +7109,16 @@ export {
6131
7109
  runMemoryRecall,
6132
7110
  runMemoryReview,
6133
7111
  runMemoryImport,
7112
+ buildVectorIndex,
7113
+ vectorIndexStatus,
7114
+ reindexPageInVectorIndex,
7115
+ pruneVectorIndex,
6134
7116
  runQuery,
7117
+ runSourceCompileClaim,
7118
+ runSourceCompileRelease,
7119
+ runSourceCompilePublished,
7120
+ runSourceReview,
7121
+ runSourceCompileStatus,
7122
+ runSourceReviews,
6135
7123
  runSkillwikiMcpStdio
6136
7124
  };