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.
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-YYWGP7DN.js";
86
+ } from "./chunk-2AOOMDZ7.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-3ZVZ2YEE.js";
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-TYN2IHBY.js";
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 join36 } from "path";
152
+ import { join as join37 } from "path";
147
153
  import { Command } from "commander";
148
154
 
149
155
  // src/utils/output.ts
@@ -2247,6 +2253,9 @@ function writeWorkJournal(vault, opId, fields) {
2247
2253
  writeFileSync3(tmp, body, "utf8");
2248
2254
  renameSync(tmp, path);
2249
2255
  }
2256
+ var DEFAULT_DEPS = {
2257
+ writeEvidenceText: atomicWriteText
2258
+ };
2250
2259
  var PHASE_ORDER = ["validate", "evidence", "log", "projection", "commit", "done"];
2251
2260
  function resolveWorkDir(vault, workItem) {
2252
2261
  const rel = normalizeWorkItemRel(workItem);
@@ -2301,7 +2310,7 @@ ${body}`);
2301
2310
  async function setStatusCompleted(filePath) {
2302
2311
  return patchFrontmatterStatus(filePath);
2303
2312
  }
2304
- async function writeEvidence(workDir, opId, phases) {
2313
+ async function writeEvidence(workDir, opId, phases, writeText) {
2305
2314
  const path = join16(workDir, "evidence.md");
2306
2315
  const body = [
2307
2316
  "---",
@@ -2317,7 +2326,7 @@ async function writeEvidence(workDir, opId, phases) {
2317
2326
  `- completed_at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
2318
2327
  ""
2319
2328
  ].join("\n");
2320
- return atomicWriteText(path, body);
2329
+ return writeText(path, body);
2321
2330
  }
2322
2331
  async function markPlanComplete(workDir) {
2323
2332
  const planPath = join16(workDir, "plan.md");
@@ -2330,7 +2339,7 @@ function writeFailure(stage, result) {
2330
2339
  result: result.ok ? err("WRITE_FAILED", { stage, message: "unexpected ok" }) : result
2331
2340
  };
2332
2341
  }
2333
- async function runWorkComplete(input) {
2342
+ async function runWorkComplete(input, deps = DEFAULT_DEPS) {
2334
2343
  const workDirResult = resolveWorkDir(input.vault, input.workItem);
2335
2344
  if (!workDirResult.ok) {
2336
2345
  return {
@@ -2411,7 +2420,12 @@ async function runWorkComplete(input) {
2411
2420
  if (!statusWrite.ok) return writeFailure("evidence-spec", statusWrite);
2412
2421
  const planWrite = await markPlanComplete(workDir);
2413
2422
  if (!planWrite.ok) return writeFailure("evidence-plan", planWrite);
2414
- const evidenceWrite = await writeEvidence(workDir, opId, completedPhases);
2423
+ const evidenceWrite = await writeEvidence(
2424
+ workDir,
2425
+ opId,
2426
+ completedPhases,
2427
+ deps.writeEvidenceText
2428
+ );
2415
2429
  if (!evidenceWrite.ok) return writeFailure("evidence", evidenceWrite);
2416
2430
  if (input.failAfter === "evidence") {
2417
2431
  throw new Error("simulated failure after evidence");
@@ -2442,7 +2456,12 @@ async function runWorkComplete(input) {
2442
2456
  }
2443
2457
  if (phaseIndex(phase) <= phaseIndex("projection")) {
2444
2458
  if (!existsSync4(join16(workDir, "evidence.md"))) {
2445
- const evidenceWrite = await writeEvidence(workDir, opId, completedPhases);
2459
+ const evidenceWrite = await writeEvidence(
2460
+ workDir,
2461
+ opId,
2462
+ completedPhases,
2463
+ deps.writeEvidenceText
2464
+ );
2446
2465
  if (!evidenceWrite.ok) return writeFailure("projection-evidence", evidenceWrite);
2447
2466
  }
2448
2467
  const finalCheck = await runWorkValidate({
@@ -2649,7 +2668,30 @@ function classifyLog(path, id, label, okPattern) {
2649
2668
  if (okPattern.test(last)) return { id, label, status: "pass", detail: last.slice(0, 120) };
2650
2669
  return { id, label, status: "warn", detail: last.slice(0, 120) };
2651
2670
  }
2652
- function runVaultSyncHealth(home, syncMode) {
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) {
2653
2695
  if (syncMode === "off") {
2654
2696
  return {
2655
2697
  status: "pass",
@@ -2678,6 +2720,30 @@ function runVaultSyncHealth(home, syncMode) {
2678
2720
  }]
2679
2721
  };
2680
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
+ }
2681
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}` });
2682
2748
  if (isMac) {
2683
2749
  const pushPlist = join17(home, "Library", "LaunchAgents", "com.karlchow.wiki-push.plist");
@@ -4698,7 +4764,7 @@ import { createHash as createHash4 } from "crypto";
4698
4764
  import { realpathSync } from "fs";
4699
4765
  import { readFile as readFile12 } from "fs/promises";
4700
4766
  import { join as join26, resolve as resolve3 } from "path";
4701
- var DEFAULT_DEPS = {
4767
+ var DEFAULT_DEPS2 = {
4702
4768
  afterStage: async () => void 0,
4703
4769
  preflight: (input) => runManagedWritePreflight(input)
4704
4770
  };
@@ -5018,7 +5084,7 @@ async function previewPreparedPagePublication(input, vault) {
5018
5084
  true
5019
5085
  );
5020
5086
  }
5021
- async function publishPreparedPage(input, vault, deps = DEFAULT_DEPS) {
5087
+ async function publishPreparedPage(input, vault, deps = DEFAULT_DEPS2) {
5022
5088
  const managed = acquireManagedWriteLock(vault, `page publish ${input.page.target}`);
5023
5089
  if (!managed.ok) return { exitCode: errorExitCode(managed.error), result: managed };
5024
5090
  try {
@@ -5173,7 +5239,7 @@ async function publishPreparedPage(input, vault, deps = DEFAULT_DEPS) {
5173
5239
  releaseManagedWriteLock(managed.data);
5174
5240
  }
5175
5241
  }
5176
- async function runPagePublish(input, deps = DEFAULT_DEPS) {
5242
+ async function runPagePublish(input, deps = DEFAULT_DEPS2) {
5177
5243
  const prepared = await preparePagePublication(input);
5178
5244
  if (!prepared.ok) return { exitCode: errorExitCode(prepared.error), result: prepared };
5179
5245
  if (!input.write) return previewPreparedPagePublication(prepared.data, input.vault);
@@ -6490,9 +6556,352 @@ function runSyncJournalClearStale(input) {
6490
6556
  };
6491
6557
  }
6492
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
+
6493
6902
  // src/commands/backup.ts
6494
- import { statSync as statSync2, readdirSync as readdirSync2, readFileSync as readFileSync13, mkdirSync as mkdirSync3, writeFileSync as writeFileSync5 } from "fs";
6495
- import { join as join30, relative as relative2, dirname as dirname6 } from "path";
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";
6496
6905
  import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
6497
6906
 
6498
6907
  // src/utils/s3-client.ts
@@ -6516,7 +6925,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node_
6516
6925
  function* walkMarkdown(dir, base) {
6517
6926
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
6518
6927
  if (SKIP_DIRS.has(entry.name)) continue;
6519
- const full = join30(dir, entry.name);
6928
+ const full = join31(dir, entry.name);
6520
6929
  if (entry.isDirectory()) {
6521
6930
  yield* walkMarkdown(full, base);
6522
6931
  } else if (entry.name.endsWith(".md")) {
@@ -6539,7 +6948,7 @@ async function runBackupSync(input) {
6539
6948
  let failed = 0;
6540
6949
  const files = [...walkMarkdown(input.vault, input.vault)];
6541
6950
  for (const relPath of files) {
6542
- const absPath = join30(input.vault, relPath);
6951
+ const absPath = join31(input.vault, relPath);
6543
6952
  const localStat = statSync2(absPath);
6544
6953
  let needsUpload = true;
6545
6954
  try {
@@ -6558,7 +6967,7 @@ async function runBackupSync(input) {
6558
6967
  continue;
6559
6968
  }
6560
6969
  try {
6561
- const body = readFileSync13(absPath);
6970
+ const body = readFileSync14(absPath);
6562
6971
  await client.send(new PutObjectCommand({ Bucket: input.bucket, Key: relPath, Body: body }));
6563
6972
  uploaded++;
6564
6973
  } catch {
@@ -6615,7 +7024,7 @@ async function runBackupRestore(input) {
6615
7024
  const objects = list.Contents ?? [];
6616
7025
  for (const obj of objects) {
6617
7026
  if (!obj.Key) continue;
6618
- const localPath = join30(target, obj.Key);
7027
+ const localPath = join31(target, obj.Key);
6619
7028
  try {
6620
7029
  const localStat = statSync2(localPath);
6621
7030
  if (obj.LastModified && localStat.mtime > obj.LastModified) {
@@ -6628,7 +7037,7 @@ async function runBackupRestore(input) {
6628
7037
  const resp = await client.send(new GetObjectCommand({ Bucket: input.bucket, Key: obj.Key }));
6629
7038
  const body = await resp.Body?.transformToByteArray();
6630
7039
  if (body) {
6631
- mkdirSync3(dirname6(localPath), { recursive: true });
7040
+ mkdirSync4(dirname6(localPath), { recursive: true });
6632
7041
  writeFileSync5(localPath, Buffer.from(body));
6633
7042
  downloaded++;
6634
7043
  }
@@ -6661,11 +7070,11 @@ async function runBackupRestore(input) {
6661
7070
  }
6662
7071
 
6663
7072
  // src/commands/status.ts
6664
- import { existsSync as existsSync9, statSync as statSync3 } from "fs";
7073
+ import { existsSync as existsSync10, statSync as statSync3 } from "fs";
6665
7074
  import { readFile as readFile15 } from "fs/promises";
6666
- import { join as join31 } from "path";
7075
+ import { join as join32 } from "path";
6667
7076
  async function runStatus(input) {
6668
- if (!existsSync9(input.vault)) {
7077
+ if (!existsSync10(input.vault)) {
6669
7078
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
6670
7079
  }
6671
7080
  const scan = await scanVault(input.vault);
@@ -6690,7 +7099,7 @@ async function runStatus(input) {
6690
7099
  const compound = scan.data.compound.length;
6691
7100
  let schemaVersion = "v1";
6692
7101
  try {
6693
- const schemaContent = await readFile15(join31(input.vault, "SCHEMA.md"), "utf8");
7102
+ const schemaContent = await readFile15(join32(input.vault, "SCHEMA.md"), "utf8");
6694
7103
  const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
6695
7104
  if (versionMatch) schemaVersion = versionMatch[1];
6696
7105
  } catch {
@@ -6751,7 +7160,7 @@ async function runStatus(input) {
6751
7160
 
6752
7161
  // src/commands/seed.ts
6753
7162
  import { mkdir as mkdir10, writeFile as writeFile9, stat as stat4 } from "fs/promises";
6754
- import { join as join32 } from "path";
7163
+ import { join as join33 } from "path";
6755
7164
  var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6756
7165
  var EXAMPLE_PAGES = {
6757
7166
  "entities/example-project.md": `---
@@ -6820,29 +7229,29 @@ Real sources are immutable after ingestion \u2014 never edit them.
6820
7229
  `;
6821
7230
  async function runSeed(input) {
6822
7231
  try {
6823
- await stat4(join32(input.vault, "SCHEMA.md"));
7232
+ await stat4(join33(input.vault, "SCHEMA.md"));
6824
7233
  } catch {
6825
7234
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
6826
7235
  }
6827
7236
  const created = [];
6828
7237
  const skipped = [];
6829
7238
  for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
6830
- const absPath = join32(input.vault, relPath);
7239
+ const absPath = join33(input.vault, relPath);
6831
7240
  try {
6832
7241
  await stat4(absPath);
6833
7242
  skipped.push(relPath);
6834
7243
  } catch {
6835
- await mkdir10(join32(absPath, ".."), { recursive: true });
7244
+ await mkdir10(join33(absPath, ".."), { recursive: true });
6836
7245
  await writeFile9(absPath, content, "utf8");
6837
7246
  created.push(relPath);
6838
7247
  }
6839
7248
  }
6840
- const rawPath = join32(input.vault, "raw", "articles", "example-source.md");
7249
+ const rawPath = join33(input.vault, "raw", "articles", "example-source.md");
6841
7250
  try {
6842
7251
  await stat4(rawPath);
6843
7252
  skipped.push("raw/articles/example-source.md");
6844
7253
  } catch {
6845
- await mkdir10(join32(rawPath, ".."), { recursive: true });
7254
+ await mkdir10(join33(rawPath, ".."), { recursive: true });
6846
7255
  await writeFile9(rawPath, EXAMPLE_RAW, "utf8");
6847
7256
  created.push("raw/articles/example-source.md");
6848
7257
  }
@@ -6866,8 +7275,8 @@ async function runSeed(input) {
6866
7275
 
6867
7276
  // src/commands/canvas.ts
6868
7277
  import { readFile as readFile16, writeFile as writeFile10 } from "fs/promises";
6869
- import { existsSync as existsSync10 } from "fs";
6870
- import { join as join33 } from "path";
7278
+ import { existsSync as existsSync11 } from "fs";
7279
+ import { join as join34 } from "path";
6871
7280
  var NODE_WIDTH = 240;
6872
7281
  var NODE_HEIGHT = 60;
6873
7282
  var COLUMN_SPACING = 400;
@@ -6945,8 +7354,8 @@ function buildCanvasEdges(adjacency) {
6945
7354
  return edges;
6946
7355
  }
6947
7356
  async function runCanvasGenerate(input) {
6948
- const graphPath = input.graphPath ?? join33(input.vault, ".skillwiki", "graph.json");
6949
- if (!existsSync10(graphPath)) {
7357
+ const graphPath = input.graphPath ?? join34(input.vault, ".skillwiki", "graph.json");
7358
+ if (!existsSync11(graphPath)) {
6950
7359
  return {
6951
7360
  exitCode: ExitCode.FILE_NOT_FOUND,
6952
7361
  result: err("FILE_NOT_FOUND", {
@@ -6983,7 +7392,7 @@ async function runCanvasGenerate(input) {
6983
7392
  const nodes = buildCanvasNodes(paths);
6984
7393
  const edges = buildCanvasEdges(graph.adjacency);
6985
7394
  const canvas = { nodes, edges };
6986
- const outPath = join33(input.vault, "vault-graph.canvas");
7395
+ const outPath = join34(input.vault, "vault-graph.canvas");
6987
7396
  try {
6988
7397
  await writeFile10(outPath, JSON.stringify(canvas, null, 2));
6989
7398
  } catch (e) {
@@ -7005,10 +7414,10 @@ written: ${outPath}`
7005
7414
  }
7006
7415
 
7007
7416
  // src/commands/fleet-health.ts
7008
- import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
7417
+ import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
7009
7418
  import { execSync as nodeExecSync } from "child_process";
7010
7419
  import { hostname as nodeHostname, platform as nodePlatform } from "os";
7011
- import { join as join34 } from "path";
7420
+ import { join as join35 } from "path";
7012
7421
  var SSH_TIMEOUT_MS = 15e3;
7013
7422
  var TIMER_UNIT = "agent-memory-trends.timer";
7014
7423
  var SERVICE_UNIT = "agent-memory-trends.service";
@@ -7105,9 +7514,9 @@ function applyServiceFailedOverlay(run, serviceFailed) {
7105
7514
  function probeLocal(vaultPath, deps) {
7106
7515
  const latestPath = satelliteLatestRunPath(vaultPath);
7107
7516
  let parsed = null;
7108
- if (existsSync11(latestPath)) {
7517
+ if (existsSync12(latestPath)) {
7109
7518
  try {
7110
- const wire = readSatelliteLatestRunFromText(readFileSync14(latestPath, "utf8"));
7519
+ const wire = readSatelliteLatestRunFromText(readFileSync15(latestPath, "utf8"));
7111
7520
  if (wire) {
7112
7521
  parsed = {
7113
7522
  status: wire.status,
@@ -7224,12 +7633,12 @@ function formatTable(rows) {
7224
7633
  );
7225
7634
  return [header, ...lines].join("\n");
7226
7635
  }
7227
- function rowHealthy(reachable, timer, runUnhealthy, timerBad, platform2) {
7636
+ function rowHealthy(reachable, timer, runUnhealthy, timerBad, platform3) {
7228
7637
  if (reachable === "no-access") return true;
7229
7638
  if (reachable === "no") return false;
7230
7639
  if (runUnhealthy) return false;
7231
- if (platform2 === "linux" && timerBad) return false;
7232
- if (platform2 === "linux" && timer === "inactive") return false;
7640
+ if (platform3 === "linux" && timerBad) return false;
7641
+ if (platform3 === "linux" && timer === "inactive") return false;
7233
7642
  return true;
7234
7643
  }
7235
7644
  async function runFleetHealth(input) {
@@ -7238,7 +7647,7 @@ async function runFleetHealth(input) {
7238
7647
  const home = input.home ?? env.HOME ?? "";
7239
7648
  const osHostname = input.osHostname ?? env.HOSTNAME ?? nodeHostname();
7240
7649
  const vault = input.vault ?? env.WIKI_PATH;
7241
- const file = input.file ?? (vault ? join34(vault, FLEET_REL_PATH) : void 0);
7650
+ const file = input.file ?? (vault ? join35(vault, FLEET_REL_PATH) : void 0);
7242
7651
  if (!file) {
7243
7652
  return {
7244
7653
  exitCode: ExitCode.NO_VAULT_CONFIGURED,
@@ -7322,15 +7731,15 @@ async function runFleetHealth(input) {
7322
7731
  }
7323
7732
 
7324
7733
  // src/utils/auto-commit.ts
7325
- import { existsSync as existsSync12 } from "fs";
7326
- import { join as join35 } from "path";
7734
+ import { existsSync as existsSync13 } from "fs";
7735
+ import { join as join36 } from "path";
7327
7736
  async function postCommit(vault, exitCode) {
7328
7737
  if (exitCode !== 0) return;
7329
7738
  const home = process.env.HOME ?? "";
7330
7739
  const dotenv = await parseDotenvFile(configPath(home));
7331
7740
  const autoCommit = process.env.AUTO_COMMIT ?? dotenv["AUTO_COMMIT"];
7332
7741
  if (autoCommit === "false") return;
7333
- if (!existsSync12(join35(vault, ".git"))) return;
7742
+ if (!existsSync13(join36(vault, ".git"))) return;
7334
7743
  const lastOps = readLastOp(vault);
7335
7744
  if (lastOps.length === 0) return;
7336
7745
  const porcelain = git(vault, ["status", "--porcelain", "--", ...VAULT_COMMIT_PATHSPEC]);
@@ -7354,9 +7763,9 @@ async function postCommit(vault, exitCode) {
7354
7763
  }
7355
7764
 
7356
7765
  // src/commands/write-preflight.ts
7357
- import { existsSync as existsSync13, readFileSync as readFileSync15, statSync as statSync4 } from "fs";
7766
+ import { existsSync as existsSync14, readFileSync as readFileSync16, statSync as statSync4 } from "fs";
7358
7767
  async function runWritePreflightCommand(input) {
7359
- if (!existsSync13(input.vault) || !statSync4(input.vault).isDirectory()) {
7768
+ if (!existsSync14(input.vault) || !statSync4(input.vault).isDirectory()) {
7360
7769
  return {
7361
7770
  exitCode: ExitCode.VAULT_PATH_INVALID,
7362
7771
  result: err(GateError.VAULT_PATH_INVALID, { path: input.vault })
@@ -7364,13 +7773,13 @@ async function runWritePreflightCommand(input) {
7364
7773
  }
7365
7774
  let priorText = input.priorArtifactText;
7366
7775
  if (input.priorArtifactFile) {
7367
- if (!existsSync13(input.priorArtifactFile)) {
7776
+ if (!existsSync14(input.priorArtifactFile)) {
7368
7777
  return {
7369
7778
  exitCode: ExitCode.FILE_NOT_FOUND,
7370
7779
  result: err("FILE_NOT_FOUND", { path: input.priorArtifactFile })
7371
7780
  };
7372
7781
  }
7373
- priorText = readFileSync15(input.priorArtifactFile, "utf8");
7782
+ priorText = readFileSync16(input.priorArtifactFile, "utf8");
7374
7783
  }
7375
7784
  const checkList = input.checks ? input.checks.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
7376
7785
  const result = runWritePreflight({
@@ -7484,7 +7893,7 @@ async function emitManagedVaultWrite(vault, command, mutate, opts) {
7484
7893
  if (dirty) {
7485
7894
  return emit(dirty, void 0, { postCommit: false });
7486
7895
  }
7487
- const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-7TWSEZJZ.js");
7896
+ const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-M2HL6DDY.js");
7488
7897
  const run = await runManagedWriteTransaction2({
7489
7898
  vault,
7490
7899
  command,
@@ -7509,7 +7918,7 @@ program.command("validate <file>").description("validate vault page frontmatter
7509
7918
  emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
7510
7919
  });
7511
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) => {
7512
- const out = opts.out ?? join36(vault, ".skillwiki", "graph.json");
7921
+ const out = opts.out ?? join37(vault, ".skillwiki", "graph.json");
7513
7922
  return emitGuardedVaultWrite(vault, "graph build", () => runGraphBuild({ vault, out }));
7514
7923
  });
7515
7924
  var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
@@ -8119,6 +8528,21 @@ syncJournalCmd.command("clear-stale [vault]").description("supersede stale revie
8119
8528
  );
8120
8529
  }
8121
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
+ });
8122
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) => {
8123
8547
  const v = await resolveVaultArg(vault, opts.wiki);
8124
8548
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });