skillwiki 0.10.49 → 0.10.51

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-UPQ6XJSV.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,1179 +1195,950 @@ 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
- var GROK_ACTIVATION_REFERENCE = "Read @~/.grok/skillwiki.md for SkillWiki activation context.";
1738
- var STALE_GROK_ACTIVATION_REFERENCE = "Read @skillwiki.md";
1739
- var ACTIVATION_FIX_HINT = "run `npm run install:activation` from the llm-wiki repo";
1740
- function findGrokActivationTemplate(home, cwd) {
1741
- if (cwd) {
1742
- const src = join10(cwd, "packages", "skills", "using-skillwiki", "activation.md");
1743
- if (existsSync7(src)) return src;
1744
- }
1745
- const pluginsRoot = join10(home, ".grok", "installed-plugins");
1746
- if (!existsSync7(pluginsRoot)) return void 0;
1747
- let entries;
1537
+ function remoteMainHash(resolvedPath) {
1748
1538
  try {
1749
- entries = readdirSync3(pluginsRoot, { withFileTypes: true });
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;
1750
1547
  } catch {
1751
1548
  return void 0;
1752
1549
  }
1753
- for (const entry of entries) {
1754
- if (!entry.isDirectory()) continue;
1755
- const root = join10(pluginsRoot, entry.name);
1756
- for (const rel of ["using-skillwiki/activation.md", "skills/using-skillwiki/activation.md"]) {
1757
- const candidate = join10(root, rel);
1758
- if (existsSync7(candidate)) return candidate;
1759
- }
1760
- }
1761
- return void 0;
1762
1550
  }
1763
- function checkGrokActivation(home, cwd) {
1764
- const grokDir = join10(home, ".grok");
1765
- if (!existsSync7(grokDir)) {
1766
- return check("pass", "activation_grok", "Grok activation", "Not a Grok host");
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;
1558
+ return check(
1559
+ "warn",
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`
1563
+ );
1564
+ }
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");
1767
1568
  }
1768
- const activationPath = join10(grokDir, "skillwiki.md");
1769
- const agentsPath = join10(grokDir, "AGENTS.md");
1770
- const hasActivation = existsSync7(activationPath);
1771
- const issues = [];
1772
- if (!hasActivation) {
1773
- issues.push("~/.grok/skillwiki.md missing");
1569
+ if (!existsSync6(join9(resolvedPath, ".git"))) {
1570
+ return check("pass", id, label, "No git repo \u2014 check skipped");
1774
1571
  }
1775
- if (!existsSync7(agentsPath)) {
1776
- issues.push("~/.grok/AGENTS.md missing");
1777
- } else {
1778
- const agents = readFileSync6(agentsPath, "utf8");
1779
- const hasBegin = agents.includes("<!-- skillwiki:begin -->");
1780
- const hasExpected = agents.includes(GROK_ACTIVATION_REFERENCE);
1781
- const hasStale = agents.includes(STALE_GROK_ACTIVATION_REFERENCE);
1782
- if (!hasBegin) {
1783
- issues.push("AGENTS.md marker missing");
1784
- } else if (hasStale && !hasExpected) {
1785
- issues.push("AGENTS.md marker is stale (@skillwiki.md)");
1786
- } else if (!hasExpected) {
1787
- issues.push("AGENTS.md marker is stale");
1788
- }
1572
+ if (!hasOriginMain(resolvedPath)) {
1573
+ return check("pass", id, label, "origin/main unavailable \u2014 check skipped");
1789
1574
  }
1790
- if (hasActivation) {
1791
- const template = findGrokActivationTemplate(home, cwd);
1792
- if (template) {
1793
- try {
1794
- const installed = readFileSync6(activationPath, "utf8");
1795
- const expected = readFileSync6(template, "utf8");
1796
- if (installed !== expected) {
1797
- issues.push("~/.grok/skillwiki.md differs from template");
1798
- }
1799
- } catch {
1800
- }
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}`);
1801
1584
  }
1585
+ return check("pass", id, label, zeroDetail);
1586
+ } catch {
1587
+ return check("warn", id, label, "Could not compare HEAD with origin/main");
1802
1588
  }
1803
- if (issues.length > 0) {
1804
- return check(
1805
- "warn",
1806
- "activation_grok",
1807
- "Grok activation",
1808
- `${issues.join("; ")} \u2014 ${ACTIVATION_FIX_HINT}`
1809
- );
1810
- }
1811
- return check("pass", "activation_grok", "Grok activation", "Marker and compact file are current");
1812
1589
  }
1813
- function checkNpmUpdate(home, currentVersion) {
1814
- const { hasUpdate, latest, distTag } = latestFromCache(home, currentVersion);
1815
- if (!latest) {
1816
- return check("pass", "npm_update", "npm CLI version", `v${currentVersion} (${distTag}: no cache yet)`);
1817
- }
1818
- if (hasUpdate) {
1819
- return check("warn", "npm_update", "npm CLI version", `v${currentVersion} \u2014 ${distTag} update available: v${latest}. Run \`skillwiki update --tag ${distTag}\`.`);
1820
- }
1821
- return check("pass", "npm_update", "npm CLI version", `v${currentVersion} (${distTag}: v${latest})`);
1590
+ function checkVaultGitAhead(resolvedPath) {
1591
+ return checkVaultGitComparison(
1592
+ resolvedPath,
1593
+ "vault_git_ahead",
1594
+ "Vault commits ahead",
1595
+ "origin/main..HEAD",
1596
+ "ahead of origin/main",
1597
+ "0 commits ahead of origin/main"
1598
+ );
1822
1599
  }
1823
- function checkPluginVersionDrift(home, currentVersion, devSourceRun) {
1824
- const plugins = findPluginInstallations(home);
1825
- if (plugins.length === 0) {
1826
- return check("pass", "plugin_version_drift", "Plugin/CLI version", "Plugin not installed \u2014 CLI only");
1827
- }
1828
- const drifted = plugins.filter((plugin) => plugin.version !== currentVersion);
1829
- if (drifted.length === 0) {
1830
- if (plugins.length === 1 && plugins[0].channel === "claude") {
1831
- return check("pass", "plugin_version_drift", "Plugin/CLI version", `Both at v${currentVersion}`);
1832
- }
1833
- if (plugins.length === 1) {
1834
- return check("pass", "plugin_version_drift", "Plugin/CLI version", `${plugins[0].label} plugin and CLI both at v${currentVersion}`);
1835
- }
1836
- const labels = plugins.map((plugin) => `${plugin.label} plugin`).join(", ");
1837
- return check("pass", "plugin_version_drift", "Plugin/CLI version", `${labels}, and CLI all at v${currentVersion}`);
1838
- }
1839
- if (devSourceRun && drifted.every((plugin) => semverGt(currentVersion, plugin.version))) {
1840
- const details2 = drifted.map((plugin) => `${plugin.label} plugin v${plugin.version}`).join(", ");
1841
- return check("info", "plugin_version_drift", "Plugin/CLI version", `Dev source v${currentVersion} is ahead of installed ${details2}`);
1842
- }
1843
- const details = drifted.map((plugin) => {
1844
- const updateCmd = pluginUpdateCommand(plugin, currentVersion);
1845
- return `${plugin.label} plugin v${plugin.version} \u2260 CLI v${currentVersion} \u2014 run \`${updateCmd}\``;
1846
- });
1847
- return check(
1848
- "warn",
1849
- "plugin_version_drift",
1850
- "Plugin/CLI version",
1851
- details.join("; ")
1600
+ function checkVaultGitBehind(resolvedPath) {
1601
+ const staleRemote = checkStaleRemoteMain(resolvedPath);
1602
+ if (staleRemote) return staleRemote;
1603
+ return checkVaultGitComparison(
1604
+ resolvedPath,
1605
+ "vault_git_behind",
1606
+ "Vault commits behind",
1607
+ "HEAD..origin/main",
1608
+ "behind origin/main",
1609
+ "0 commits behind origin/main"
1852
1610
  );
1853
1611
  }
1854
- function pluginUpdateCommand(plugin, currentVersion) {
1855
- if (semverGt(plugin.version, currentVersion)) {
1856
- return "npm install -g skillwiki@latest";
1857
- }
1858
- if (plugin.channel === "claude") {
1859
- return "claude plugin update skillwiki@llm-wiki";
1860
- }
1861
- if (plugin.sourceType === "git") {
1862
- return "codex plugin marketplace upgrade llm-wiki && codex plugin remove skillwiki@llm-wiki && codex plugin add skillwiki@llm-wiki";
1863
- }
1864
- return "codex plugin remove skillwiki@llm-wiki && codex plugin add skillwiki@llm-wiki";
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)];
1865
1621
  }
1866
- async function checkProfiles(home) {
1867
- const map = await parseDotenvFile(configPath(home));
1868
- const profiles = [];
1869
- for (const key of Object.keys(map)) {
1870
- if (key.startsWith("WIKI_") && key.endsWith("_PATH") && key !== "WIKI_PATH") {
1871
- const name = key.slice(5, -5).toLowerCase().replace(/_/g, "-");
1872
- profiles.push(name);
1873
- }
1874
- }
1875
- if (profiles.length === 0) {
1876
- return check("pass", "wiki_profiles", "Wiki profiles", "No named profiles configured");
1877
- }
1878
- const defaultProfile = map["WIKI_DEFAULT"] ?? "(none)";
1879
- return check(
1880
- "pass",
1881
- "wiki_profiles",
1882
- "Wiki profiles",
1883
- `${profiles.length} profile(s): ${profiles.join(", ")}; default: ${defaultProfile}`
1884
- );
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;
1885
1628
  }
1886
- async function checkProjectLocalOverride(cwd) {
1887
- const dir = cwd ?? process.cwd();
1888
- const envPath = join10(dir, ".skillwiki", ".env");
1889
- if (existsSync7(envPath)) {
1890
- return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
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
+ }
1634
+ try {
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");
1645
+ } catch {
1646
+ return check("warn", "vault_git_pull_failures", "Vault pull failures", `Could not read ${path}`);
1891
1647
  }
1892
- return check("pass", "project_local", "Project-local config", "None");
1893
1648
  }
1894
- function checkVaultGitRemote(resolvedPath) {
1649
+ function checkVaultLocalGit(resolvedPath) {
1895
1650
  if (resolvedPath === void 0) {
1896
- return check("error", "vault_git_remote", "Vault git remote", "Cannot check \u2014 WIKI_PATH not resolved");
1651
+ return check("warn", "vault_local_git", "Vault local git", "Cannot check \u2014 WIKI_PATH not resolved");
1897
1652
  }
1898
- if (!existsSync7(join10(resolvedPath, ".git"))) {
1899
- return check("warn", "vault_git_remote", "Vault git remote", "Vault is not a git repository \u2014 sync features unavailable");
1653
+ if (!existsSync6(join9(resolvedPath, ".git"))) {
1654
+ return check("warn", "vault_local_git", "Vault local git", "Not a git repository - sync features unavailable");
1900
1655
  }
1901
1656
  try {
1902
- const remote = execSync2("git remote", { cwd: resolvedPath, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
1903
- if (!remote) {
1904
- return check("warn", "vault_git_remote", "Vault git remote", "No remote configured \u2014 push/pull unavailable");
1905
- }
1906
- let branch = "(no commits yet)";
1907
- try {
1908
- branch = execSync2("git rev-parse --abbrev-ref HEAD", { cwd: resolvedPath, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
1909
- } catch {
1910
- }
1911
- return check("pass", "vault_git_remote", "Vault git remote", `Remote: ${remote.split("\n")[0]}, branch: ${branch}`);
1657
+ execSync2("git rev-parse --git-dir", {
1658
+ cwd: resolvedPath,
1659
+ encoding: "utf8",
1660
+ stdio: ["pipe", "pipe", "pipe"],
1661
+ timeout: 2e3
1662
+ });
1663
+ return check("pass", "vault_local_git", "Vault local git", "Git metadata readable");
1912
1664
  } catch {
1913
- return check("warn", "vault_git_remote", "Vault git remote", "Could not read git remote info");
1665
+ return check("error", "vault_local_git", "Vault local git", "Git metadata unreadable \u2014 local vault may be corrupt");
1914
1666
  }
1915
1667
  }
1916
- function checkObsidianTemplates(resolvedPath) {
1668
+ function checkVaultGithubRemote(resolvedPath, exec) {
1917
1669
  if (resolvedPath === void 0) {
1918
- return check("error", "obsidian_templates", "Obsidian templates", "Cannot check \u2014 WIKI_PATH not resolved");
1670
+ return check("pass", "vault_github_remote", "Vault GitHub remote", "No vault path \u2014 check skipped");
1919
1671
  }
1920
- const missing = [];
1921
- if (!existsSync7(join10(resolvedPath, "_Templates"))) missing.push("_Templates/");
1922
- if (!existsSync7(join10(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
1923
- if (!existsSync7(join10(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
1924
- if (missing.length === 0) {
1925
- return check("pass", "obsidian_templates", "Obsidian templates", "Template folder and config present");
1672
+ if (!existsSync6(join9(resolvedPath, ".git"))) {
1673
+ return check("pass", "vault_github_remote", "Vault GitHub remote", "No git repo \u2014 check skipped");
1926
1674
  }
1927
- return check("warn", "obsidian_templates", "Obsidian templates", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to create`);
1675
+ const state = probeGithubReachability(resolvedPath, exec);
1676
+ if (state === "ok") {
1677
+ return check("pass", "vault_github_remote", "Vault GitHub remote", "git ls-remote origin main succeeded");
1678
+ }
1679
+ if (state === "unreachable") {
1680
+ return check("warn", "vault_github_remote", "Vault GitHub remote", "GitHub unreachable (ls-remote failed) \u2014 local vault still usable");
1681
+ }
1682
+ return check("pass", "vault_github_remote", "Vault GitHub remote", "No origin remote \u2014 network probe skipped");
1928
1683
  }
1929
- function checkDotStoreClean(resolvedPath) {
1930
- if (resolvedPath === void 0) {
1931
- return check("error", "dsstore_clean", "No .DS_Store in raw/", "Cannot check \u2014 WIKI_PATH not resolved");
1684
+ function checkVaultS3Remote(home, exec, env) {
1685
+ const remote = resolveWikiS3Remote({ home, env });
1686
+ if (!remote) {
1687
+ return check("pass", "vault_s3_remote", "Vault S3 remote", "S3 remote not configured \u2014 check skipped");
1932
1688
  }
1933
- const rawDir = join10(resolvedPath, "raw");
1934
- if (!existsSync7(rawDir)) {
1935
- return check("pass", "dsstore_clean", "No .DS_Store in raw/", "raw/ directory not found \u2014 check skipped");
1689
+ const state = probeS3Reachability(remote, exec);
1690
+ if (state === "ok") {
1691
+ return check("pass", "vault_s3_remote", "Vault S3 remote", `rclone lsf ${remote} succeeded`);
1936
1692
  }
1937
- const found = [];
1938
- (function walk(dir, rel) {
1939
- let entries;
1940
- try {
1941
- entries = readdirSync3(dir, { withFileTypes: true });
1942
- } catch {
1943
- return;
1944
- }
1945
- for (const entry of entries) {
1946
- if (entry.name === ".DS_Store") {
1947
- found.push(rel ? `${rel}/.DS_Store` : ".DS_Store");
1948
- } else if (entry.isDirectory()) {
1949
- walk(join10(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
1950
- }
1951
- }
1952
- })(rawDir, "");
1953
- if (found.length === 0) {
1954
- return check("pass", "dsstore_clean", "No .DS_Store in raw/", "No .DS_Store files found");
1693
+ if (state === "unreachable") {
1694
+ return check("warn", "vault_s3_remote", "Vault S3 remote", `S3 remote unreachable (${remote}) \u2014 local/GitHub work may continue`);
1955
1695
  }
1956
- 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`);
1696
+ return check("pass", "vault_s3_remote", "Vault S3 remote", "S3 remote not configured \u2014 check skipped");
1957
1697
  }
1958
- function checkVaultConflictMarkers(resolvedPath) {
1959
- if (resolvedPath === void 0) {
1960
- return check("pass", "vault_conflict_markers", "Vault conflict markers", "No vault path \u2014 check skipped");
1698
+ function checkVaultSnapshotterReachable(fleetLoad, checkSnapshotter, exec) {
1699
+ if (!checkSnapshotter) {
1700
+ return check("pass", "vault_snapshotter_reachable", "Vault snapshotter host", "Snapshotter SSH probe not requested \u2014 check skipped");
1961
1701
  }
1962
- const findings = scanVaultConflictMarkers(resolvedPath);
1963
- if (findings.length === 0) {
1964
- return check("pass", "vault_conflict_markers", "Vault conflict markers", "No complete conflict-marker blocks");
1702
+ const alias = snapshotterAliasForLocalHost(fleetLoad);
1703
+ if (!alias) {
1704
+ return check("pass", "vault_snapshotter_reachable", "Vault snapshotter host", "No declared SSH alias from this host \u2014 check skipped");
1965
1705
  }
1966
- const first = findings[0];
1967
- const n = findings.length;
1968
- const fileWord = n === 1 ? "file" : "files";
1969
- return check(
1970
- "error",
1971
- "vault_conflict_markers",
1972
- "Vault conflict markers",
1973
- `${n} ${fileWord}, first: ${first.path}:${first.line}`
1974
- );
1706
+ const state = probeSnapshotterSsh(alias, exec);
1707
+ if (state === "ok") {
1708
+ return check("pass", "vault_snapshotter_reachable", "Vault snapshotter host", `SSH reachable via ${alias}`);
1709
+ }
1710
+ return check("warn", "vault_snapshotter_reachable", "Vault snapshotter host", `Snapshotter unreachable via ${alias} \u2014 not a local vault corruption signal`);
1975
1711
  }
1976
- function checkSyncLastPush(resolvedPath) {
1712
+ function checkVaultPromotionLag(resolvedPath) {
1977
1713
  if (resolvedPath === void 0) {
1978
- return check("error", "sync_last_push", "Vault sync recency", "Cannot check \u2014 WIKI_PATH not resolved");
1714
+ return check("pass", "vault_promotion_lag", "Vault promotion lag", "No vault path \u2014 check skipped");
1979
1715
  }
1980
- if (!existsSync7(join10(resolvedPath, ".git"))) {
1981
- return check("pass", "sync_last_push", "Vault sync recency", "No git repo \u2014 sync check skipped");
1716
+ if (!existsSync6(join9(resolvedPath, ".git"))) {
1717
+ return check("pass", "vault_promotion_lag", "Vault promotion lag", "No git repo \u2014 check skipped");
1982
1718
  }
1983
- let timestamp;
1984
1719
  try {
1985
- const out = execSync2("git log -1 --format=%ct origin/HEAD", {
1720
+ const out = execSync2("git log -1 --format=%ct origin/main", {
1986
1721
  cwd: resolvedPath,
1987
1722
  encoding: "utf8",
1988
- stdio: ["pipe", "pipe", "pipe"]
1723
+ stdio: ["pipe", "pipe", "pipe"],
1724
+ timeout: 2e3
1989
1725
  }).trim();
1990
- timestamp = parseInt(out, 10);
1991
- } catch {
1992
- try {
1993
- const out = execSync2("git log -1 --format=%ct HEAD", {
1994
- cwd: resolvedPath,
1995
- encoding: "utf8",
1996
- stdio: ["pipe", "pipe", "pipe"]
1997
- }).trim();
1998
- timestamp = parseInt(out, 10);
1999
- } catch {
1726
+ const ts = parseInt(out, 10);
1727
+ if (!Number.isFinite(ts) || ts <= 0) {
1728
+ return check("pass", "vault_promotion_lag", "Vault promotion lag", "origin/main timestamp unavailable \u2014 check skipped");
1729
+ }
1730
+ const ageHours = Math.floor((Date.now() / 1e3 - ts) / 3600);
1731
+ if (ageHours > 48) {
1732
+ return check("warn", "vault_promotion_lag", "Vault promotion lag", `Local origin/main snapshot is ${ageHours}h old \u2014 verify snapshotter/GitHub when online`);
2000
1733
  }
1734
+ return check("pass", "vault_promotion_lag", "Vault promotion lag", `origin/main age ${ageHours}h`);
1735
+ } catch {
1736
+ return check("pass", "vault_promotion_lag", "Vault promotion lag", "Could not read origin/main \u2014 check skipped");
2001
1737
  }
2002
- if (timestamp === void 0 || isNaN(timestamp)) {
2003
- return check("warn", "sync_last_push", "Vault sync recency", "No commits found \u2014 consider running `skillwiki sync status`");
1738
+ }
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
+ ];
2004
1762
  }
2005
- const daysSince = Math.floor((Date.now() / 1e3 - timestamp) / 86400);
2006
- const dateStr = new Date(timestamp * 1e3).toISOString().slice(0, 10);
2007
- if (daysSince > 7) {
2008
- return check("warn", "sync_last_push", "Vault sync recency", `Last push was ${daysSince} days ago \u2014 consider running \`skillwiki sync status\``);
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;
1783
+ }
1784
+ if (inFence) continue;
1785
+ if (line.startsWith("<<<<<<< ")) {
1786
+ openLine = i + 1;
1787
+ sawSeparator = false;
1788
+ continue;
1789
+ }
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
+ }
2009
1801
  }
2010
- return check("pass", "sync_last_push", "Vault sync recency", `Last push: ${dateStr} (${daysSince} day(s) ago)`);
1802
+ return findings;
2011
1803
  }
2012
- function hasOriginMain(resolvedPath) {
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;
2013
1814
  try {
2014
- execSync2("git rev-parse --verify --quiet origin/main", {
2015
- cwd: resolvedPath,
2016
- encoding: "utf8",
2017
- stdio: ["pipe", "pipe", "pipe"],
2018
- timeout: 2e3
2019
- });
2020
- return true;
1815
+ entries = readdirSync2(dir, { withFileTypes: true });
2021
1816
  } catch {
2022
- return false;
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
+ }
2023
1826
  }
2024
1827
  }
2025
- function checkVaultGitDirty(resolvedPath) {
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;
1839
+ }
1840
+ all.push(...scanConflictMarkerBlocksInText(rel, text));
1841
+ }
1842
+ return all;
1843
+ }
1844
+
1845
+ // src/doctor/probes/hygiene.ts
1846
+ function checkDotStoreClean(resolvedPath) {
2026
1847
  if (resolvedPath === void 0) {
2027
- return check("pass", "vault_git_dirty", "Vault git dirty state", "No vault path \u2014 check skipped");
1848
+ return check("error", "dsstore_clean", "No .DS_Store in raw/", "Cannot check \u2014 WIKI_PATH not resolved");
2028
1849
  }
2029
- if (!existsSync7(join10(resolvedPath, ".git"))) {
2030
- return check("pass", "vault_git_dirty", "Vault git dirty state", "No git repo \u2014 check skipped");
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");
2031
1853
  }
2032
- try {
2033
- const lines = execSync2("git status --porcelain", {
2034
- cwd: resolvedPath,
2035
- encoding: "utf8",
2036
- stdio: ["pipe", "pipe", "pipe"],
2037
- timeout: 5e3
2038
- }).trim().split("\n").filter(Boolean);
2039
- if (lines.length > 0) {
2040
- return check("warn", "vault_git_dirty", "Vault git dirty state", `${lines.length} dirty file(s) in vault worktree`);
1854
+ const found = [];
1855
+ (function walk(dir, rel) {
1856
+ let entries;
1857
+ try {
1858
+ entries = readdirSync3(dir, { withFileTypes: true });
1859
+ } catch {
1860
+ return;
2041
1861
  }
2042
- return check("pass", "vault_git_dirty", "Vault git dirty state", "Clean worktree");
2043
- } catch {
2044
- return check("warn", "vault_git_dirty", "Vault git dirty state", "Could not read git status");
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");
2045
1872
  }
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`);
2046
1874
  }
2047
- function checkVaultGitAhead(resolvedPath) {
2048
- return checkVaultGitComparison(
2049
- resolvedPath,
2050
- "vault_git_ahead",
2051
- "Vault commits ahead",
2052
- "origin/main..HEAD",
2053
- "ahead of origin/main",
2054
- "0 commits ahead of origin/main"
2055
- );
2056
- }
2057
- function checkVaultGitBehind(resolvedPath) {
2058
- const staleRemote = checkStaleRemoteMain(resolvedPath);
2059
- if (staleRemote) return staleRemote;
2060
- return checkVaultGitComparison(
2061
- resolvedPath,
2062
- "vault_git_behind",
2063
- "Vault commits behind",
2064
- "HEAD..origin/main",
2065
- "behind origin/main",
2066
- "0 commits behind origin/main"
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");
1882
+ }
1883
+ const first = findings[0];
1884
+ const n = findings.length;
1885
+ const fileWord = n === 1 ? "file" : "files";
1886
+ return check(
1887
+ "error",
1888
+ "vault_conflict_markers",
1889
+ "Vault conflict markers",
1890
+ `${n} ${fileWord}, first: ${first.path}:${first.line}`
2067
1891
  );
2068
1892
  }
2069
- function gitRefHash(resolvedPath, ref) {
1893
+ var hygieneProbe = {
1894
+ id: "hygiene",
1895
+ run(ctx) {
1896
+ return [
1897
+ checkDotStoreClean(ctx.readOnlyScanRoot),
1898
+ checkVaultConflictMarkers(ctx.readOnlyScanRoot)
1899
+ ];
1900
+ }
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() {
2070
1915
  try {
2071
- const out = execSync2(`git rev-parse --verify ${ref}`, {
2072
- cwd: resolvedPath,
1916
+ const out = execSync3("pgrep -f 'rclone.*mount'", {
2073
1917
  encoding: "utf8",
2074
- stdio: ["pipe", "pipe", "pipe"],
2075
- timeout: 2e3
1918
+ timeout: 2e3,
1919
+ stdio: ["pipe", "pipe", "pipe"]
2076
1920
  }).trim();
2077
- return out || void 0;
1921
+ const pids = out.split("\n").filter(Boolean);
1922
+ if (pids.length === 0) return null;
1923
+ return parseInt(pids[0], 10);
2078
1924
  } catch {
2079
- return void 0;
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;
2080
1936
  }
2081
1937
  }
2082
- function remoteMainHash(resolvedPath) {
1938
+ function parseRcloneFlags(pid) {
1939
+ const flags = /* @__PURE__ */ new Map();
2083
1940
  try {
2084
- const out = execSync2("git ls-remote origin refs/heads/main", {
2085
- cwd: resolvedPath,
2086
- encoding: "utf8",
2087
- stdio: ["pipe", "pipe", "pipe"],
2088
- timeout: 3e3
2089
- }).trim();
2090
- const hash = out.split(/\s+/)[0];
2091
- return /^[0-9a-f]{40}$/i.test(hash) ? hash : void 0;
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
+ }
2092
1957
  } catch {
2093
- return void 0;
2094
1958
  }
1959
+ return flags;
2095
1960
  }
2096
- function checkStaleRemoteMain(resolvedPath) {
2097
- if (resolvedPath === void 0) return void 0;
2098
- if (!existsSync7(join10(resolvedPath, ".git"))) return void 0;
2099
- const localOrigin = gitRefHash(resolvedPath, "origin/main");
2100
- if (!localOrigin) return void 0;
2101
- const remoteMain = remoteMainHash(resolvedPath);
2102
- if (!remoteMain || remoteMain === localOrigin) return void 0;
2103
- return check(
2104
- "warn",
2105
- "vault_git_behind",
2106
- "Vault commits behind",
2107
- `Remote main differs from local origin/main (${remoteMain.slice(0, 8)} != ${localOrigin.slice(0, 8)}) \u2014 run git fetch before trusting behind count`
2108
- );
2109
- }
2110
- function checkVaultLocalGit(resolvedPath) {
2111
- if (resolvedPath === void 0) {
2112
- return check("warn", "vault_local_git", "Vault local git", "Cannot check \u2014 WIKI_PATH not resolved");
2113
- }
2114
- if (!existsSync7(join10(resolvedPath, ".git"))) {
2115
- return check("warn", "vault_local_git", "Vault local git", "Not a git repository - sync features unavailable");
2116
- }
1961
+ function getRcloneVersion() {
2117
1962
  try {
2118
- execSync2("git rev-parse --git-dir", {
2119
- cwd: resolvedPath,
1963
+ const out = execSync3("rclone version", {
2120
1964
  encoding: "utf8",
2121
- stdio: ["pipe", "pipe", "pipe"],
2122
- timeout: 2e3
1965
+ timeout: 3e3,
1966
+ stdio: ["pipe", "pipe", "pipe"]
2123
1967
  });
2124
- return check("pass", "vault_local_git", "Vault local git", "Git metadata readable");
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
+ };
2125
1976
  } catch {
2126
- return check("error", "vault_local_git", "Vault local git", "Git metadata unreadable \u2014 local vault may be corrupt");
2127
- }
2128
- }
2129
- function checkVaultGithubRemote(resolvedPath, exec) {
2130
- if (resolvedPath === void 0) {
2131
- return check("pass", "vault_github_remote", "Vault GitHub remote", "No vault path \u2014 check skipped");
2132
- }
2133
- if (!existsSync7(join10(resolvedPath, ".git"))) {
2134
- return check("pass", "vault_github_remote", "Vault GitHub remote", "No git repo \u2014 check skipped");
2135
- }
2136
- const state = probeGithubReachability(resolvedPath, exec);
2137
- if (state === "ok") {
2138
- return check("pass", "vault_github_remote", "Vault GitHub remote", "git ls-remote origin main succeeded");
2139
- }
2140
- if (state === "unreachable") {
2141
- return check("warn", "vault_github_remote", "Vault GitHub remote", "GitHub unreachable (ls-remote failed) \u2014 local vault still usable");
2142
- }
2143
- return check("pass", "vault_github_remote", "Vault GitHub remote", "No origin remote \u2014 network probe skipped");
2144
- }
2145
- function checkVaultS3Remote(home, exec, env) {
2146
- const remote = resolveWikiS3Remote({ home, env });
2147
- if (!remote) {
2148
- return check("pass", "vault_s3_remote", "Vault S3 remote", "S3 remote not configured \u2014 check skipped");
2149
- }
2150
- const state = probeS3Reachability(remote, exec);
2151
- if (state === "ok") {
2152
- return check("pass", "vault_s3_remote", "Vault S3 remote", `rclone lsf ${remote} succeeded`);
2153
- }
2154
- if (state === "unreachable") {
2155
- return check("warn", "vault_s3_remote", "Vault S3 remote", `S3 remote unreachable (${remote}) \u2014 local/GitHub work may continue`);
1977
+ return null;
2156
1978
  }
2157
- return check("pass", "vault_s3_remote", "Vault S3 remote", "S3 remote not configured \u2014 check skipped");
2158
1979
  }
2159
- function checkVaultSnapshotterReachable(fleetLoad, checkSnapshotter, exec) {
2160
- if (!checkSnapshotter) {
2161
- return check("pass", "vault_snapshotter_reachable", "Vault snapshotter host", "Snapshotter SSH probe not requested \u2014 check skipped");
2162
- }
2163
- const alias = snapshotterAliasForLocalHost(fleetLoad);
2164
- if (!alias) {
2165
- return check("pass", "vault_snapshotter_reachable", "Vault snapshotter host", "No declared SSH alias from this host \u2014 check skipped");
2166
- }
2167
- const state = probeSnapshotterSsh(alias, exec);
2168
- if (state === "ok") {
2169
- return check("pass", "vault_snapshotter_reachable", "Vault snapshotter host", `SSH reachable via ${alias}`);
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
+ }
2170
1990
  }
2171
- return check("warn", "vault_snapshotter_reachable", "Vault snapshotter host", `Snapshotter unreachable via ${alias} \u2014 not a local vault corruption signal`);
1991
+ return null;
2172
1992
  }
2173
- function checkVaultPromotionLag(resolvedPath) {
2174
- if (resolvedPath === void 0) {
2175
- return check("pass", "vault_promotion_lag", "Vault promotion lag", "No vault path \u2014 check skipped");
2176
- }
2177
- if (!existsSync7(join10(resolvedPath, ".git"))) {
2178
- return check("pass", "vault_promotion_lag", "Vault promotion lag", "No git repo \u2014 check skipped");
2179
- }
1993
+ function getRcloneArgs(pid) {
2180
1994
  try {
2181
- const out = execSync2("git log -1 --format=%ct origin/main", {
2182
- cwd: resolvedPath,
2183
- encoding: "utf8",
2184
- stdio: ["pipe", "pipe", "pipe"],
2185
- timeout: 2e3
2186
- }).trim();
2187
- const ts = parseInt(out, 10);
2188
- if (!Number.isFinite(ts) || ts <= 0) {
2189
- return check("pass", "vault_promotion_lag", "Vault promotion lag", "origin/main timestamp unavailable \u2014 check skipped");
2190
- }
2191
- const ageHours = Math.floor((Date.now() / 1e3 - ts) / 3600);
2192
- if (ageHours > 48) {
2193
- return check("warn", "vault_promotion_lag", "Vault promotion lag", `Local origin/main snapshot is ${ageHours}h old \u2014 verify snapshotter/GitHub when online`);
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+/);
2194
2005
  }
2195
- return check("pass", "vault_promotion_lag", "Vault promotion lag", `origin/main age ${ageHours}h`);
2196
2006
  } catch {
2197
- return check("pass", "vault_promotion_lag", "Vault promotion lag", "Could not read origin/main \u2014 check skipped");
2007
+ return [];
2198
2008
  }
2199
2009
  }
2200
- function checkVaultGitComparison(resolvedPath, id, label, range, nonZeroSuffix, zeroDetail) {
2201
- if (resolvedPath === void 0) {
2202
- return check("pass", id, label, "No vault path \u2014 check skipped");
2203
- }
2204
- if (!existsSync7(join10(resolvedPath, ".git"))) {
2205
- return check("pass", id, label, "No git repo \u2014 check skipped");
2206
- }
2207
- if (!hasOriginMain(resolvedPath)) {
2208
- return check("pass", id, label, "origin/main unavailable \u2014 check skipped");
2209
- }
2010
+ function queryRcloneRC(rcAddr, fs) {
2210
2011
  try {
2211
- const count = parseInt(execSync2(`git rev-list --count ${range}`, {
2212
- cwd: resolvedPath,
2213
- encoding: "utf8",
2214
- stdio: ["pipe", "pipe", "pipe"],
2215
- timeout: 5e3
2216
- }).trim(), 10);
2217
- if (count > 0) {
2218
- return check("warn", id, label, `${count} commit(s) ${nonZeroSuffix}`);
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" };
2219
2021
  }
2220
- return check("pass", id, label, zeroDetail);
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
+ };
2221
2032
  } catch {
2222
- return check("warn", id, label, "Could not compare HEAD with origin/main");
2033
+ return { error: "RC endpoint unreachable", erroredFiles: 0, uploadsInProgress: 0, uploadsQueued: 0, outOfSpace: false, bytesUsed: 0, files: 0, totalSize: "unknown" };
2223
2034
  }
2224
2035
  }
2225
- function checkSatelliteLastRun(vaultPath, satelliteExpected) {
2226
- if (!satelliteExpected) {
2227
- return check("pass", "satellite_job_last_run", "Satellite job last run", "Satellite job not expected on this host");
2228
- }
2229
- if (vaultPath === void 0) {
2230
- return check("pass", "satellite_job_last_run", "Satellite job last run", "No vault path \u2014 check skipped");
2231
- }
2232
- const latestPath = satelliteLatestRunPath(vaultPath);
2233
- if (!existsSync7(latestPath)) {
2234
- return check("pass", "satellite_job_last_run", "Satellite job last run", "No latest-run.json \u2014 satellite has not run yet");
2235
- }
2036
+ function detectFuseMount(vaultPath) {
2236
2037
  try {
2237
- const health = evaluateSatelliteRunHealth(vaultPath, /* @__PURE__ */ new Date());
2238
- if (health.failed) {
2239
- const fc = health.failureClass;
2240
- const detail = fc ? `Last satellite run failed (failure_class: ${fc})` : "Last satellite run failed";
2241
- return check("error", "satellite_job_last_run", "Satellite job last run", detail);
2242
- }
2243
- if (health.stale && health.finishedAt) {
2244
- return check(
2245
- "warn",
2246
- "satellite_job_last_run",
2247
- "Satellite job last run",
2248
- `Last run finished_at is older than 26h (${health.finishedAt})`
2249
- );
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 };
2250
2064
  }
2251
- return check(
2252
- "pass",
2253
- "satellite_job_last_run",
2254
- "Satellite job last run",
2255
- health.finishedAt ? `Last run ok (finished_at ${health.finishedAt})` : "Last run ok"
2256
- );
2257
2065
  } catch {
2258
- return check("warn", "satellite_job_last_run", "Satellite job last run", `Could not read ${latestPath}`);
2259
2066
  }
2067
+ return null;
2260
2068
  }
2261
- function defaultSatelliteTimerDeps() {
2262
- return {
2263
- platform: () => platform2(),
2264
- systemctlIsActive: (unit) => {
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}` };
2077
+ }
2078
+ const writeMs = Date.now() - start;
2079
+ const readStart = Date.now();
2080
+ try {
2081
+ const back = readFile6(testFile, "utf8");
2082
+ const readMs = Date.now() - readStart;
2083
+ if (back !== payload) {
2265
2084
  try {
2266
- return execSync2(`systemctl is-active ${unit}`, {
2267
- encoding: "utf8",
2268
- timeout: 2e3,
2269
- stdio: ["pipe", "pipe", "pipe"]
2270
- }).trim();
2085
+ unlinkSync2(testFile);
2271
2086
  } catch {
2272
- return void 0;
2273
2087
  }
2088
+ return { success: false, writeMs, readMs, size: Buffer.byteLength(payload, "utf8"), error: "content mismatch \u2014 wrote and read-back differ" };
2274
2089
  }
2275
- };
2276
- }
2277
- function checkSatelliteTimer(satelliteExpected, deps = defaultSatelliteTimerDeps()) {
2278
- if (!satelliteExpected) {
2279
- return check("pass", "satellite_job_timer", "Satellite job timer", "Satellite job not expected on this host");
2280
- }
2281
- if (deps.platform() !== "linux") {
2282
- return check("pass", "satellite_job_timer", "Satellite job timer", "Timer check skipped \u2014 Linux only");
2283
- }
2284
- const out = deps.systemctlIsActive("agent-memory-trends.timer");
2285
- if (out === void 0) {
2286
- return check("pass", "satellite_job_timer", "Satellite job timer", "systemctl unavailable");
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}` };
2287
2096
  }
2288
- if (out === "active") {
2289
- return check("pass", "satellite_job_timer", "Satellite job timer", "systemd: agent-memory-trends.timer active");
2097
+ try {
2098
+ unlinkSync2(testFile);
2099
+ } catch {
2290
2100
  }
2291
- return check(
2292
- "error",
2293
- "satellite_job_timer",
2294
- "Satellite job timer",
2295
- `systemd: agent-memory-trends.timer is ${out || "not active"}`
2296
- );
2101
+ return { success: true, writeMs, readMs: Date.now() - readStart, size: Buffer.byteLength(payload, "utf8") };
2297
2102
  }
2298
- async function checkFleetIdentity(input) {
2299
- if (!input.vaultPath) {
2300
- return check("pass", "fleet_identity", "Fleet identity", "No vault path \u2014 check skipped");
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;
2301
2117
  }
2302
- const load = input.fleetLoad !== void 0 ? input.fleetLoad : await loadFleetManifestAndHost({
2303
- vault: input.vaultPath,
2304
- env: { ...process.env, WIKI_PATH: input.envValue ?? input.vaultPath },
2305
- home: input.home,
2306
- cwd: input.cwd ?? process.cwd(),
2307
- osHostname: process.env.HOSTNAME,
2308
- user: process.env.USER
2309
- });
2310
- if (!load) {
2311
- return check("pass", "fleet_identity", "Fleet identity", "Fleet manifest unavailable \u2014 check skipped");
2312
- }
2313
- if (load.identityStatus === "known") {
2314
- return check("pass", "fleet_identity", "Fleet identity", `Resolved ${load.hostId ?? "unknown"} via ${load.source ?? "unknown"}`);
2315
- }
2316
- const detail = load.warnings.length > 0 ? load.warnings.join("; ") : "Fleet identity is unresolved";
2317
- return check("warn", "fleet_identity", "Fleet identity", detail);
2318
- }
2319
- function pullLogPaths(home) {
2320
- const paths = platform2() === "darwin" ? [
2321
- join10(home, "Library", "Logs", "wiki-pull.log"),
2322
- join10(home, ".local", "state", "vault-sync", "log", "wiki-pull.log")
2323
- ] : [
2324
- join10(home, ".local", "state", "vault-sync", "log", "wiki-pull.log"),
2325
- join10(home, "Library", "Logs", "wiki-pull.log")
2326
- ];
2327
- return [...new Set(paths)];
2328
- }
2329
- function isRecentLogLine(line, nowMs) {
2330
- const match = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)/);
2331
- if (!match) return true;
2332
- const ts = Date.parse(match[1]);
2333
- if (!Number.isFinite(ts)) return true;
2334
- return nowMs - ts <= 24 * 60 * 60 * 1e3;
2335
- }
2336
- function checkVaultGitPullFailures(home) {
2337
- const path = pullLogPaths(home).find((p) => existsSync7(p));
2338
- if (!path) {
2339
- return check("pass", "vault_git_pull_failures", "Vault pull failures", "No wiki-pull.log found \u2014 check skipped");
2340
- }
2341
- try {
2342
- const lines = readFileSync6(path, "utf8").split(/\r?\n/).filter(Boolean);
2343
- const now = Date.now();
2344
- const failures = lines.filter(
2345
- (line) => isRecentLogLine(line, now) && /(pre-push pull failed|FAIL .*pull|FAIL .*rebase|cannot pull with rebase|unstaged changes)/i.test(line)
2346
- );
2347
- if (failures.length > 0) {
2348
- const sample = failures.slice(-2).map((line) => line.slice(0, 100)).join(" | ");
2349
- return check("warn", "vault_git_pull_failures", "Vault pull failures", `${failures.length} recent pull failure(s): ${sample}`);
2350
- }
2351
- return check("pass", "vault_git_pull_failures", "Vault pull failures", "No recent pull failures logged");
2352
- } catch {
2353
- return check("warn", "vault_git_pull_failures", "Vault pull failures", `Could not read ${path}`);
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;
2354
2130
  }
2131
+ if (consumed !== input.length) return null;
2132
+ return total;
2355
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
2356
2142
  function checkS3MountPerf(resolvedPath) {
2357
2143
  if (resolvedPath === void 0) {
2358
2144
  return check("pass", "s3_mount_perf", "S3 mount performance", "No vault path \u2014 check skipped");
@@ -2362,14 +2148,14 @@ function checkS3MountPerf(resolvedPath) {
2362
2148
  return check("pass", "s3_mount_perf", "S3 mount performance", "local disk");
2363
2149
  }
2364
2150
  const mountPoint = fuse.mountPoint;
2365
- const conceptsDir = join10(resolvedPath, "concepts");
2366
- if (!existsSync7(conceptsDir)) {
2151
+ const conceptsDir = join13(resolvedPath, "concepts");
2152
+ if (!existsSync10(conceptsDir)) {
2367
2153
  return check("pass", "s3_mount_perf", "S3 mount performance", `S3 FUSE mount (${mountPoint}), no concepts/ to benchmark`);
2368
2154
  }
2369
2155
  const start = Date.now();
2370
2156
  let timedOut = false;
2371
2157
  try {
2372
- execSync2(`rg -l "." "${conceptsDir}"`, {
2158
+ execSync4(`rg -l "." "${conceptsDir}"`, {
2373
2159
  timeout: 5e3,
2374
2160
  encoding: "utf8",
2375
2161
  stdio: ["pipe", "pipe", "pipe"]
@@ -2545,8 +2331,8 @@ function checkWriteTest(resolvedPath) {
2545
2331
  if (!fuse) {
2546
2332
  return check("pass", "s3_write_test", "S3 write test", "local disk \u2014 check skipped");
2547
2333
  }
2548
- const conceptsDir = join10(resolvedPath, "concepts");
2549
- if (!existsSync7(conceptsDir)) {
2334
+ const conceptsDir = join13(resolvedPath, "concepts");
2335
+ if (!existsSync10(conceptsDir)) {
2550
2336
  return check("pass", "s3_write_test", "S3 write test", "no concepts/ dir to test \u2014 check skipped");
2551
2337
  }
2552
2338
  const result = writeTest(conceptsDir);
@@ -2630,139 +2416,354 @@ function checkVfsCacheHealth(resolvedPath) {
2630
2416
  `${stats.files} files, ${(stats.bytesUsed / 1024 / 1024).toFixed(1)}MB \u2014 clean (0 errored, 0 pending)`
2631
2417
  );
2632
2418
  }
2633
- function checkVaultSyncPullHelper(home, env) {
2634
- const path = resolveVaultSyncPullHelper({
2635
- vault: "",
2636
- home,
2637
- env
2638
- });
2639
- if (path) {
2640
- return check("pass", "vault_sync_pull_helper", "Vault-sync pull helper", `Resolved: ${path}`);
2641
- }
2642
- return check(
2643
- "error",
2644
- "vault_sync_pull_helper",
2645
- "Vault-sync pull helper",
2646
- "Not found \u2014 install skillwiki@0.10.1+, redeploy vault-sync, or set SKILLWIKI_VAULT_SYNC_PULL_HELPER"
2647
- );
2648
- }
2649
- function checkVaultSyncReviewRequiredJournals(vaultPath) {
2650
- if (!vaultPath || !existsSync7(join10(vaultPath, ".git"))) {
2651
- return check("pass", "vault_sync_review_required_journals", "Review-required journals", "No git vault \u2014 check skipped");
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
+ ];
2652
2430
  }
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;
2653
2439
  try {
2654
- const ops = listReviewRequiredOps(vaultPath);
2655
- if (ops.length === 0) {
2656
- return check("pass", "vault_sync_review_required_journals", "Review-required journals", "None");
2657
- }
2658
- const sample = ops[0]?.opId ?? "?";
2659
- return check(
2660
- "warn",
2661
- "vault_sync_review_required_journals",
2662
- "Review-required journals",
2663
- `${ops.length} handoff(s); oldest/sample: ${sample} \u2014 if worktree clean: skillwiki sync journal clear-stale --dry-run`
2664
- );
2440
+ entries = readdirSync4(dir, { withFileTypes: true });
2665
2441
  } catch {
2666
- return check("pass", "vault_sync_review_required_journals", "Review-required journals", "Could not read journals \u2014 check skipped");
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
+ }
2667
2450
  }
2451
+ return results;
2668
2452
  }
2669
- function readVaultSyncConfig(home) {
2453
+ function findSkillNames(dir) {
2454
+ const results = [];
2455
+ let entries;
2670
2456
  try {
2671
- const content = readFileSync6(join10(home, ".skillwiki", ".env"), "utf8");
2672
- let installed = false;
2673
- let role;
2674
- let serviceScope;
2675
- let snapshotScript;
2676
- for (const line of content.split(/\r?\n/)) {
2677
- const trimmed = line.trim();
2678
- if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
2679
- const eq = trimmed.indexOf("=");
2680
- if (eq <= 0) continue;
2681
- const k = trimmed.slice(0, eq).trim();
2682
- const v = trimmed.slice(eq + 1).trim();
2683
- if (v.length === 0) continue;
2684
- if (k === "vault_sync.installed" && v === "true") installed = true;
2685
- if (k === "vault_sync.role") role = v;
2686
- if (k === "vault_sync.service_scope") serviceScope = v;
2687
- if (k === "vault_sync.snapshot_script") snapshotScript = v;
2688
- }
2689
- return { installed, role, serviceScope, snapshotScript };
2457
+ entries = readdirSync4(dir, { withFileTypes: true });
2690
2458
  } catch {
2691
- return { installed: false };
2459
+ return results;
2692
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;
2693
2467
  }
2694
- function resolveSnapshotGitWorktree(home) {
2695
- const configured = resolveConfiguredSnapshotWorktree(home);
2696
- if (configured) return configured;
2697
- const defaultPath = "/root/wiki-git";
2698
- return existsSync7(defaultPath) ? defaultPath : void 0;
2699
- }
2700
- function normalizeSystemdValue(raw) {
2701
- if (raw == null) return void 0;
2702
- const v = raw.trim();
2703
- if (!v || v === "n/a" || v === "N/A") return void 0;
2704
- return v;
2468
+ function findInstalledSkillMd(dir) {
2469
+ const directSkills = findSkillNames(dir).map((name) => join14(dir, name, "SKILL.md"));
2470
+ return directSkills.length > 0 ? directSkills : findSkillMd(dir);
2705
2471
  }
2706
- function hasCompletedRunEvidence(...timestamps) {
2707
- return timestamps.some((t) => normalizeSystemdValue(t ?? void 0) != null);
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");
2708
2495
  }
2709
- function loadSnapshotFixture(env) {
2710
- const path = env.VS_SNAPSHOT_HEALTH_FIXTURE;
2711
- if (!path || !existsSync7(path)) return null;
2712
- try {
2713
- return JSON.parse(readFileSync6(path, "utf8"));
2714
- } catch {
2715
- return null;
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" : ""}`);
2716
2525
  }
2526
+ const status = cliDuplicates.length > 0 ? "warn" : "info";
2527
+ return check(status, "skills_duplicate", "Skills not duplicated", parts.join("; "));
2717
2528
  }
2718
- function systemctlShowProperty(scope, unit, prop) {
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;
2536
+ }
2537
+ const pluginsRoot = join14(home, ".grok", "installed-plugins");
2538
+ if (!existsSync11(pluginsRoot)) return void 0;
2539
+ let entries;
2719
2540
  try {
2720
- const cmd = scope === "system" ? `systemctl show ${unit} --property=${prop} --value` : `systemctl --user show ${unit} --property=${prop} --value`;
2721
- const out = execSync2(cmd, {
2722
- encoding: "utf8",
2723
- timeout: 2e3,
2724
- stdio: ["pipe", "pipe", "pipe"]
2725
- });
2726
- return normalizeSystemdValue(out);
2541
+ entries = readdirSync4(pluginsRoot, { withFileTypes: true });
2727
2542
  } catch {
2728
2543
  return void 0;
2729
2544
  }
2730
- }
2731
- function snapshotProp(kind, prop, fixture, scope) {
2732
- if (fixture) {
2733
- const bag = fixture[kind];
2734
- const v = bag[prop];
2735
- return v == null ? void 0 : String(v);
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;
2551
+ }
2736
2552
  }
2737
- const unit = kind === "timer" ? "wiki-snapshot.timer" : "wiki-snapshot.service";
2738
- const liveProp = systemdPropertyFor(prop);
2739
- if (!liveProp) return void 0;
2740
- return systemctlShowProperty(scope, unit, liveProp);
2741
- }
2742
- function parseIsoToMs(ts) {
2743
- if (!ts || ts === "MISSING") return null;
2744
- const ms = Date.parse(ts);
2745
- return Number.isFinite(ms) ? ms : null;
2746
- }
2747
- function ageMinutes(nowMs, tsMs) {
2748
- if (tsMs == null) return null;
2749
- return Math.floor((nowMs - tsMs) / 6e4);
2553
+ return void 0;
2750
2554
  }
2751
- function snapshotterHealthChecks(scope, logDir, env) {
2752
- const fixture = loadSnapshotFixture(env);
2753
- const cadence = fixture ? fixture.cadence_minutes : parseInt(env.VS_SNAPSHOT_CADENCE_MINUTES ?? "30", 10) || 30;
2754
- const timeout = fixture ? fixture.service_timeout_seconds : parseInt(env.VS_SNAPSHOT_SERVICE_TIMEOUT_SECONDS ?? "900", 10) || 900;
2755
- const injectedNowMs = env.VS_SNAPSHOT_HEALTH_NOW ? Date.parse(env.VS_SNAPSHOT_HEALTH_NOW) : Number.NaN;
2756
- const nowMs = fixture ? Date.parse(fixture.now) : Number.isFinite(injectedNowMs) ? injectedNowMs : Date.now();
2757
- const warnAge = cadence * 2 + 15;
2758
- const errorAge = cadence * 4 + 15;
2759
- const tUnitfile = snapshotProp("timer", "unit_file_state", fixture, scope);
2760
- const tActive = snapshotProp("timer", "active_state", fixture, scope);
2761
- const tNext = snapshotProp("timer", "next_elapse", fixture, scope);
2762
- let jobs;
2763
- if (tUnitfile == null && tActive == null) {
2764
- jobs = check("warn", "vault_sync_jobs_enabled", "Vault sync jobs enabled", "wiki-snapshot.timer properties unavailable (read-only)");
2765
- } else if (tUnitfile === "enabled" && tActive === "active" && tNext) {
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) {
2596
+ return check(
2597
+ "warn",
2598
+ "activation_grok",
2599
+ "Grok activation",
2600
+ `${issues.join("; ")} \u2014 ${ACTIVATION_FIX_HINT}`
2601
+ );
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
+ );
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";
2676
+ function readVaultSyncConfig(home) {
2677
+ try {
2678
+ const content = readFileSync7(join15(home, ".skillwiki", ".env"), "utf8");
2679
+ let installed = false;
2680
+ let role;
2681
+ let serviceScope;
2682
+ let snapshotScript;
2683
+ for (const line of content.split(/\r?\n/)) {
2684
+ const trimmed = line.trim();
2685
+ if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
2686
+ const eq = trimmed.indexOf("=");
2687
+ if (eq <= 0) continue;
2688
+ const k = trimmed.slice(0, eq).trim();
2689
+ const v = trimmed.slice(eq + 1).trim();
2690
+ if (v.length === 0) continue;
2691
+ if (k === "vault_sync.installed" && v === "true") installed = true;
2692
+ if (k === "vault_sync.role") role = v;
2693
+ if (k === "vault_sync.service_scope") serviceScope = v;
2694
+ if (k === "vault_sync.snapshot_script") snapshotScript = v;
2695
+ }
2696
+ return { installed, role, serviceScope, snapshotScript };
2697
+ } catch {
2698
+ return { installed: false };
2699
+ }
2700
+ }
2701
+ function normalizeSystemdValue(raw) {
2702
+ if (raw == null) return void 0;
2703
+ const v = raw.trim();
2704
+ if (!v || v === "n/a" || v === "N/A") return void 0;
2705
+ return v;
2706
+ }
2707
+ function hasCompletedRunEvidence(...timestamps) {
2708
+ return timestamps.some((t) => normalizeSystemdValue(t ?? void 0) != null);
2709
+ }
2710
+ function loadSnapshotFixture(env) {
2711
+ const path = env.VS_SNAPSHOT_HEALTH_FIXTURE;
2712
+ if (!path || !existsSync12(path)) return null;
2713
+ try {
2714
+ return JSON.parse(readFileSync7(path, "utf8"));
2715
+ } catch {
2716
+ return null;
2717
+ }
2718
+ }
2719
+ function systemctlShowProperty(scope, unit, prop) {
2720
+ try {
2721
+ const cmd = scope === "system" ? `systemctl show ${unit} --property=${prop} --value` : `systemctl --user show ${unit} --property=${prop} --value`;
2722
+ const out = execSync5(cmd, {
2723
+ encoding: "utf8",
2724
+ timeout: 2e3,
2725
+ stdio: ["pipe", "pipe", "pipe"]
2726
+ });
2727
+ return normalizeSystemdValue(out);
2728
+ } catch {
2729
+ return void 0;
2730
+ }
2731
+ }
2732
+ function snapshotProp(kind, prop, fixture, scope) {
2733
+ if (fixture) {
2734
+ const bag = fixture[kind];
2735
+ const v = bag[prop];
2736
+ return v == null ? void 0 : String(v);
2737
+ }
2738
+ const unit = kind === "timer" ? "wiki-snapshot.timer" : "wiki-snapshot.service";
2739
+ const liveProp = systemdPropertyFor(prop);
2740
+ if (!liveProp) return void 0;
2741
+ return systemctlShowProperty(scope, unit, liveProp);
2742
+ }
2743
+ function parseIsoToMs(ts) {
2744
+ if (!ts || ts === "MISSING") return null;
2745
+ const ms = Date.parse(ts);
2746
+ return Number.isFinite(ms) ? ms : null;
2747
+ }
2748
+ function ageMinutes(nowMs, tsMs) {
2749
+ if (tsMs == null) return null;
2750
+ return Math.floor((nowMs - tsMs) / 6e4);
2751
+ }
2752
+ function snapshotterHealthChecks(scope, logDir, env) {
2753
+ const fixture = loadSnapshotFixture(env);
2754
+ const cadence = fixture ? fixture.cadence_minutes : parseInt(env.VS_SNAPSHOT_CADENCE_MINUTES ?? "30", 10) || 30;
2755
+ const timeout = fixture ? fixture.service_timeout_seconds : parseInt(env.VS_SNAPSHOT_SERVICE_TIMEOUT_SECONDS ?? "900", 10) || 900;
2756
+ const injectedNowMs = env.VS_SNAPSHOT_HEALTH_NOW ? Date.parse(env.VS_SNAPSHOT_HEALTH_NOW) : Number.NaN;
2757
+ const nowMs = fixture ? Date.parse(fixture.now) : Number.isFinite(injectedNowMs) ? injectedNowMs : Date.now();
2758
+ const warnAge = cadence * 2 + 15;
2759
+ const errorAge = cadence * 4 + 15;
2760
+ const tUnitfile = snapshotProp("timer", "unit_file_state", fixture, scope);
2761
+ const tActive = snapshotProp("timer", "active_state", fixture, scope);
2762
+ const tNext = snapshotProp("timer", "next_elapse", fixture, scope);
2763
+ let jobs;
2764
+ if (tUnitfile == null && tActive == null) {
2765
+ jobs = check("warn", "vault_sync_jobs_enabled", "Vault sync jobs enabled", "wiki-snapshot.timer properties unavailable (read-only)");
2766
+ } else if (tUnitfile === "enabled" && tActive === "active" && tNext) {
2766
2767
  jobs = check("pass", "vault_sync_jobs_enabled", "Vault sync jobs enabled", `wiki-snapshot.timer enabled+active, next=${tNext} (${scope})`);
2767
2768
  } else {
2768
2769
  jobs = check("error", "vault_sync_jobs_enabled", "Vault sync jobs enabled", `wiki-snapshot.timer not eligible: unit_file_state=${tUnitfile ?? "missing"} active_state=${tActive ?? "missing"} next_elapse=${tNext ?? "missing"} (${scope})`);
@@ -2797,7 +2798,7 @@ function snapshotterHealthChecks(scope, logDir, env) {
2797
2798
  let completionOutcome = "unknown";
2798
2799
  const logRecords = fixture ? fixture.log_records : (() => {
2799
2800
  try {
2800
- const content = readFileSync6(join10(logDir, "wiki-snapshot.log"), "utf8");
2801
+ const content = readFileSync7(join15(logDir, "wiki-snapshot.log"), "utf8");
2801
2802
  return content.split(/\r?\n/).filter(Boolean);
2802
2803
  } catch {
2803
2804
  return [];
@@ -2851,7 +2852,7 @@ function snapshotterHealthChecks(scope, logDir, env) {
2851
2852
  return [jobs, serviceResult, freshness, consecutiveFailures];
2852
2853
  }
2853
2854
  function vaultSyncChecks(input) {
2854
- const os = input.os ?? platform2();
2855
+ const os = input.os ?? platform3();
2855
2856
  const home = input.home;
2856
2857
  if (!input.vaultSyncInstalled) {
2857
2858
  const skip = (id, label) => check("pass", id, label, "vault-sync not installed \u2014 check skipped");
@@ -2867,14 +2868,14 @@ function vaultSyncChecks(input) {
2867
2868
  ];
2868
2869
  }
2869
2870
  const isMac = os === "darwin";
2870
- const logDir = input.logDir ?? (isMac ? join10(home, "Library", "Logs") : join10(home, ".local", "state", "vault-sync", "log"));
2871
- const shareDir = input.shareDir ?? (isMac ? join10(home, "Library", "Application Support", "vault-sync", "bin") : join10(home, ".local", "share", "vault-sync", "bin"));
2872
- const filterPath = input.filterPath ?? join10(home, ".config", "rclone", "wiki-push-filters.txt");
2873
- 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");
2874
2875
  const legacySnapshotPath = "/root/.hermes/scripts/wiki-snapshot-v3.sh";
2875
- const snapshotPath = input.snapshotScriptPath ?? (existsSync7(packagedSnapshotPath) ? packagedSnapshotPath : legacySnapshotPath);
2876
+ const snapshotPath = input.snapshotScriptPath ?? (existsSync12(packagedSnapshotPath) ? packagedSnapshotPath : legacySnapshotPath);
2876
2877
  if (input.vaultSyncRole === "snapshotter") {
2877
- 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}`);
2878
2879
  const serviceScope = input.vaultSyncServiceScope ?? "user";
2879
2880
  const healthChecks = snapshotterHealthChecks(serviceScope, logDir, input.env ?? process.env);
2880
2881
  const cFetch2 = check(
@@ -2891,7 +2892,7 @@ function vaultSyncChecks(input) {
2891
2892
  );
2892
2893
  let c52;
2893
2894
  try {
2894
- if (!existsSync7(snapshotPath)) {
2895
+ if (!existsSync12(snapshotPath)) {
2895
2896
  c52 = check(
2896
2897
  "error",
2897
2898
  "vault_sync_snapshot_guard",
@@ -2899,7 +2900,7 @@ function vaultSyncChecks(input) {
2899
2900
  `Snapshot script not found at ${snapshotPath}`
2900
2901
  );
2901
2902
  } else {
2902
- const content = readFileSync6(snapshotPath, "utf8");
2903
+ const content = readFileSync7(snapshotPath, "utf8");
2903
2904
  if (!content.includes("--max-delete")) {
2904
2905
  c52 = check(
2905
2906
  "error",
@@ -2926,18 +2927,18 @@ function vaultSyncChecks(input) {
2926
2927
  }
2927
2928
  return [c12, ...healthChecks, cFetch2, c42, c52];
2928
2929
  }
2929
- const pushScriptPath = join10(shareDir, "wiki-push.sh");
2930
- 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`);
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`);
2931
2932
  let c2;
2932
2933
  try {
2933
2934
  if (isMac) {
2934
- const uidStr = execSync2("id -u", {
2935
+ const uidStr = execSync5("id -u", {
2935
2936
  encoding: "utf8",
2936
2937
  timeout: 2e3,
2937
2938
  stdio: ["pipe", "pipe", "pipe"]
2938
2939
  }).trim();
2939
2940
  const uid = parseInt(uidStr, 10);
2940
- execSync2(`launchctl print gui/${uid}/com.karlchow.wiki-push`, {
2941
+ execSync5(`launchctl print gui/${uid}/com.karlchow.wiki-push`, {
2941
2942
  encoding: "utf8",
2942
2943
  timeout: 2e3,
2943
2944
  stdio: ["pipe", "ignore", "ignore"]
@@ -2949,7 +2950,7 @@ function vaultSyncChecks(input) {
2949
2950
  "launchd: com.karlchow.wiki-push loaded"
2950
2951
  );
2951
2952
  } else {
2952
- const out = execSync2("systemctl --user is-enabled wiki-push.timer", {
2953
+ const out = execSync5("systemctl --user is-enabled wiki-push.timer", {
2953
2954
  encoding: "utf8",
2954
2955
  timeout: 2e3,
2955
2956
  stdio: ["pipe", "pipe", "pipe"]
@@ -2978,10 +2979,10 @@ function vaultSyncChecks(input) {
2978
2979
  "Scheduler check failed \u2014 run vault-sync-install"
2979
2980
  );
2980
2981
  }
2981
- const logFile = join10(logDir, "wiki-push.log");
2982
+ const logFile = join15(logDir, "wiki-push.log");
2982
2983
  let c3;
2983
2984
  try {
2984
- const logContent = readFileSync6(logFile, "utf8");
2985
+ const logContent = readFileSync7(logFile, "utf8");
2985
2986
  const lines = logContent.trim().split("\n").filter(Boolean);
2986
2987
  if (lines.length === 0) {
2987
2988
  c3 = check(
@@ -3037,7 +3038,7 @@ function vaultSyncChecks(input) {
3037
3038
  }
3038
3039
  }
3039
3040
  } catch {
3040
- c3 = existsSync7(logDir) ? check(
3041
+ c3 = existsSync12(logDir) ? check(
3041
3042
  "warn",
3042
3043
  "vault_sync_last_push_age",
3043
3044
  "Vault sync last push recency",
@@ -3049,10 +3050,10 @@ function vaultSyncChecks(input) {
3049
3050
  `Log directory not found at ${logDir}`
3050
3051
  );
3051
3052
  }
3052
- const fetchLogFile = join10(logDir, "wiki-fetch.log");
3053
+ const fetchLogFile = join15(logDir, "wiki-fetch.log");
3053
3054
  let cFetch;
3054
3055
  try {
3055
- const logContent = readFileSync6(fetchLogFile, "utf8");
3056
+ const logContent = readFileSync7(fetchLogFile, "utf8");
3056
3057
  const lines = logContent.trim().split("\n").filter(Boolean);
3057
3058
  if (lines.length === 0) {
3058
3059
  cFetch = check(
@@ -3096,7 +3097,7 @@ function vaultSyncChecks(input) {
3096
3097
  }
3097
3098
  let c4;
3098
3099
  try {
3099
- if (!existsSync7(filterPath)) {
3100
+ if (!existsSync12(filterPath)) {
3100
3101
  c4 = check(
3101
3102
  "error",
3102
3103
  "vault_sync_filter_present",
@@ -3104,7 +3105,7 @@ function vaultSyncChecks(input) {
3104
3105
  `Filter file not found at ${filterPath}`
3105
3106
  );
3106
3107
  } else {
3107
- const content = readFileSync6(filterPath, "utf8");
3108
+ const content = readFileSync7(filterPath, "utf8");
3108
3109
  const requiredExcludes = [
3109
3110
  "remotely-save/data.json",
3110
3111
  ".skillwiki/sync.lock",
@@ -3147,7 +3148,7 @@ function vaultSyncChecks(input) {
3147
3148
  );
3148
3149
  } else {
3149
3150
  try {
3150
- if (!existsSync7(snapshotPath)) {
3151
+ if (!existsSync12(snapshotPath)) {
3151
3152
  c5 = check(
3152
3153
  "error",
3153
3154
  "vault_sync_snapshot_guard",
@@ -3155,7 +3156,7 @@ function vaultSyncChecks(input) {
3155
3156
  `Snapshot script not found at ${snapshotPath}`
3156
3157
  );
3157
3158
  } else {
3158
- const content = readFileSync6(snapshotPath, "utf8");
3159
+ const content = readFileSync7(snapshotPath, "utf8");
3159
3160
  if (!content.includes("--max-delete")) {
3160
3161
  c5 = check(
3161
3162
  "error",
@@ -3183,199 +3184,633 @@ function vaultSyncChecks(input) {
3183
3184
  }
3184
3185
  return [c1, c2, c3, cFetch, c4, c5];
3185
3186
  }
3186
- function findSkillMd(dir) {
3187
- const results = [];
3188
- let entries;
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
+ }
3189
3207
  try {
3190
- entries = readdirSync3(dir, { withFileTypes: true });
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
+ );
3191
3219
  } catch {
3192
- return results;
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})`
3326
+ );
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;
3350
+ }
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;
3432
+ } catch {
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`
3478
+ );
3479
+ }
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(
3500
+ "warn",
3501
+ "fuse_staleness",
3502
+ "FUSE visibility freshness",
3503
+ `S3 FUSE mount (${fuse.mountPoint}) but no rclone process found \u2014 cannot audit dir-cache freshness`
3504
+ );
3505
+ }
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(
3516
+ "warn",
3517
+ "fuse_staleness",
3518
+ "FUSE visibility freshness",
3519
+ `PID ${pid}: could not parse --dir-cache-time=${rawDirCache}`
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`;
3525
+ } else {
3526
+ dirCacheDetail = `PID ${pid}: --dir-cache-time=${rawDirCache} (${formatDurationForHumans2(seconds)}), within <=15m SLA`;
3527
+ }
3528
+ }
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(
3548
+ "warn",
3549
+ "fuse_staleness",
3550
+ "FUSE visibility freshness",
3551
+ `${dirCacheDetail}; VFS cache degradation: ${issues.join(", ")}`
3552
+ );
3553
+ }
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 });
3583
+ } catch {
3584
+ return void 0;
3585
+ }
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");
3610
+ } else {
3611
+ try {
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");
3622
+ }
3623
+ } catch {
3624
+ issues.push("could not read ~/.grok/AGENTS.md");
3625
+ }
3626
+ }
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");
3649
+ }
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
+ }
3193
3684
  }
3194
- for (const entry of entries) {
3195
- if (entry.isFile() && entry.name === "SKILL.md") {
3196
- results.push(join10(dir, entry.name));
3197
- } else if (entry.isDirectory()) {
3198
- 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);
3199
3691
  }
3200
3692
  }
3201
- return results;
3202
- }
3203
- function findInstalledSkillMd(dir) {
3204
- const directSkills = findSkillNames(dir).map((name) => join10(dir, name, "SKILL.md"));
3205
- return directSkills.length > 0 ? directSkills : findSkillMd(dir);
3206
- }
3207
- function findSkillNames(dir) {
3208
- const results = [];
3209
- let entries;
3210
- try {
3211
- entries = readdirSync3(dir, { withFileTypes: true });
3212
- } catch {
3213
- return results;
3693
+ if (!scannedAny) {
3694
+ walk(resolvedPath, "");
3214
3695
  }
3215
- for (const entry of entries) {
3216
- if (entry.isDirectory() && existsSync7(join10(dir, entry.name, "SKILL.md"))) {
3217
- results.push(entry.name);
3218
- }
3696
+ if (found.length === 0) {
3697
+ return check("pass", "ds_store_noise", "No .DS_Store noise", "No .DS_Store files found in vault");
3219
3698
  }
3220
- 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
+ );
3221
3707
  }
3222
- var METRIC_TYPES = ["entities", "concepts", "comparisons", "queries", "meta"];
3223
- function doctorReadOnlyScanRoot(resolvedPath) {
3224
- 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");
3225
3734
  }
3226
- async function vaultMetrics(resolvedPath) {
3227
- const ids = [
3228
- ["vault_metric_pages", "Vault pages by type"],
3229
- ["vault_metric_orphans", "Vault orphan rate"],
3230
- ["vault_metric_bridges", "Vault bridge count"],
3231
- ["vault_metric_cohesion", "Mean community cohesion"],
3232
- ["vault_metric_log_size", "Vault log size"]
3233
- ];
3234
- const noVault = () => ids.map(([id, label]) => check("info", id, label, "no vault configured"));
3235
- if (!resolvedPath) return noVault();
3236
- const scanRoot = doctorReadOnlyScanRoot(resolvedPath);
3237
- const scan = await scanVault(scanRoot);
3238
- if (!scan.ok) return noVault();
3239
- const tk = scan.data.typedKnowledge;
3240
- const typedCount = tk.length;
3241
- const perType = METRIC_TYPES.map((d) => `${d} ${tk.filter((p) => p.relPath.startsWith(d + "/")).length}`).join(", ");
3242
- const adj = await buildWikilinkAdjacency(tk, void 0, scan.data.allMarkdown);
3243
- const g = toUndirectedWeighted(adj);
3244
- const nodes = [...g.keys()];
3245
- const total = nodes.length;
3246
- const orphanCount = nodes.filter((n) => g.get(n).size === 0).length;
3247
- const orphanRate = total > 0 ? Math.round(orphanCount / total * 1e3) / 10 : 0;
3248
- const comm = louvain(g);
3249
- const groups = /* @__PURE__ */ new Map();
3250
- for (const [node, c] of comm) {
3251
- const arr = groups.get(c);
3252
- if (arr) arr.push(node);
3253
- 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;
3254
3745
  }
3255
- const cohesions = [...groups.values()].filter((m) => m.length >= 2).map((m) => communityCohesion(m, g));
3256
- const meanCohesion = cohesions.length > 0 ? Math.round(cohesions.reduce((a, b) => a + b, 0) / cohesions.length * 1e3) / 1e3 : 0;
3257
- let bridges = 0;
3258
- for (const n of nodes) {
3259
- const nbrComms = /* @__PURE__ */ new Set();
3260
- for (const nb of g.get(n).keys()) nbrComms.add(comm.get(nb));
3261
- if (nbrComms.size >= 3) bridges++;
3746
+ getRegisteredProbes() {
3747
+ return this.probes;
3262
3748
  }
3263
- let logLines = 0;
3264
- try {
3265
- logLines = readFileSync6(join10(scanRoot, "log.md"), "utf8").split("\n").length;
3266
- } 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 }) };
3267
3806
  }
3268
- return [
3269
- check("info", "vault_metric_pages", "Vault pages by type", `${total} graph node(s) (${typedCount} typed; ${perType})`),
3270
- check("info", "vault_metric_orphans", "Vault orphan rate", `${orphanRate}% (${orphanCount}/${total} degree-0)`),
3271
- check("info", "vault_metric_bridges", "Vault bridge count", `${bridges} page(s) link >= 3 communities`),
3272
- check("info", "vault_metric_cohesion", "Mean community cohesion", `${meanCohesion} across ${cohesions.length} communities (size >= 2)`),
3273
- check("info", "vault_metric_log_size", "Vault log size", `${logLines} lines`)
3274
- ];
3275
- }
3807
+ };
3276
3808
  async function runDoctor(input) {
3277
- const checks = [];
3278
- const devSourceRun = isDevSourceRun(input.argv);
3279
- const vsConfig = readVaultSyncConfig(input.home);
3280
- checks.push(checkNodeVersion());
3281
- checks.push(checkCliChannels(input.argv, input.home));
3282
- checks.push(await checkConfigFile(input.home));
3283
- checks.push(await checkProfiles(input.home));
3284
- checks.push(await checkProjectLocalOverride(input.cwd));
3285
- const resolved = await resolveRuntimePath({
3286
- flag: void 0,
3287
- envValue: input.envValue,
3288
- home: input.home,
3289
- cwd: input.cwd
3290
- });
3291
- if (resolved.ok) {
3292
- checks.push(check("pass", "wiki_path_set", "WIKI_PATH configured", `Resolved via ${resolved.data.source}: ${resolved.data.path}`));
3293
- } else {
3294
- checks.push(check("error", "wiki_path_set", "WIKI_PATH configured", "No vault configured. Run `skillwiki init` or pass --vault."));
3295
- }
3296
- const resolvedPath = resolved.ok ? resolved.data.path : void 0;
3297
- const gitCheckPath = vsConfig.role === "snapshotter" ? resolveSnapshotGitWorktree(input.home) ?? resolvedPath : resolvedPath;
3298
- checks.push(checkWikiPathExists(resolvedPath));
3299
- checks.push(checkVaultStructure(resolvedPath));
3300
- checks.push(checkObsidianTemplates(resolvedPath));
3301
- checks.push(checkVaultGitRemote(gitCheckPath));
3302
- const fleetLoad = resolvedPath ? await loadFleetManifestAndHost({
3303
- vault: resolvedPath,
3304
- env: { ...process.env, WIKI_PATH: input.envValue ?? resolvedPath },
3305
- home: input.home,
3306
- cwd: input.cwd,
3307
- osHostname: process.env.HOSTNAME,
3308
- user: process.env.USER
3309
- }) : null;
3310
- checks.push(await checkFleetIdentity({
3311
- vaultPath: resolvedPath,
3312
- home: input.home,
3313
- cwd: input.cwd,
3314
- envValue: input.envValue,
3315
- fleetLoad
3316
- }));
3317
- checks.push(checkSyncLastPush(gitCheckPath));
3318
- checks.push(checkVaultGitDirty(gitCheckPath));
3319
- checks.push(checkVaultGitAhead(gitCheckPath));
3320
- checks.push(checkVaultGitBehind(gitCheckPath));
3321
- checks.push(checkVaultGitPullFailures(input.home));
3322
- checks.push(checkVaultLocalGit(gitCheckPath));
3323
- checks.push(checkVaultGithubRemote(gitCheckPath, input.execProbe));
3324
- checks.push(checkVaultS3Remote(input.home, input.execProbe, input.env ?? process.env));
3325
- checks.push(checkVaultSnapshotterReachable(fleetLoad, input.checkSnapshotter, input.execProbe));
3326
- checks.push(checkVaultPromotionLag(gitCheckPath));
3327
- const readOnlyScanRoot = resolvedPath ? doctorReadOnlyScanRoot(resolvedPath) : void 0;
3328
- checks.push(checkDotStoreClean(readOnlyScanRoot));
3329
- checks.push(checkVaultConflictMarkers(readOnlyScanRoot));
3330
- checks.push(checkS3MountPerf(resolvedPath));
3331
- checks.push(checkS3MountFreshness(resolvedPath));
3332
- checks.push(checkRcloneFlagAudit(resolvedPath));
3333
- checks.push(checkRcloneVersion(resolvedPath, vsConfig.installed));
3334
- checks.push(checkWriteTest(resolvedPath));
3335
- checks.push(checkVfsCacheHealth(resolvedPath));
3336
- checks.push(checkSkillsInstalled(input.home, input.cwd));
3337
- checks.push(checkDuplicateSkills(input.home));
3338
- checks.push(checkGrokActivation(input.home, input.cwd));
3339
- checks.push(checkNpmUpdate(input.home, input.currentVersion));
3340
- checks.push(checkPluginVersionDrift(input.home, input.currentVersion, devSourceRun));
3341
- checks.push(...vaultSyncChecks({
3342
- home: input.home,
3343
- vaultSyncInstalled: vsConfig.installed,
3344
- vaultSyncRole: vsConfig.role,
3345
- vaultSyncServiceScope: vsConfig.serviceScope,
3346
- snapshotScriptPath: vsConfig.snapshotScript,
3347
- env: input.env ?? process.env
3348
- }));
3349
- checks.push(checkVaultSyncPullHelper(input.home, input.env ?? process.env));
3350
- checks.push(checkVaultSyncReviewRequiredJournals(resolvedPath));
3351
- const satelliteGate = satelliteGateFromFleetLoad(fleetLoad);
3352
- checks.push(checkSatelliteLastRun(resolvedPath, satelliteGate.satelliteExpected));
3353
- checks.push(checkSatelliteTimer(satelliteGate.satelliteExpected));
3354
- checks.push(...await vaultMetrics(resolvedPath));
3355
- const summary = {
3356
- pass: checks.filter((c) => c.status === "pass").length,
3357
- info: checks.filter((c) => c.status === "info").length,
3358
- warn: checks.filter((c) => c.status === "warn").length,
3359
- error: checks.filter((c) => c.status === "error").length
3360
- };
3361
- const exitCode = summary.error > 0 ? ExitCode.DOCTOR_HAS_ERRORS : summary.warn > 0 ? ExitCode.DOCTOR_HAS_WARNINGS : ExitCode.OK;
3362
- const statusIcon = { pass: "\u2713", info: "i", warn: "\u26A0", error: "\u2717" };
3363
- const lines = checks.map((c) => {
3364
- const icon = statusIcon[c.status];
3365
- const padded = c.label.padEnd(24);
3366
- return ` ${icon} ${padded} ${c.detail}`;
3367
- });
3368
- lines.push("");
3369
- const summaryParts = [`${summary.pass} pass`];
3370
- if (summary.info > 0) summaryParts.push(`${summary.info} info`);
3371
- summaryParts.push(`${summary.warn} warn`, `${summary.error} error`);
3372
- lines.push(summaryParts.join(" \xB7 "));
3373
- const humanHint = lines.join("\n");
3374
- return { exitCode, result: ok({ checks, summary, humanHint }) };
3809
+ return new DoctorRunner().run(input);
3375
3810
  }
3376
3811
 
3377
3812
  // src/utils/package-info.ts
3378
- import { readFileSync as readFileSync7 } from "fs";
3813
+ import { readFileSync as readFileSync11 } from "fs";
3379
3814
  function packageJsonCandidateUrls(baseUrl = import.meta.url) {
3380
3815
  return [
3381
3816
  new URL("../package.json", baseUrl),
@@ -3385,7 +3820,7 @@ function packageJsonCandidateUrls(baseUrl = import.meta.url) {
3385
3820
  function readCliPackageJson(baseUrl = import.meta.url) {
3386
3821
  for (const url of packageJsonCandidateUrls(baseUrl)) {
3387
3822
  try {
3388
- const pkg = JSON.parse(readFileSync7(url, "utf8"));
3823
+ const pkg = JSON.parse(readFileSync11(url, "utf8"));
3389
3824
  if (typeof pkg.version === "string") {
3390
3825
  return { ...pkg, version: pkg.version };
3391
3826
  }
@@ -3397,8 +3832,8 @@ function readCliPackageJson(baseUrl = import.meta.url) {
3397
3832
 
3398
3833
  // src/utils/vault-write-gates.ts
3399
3834
  import { execFileSync } from "child_process";
3400
- import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
3401
- 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";
3402
3837
  var DEFAULT_DIRTY_VOLUME_THRESHOLD = 50;
3403
3838
  var DEFAULT_CAPTURE_BUDGET = 20;
3404
3839
  var DEFAULT_NO_DECISION_STREAK = 3;
@@ -3460,7 +3895,7 @@ function measureDirtyVolume(vault) {
3460
3895
  is_git_repo: false,
3461
3896
  ...extra
3462
3897
  });
3463
- if (!existsSync8(vault) || !statSync3(vault).isDirectory()) {
3898
+ if (!existsSync18(vault) || !statSync3(vault).isDirectory()) {
3464
3899
  return empty();
3465
3900
  }
3466
3901
  const gitDir = git(vault, ["rev-parse", "--absolute-git-dir"]);
@@ -3498,8 +3933,8 @@ function measureDirtyVolume(vault) {
3498
3933
  if (!rel) continue;
3499
3934
  if (code === "??") {
3500
3935
  untracked += 1;
3501
- const abs = join11(vault, rel);
3502
- if (existsSync8(abs) && statSync3(abs).isDirectory()) {
3936
+ const abs = join20(vault, rel);
3937
+ if (existsSync18(abs) && statSync3(abs).isDirectory()) {
3503
3938
  const files = listFilesRecursive(abs);
3504
3939
  expanded += files.length;
3505
3940
  addBucket(rel, files.length);
@@ -3529,13 +3964,13 @@ function listFilesRecursive(dir) {
3529
3964
  const out = [];
3530
3965
  let entries;
3531
3966
  try {
3532
- entries = readdirSync4(dir);
3967
+ entries = readdirSync7(dir);
3533
3968
  } catch {
3534
3969
  return out;
3535
3970
  }
3536
3971
  for (const name of entries) {
3537
3972
  if (name === ".git") continue;
3538
- const p = join11(dir, name);
3973
+ const p = join20(dir, name);
3539
3974
  try {
3540
3975
  const st = statSync3(p);
3541
3976
  if (st.isDirectory()) out.push(...listFilesRecursive(p));
@@ -3630,7 +4065,7 @@ var CAPTURE_HYGIENE_CONTRACT = `After each productive cycle (or when daily captu
3630
4065
  function listProjectDayCaptures(vault, project, day) {
3631
4066
  const found = [];
3632
4067
  const slug = project.replace(/^\[\[/, "").replace(/\]\]$/, "").trim();
3633
- if (!slug || !existsSync8(vault)) return found;
4068
+ if (!slug || !existsSync18(vault)) return found;
3634
4069
  const consider = (abs, rel) => {
3635
4070
  if (!rel.endsWith(".md")) return;
3636
4071
  const base = rel.split(/[/\\]/).pop() ?? "";
@@ -3647,7 +4082,7 @@ function listProjectDayCaptures(vault, project, day) {
3647
4082
  }
3648
4083
  if (norm.startsWith("raw/transcripts/")) {
3649
4084
  try {
3650
- const body = readFileSync8(abs, "utf8");
4085
+ const body = readFileSync12(abs, "utf8");
3651
4086
  if (body.includes(`project: ${slug}`) || body.includes(`project: "[[${slug}]]"`) || body.includes(`project: [[${slug}]]`)) {
3652
4087
  found.push(norm);
3653
4088
  } else if (base.includes(slug)) {
@@ -3658,15 +4093,15 @@ function listProjectDayCaptures(vault, project, day) {
3658
4093
  }
3659
4094
  };
3660
4095
  const walk = (dir, relBase) => {
3661
- if (!existsSync8(dir)) return;
4096
+ if (!existsSync18(dir)) return;
3662
4097
  let entries;
3663
4098
  try {
3664
- entries = readdirSync4(dir);
4099
+ entries = readdirSync7(dir);
3665
4100
  } catch {
3666
4101
  return;
3667
4102
  }
3668
4103
  for (const name of entries) {
3669
- const abs = join11(dir, name);
4104
+ const abs = join20(dir, name);
3670
4105
  const rel = relBase ? `${relBase}/${name}` : name;
3671
4106
  try {
3672
4107
  const st = statSync3(abs);
@@ -3676,16 +4111,16 @@ function listProjectDayCaptures(vault, project, day) {
3676
4111
  }
3677
4112
  }
3678
4113
  };
3679
- walk(join11(vault, "raw", "transcripts"), "raw/transcripts");
3680
- walk(join11(vault, "projects", slug, "raw", "transcripts"), `projects/${slug}/raw/transcripts`);
3681
- walk(join11(vault, "projects", slug, "requirements"), `projects/${slug}/requirements`);
3682
- const workRoot = join11(vault, "projects", slug, "work");
3683
- 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)) {
3684
4119
  try {
3685
- for (const name of readdirSync4(workRoot)) {
4120
+ for (const name of readdirSync7(workRoot)) {
3686
4121
  if (!name.startsWith(day)) continue;
3687
4122
  if (!/investigate|pilot-q|research|cycle|dev-loop/.test(name)) continue;
3688
- const abs = join11(workRoot, name);
4123
+ const abs = join20(workRoot, name);
3689
4124
  if (statSync3(abs).isDirectory()) {
3690
4125
  for (const f of listFilesRecursive(abs)) {
3691
4126
  const rel = relative2(vault, f).replace(/\\/g, "/");
@@ -3730,7 +4165,7 @@ function evaluateCaptureBudget(input) {
3730
4165
  return { allowed: true, reason: "under_budget", report };
3731
4166
  }
3732
4167
  function runWritePreflight(input) {
3733
- if (!input.vault || !existsSync8(input.vault)) {
4168
+ if (!input.vault || !existsSync18(input.vault)) {
3734
4169
  return err(GateError.VAULT_PATH_INVALID, { path: input.vault });
3735
4170
  }
3736
4171
  const want = new Set(input.checks ?? ["all"]);
@@ -3790,8 +4225,8 @@ function runWritePreflight(input) {
3790
4225
 
3791
4226
  // src/commands/observe.ts
3792
4227
  import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
3793
- import { existsSync as existsSync9, statSync as statSync4 } from "fs";
3794
- import { join as join12 } from "path";
4228
+ import { existsSync as existsSync19, statSync as statSync4 } from "fs";
4229
+ import { join as join21 } from "path";
3795
4230
  import { createHash as createHash2 } from "crypto";
3796
4231
  var ALLOWED_KINDS = /* @__PURE__ */ new Set(["note", "bug", "task", "idea", "session-log"]);
3797
4232
  function slugify(text) {
@@ -3814,7 +4249,7 @@ async function runObserve(input) {
3814
4249
  result: err("SCHEME_REJECTED", { message: "Text must not be empty" })
3815
4250
  };
3816
4251
  }
3817
- if (!existsSync9(input.vault) || !statSync4(input.vault).isDirectory()) {
4252
+ if (!existsSync19(input.vault) || !statSync4(input.vault).isDirectory()) {
3818
4253
  return {
3819
4254
  exitCode: ExitCode.VAULT_PATH_INVALID,
3820
4255
  result: err("VAULT_PATH_INVALID", { path: input.vault })
@@ -3840,7 +4275,7 @@ async function runObserve(input) {
3840
4275
  };
3841
4276
  }
3842
4277
  }
3843
- const transcriptsDir = join12(input.vault, "raw", "transcripts");
4278
+ const transcriptsDir = join21(input.vault, "raw", "transcripts");
3844
4279
  try {
3845
4280
  await mkdir3(transcriptsDir, { recursive: true });
3846
4281
  } catch {
@@ -3852,7 +4287,7 @@ async function runObserve(input) {
3852
4287
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3853
4288
  const slug = slugify(input.text);
3854
4289
  const fileName = `${today}-observation-${slug}.md`;
3855
- const filePath = join12(transcriptsDir, fileName);
4290
+ const filePath = join21(transcriptsDir, fileName);
3856
4291
  const body = `
3857
4292
  ${input.text.trim()}
3858
4293
  `;
@@ -3894,7 +4329,7 @@ ${input.text.trim()}
3894
4329
  // src/commands/memory.ts
3895
4330
  import { createHash as createHash3 } from "crypto";
3896
4331
  import { mkdir as mkdir4, readFile as readFile7, readdir as readdir2, stat as stat2, writeFile as writeFile3 } from "fs/promises";
3897
- 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";
3898
4333
 
3899
4334
  // src/utils/memory-authority.ts
3900
4335
  var TIER_RANK = {
@@ -4013,8 +4448,8 @@ async function runMemoryIndex(input) {
4013
4448
  }
4014
4449
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
4015
4450
  const relCachePath = memoryCacheRelPath(input.project);
4016
- const absCachePath = join13(input.vault, relCachePath);
4017
- 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 });
4018
4453
  await writeFile3(absCachePath, `${JSON.stringify({
4019
4454
  generated_at: generatedAt,
4020
4455
  project: input.project,
@@ -4206,7 +4641,7 @@ async function buildMemoryIndexState(pages, project) {
4206
4641
  }
4207
4642
  async function checkMemoryIndex(vault, project, current) {
4208
4643
  const relCachePath = memoryCacheRelPath(project);
4209
- const cacheText = await readIfExists(join13(vault, relCachePath));
4644
+ const cacheText = await readIfExists(join22(vault, relCachePath));
4210
4645
  if (!cacheText) {
4211
4646
  return {
4212
4647
  ok: true,
@@ -4615,7 +5050,7 @@ async function walkImportFiles(dir, out) {
4615
5050
  const entries = await readdir2(dir, { withFileTypes: true });
4616
5051
  for (const entry of entries) {
4617
5052
  if (entry.name === ".git" || entry.name === "node_modules") continue;
4618
- const path = join13(dir, entry.name);
5053
+ const path = join22(dir, entry.name);
4619
5054
  if (entry.isDirectory()) {
4620
5055
  await walkImportFiles(path, out);
4621
5056
  } else if (entry.isFile() && isImportCandidate(path)) {
@@ -4682,8 +5117,8 @@ async function writeImportCapture(vault, entry, today) {
4682
5117
  const content = hiddenString(entry, "__content");
4683
5118
  const project = hiddenString(entry, "__project");
4684
5119
  const relPath = await availableImportPath(vault, entry.proposed_path);
4685
- const absPath = join13(vault, relPath);
4686
- await mkdir4(join13(vault, "raw", "transcripts"), { recursive: true });
5120
+ const absPath = join22(vault, relPath);
5121
+ await mkdir4(join22(vault, "raw", "transcripts"), { recursive: true });
4687
5122
  await writeFile3(absPath, renderImportCapture(entry, content, project, today), "utf8");
4688
5123
  const validation = await runValidate({ file: absPath });
4689
5124
  return {
@@ -4699,7 +5134,7 @@ async function availableImportPath(vault, proposed) {
4699
5134
  const stem = proposed.slice(0, -ext.length);
4700
5135
  let candidate = proposed;
4701
5136
  let i = 2;
4702
- while (await readIfExists(join13(vault, candidate))) {
5137
+ while (await readIfExists(join22(vault, candidate))) {
4703
5138
  candidate = `${stem}-${i}${ext}`;
4704
5139
  i++;
4705
5140
  }
@@ -4884,10 +5319,10 @@ function memoryCacheRelPath(project) {
4884
5319
  }
4885
5320
  async function readMemoryCache(vault, project) {
4886
5321
  if (project) {
4887
- const projectCache = await readIfExists(join13(vault, memoryCacheRelPath(project)));
5322
+ const projectCache = await readIfExists(join22(vault, memoryCacheRelPath(project)));
4888
5323
  if (projectCache) return projectCache;
4889
5324
  }
4890
- return readIfExists(join13(vault, ".skillwiki", "memory-topics.json"));
5325
+ return readIfExists(join22(vault, ".skillwiki", "memory-topics.json"));
4891
5326
  }
4892
5327
  function dedupePages(pages) {
4893
5328
  const seen = /* @__PURE__ */ new Set();
@@ -4978,8 +5413,310 @@ function slugify2(value) {
4978
5413
  }
4979
5414
 
4980
5415
  // src/commands/query.ts
4981
- import { readFile as readFile8, stat as stat3 } from "fs/promises";
4982
- 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
4983
5720
  var W_KEYWORD = 2;
4984
5721
  var W_SOURCE_OVERLAP = 4;
4985
5722
  var W_WIKILINK = 3;
@@ -5007,7 +5744,7 @@ async function runQuery(input) {
5007
5744
  const scan = await scanVault(input.vault);
5008
5745
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
5009
5746
  const limit = input.limit ?? 10;
5010
- const queryTerms = tokenize(input.text);
5747
+ const queryTerms = tokenize2(input.text);
5011
5748
  if (queryTerms.length === 0) {
5012
5749
  return {
5013
5750
  exitCode: ExitCode.OK,
@@ -5052,7 +5789,7 @@ async function runQuery(input) {
5052
5789
  }
5053
5790
  const suppressRepetitiveHistoricalCycles = historicalCyclePageCount >= 3 && hasDirectOperationalSeed;
5054
5791
  const structuralSeedPaths = suppressRepetitiveHistoricalCycles ? operationalSeedPaths : seedPaths;
5055
- const results = pages.map((page) => {
5792
+ const structural = pages.map((page) => {
5056
5793
  const sourceOverlap = scoreSourceOverlap(page, pages, structuralSeedPaths);
5057
5794
  const wikilink = scoreWikilink(page.relPath, structuralSeedPaths, graph);
5058
5795
  const aa = scoreAdamicAdar(page.relPath, structuralSeedPaths, graph);
@@ -5067,11 +5804,31 @@ async function runQuery(input) {
5067
5804
  title: page.title,
5068
5805
  type: page.type
5069
5806
  };
5070
- }).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
+ }
5071
5828
  let pendingSources;
5072
5829
  if (input.includePending) {
5073
- const { runSourcesPending } = await import("./sources-CMV5DKS5.js");
5074
- const pending = await runSourcesPending({
5830
+ const { runSourcesPending: runSourcesPending2 } = await import("./sources-BFYEFTS4.js");
5831
+ const pending = await runSourcesPending2({
5075
5832
  vault: input.vault,
5076
5833
  match: input.text,
5077
5834
  limit: input.limit ?? 10
@@ -5090,6 +5847,7 @@ ${pendingSources.length} matching pending source(s)` : "no matching pages found"
5090
5847
  results,
5091
5848
  ...pendingSources ? { pending_sources: pendingSources } : {},
5092
5849
  ...rankingGuardrails ? { ranking_guardrails: rankingGuardrails } : {},
5850
+ ...hybridMeta ? { hybrid: hybridMeta } : {},
5093
5851
  humanHint
5094
5852
  })
5095
5853
  };
@@ -5133,7 +5891,7 @@ function scoreTypeAffinity(pageType, queryTerms) {
5133
5891
  function isHistoricalCyclePage(relPath, title) {
5134
5892
  return HISTORICAL_CYCLE_RE.test(`${relPath} ${title}`);
5135
5893
  }
5136
- function tokenize(text) {
5894
+ function tokenize2(text) {
5137
5895
  return text.toLowerCase().split(/\s+/).filter((t) => t.length > 0);
5138
5896
  }
5139
5897
  function computeKeywordScore(terms, title, tags, body) {
@@ -5149,10 +5907,10 @@ function computeKeywordScore(terms, title, tags, body) {
5149
5907
  return score;
5150
5908
  }
5151
5909
  async function loadOrBuildGraph(vault) {
5152
- const graphPath = join14(vault, ".skillwiki", "graph.json");
5910
+ const graphPath = join24(vault, ".skillwiki", "graph.json");
5153
5911
  let needsBuild = false;
5154
5912
  try {
5155
- const fileStat = await stat3(graphPath);
5913
+ const fileStat = await stat4(graphPath);
5156
5914
  const ageHours = (Date.now() - fileStat.mtimeMs) / (1e3 * 60 * 60);
5157
5915
  if (ageHours > 24) needsBuild = true;
5158
5916
  } catch {
@@ -5163,13 +5921,73 @@ async function loadOrBuildGraph(vault) {
5163
5921
  if (buildResult.exitCode !== 0) return null;
5164
5922
  }
5165
5923
  try {
5166
- const raw = await readFile8(graphPath, "utf8");
5924
+ const raw = await readFile9(graphPath, "utf8");
5167
5925
  return JSON.parse(raw);
5168
5926
  } catch {
5169
5927
  return null;
5170
5928
  }
5171
5929
  }
5172
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
+
5173
5991
  // src/mcp/server.ts
5174
5992
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5175
5993
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -5178,7 +5996,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
5178
5996
  import { z } from "zod";
5179
5997
 
5180
5998
  // src/mcp/vault-resolve.ts
5181
- import { join as join15, resolve as resolve4 } from "path";
5999
+ import { join as join25, resolve as resolve4 } from "path";
5182
6000
 
5183
6001
  // src/mcp/allowlist.ts
5184
6002
  import { resolve as resolve3, sep as sep3 } from "path";
@@ -5240,7 +6058,7 @@ async function resolveMcpVault(input) {
5240
6058
  return ok({ vault: vaultPath, source });
5241
6059
  }
5242
6060
  function defaultGraphOut(vault) {
5243
- return join15(vault, ".skillwiki", "graph.json");
6061
+ return join25(vault, ".skillwiki", "graph.json");
5244
6062
  }
5245
6063
 
5246
6064
  // src/mcp/result-format.ts
@@ -5257,7 +6075,7 @@ function formatToolResult(payload) {
5257
6075
  // src/mcp/audit-log.ts
5258
6076
  import { appendFileSync, mkdirSync as mkdirSync2 } from "fs";
5259
6077
  import { homedir } from "os";
5260
- import { join as join16 } from "path";
6078
+ import { join as join26 } from "path";
5261
6079
  function auditEnabled() {
5262
6080
  const v = process.env.SKILLWIKI_MCP_AUDIT;
5263
6081
  if (v === "0" || v === "false") return false;
@@ -5269,7 +6087,7 @@ function auditSink() {
5269
6087
  function auditFilePath() {
5270
6088
  const custom = process.env.SKILLWIKI_MCP_AUDIT_FILE;
5271
6089
  if (custom && custom.length > 0) return custom;
5272
- return join16(homedir(), ".skillwiki", "mcp-audit.jsonl");
6090
+ return join26(homedir(), ".skillwiki", "mcp-audit.jsonl");
5273
6091
  }
5274
6092
  function auditMcpToolCall(entry) {
5275
6093
  if (!auditEnabled()) return;
@@ -5279,7 +6097,7 @@ function auditMcpToolCall(entry) {
5279
6097
  return;
5280
6098
  }
5281
6099
  const path = auditFilePath();
5282
- mkdirSync2(join16(path, ".."), { recursive: true });
6100
+ mkdirSync2(join26(path, ".."), { recursive: true });
5283
6101
  appendFileSync(path, line, "utf8");
5284
6102
  }
5285
6103
  async function runMcpToolHandler(tool, input, fn) {
@@ -5448,6 +6266,59 @@ function registerMcpTools(server) {
5448
6266
  return formatToolResult(r);
5449
6267
  })
5450
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
+ );
5451
6322
  }
5452
6323
 
5453
6324
  // src/mcp/mutating-tools.ts
@@ -5499,8 +6370,8 @@ function registerMcpMutatingTools(server) {
5499
6370
  }
5500
6371
 
5501
6372
  // src/mcp/resources.ts
5502
- import { readFile as readFile10 } from "fs/promises";
5503
- import { join as join18 } from "path";
6373
+ import { readFile as readFile11 } from "fs/promises";
6374
+ import { join as join28 } from "path";
5504
6375
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
5505
6376
 
5506
6377
  // src/mcp/lint-bucket.ts
@@ -5531,7 +6402,8 @@ var WARNING_KINDS = /* @__PURE__ */ new Set([
5531
6402
  "work_item_health",
5532
6403
  "orphaned_project_pages",
5533
6404
  "missing_overview",
5534
- "missing_diagram"
6405
+ "missing_diagram",
6406
+ "cycle_traps"
5535
6407
  ]);
5536
6408
  function bucketSeverity(kind) {
5537
6409
  if (ERROR_KINDS.has(kind)) return "error";
@@ -5628,9 +6500,9 @@ async function fetchQueryPreview(input) {
5628
6500
  }
5629
6501
 
5630
6502
  // src/mcp/graph-html.ts
5631
- import { readFile as readFile9 } from "fs/promises";
5632
- import { join as join17 } from "path";
5633
- 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";
5634
6506
  var TYPE_COLORS = {
5635
6507
  entities: "#e74c3c",
5636
6508
  concepts: "#27ae60",
@@ -5698,9 +6570,9 @@ ${nodeSvg}
5698
6570
  return { html, node_count: nodes.length, edge_count: edges.length, truncated };
5699
6571
  }
5700
6572
  async function fetchGraphHtmlReport(input) {
5701
- const graphPath = input.graphPath ?? join17(input.vault, ".skillwiki", "graph.json");
6573
+ const graphPath = input.graphPath ?? join27(input.vault, ".skillwiki", "graph.json");
5702
6574
  const maxNodes = Math.min(Math.max(10, input.maxNodes ?? 120), 500);
5703
- if (!existsSync10(graphPath)) {
6575
+ if (!existsSync21(graphPath)) {
5704
6576
  return {
5705
6577
  exitCode: ExitCode.FILE_NOT_FOUND,
5706
6578
  result: err("GRAPH_MISSING", { path: graphPath, hint: "Run skillwiki.graph_build first." })
@@ -5708,7 +6580,7 @@ async function fetchGraphHtmlReport(input) {
5708
6580
  }
5709
6581
  let raw;
5710
6582
  try {
5711
- raw = await readFile9(graphPath, "utf8");
6583
+ raw = await readFile10(graphPath, "utf8");
5712
6584
  } catch (e) {
5713
6585
  return {
5714
6586
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -5772,7 +6644,7 @@ async function fetchStaleSummary(input) {
5772
6644
 
5773
6645
  // src/mcp/resources.ts
5774
6646
  async function readVaultFile(vault, rel) {
5775
- return readFile10(join18(vault, rel), "utf8");
6647
+ return readFile11(join28(vault, rel), "utf8");
5776
6648
  }
5777
6649
  async function tailLines(text, lines) {
5778
6650
  const parts = text.split(/\r?\n/);
@@ -5858,9 +6730,9 @@ function registerMcpResources(server) {
5858
6730
  if (!v.ok) {
5859
6731
  return { contents: [{ uri: uri.href, mimeType: "text/plain", text: JSON.stringify(v) }] };
5860
6732
  }
5861
- const path = join18(v.data.vault, ".skillwiki", "graph.json");
6733
+ const path = join28(v.data.vault, ".skillwiki", "graph.json");
5862
6734
  try {
5863
- const raw = await readFile10(path, "utf8");
6735
+ const raw = await readFile11(path, "utf8");
5864
6736
  const graph = JSON.parse(raw);
5865
6737
  const adjacency = graph.adjacency ?? {};
5866
6738
  const nodes = Object.keys(adjacency);
@@ -6148,6 +7020,35 @@ function registerMcpPrompts(server) {
6148
7020
  ]
6149
7021
  })
6150
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
+ );
6151
7052
  }
6152
7053
 
6153
7054
  // src/mcp/server.ts
@@ -6185,13 +7086,13 @@ export {
6185
7086
  runConfigSet,
6186
7087
  runConfigList,
6187
7088
  runConfigPath,
7089
+ detectFuseMount,
7090
+ snapshotterHealthChecks,
6188
7091
  SATELLITE_STALE_MS,
6189
7092
  satelliteLatestRunPath,
6190
7093
  isFailedRunStatus,
6191
7094
  readSatelliteLatestRunFromText,
6192
7095
  evaluateSatelliteRunHealth,
6193
- detectFuseMount,
6194
- snapshotterHealthChecks,
6195
7096
  runDoctor,
6196
7097
  readCliPackageJson,
6197
7098
  DEFAULT_DIRTY_VOLUME_THRESHOLD,
@@ -6208,6 +7109,16 @@ export {
6208
7109
  runMemoryRecall,
6209
7110
  runMemoryReview,
6210
7111
  runMemoryImport,
7112
+ buildVectorIndex,
7113
+ vectorIndexStatus,
7114
+ reindexPageInVectorIndex,
7115
+ pruneVectorIndex,
6211
7116
  runQuery,
7117
+ runSourceCompileClaim,
7118
+ runSourceCompileRelease,
7119
+ runSourceCompilePublished,
7120
+ runSourceReview,
7121
+ runSourceCompileStatus,
7122
+ runSourceReviews,
6212
7123
  runSkillwikiMcpStdio
6213
7124
  };