skillwiki 0.10.16 → 0.10.18

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.
@@ -3,8 +3,9 @@ import {
3
3
  ExitCode,
4
4
  FleetManifestSchema,
5
5
  err,
6
+ isVaultSyncKey,
6
7
  ok
7
- } from "./chunk-C5OLZRRM.js";
8
+ } from "./chunk-Y6KRDGI2.js";
8
9
 
9
10
  // src/utils/dotenv.ts
10
11
  import { readFile, writeFile, mkdir } from "fs/promises";
@@ -40,7 +41,7 @@ function parseDotenvText(text) {
40
41
  if (eq <= 0) continue;
41
42
  const key = line.slice(0, eq).trim();
42
43
  const value = line.slice(eq + 1).trim();
43
- if (!_whitelist.has(key) && !isValidWikiProfileKey(key)) continue;
44
+ if (!_whitelist.has(key) && !isValidWikiProfileKey(key) && !isVaultSyncKey(key)) continue;
44
45
  if (value.length === 0) continue;
45
46
  out[key] = value;
46
47
  }
@@ -866,6 +867,45 @@ async function runVaultSyncPullHelper(input) {
866
867
  });
867
868
  }
868
869
 
870
+ // src/utils/snapshot-worktree.ts
871
+ import { readFileSync as readFileSync2 } from "fs";
872
+ import { join as join4, resolve } from "path";
873
+ function readSkillWikiConfig(path) {
874
+ try {
875
+ return parseDotenvText(readFileSync2(path, "utf8"));
876
+ } catch {
877
+ return {};
878
+ }
879
+ }
880
+ function readSnapshotProfile(path) {
881
+ const allowed = /* @__PURE__ */ new Set(["WIKI_GIT_WORKTREE", "SNAPSHOT_WORKTREE", "GIT_DIR"]);
882
+ const profile = {};
883
+ try {
884
+ for (const rawLine of readFileSync2(path, "utf8").split(/\r?\n/)) {
885
+ const line = rawLine.trim();
886
+ if (line.length === 0 || line.startsWith("#")) continue;
887
+ const eq = line.indexOf("=");
888
+ if (eq <= 0) continue;
889
+ const key = line.slice(0, eq).trim();
890
+ const value = line.slice(eq + 1).trim();
891
+ if (allowed.has(key) && value.length > 0) profile[key] = value;
892
+ }
893
+ } catch {
894
+ }
895
+ return profile;
896
+ }
897
+ function resolveConfiguredSnapshotWorktree(home) {
898
+ if (!home) return void 0;
899
+ const config = readSkillWikiConfig(join4(home, ".skillwiki", ".env"));
900
+ const explicit = config["vault_sync.snapshot_worktree"];
901
+ if (explicit) return resolve(explicit);
902
+ const snapshotProfile = config["vault_sync.snapshot_profile"];
903
+ if (!snapshotProfile) return void 0;
904
+ const profile = readSnapshotProfile(resolve(snapshotProfile));
905
+ const fromProfile = profile.WIKI_GIT_WORKTREE ?? profile.SNAPSHOT_WORKTREE ?? profile.GIT_DIR;
906
+ return fromProfile ? resolve(fromProfile) : void 0;
907
+ }
908
+
869
909
  export {
870
910
  CONFIG_KEYS,
871
911
  isValidWikiProfileKey,
@@ -896,5 +936,6 @@ export {
896
936
  markJournalSuperseded,
897
937
  supersedeStaleReviewRequiredJournals,
898
938
  resolveVaultSyncPullHelper,
899
- runVaultSyncPullHelper
939
+ runVaultSyncPullHelper,
940
+ resolveConfiguredSnapshotWorktree
900
941
  };
@@ -19,7 +19,7 @@ import {
19
19
  splitFrontmatter,
20
20
  vaultIoConcurrency,
21
21
  writeRootIndexProjection
22
- } from "./chunk-NMUYMNNB.js";
22
+ } from "./chunk-IGCW3DR2.js";
23
23
  import {
24
24
  CONFIG_KEYS,
25
25
  git,
@@ -29,11 +29,12 @@ import {
29
29
  parseDotenvFile,
30
30
  parseDotenvText,
31
31
  profileKey,
32
+ resolveConfiguredSnapshotWorktree,
32
33
  resolveVaultSyncPullHelper,
33
34
  satelliteGateFromFleetLoad,
34
35
  snapshotterAliasForLocalHost,
35
36
  writeDotenv
36
- } from "./chunk-OWPQJOFG.js";
37
+ } from "./chunk-GNS2ZV5P.js";
37
38
  import {
38
39
  CompoundSchema,
39
40
  ExitCode,
@@ -44,8 +45,11 @@ import {
44
45
  detectSchema,
45
46
  err,
46
47
  getErrorMessage,
47
- ok
48
- } from "./chunk-C5OLZRRM.js";
48
+ isVaultSyncKey,
49
+ ok,
50
+ parseVaultSyncKeyValue,
51
+ systemdPropertyFor
52
+ } from "./chunk-Y6KRDGI2.js";
49
53
 
50
54
  // src/commands/log-append.ts
51
55
  import { readFile as readFile2, stat } from "fs/promises";
@@ -4774,7 +4778,7 @@ import { readFile as readFile15 } from "fs/promises";
4774
4778
  import { existsSync as existsSync7 } from "fs";
4775
4779
  import { join as join19 } from "path";
4776
4780
  function validateKey(key) {
4777
- return CONFIG_KEYS.includes(key) || isValidWikiProfileKey(key);
4781
+ return CONFIG_KEYS.includes(key) || isValidWikiProfileKey(key) || isVaultSyncKey(key);
4778
4782
  }
4779
4783
  function configPath(home) {
4780
4784
  return join19(home, ".skillwiki", ".env");
@@ -4791,6 +4795,12 @@ async function runConfigSet(input) {
4791
4795
  if (!validateKey(input.key)) {
4792
4796
  return { exitCode: ExitCode.INVALID_CONFIG_KEY, result: err("INVALID_CONFIG_KEY", { key: input.key }) };
4793
4797
  }
4798
+ if (isVaultSyncKey(input.key)) {
4799
+ const parsed = parseVaultSyncKeyValue(input.key, input.value);
4800
+ if (!parsed.ok) {
4801
+ return { exitCode: ExitCode.INVALID_CONFIG_VALUE, result: err("INVALID_CONFIG_VALUE", { key: input.key, value: input.value, message: parsed.message }) };
4802
+ }
4803
+ }
4794
4804
  const filePath = configPath(input.home);
4795
4805
  try {
4796
4806
  let originalContent;
@@ -6473,8 +6483,6 @@ function readVaultSyncConfig(home) {
6473
6483
  let role;
6474
6484
  let serviceScope;
6475
6485
  let snapshotScript;
6476
- let snapshotProfile;
6477
- let snapshotWorktree;
6478
6486
  for (const line of content.split(/\r?\n/)) {
6479
6487
  const trimmed = line.trim();
6480
6488
  if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
@@ -6487,58 +6495,18 @@ function readVaultSyncConfig(home) {
6487
6495
  if (k === "vault_sync.role") role = v;
6488
6496
  if (k === "vault_sync.service_scope") serviceScope = v;
6489
6497
  if (k === "vault_sync.snapshot_script") snapshotScript = v;
6490
- if (k === "vault_sync.snapshot_profile") snapshotProfile = v;
6491
- if (k === "vault_sync.snapshot_worktree") snapshotWorktree = v;
6492
6498
  }
6493
- return { installed, role, serviceScope, snapshotScript, snapshotProfile, snapshotWorktree };
6499
+ return { installed, role, serviceScope, snapshotScript };
6494
6500
  } catch {
6495
6501
  return { installed: false };
6496
6502
  }
6497
6503
  }
6498
- function readKeyFromEnvFile(path, keys) {
6499
- try {
6500
- const content = readFileSync10(path, "utf8");
6501
- for (const line of content.split(/\r?\n/)) {
6502
- const trimmed = line.trim();
6503
- if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
6504
- const eq = trimmed.indexOf("=");
6505
- if (eq <= 0) continue;
6506
- const key = trimmed.slice(0, eq).trim();
6507
- if (!keys.includes(key)) continue;
6508
- const value = trimmed.slice(eq + 1).trim();
6509
- if (value.length > 0) return value;
6510
- }
6511
- } catch {
6512
- }
6513
- return void 0;
6514
- }
6515
- function resolveSnapshotGitWorktree(config) {
6516
- if (config.snapshotWorktree) return config.snapshotWorktree;
6517
- if (config.snapshotProfile) {
6518
- const fromProfile = readKeyFromEnvFile(config.snapshotProfile, ["WIKI_GIT_WORKTREE", "SNAPSHOT_WORKTREE", "GIT_DIR"]);
6519
- if (fromProfile) return fromProfile;
6520
- }
6504
+ function resolveSnapshotGitWorktree(home) {
6505
+ const configured = resolveConfiguredSnapshotWorktree(home);
6506
+ if (configured) return configured;
6521
6507
  const defaultPath = "/root/wiki-git";
6522
6508
  return existsSync13(defaultPath) ? defaultPath : void 0;
6523
6509
  }
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
6510
  function normalizeSystemdValue(raw) {
6543
6511
  if (raw == null) return void 0;
6544
6512
  const v = raw.trim();
@@ -6577,7 +6545,7 @@ function snapshotProp(kind, prop, fixture, scope) {
6577
6545
  return v == null ? void 0 : String(v);
6578
6546
  }
6579
6547
  const unit = kind === "timer" ? "wiki-snapshot.timer" : "wiki-snapshot.service";
6580
- const liveProp = semanticToSystemdProp(prop);
6548
+ const liveProp = systemdPropertyFor(prop);
6581
6549
  if (!liveProp) return void 0;
6582
6550
  return systemctlShowProperty(scope, unit, liveProp);
6583
6551
  }
@@ -6594,7 +6562,8 @@ function snapshotterHealthChecks(scope, logDir, env) {
6594
6562
  const fixture = loadSnapshotFixture(env);
6595
6563
  const cadence = fixture ? fixture.cadence_minutes : parseInt(env.VS_SNAPSHOT_CADENCE_MINUTES ?? "30", 10) || 30;
6596
6564
  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();
6565
+ const injectedNowMs = env.VS_SNAPSHOT_HEALTH_NOW ? Date.parse(env.VS_SNAPSHOT_HEALTH_NOW) : Number.NaN;
6566
+ const nowMs = fixture ? Date.parse(fixture.now) : Number.isFinite(injectedNowMs) ? injectedNowMs : Date.now();
6598
6567
  const warnAge = cadence * 2 + 15;
6599
6568
  const errorAge = cadence * 4 + 15;
6600
6569
  const tUnitfile = snapshotProp("timer", "unit_file_state", fixture, scope);
@@ -6781,7 +6750,7 @@ function vaultSyncChecks(input) {
6781
6750
  execSync2(`launchctl print gui/${uid}/com.karlchow.wiki-push`, {
6782
6751
  encoding: "utf8",
6783
6752
  timeout: 2e3,
6784
- stdio: ["pipe", "pipe", "pipe"]
6753
+ stdio: ["pipe", "ignore", "ignore"]
6785
6754
  });
6786
6755
  c2 = check(
6787
6756
  "pass",
@@ -7134,7 +7103,7 @@ async function runDoctor(input) {
7134
7103
  checks.push(check("error", "wiki_path_set", "WIKI_PATH configured", "No vault configured. Run `skillwiki init` or pass --vault."));
7135
7104
  }
7136
7105
  const resolvedPath = resolved.ok ? resolved.data.path : void 0;
7137
- const gitCheckPath = vsConfig.role === "snapshotter" ? resolveSnapshotGitWorktree(vsConfig) ?? resolvedPath : resolvedPath;
7106
+ const gitCheckPath = vsConfig.role === "snapshotter" ? resolveSnapshotGitWorktree(input.home) ?? resolvedPath : resolvedPath;
7138
7107
  checks.push(checkWikiPathExists(resolvedPath));
7139
7108
  checks.push(checkVaultStructure(resolvedPath));
7140
7109
  checks.push(checkObsidianTemplates(resolvedPath));
@@ -6,7 +6,7 @@ import {
6
6
  err,
7
7
  getErrorMessage,
8
8
  ok
9
- } from "./chunk-C5OLZRRM.js";
9
+ } from "./chunk-Y6KRDGI2.js";
10
10
 
11
11
  // src/utils/index-projection.ts
12
12
  import { readFile as readFile4 } from "fs/promises";
@@ -6,17 +6,18 @@ import {
6
6
  hasActiveGitSequencer,
7
7
  hasUnmergedPaths,
8
8
  loadFleetManifestAndHost,
9
+ resolveConfiguredSnapshotWorktree,
9
10
  runVaultSyncPullHelper,
10
11
  supersedeStaleReviewRequiredJournals
11
- } from "./chunk-OWPQJOFG.js";
12
+ } from "./chunk-GNS2ZV5P.js";
12
13
  import {
13
14
  ExitCode,
14
15
  err,
15
16
  ok
16
- } from "./chunk-C5OLZRRM.js";
17
+ } from "./chunk-Y6KRDGI2.js";
17
18
 
18
19
  // src/utils/managed-write-preflight.ts
19
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
20
+ import { existsSync as existsSync2 } from "fs";
20
21
  import { join as join2, resolve } from "path";
21
22
 
22
23
  // src/utils/managed-write-lock.ts
@@ -149,7 +150,8 @@ function releaseManagedWriteLock(handle) {
149
150
 
150
151
  // src/utils/managed-write-preflight.ts
151
152
  var DEFAULT_DEPS = {
152
- converge: (input) => runVaultSyncPullHelper(input)
153
+ converge: (input) => runVaultSyncPullHelper(input),
154
+ resolveConfiguredSnapshotWorktree
153
155
  };
154
156
  function preflightBlocker(vault) {
155
157
  const unmerged = hasUnmergedPaths(vault);
@@ -171,19 +173,17 @@ function preflightBlocker(vault) {
171
173
  if (op) return { reason: "review-required", operation_id: op };
172
174
  return null;
173
175
  }
174
- function fleetManifestBytes(vault) {
175
- const path = join2(vault, FLEET_REL_PATH);
176
- if (!existsSync2(path)) return null;
177
- return readFileSync2(path);
176
+ function hasFleetManifest(vault) {
177
+ return existsSync2(join2(vault, FLEET_REL_PATH));
178
178
  }
179
179
  function isGitVault(vault) {
180
180
  return Boolean(git(vault, ["rev-parse", "--absolute-git-dir"]));
181
181
  }
182
182
  async function runManagedWritePreflight(input, deps = DEFAULT_DEPS) {
183
- const vault = input.vault;
184
- const convergenceVault = input.convergenceVault && resolve(input.convergenceVault) !== resolve(vault) ? resolve(input.convergenceVault) : void 0;
185
- const gitVault = convergenceVault ?? vault;
186
- const mutationBlocker = preflightBlocker(vault);
183
+ const mutationVault = resolve(input.vault);
184
+ let convergenceVault = input.convergenceVault && resolve(input.convergenceVault) !== mutationVault ? resolve(input.convergenceVault) : void 0;
185
+ let convergenceSource = convergenceVault ? "explicit" : "single-path";
186
+ const mutationBlocker = preflightBlocker(mutationVault);
187
187
  if (mutationBlocker) {
188
188
  return {
189
189
  exitCode: ExitCode.PREFLIGHT_FAILED,
@@ -194,46 +194,27 @@ async function runManagedWritePreflight(input, deps = DEFAULT_DEPS) {
194
194
  })
195
195
  };
196
196
  }
197
- if (convergenceVault) {
198
- if (!isGitVault(convergenceVault)) {
199
- return {
200
- exitCode: ExitCode.PREFLIGHT_FAILED,
201
- result: err("PREFLIGHT_FAILED", {
202
- reason: "convergence-vault-not-git",
203
- convergence_vault: convergenceVault
204
- })
205
- };
206
- }
207
- const gitBlocker = preflightBlocker(convergenceVault);
208
- if (gitBlocker) {
209
- return {
210
- exitCode: ExitCode.PREFLIGHT_FAILED,
211
- result: err("PREFLIGHT_FAILED", {
212
- reason: gitBlocker.reason,
213
- operation_id: gitBlocker.operation_id,
214
- unmerged_paths: gitBlocker.unmerged_paths,
215
- convergence_vault: convergenceVault
216
- })
217
- };
218
- }
219
- }
220
197
  const fleet = await loadFleetManifestAndHost({
221
- vault,
198
+ vault: mutationVault,
222
199
  hostId: input.hostId,
223
200
  env: input.env,
224
201
  home: input.home,
225
- osHostname: input.osHostname
202
+ cwd: input.cwd,
203
+ osHostname: input.osHostname,
204
+ user: input.user
226
205
  });
227
- const dualPathMeta = convergenceVault ? { convergence_vault: convergenceVault } : {};
228
206
  if (!fleet) {
229
- const head = git(gitVault, ["rev-parse", "HEAD"]) || null;
207
+ const gitVault2 = isGitVault(mutationVault) ? mutationVault : null;
208
+ const head = gitVault2 ? git(gitVault2, ["rev-parse", "HEAD"]) || null : null;
230
209
  return {
231
210
  exitCode: ExitCode.OK,
232
211
  result: ok({
233
212
  mode: "standalone",
213
+ mutation_vault: mutationVault,
214
+ git_vault: gitVault2,
234
215
  base_oid: head,
235
216
  converged: false,
236
- ...dualPathMeta
217
+ convergence_source: "single-path"
237
218
  })
238
219
  };
239
220
  }
@@ -247,27 +228,6 @@ async function runManagedWritePreflight(input, deps = DEFAULT_DEPS) {
247
228
  })
248
229
  };
249
230
  }
250
- if (convergenceVault && fleetManifestBytes(convergenceVault)) {
251
- const convergeFleetCtx = await loadFleetManifestAndHost({
252
- vault: convergenceVault,
253
- hostId: input.hostId ?? fleet.hostId,
254
- env: input.env,
255
- home: input.home,
256
- osHostname: input.osHostname
257
- });
258
- if (!convergeFleetCtx || convergeFleetCtx.identityStatus !== "known" || convergeFleetCtx.hostId !== fleet.hostId) {
259
- return {
260
- exitCode: ExitCode.PREFLIGHT_FAILED,
261
- result: err("PREFLIGHT_FAILED", {
262
- reason: "convergence-vault-identity-mismatch",
263
- host_id: fleet.hostId,
264
- convergence_host_id: convergeFleetCtx?.hostId,
265
- convergence_identity_status: convergeFleetCtx?.identityStatus,
266
- convergence_vault: convergenceVault
267
- })
268
- };
269
- }
270
- }
271
231
  const host = fleet.manifest.hosts[fleet.hostId];
272
232
  if (!host) {
273
233
  return {
@@ -282,12 +242,101 @@ async function runManagedWritePreflight(input, deps = DEFAULT_DEPS) {
282
242
  result: ok({
283
243
  mode: "immutable-record",
284
244
  host_id: fleet.hostId,
245
+ mutation_vault: mutationVault,
246
+ git_vault: null,
285
247
  base_oid: null,
286
248
  converged: false,
287
- ...dualPathMeta
249
+ ...convergenceVault ? { convergence_vault: convergenceVault } : {},
250
+ convergence_source: convergenceSource
288
251
  })
289
252
  };
290
253
  }
254
+ if (!convergenceVault && host.role === "snapshotter" && host.protected === true) {
255
+ const home = input.home ?? input.env?.HOME ?? process.env.HOME ?? "";
256
+ const configured = (deps.resolveConfiguredSnapshotWorktree ?? resolveConfiguredSnapshotWorktree)(home);
257
+ if (!configured) {
258
+ return {
259
+ exitCode: ExitCode.PREFLIGHT_FAILED,
260
+ result: err("PREFLIGHT_FAILED", {
261
+ reason: "convergence-vault-not-configured",
262
+ host_id: fleet.hostId,
263
+ mutation_vault: mutationVault
264
+ })
265
+ };
266
+ }
267
+ convergenceVault = resolve(configured);
268
+ convergenceSource = "configured";
269
+ if (convergenceVault === mutationVault) {
270
+ return {
271
+ exitCode: ExitCode.PREFLIGHT_FAILED,
272
+ result: err("PREFLIGHT_FAILED", {
273
+ reason: "convergence-vault-not-distinct",
274
+ host_id: fleet.hostId,
275
+ mutation_vault: mutationVault,
276
+ convergence_vault: convergenceVault
277
+ })
278
+ };
279
+ }
280
+ }
281
+ const gitVault = convergenceVault ?? mutationVault;
282
+ if (convergenceVault) {
283
+ if (!isGitVault(convergenceVault)) {
284
+ return {
285
+ exitCode: ExitCode.PREFLIGHT_FAILED,
286
+ result: err("PREFLIGHT_FAILED", {
287
+ reason: "convergence-vault-not-git",
288
+ convergence_vault: convergenceVault
289
+ })
290
+ };
291
+ }
292
+ const convergenceHasFleet = hasFleetManifest(convergenceVault);
293
+ if (convergenceSource === "configured" && !convergenceHasFleet) {
294
+ return {
295
+ exitCode: ExitCode.PREFLIGHT_FAILED,
296
+ result: err("PREFLIGHT_FAILED", {
297
+ reason: "convergence-vault-fleet-missing",
298
+ host_id: fleet.hostId,
299
+ convergence_vault: convergenceVault
300
+ })
301
+ };
302
+ }
303
+ const gitBlocker = preflightBlocker(convergenceVault);
304
+ if (gitBlocker) {
305
+ return {
306
+ exitCode: ExitCode.PREFLIGHT_FAILED,
307
+ result: err("PREFLIGHT_FAILED", {
308
+ reason: gitBlocker.reason,
309
+ operation_id: gitBlocker.operation_id,
310
+ unmerged_paths: gitBlocker.unmerged_paths,
311
+ convergence_vault: convergenceVault
312
+ })
313
+ };
314
+ }
315
+ }
316
+ if (convergenceVault && hasFleetManifest(convergenceVault)) {
317
+ const convergeFleetCtx = await loadFleetManifestAndHost({
318
+ vault: convergenceVault,
319
+ hostId: input.hostId ?? fleet.hostId,
320
+ env: input.env,
321
+ home: input.home,
322
+ cwd: input.cwd,
323
+ osHostname: input.osHostname,
324
+ user: input.user
325
+ });
326
+ if (!convergeFleetCtx || convergeFleetCtx.identityStatus !== "known" || convergeFleetCtx.hostId !== fleet.hostId) {
327
+ return {
328
+ exitCode: ExitCode.PREFLIGHT_FAILED,
329
+ result: err("PREFLIGHT_FAILED", {
330
+ reason: "convergence-vault-identity-mismatch",
331
+ host_id: fleet.hostId,
332
+ convergence_host_id: convergeFleetCtx?.hostId,
333
+ convergence_identity_status: convergeFleetCtx?.identityStatus,
334
+ convergence_vault: convergenceVault
335
+ })
336
+ };
337
+ }
338
+ }
339
+ const dualPathMeta = convergenceVault ? { convergence_vault: convergenceVault } : {};
291
340
  const converge = await deps.converge({
292
341
  vault: gitVault,
293
342
  lockToken: convergenceVault ? void 0 : input.lockToken,
@@ -313,37 +362,51 @@ async function runManagedWritePreflight(input, deps = DEFAULT_DEPS) {
313
362
  result: ok({
314
363
  mode: "git-writer",
315
364
  host_id: fleet.hostId,
365
+ mutation_vault: mutationVault,
366
+ git_vault: gitVault,
316
367
  base_oid: baseOid,
317
368
  converged: true,
318
369
  helper_path: converge.data.helper_path,
319
- ...dualPathMeta
370
+ ...dualPathMeta,
371
+ convergence_source: convergenceSource
320
372
  })
321
373
  };
322
374
  }
323
375
  async function runManagedWriteTransaction(input, deps = DEFAULT_DEPS) {
324
- const lock = acquireManagedWriteLock(input.vault, input.command);
376
+ const mutationVault = resolve(input.vault);
377
+ const lock = acquireManagedWriteLock(mutationVault, input.command);
325
378
  if (!lock.ok) {
326
379
  return { exitCode: ExitCode.SYNC_LOCK_HELD, result: lock };
327
380
  }
328
- let handle = lock.data;
381
+ const handle = lock.data;
329
382
  try {
330
- const preflight = await runManagedWritePreflight(
331
- {
332
- vault: input.vault,
333
- command: input.command,
334
- convergenceVault: input.convergenceVault,
335
- hostId: input.hostId,
336
- lockToken: handle.ownerToken,
337
- env: input.env,
338
- home: input.home,
339
- osHostname: input.osHostname
340
- },
341
- deps
342
- );
383
+ const preflightInput = {
384
+ vault: mutationVault,
385
+ command: input.command,
386
+ convergenceVault: input.convergenceVault,
387
+ hostId: input.hostId,
388
+ lockToken: handle.ownerToken,
389
+ env: input.env,
390
+ home: input.home,
391
+ cwd: input.cwd,
392
+ osHostname: input.osHostname,
393
+ user: input.user
394
+ };
395
+ const preflight = input.preflight ? await input.preflight(preflightInput) : await runManagedWritePreflight(preflightInput, deps);
343
396
  if (!preflight.result.ok) {
344
397
  return { exitCode: preflight.exitCode, result: preflight.result };
345
398
  }
346
399
  const receipt = preflight.result.data;
400
+ if (receipt.mutation_vault !== mutationVault) {
401
+ return {
402
+ exitCode: ExitCode.PREFLIGHT_FAILED,
403
+ result: err("PREFLIGHT_FAILED", {
404
+ reason: "mutation-vault-receipt-mismatch",
405
+ expected: mutationVault,
406
+ actual: receipt.mutation_vault
407
+ })
408
+ };
409
+ }
347
410
  if (receipt.mode === "immutable-record" && !input.allowImmutableRecord) {
348
411
  return {
349
412
  exitCode: ExitCode.PREFLIGHT_FAILED,
@@ -361,8 +424,6 @@ async function runManagedWriteTransaction(input, deps = DEFAULT_DEPS) {
361
424
  }
362
425
 
363
426
  export {
364
- acquireManagedWriteLock,
365
- releaseManagedWriteLock,
366
427
  runManagedWritePreflight,
367
428
  runManagedWriteTransaction
368
429
  };
@@ -55,7 +55,8 @@ var ExitCode = {
55
55
  FLEET_MANIFEST_INVALID: 50,
56
56
  SENSITIVE_CONTENT_DETECTED: 51,
57
57
  FLEET_SATELLITE_HEALTH_FAILED: 52,
58
- PROTECTED_SNAPSHOTTER_WRITE_BLOCKED: 53
58
+ PROTECTED_SNAPSHOTTER_WRITE_BLOCKED: 53,
59
+ INVALID_CONFIG_VALUE: 54
59
60
  };
60
61
 
61
62
  // ../shared/src/json-output.ts
@@ -285,6 +286,47 @@ var FleetManifestSchema = z.object({
285
286
  }
286
287
  }
287
288
  });
289
+ var vaultSyncRole = z.enum(["leaf", "snapshotter", "none"]);
290
+ var vaultSyncScheduler = z.enum(["systemd", "launchd", "none"]);
291
+ var vaultSyncScope = z.enum(["user", "system", "none"]);
292
+ var vaultSyncBool = z.enum(["true", "false"]);
293
+ var vaultSyncDuration = z.union([
294
+ z.string().regex(/^[1-9][0-9]*[mhs]$/, "must be <positive-int>m|h|s (e.g. 15m, 300s) or none"),
295
+ z.literal("none")
296
+ ]);
297
+ var vaultSyncPathOrNone = z.union([
298
+ absolutePath,
299
+ z.literal("none")
300
+ ]);
301
+ var VaultSyncConfigSchema = z.object({
302
+ "vault_sync.installed": vaultSyncBool,
303
+ "vault_sync.role": vaultSyncRole,
304
+ "vault_sync.scheduler": vaultSyncScheduler,
305
+ "vault_sync.service_scope": vaultSyncScope,
306
+ "vault_sync.snapshot_profile": vaultSyncPathOrNone,
307
+ "vault_sync.snapshot_script": vaultSyncPathOrNone,
308
+ "vault_sync.snapshot_worktree": vaultSyncPathOrNone,
309
+ "vault_sync.fuse_refresh_enabled": vaultSyncBool,
310
+ "vault_sync.fuse_refresh_interval": vaultSyncDuration,
311
+ "vault_sync.fuse_max_dir_cache": vaultSyncDuration,
312
+ "vault_sync.fuse_service_scope": vaultSyncScope
313
+ }).strict();
314
+ var VAULT_SYNC_KEYS = Object.keys(VaultSyncConfigSchema.shape);
315
+ function isVaultSyncKey(key) {
316
+ return VAULT_SYNC_KEYS.includes(key);
317
+ }
318
+ function parseVaultSyncKeyValue(key, value) {
319
+ if (!isVaultSyncKey(key)) {
320
+ return { ok: false, message: `unknown vault_sync key: ${key}` };
321
+ }
322
+ const fieldSchema = VaultSyncConfigSchema.shape[key];
323
+ const parsed = fieldSchema.safeParse(value);
324
+ if (parsed.success) {
325
+ return { ok: true };
326
+ }
327
+ const first = parsed.error.issues[0];
328
+ return { ok: false, message: first?.message ?? "invalid value" };
329
+ }
288
330
  function detectSchema(fm) {
289
331
  const COMPOUND_TYPES = /* @__PURE__ */ new Set(["lesson", "pattern", "antipattern", "gotcha"]);
290
332
  if (typeof fm.type === "string" && COMPOUND_TYPES.has(fm.type) && "project" in fm) return { schema: "compound" };
@@ -341,6 +383,29 @@ function getErrorMessage(e) {
341
383
  return e instanceof Error ? e.message : String(e);
342
384
  }
343
385
 
386
+ // ../shared/src/systemd-property-catalog.json
387
+ var systemd_property_catalog_default = {
388
+ load_state: "LoadState",
389
+ unit_file_state: "UnitFileState",
390
+ active_state: "ActiveState",
391
+ sub_state: "SubState",
392
+ next_elapse: "NextElapseUSecRealtime",
393
+ result: "Result",
394
+ exec_main_status: "ExecMainStatus",
395
+ exec_main_code: "ExecMainCode",
396
+ active_enter_timestamp: "ActiveEnterTimestamp",
397
+ inactive_enter_timestamp: "InactiveEnterTimestamp",
398
+ exec_main_start_timestamp: "ExecMainStartTimestamp",
399
+ exec_main_exit_timestamp: "ExecMainExitTimestamp",
400
+ invocation_id: "InvocationID"
401
+ };
402
+
403
+ // ../shared/src/systemd-property-catalog.ts
404
+ var SYSTEMD_PROPERTY_CATALOG = Object.freeze({ ...systemd_property_catalog_default });
405
+ function systemdPropertyFor(semantic) {
406
+ return SYSTEMD_PROPERTY_CATALOG[semantic];
407
+ }
408
+
344
409
  export {
345
410
  ExitCode,
346
411
  ok,
@@ -351,7 +416,10 @@ export {
351
416
  CompoundSchema,
352
417
  MetaSchema,
353
418
  FleetManifestSchema,
419
+ isVaultSyncKey,
420
+ parseVaultSyncKeyValue,
354
421
  detectSchema,
355
422
  isBlockedHost,
356
- getErrorMessage
423
+ getErrorMessage,
424
+ systemdPropertyFor
357
425
  };