skillwiki 0.10.12 → 0.10.14

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.
@@ -33,7 +33,7 @@ import {
33
33
  satelliteGateFromFleetLoad,
34
34
  snapshotterAliasForLocalHost,
35
35
  writeDotenv
36
- } from "./chunk-TYN2IHBY.js";
36
+ } from "./chunk-OWPQJOFG.js";
37
37
  import {
38
38
  CompoundSchema,
39
39
  ExitCode,
@@ -3211,6 +3211,7 @@ function buildCliSurface() {
3211
3211
  program.command("tag");
3212
3212
  program.command("tag-sync").option("--dry-run").option("--wiki <name>");
3213
3213
  program.command("sync");
3214
+ program.command("snapshot-maintenance");
3214
3215
  program.command("backup");
3215
3216
  program.command("seed").option("--wiki <name>");
3216
3217
  program.command("observe").requiredOption("--text <text>").option("--kind <kind>").option("--project <slug>").option("--severity <level>").option("--capture-budget <n>").option("--wiki <name>");
@@ -3252,6 +3253,9 @@ function buildCliSurface() {
3252
3253
  const backupCmd = program.commands.find((c) => c.name() === "backup");
3253
3254
  backupCmd.command("sync").option("--dry-run").option("--bucket <name>").option("--endpoint <url>").option("--region <region>").option("--prune").option("--wiki <name>");
3254
3255
  backupCmd.command("restore").option("--bucket <name>").option("--endpoint <url>").option("--region <region>").option("--target <dir>").option("--wiki <name>");
3256
+ const snapshotMaintenanceCmd = program.commands.find((c) => c.name() === "snapshot-maintenance");
3257
+ const snapMaintJournalCmd = snapshotMaintenanceCmd.command("journal");
3258
+ snapMaintJournalCmd.command("clear-stale").option("--dry-run").option("--approve <id>").option("--reason <text>").option("--wiki <name>");
3255
3259
  const memoryCmd = program.commands.find((c) => c.name() === "memory");
3256
3260
  memoryCmd.command("topics").option("--project <slug>").option("--limit <n>").option("--wiki <name>");
3257
3261
  memoryCmd.command("index").requiredOption("--project <slug>").option("--check").option("--if-stale").option("--wiki <name>");
@@ -6517,6 +6521,142 @@ function resolveSnapshotGitWorktree(config) {
6517
6521
  const defaultPath = "/root/wiki-git";
6518
6522
  return existsSync13(defaultPath) ? defaultPath : void 0;
6519
6523
  }
6524
+ function loadSnapshotFixture(env) {
6525
+ const path = env.VS_SNAPSHOT_HEALTH_FIXTURE;
6526
+ if (!path || !existsSync13(path)) return null;
6527
+ try {
6528
+ return JSON.parse(readFileSync10(path, "utf8"));
6529
+ } catch {
6530
+ return null;
6531
+ }
6532
+ }
6533
+ function systemctlShowProperty(scope, unit, prop) {
6534
+ try {
6535
+ const cmd = scope === "system" ? `systemctl show ${unit} --property=${prop} --value` : `systemctl --user show ${unit} --property=${prop} --value`;
6536
+ const out = execSync2(cmd, {
6537
+ encoding: "utf8",
6538
+ timeout: 2e3,
6539
+ stdio: ["pipe", "pipe", "pipe"]
6540
+ }).trim();
6541
+ return out || void 0;
6542
+ } catch {
6543
+ return void 0;
6544
+ }
6545
+ }
6546
+ function snapshotProp(kind, prop, fixture, scope) {
6547
+ if (fixture) {
6548
+ const v = fixture[kind][prop];
6549
+ return v == null ? void 0 : String(v);
6550
+ }
6551
+ const unit = kind === "timer" ? "wiki-snapshot.timer" : "wiki-snapshot.service";
6552
+ return systemctlShowProperty(scope, unit, prop);
6553
+ }
6554
+ function parseIsoToMs(ts) {
6555
+ if (!ts || ts === "MISSING") return null;
6556
+ const ms = Date.parse(ts);
6557
+ return Number.isFinite(ms) ? ms : null;
6558
+ }
6559
+ function ageMinutes(nowMs, tsMs) {
6560
+ if (tsMs == null) return null;
6561
+ return Math.floor((nowMs - tsMs) / 6e4);
6562
+ }
6563
+ function snapshotterHealthChecks(scope, logDir, env) {
6564
+ const fixture = loadSnapshotFixture(env);
6565
+ const cadence = fixture ? fixture.cadence_minutes : parseInt(env.VS_SNAPSHOT_CADENCE_MINUTES ?? "30", 10) || 30;
6566
+ const timeout = fixture ? fixture.service_timeout_seconds : parseInt(env.VS_SNAPSHOT_SERVICE_TIMEOUT_SECONDS ?? "900", 10) || 900;
6567
+ const nowMs = fixture ? Date.parse(fixture.now) : Date.now();
6568
+ const warnAge = cadence * 2 + 15;
6569
+ const errorAge = cadence * 4 + 15;
6570
+ const tUnitfile = snapshotProp("timer", "unit_file_state", fixture, scope);
6571
+ const tActive = snapshotProp("timer", "active_state", fixture, scope);
6572
+ const tNext = snapshotProp("timer", "next_elapse", fixture, scope);
6573
+ let jobs;
6574
+ if (tUnitfile == null && tActive == null) {
6575
+ jobs = check("warn", "vault_sync_jobs_enabled", "Vault sync jobs enabled", "wiki-snapshot.timer properties unavailable (read-only)");
6576
+ } else if (tUnitfile === "enabled" && tActive === "active" && tNext) {
6577
+ jobs = check("pass", "vault_sync_jobs_enabled", "Vault sync jobs enabled", `wiki-snapshot.timer enabled+active, next=${tNext} (${scope})`);
6578
+ } else {
6579
+ 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})`);
6580
+ }
6581
+ const sActive = snapshotProp("service", "active_state", fixture, scope) ?? null;
6582
+ const sResult = snapshotProp("service", "result", fixture, scope) ?? null;
6583
+ const sExecMainStatus = snapshotProp("service", "exec_main_status", fixture, scope);
6584
+ const sActiveEnter = snapshotProp("service", "active_enter_timestamp", fixture, scope) ?? null;
6585
+ let serviceResult;
6586
+ if (sActive == null && sResult == null) {
6587
+ serviceResult = check("warn", "vault_sync_snapshot_service_result", "Vault sync snapshot service result", "wiki-snapshot.service properties unavailable (read-only)");
6588
+ } else if (sActive === "active" || sActive === "activating") {
6589
+ const startMs = parseIsoToMs(sActiveEnter);
6590
+ const runSec = startMs == null ? null : Math.floor((nowMs - startMs) / 1e3);
6591
+ if (runSec != null && runSec > timeout) {
6592
+ serviceResult = check("error", "vault_sync_snapshot_service_result", "Vault sync snapshot service result", `wiki-snapshot.service running ${runSec}s beyond timeout ${timeout}s`);
6593
+ } else {
6594
+ serviceResult = check("pass", "vault_sync_snapshot_service_result", "Vault sync snapshot service result", `wiki-snapshot.service in progress (running ${runSec ?? "?"}s)`);
6595
+ }
6596
+ } else if (sResult === "success" && (sExecMainStatus ?? "0") === "0" && sActiveEnter != null) {
6597
+ serviceResult = check("pass", "vault_sync_snapshot_service_result", "Vault sync snapshot service result", "wiki-snapshot.service result=success ExecMainStatus=0");
6598
+ } else if (sResult === "failed" || sExecMainStatus != null && sExecMainStatus !== "0") {
6599
+ serviceResult = check("error", "vault_sync_snapshot_service_result", "Vault sync snapshot service result", `wiki-snapshot.service result=${sResult ?? "missing"} ExecMainStatus=${sExecMainStatus ?? "missing"}`);
6600
+ } else {
6601
+ serviceResult = check("warn", "vault_sync_snapshot_service_result", "Vault sync snapshot service result", `wiki-snapshot.service result=${sResult ?? "missing"} (never ran or unrecognized)`);
6602
+ }
6603
+ let completionTs = null;
6604
+ let completionOutcome = "unknown";
6605
+ const logRecords = fixture ? fixture.log_records : (() => {
6606
+ try {
6607
+ const content = readFileSync10(join25(logDir, "wiki-snapshot.log"), "utf8");
6608
+ return content.split(/\r?\n/).filter(Boolean);
6609
+ } catch {
6610
+ return [];
6611
+ }
6612
+ })();
6613
+ for (let i = logRecords.length - 1; i >= 0; i--) {
6614
+ const m = logRecords[i].match(/SNAPSHOT_COMPLETE schema=v1 .*ts=(\S+)/);
6615
+ if (m) {
6616
+ completionTs = m[1];
6617
+ const om = logRecords[i].match(/outcome=(\S+)/);
6618
+ if (om) completionOutcome = om[1];
6619
+ break;
6620
+ }
6621
+ }
6622
+ let freshness;
6623
+ if (!completionTs || completionTs === "MISSING") {
6624
+ if (sActive === "active" || sActive === "activating") {
6625
+ freshness = check("warn", "vault_sync_last_push_age", "Vault sync last snapshot recency", "snapshot in progress; no prior canonical completion record");
6626
+ } else {
6627
+ freshness = check("error", "vault_sync_last_push_age", "Vault sync last snapshot recency", "no canonical SNAPSHOT_COMPLETE record found");
6628
+ }
6629
+ } else {
6630
+ const ageMin = ageMinutes(nowMs, parseIsoToMs(completionTs));
6631
+ if (ageMin == null) {
6632
+ freshness = check("error", "vault_sync_last_push_age", "Vault sync last snapshot recency", `unparseable completion timestamp: ${completionTs}`);
6633
+ } else if (ageMin <= warnAge) {
6634
+ freshness = check("pass", "vault_sync_last_push_age", "Vault sync last snapshot recency", `last snapshot ${ageMin}m ago (outcome=${completionOutcome}, <=${warnAge}m)`);
6635
+ } else if (ageMin <= errorAge) {
6636
+ freshness = check("warn", "vault_sync_last_push_age", "Vault sync last snapshot recency", `last snapshot ${ageMin}m ago (outcome=${completionOutcome}, >${warnAge}m)`);
6637
+ } else {
6638
+ freshness = check("error", "vault_sync_last_push_age", "Vault sync last snapshot recency", `last snapshot ${ageMin}m ago (outcome=${completionOutcome}, >${errorAge}m)`);
6639
+ }
6640
+ }
6641
+ if (serviceResult.status === "error" && sActive !== "active" && sActive !== "activating") {
6642
+ freshness = check("error", "vault_sync_last_push_age", "Vault sync last snapshot recency", `latest service result failed: ${serviceResult.detail}`);
6643
+ }
6644
+ let failCount = 0;
6645
+ let mostRecentFail = "";
6646
+ for (let i = logRecords.length - 1; i >= 0 && i >= logRecords.length - 60; i--) {
6647
+ const line = logRecords[i];
6648
+ if (/SNAPSHOT_COMPLETE schema=v1/.test(line)) break;
6649
+ if (/ERROR/.test(line)) {
6650
+ failCount++;
6651
+ if (!mostRecentFail) {
6652
+ const m = line.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/);
6653
+ mostRecentFail = m ? m[1] : "unknown";
6654
+ }
6655
+ }
6656
+ }
6657
+ const consecutiveFailures = failCount >= 2 ? check("error", "vault_sync_snapshot_consecutive_failures", "Vault sync snapshot consecutive failures", `${failCount} consecutive snapshot failure(s); most recent: ${mostRecentFail || "unknown"}`) : check("pass", "vault_sync_snapshot_consecutive_failures", "Vault sync snapshot consecutive failures", `${failCount} consecutive failure(s) in recent window (recurrence threshold: 2)`);
6658
+ return [jobs, serviceResult, freshness, consecutiveFailures];
6659
+ }
6520
6660
  function vaultSyncChecks(input) {
6521
6661
  const os = input.os ?? platform2();
6522
6662
  const home = input.home;
@@ -6525,7 +6665,9 @@ function vaultSyncChecks(input) {
6525
6665
  return [
6526
6666
  skip("vault_sync_installed", "Vault sync installed"),
6527
6667
  skip("vault_sync_jobs_enabled", "Vault sync jobs enabled"),
6668
+ skip("vault_sync_snapshot_service_result", "Vault sync snapshot service result"),
6528
6669
  skip("vault_sync_last_push_age", "Vault sync last push recency"),
6670
+ skip("vault_sync_snapshot_consecutive_failures", "Vault sync snapshot consecutive failures"),
6529
6671
  skip("vault_sync_last_fetch_status", "Vault sync last fetch status"),
6530
6672
  skip("vault_sync_filter_present", "Vault sync filter file present"),
6531
6673
  skip("vault_sync_snapshot_guard", "Snapshot script guard")
@@ -6538,79 +6680,10 @@ function vaultSyncChecks(input) {
6538
6680
  const packagedSnapshotPath = join25(shareDir, "wiki-snapshot.sh");
6539
6681
  const legacySnapshotPath = "/root/.hermes/scripts/wiki-snapshot-v3.sh";
6540
6682
  const snapshotPath = input.snapshotScriptPath ?? (existsSync13(packagedSnapshotPath) ? packagedSnapshotPath : legacySnapshotPath);
6541
- function snapshotLastStatusCheck() {
6542
- const snapshotLog = join25(logDir, "wiki-snapshot.log");
6543
- try {
6544
- const logContent = readFileSync10(snapshotLog, "utf8");
6545
- const lines = logContent.trim().split("\n").filter(Boolean);
6546
- if (lines.length === 0) {
6547
- return check(
6548
- "warn",
6549
- "vault_sync_last_push_age",
6550
- "Vault sync last snapshot status",
6551
- "Snapshot log file is empty"
6552
- );
6553
- }
6554
- const lastLine = [...lines].reverse().find(
6555
- (line) => /ERROR|Status: complete|Push successful|No changes to commit/.test(line)
6556
- ) ?? lines[lines.length - 1];
6557
- if (/ERROR/.test(lastLine)) {
6558
- return check(
6559
- "error",
6560
- "vault_sync_last_push_age",
6561
- "Vault sync last snapshot status",
6562
- `Last snapshot failed: ${lastLine.slice(0, 160)}`
6563
- );
6564
- }
6565
- if (/Status: complete|Push successful|No changes to commit/.test(lastLine)) {
6566
- return check(
6567
- "pass",
6568
- "vault_sync_last_push_age",
6569
- "Vault sync last snapshot status",
6570
- lastLine.slice(0, 160)
6571
- );
6572
- }
6573
- return check(
6574
- "warn",
6575
- "vault_sync_last_push_age",
6576
- "Vault sync last snapshot status",
6577
- `Last snapshot log entry: ${lastLine.slice(0, 160)}`
6578
- );
6579
- } catch {
6580
- return check(
6581
- "warn",
6582
- "vault_sync_last_push_age",
6583
- "Vault sync last snapshot status",
6584
- `Snapshot log not found at ${snapshotLog}`
6585
- );
6586
- }
6587
- }
6588
6683
  if (input.vaultSyncRole === "snapshotter") {
6589
6684
  const c12 = existsSync13(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}`);
6590
6685
  const serviceScope = input.vaultSyncServiceScope ?? "user";
6591
- const userTimerPath = join25(home, ".config", "systemd", "user", "wiki-snapshot.timer");
6592
- const systemTimerPath = "/etc/systemd/system/wiki-snapshot.timer";
6593
- let c22;
6594
- if (serviceScope === "user" && existsSync13(userTimerPath)) {
6595
- c22 = check("pass", "vault_sync_jobs_enabled", "Vault sync jobs enabled", `Found: ${userTimerPath}`);
6596
- } else if (serviceScope === "system" && existsSync13(systemTimerPath)) {
6597
- c22 = check("pass", "vault_sync_jobs_enabled", "Vault sync jobs enabled", `Found: ${systemTimerPath}`);
6598
- } else if (os !== "linux") {
6599
- c22 = check("warn", "vault_sync_jobs_enabled", "Vault sync jobs enabled", "Snapshotter scheduler is Linux-only and no wiki-snapshot.timer file was found");
6600
- } else {
6601
- try {
6602
- const command = serviceScope === "system" ? "systemctl is-enabled wiki-snapshot.timer" : "systemctl --user is-enabled wiki-snapshot.timer";
6603
- const out = execSync2(command, {
6604
- encoding: "utf8",
6605
- timeout: 2e3,
6606
- stdio: ["pipe", "pipe", "pipe"]
6607
- }).trim();
6608
- c22 = out === "enabled" ? check("pass", "vault_sync_jobs_enabled", "Vault sync jobs enabled", `systemd: wiki-snapshot.timer enabled (${serviceScope})`) : check("error", "vault_sync_jobs_enabled", "Vault sync jobs enabled", `systemd: wiki-snapshot.timer is ${out || "not enabled"} (${serviceScope})`);
6609
- } catch {
6610
- c22 = check("error", "vault_sync_jobs_enabled", "Vault sync jobs enabled", `wiki-snapshot.timer check failed (${serviceScope})`);
6611
- }
6612
- }
6613
- const c32 = snapshotLastStatusCheck();
6686
+ const healthChecks = snapshotterHealthChecks(serviceScope, logDir, input.env ?? process.env);
6614
6687
  const cFetch2 = check(
6615
6688
  "pass",
6616
6689
  "vault_sync_last_fetch_status",
@@ -6658,7 +6731,7 @@ function vaultSyncChecks(input) {
6658
6731
  `Cannot read ${snapshotPath}`
6659
6732
  );
6660
6733
  }
6661
- return [c12, c22, c32, cFetch2, c42, c52];
6734
+ return [c12, ...healthChecks, cFetch2, c42, c52];
6662
6735
  }
6663
6736
  const pushScriptPath = join25(shareDir, "wiki-push.sh");
6664
6737
  const c1 = existsSync13(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`);
@@ -7075,7 +7148,8 @@ async function runDoctor(input) {
7075
7148
  vaultSyncInstalled: vsConfig.installed,
7076
7149
  vaultSyncRole: vsConfig.role,
7077
7150
  vaultSyncServiceScope: vsConfig.serviceScope,
7078
- snapshotScriptPath: vsConfig.snapshotScript
7151
+ snapshotScriptPath: vsConfig.snapshotScript,
7152
+ env: input.env ?? process.env
7079
7153
  }));
7080
7154
  checks.push(checkVaultSyncPullHelper(input.home, input.env ?? process.env));
7081
7155
  checks.push(checkVaultSyncReviewRequiredJournals(resolvedPath));
@@ -9915,6 +9989,7 @@ export {
9915
9989
  isFailedRunStatus,
9916
9990
  readSatelliteLatestRunFromText,
9917
9991
  evaluateSatelliteRunHealth,
9992
+ snapshotterHealthChecks,
9918
9993
  runDoctor,
9919
9994
  readCliPackageJson,
9920
9995
  DEFAULT_DIRTY_VOLUME_THRESHOLD,
@@ -702,16 +702,23 @@ function isWorktreeClean(vault) {
702
702
  const porcelain = git(vault, ["status", "--porcelain"]);
703
703
  return !porcelain || porcelain.trim() === "";
704
704
  }
705
- function canSupersedeJournal(vault, fields) {
706
- if (hasUnmergedPaths(vault).length > 0) return false;
707
- if (hasActiveGitSequencer(vault)) return false;
708
- if (!isWorktreeClean(vault)) return false;
709
- const target = fields.target_oid?.trim();
710
- if (!target) return false;
705
+ function journalSupersedeHead(vault, requireClean) {
706
+ if (hasUnmergedPaths(vault).length > 0 || hasActiveGitSequencer(vault) || requireClean && !isWorktreeClean(vault)) {
707
+ return null;
708
+ }
711
709
  const head = git(vault, ["rev-parse", "HEAD"]);
710
+ return head || null;
711
+ }
712
+ function canSupersedeJournalInContext(vault, fields, head) {
712
713
  if (!head) return false;
714
+ const target = fields.target_oid?.trim();
715
+ if (!target) return false;
713
716
  return gitMergeBaseIsAncestor(vault, target, head);
714
717
  }
718
+ function canSupersedeJournal(vault, fields, opts = {}) {
719
+ const head = journalSupersedeHead(vault, opts.requireClean !== false);
720
+ return canSupersedeJournalInContext(vault, fields, head);
721
+ }
715
722
  function gitMergeBaseIsAncestor(vault, ancestor, tip) {
716
723
  if (ancestor === tip) return true;
717
724
  const mb = git(vault, ["merge-base", ancestor, tip]);
@@ -733,14 +740,12 @@ function markJournalSuperseded(vault, opId, fields, by) {
733
740
  }
734
741
  function supersedeStaleReviewRequiredJournals(vault, opts = {}) {
735
742
  const by = opts.by ?? "skillwiki-preflight";
743
+ const requireClean = opts.requireClean !== false;
736
744
  const superseded = [];
737
745
  const skipped = [];
738
- if (hasUnmergedPaths(vault).length > 0 || hasActiveGitSequencer(vault) || !isWorktreeClean(vault)) {
739
- for (const { opId } of listReviewRequiredOps(vault)) skipped.push(opId);
740
- return { superseded, skipped };
741
- }
746
+ const head = journalSupersedeHead(vault, requireClean);
742
747
  for (const { opId, fields } of listReviewRequiredOps(vault)) {
743
- if (!canSupersedeJournal(vault, fields)) {
748
+ if (!canSupersedeJournalInContext(vault, fields, head)) {
744
749
  skipped.push(opId);
745
750
  continue;
746
751
  }
@@ -886,6 +891,9 @@ export {
886
891
  findReviewRequiredOp,
887
892
  hasUnmergedPaths,
888
893
  hasActiveGitSequencer,
894
+ isWorktreeClean,
895
+ canSupersedeJournal,
896
+ markJournalSuperseded,
889
897
  supersedeStaleReviewRequiredJournals,
890
898
  resolveVaultSyncPullHelper,
891
899
  runVaultSyncPullHelper
@@ -8,7 +8,7 @@ import {
8
8
  loadFleetManifestAndHost,
9
9
  runVaultSyncPullHelper,
10
10
  supersedeStaleReviewRequiredJournals
11
- } from "./chunk-TYN2IHBY.js";
11
+ } from "./chunk-OWPQJOFG.js";
12
12
  import {
13
13
  ExitCode,
14
14
  err,
@@ -163,7 +163,10 @@ function preflightBlocker(vault) {
163
163
  if (hasActiveGitSequencer(vault)) {
164
164
  return { reason: "git-operation-in-progress" };
165
165
  }
166
- supersedeStaleReviewRequiredJournals(vault, { by: "skillwiki-managed-write-preflight" });
166
+ supersedeStaleReviewRequiredJournals(vault, {
167
+ by: "skillwiki-managed-write-preflight",
168
+ requireClean: false
169
+ });
167
170
  const op = findReviewRequiredOp(vault);
168
171
  if (op) return { reason: "review-required", operation_id: op };
169
172
  return null;