skillwiki 0.10.13 → 0.10.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-X2CPXF4T.js → chunk-24MVNAFJ.js} +182 -73
- package/dist/{chunk-E3PMAHS3.js → chunk-OWPQJOFG.js} +7 -0
- package/dist/{chunk-IIUMTKKA.js → chunk-UJLSUJB6.js} +1 -1
- package/dist/cli.js +455 -44
- package/dist/{managed-write-preflight-4SWWFT75.js → managed-write-preflight-M2HL6DDY.js} +2 -2
- package/dist/skillwiki-mcp.js +2 -2
- package/dist/vault-sync/scripts/wiki-snapshot.sh +14 -0
- package/package.json +1 -1
- package/skills/.claude-plugin/plugin.json +1 -1
- package/skills/.codex-plugin/plugin.json +1 -1
- package/skills/package.json +1 -1
- package/skills/skills/using-skillwiki/SKILL.md +2 -1
- package/skills/using-skillwiki/SKILL.md +2 -1
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
satelliteGateFromFleetLoad,
|
|
34
34
|
snapshotterAliasForLocalHost,
|
|
35
35
|
writeDotenv
|
|
36
|
-
} from "./chunk-
|
|
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,176 @@ function resolveSnapshotGitWorktree(config) {
|
|
|
6517
6521
|
const defaultPath = "/root/wiki-git";
|
|
6518
6522
|
return existsSync13(defaultPath) ? defaultPath : void 0;
|
|
6519
6523
|
}
|
|
6524
|
+
var SNAPSHOT_SEMANTIC_TO_SYSTEMD = {
|
|
6525
|
+
load_state: "LoadState",
|
|
6526
|
+
unit_file_state: "UnitFileState",
|
|
6527
|
+
active_state: "ActiveState",
|
|
6528
|
+
sub_state: "SubState",
|
|
6529
|
+
next_elapse: "NextElapseUSecRealtime",
|
|
6530
|
+
result: "Result",
|
|
6531
|
+
exec_main_status: "ExecMainStatus",
|
|
6532
|
+
exec_main_code: "ExecMainCode",
|
|
6533
|
+
active_enter_timestamp: "ActiveEnterTimestamp",
|
|
6534
|
+
inactive_enter_timestamp: "InactiveEnterTimestamp",
|
|
6535
|
+
exec_main_start_timestamp: "ExecMainStartTimestamp",
|
|
6536
|
+
exec_main_exit_timestamp: "ExecMainExitTimestamp",
|
|
6537
|
+
invocation_id: "InvocationID"
|
|
6538
|
+
};
|
|
6539
|
+
function semanticToSystemdProp(semantic) {
|
|
6540
|
+
return SNAPSHOT_SEMANTIC_TO_SYSTEMD[semantic];
|
|
6541
|
+
}
|
|
6542
|
+
function normalizeSystemdValue(raw) {
|
|
6543
|
+
if (raw == null) return void 0;
|
|
6544
|
+
const v = raw.trim();
|
|
6545
|
+
if (!v || v === "n/a" || v === "N/A") return void 0;
|
|
6546
|
+
return v;
|
|
6547
|
+
}
|
|
6548
|
+
function hasCompletedRunEvidence(...timestamps) {
|
|
6549
|
+
return timestamps.some((t) => normalizeSystemdValue(t ?? void 0) != null);
|
|
6550
|
+
}
|
|
6551
|
+
function loadSnapshotFixture(env) {
|
|
6552
|
+
const path = env.VS_SNAPSHOT_HEALTH_FIXTURE;
|
|
6553
|
+
if (!path || !existsSync13(path)) return null;
|
|
6554
|
+
try {
|
|
6555
|
+
return JSON.parse(readFileSync10(path, "utf8"));
|
|
6556
|
+
} catch {
|
|
6557
|
+
return null;
|
|
6558
|
+
}
|
|
6559
|
+
}
|
|
6560
|
+
function systemctlShowProperty(scope, unit, prop) {
|
|
6561
|
+
try {
|
|
6562
|
+
const cmd = scope === "system" ? `systemctl show ${unit} --property=${prop} --value` : `systemctl --user show ${unit} --property=${prop} --value`;
|
|
6563
|
+
const out = execSync2(cmd, {
|
|
6564
|
+
encoding: "utf8",
|
|
6565
|
+
timeout: 2e3,
|
|
6566
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
6567
|
+
});
|
|
6568
|
+
return normalizeSystemdValue(out);
|
|
6569
|
+
} catch {
|
|
6570
|
+
return void 0;
|
|
6571
|
+
}
|
|
6572
|
+
}
|
|
6573
|
+
function snapshotProp(kind, prop, fixture, scope) {
|
|
6574
|
+
if (fixture) {
|
|
6575
|
+
const bag = fixture[kind];
|
|
6576
|
+
const v = bag[prop];
|
|
6577
|
+
return v == null ? void 0 : String(v);
|
|
6578
|
+
}
|
|
6579
|
+
const unit = kind === "timer" ? "wiki-snapshot.timer" : "wiki-snapshot.service";
|
|
6580
|
+
const liveProp = semanticToSystemdProp(prop);
|
|
6581
|
+
if (!liveProp) return void 0;
|
|
6582
|
+
return systemctlShowProperty(scope, unit, liveProp);
|
|
6583
|
+
}
|
|
6584
|
+
function parseIsoToMs(ts) {
|
|
6585
|
+
if (!ts || ts === "MISSING") return null;
|
|
6586
|
+
const ms = Date.parse(ts);
|
|
6587
|
+
return Number.isFinite(ms) ? ms : null;
|
|
6588
|
+
}
|
|
6589
|
+
function ageMinutes(nowMs, tsMs) {
|
|
6590
|
+
if (tsMs == null) return null;
|
|
6591
|
+
return Math.floor((nowMs - tsMs) / 6e4);
|
|
6592
|
+
}
|
|
6593
|
+
function snapshotterHealthChecks(scope, logDir, env) {
|
|
6594
|
+
const fixture = loadSnapshotFixture(env);
|
|
6595
|
+
const cadence = fixture ? fixture.cadence_minutes : parseInt(env.VS_SNAPSHOT_CADENCE_MINUTES ?? "30", 10) || 30;
|
|
6596
|
+
const timeout = fixture ? fixture.service_timeout_seconds : parseInt(env.VS_SNAPSHOT_SERVICE_TIMEOUT_SECONDS ?? "900", 10) || 900;
|
|
6597
|
+
const nowMs = fixture ? Date.parse(fixture.now) : Date.now();
|
|
6598
|
+
const warnAge = cadence * 2 + 15;
|
|
6599
|
+
const errorAge = cadence * 4 + 15;
|
|
6600
|
+
const tUnitfile = snapshotProp("timer", "unit_file_state", fixture, scope);
|
|
6601
|
+
const tActive = snapshotProp("timer", "active_state", fixture, scope);
|
|
6602
|
+
const tNext = snapshotProp("timer", "next_elapse", fixture, scope);
|
|
6603
|
+
let jobs;
|
|
6604
|
+
if (tUnitfile == null && tActive == null) {
|
|
6605
|
+
jobs = check("warn", "vault_sync_jobs_enabled", "Vault sync jobs enabled", "wiki-snapshot.timer properties unavailable (read-only)");
|
|
6606
|
+
} else if (tUnitfile === "enabled" && tActive === "active" && tNext) {
|
|
6607
|
+
jobs = check("pass", "vault_sync_jobs_enabled", "Vault sync jobs enabled", `wiki-snapshot.timer enabled+active, next=${tNext} (${scope})`);
|
|
6608
|
+
} else {
|
|
6609
|
+
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})`);
|
|
6610
|
+
}
|
|
6611
|
+
const sActive = snapshotProp("service", "active_state", fixture, scope) ?? null;
|
|
6612
|
+
const sResult = snapshotProp("service", "result", fixture, scope) ?? null;
|
|
6613
|
+
const sExecMainStatus = snapshotProp("service", "exec_main_status", fixture, scope);
|
|
6614
|
+
const sActiveEnter = snapshotProp("service", "active_enter_timestamp", fixture, scope) ?? null;
|
|
6615
|
+
const sInactiveEnter = snapshotProp("service", "inactive_enter_timestamp", fixture, scope) ?? null;
|
|
6616
|
+
const sExecMainStart = snapshotProp("service", "exec_main_start_timestamp", fixture, scope) ?? null;
|
|
6617
|
+
const sExecMainExit = snapshotProp("service", "exec_main_exit_timestamp", fixture, scope) ?? null;
|
|
6618
|
+
const completedEvidence = hasCompletedRunEvidence(sExecMainExit, sInactiveEnter, sActiveEnter);
|
|
6619
|
+
let serviceResult;
|
|
6620
|
+
if (sActive == null && sResult == null) {
|
|
6621
|
+
serviceResult = check("warn", "vault_sync_snapshot_service_result", "Vault sync snapshot service result", "wiki-snapshot.service properties unavailable (read-only)");
|
|
6622
|
+
} else if (sActive === "active" || sActive === "activating") {
|
|
6623
|
+
const startMs = parseIsoToMs(sExecMainStart) ?? parseIsoToMs(sActiveEnter);
|
|
6624
|
+
const runSec = startMs == null ? null : Math.floor((nowMs - startMs) / 1e3);
|
|
6625
|
+
if (runSec != null && runSec > timeout) {
|
|
6626
|
+
serviceResult = check("error", "vault_sync_snapshot_service_result", "Vault sync snapshot service result", `wiki-snapshot.service running ${runSec}s beyond timeout ${timeout}s`);
|
|
6627
|
+
} else {
|
|
6628
|
+
serviceResult = check("pass", "vault_sync_snapshot_service_result", "Vault sync snapshot service result", `wiki-snapshot.service in progress (running ${runSec ?? "?"}s)`);
|
|
6629
|
+
}
|
|
6630
|
+
} else if (sResult === "success" && (sExecMainStatus ?? "0") === "0" && completedEvidence) {
|
|
6631
|
+
serviceResult = check("pass", "vault_sync_snapshot_service_result", "Vault sync snapshot service result", "wiki-snapshot.service result=success ExecMainStatus=0");
|
|
6632
|
+
} else if (sResult === "failed" || sExecMainStatus != null && sExecMainStatus !== "0") {
|
|
6633
|
+
serviceResult = check("error", "vault_sync_snapshot_service_result", "Vault sync snapshot service result", `wiki-snapshot.service result=${sResult ?? "missing"} ExecMainStatus=${sExecMainStatus ?? "missing"}`);
|
|
6634
|
+
} else {
|
|
6635
|
+
serviceResult = check("warn", "vault_sync_snapshot_service_result", "Vault sync snapshot service result", `wiki-snapshot.service result=${sResult ?? "missing"} (never ran or unrecognized)`);
|
|
6636
|
+
}
|
|
6637
|
+
let completionTs = null;
|
|
6638
|
+
let completionOutcome = "unknown";
|
|
6639
|
+
const logRecords = fixture ? fixture.log_records : (() => {
|
|
6640
|
+
try {
|
|
6641
|
+
const content = readFileSync10(join25(logDir, "wiki-snapshot.log"), "utf8");
|
|
6642
|
+
return content.split(/\r?\n/).filter(Boolean);
|
|
6643
|
+
} catch {
|
|
6644
|
+
return [];
|
|
6645
|
+
}
|
|
6646
|
+
})();
|
|
6647
|
+
for (let i = logRecords.length - 1; i >= 0; i--) {
|
|
6648
|
+
const m = logRecords[i].match(/SNAPSHOT_COMPLETE schema=v1 .*ts=(\S+)/);
|
|
6649
|
+
if (m) {
|
|
6650
|
+
completionTs = m[1];
|
|
6651
|
+
const om = logRecords[i].match(/outcome=(\S+)/);
|
|
6652
|
+
if (om) completionOutcome = om[1];
|
|
6653
|
+
break;
|
|
6654
|
+
}
|
|
6655
|
+
}
|
|
6656
|
+
let freshness;
|
|
6657
|
+
if (!completionTs || completionTs === "MISSING") {
|
|
6658
|
+
if (sActive === "active" || sActive === "activating") {
|
|
6659
|
+
freshness = check("warn", "vault_sync_last_push_age", "Vault sync last snapshot recency", "snapshot in progress; no prior canonical completion record");
|
|
6660
|
+
} else {
|
|
6661
|
+
freshness = check("error", "vault_sync_last_push_age", "Vault sync last snapshot recency", "no canonical SNAPSHOT_COMPLETE record found");
|
|
6662
|
+
}
|
|
6663
|
+
} else {
|
|
6664
|
+
const ageMin = ageMinutes(nowMs, parseIsoToMs(completionTs));
|
|
6665
|
+
if (ageMin == null) {
|
|
6666
|
+
freshness = check("error", "vault_sync_last_push_age", "Vault sync last snapshot recency", `unparseable completion timestamp: ${completionTs}`);
|
|
6667
|
+
} else if (ageMin <= warnAge) {
|
|
6668
|
+
freshness = check("pass", "vault_sync_last_push_age", "Vault sync last snapshot recency", `last snapshot ${ageMin}m ago (outcome=${completionOutcome}, <=${warnAge}m)`);
|
|
6669
|
+
} else if (ageMin <= errorAge) {
|
|
6670
|
+
freshness = check("warn", "vault_sync_last_push_age", "Vault sync last snapshot recency", `last snapshot ${ageMin}m ago (outcome=${completionOutcome}, >${warnAge}m)`);
|
|
6671
|
+
} else {
|
|
6672
|
+
freshness = check("error", "vault_sync_last_push_age", "Vault sync last snapshot recency", `last snapshot ${ageMin}m ago (outcome=${completionOutcome}, >${errorAge}m)`);
|
|
6673
|
+
}
|
|
6674
|
+
}
|
|
6675
|
+
if (serviceResult.status === "error" && sActive !== "active" && sActive !== "activating") {
|
|
6676
|
+
freshness = check("error", "vault_sync_last_push_age", "Vault sync last snapshot recency", `latest service result failed: ${serviceResult.detail}`);
|
|
6677
|
+
}
|
|
6678
|
+
let failCount = 0;
|
|
6679
|
+
let mostRecentFail = "";
|
|
6680
|
+
for (let i = logRecords.length - 1; i >= 0 && i >= logRecords.length - 60; i--) {
|
|
6681
|
+
const line = logRecords[i];
|
|
6682
|
+
if (/SNAPSHOT_COMPLETE schema=v1/.test(line)) break;
|
|
6683
|
+
if (/ERROR/.test(line)) {
|
|
6684
|
+
failCount++;
|
|
6685
|
+
if (!mostRecentFail) {
|
|
6686
|
+
const m = line.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/);
|
|
6687
|
+
mostRecentFail = m ? m[1] : "unknown";
|
|
6688
|
+
}
|
|
6689
|
+
}
|
|
6690
|
+
}
|
|
6691
|
+
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)`);
|
|
6692
|
+
return [jobs, serviceResult, freshness, consecutiveFailures];
|
|
6693
|
+
}
|
|
6520
6694
|
function vaultSyncChecks(input) {
|
|
6521
6695
|
const os = input.os ?? platform2();
|
|
6522
6696
|
const home = input.home;
|
|
@@ -6525,7 +6699,9 @@ function vaultSyncChecks(input) {
|
|
|
6525
6699
|
return [
|
|
6526
6700
|
skip("vault_sync_installed", "Vault sync installed"),
|
|
6527
6701
|
skip("vault_sync_jobs_enabled", "Vault sync jobs enabled"),
|
|
6702
|
+
skip("vault_sync_snapshot_service_result", "Vault sync snapshot service result"),
|
|
6528
6703
|
skip("vault_sync_last_push_age", "Vault sync last push recency"),
|
|
6704
|
+
skip("vault_sync_snapshot_consecutive_failures", "Vault sync snapshot consecutive failures"),
|
|
6529
6705
|
skip("vault_sync_last_fetch_status", "Vault sync last fetch status"),
|
|
6530
6706
|
skip("vault_sync_filter_present", "Vault sync filter file present"),
|
|
6531
6707
|
skip("vault_sync_snapshot_guard", "Snapshot script guard")
|
|
@@ -6538,79 +6714,10 @@ function vaultSyncChecks(input) {
|
|
|
6538
6714
|
const packagedSnapshotPath = join25(shareDir, "wiki-snapshot.sh");
|
|
6539
6715
|
const legacySnapshotPath = "/root/.hermes/scripts/wiki-snapshot-v3.sh";
|
|
6540
6716
|
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
6717
|
if (input.vaultSyncRole === "snapshotter") {
|
|
6589
6718
|
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
6719
|
const serviceScope = input.vaultSyncServiceScope ?? "user";
|
|
6591
|
-
const
|
|
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();
|
|
6720
|
+
const healthChecks = snapshotterHealthChecks(serviceScope, logDir, input.env ?? process.env);
|
|
6614
6721
|
const cFetch2 = check(
|
|
6615
6722
|
"pass",
|
|
6616
6723
|
"vault_sync_last_fetch_status",
|
|
@@ -6658,7 +6765,7 @@ function vaultSyncChecks(input) {
|
|
|
6658
6765
|
`Cannot read ${snapshotPath}`
|
|
6659
6766
|
);
|
|
6660
6767
|
}
|
|
6661
|
-
return [c12,
|
|
6768
|
+
return [c12, ...healthChecks, cFetch2, c42, c52];
|
|
6662
6769
|
}
|
|
6663
6770
|
const pushScriptPath = join25(shareDir, "wiki-push.sh");
|
|
6664
6771
|
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 +7182,8 @@ async function runDoctor(input) {
|
|
|
7075
7182
|
vaultSyncInstalled: vsConfig.installed,
|
|
7076
7183
|
vaultSyncRole: vsConfig.role,
|
|
7077
7184
|
vaultSyncServiceScope: vsConfig.serviceScope,
|
|
7078
|
-
snapshotScriptPath: vsConfig.snapshotScript
|
|
7185
|
+
snapshotScriptPath: vsConfig.snapshotScript,
|
|
7186
|
+
env: input.env ?? process.env
|
|
7079
7187
|
}));
|
|
7080
7188
|
checks.push(checkVaultSyncPullHelper(input.home, input.env ?? process.env));
|
|
7081
7189
|
checks.push(checkVaultSyncReviewRequiredJournals(resolvedPath));
|
|
@@ -9915,6 +10023,7 @@ export {
|
|
|
9915
10023
|
isFailedRunStatus,
|
|
9916
10024
|
readSatelliteLatestRunFromText,
|
|
9917
10025
|
evaluateSatelliteRunHealth,
|
|
10026
|
+
snapshotterHealthChecks,
|
|
9918
10027
|
runDoctor,
|
|
9919
10028
|
readCliPackageJson,
|
|
9920
10029
|
DEFAULT_DIRTY_VOLUME_THRESHOLD,
|
|
@@ -715,6 +715,10 @@ function canSupersedeJournalInContext(vault, fields, head) {
|
|
|
715
715
|
if (!target) return false;
|
|
716
716
|
return gitMergeBaseIsAncestor(vault, target, head);
|
|
717
717
|
}
|
|
718
|
+
function canSupersedeJournal(vault, fields, opts = {}) {
|
|
719
|
+
const head = journalSupersedeHead(vault, opts.requireClean !== false);
|
|
720
|
+
return canSupersedeJournalInContext(vault, fields, head);
|
|
721
|
+
}
|
|
718
722
|
function gitMergeBaseIsAncestor(vault, ancestor, tip) {
|
|
719
723
|
if (ancestor === tip) return true;
|
|
720
724
|
const mb = git(vault, ["merge-base", ancestor, tip]);
|
|
@@ -887,6 +891,9 @@ export {
|
|
|
887
891
|
findReviewRequiredOp,
|
|
888
892
|
hasUnmergedPaths,
|
|
889
893
|
hasActiveGitSequencer,
|
|
894
|
+
isWorktreeClean,
|
|
895
|
+
canSupersedeJournal,
|
|
896
|
+
markJournalSuperseded,
|
|
890
897
|
supersedeStaleReviewRequiredJournals,
|
|
891
898
|
resolveVaultSyncPullHelper,
|
|
892
899
|
runVaultSyncPullHelper
|
package/dist/cli.js
CHANGED
|
@@ -78,11 +78,12 @@ import {
|
|
|
78
78
|
safeWritePage,
|
|
79
79
|
satelliteLatestRunPath,
|
|
80
80
|
scanConflictMarkerBlocksInText,
|
|
81
|
+
snapshotterHealthChecks,
|
|
81
82
|
taxonomyCommentForPage,
|
|
82
83
|
upsertIndexEntry,
|
|
83
84
|
validateLogEvent,
|
|
84
85
|
writeLogEvent
|
|
85
|
-
} from "./chunk-
|
|
86
|
+
} from "./chunk-24MVNAFJ.js";
|
|
86
87
|
import {
|
|
87
88
|
normalizeDistTag,
|
|
88
89
|
readCache,
|
|
@@ -110,15 +111,20 @@ import {
|
|
|
110
111
|
releaseManagedWriteLock,
|
|
111
112
|
runManagedWritePreflight,
|
|
112
113
|
runManagedWriteTransaction
|
|
113
|
-
} from "./chunk-
|
|
114
|
+
} from "./chunk-UJLSUJB6.js";
|
|
114
115
|
import {
|
|
115
116
|
FLEET_REL_PATH,
|
|
117
|
+
canSupersedeJournal,
|
|
116
118
|
git,
|
|
117
119
|
gitStrict,
|
|
120
|
+
hasActiveGitSequencer,
|
|
121
|
+
hasUnmergedPaths,
|
|
122
|
+
isWorktreeClean,
|
|
118
123
|
listJournalOpIds,
|
|
119
124
|
listReviewRequiredOps,
|
|
120
125
|
loadFleetManifest,
|
|
121
126
|
loadFleetManifestAndHost,
|
|
127
|
+
markJournalSuperseded,
|
|
122
128
|
parseDotenvFile,
|
|
123
129
|
parseDotenvText,
|
|
124
130
|
parseJournalEnv,
|
|
@@ -132,7 +138,7 @@ import {
|
|
|
132
138
|
snapshotterAliasForLocalHost,
|
|
133
139
|
supersedeStaleReviewRequiredJournals,
|
|
134
140
|
writeDotenv
|
|
135
|
-
} from "./chunk-
|
|
141
|
+
} from "./chunk-OWPQJOFG.js";
|
|
136
142
|
import {
|
|
137
143
|
ExitCode,
|
|
138
144
|
MetaSchema,
|
|
@@ -143,7 +149,7 @@ import {
|
|
|
143
149
|
} from "./chunk-C5OLZRRM.js";
|
|
144
150
|
|
|
145
151
|
// src/cli.ts
|
|
146
|
-
import { join as
|
|
152
|
+
import { join as join37 } from "path";
|
|
147
153
|
import { Command } from "commander";
|
|
148
154
|
|
|
149
155
|
// src/utils/output.ts
|
|
@@ -2662,7 +2668,30 @@ function classifyLog(path, id, label, okPattern) {
|
|
|
2662
2668
|
if (okPattern.test(last)) return { id, label, status: "pass", detail: last.slice(0, 120) };
|
|
2663
2669
|
return { id, label, status: "warn", detail: last.slice(0, 120) };
|
|
2664
2670
|
}
|
|
2665
|
-
function
|
|
2671
|
+
function readVaultSyncRoleAndScope(home) {
|
|
2672
|
+
try {
|
|
2673
|
+
const content = readFileSync11(join17(home, ".skillwiki", ".env"), "utf8");
|
|
2674
|
+
let role;
|
|
2675
|
+
let serviceScope;
|
|
2676
|
+
let snapshotScript;
|
|
2677
|
+
for (const line of content.split(/\r?\n/)) {
|
|
2678
|
+
const trimmed = line.trim();
|
|
2679
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
2680
|
+
const eq = trimmed.indexOf("=");
|
|
2681
|
+
if (eq <= 0) continue;
|
|
2682
|
+
const k = trimmed.slice(0, eq).trim();
|
|
2683
|
+
const v = trimmed.slice(eq + 1).trim();
|
|
2684
|
+
if (v.length === 0) continue;
|
|
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 { role, serviceScope, snapshotScript };
|
|
2690
|
+
} catch {
|
|
2691
|
+
return {};
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
function runVaultSyncHealth(home, syncMode, env = process.env) {
|
|
2666
2695
|
if (syncMode === "off") {
|
|
2667
2696
|
return {
|
|
2668
2697
|
status: "pass",
|
|
@@ -2691,6 +2720,30 @@ function runVaultSyncHealth(home, syncMode) {
|
|
|
2691
2720
|
}]
|
|
2692
2721
|
};
|
|
2693
2722
|
}
|
|
2723
|
+
const vsConfig = readVaultSyncRoleAndScope(home);
|
|
2724
|
+
if (vsConfig.role === "snapshotter") {
|
|
2725
|
+
const snapshotScript = vsConfig.snapshotScript ?? join17(shareDir, "wiki-snapshot.sh");
|
|
2726
|
+
const installed = existsSync5(snapshotScript) ? { id: "vault_sync_installed", label: "Vault sync installed", status: "pass", detail: `Found snapshot script: ${snapshotScript}` } : { id: "vault_sync_installed", label: "Vault sync installed", status: "error", detail: `Snapshot script not found at ${snapshotScript}` };
|
|
2727
|
+
const healthChecks = snapshotterHealthChecks(vsConfig.serviceScope ?? "user", logDir, env);
|
|
2728
|
+
const fetchNA = { id: "vault_sync_last_fetch_status", label: "Vault sync last fetch status", status: "pass", detail: "Snapshotter host \u2014 leaf wiki-fetch-notify log not applicable" };
|
|
2729
|
+
const filterNA = { id: "vault_sync_filter_present", label: "Vault sync filter file present", status: "pass", detail: "Snapshotter host \u2014 leaf wiki-push filter not applicable" };
|
|
2730
|
+
let guard;
|
|
2731
|
+
if (!existsSync5(snapshotScript)) {
|
|
2732
|
+
guard = { id: "vault_sync_snapshot_guard", label: "Snapshot script guard", status: "error", detail: `Snapshot script not found at ${snapshotScript}` };
|
|
2733
|
+
} else {
|
|
2734
|
+
const content = readFileSync11(snapshotScript, "utf8");
|
|
2735
|
+
guard = content.includes("--max-delete") ? { id: "vault_sync_snapshot_guard", label: "Snapshot script guard", status: "pass", detail: `--max-delete present in ${snapshotScript}` } : { id: "vault_sync_snapshot_guard", label: "Snapshot script guard", status: "error", detail: `${snapshotScript} is missing --max-delete guard` };
|
|
2736
|
+
}
|
|
2737
|
+
const allChecks = [installed, ...healthChecks, fetchNA, filterNA, guard];
|
|
2738
|
+
const summary2 = summarizeChecks(allChecks);
|
|
2739
|
+
return {
|
|
2740
|
+
status: statusFromCounts(summary2),
|
|
2741
|
+
blocking: syncMode === "required",
|
|
2742
|
+
installed: installed.status === "pass",
|
|
2743
|
+
summary: summary2,
|
|
2744
|
+
checks: allChecks.map((c) => ({ id: c.id, status: c.status, detail: c.detail }))
|
|
2745
|
+
};
|
|
2746
|
+
}
|
|
2694
2747
|
checks.push(existsSync5(pushScript) ? { id: "vault_sync_installed", label: "Vault sync installed", status: "pass", detail: `Found: ${pushScript}` } : { id: "vault_sync_installed", label: "Vault sync installed", status: "error", detail: `Script missing: ${pushScript}` });
|
|
2695
2748
|
if (isMac) {
|
|
2696
2749
|
const pushPlist = join17(home, "Library", "LaunchAgents", "com.karlchow.wiki-push.plist");
|
|
@@ -6503,9 +6556,352 @@ function runSyncJournalClearStale(input) {
|
|
|
6503
6556
|
};
|
|
6504
6557
|
}
|
|
6505
6558
|
|
|
6559
|
+
// src/commands/snapshot-maintenance.ts
|
|
6560
|
+
import { existsSync as existsSync9, readFileSync as readFileSync13, mkdirSync as mkdirSync3, appendFileSync } from "fs";
|
|
6561
|
+
import { createHash as createHash5 } from "crypto";
|
|
6562
|
+
import { execSync as execSync3, spawn } from "child_process";
|
|
6563
|
+
import { platform as platform2 } from "os";
|
|
6564
|
+
import { join as join30, resolve as resolvePath2 } from "path";
|
|
6565
|
+
var MAINTENANCE_SCHEMA_VERSION = 1;
|
|
6566
|
+
var MAINTENANCE_COMMAND = "snapshot-maintenance journal clear-stale";
|
|
6567
|
+
function resolveConfiguredSnapshotWorktree(home) {
|
|
6568
|
+
const skillwikiEnv = join30(home, ".skillwiki", ".env");
|
|
6569
|
+
const explicit = readEnvKey2(skillwikiEnv, ["vault_sync.snapshot_worktree"]);
|
|
6570
|
+
if (explicit) return resolvePath2(explicit);
|
|
6571
|
+
const snapshotProfile = readEnvKey2(skillwikiEnv, ["vault_sync.snapshot_profile"]);
|
|
6572
|
+
if (snapshotProfile) {
|
|
6573
|
+
const fromProfile = readEnvKey2(snapshotProfile, ["WIKI_GIT_WORKTREE", "SNAPSHOT_WORKTREE", "GIT_DIR"]);
|
|
6574
|
+
if (fromProfile) return resolvePath2(fromProfile);
|
|
6575
|
+
}
|
|
6576
|
+
return void 0;
|
|
6577
|
+
}
|
|
6578
|
+
function readEnvKey2(path, keys) {
|
|
6579
|
+
try {
|
|
6580
|
+
const content = readFileSync13(path, "utf8");
|
|
6581
|
+
for (const line of content.split(/\r?\n/)) {
|
|
6582
|
+
const trimmed = line.trim();
|
|
6583
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
6584
|
+
const eq = trimmed.indexOf("=");
|
|
6585
|
+
if (eq <= 0) continue;
|
|
6586
|
+
const key = trimmed.slice(0, eq).trim();
|
|
6587
|
+
if (!keys.includes(key)) continue;
|
|
6588
|
+
const value = trimmed.slice(eq + 1).trim();
|
|
6589
|
+
if (value.length > 0) return value;
|
|
6590
|
+
}
|
|
6591
|
+
} catch {
|
|
6592
|
+
return void 0;
|
|
6593
|
+
}
|
|
6594
|
+
return void 0;
|
|
6595
|
+
}
|
|
6596
|
+
function canonicalize(p) {
|
|
6597
|
+
return resolvePath2(p);
|
|
6598
|
+
}
|
|
6599
|
+
function computeApprovalId(plan) {
|
|
6600
|
+
const eligible = [...plan.eligible_journals].sort((a, b) => a.operation_id.localeCompare(b.operation_id));
|
|
6601
|
+
const targets = eligible.map((j) => `${j.operation_id}:${j.target_oid}`).join(",");
|
|
6602
|
+
const payload = [
|
|
6603
|
+
`v${plan.schema_version}`,
|
|
6604
|
+
plan.command,
|
|
6605
|
+
plan.host_id,
|
|
6606
|
+
plan.role,
|
|
6607
|
+
plan.snapshot_worktree,
|
|
6608
|
+
plan.snapshot_lock_path,
|
|
6609
|
+
plan.git_directory,
|
|
6610
|
+
plan.branch,
|
|
6611
|
+
plan.head_oid,
|
|
6612
|
+
targets,
|
|
6613
|
+
normalizeReason(plan.operator_reason)
|
|
6614
|
+
].join("|");
|
|
6615
|
+
return "smap1-" + createHash5("sha256").update(payload).digest("hex").slice(0, 32);
|
|
6616
|
+
}
|
|
6617
|
+
function normalizeReason(reason) {
|
|
6618
|
+
return reason.trim().replace(/\s+/g, " ");
|
|
6619
|
+
}
|
|
6620
|
+
function refusalErr(code, reason, guidance) {
|
|
6621
|
+
return err(code, { reason, guidance: guidance ?? "" });
|
|
6622
|
+
}
|
|
6623
|
+
async function runSnapshotMaintenanceDryRun(input) {
|
|
6624
|
+
const env = input.env ?? process.env;
|
|
6625
|
+
const home = input.home ?? env.HOME ?? "";
|
|
6626
|
+
const audit = input.auditSink ?? defaultAuditSink(home);
|
|
6627
|
+
const now = input.now ?? Date.now();
|
|
6628
|
+
const fleetLoad = input.fleetLoad !== void 0 ? input.fleetLoad : await loadFleetManifestAndHost({
|
|
6629
|
+
vault: input.liveVaultPath ?? "",
|
|
6630
|
+
env,
|
|
6631
|
+
home,
|
|
6632
|
+
cwd: process.cwd(),
|
|
6633
|
+
osHostname: env.HOSTNAME,
|
|
6634
|
+
user: env.USER
|
|
6635
|
+
});
|
|
6636
|
+
if (!fleetLoad || !fleetLoad.hostId || fleetLoad.identityStatus !== "known") {
|
|
6637
|
+
audit(makeAuditEvent(input, fleetLoad?.hostId ?? "unknown", now, "refusal", "MAINTENANCE_UNKNOWN_IDENTITY"));
|
|
6638
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_UNKNOWN_IDENTITY", "unknown or unresolved fleet identity; cannot authorize snapshot maintenance") };
|
|
6639
|
+
}
|
|
6640
|
+
const host = fleetLoad.manifest.hosts[fleetLoad.hostId];
|
|
6641
|
+
if (!host || host.role !== "snapshotter" || host.protected !== true) {
|
|
6642
|
+
audit(makeAuditEvent(input, fleetLoad.hostId, now, "refusal", "MAINTENANCE_NOT_PROTECTED_SNAPSHOTTER"));
|
|
6643
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_NOT_PROTECTED_SNAPSHOTTER", `host '${fleetLoad.hostId}' is not a protected snapshotter (role=${host?.role ?? "missing"}, protected=${host?.protected ?? false})`) };
|
|
6644
|
+
}
|
|
6645
|
+
const configuredWorktree = resolveConfiguredSnapshotWorktree(home);
|
|
6646
|
+
if (!configuredWorktree) {
|
|
6647
|
+
audit(makeAuditEvent(input, fleetLoad.hostId, now, "refusal", "MAINTENANCE_NO_CONFIGURED_WORKTREE"));
|
|
6648
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_NO_CONFIGURED_WORKTREE", "no configured snapshot worktree (vault_sync.snapshot_worktree or snapshot_profile required)") };
|
|
6649
|
+
}
|
|
6650
|
+
const requested = canonicalize(input.snapshotWorktree);
|
|
6651
|
+
if (requested !== canonicalize(configuredWorktree)) {
|
|
6652
|
+
audit(makeAuditEvent(input, fleetLoad.hostId, now, "refusal", "MAINTENANCE_WRONG_WORKTREE"));
|
|
6653
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_WRONG_WORKTREE", `requested path '${requested}' is not the configured snapshot worktree '${canonicalize(configuredWorktree)}'`) };
|
|
6654
|
+
}
|
|
6655
|
+
if (!existsSync9(requested) || !existsSync9(join30(requested, ".git"))) {
|
|
6656
|
+
audit(makeAuditEvent(input, fleetLoad.hostId, now, "refusal", "MAINTENANCE_MISSING_GIT_REPO"));
|
|
6657
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_MISSING_GIT_REPO", `snapshot worktree is not a git repository: ${requested}`) };
|
|
6658
|
+
}
|
|
6659
|
+
const liveVaultPath = input.liveVaultPath ? canonicalize(input.liveVaultPath) : await resolveLiveVault({ env, home });
|
|
6660
|
+
if (liveVaultPath && requested === canonicalize(liveVaultPath)) {
|
|
6661
|
+
audit(makeAuditEvent(input, fleetLoad.hostId, now, "refusal", "MAINTENANCE_LIVE_VAULT_TARGET"));
|
|
6662
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_LIVE_VAULT_TARGET", "requested path is the live vault, not the snapshot worktree") };
|
|
6663
|
+
}
|
|
6664
|
+
const branch = git(requested, ["rev-parse", "--abbrev-ref", "HEAD"]) || "HEAD";
|
|
6665
|
+
const headOid = git(requested, ["rev-parse", "HEAD"]) || "";
|
|
6666
|
+
const gitDirectory = git(requested, ["rev-parse", "--absolute-git-dir"]) || "";
|
|
6667
|
+
const worktreeClean = isWorktreeClean(requested);
|
|
6668
|
+
const activeSequencer = hasActiveGitSequencer(requested);
|
|
6669
|
+
const unmerged = hasUnmergedPaths(requested);
|
|
6670
|
+
const reviewRequired = listReviewRequiredOps(requested);
|
|
6671
|
+
const eligible = [];
|
|
6672
|
+
const skipped = [];
|
|
6673
|
+
for (const { opId, fields } of reviewRequired) {
|
|
6674
|
+
const target = fields.target_oid?.trim();
|
|
6675
|
+
if (!target) {
|
|
6676
|
+
skipped.push({ operation_id: opId, target_oid: "", reason: fields.reason, prior_reason: fields.prior_reason, eligible: false, refusal_reason: "missing-target-oid" });
|
|
6677
|
+
continue;
|
|
6678
|
+
}
|
|
6679
|
+
if (!canSupersedeJournal(requested, fields, { requireClean: false })) {
|
|
6680
|
+
let refusal = "not-ancestor";
|
|
6681
|
+
if (activeSequencer) refusal = "active-sequencer";
|
|
6682
|
+
else if (unmerged.length > 0) refusal = "unmerged-paths";
|
|
6683
|
+
else if (!gitMergeBaseIsAncestor(requested, target, headOid)) refusal = "not-ancestor";
|
|
6684
|
+
skipped.push({ operation_id: opId, target_oid: target, reason: fields.reason, prior_reason: fields.prior_reason, eligible: false, refusal_reason: refusal });
|
|
6685
|
+
continue;
|
|
6686
|
+
}
|
|
6687
|
+
eligible.push({ operation_id: opId, target_oid: target, reason: fields.reason, prior_reason: fields.prior_reason, eligible: true });
|
|
6688
|
+
}
|
|
6689
|
+
const snapshotLockPath = input.snapshotLockPath ?? "/var/lock/wiki-snapshot.lock";
|
|
6690
|
+
const planBase = {
|
|
6691
|
+
schema_version: MAINTENANCE_SCHEMA_VERSION,
|
|
6692
|
+
command: MAINTENANCE_COMMAND,
|
|
6693
|
+
host_id: fleetLoad.hostId,
|
|
6694
|
+
role: host.role,
|
|
6695
|
+
protected: host.protected === true,
|
|
6696
|
+
snapshot_worktree: requested,
|
|
6697
|
+
snapshot_lock_path: snapshotLockPath,
|
|
6698
|
+
git_directory: gitDirectory,
|
|
6699
|
+
branch,
|
|
6700
|
+
head_oid: headOid,
|
|
6701
|
+
worktree_clean: worktreeClean,
|
|
6702
|
+
active_sequencer: activeSequencer,
|
|
6703
|
+
unmerged_paths: unmerged,
|
|
6704
|
+
eligible_journals: eligible,
|
|
6705
|
+
skipped_journals: skipped,
|
|
6706
|
+
operator_reason: normalizeReason(input.reason ?? "")
|
|
6707
|
+
};
|
|
6708
|
+
const approvalId = eligible.length > 0 ? computeApprovalId(planBase) : null;
|
|
6709
|
+
const plan = { ...planBase, approval_id: approvalId };
|
|
6710
|
+
audit(makeAuditEvent(input, fleetLoad.hostId, now, "dry-run", void 0, approvalId ?? void 0));
|
|
6711
|
+
return {
|
|
6712
|
+
exitCode: ExitCode.OK,
|
|
6713
|
+
result: ok({
|
|
6714
|
+
dry_run: true,
|
|
6715
|
+
plan,
|
|
6716
|
+
humanHint: eligible.length === 0 ? `dry-run: 0 eligible journals; skipped=${skipped.length}` : `dry-run: ${eligible.length} eligible journal(s); approval_id=${approvalId}`
|
|
6717
|
+
})
|
|
6718
|
+
};
|
|
6719
|
+
}
|
|
6720
|
+
function gitMergeBaseIsAncestor(repo, ancestor, tip) {
|
|
6721
|
+
if (ancestor === tip) return true;
|
|
6722
|
+
const mb = git(repo, ["merge-base", ancestor, tip]);
|
|
6723
|
+
return mb !== "" && mb === ancestor;
|
|
6724
|
+
}
|
|
6725
|
+
async function resolveLiveVault(input) {
|
|
6726
|
+
const resolved = await resolveRuntimePath({
|
|
6727
|
+
flag: void 0,
|
|
6728
|
+
envValue: input.env.WIKI_PATH,
|
|
6729
|
+
wikiEnv: input.env.WIKI,
|
|
6730
|
+
home: input.home,
|
|
6731
|
+
cwd: process.cwd()
|
|
6732
|
+
});
|
|
6733
|
+
return resolved.ok ? canonicalize(resolved.data.path) : void 0;
|
|
6734
|
+
}
|
|
6735
|
+
function snapshotMaintenanceAuditLogPath(home) {
|
|
6736
|
+
const stateDir = platform2() === "darwin" ? join30(home, "Library", "Application Support", "vault-sync") : join30(home, ".local", "state", "vault-sync");
|
|
6737
|
+
return join30(stateDir, "snapshot-maintenance-audit.jsonl");
|
|
6738
|
+
}
|
|
6739
|
+
function defaultAuditSink(home) {
|
|
6740
|
+
return (event) => {
|
|
6741
|
+
try {
|
|
6742
|
+
const logPath = snapshotMaintenanceAuditLogPath(home);
|
|
6743
|
+
mkdirSync3(join30(logPath, ".."), { recursive: true });
|
|
6744
|
+
appendFileSync(logPath, JSON.stringify(event) + "\n", { encoding: "utf8" });
|
|
6745
|
+
} catch {
|
|
6746
|
+
}
|
|
6747
|
+
};
|
|
6748
|
+
}
|
|
6749
|
+
function makeAuditEvent(input, hostId, now, result, errorCode, approvalId) {
|
|
6750
|
+
return {
|
|
6751
|
+
ts: new Date(now).toISOString(),
|
|
6752
|
+
schema_version: MAINTENANCE_SCHEMA_VERSION,
|
|
6753
|
+
command: MAINTENANCE_COMMAND,
|
|
6754
|
+
host: hostId,
|
|
6755
|
+
actor: input.env?.USER ?? process.env.USER ?? "unknown",
|
|
6756
|
+
session: input.sessionId ?? "unknown",
|
|
6757
|
+
canonical_target: canonicalize(input.snapshotWorktree),
|
|
6758
|
+
reason: normalizeReason(input.reason ?? ""),
|
|
6759
|
+
approval_id: approvalId,
|
|
6760
|
+
result,
|
|
6761
|
+
error_code: errorCode
|
|
6762
|
+
};
|
|
6763
|
+
}
|
|
6764
|
+
async function runSnapshotMaintenanceExecute(input) {
|
|
6765
|
+
const env = input.env ?? process.env;
|
|
6766
|
+
const home = input.home ?? env.HOME ?? "";
|
|
6767
|
+
const audit = input.auditSink ?? defaultAuditSink(home);
|
|
6768
|
+
const now = input.now ?? Date.now();
|
|
6769
|
+
const isTty = input.isTty ?? !!process.stdin.isTTY;
|
|
6770
|
+
if (!isTty) {
|
|
6771
|
+
audit(makeAuditEvent(input, "unknown", now, "refusal", "MAINTENANCE_NO_TTY"));
|
|
6772
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_NO_TTY", "snapshot maintenance requires an attended TTY") };
|
|
6773
|
+
}
|
|
6774
|
+
const reason = normalizeReason(input.reason ?? "");
|
|
6775
|
+
if (!reason) {
|
|
6776
|
+
audit(makeAuditEvent(input, "unknown", now, "refusal", "MAINTENANCE_NO_REASON"));
|
|
6777
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_NO_REASON", "snapshot maintenance requires a non-empty operator reason") };
|
|
6778
|
+
}
|
|
6779
|
+
if (!input.approvalId) {
|
|
6780
|
+
audit(makeAuditEvent(input, "unknown", now, "refusal", "MAINTENANCE_NO_APPROVAL_ID"));
|
|
6781
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_NO_APPROVAL_ID", "snapshot maintenance requires an approval ID from a prior dry run") };
|
|
6782
|
+
}
|
|
6783
|
+
const dryRunResult = await runSnapshotMaintenanceDryRun(input);
|
|
6784
|
+
if (!dryRunResult.result.ok) return dryRunResult;
|
|
6785
|
+
const plan = dryRunResult.result.data.plan;
|
|
6786
|
+
if (!plan.approval_id) {
|
|
6787
|
+
audit(makeAuditEvent(input, plan.host_id, now, "refusal", "MAINTENANCE_NO_ELIGIBLE_JOURNALS"));
|
|
6788
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_NO_ELIGIBLE_JOURNALS", "no eligible journals to supersede") };
|
|
6789
|
+
}
|
|
6790
|
+
if (plan.approval_id !== input.approvalId) {
|
|
6791
|
+
audit(makeAuditEvent(input, plan.host_id, now, "refusal", "MAINTENANCE_STALE_APPROVAL_ID"));
|
|
6792
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_STALE_APPROVAL_ID", "approval ID does not match the current state; rerun dry run") };
|
|
6793
|
+
}
|
|
6794
|
+
if (!plan.worktree_clean) {
|
|
6795
|
+
audit(makeAuditEvent(input, plan.host_id, now, "refusal", "MAINTENANCE_DIRTY_WORKTREE"));
|
|
6796
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_DIRTY_WORKTREE", "snapshot worktree is dirty") };
|
|
6797
|
+
}
|
|
6798
|
+
if (plan.active_sequencer) {
|
|
6799
|
+
audit(makeAuditEvent(input, plan.host_id, now, "refusal", "MAINTENANCE_ACTIVE_SEQUENCER"));
|
|
6800
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_ACTIVE_SEQUENCER", "git sequencer (merge/rebase/cherry-pick/revert) is active") };
|
|
6801
|
+
}
|
|
6802
|
+
if (plan.unmerged_paths.length > 0) {
|
|
6803
|
+
audit(makeAuditEvent(input, plan.host_id, now, "refusal", "MAINTENANCE_UNMERGED_PATHS"));
|
|
6804
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_UNMERGED_PATHS", `unmerged paths: ${plan.unmerged_paths.join(", ")}`) };
|
|
6805
|
+
}
|
|
6806
|
+
if (!input.skipFlock) {
|
|
6807
|
+
const flockResult = acquireSnapshotFlock(plan.snapshot_lock_path);
|
|
6808
|
+
if (!flockResult.acquired) {
|
|
6809
|
+
audit(makeAuditEvent(input, plan.host_id, now, "refusal", "MAINTENANCE_FLOCK_BUSY"));
|
|
6810
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_FLOCK_BUSY", `snapshot flock busy: ${plan.snapshot_lock_path}`) };
|
|
6811
|
+
}
|
|
6812
|
+
try {
|
|
6813
|
+
return await performSupersession(input, plan, audit, now);
|
|
6814
|
+
} finally {
|
|
6815
|
+
releaseSnapshotFlock(flockResult);
|
|
6816
|
+
}
|
|
6817
|
+
}
|
|
6818
|
+
return performSupersession(input, plan, audit, now);
|
|
6819
|
+
}
|
|
6820
|
+
async function performSupersession(input, plan, audit, now) {
|
|
6821
|
+
const reDryRun = await runSnapshotMaintenanceDryRun(input);
|
|
6822
|
+
if (!reDryRun.result.ok) return reDryRun;
|
|
6823
|
+
const recomputed = reDryRun.result.data.plan;
|
|
6824
|
+
if (recomputed.head_oid !== plan.head_oid) {
|
|
6825
|
+
audit(makeAuditEvent(input, plan.host_id, now, "refusal", "MAINTENANCE_HEAD_CHANGED"));
|
|
6826
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_HEAD_CHANGED", "HEAD changed after dry run") };
|
|
6827
|
+
}
|
|
6828
|
+
const reEligible = new Set(recomputed.eligible_journals.map((j) => j.operation_id));
|
|
6829
|
+
const approvedSet = new Set(plan.eligible_journals.map((j) => j.operation_id));
|
|
6830
|
+
for (const id of approvedSet) {
|
|
6831
|
+
if (!reEligible.has(id)) {
|
|
6832
|
+
audit(makeAuditEvent(input, plan.host_id, now, "refusal", "MAINTENANCE_JOURNAL_SET_CHANGED"));
|
|
6833
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_JOURNAL_SET_CHANGED", `approved journal '${id}' is no longer eligible`) };
|
|
6834
|
+
}
|
|
6835
|
+
}
|
|
6836
|
+
const superseded = [];
|
|
6837
|
+
const skipped = [];
|
|
6838
|
+
for (const j of plan.eligible_journals) {
|
|
6839
|
+
const fields = readJournal(plan.snapshot_worktree, j.operation_id);
|
|
6840
|
+
if (!fields) {
|
|
6841
|
+
skipped.push(j.operation_id);
|
|
6842
|
+
continue;
|
|
6843
|
+
}
|
|
6844
|
+
if (fields.target_oid?.trim() !== j.target_oid) {
|
|
6845
|
+
skipped.push(j.operation_id);
|
|
6846
|
+
continue;
|
|
6847
|
+
}
|
|
6848
|
+
const by = `snapshot-maintenance:${input.env?.USER ?? process.env.USER ?? "unknown"}:${input.sessionId ?? "unknown"}`;
|
|
6849
|
+
if (markJournalSuperseded(plan.snapshot_worktree, j.operation_id, fields, by)) {
|
|
6850
|
+
superseded.push(j.operation_id);
|
|
6851
|
+
} else {
|
|
6852
|
+
skipped.push(j.operation_id);
|
|
6853
|
+
}
|
|
6854
|
+
}
|
|
6855
|
+
const noOp = superseded.length === 0;
|
|
6856
|
+
const result = {
|
|
6857
|
+
superseded,
|
|
6858
|
+
skipped,
|
|
6859
|
+
approval_id: plan.approval_id,
|
|
6860
|
+
no_op: noOp
|
|
6861
|
+
};
|
|
6862
|
+
audit(makeAuditEvent(input, plan.host_id, now, noOp ? "no-op" : "success", void 0, plan.approval_id ?? void 0));
|
|
6863
|
+
return {
|
|
6864
|
+
exitCode: ExitCode.OK,
|
|
6865
|
+
result: ok({
|
|
6866
|
+
dry_run: false,
|
|
6867
|
+
execution: result,
|
|
6868
|
+
humanHint: noOp ? `execution: no-op (0 superseded; skipped=${skipped.length})` : `execution: ${superseded.length} journal(s) superseded; skipped=${skipped.length}`
|
|
6869
|
+
})
|
|
6870
|
+
};
|
|
6871
|
+
}
|
|
6872
|
+
function acquireSnapshotFlock(lockPath) {
|
|
6873
|
+
try {
|
|
6874
|
+
const child = spawn("bash", ["-c", `exec 9>"${lockPath}"; flock -n 9 || exit 1; while true; do sleep 3600; done`], {
|
|
6875
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
6876
|
+
detached: false
|
|
6877
|
+
});
|
|
6878
|
+
const deadline = Date.now() + 300;
|
|
6879
|
+
while (Date.now() < deadline) {
|
|
6880
|
+
if (child.exitCode !== null) {
|
|
6881
|
+
return { acquired: false, path: lockPath };
|
|
6882
|
+
}
|
|
6883
|
+
try {
|
|
6884
|
+
execSync3("sleep 0.05", { timeout: 200 });
|
|
6885
|
+
} catch {
|
|
6886
|
+
}
|
|
6887
|
+
}
|
|
6888
|
+
return { acquired: true, path: lockPath, holder: { kill: () => child.kill("SIGTERM") } };
|
|
6889
|
+
} catch {
|
|
6890
|
+
return { acquired: false, path: lockPath };
|
|
6891
|
+
}
|
|
6892
|
+
}
|
|
6893
|
+
function releaseSnapshotFlock(handle) {
|
|
6894
|
+
if (handle.holder) {
|
|
6895
|
+
try {
|
|
6896
|
+
handle.holder.kill();
|
|
6897
|
+
} catch {
|
|
6898
|
+
}
|
|
6899
|
+
}
|
|
6900
|
+
}
|
|
6901
|
+
|
|
6506
6902
|
// src/commands/backup.ts
|
|
6507
|
-
import { statSync as statSync2, readdirSync as readdirSync2, readFileSync as
|
|
6508
|
-
import { join as
|
|
6903
|
+
import { statSync as statSync2, readdirSync as readdirSync2, readFileSync as readFileSync14, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
6904
|
+
import { join as join31, relative as relative2, dirname as dirname6 } from "path";
|
|
6509
6905
|
import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
|
|
6510
6906
|
|
|
6511
6907
|
// src/utils/s3-client.ts
|
|
@@ -6529,7 +6925,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node_
|
|
|
6529
6925
|
function* walkMarkdown(dir, base) {
|
|
6530
6926
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
6531
6927
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
6532
|
-
const full =
|
|
6928
|
+
const full = join31(dir, entry.name);
|
|
6533
6929
|
if (entry.isDirectory()) {
|
|
6534
6930
|
yield* walkMarkdown(full, base);
|
|
6535
6931
|
} else if (entry.name.endsWith(".md")) {
|
|
@@ -6552,7 +6948,7 @@ async function runBackupSync(input) {
|
|
|
6552
6948
|
let failed = 0;
|
|
6553
6949
|
const files = [...walkMarkdown(input.vault, input.vault)];
|
|
6554
6950
|
for (const relPath of files) {
|
|
6555
|
-
const absPath =
|
|
6951
|
+
const absPath = join31(input.vault, relPath);
|
|
6556
6952
|
const localStat = statSync2(absPath);
|
|
6557
6953
|
let needsUpload = true;
|
|
6558
6954
|
try {
|
|
@@ -6571,7 +6967,7 @@ async function runBackupSync(input) {
|
|
|
6571
6967
|
continue;
|
|
6572
6968
|
}
|
|
6573
6969
|
try {
|
|
6574
|
-
const body =
|
|
6970
|
+
const body = readFileSync14(absPath);
|
|
6575
6971
|
await client.send(new PutObjectCommand({ Bucket: input.bucket, Key: relPath, Body: body }));
|
|
6576
6972
|
uploaded++;
|
|
6577
6973
|
} catch {
|
|
@@ -6628,7 +7024,7 @@ async function runBackupRestore(input) {
|
|
|
6628
7024
|
const objects = list.Contents ?? [];
|
|
6629
7025
|
for (const obj of objects) {
|
|
6630
7026
|
if (!obj.Key) continue;
|
|
6631
|
-
const localPath =
|
|
7027
|
+
const localPath = join31(target, obj.Key);
|
|
6632
7028
|
try {
|
|
6633
7029
|
const localStat = statSync2(localPath);
|
|
6634
7030
|
if (obj.LastModified && localStat.mtime > obj.LastModified) {
|
|
@@ -6641,7 +7037,7 @@ async function runBackupRestore(input) {
|
|
|
6641
7037
|
const resp = await client.send(new GetObjectCommand({ Bucket: input.bucket, Key: obj.Key }));
|
|
6642
7038
|
const body = await resp.Body?.transformToByteArray();
|
|
6643
7039
|
if (body) {
|
|
6644
|
-
|
|
7040
|
+
mkdirSync4(dirname6(localPath), { recursive: true });
|
|
6645
7041
|
writeFileSync5(localPath, Buffer.from(body));
|
|
6646
7042
|
downloaded++;
|
|
6647
7043
|
}
|
|
@@ -6674,11 +7070,11 @@ async function runBackupRestore(input) {
|
|
|
6674
7070
|
}
|
|
6675
7071
|
|
|
6676
7072
|
// src/commands/status.ts
|
|
6677
|
-
import { existsSync as
|
|
7073
|
+
import { existsSync as existsSync10, statSync as statSync3 } from "fs";
|
|
6678
7074
|
import { readFile as readFile15 } from "fs/promises";
|
|
6679
|
-
import { join as
|
|
7075
|
+
import { join as join32 } from "path";
|
|
6680
7076
|
async function runStatus(input) {
|
|
6681
|
-
if (!
|
|
7077
|
+
if (!existsSync10(input.vault)) {
|
|
6682
7078
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
6683
7079
|
}
|
|
6684
7080
|
const scan = await scanVault(input.vault);
|
|
@@ -6703,7 +7099,7 @@ async function runStatus(input) {
|
|
|
6703
7099
|
const compound = scan.data.compound.length;
|
|
6704
7100
|
let schemaVersion = "v1";
|
|
6705
7101
|
try {
|
|
6706
|
-
const schemaContent = await readFile15(
|
|
7102
|
+
const schemaContent = await readFile15(join32(input.vault, "SCHEMA.md"), "utf8");
|
|
6707
7103
|
const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
|
|
6708
7104
|
if (versionMatch) schemaVersion = versionMatch[1];
|
|
6709
7105
|
} catch {
|
|
@@ -6764,7 +7160,7 @@ async function runStatus(input) {
|
|
|
6764
7160
|
|
|
6765
7161
|
// src/commands/seed.ts
|
|
6766
7162
|
import { mkdir as mkdir10, writeFile as writeFile9, stat as stat4 } from "fs/promises";
|
|
6767
|
-
import { join as
|
|
7163
|
+
import { join as join33 } from "path";
|
|
6768
7164
|
var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6769
7165
|
var EXAMPLE_PAGES = {
|
|
6770
7166
|
"entities/example-project.md": `---
|
|
@@ -6833,29 +7229,29 @@ Real sources are immutable after ingestion \u2014 never edit them.
|
|
|
6833
7229
|
`;
|
|
6834
7230
|
async function runSeed(input) {
|
|
6835
7231
|
try {
|
|
6836
|
-
await stat4(
|
|
7232
|
+
await stat4(join33(input.vault, "SCHEMA.md"));
|
|
6837
7233
|
} catch {
|
|
6838
7234
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
|
|
6839
7235
|
}
|
|
6840
7236
|
const created = [];
|
|
6841
7237
|
const skipped = [];
|
|
6842
7238
|
for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
|
|
6843
|
-
const absPath =
|
|
7239
|
+
const absPath = join33(input.vault, relPath);
|
|
6844
7240
|
try {
|
|
6845
7241
|
await stat4(absPath);
|
|
6846
7242
|
skipped.push(relPath);
|
|
6847
7243
|
} catch {
|
|
6848
|
-
await mkdir10(
|
|
7244
|
+
await mkdir10(join33(absPath, ".."), { recursive: true });
|
|
6849
7245
|
await writeFile9(absPath, content, "utf8");
|
|
6850
7246
|
created.push(relPath);
|
|
6851
7247
|
}
|
|
6852
7248
|
}
|
|
6853
|
-
const rawPath =
|
|
7249
|
+
const rawPath = join33(input.vault, "raw", "articles", "example-source.md");
|
|
6854
7250
|
try {
|
|
6855
7251
|
await stat4(rawPath);
|
|
6856
7252
|
skipped.push("raw/articles/example-source.md");
|
|
6857
7253
|
} catch {
|
|
6858
|
-
await mkdir10(
|
|
7254
|
+
await mkdir10(join33(rawPath, ".."), { recursive: true });
|
|
6859
7255
|
await writeFile9(rawPath, EXAMPLE_RAW, "utf8");
|
|
6860
7256
|
created.push("raw/articles/example-source.md");
|
|
6861
7257
|
}
|
|
@@ -6879,8 +7275,8 @@ async function runSeed(input) {
|
|
|
6879
7275
|
|
|
6880
7276
|
// src/commands/canvas.ts
|
|
6881
7277
|
import { readFile as readFile16, writeFile as writeFile10 } from "fs/promises";
|
|
6882
|
-
import { existsSync as
|
|
6883
|
-
import { join as
|
|
7278
|
+
import { existsSync as existsSync11 } from "fs";
|
|
7279
|
+
import { join as join34 } from "path";
|
|
6884
7280
|
var NODE_WIDTH = 240;
|
|
6885
7281
|
var NODE_HEIGHT = 60;
|
|
6886
7282
|
var COLUMN_SPACING = 400;
|
|
@@ -6958,8 +7354,8 @@ function buildCanvasEdges(adjacency) {
|
|
|
6958
7354
|
return edges;
|
|
6959
7355
|
}
|
|
6960
7356
|
async function runCanvasGenerate(input) {
|
|
6961
|
-
const graphPath = input.graphPath ??
|
|
6962
|
-
if (!
|
|
7357
|
+
const graphPath = input.graphPath ?? join34(input.vault, ".skillwiki", "graph.json");
|
|
7358
|
+
if (!existsSync11(graphPath)) {
|
|
6963
7359
|
return {
|
|
6964
7360
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
6965
7361
|
result: err("FILE_NOT_FOUND", {
|
|
@@ -6996,7 +7392,7 @@ async function runCanvasGenerate(input) {
|
|
|
6996
7392
|
const nodes = buildCanvasNodes(paths);
|
|
6997
7393
|
const edges = buildCanvasEdges(graph.adjacency);
|
|
6998
7394
|
const canvas = { nodes, edges };
|
|
6999
|
-
const outPath =
|
|
7395
|
+
const outPath = join34(input.vault, "vault-graph.canvas");
|
|
7000
7396
|
try {
|
|
7001
7397
|
await writeFile10(outPath, JSON.stringify(canvas, null, 2));
|
|
7002
7398
|
} catch (e) {
|
|
@@ -7018,10 +7414,10 @@ written: ${outPath}`
|
|
|
7018
7414
|
}
|
|
7019
7415
|
|
|
7020
7416
|
// src/commands/fleet-health.ts
|
|
7021
|
-
import { existsSync as
|
|
7417
|
+
import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
|
|
7022
7418
|
import { execSync as nodeExecSync } from "child_process";
|
|
7023
7419
|
import { hostname as nodeHostname, platform as nodePlatform } from "os";
|
|
7024
|
-
import { join as
|
|
7420
|
+
import { join as join35 } from "path";
|
|
7025
7421
|
var SSH_TIMEOUT_MS = 15e3;
|
|
7026
7422
|
var TIMER_UNIT = "agent-memory-trends.timer";
|
|
7027
7423
|
var SERVICE_UNIT = "agent-memory-trends.service";
|
|
@@ -7118,9 +7514,9 @@ function applyServiceFailedOverlay(run, serviceFailed) {
|
|
|
7118
7514
|
function probeLocal(vaultPath, deps) {
|
|
7119
7515
|
const latestPath = satelliteLatestRunPath(vaultPath);
|
|
7120
7516
|
let parsed = null;
|
|
7121
|
-
if (
|
|
7517
|
+
if (existsSync12(latestPath)) {
|
|
7122
7518
|
try {
|
|
7123
|
-
const wire = readSatelliteLatestRunFromText(
|
|
7519
|
+
const wire = readSatelliteLatestRunFromText(readFileSync15(latestPath, "utf8"));
|
|
7124
7520
|
if (wire) {
|
|
7125
7521
|
parsed = {
|
|
7126
7522
|
status: wire.status,
|
|
@@ -7237,12 +7633,12 @@ function formatTable(rows) {
|
|
|
7237
7633
|
);
|
|
7238
7634
|
return [header, ...lines].join("\n");
|
|
7239
7635
|
}
|
|
7240
|
-
function rowHealthy(reachable, timer, runUnhealthy, timerBad,
|
|
7636
|
+
function rowHealthy(reachable, timer, runUnhealthy, timerBad, platform3) {
|
|
7241
7637
|
if (reachable === "no-access") return true;
|
|
7242
7638
|
if (reachable === "no") return false;
|
|
7243
7639
|
if (runUnhealthy) return false;
|
|
7244
|
-
if (
|
|
7245
|
-
if (
|
|
7640
|
+
if (platform3 === "linux" && timerBad) return false;
|
|
7641
|
+
if (platform3 === "linux" && timer === "inactive") return false;
|
|
7246
7642
|
return true;
|
|
7247
7643
|
}
|
|
7248
7644
|
async function runFleetHealth(input) {
|
|
@@ -7251,7 +7647,7 @@ async function runFleetHealth(input) {
|
|
|
7251
7647
|
const home = input.home ?? env.HOME ?? "";
|
|
7252
7648
|
const osHostname = input.osHostname ?? env.HOSTNAME ?? nodeHostname();
|
|
7253
7649
|
const vault = input.vault ?? env.WIKI_PATH;
|
|
7254
|
-
const file = input.file ?? (vault ?
|
|
7650
|
+
const file = input.file ?? (vault ? join35(vault, FLEET_REL_PATH) : void 0);
|
|
7255
7651
|
if (!file) {
|
|
7256
7652
|
return {
|
|
7257
7653
|
exitCode: ExitCode.NO_VAULT_CONFIGURED,
|
|
@@ -7335,15 +7731,15 @@ async function runFleetHealth(input) {
|
|
|
7335
7731
|
}
|
|
7336
7732
|
|
|
7337
7733
|
// src/utils/auto-commit.ts
|
|
7338
|
-
import { existsSync as
|
|
7339
|
-
import { join as
|
|
7734
|
+
import { existsSync as existsSync13 } from "fs";
|
|
7735
|
+
import { join as join36 } from "path";
|
|
7340
7736
|
async function postCommit(vault, exitCode) {
|
|
7341
7737
|
if (exitCode !== 0) return;
|
|
7342
7738
|
const home = process.env.HOME ?? "";
|
|
7343
7739
|
const dotenv = await parseDotenvFile(configPath(home));
|
|
7344
7740
|
const autoCommit = process.env.AUTO_COMMIT ?? dotenv["AUTO_COMMIT"];
|
|
7345
7741
|
if (autoCommit === "false") return;
|
|
7346
|
-
if (!
|
|
7742
|
+
if (!existsSync13(join36(vault, ".git"))) return;
|
|
7347
7743
|
const lastOps = readLastOp(vault);
|
|
7348
7744
|
if (lastOps.length === 0) return;
|
|
7349
7745
|
const porcelain = git(vault, ["status", "--porcelain", "--", ...VAULT_COMMIT_PATHSPEC]);
|
|
@@ -7367,9 +7763,9 @@ async function postCommit(vault, exitCode) {
|
|
|
7367
7763
|
}
|
|
7368
7764
|
|
|
7369
7765
|
// src/commands/write-preflight.ts
|
|
7370
|
-
import { existsSync as
|
|
7766
|
+
import { existsSync as existsSync14, readFileSync as readFileSync16, statSync as statSync4 } from "fs";
|
|
7371
7767
|
async function runWritePreflightCommand(input) {
|
|
7372
|
-
if (!
|
|
7768
|
+
if (!existsSync14(input.vault) || !statSync4(input.vault).isDirectory()) {
|
|
7373
7769
|
return {
|
|
7374
7770
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
7375
7771
|
result: err(GateError.VAULT_PATH_INVALID, { path: input.vault })
|
|
@@ -7377,13 +7773,13 @@ async function runWritePreflightCommand(input) {
|
|
|
7377
7773
|
}
|
|
7378
7774
|
let priorText = input.priorArtifactText;
|
|
7379
7775
|
if (input.priorArtifactFile) {
|
|
7380
|
-
if (!
|
|
7776
|
+
if (!existsSync14(input.priorArtifactFile)) {
|
|
7381
7777
|
return {
|
|
7382
7778
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
7383
7779
|
result: err("FILE_NOT_FOUND", { path: input.priorArtifactFile })
|
|
7384
7780
|
};
|
|
7385
7781
|
}
|
|
7386
|
-
priorText =
|
|
7782
|
+
priorText = readFileSync16(input.priorArtifactFile, "utf8");
|
|
7387
7783
|
}
|
|
7388
7784
|
const checkList = input.checks ? input.checks.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
7389
7785
|
const result = runWritePreflight({
|
|
@@ -7497,7 +7893,7 @@ async function emitManagedVaultWrite(vault, command, mutate, opts) {
|
|
|
7497
7893
|
if (dirty) {
|
|
7498
7894
|
return emit(dirty, void 0, { postCommit: false });
|
|
7499
7895
|
}
|
|
7500
|
-
const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-
|
|
7896
|
+
const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-M2HL6DDY.js");
|
|
7501
7897
|
const run = await runManagedWriteTransaction2({
|
|
7502
7898
|
vault,
|
|
7503
7899
|
command,
|
|
@@ -7522,7 +7918,7 @@ program.command("validate <file>").description("validate vault page frontmatter
|
|
|
7522
7918
|
emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
|
|
7523
7919
|
});
|
|
7524
7920
|
program.command("graph").description("graph subcommands").command("build <vault>").option("--out <path>", "graph output path (default: <vault>/.skillwiki/graph.json)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
7525
|
-
const out = opts.out ??
|
|
7921
|
+
const out = opts.out ?? join37(vault, ".skillwiki", "graph.json");
|
|
7526
7922
|
return emitGuardedVaultWrite(vault, "graph build", () => runGraphBuild({ vault, out }));
|
|
7527
7923
|
});
|
|
7528
7924
|
var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
|
|
@@ -8132,6 +8528,21 @@ syncJournalCmd.command("clear-stale [vault]").description("supersede stale revie
|
|
|
8132
8528
|
);
|
|
8133
8529
|
}
|
|
8134
8530
|
});
|
|
8531
|
+
var snapshotMaintenanceCmd = program.command("snapshot-maintenance").description("attended protected-snapshotter maintenance operations");
|
|
8532
|
+
var snapMaintJournalCmd = snapshotMaintenanceCmd.command("journal").description("snapshot operation journal maintenance");
|
|
8533
|
+
snapMaintJournalCmd.command("clear-stale [snapshot-worktree]").description("supersede safe stale review-required journals on a protected snapshotter (attended, one-shot)").option("--dry-run", "non-mutating plan + approval ID (no execution)", false).option("--approve <id>", "state-bound approval ID from a prior --dry-run").option("--reason <text>", "non-empty operator reason for the maintenance").option("--wiki <name>", "wiki profile name").action(async (snapshotWorktree, opts) => {
|
|
8534
|
+
if (!snapshotWorktree) {
|
|
8535
|
+
emit({ exitCode: ExitCode.USAGE, result: err("USAGE", "snapshot-maintenance journal clear-stale requires a snapshot-worktree path argument") });
|
|
8536
|
+
return;
|
|
8537
|
+
}
|
|
8538
|
+
if (opts.dryRun) {
|
|
8539
|
+
emit(await runSnapshotMaintenanceDryRun({ snapshotWorktree, dryRun: true, reason: opts.reason }));
|
|
8540
|
+
} else if (opts.approve) {
|
|
8541
|
+
emit(await runSnapshotMaintenanceExecute({ snapshotWorktree, dryRun: false, approvalId: opts.approve, reason: opts.reason }));
|
|
8542
|
+
} else {
|
|
8543
|
+
emit({ exitCode: ExitCode.USAGE, result: err("USAGE", "provide --dry-run for a plan, or --approve <id> --reason <text> to execute") });
|
|
8544
|
+
}
|
|
8545
|
+
});
|
|
8135
8546
|
syncCmd.command("lock [vault]").description("acquire advisory lock on vault").option("--summary <text>", "lock description", "skillwiki sync").option("--ttl-minutes <n>", "lock time-to-live in minutes", "30").option("--force", "overwrite existing lock", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
8136
8547
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
8137
8548
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import {
|
|
3
3
|
runManagedWritePreflight,
|
|
4
4
|
runManagedWriteTransaction
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-UJLSUJB6.js";
|
|
6
|
+
import "./chunk-OWPQJOFG.js";
|
|
7
7
|
import "./chunk-C5OLZRRM.js";
|
|
8
8
|
export {
|
|
9
9
|
runManagedWritePreflight,
|
package/dist/skillwiki-mcp.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
runSkillwikiMcpStdio
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-24MVNAFJ.js";
|
|
5
5
|
import "./chunk-7I2TPIV5.js";
|
|
6
6
|
import "./chunk-NMUYMNNB.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-OWPQJOFG.js";
|
|
8
8
|
import "./chunk-C5OLZRRM.js";
|
|
9
9
|
|
|
10
10
|
// src/mcp-entry.ts
|
|
@@ -309,6 +309,18 @@ log() {
|
|
|
309
309
|
echo "$(date '+%Y-%m-%d %H:%M:%S') $1" | tee -a "$LOG_FILE"
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
+
# Emit the canonical snapshot-completion terminal record (v0.10.14).
|
|
313
|
+
# One stable machine-parseable line written for both pushed and no-change
|
|
314
|
+
# success outcomes. Failure paths must never call this.
|
|
315
|
+
# SNAPSHOT_COMPLETE schema=v1 outcome=<pushed|no-change> result=success ts=<ISO> head=<oid> origin=<oid|unknown>
|
|
316
|
+
emit_snapshot_complete() {
|
|
317
|
+
local outcome="$1"
|
|
318
|
+
local head_oid origin_oid
|
|
319
|
+
head_oid="$(git -C "$SNAPSHOT_WORKTREE" rev-parse HEAD 2>/dev/null || echo unknown)"
|
|
320
|
+
origin_oid="$(git -C "$SNAPSHOT_WORKTREE" rev-parse --verify -q origin/main 2>/dev/null || echo unknown)"
|
|
321
|
+
log "SNAPSHOT_COMPLETE schema=v1 outcome=${outcome} result=success ts=$(date -u +%Y-%m-%dT%H:%M:%SZ) head=${head_oid} origin=${origin_oid}"
|
|
322
|
+
}
|
|
323
|
+
|
|
312
324
|
validate_tombstone_prune_cap() {
|
|
313
325
|
case "$MAX_TOMBSTONE_PRUNES" in
|
|
314
326
|
''|*[!0-9]*)
|
|
@@ -731,6 +743,7 @@ fi
|
|
|
731
743
|
# Check for changes
|
|
732
744
|
if [ -z "$(git status --porcelain)" ]; then
|
|
733
745
|
log "No changes to commit"
|
|
746
|
+
emit_snapshot_complete "no-change"
|
|
734
747
|
exit 0
|
|
735
748
|
fi
|
|
736
749
|
|
|
@@ -807,6 +820,7 @@ done
|
|
|
807
820
|
if [ "$PUSH_SUCCESS" = true ]; then
|
|
808
821
|
log "Push successful"
|
|
809
822
|
log "Status: complete"
|
|
823
|
+
emit_snapshot_complete "pushed"
|
|
810
824
|
exit 0
|
|
811
825
|
else
|
|
812
826
|
log "ERROR: Push failed after $PUSH_RETRIES attempts"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillwiki",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.15",
|
|
4
4
|
"skills": "./",
|
|
5
5
|
"description": "Project-aware Karpathy-style knowledge base for Claude Code: 19 prompt-only skills (wiki-*, proj-*, using-skillwiki) backed by the deterministic `skillwiki` CLI.",
|
|
6
6
|
"author": {
|
package/skills/package.json
CHANGED
|
@@ -87,7 +87,8 @@ After upgrading to skillwiki **≥0.10.1** (or when managed write fails):
|
|
|
87
87
|
1. Run `skillwiki doctor` — checks `vault_sync_pull_helper` and `vault_sync_review_required_journals`.
|
|
88
88
|
2. If pull helper is missing: install `skillwiki@0.10.1+` (helper must resolve from `dist/vault-sync/scripts/`) and/or redeploy vault-sync host install. Last-resort override: `SKILLWIKI_VAULT_SYNC_PULL_HELPER` pointing at `wiki-pull-with-auto-resolve.sh` under host vault-sync `bin/` (macOS Application Support or Linux `~/.local/share/vault-sync/bin`).
|
|
89
89
|
3. If preflight reports `review-required` on a **clean** worktree: `skillwiki sync journal list`, then `skillwiki sync journal clear-stale --dry-run`, then `clear-stale` without dry-run. Managed preflight automatically supersedes a handoff when its `target_oid` is already an ancestor of `HEAD` and Git has no active sequencer or unmerged paths; unrelated dirty WIP is preserved. When the same incident also left a dead-owner managed-write lock, that preflight reclaims it with a recovery record in the same invocation. Live owners, active sequencers/unmerged paths, missing/non-ancestor targets, and remaining review-required journals still fail closed.
|
|
90
|
-
4.
|
|
90
|
+
4. **Protected snapshotter stale-journal cleanup (v0.10.14+):** On a known protected snapshotter (sg01) where `sync journal clear-stale` is blocked by `PROTECTED_SNAPSHOTTER_WRITE_BLOCKED`, use the attended one-shot maintenance authority instead: `skillwiki snapshot-maintenance journal clear-stale <snapshot-worktree> --dry-run --reason "<operator reason>"` to produce a state-bound approval ID, then `skillwiki snapshot-maintenance journal clear-stale <snapshot-worktree> --approve <id> --reason "<same reason>"` on an attended TTY to execute. This requires the exact configured snapshot worktree, the production snapshot flock, and recomputes the plan under the flock; it is the only allowlisted mutation across the protected boundary. No generic force flag or env bypass exists.
|
|
91
|
+
5. After `skillwiki update` across 0.10.1, read the printed Migration 0.10.1 notes.
|
|
91
92
|
|
|
92
93
|
Also mirror these pointers in vault-presync / vault-sync-status skills when operating pull/push.
|
|
93
94
|
|
|
@@ -87,7 +87,8 @@ After upgrading to skillwiki **≥0.10.1** (or when managed write fails):
|
|
|
87
87
|
1. Run `skillwiki doctor` — checks `vault_sync_pull_helper` and `vault_sync_review_required_journals`.
|
|
88
88
|
2. If pull helper is missing: install `skillwiki@0.10.1+` (helper must resolve from `dist/vault-sync/scripts/`) and/or redeploy vault-sync host install. Last-resort override: `SKILLWIKI_VAULT_SYNC_PULL_HELPER` pointing at `wiki-pull-with-auto-resolve.sh` under host vault-sync `bin/` (macOS Application Support or Linux `~/.local/share/vault-sync/bin`).
|
|
89
89
|
3. If preflight reports `review-required` on a **clean** worktree: `skillwiki sync journal list`, then `skillwiki sync journal clear-stale --dry-run`, then `clear-stale` without dry-run. Managed preflight automatically supersedes a handoff when its `target_oid` is already an ancestor of `HEAD` and Git has no active sequencer or unmerged paths; unrelated dirty WIP is preserved. When the same incident also left a dead-owner managed-write lock, that preflight reclaims it with a recovery record in the same invocation. Live owners, active sequencers/unmerged paths, missing/non-ancestor targets, and remaining review-required journals still fail closed.
|
|
90
|
-
4.
|
|
90
|
+
4. **Protected snapshotter stale-journal cleanup (v0.10.14+):** On a known protected snapshotter (sg01) where `sync journal clear-stale` is blocked by `PROTECTED_SNAPSHOTTER_WRITE_BLOCKED`, use the attended one-shot maintenance authority instead: `skillwiki snapshot-maintenance journal clear-stale <snapshot-worktree> --dry-run --reason "<operator reason>"` to produce a state-bound approval ID, then `skillwiki snapshot-maintenance journal clear-stale <snapshot-worktree> --approve <id> --reason "<same reason>"` on an attended TTY to execute. This requires the exact configured snapshot worktree, the production snapshot flock, and recomputes the plan under the flock; it is the only allowlisted mutation across the protected boundary. No generic force flag or env bypass exists.
|
|
91
|
+
5. After `skillwiki update` across 0.10.1, read the printed Migration 0.10.1 notes.
|
|
91
92
|
|
|
92
93
|
Also mirror these pointers in vault-presync / vault-sync-status skills when operating pull/push.
|
|
93
94
|
|