skillwiki 0.10.44 → 0.10.45

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.
@@ -1,1212 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- FLEET_REL_PATH,
4
- acquireLock,
5
- appendLastOp,
6
- buildDegradedReasons,
7
- clearLastOp,
8
- findReviewRequiredOp,
9
- fixPathTooLong,
10
- getSessionId,
11
- git,
12
- gitStrict,
13
- hasActiveGitSequencer,
14
- hasUnmergedPaths,
15
- loadFleetManifestAndHost,
16
- probeRemoteHealth,
17
- readLastOp,
18
- readLock,
19
- releaseLock,
20
- resolveConfiguredSnapshotWorktree,
21
- runLint,
22
- runSyncLintDelta,
23
- runVaultSyncPullHelper,
24
- supersedeStaleReviewRequiredJournals
25
- } from "./chunk-NPHRDXHD.js";
26
- import {
27
- ExitCode,
28
- err,
29
- ok
30
- } from "./chunk-APDMYPM6.js";
31
-
32
- // src/utils/managed-write-preflight.ts
33
- import { existsSync as existsSync3 } from "fs";
34
- import { join as join3, resolve as resolve2 } from "path";
35
-
36
- // src/commands/sync.ts
37
- import { existsSync } from "fs";
38
- import { join } from "path";
39
- import { execFileSync } from "child_process";
40
-
41
- // src/utils/vault-git-pathspec.ts
42
- var VAULT_GENERATED_COMMIT_PATHS = [
43
- ".skillwiki/last-op.json",
44
- ".skillwiki/memory",
45
- ".skillwiki/memory-topics.json"
46
- ];
47
- var VAULT_GENERATED_COMMIT_EXCLUDES = [
48
- ...VAULT_GENERATED_COMMIT_PATHS.map((path) => `:!${path}`)
49
- ];
50
- var VAULT_COMMIT_PATHSPEC = [".", ...VAULT_GENERATED_COMMIT_EXCLUDES];
51
- function stageVaultContentChanges(vault) {
52
- gitStrict(vault, ["add", "-A", "--", "."]);
53
- for (const generatedPath of VAULT_GENERATED_COMMIT_PATHS) {
54
- try {
55
- gitStrict(vault, ["reset", "HEAD", "--", generatedPath]);
56
- } catch (_e) {
57
- }
58
- }
59
- }
60
-
61
- // src/commands/sync.ts
62
- function parseDirtyPaths(porcelain) {
63
- if (!porcelain) return [];
64
- return porcelain.split("\n").map((line) => line.trimEnd()).filter((line) => line.length >= 4).map((line) => {
65
- const payload = line.length >= 3 && line[2] === " " ? line.slice(3) : line.length >= 2 && line[1] === " " ? line.slice(2) : line;
66
- const arrow = payload.lastIndexOf(" -> ");
67
- return arrow >= 0 ? payload.slice(arrow + 4) : payload;
68
- }).filter((path) => path.length > 0);
69
- }
70
- function splitNonEmptyLines(text) {
71
- if (!text) return [];
72
- return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
73
- }
74
- function isTrackedNotePath(path) {
75
- if (!path.endsWith(".md")) return false;
76
- return !/^\.(skillwiki|claude|obsidian|antigravitycli|playwright-cli)\//.test(path);
77
- }
78
- function refHasPath(vault, ref, path) {
79
- try {
80
- execFileSync("git", ["cat-file", "-e", `${ref}:${path}`], {
81
- cwd: vault,
82
- stdio: ["pipe", "pipe", "pipe"]
83
- });
84
- return true;
85
- } catch {
86
- return false;
87
- }
88
- }
89
- function runSyncStatus(input) {
90
- const vault = input.vault;
91
- const includeStashes = input.includeStashes ?? false;
92
- if (!existsSync(join(vault, ".git"))) {
93
- return {
94
- exitCode: ExitCode.VAULT_PATH_INVALID,
95
- result: ok({
96
- is_git_repo: false,
97
- dirty: 0,
98
- ahead: 0,
99
- behind: 0,
100
- unpromoted_note_paths: 0,
101
- unpromoted_note_examples: [],
102
- last_commit: "never",
103
- status: "not_a_repo",
104
- humanHint: "not a git repository"
105
- })
106
- };
107
- }
108
- enableGitLongPathsOnWindows(vault);
109
- const porcelain = git(vault, ["status", "--porcelain"]);
110
- const dirty = porcelain ? porcelain.split("\n").filter((l) => l.trim().length > 0).length : 0;
111
- const dirtyPaths = parseDirtyPaths(porcelain);
112
- const untrackedPaths = splitNonEmptyLines(git(vault, ["ls-files", "--others", "--exclude-standard"]));
113
- const revOutput = git(vault, ["rev-list", "--left-right", "--count", "origin/HEAD...HEAD"]);
114
- let ahead = 0;
115
- let behind = 0;
116
- if (revOutput) {
117
- const parts = revOutput.split(/\s+/);
118
- behind = parseInt(parts[0], 10) || 0;
119
- ahead = parseInt(parts[1], 10) || 0;
120
- }
121
- const tsRaw = git(vault, ["log", "-1", "--format=%ct"]);
122
- let last_commit;
123
- if (tsRaw) {
124
- const ts = parseInt(tsRaw, 10);
125
- if (!isNaN(ts) && ts > 0) {
126
- last_commit = new Date(ts * 1e3).toISOString();
127
- } else {
128
- last_commit = "never";
129
- }
130
- } else {
131
- last_commit = "never";
132
- }
133
- const remoteRef = git(vault, ["rev-parse", "--verify", "origin/main"]) ? "origin/main" : git(vault, ["rev-parse", "--verify", "origin/HEAD"]) ? "origin/HEAD" : "HEAD";
134
- const unpromotedNoteAll = Array.from(/* @__PURE__ */ new Set([...dirtyPaths, ...untrackedPaths])).filter(isTrackedNotePath).filter((path) => !refHasPath(vault, remoteRef, path));
135
- const unpromotedNoteExamples = unpromotedNoteAll.slice(0, 5);
136
- const unpromotedNotePaths = unpromotedNoteAll.length;
137
- let status;
138
- if (dirty > 0) {
139
- status = "dirty";
140
- } else if (ahead > 0) {
141
- status = "ahead";
142
- } else if (behind > 0) {
143
- status = "behind";
144
- } else {
145
- status = "clean";
146
- }
147
- const hintLines = [
148
- `status: ${status}`,
149
- `dirty: ${dirty}`,
150
- `ahead: ${ahead}`,
151
- `behind: ${behind}`,
152
- `unpromoted_note_paths: ${unpromotedNotePaths}`,
153
- `last_commit: ${last_commit}`
154
- ];
155
- const exitCode = status === "clean" ? ExitCode.OK : ExitCode.LINT_HAS_WARNINGS;
156
- let stashes;
157
- if (includeStashes) {
158
- stashes = enumerateStashes(vault);
159
- }
160
- const output = {
161
- is_git_repo: true,
162
- dirty,
163
- ahead,
164
- behind,
165
- unpromoted_note_paths: unpromotedNotePaths,
166
- unpromoted_note_examples: unpromotedNoteExamples,
167
- last_commit,
168
- status,
169
- humanHint: hintLines.join("\n")
170
- };
171
- if (stashes !== void 0) {
172
- output.stashes = stashes;
173
- }
174
- if (input.includeRemoteHealth) {
175
- const home = input.home ?? process.env.HOME ?? "";
176
- const remote_health = probeRemoteHealth({
177
- vaultPath: vault,
178
- home,
179
- s3Remote: input.s3Remote,
180
- snapshotterAlias: input.snapshotterAlias,
181
- checkSnapshotter: input.checkSnapshotter,
182
- exec: input.execProbe
183
- });
184
- output.remote_health = remote_health;
185
- const degraded = buildDegradedReasons(remote_health);
186
- if (degraded.length > 0) {
187
- output.degraded_reasons = degraded;
188
- hintLines.push(`degraded_reasons: ${degraded.join(", ")}`);
189
- output.humanHint = hintLines.join("\n");
190
- }
191
- }
192
- return {
193
- exitCode,
194
- result: ok(output)
195
- };
196
- }
197
- async function runSyncPush(input) {
198
- const vault = input.vault;
199
- if (!existsSync(join(vault, ".git"))) {
200
- return {
201
- exitCode: ExitCode.VAULT_PATH_INVALID,
202
- result: err("NOT_A_GIT_REPO", { path: vault })
203
- };
204
- }
205
- enableGitLongPathsOnWindows(vault);
206
- let pathFixes = 0;
207
- const pathFix = await fixPathTooLong({ vault });
208
- if (pathFix.result.ok && pathFix.result.data.fixed.length > 0) {
209
- pathFixes = pathFix.result.data.fixed.length;
210
- appendLastOp(vault, {
211
- operation: "lint-fix",
212
- summary: `fixed ${pathFixes} long path(s)`,
213
- files: pathFix.result.data.fixed.flatMap((f) => [f.from, f.to]),
214
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
215
- });
216
- }
217
- const porcelain = git(vault, ["status", "--porcelain"]);
218
- const dirtyFiles = porcelain ? porcelain.split("\n").filter((l) => l.trim().length > 0) : [];
219
- if (dirtyFiles.length === 0) {
220
- return {
221
- exitCode: ExitCode.OK,
222
- result: ok({
223
- files_committed: 0,
224
- commit_message: "",
225
- pushed: false,
226
- path_fixes: pathFixes,
227
- humanHint: "nothing to commit, working tree clean"
228
- })
229
- };
230
- }
231
- let delta = { full_errors: 0, base_errors: 0, new_errors: 0, resolved_errors: 0 };
232
- const preferredBase = git(vault, ["rev-parse", "--verify", "origin/main"]) ? "origin/main" : git(vault, ["rev-parse", "--verify", "origin/HEAD"]) ? "origin/HEAD" : "";
233
- if (preferredBase) {
234
- const deltaResult = await runSyncLintDelta({ vault, baseRef: preferredBase });
235
- if (!deltaResult.result.ok) {
236
- return {
237
- exitCode: ExitCode.LINT_HAS_ERRORS,
238
- result: err("LINT_DELTA_UNAVAILABLE", {
239
- message: "lint-delta evidence missing or failed \u2014 fail closed",
240
- detail: deltaResult.result
241
- })
242
- };
243
- }
244
- delta = deltaResult.result.data;
245
- if (delta.new_errors > 0) {
246
- return {
247
- exitCode: ExitCode.LINT_HAS_ERRORS,
248
- result: err("LINT_NEW_ERRORS_BLOCK_PUSH", {
249
- full_errors: delta.full_errors,
250
- base_errors: delta.base_errors,
251
- new_errors: delta.new_errors,
252
- resolved_errors: delta.resolved_errors,
253
- new_fingerprints: deltaResult.result.data.new_fingerprints
254
- })
255
- };
256
- }
257
- } else {
258
- const lintResult = await runLint({ vault, days: 90, lines: 200, logThreshold: 500 });
259
- if (lintResult.result.ok) {
260
- const fullErrors = lintResult.result.data.summary.errors;
261
- delta = { full_errors: fullErrors, base_errors: 0, new_errors: fullErrors, resolved_errors: 0 };
262
- if (fullErrors > 0) {
263
- const buckets = "by_severity" in lintResult.result.data ? lintResult.result.data.by_severity.error : [];
264
- return {
265
- exitCode: ExitCode.LINT_HAS_ERRORS,
266
- result: err("LINT_ERRORS_BLOCK_PUSH", {
267
- errors: fullErrors,
268
- buckets,
269
- message: "no origin base ref for delta; absolute lint errors block push"
270
- })
271
- };
272
- }
273
- } else {
274
- delta = { full_errors: 0, base_errors: 0, new_errors: 0, resolved_errors: 0 };
275
- }
276
- }
277
- try {
278
- stageVaultContentChanges(vault);
279
- } catch (e) {
280
- return {
281
- exitCode: ExitCode.SYNC_PUSH_FAILED,
282
- result: err("GIT_ADD_FAILED", { message: String(e) })
283
- };
284
- }
285
- const lastOps = readLastOp(vault);
286
- let commitMessage;
287
- if (lastOps.length > 0) {
288
- commitMessage = lastOps.map((op) => `${op.operation}: ${op.summary} (${op.files.length} files)`).join("; ");
289
- } else {
290
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
291
- commitMessage = `sync: vault update ${timestamp}`;
292
- }
293
- try {
294
- gitStrict(vault, ["commit", "-m", commitMessage]);
295
- } catch (e) {
296
- return {
297
- exitCode: ExitCode.SYNC_PUSH_FAILED,
298
- result: err("GIT_COMMIT_FAILED", { message: String(e) })
299
- };
300
- }
301
- clearLastOp(vault);
302
- let pushed = false;
303
- try {
304
- gitStrict(vault, ["push", "origin", "HEAD"]);
305
- pushed = true;
306
- } catch (e) {
307
- return {
308
- exitCode: ExitCode.SYNC_PUSH_FAILED,
309
- result: ok({
310
- files_committed: dirtyFiles.length,
311
- commit_message: commitMessage,
312
- pushed: false,
313
- path_fixes: pathFixes,
314
- humanHint: `committed ${dirtyFiles.length} file(s)${pathFixes > 0 ? ` after ${pathFixes} long-path fix(es)` : ""} but push failed: ${String(e)}`
315
- })
316
- };
317
- }
318
- const inheritedNote = delta.full_errors > 0 ? `; lint full=${delta.full_errors} base=${delta.base_errors} new=${delta.new_errors} resolved=${delta.resolved_errors} (inherited debt only)` : `; lint full=0 new=0`;
319
- return {
320
- exitCode: ExitCode.OK,
321
- result: ok({
322
- files_committed: dirtyFiles.length,
323
- commit_message: commitMessage,
324
- pushed,
325
- path_fixes: pathFixes,
326
- lint_full_errors: delta.full_errors,
327
- lint_base_errors: delta.base_errors,
328
- lint_new_errors: delta.new_errors,
329
- lint_resolved_errors: delta.resolved_errors,
330
- humanHint: `committed and pushed ${dirtyFiles.length} file(s)${pathFixes > 0 ? ` after ${pathFixes} long-path fix(es)` : ""}${inheritedNote}`
331
- })
332
- };
333
- }
334
- function enumerateStashes(vault, nowMs = Date.now()) {
335
- const output = git(vault, ["log", "--format=%gd%x09%H%x09%s%x09%ct", "-g", "stash"]);
336
- if (!output) return [];
337
- const stashes = [];
338
- const lines = output.split("\n").filter((l) => l.trim().length > 0);
339
- for (const line of lines) {
340
- const parts = line.split(" ");
341
- if (parts.length < 4) continue;
342
- const ref = parts[0];
343
- const oid = parts[1];
344
- const message = parts[2];
345
- const ctStr = parts[3];
346
- const ct = parseInt(ctStr, 10);
347
- if (isNaN(ct)) continue;
348
- const age_minutes = Math.floor((nowMs - ct * 1e3) / (60 * 1e3));
349
- stashes.push({ ref, oid, message, age_minutes });
350
- }
351
- return stashes;
352
- }
353
- function enableGitLongPathsOnWindows(vault) {
354
- if (process.platform !== "win32") return;
355
- git(vault, ["config", "core.longpaths", "true"]);
356
- }
357
- async function runSyncPull(input) {
358
- const vault = input.vault;
359
- if (!existsSync(join(vault, ".git"))) {
360
- return {
361
- exitCode: ExitCode.VAULT_PATH_INVALID,
362
- result: err("NOT_A_GIT_REPO", { path: vault })
363
- };
364
- }
365
- enableGitLongPathsOnWindows(vault);
366
- const remoteUrl = git(vault, ["remote", "get-url", "origin"]);
367
- if (!remoteUrl) {
368
- return {
369
- exitCode: ExitCode.SYNC_PULL_FAILED,
370
- result: err("GIT_PULL_FAILED", { message: "no remote configured (origin)" })
371
- };
372
- }
373
- const helper = await runVaultSyncPullHelper({ vault, remote: "origin", branch: "main" });
374
- if (!helper.ok) {
375
- if (helper.error === "PREFLIGHT_FAILED") {
376
- return {
377
- exitCode: ExitCode.PREFLIGHT_FAILED,
378
- result: err("PREFLIGHT_FAILED", helper.detail)
379
- };
380
- }
381
- return {
382
- exitCode: ExitCode.SYNC_PULL_FAILED,
383
- result: err("GIT_PULL_FAILED", helper.detail)
384
- };
385
- }
386
- const fetched = true;
387
- const pulled = helper.data.changed;
388
- const conflicts = 0;
389
- const autoResolved = 0;
390
- let filesUpdated = 0;
391
- if (helper.data.changed) {
392
- const diffOutput = git(vault, ["diff", "--stat", `${helper.data.before_oid}..${helper.data.after_oid}`]);
393
- if (diffOutput) {
394
- const fileMatch = diffOutput.match(/(\d+) file[s]? changed/);
395
- if (fileMatch) filesUpdated = parseInt(fileMatch[1], 10);
396
- }
397
- }
398
- const pathFix = await fixPathTooLong({ vault });
399
- const pathFixCount = pathFix.result.ok ? pathFix.result.data.fixed.length : 0;
400
- let lintErrors = 0;
401
- let lintWarnings = 0;
402
- const lintResult = await runLint({ vault, days: 90, lines: 200, logThreshold: 500 });
403
- if (lintResult.result.ok) {
404
- lintErrors = lintResult.result.data.summary.errors;
405
- lintWarnings = lintResult.result.data.summary.warnings;
406
- }
407
- const hintParts = [];
408
- if (filesUpdated > 0) hintParts.push(`updated ${filesUpdated} file(s)`);
409
- else hintParts.push(pulled ? "pulled via vault-sync helper" : "already up to date");
410
- if (pathFixCount > 0) hintParts.push(`${pathFixCount} long path(s) fixed`);
411
- if (lintErrors > 0) hintParts.push(`${lintErrors} lint error(s)`);
412
- if (lintWarnings > 0) hintParts.push(`${lintWarnings} lint warning(s)`);
413
- const exitCode = lintErrors > 0 ? ExitCode.LINT_HAS_ERRORS : lintWarnings > 0 ? ExitCode.LINT_HAS_WARNINGS : ExitCode.OK;
414
- return {
415
- exitCode,
416
- result: ok({
417
- fetched,
418
- pulled,
419
- files_updated: filesUpdated,
420
- conflicts,
421
- auto_resolved: autoResolved,
422
- lint_errors: lintErrors,
423
- lint_warnings: lintWarnings,
424
- humanHint: hintParts.join(", ")
425
- })
426
- };
427
- }
428
- var STASH_AUDIT_CLASSIFICATIONS = [
429
- "recent_known_peer_stash",
430
- "self_or_local_recovery_stash",
431
- "stale_stash_backlog",
432
- "unknown_stash_ownership"
433
- ];
434
- var STASH_AUDIT_FORMATS = ["wiki-sync", "vault-sync", "manual", "unknown"];
435
- var STASH_RECENT_MINUTES = 120;
436
- var MANAGED_WRITER_SPECS = [
437
- // Real runner only: wiki-push.sh on Unix, wiki-push/wiki-push.exe as a
438
- // Windows image name. Bare text mentions never match.
439
- ["wiki-push", /(?:^wiki-push(?:\.exe)?$)|(?:wiki-push\.sh(?:\s|$))/i],
440
- // Write verbs only on Unix; Windows image name only (tasklist exposes
441
- // no arguments). Read-only verbs and `rclone mount` (FUSE consumer)
442
- // never match.
443
- ["rclone", /(?:^rclone(?:\.exe)?$)|(?:\brclone(?:\.exe)?\s+(?:copy|sync|move|delete|purge|deletefile|copyto|moveto|dedupe|mkdir|rmdir|rmdirs|touch|settier|bisync|copyurl)(?:\s|$))/i],
444
- // Writing helper scripts on Unix; Windows image name only.
445
- ["vault-sync", /(?:^vault-sync(?:\.exe)?$)|(?:wiki-(?:snapshot|pull-with-auto-resolve|git-repair-v3)\.sh(?:\s|$))/i]
446
- ];
447
- var MANAGED_WRITER_KINDS = MANAGED_WRITER_SPECS.map(([kind]) => kind);
448
- function classifyStash(ageMinutes, sessionId, currentSession) {
449
- if (sessionId && sessionId === currentSession) return "self_or_local_recovery_stash";
450
- if (sessionId && ageMinutes <= STASH_RECENT_MINUTES) return "recent_known_peer_stash";
451
- if (ageMinutes > STASH_RECENT_MINUTES) return "stale_stash_backlog";
452
- return "unknown_stash_ownership";
453
- }
454
- function classifyManagedWriterProcesses(snapshot, currentPid = process.pid) {
455
- const kinds = /* @__PURE__ */ new Set();
456
- let count = 0;
457
- for (const line of snapshot.split(/\r?\n/)) {
458
- const trimmed = line.trim();
459
- if (!trimmed) continue;
460
- let pid;
461
- let processName = "";
462
- const csvMatch = trimmed.match(/^"([^"]*)","(\d+)"(?:,|$)/);
463
- if (csvMatch) {
464
- processName = csvMatch[1];
465
- pid = Number(csvMatch[2]);
466
- } else {
467
- const pidMatch = trimmed.match(/^(\d+)\s+(.*)$/);
468
- if (!pidMatch) continue;
469
- pid = Number(pidMatch[1]);
470
- processName = pidMatch[2];
471
- }
472
- if (pid === currentPid) continue;
473
- const kind = MANAGED_WRITER_SPECS.find(([, pattern]) => pattern.test(processName))?.[0];
474
- if (!kind) continue;
475
- count += 1;
476
- kinds.add(kind);
477
- }
478
- return { count, kinds: [...kinds].sort(), blocking: count > 0 };
479
- }
480
- function managedWriterSnapshot() {
481
- try {
482
- if (process.platform === "win32") {
483
- return execFileSync("tasklist", ["/FO", "CSV", "/NH"], {
484
- encoding: "utf8",
485
- stdio: ["pipe", "pipe", "pipe"]
486
- });
487
- }
488
- return execFileSync("ps", ["-axo", "pid=,command="], {
489
- encoding: "utf8",
490
- stdio: ["pipe", "pipe", "pipe"]
491
- });
492
- } catch {
493
- return "";
494
- }
495
- }
496
- function runSyncPeers(input) {
497
- const vault = input.vault;
498
- const currentSession = input.sessionId ?? getSessionId();
499
- const locks = [];
500
- const existingLock = readLock(vault);
501
- if (existingLock) {
502
- const self = existingLock.session_id === currentSession;
503
- locks.push({ ...existingLock, is_self: self });
504
- }
505
- const allStashes = enumerateStashes(vault, input.nowMs);
506
- const stashes = [];
507
- const wikiSyncAudit = [];
508
- const otherStashAudit = [];
509
- for (const stash of allStashes) {
510
- let actualMessage = stash.message;
511
- const prefixMatch = stash.message.match(/^On [^:]+:\s*(.*)/);
512
- if (prefixMatch) {
513
- actualMessage = prefixMatch[1];
514
- }
515
- const match = actualMessage.match(/^wiki-sync:([^:]+):([^:]+):(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z):(.*)$/);
516
- if (match) {
517
- const session_id = match[1];
518
- const cwd_hash = match[2];
519
- const timestamp = match[3];
520
- const summary = match[4];
521
- stashes.push({
522
- ref: stash.ref,
523
- oid: stash.oid,
524
- session_id,
525
- cwd_hash,
526
- timestamp,
527
- summary,
528
- age_minutes: stash.age_minutes
529
- });
530
- wikiSyncAudit.push({
531
- ref: stash.ref,
532
- oid: stash.oid,
533
- age_minutes: stash.age_minutes,
534
- classification: classifyStash(stash.age_minutes, session_id, currentSession),
535
- format: "wiki-sync",
536
- session_id
537
- });
538
- continue;
539
- }
540
- const vaultSyncMatch = actualMessage.match(/^vault-sync\s+op=([^\s]+)(?:\s+.*)?$/i);
541
- const operationId = vaultSyncMatch?.[1];
542
- const manualPeer = /^peer$/i.test(actualMessage.trim());
543
- const format = operationId ? "vault-sync" : manualPeer ? "manual" : "unknown";
544
- const classification = operationId ? stash.age_minutes <= STASH_RECENT_MINUTES ? "recent_known_peer_stash" : "stale_stash_backlog" : "unknown_stash_ownership";
545
- otherStashAudit.push({
546
- ref: stash.ref,
547
- oid: stash.oid,
548
- age_minutes: stash.age_minutes,
549
- classification,
550
- format,
551
- ...operationId ? { operation_id: operationId } : {}
552
- });
553
- }
554
- const stashAudit = [...wikiSyncAudit, ...otherStashAudit];
555
- const managedWriters = classifyManagedWriterProcesses(
556
- input.processSnapshot ?? managedWriterSnapshot()
557
- );
558
- const hintParts = [];
559
- if (locks.length > 0) hintParts.push(`${locks.length} lock(s)`);
560
- if (stashes.length > 0) hintParts.push(`${stashes.length} wiki-sync stash(es)`);
561
- if (managedWriters.count > 0) hintParts.push(`${managedWriters.count} live writer overlap(s)`);
562
- if (stashAudit.length > 0) hintParts.push(`${stashAudit.length} stash audit item(s)`);
563
- const blocking = locks.some((lock) => !lock.is_self) || managedWriters.blocking || stashAudit.some((entry) => entry.classification === "recent_known_peer_stash");
564
- const humanHint = hintParts.length > 0 ? hintParts.join(", ") : "no peers detected";
565
- return {
566
- exitCode: ExitCode.OK,
567
- result: ok({
568
- locks,
569
- stashes,
570
- stash_audit: stashAudit,
571
- managed_writers: managedWriters,
572
- blocking,
573
- humanHint
574
- })
575
- };
576
- }
577
- function runSyncLock(input) {
578
- const vault = input.vault;
579
- if (!existsSync(vault)) {
580
- return {
581
- exitCode: ExitCode.VAULT_PATH_INVALID,
582
- result: err("VAULT_PATH_INVALID", { path: vault })
583
- };
584
- }
585
- const result = acquireLock(vault, {
586
- sessionId: input.sessionId,
587
- summary: input.summary,
588
- ttlMinutes: input.ttlMinutes,
589
- force: input.force
590
- });
591
- if (result.ok) {
592
- return {
593
- exitCode: ExitCode.OK,
594
- result: ok({
595
- acquired: true,
596
- lock: result.lock,
597
- humanHint: `lock acquired for ${result.lock.summary} (expires ${result.lock.expires})`
598
- })
599
- };
600
- } else {
601
- return {
602
- exitCode: ExitCode.SYNC_LOCK_HELD,
603
- result: ok({
604
- acquired: false,
605
- lock: result.held,
606
- held_by: result.held,
607
- humanHint: `lock held by session ${result.held.session_id} (PID ${result.held.pid}) for ${result.held.summary}`
608
- })
609
- };
610
- }
611
- }
612
- function runSyncUnlock(input) {
613
- const vault = input.vault;
614
- if (!existsSync(vault)) {
615
- return {
616
- exitCode: ExitCode.VAULT_PATH_INVALID,
617
- result: err("VAULT_PATH_INVALID", { path: vault })
618
- };
619
- }
620
- const result = releaseLock(vault, { sessionId: input.sessionId, force: input.force });
621
- let humanHint;
622
- if (result.released && result.prior) {
623
- humanHint = `lock force-released (was held by session ${result.prior.session_id}, PID ${result.prior.pid})`;
624
- } else if (result.released) {
625
- humanHint = "lock released";
626
- } else {
627
- humanHint = "lock not held by this session (no-op)";
628
- }
629
- const output = {
630
- released: result.released,
631
- humanHint
632
- };
633
- if (result.prior) {
634
- output.prior = {
635
- session_id: result.prior.session_id,
636
- pid: result.prior.pid,
637
- summary: result.prior.summary
638
- };
639
- }
640
- return {
641
- exitCode: ExitCode.OK,
642
- result: ok(output)
643
- };
644
- }
645
-
646
- // src/utils/managed-write-lock.ts
647
- import { randomBytes } from "crypto";
648
- import {
649
- existsSync as existsSync2,
650
- mkdirSync,
651
- readFileSync,
652
- unlinkSync,
653
- writeFileSync
654
- } from "fs";
655
- import { hostname } from "os";
656
- import { dirname, join as join2, resolve } from "path";
657
- function managedWriteLockPath(vault) {
658
- const gitPath = git(vault, ["rev-parse", "--git-path", "vault-sync/managed-write.lock"]);
659
- if (gitPath) return gitPath.startsWith("/") ? gitPath : join2(vault, gitPath);
660
- return join2(vault, ".skillwiki", "managed-write.lock");
661
- }
662
- function readLockRecord(path) {
663
- try {
664
- return JSON.parse(readFileSync(path, "utf8"));
665
- } catch {
666
- return null;
667
- }
668
- }
669
- function isManagedWriteLockOwnerAlive(pid) {
670
- if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false;
671
- try {
672
- process.kill(pid, 0);
673
- return true;
674
- } catch (error) {
675
- if (error.code === "EPERM") return true;
676
- return false;
677
- }
678
- }
679
- function hasUnsafeGitState(vault) {
680
- if (!isGitBackedVault(vault)) return false;
681
- const gitDirRaw = git(vault, ["rev-parse", "--git-dir"]);
682
- if (!gitDirRaw) return true;
683
- const gitDir = gitDirRaw.startsWith("/") ? gitDirRaw : join2(vault, gitDirRaw);
684
- for (const rel of ["rebase-merge", "rebase-apply"]) {
685
- if (existsSync2(join2(gitDir, rel))) return true;
686
- }
687
- for (const rel of ["MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD"]) {
688
- if (existsSync2(join2(gitDir, rel))) return true;
689
- }
690
- const unmerged = git(vault, ["ls-files", "-u"]);
691
- return Boolean(unmerged && unmerged.trim().length > 0);
692
- }
693
- function isGitBackedVault(vault) {
694
- return Boolean(git(vault, ["rev-parse", "--absolute-git-dir"]));
695
- }
696
- function hasLocalOwnerProof(vault, record) {
697
- return isGitBackedVault(vault) || typeof record.owner_hostname === "string" && record.owner_hostname === hostname();
698
- }
699
- function reclaimDeadManagedWriteLockOwner(vault, options = {}) {
700
- const path = managedWriteLockPath(vault);
701
- const gitStateVault = resolve(options.gitStateVault ?? vault);
702
- if (!existsSync2(path)) return ok({ reclaimed: false });
703
- const record = readLockRecord(path);
704
- if (!record) {
705
- return err("SYNC_LOCK_HELD", { path, message: "managed-write lock unreadable" });
706
- }
707
- if (!hasLocalOwnerProof(vault, record)) {
708
- return err("SYNC_LOCK_HELD", {
709
- path,
710
- owner_hostname: record.owner_hostname,
711
- current_hostname: hostname(),
712
- message: "managed-write lock origin is foreign or unknown"
713
- });
714
- }
715
- if (isManagedWriteLockOwnerAlive(record.pid)) {
716
- return err("SYNC_LOCK_HELD", { path, message: "managed-write lock owner is alive" });
717
- }
718
- if (hasUnsafeGitState(gitStateVault)) {
719
- return err("SYNC_LOCK_HELD", {
720
- path,
721
- git_state_vault: gitStateVault,
722
- message: "managed-write lock not reclaimed: unsafe git state"
723
- });
724
- }
725
- try {
726
- const recoveryDir = join2(dirname(path), "recovery");
727
- mkdirSync(recoveryDir, { recursive: true });
728
- const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
729
- const recoveryPath = join2(recoveryDir, `stale-managed-write-lock-${stamp}-${process.pid}.json`);
730
- const meta = {
731
- recovered_at: (/* @__PURE__ */ new Date()).toISOString(),
732
- recovery_reason: "owner_pid_dead",
733
- owner_pid_alive: false,
734
- git_state_vault: gitStateVault,
735
- lock: record
736
- };
737
- writeFileSync(recoveryPath, `${JSON.stringify(meta, null, 2)}
738
- `, { flag: "wx" });
739
- unlinkSync(path);
740
- return ok({ reclaimed: true, recoveryPath });
741
- } catch (error) {
742
- return err("WRITE_FAILED", { path, message: String(error) });
743
- }
744
- }
745
- function tryCreateLock(path, command) {
746
- const ownerToken = randomBytes(16).toString("hex");
747
- const acquired = (/* @__PURE__ */ new Date()).toISOString();
748
- try {
749
- mkdirSync(dirname(path), { recursive: true });
750
- writeFileSync(
751
- path,
752
- `${JSON.stringify({
753
- pid: process.pid,
754
- owner_hostname: hostname(),
755
- owner_token: ownerToken,
756
- acquired,
757
- command
758
- })}
759
- `,
760
- { flag: "wx" }
761
- );
762
- return ok({ vault: "", path, ownerToken, acquired });
763
- } catch (error) {
764
- if (error.code === "EEXIST") return err("SYNC_LOCK_HELD", { path });
765
- return err("WRITE_FAILED", { path, message: String(error) });
766
- }
767
- }
768
- function acquireManagedWriteLock(vault, command, options = {}) {
769
- const path = managedWriteLockPath(vault);
770
- const first = tryCreateLock(path, command);
771
- if (first.ok) {
772
- return ok({ ...first.data, vault });
773
- }
774
- if (first.error !== "SYNC_LOCK_HELD") return first;
775
- const reclaimed = reclaimDeadManagedWriteLockOwner(vault, options);
776
- if (!reclaimed.ok || !reclaimed.data.reclaimed) {
777
- return err("SYNC_LOCK_HELD", { path });
778
- }
779
- const second = tryCreateLock(path, command);
780
- if (second.ok) return ok({ ...second.data, vault });
781
- return second.ok === false ? second : err("SYNC_LOCK_HELD", { path });
782
- }
783
- function releaseManagedWriteLock(handle) {
784
- try {
785
- const parsed = JSON.parse(readFileSync(handle.path, "utf8"));
786
- if (parsed.owner_token !== handle.ownerToken || parsed.acquired !== handle.acquired) {
787
- return err("SYNC_LOCK_HELD", {
788
- path: handle.path,
789
- message: "managed-write lock ownership changed"
790
- });
791
- }
792
- unlinkSync(handle.path);
793
- return ok({ released: true });
794
- } catch (error) {
795
- return err("WRITE_FAILED", { path: handle.path, message: String(error) });
796
- }
797
- }
798
-
799
- // src/utils/managed-write-preflight.ts
800
- var DEFAULT_DEPS = {
801
- converge: (input) => runVaultSyncPullHelper(input),
802
- resolveConfiguredSnapshotWorktree,
803
- syncPeers: runSyncPeers
804
- };
805
- var SAFE_MANAGED_WRITER_KINDS = new Set(MANAGED_WRITER_KINDS);
806
- var SAFE_STASH_AUDIT_CLASSIFICATIONS = new Set(STASH_AUDIT_CLASSIFICATIONS);
807
- var SAFE_STASH_AUDIT_FORMATS = new Set(STASH_AUDIT_FORMATS);
808
- function isRecord(value) {
809
- return typeof value === "object" && value !== null;
810
- }
811
- function isNonNegativeFiniteNumber(value) {
812
- return typeof value === "number" && Number.isFinite(value) && value >= 0;
813
- }
814
- function isNonNegativeInteger(value) {
815
- return typeof value === "number" && Number.isInteger(value) && value >= 0;
816
- }
817
- function isPeerLock(value) {
818
- if (!isRecord(value)) return false;
819
- return typeof value.session_id === "string" && isNonNegativeInteger(value.pid) && typeof value.cwd === "string" && typeof value.summary === "string" && typeof value.acquired === "string" && typeof value.expires === "string" && typeof value.is_self === "boolean";
820
- }
821
- function isWikiSyncStash(value) {
822
- if (!isRecord(value)) return false;
823
- return typeof value.ref === "string" && typeof value.oid === "string" && typeof value.session_id === "string" && typeof value.cwd_hash === "string" && typeof value.timestamp === "string" && typeof value.summary === "string" && isNonNegativeFiniteNumber(value.age_minutes);
824
- }
825
- function isStashAuditEntry(value) {
826
- if (!isRecord(value)) return false;
827
- if (typeof value.ref !== "string" || typeof value.oid !== "string" || !isNonNegativeFiniteNumber(value.age_minutes) || typeof value.classification !== "string" || !SAFE_STASH_AUDIT_CLASSIFICATIONS.has(value.classification) || typeof value.format !== "string" || !SAFE_STASH_AUDIT_FORMATS.has(value.format)) {
828
- return false;
829
- }
830
- if (value.session_id !== void 0 && typeof value.session_id !== "string") return false;
831
- if (value.operation_id !== void 0 && typeof value.operation_id !== "string") return false;
832
- return true;
833
- }
834
- function isManagedWriterObservation(value) {
835
- if (!isRecord(value) || !Array.isArray(value.kinds)) return false;
836
- if (!isNonNegativeInteger(value.count) || typeof value.blocking !== "boolean" || value.kinds.some(
837
- (kind) => typeof kind !== "string" || !SAFE_MANAGED_WRITER_KINDS.has(kind)
838
- )) {
839
- return false;
840
- }
841
- if (value.blocking !== value.count > 0) return false;
842
- if (value.count === 0 && value.kinds.length > 0) return false;
843
- if (value.count > 0 && value.kinds.length === 0) return false;
844
- return true;
845
- }
846
- function validateSyncPeersOutput(value) {
847
- if (!isRecord(value)) return null;
848
- if (!Array.isArray(value.locks) || !Array.isArray(value.stashes) || !Array.isArray(value.stash_audit) || typeof value.humanHint !== "string" || typeof value.blocking !== "boolean" || !isManagedWriterObservation(value.managed_writers)) {
849
- return null;
850
- }
851
- let foreignLockCount = 0;
852
- for (const lock of value.locks) {
853
- if (!isPeerLock(lock)) return null;
854
- if (!lock.is_self) foreignLockCount += 1;
855
- }
856
- for (const stash of value.stashes) {
857
- if (!isWikiSyncStash(stash)) return null;
858
- }
859
- let recentPeerStashCount = 0;
860
- for (const entry of value.stash_audit) {
861
- if (!isStashAuditEntry(entry)) return null;
862
- if (entry.classification === "recent_known_peer_stash") recentPeerStashCount += 1;
863
- }
864
- return {
865
- output: value,
866
- foreignLockCount,
867
- recentPeerStashCount
868
- };
869
- }
870
- function peerCheckFailure(reason, detail = {}) {
871
- return {
872
- exitCode: ExitCode.PREFLIGHT_FAILED,
873
- result: err("PREFLIGHT_FAILED", { reason, ...detail })
874
- };
875
- }
876
- function runManagedWritePeerGate(vault, mode, deps) {
877
- try {
878
- const check = (deps.syncPeers ?? DEFAULT_DEPS.syncPeers)({ vault });
879
- if (check.exitCode !== ExitCode.OK || !check.result.ok) {
880
- return peerCheckFailure("peer-check-failed");
881
- }
882
- const validated = validateSyncPeersOutput(check.result.data);
883
- if (!validated) {
884
- return peerCheckFailure("peer-check-failed");
885
- }
886
- const { output: peerOutput, foreignLockCount, recentPeerStashCount } = validated;
887
- const nonWriterBlockingSignal = foreignLockCount > 0 || recentPeerStashCount > 0;
888
- const writerOnly = peerOutput.managed_writers.blocking && !nonWriterBlockingSignal;
889
- if (mode !== "git-writer" && writerOnly) return null;
890
- const managedWriterBlocking = mode === "git-writer" && peerOutput.managed_writers.blocking;
891
- const hasKnownBlockingSignal = foreignLockCount > 0 || managedWriterBlocking || recentPeerStashCount > 0;
892
- if (!peerOutput.blocking) {
893
- return hasKnownBlockingSignal ? peerCheckFailure("peer-check-failed") : null;
894
- }
895
- if (managedWriterBlocking) {
896
- return peerCheckFailure("live-writer-overlap", {
897
- managed_writer_count: peerOutput.managed_writers.count,
898
- managed_writer_kinds: peerOutput.managed_writers.kinds.slice(0, 8),
899
- blocking: true
900
- });
901
- }
902
- if (foreignLockCount > 0) {
903
- return peerCheckFailure("peer-lock", {
904
- foreign_lock_count: foreignLockCount,
905
- blocking: true
906
- });
907
- }
908
- if (recentPeerStashCount > 0) {
909
- return peerCheckFailure("recent-peer-stash", {
910
- recent_peer_stash_count: recentPeerStashCount,
911
- stash_classification: "recent_known_peer_stash",
912
- blocking: true
913
- });
914
- }
915
- return peerCheckFailure("peer-blocked", { blocking: true });
916
- } catch {
917
- return peerCheckFailure("peer-check-failed");
918
- }
919
- }
920
- function preflightBlocker(vault) {
921
- const unmerged = hasUnmergedPaths(vault);
922
- if (unmerged.length > 0) {
923
- return {
924
- reason: "unmerged-paths",
925
- operation_id: findReviewRequiredOp(vault),
926
- unmerged_paths: unmerged
927
- };
928
- }
929
- if (hasActiveGitSequencer(vault)) {
930
- return { reason: "git-operation-in-progress" };
931
- }
932
- supersedeStaleReviewRequiredJournals(vault, {
933
- by: "skillwiki-managed-write-preflight",
934
- requireClean: false
935
- });
936
- const op = findReviewRequiredOp(vault);
937
- if (op) return { reason: "review-required", operation_id: op };
938
- return null;
939
- }
940
- function hasFleetManifest(vault) {
941
- return existsSync3(join3(vault, FLEET_REL_PATH));
942
- }
943
- function isGitVault(vault) {
944
- return Boolean(git(vault, ["rev-parse", "--absolute-git-dir"]));
945
- }
946
- async function runManagedWritePreflight(input, deps = DEFAULT_DEPS) {
947
- const mutationVault = resolve2(input.vault);
948
- let convergenceVault = input.convergenceVault && resolve2(input.convergenceVault) !== mutationVault ? resolve2(input.convergenceVault) : void 0;
949
- let convergenceSource = convergenceVault ? "explicit" : "single-path";
950
- const mutationBlocker = preflightBlocker(mutationVault);
951
- if (mutationBlocker) {
952
- return {
953
- exitCode: ExitCode.PREFLIGHT_FAILED,
954
- result: err("PREFLIGHT_FAILED", {
955
- reason: mutationBlocker.reason,
956
- operation_id: mutationBlocker.operation_id,
957
- unmerged_paths: mutationBlocker.unmerged_paths
958
- })
959
- };
960
- }
961
- const fleet = await loadFleetManifestAndHost({
962
- vault: mutationVault,
963
- hostId: input.hostId,
964
- env: input.env,
965
- home: input.home,
966
- cwd: input.cwd,
967
- osHostname: input.osHostname,
968
- user: input.user
969
- });
970
- if (!fleet) {
971
- if (hasFleetManifest(mutationVault)) {
972
- return {
973
- exitCode: ExitCode.PREFLIGHT_FAILED,
974
- result: err("PREFLIGHT_FAILED", { reason: "fleet-unreadable" })
975
- };
976
- }
977
- const gitVault2 = isGitVault(mutationVault) ? mutationVault : null;
978
- const head = gitVault2 ? git(gitVault2, ["rev-parse", "HEAD"]) || null : null;
979
- return {
980
- exitCode: ExitCode.OK,
981
- result: ok({
982
- mode: "standalone",
983
- mutation_vault: mutationVault,
984
- git_vault: gitVault2,
985
- base_oid: head,
986
- converged: false,
987
- ...convergenceVault ? { convergence_vault: convergenceVault } : {},
988
- convergence_source: convergenceSource
989
- })
990
- };
991
- }
992
- if (fleet.identityStatus === "unknown" || fleet.identityStatus === "invalid" || !fleet.hostId) {
993
- return {
994
- exitCode: ExitCode.PREFLIGHT_FAILED,
995
- result: err("PREFLIGHT_FAILED", {
996
- reason: "fleet-identity-unresolved",
997
- identity_status: fleet.identityStatus,
998
- host_id: fleet.hostId
999
- })
1000
- };
1001
- }
1002
- const host = fleet.manifest.hosts[fleet.hostId];
1003
- if (!host) {
1004
- return {
1005
- exitCode: ExitCode.PREFLIGHT_FAILED,
1006
- result: err("PREFLIGHT_FAILED", { reason: "fleet-host-missing", host_id: fleet.hostId })
1007
- };
1008
- }
1009
- const writesGithub = host.writes_to.includes("github");
1010
- if (!writesGithub) {
1011
- return {
1012
- exitCode: ExitCode.OK,
1013
- result: ok({
1014
- mode: "immutable-record",
1015
- host_id: fleet.hostId,
1016
- mutation_vault: mutationVault,
1017
- git_vault: null,
1018
- base_oid: null,
1019
- converged: false,
1020
- ...convergenceVault ? { convergence_vault: convergenceVault } : {},
1021
- convergence_source: convergenceSource
1022
- })
1023
- };
1024
- }
1025
- if (!convergenceVault && host.role === "snapshotter" && host.protected === true) {
1026
- const home = input.home ?? input.env?.HOME ?? process.env.HOME ?? "";
1027
- const configured = (deps.resolveConfiguredSnapshotWorktree ?? resolveConfiguredSnapshotWorktree)(home);
1028
- if (!configured) {
1029
- return {
1030
- exitCode: ExitCode.PREFLIGHT_FAILED,
1031
- result: err("PREFLIGHT_FAILED", {
1032
- reason: "convergence-vault-not-configured",
1033
- host_id: fleet.hostId,
1034
- mutation_vault: mutationVault
1035
- })
1036
- };
1037
- }
1038
- convergenceVault = resolve2(configured);
1039
- convergenceSource = "configured";
1040
- if (convergenceVault === mutationVault) {
1041
- return {
1042
- exitCode: ExitCode.PREFLIGHT_FAILED,
1043
- result: err("PREFLIGHT_FAILED", {
1044
- reason: "convergence-vault-not-distinct",
1045
- host_id: fleet.hostId,
1046
- mutation_vault: mutationVault,
1047
- convergence_vault: convergenceVault
1048
- })
1049
- };
1050
- }
1051
- }
1052
- const gitVault = convergenceVault ?? mutationVault;
1053
- if (convergenceVault) {
1054
- if (!isGitVault(convergenceVault)) {
1055
- return {
1056
- exitCode: ExitCode.PREFLIGHT_FAILED,
1057
- result: err("PREFLIGHT_FAILED", {
1058
- reason: "convergence-vault-not-git",
1059
- convergence_vault: convergenceVault
1060
- })
1061
- };
1062
- }
1063
- const convergenceHasFleet = hasFleetManifest(convergenceVault);
1064
- if (convergenceSource === "configured" && !convergenceHasFleet) {
1065
- return {
1066
- exitCode: ExitCode.PREFLIGHT_FAILED,
1067
- result: err("PREFLIGHT_FAILED", {
1068
- reason: "convergence-vault-fleet-missing",
1069
- host_id: fleet.hostId,
1070
- convergence_vault: convergenceVault
1071
- })
1072
- };
1073
- }
1074
- const gitBlocker = preflightBlocker(convergenceVault);
1075
- if (gitBlocker) {
1076
- return {
1077
- exitCode: ExitCode.PREFLIGHT_FAILED,
1078
- result: err("PREFLIGHT_FAILED", {
1079
- reason: gitBlocker.reason,
1080
- operation_id: gitBlocker.operation_id,
1081
- unmerged_paths: gitBlocker.unmerged_paths,
1082
- convergence_vault: convergenceVault
1083
- })
1084
- };
1085
- }
1086
- }
1087
- if (convergenceVault && hasFleetManifest(convergenceVault)) {
1088
- const convergeFleetCtx = await loadFleetManifestAndHost({
1089
- vault: convergenceVault,
1090
- hostId: input.hostId ?? fleet.hostId,
1091
- env: input.env,
1092
- home: input.home,
1093
- cwd: input.cwd,
1094
- osHostname: input.osHostname,
1095
- user: input.user
1096
- });
1097
- if (!convergeFleetCtx || convergeFleetCtx.identityStatus !== "known" || convergeFleetCtx.hostId !== fleet.hostId) {
1098
- return {
1099
- exitCode: ExitCode.PREFLIGHT_FAILED,
1100
- result: err("PREFLIGHT_FAILED", {
1101
- reason: "convergence-vault-identity-mismatch",
1102
- host_id: fleet.hostId,
1103
- convergence_host_id: convergeFleetCtx?.hostId,
1104
- convergence_identity_status: convergeFleetCtx?.identityStatus,
1105
- convergence_vault: convergenceVault
1106
- })
1107
- };
1108
- }
1109
- }
1110
- const dualPathMeta = convergenceVault ? { convergence_vault: convergenceVault } : {};
1111
- const converge = await deps.converge({
1112
- vault: gitVault,
1113
- lockToken: convergenceVault ? void 0 : input.lockToken,
1114
- env: input.env,
1115
- home: input.home
1116
- });
1117
- if (!converge.ok) {
1118
- const exitCode = converge.error === "PREFLIGHT_FAILED" ? ExitCode.PREFLIGHT_FAILED : ExitCode.SYNC_PULL_FAILED;
1119
- return { exitCode, result: converge };
1120
- }
1121
- const baseOid = git(gitVault, ["rev-parse", "HEAD"]);
1122
- if (!baseOid) {
1123
- return {
1124
- exitCode: ExitCode.PREFLIGHT_FAILED,
1125
- result: err("PREFLIGHT_FAILED", {
1126
- reason: "missing-head-after-converge",
1127
- ...dualPathMeta
1128
- })
1129
- };
1130
- }
1131
- return {
1132
- exitCode: ExitCode.OK,
1133
- result: ok({
1134
- mode: "git-writer",
1135
- host_id: fleet.hostId,
1136
- mutation_vault: mutationVault,
1137
- git_vault: gitVault,
1138
- base_oid: baseOid,
1139
- converged: true,
1140
- helper_path: converge.data.helper_path,
1141
- ...dualPathMeta,
1142
- convergence_source: convergenceSource
1143
- })
1144
- };
1145
- }
1146
- async function runManagedWriteTransaction(input, deps = DEFAULT_DEPS) {
1147
- const mutationVault = resolve2(input.vault);
1148
- const lock = acquireManagedWriteLock(mutationVault, input.command, {
1149
- gitStateVault: input.convergenceVault ? resolve2(input.convergenceVault) : mutationVault
1150
- });
1151
- if (!lock.ok) {
1152
- return { exitCode: ExitCode.SYNC_LOCK_HELD, result: lock };
1153
- }
1154
- const handle = lock.data;
1155
- try {
1156
- const preflightInput = {
1157
- vault: mutationVault,
1158
- command: input.command,
1159
- convergenceVault: input.convergenceVault,
1160
- hostId: input.hostId,
1161
- lockToken: handle.ownerToken,
1162
- env: input.env,
1163
- home: input.home,
1164
- cwd: input.cwd,
1165
- osHostname: input.osHostname,
1166
- user: input.user
1167
- };
1168
- const preflight = input.preflight ? await input.preflight(preflightInput) : await runManagedWritePreflight(preflightInput, deps);
1169
- if (!preflight.result.ok) {
1170
- return { exitCode: preflight.exitCode, result: preflight.result };
1171
- }
1172
- const receipt = preflight.result.data;
1173
- if (receipt.mutation_vault !== mutationVault) {
1174
- return {
1175
- exitCode: ExitCode.PREFLIGHT_FAILED,
1176
- result: err("PREFLIGHT_FAILED", {
1177
- reason: "mutation-vault-receipt-mismatch",
1178
- expected: mutationVault,
1179
- actual: receipt.mutation_vault
1180
- })
1181
- };
1182
- }
1183
- if (receipt.mode === "immutable-record" && !input.allowImmutableRecord) {
1184
- return {
1185
- exitCode: ExitCode.PREFLIGHT_FAILED,
1186
- result: err("PREFLIGHT_FAILED", {
1187
- reason: "immutable-record-not-enabled",
1188
- message: "Release A rejects immutable-record mode; event mode arrives in Release B",
1189
- host_id: receipt.host_id
1190
- })
1191
- };
1192
- }
1193
- const peerGate = runManagedWritePeerGate(mutationVault, receipt.mode, deps);
1194
- if (peerGate) return peerGate;
1195
- return await input.mutate(receipt);
1196
- } finally {
1197
- releaseManagedWriteLock(handle);
1198
- }
1199
- }
1200
-
1201
- export {
1202
- VAULT_COMMIT_PATHSPEC,
1203
- stageVaultContentChanges,
1204
- runSyncStatus,
1205
- runSyncPush,
1206
- runSyncPull,
1207
- runSyncPeers,
1208
- runSyncLock,
1209
- runSyncUnlock,
1210
- runManagedWritePreflight,
1211
- runManagedWriteTransaction
1212
- };