@indigoai-us/hq-cloud 6.15.31 → 6.15.32

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.
Files changed (125) hide show
  1. package/dist/bandwidth.d.ts +128 -0
  2. package/dist/bandwidth.d.ts.map +1 -0
  3. package/dist/bandwidth.js +254 -0
  4. package/dist/bandwidth.js.map +1 -0
  5. package/dist/bandwidth.test.d.ts +2 -0
  6. package/dist/bandwidth.test.d.ts.map +1 -0
  7. package/dist/bandwidth.test.js +243 -0
  8. package/dist/bandwidth.test.js.map +1 -0
  9. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  10. package/dist/bin/sync-runner-company.js +13 -24
  11. package/dist/bin/sync-runner-company.js.map +1 -1
  12. package/dist/bin/sync-runner-company.test.js +71 -0
  13. package/dist/bin/sync-runner-company.test.js.map +1 -1
  14. package/dist/bin/sync-runner-watch-loop.d.ts +10 -0
  15. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  16. package/dist/bin/sync-runner-watch-loop.js +181 -16
  17. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  18. package/dist/bin/sync-runner-watch-routes.d.ts +16 -1
  19. package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
  20. package/dist/bin/sync-runner-watch-routes.js +40 -2
  21. package/dist/bin/sync-runner-watch-routes.js.map +1 -1
  22. package/dist/bin/sync-runner-watch-routes.test.js +53 -1
  23. package/dist/bin/sync-runner-watch-routes.test.js.map +1 -1
  24. package/dist/bin/sync-runner.d.ts +15 -3
  25. package/dist/bin/sync-runner.d.ts.map +1 -1
  26. package/dist/bin/sync-runner.js +66 -13
  27. package/dist/bin/sync-runner.js.map +1 -1
  28. package/dist/bin/sync-runner.test.js +433 -13
  29. package/dist/bin/sync-runner.test.js.map +1 -1
  30. package/dist/cli/conflict.d.ts +25 -2
  31. package/dist/cli/conflict.d.ts.map +1 -1
  32. package/dist/cli/conflict.js +29 -4
  33. package/dist/cli/conflict.js.map +1 -1
  34. package/dist/cli/share.d.ts +40 -4
  35. package/dist/cli/share.d.ts.map +1 -1
  36. package/dist/cli/share.js +478 -86
  37. package/dist/cli/share.js.map +1 -1
  38. package/dist/cli/share.test.js +845 -150
  39. package/dist/cli/share.test.js.map +1 -1
  40. package/dist/cli/sync-scope.test.js +147 -18
  41. package/dist/cli/sync-scope.test.js.map +1 -1
  42. package/dist/cli/sync.d.ts +20 -0
  43. package/dist/cli/sync.d.ts.map +1 -1
  44. package/dist/cli/sync.js +261 -29
  45. package/dist/cli/sync.js.map +1 -1
  46. package/dist/cli/sync.test.js +519 -90
  47. package/dist/cli/sync.test.js.map +1 -1
  48. package/dist/journal-delta-writes.test.d.ts +2 -0
  49. package/dist/journal-delta-writes.test.d.ts.map +1 -0
  50. package/dist/journal-delta-writes.test.js +140 -0
  51. package/dist/journal-delta-writes.test.js.map +1 -0
  52. package/dist/journal.d.ts +1 -0
  53. package/dist/journal.d.ts.map +1 -1
  54. package/dist/journal.js +155 -25
  55. package/dist/journal.js.map +1 -1
  56. package/dist/journal.test.js +29 -11
  57. package/dist/journal.test.js.map +1 -1
  58. package/dist/object-io.d.ts +34 -13
  59. package/dist/object-io.d.ts.map +1 -1
  60. package/dist/object-io.js +154 -29
  61. package/dist/object-io.js.map +1 -1
  62. package/dist/operation-lock.d.ts +12 -0
  63. package/dist/operation-lock.d.ts.map +1 -1
  64. package/dist/operation-lock.js +103 -21
  65. package/dist/operation-lock.js.map +1 -1
  66. package/dist/operation-lock.test.js +90 -0
  67. package/dist/operation-lock.test.js.map +1 -1
  68. package/dist/outcome-telemetry.d.ts +7 -1
  69. package/dist/outcome-telemetry.d.ts.map +1 -1
  70. package/dist/outcome-telemetry.js +15 -9
  71. package/dist/outcome-telemetry.js.map +1 -1
  72. package/dist/outcome-telemetry.test.js +15 -0
  73. package/dist/outcome-telemetry.test.js.map +1 -1
  74. package/dist/outposts/client.d.ts +2 -2
  75. package/dist/outposts/client.d.ts.map +1 -1
  76. package/dist/outposts/client.js +4 -3
  77. package/dist/outposts/client.js.map +1 -1
  78. package/dist/outposts/command.d.ts.map +1 -1
  79. package/dist/outposts/command.js +9 -6
  80. package/dist/outposts/command.js.map +1 -1
  81. package/dist/outposts/command.test.js +59 -2
  82. package/dist/outposts/command.test.js.map +1 -1
  83. package/dist/outposts/remote-command.d.ts +8 -8
  84. package/dist/outposts/remote-command.d.ts.map +1 -1
  85. package/dist/outposts/remote-command.js +10 -8
  86. package/dist/outposts/remote-command.js.map +1 -1
  87. package/dist/prefix-coalesce.d.ts +14 -0
  88. package/dist/prefix-coalesce.d.ts.map +1 -1
  89. package/dist/prefix-coalesce.js +29 -0
  90. package/dist/prefix-coalesce.js.map +1 -1
  91. package/dist/prefix-coalesce.test.js +22 -1
  92. package/dist/prefix-coalesce.test.js.map +1 -1
  93. package/dist/skill-telemetry.d.ts +6 -1
  94. package/dist/skill-telemetry.d.ts.map +1 -1
  95. package/dist/skill-telemetry.js +14 -8
  96. package/dist/skill-telemetry.js.map +1 -1
  97. package/dist/skill-telemetry.test.js +43 -0
  98. package/dist/skill-telemetry.test.js.map +1 -1
  99. package/dist/sts-credential-refresh.test.js +87 -2
  100. package/dist/sts-credential-refresh.test.js.map +1 -1
  101. package/dist/sync/state-store.d.ts.map +1 -1
  102. package/dist/sync/state-store.js +20 -1
  103. package/dist/sync/state-store.js.map +1 -1
  104. package/dist/sync/state-store.test.js +64 -0
  105. package/dist/sync/state-store.test.js.map +1 -1
  106. package/dist/sync-core.d.ts.map +1 -1
  107. package/dist/sync-core.js +7 -2
  108. package/dist/sync-core.js.map +1 -1
  109. package/dist/sync-core.test.js +36 -0
  110. package/dist/sync-core.test.js.map +1 -1
  111. package/dist/telemetry.d.ts +30 -6
  112. package/dist/telemetry.d.ts.map +1 -1
  113. package/dist/telemetry.js +331 -135
  114. package/dist/telemetry.js.map +1 -1
  115. package/dist/telemetry.test.js +403 -0
  116. package/dist/telemetry.test.js.map +1 -1
  117. package/dist/types.d.ts +26 -3
  118. package/dist/types.d.ts.map +1 -1
  119. package/dist/watcher.d.ts +76 -0
  120. package/dist/watcher.d.ts.map +1 -1
  121. package/dist/watcher.js +105 -3
  122. package/dist/watcher.js.map +1 -1
  123. package/dist/watcher.test.js +165 -1
  124. package/dist/watcher.test.js.map +1 -1
  125. package/package.json +1 -1
@@ -53,9 +53,49 @@ vi.mock("readline", () => ({
53
53
  }));
54
54
  import * as readline from "readline";
55
55
  import { share, isForbiddenCompanyVaultKey, UnreachablePushPathsError, currentUploadBytes, _testing as shareTesting } from "./share.js";
56
- import { deleteRemoteFile, downloadFile, headRemoteFile, uploadFile, uploadSymlink, WindowsSymlinkPrivilegeError } from "../s3.js";
56
+ import { deleteRemoteFile, downloadFile, headRemoteFile, primeObjectTransport, primeUploads, uploadFile, uploadSymlink, WindowsSymlinkPrivilegeError, } from "../s3.js";
57
57
  import { VaultAuthError } from "../vault-client.js";
58
- import { hashFile } from "../journal.js";
58
+ import { hashFile, readJournal, writeJournal } from "../journal.js";
59
+ /**
60
+ * Read a journal the way production does.
61
+ *
62
+ * These tests used to `JSON.parse` `sync-journal.<slug>.json` directly, back
63
+ * when that file held a full copy of the journal. It is now a small locator —
64
+ * the authority is the v3 state store — so the assertions below go through
65
+ * `readJournal`, which is what the sync engine itself uses. The slug is
66
+ * recovered from the path so each call site keeps naming the journal it means.
67
+ */
68
+ /**
69
+ * Seed a journal the way production does.
70
+ *
71
+ * These fixtures used to write `sync-journal.<slug>.json` by hand, which worked
72
+ * only while that file WAS the journal. It is a locator now, so a hand-written
73
+ * copy is simply ignored and the test silently runs against an empty journal.
74
+ * Going through `writeJournal` puts the state where the engine actually reads
75
+ * it, and migrates v1 fixtures on the way in exactly as a real upgrade would.
76
+ */
77
+ function seedJournalAt(journalPath, journal) {
78
+ const slug = path
79
+ .basename(journalPath)
80
+ .replace(/^sync-journal\./, "")
81
+ .replace(/\.json$/, "");
82
+ writeJournal(slug, journal);
83
+ }
84
+ function journalAt(journalPath) {
85
+ const slug = path
86
+ .basename(journalPath)
87
+ .replace(/^sync-journal\./, "")
88
+ .replace(/\.json$/, "");
89
+ return readJournal(slug);
90
+ }
91
+ function sidecarIn(dir) {
92
+ const found = fs.readdirSync(dir).filter((name) => name.includes(".conflict-"));
93
+ expect(found).toHaveLength(1);
94
+ return path.join(dir, found[0]);
95
+ }
96
+ function conflictIndexAt(hqRoot) {
97
+ return JSON.parse(fs.readFileSync(path.join(hqRoot, ".hq-conflicts", "index.json"), "utf-8"));
98
+ }
59
99
  const mockConfig = {
60
100
  apiUrl: "https://vault-api.test",
61
101
  authToken: "test-jwt-token",
@@ -369,6 +409,74 @@ describe("share", () => {
369
409
  // Remote key must be company-relative, not hqRoot-relative
370
410
  expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "test.md", undefined, expect.anything());
371
411
  });
412
+ it("replans an upload from the bytes present when its action starts", async () => {
413
+ const companyRoot = path.join(tmpDir, "companies", "acme");
414
+ fs.mkdirSync(companyRoot, { recursive: true });
415
+ const testFile = path.join(companyRoot, "changing.md");
416
+ fs.writeFileSync(testFile, "body seen by the whole-run preview");
417
+ let uploadedBody = "";
418
+ vi.mocked(uploadFile).mockImplementationOnce(async (_ctx, localPath) => {
419
+ uploadedBody = fs.readFileSync(localPath, "utf-8");
420
+ return { etag: '"fresh-upload-etag"' };
421
+ });
422
+ let changedAfterPlan = false;
423
+ let executionHeartbeatCalls = 0;
424
+ const result = await share({
425
+ paths: [testFile],
426
+ company: "acme",
427
+ vaultConfig: mockConfig,
428
+ hqRoot: tmpDir,
429
+ onEvent: (event) => {
430
+ if (event.type === "plan" && !changedAfterPlan) {
431
+ changedAfterPlan = true;
432
+ fs.writeFileSync(testFile, "body present when upload executes");
433
+ }
434
+ },
435
+ onHeartbeat: () => {
436
+ if (changedAfterPlan)
437
+ executionHeartbeatCalls++;
438
+ },
439
+ });
440
+ expect(changedAfterPlan).toBe(true);
441
+ expect(executionHeartbeatCalls).toBeGreaterThan(0);
442
+ expect(result.filesUploaded).toBe(1);
443
+ expect(uploadedBody).toBe("body present when upload executes");
444
+ expect(journalAt(path.join(stateDir, "sync-journal.acme.json")).files["changing.md"])
445
+ .toMatchObject({
446
+ hash: hashFile(testFile),
447
+ remoteEtag: "fresh-upload-etag",
448
+ direction: "up",
449
+ });
450
+ });
451
+ it("restarts the per-file decision when local bytes change during remote HEAD", async () => {
452
+ const companyRoot = path.join(tmpDir, "companies", "acme");
453
+ fs.mkdirSync(companyRoot, { recursive: true });
454
+ const testFile = path.join(companyRoot, "head-race.md");
455
+ fs.writeFileSync(testFile, "body before HEAD");
456
+ vi.mocked(headRemoteFile).mockImplementationOnce(async () => {
457
+ fs.writeFileSync(testFile, "body written while HEAD was in flight");
458
+ return null;
459
+ });
460
+ let uploadedBody = "";
461
+ vi.mocked(uploadFile).mockImplementationOnce(async (_ctx, localPath) => {
462
+ uploadedBody = fs.readFileSync(localPath, "utf-8");
463
+ return { etag: '"head-race-etag"' };
464
+ });
465
+ const result = await share({
466
+ paths: [testFile],
467
+ company: "acme",
468
+ vaultConfig: mockConfig,
469
+ hqRoot: tmpDir,
470
+ });
471
+ expect(result.filesUploaded).toBe(1);
472
+ expect(uploadedBody).toBe("body written while HEAD was in flight");
473
+ expect(journalAt(path.join(stateDir, "sync-journal.acme.json")).files["head-race.md"])
474
+ .toMatchObject({
475
+ hash: hashFile(testFile),
476
+ remoteEtag: "head-race-etag",
477
+ direction: "up",
478
+ });
479
+ });
372
480
  it("propagates a per-file vault 401 to the runner auth boundary", async () => {
373
481
  const companyRoot = path.join(tmpDir, "companies", "acme");
374
482
  fs.mkdirSync(companyRoot, { recursive: true });
@@ -528,7 +636,7 @@ describe("share", () => {
528
636
  const { hashFile } = await import("../journal.js");
529
637
  const hash = hashFile(testFile);
530
638
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
531
- fs.writeFileSync(journalPath, JSON.stringify({
639
+ seedJournalAt(journalPath, ({
532
640
  version: "1",
533
641
  lastSync: new Date().toISOString(),
534
642
  files: {
@@ -559,7 +667,7 @@ describe("share", () => {
559
667
  // Journal has a stale hash for this path — simulating "local has been
560
668
  // edited since the last push".
561
669
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
562
- fs.writeFileSync(journalPath, JSON.stringify({
670
+ seedJournalAt(journalPath, ({
563
671
  version: "1",
564
672
  lastSync: new Date().toISOString(),
565
673
  files: {
@@ -581,6 +689,226 @@ describe("share", () => {
581
689
  expect(result.filesUploaded).toBe(1);
582
690
  expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "changed.md", undefined, expect.anything());
583
691
  });
692
+ // ── Stat fast path for change detection ─────────────────────────────────
693
+ // Push used to hash EVERY regular file every pass before deciding whether to
694
+ // skip it — half a million reads on a real vault — because stat metadata was
695
+ // considered untrustworthy: a tool can rewrite a file to the same length and
696
+ // restore its mtime, and a size+mtime check would then skip that edit
697
+ // forever.
698
+ //
699
+ // These tests pin the narrower claim the fast path actually rests on: size,
700
+ // mtime AND ctime together. ctime is the inode-change stamp, and userspace
701
+ // cannot restore it — `cp -p`, `touch -r` and `rsync --times` all reset mtime
702
+ // while ctime moves to now. So the same-length-plus-restored-mtime rewrite
703
+ // that the old comment warned about is still caught, without reading a byte.
704
+ //
705
+ // Differential proof: drop ctime from the comparison and the "restored mtime"
706
+ // test below fails — the edit is skipped and never uploaded.
707
+ describe("stat fast path for change detection", () => {
708
+ const CONTENT_A = "aaaaaaaaaa"; // 10 bytes
709
+ const CONTENT_B = "bbbbbbbbbb"; // 10 bytes — same length, different bytes
710
+ /** Write `body` at `key` under the acme company root; return its path. */
711
+ function writeFile(key, body) {
712
+ const companyRoot = path.join(tmpDir, "companies", "acme");
713
+ fs.mkdirSync(companyRoot, { recursive: true });
714
+ const abs = path.join(companyRoot, key);
715
+ fs.writeFileSync(abs, body);
716
+ return abs;
717
+ }
718
+ function seed(key, entry) {
719
+ seedJournalAt(path.join(stateDir, "sync-journal.acme.json"), {
720
+ version: "2",
721
+ lastSync: new Date().toISOString(),
722
+ files: { [key]: { syncedAt: new Date().toISOString(), direction: "up", ...entry } },
723
+ pulls: [],
724
+ });
725
+ }
726
+ function push(abs, onConflict) {
727
+ return share({
728
+ paths: [abs],
729
+ company: "acme",
730
+ vaultConfig: mockConfig,
731
+ hqRoot: tmpDir,
732
+ skipUnchanged: true,
733
+ ...(onConflict ? { onConflict } : {}),
734
+ });
735
+ }
736
+ it("skips a file whose size, mtime and ctime all match — without reading it", async () => {
737
+ const abs = writeFile("untouched.md", CONTENT_A);
738
+ const stat = fs.lstatSync(abs);
739
+ // The recorded hash is deliberately WRONG. Hashing the file would find a
740
+ // mismatch and upload, so a skip here can only mean the bytes were never
741
+ // read — which is the whole point of the fast path.
742
+ seed("untouched.md", {
743
+ hash: "deliberately-not-the-hash-of-this-file",
744
+ size: stat.size,
745
+ mtimeMs: stat.mtimeMs,
746
+ ctimeMs: stat.ctimeMs,
747
+ });
748
+ const result = await push(abs);
749
+ expect(result.filesSkipped).toBe(1);
750
+ expect(result.filesUploaded).toBe(0);
751
+ expect(uploadFile).not.toHaveBeenCalled();
752
+ });
753
+ it("re-reads when the size differs", async () => {
754
+ const abs = writeFile("resized.md", CONTENT_A + "extra");
755
+ const stat = fs.lstatSync(abs);
756
+ seed("resized.md", {
757
+ hash: hashFile(abs) + "-stale",
758
+ size: CONTENT_A.length, // shorter than what is on disk now
759
+ mtimeMs: stat.mtimeMs,
760
+ ctimeMs: stat.ctimeMs,
761
+ });
762
+ const result = await push(abs);
763
+ expect(result.filesUploaded).toBe(1);
764
+ expect(uploadFile).toHaveBeenCalled();
765
+ });
766
+ it("re-reads when the mtime differs", async () => {
767
+ const abs = writeFile("retimed.md", CONTENT_B);
768
+ const stat = fs.lstatSync(abs);
769
+ seed("retimed.md", {
770
+ hash: "hash-of-the-previous-body",
771
+ size: stat.size, // same length
772
+ mtimeMs: stat.mtimeMs - 60_000, // journal remembers an older edit time
773
+ ctimeMs: stat.ctimeMs,
774
+ });
775
+ const result = await push(abs);
776
+ expect(result.filesUploaded).toBe(1);
777
+ expect(uploadFile).toHaveBeenCalled();
778
+ });
779
+ it("re-reads a same-length rewrite whose mtime was restored — ctime still moves", async () => {
780
+ // The exact case the old hash-everything comment existed to defend
781
+ // against, as produced by `cp -p`, `touch -r` or `rsync --times`.
782
+ const abs = writeFile("mtime-restored.md", CONTENT_A);
783
+ const before = fs.lstatSync(abs);
784
+ await new Promise((resolve) => setTimeout(resolve, 5));
785
+ fs.writeFileSync(abs, CONTENT_B); // same length, different bytes
786
+ fs.utimesSync(abs, before.atime, before.mtime); // put the old mtime back
787
+ const after = fs.lstatSync(abs);
788
+ // Preconditions. The disguise worked on size, and as closely on mtime as
789
+ // the OS allows — `utimes` takes whole milliseconds, so a restored stamp
790
+ // lands within 1ms of the original rather than exactly on it. It failed
791
+ // on ctime, which is the only signal left.
792
+ expect(after.size).toBe(before.size);
793
+ expect(Math.abs(after.mtimeMs - before.mtimeMs)).toBeLessThan(1);
794
+ expect(after.ctimeMs).not.toBe(before.ctimeMs);
795
+ // Seed the mtime that is actually on disk, so mtime matches EXACTLY and
796
+ // cannot be what triggers the re-read. Only the stale ctime can.
797
+ seed("mtime-restored.md", {
798
+ hash: "hash-of-content-a",
799
+ size: before.size,
800
+ mtimeMs: after.mtimeMs,
801
+ ctimeMs: before.ctimeMs,
802
+ });
803
+ const result = await push(abs);
804
+ expect(result.filesUploaded).toBe(1);
805
+ expect(uploadFile).toHaveBeenCalled();
806
+ });
807
+ it("falls back to reading when the journal predates stat tracking, then stamps it", async () => {
808
+ // Entries written before ctime was recorded carry no stat metadata. They
809
+ // must not be skipped on faith; they are read once and stamped so the
810
+ // next pass takes the fast path.
811
+ const abs = writeFile("legacy.md", CONTENT_A);
812
+ seed("legacy.md", { hash: hashFile(abs), size: CONTENT_A.length });
813
+ const result = await push(abs);
814
+ expect(result.filesSkipped).toBe(1);
815
+ expect(uploadFile).not.toHaveBeenCalled();
816
+ const entry = journalAt(path.join(stateDir, "sync-journal.acme.json")).files["legacy.md"];
817
+ const stat = fs.lstatSync(abs);
818
+ expect(entry.mtimeMs).toBe(stat.mtimeMs);
819
+ expect(entry.ctimeMs).toBe(stat.ctimeMs);
820
+ });
821
+ it("never lets the fast path skip a localDiverges entry", async () => {
822
+ // A pull-held divergent entry stores the LOCAL hash and local stat, so
823
+ // every field matches by construction. Skipping on that asserts "local
824
+ // matches remote", which is exactly what the flag exists to deny.
825
+ const abs = writeFile("divergent.md", CONTENT_A);
826
+ const stat = fs.lstatSync(abs);
827
+ seed("divergent.md", {
828
+ hash: hashFile(abs),
829
+ size: stat.size,
830
+ mtimeMs: stat.mtimeMs,
831
+ ctimeMs: stat.ctimeMs,
832
+ localDiverges: true,
833
+ });
834
+ // `keep` so the divergence resolves without an interactive prompt. Under
835
+ // `keep` the file is deliberately NOT uploaded, so the proof that the
836
+ // fast path did not swallow it is that it reached Stage 2 at all: a
837
+ // fast-path skip returns before any remote HEAD is issued.
838
+ await push(abs, "keep");
839
+ expect(headRemoteFile).toHaveBeenCalled();
840
+ });
841
+ it("stamps size, mtime and ctime on upload so the next pass can skip", async () => {
842
+ const abs = writeFile("fresh.md", CONTENT_A);
843
+ const result = await push(abs);
844
+ expect(result.filesUploaded).toBe(1);
845
+ const entry = journalAt(path.join(stateDir, "sync-journal.acme.json")).files["fresh.md"];
846
+ const stat = fs.lstatSync(abs);
847
+ expect(entry.size).toBe(stat.size);
848
+ expect(entry.mtimeMs).toBe(stat.mtimeMs);
849
+ expect(entry.ctimeMs).toBe(stat.ctimeMs);
850
+ });
851
+ it("replans and stamps the new body when the file changes during remote HEAD", async () => {
852
+ // The pass-level preview hashes CONTENT_A, then the asynchronous remote
853
+ // probe changes the file to CONTENT_B. The per-file execution seam must
854
+ // restart from CONTENT_B, upload it now, and journal that exact body — no
855
+ // stale hash and no redundant upload on the next pass.
856
+ const abs = writeFile("edited-mid-flight.md", CONTENT_A);
857
+ let mutated = false;
858
+ vi.mocked(headRemoteFile).mockImplementation(async () => {
859
+ if (!mutated) {
860
+ mutated = true;
861
+ fs.writeFileSync(abs, CONTENT_B); // same length, different bytes
862
+ }
863
+ return null;
864
+ });
865
+ await push(abs);
866
+ const entry = journalAt(path.join(stateDir, "sync-journal.acme.json")).files["edited-mid-flight.md"];
867
+ const currentStat = fs.lstatSync(abs);
868
+ expect(entry.hash).toBe(hashFile(abs));
869
+ expect(entry.mtimeMs).toBe(currentStat.mtimeMs);
870
+ expect(entry.ctimeMs).toBe(currentStat.ctimeMs);
871
+ // The current bytes were already shipped and journaled in this pass.
872
+ vi.mocked(uploadFile).mockClear();
873
+ const second = await push(abs);
874
+ expect(second.filesUploaded).toBe(0);
875
+ expect(uploadFile).not.toHaveBeenCalled();
876
+ });
877
+ it("distrusts whole-second timestamps as a coarse-filesystem signal", () => {
878
+ // Codex P2. FAT/exFAT, some FUSE mounts and cached network filesystems
879
+ // report timestamps rounded to whole seconds, so a same-size rewrite
880
+ // inside one tick can leave both mtime and ctime equal to the journal's
881
+ // and be skipped indefinitely. Every filesystem HQ can trust here (APFS,
882
+ // ext4, NTFS) records sub-second precision, so a pair that is exactly on
883
+ // the second is treated as untrustworthy and sent back to hashing.
884
+ const abs = writeFile("coarse.md", CONTENT_A);
885
+ const coarse = { size: CONTENT_A.length, mtimeMs: 1_700_000_000_000, ctimeMs: 1_700_000_000_000 };
886
+ const spy = vi.spyOn(fs, "lstatSync").mockReturnValue({
887
+ isFile: () => true,
888
+ ...coarse,
889
+ });
890
+ try {
891
+ expect(shareTesting.statMatchesJournal(abs, {
892
+ hash: "h",
893
+ direction: "up",
894
+ syncedAt: new Date().toISOString(),
895
+ ...coarse,
896
+ })).toBe(false);
897
+ // Same values, one of them sub-second: a real filesystem, so trusted.
898
+ const fine = { ...coarse, ctimeMs: coarse.ctimeMs + 0.5 };
899
+ spy.mockReturnValue({ isFile: () => true, ...fine });
900
+ expect(shareTesting.statMatchesJournal(abs, {
901
+ hash: "h",
902
+ direction: "up",
903
+ syncedAt: new Date().toISOString(),
904
+ ...fine,
905
+ })).toBe(true);
906
+ }
907
+ finally {
908
+ spy.mockRestore();
909
+ }
910
+ });
911
+ });
584
912
  // ── Push-side FILE_TOMBSTONE consult (delete-resync, 3B) ────────────────────
585
913
  // An authoritative delete (`hq files delete`) writes a FILE_TOMBSTONE and
586
914
  // removes the S3 object. Without a push-side consult, a behind peer that still
@@ -593,7 +921,7 @@ describe("share", () => {
593
921
  // share.ts makes the first test fail (uploadFile IS called → resurrection).
594
922
  describe("push-side tombstone consult", () => {
595
923
  function seedJournal(key, hash, size) {
596
- fs.writeFileSync(path.join(stateDir, "sync-journal.acme.json"), JSON.stringify({
924
+ seedJournalAt(path.join(stateDir, "sync-journal.acme.json"), ({
597
925
  version: "1",
598
926
  lastSync: new Date().toISOString(),
599
927
  files: {
@@ -630,6 +958,8 @@ describe("share", () => {
630
958
  });
631
959
  // The resurrection is blocked at the source: no upload for the tombstoned key.
632
960
  expect(uploadFile).not.toHaveBeenCalled();
961
+ expect(primeUploads).toHaveBeenCalledWith(expect.anything(), []);
962
+ expect(primeObjectTransport).toHaveBeenCalledWith(expect.anything(), "get", []);
633
963
  expect(result.filesUploaded).toBe(0);
634
964
  expect(result.filesSuppressedByTombstone).toBe(1);
635
965
  expect(events.some((e) => e.type === "upload-suppressed-tombstone" && e.path === "docs/shared.md")).toBe(true);
@@ -736,8 +1066,12 @@ describe("share", () => {
736
1066
  etag: '"remote-new-etag"',
737
1067
  size: 99,
738
1068
  });
1069
+ vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, dest) => {
1070
+ fs.writeFileSync(dest, "remote edit");
1071
+ return undefined;
1072
+ });
739
1073
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
740
- fs.writeFileSync(journalPath, JSON.stringify({
1074
+ seedJournalAt(journalPath, ({
741
1075
  version: "1",
742
1076
  lastSync: new Date().toISOString(),
743
1077
  files: {
@@ -768,6 +1102,24 @@ describe("share", () => {
768
1102
  direction: "push",
769
1103
  resolution: "keep",
770
1104
  });
1105
+ // `keep` is cloud-wins on the push leg too: remote takes the working
1106
+ // name, while the displaced local body remains recoverable beside it.
1107
+ expect(fs.readFileSync(testFile, "utf-8")).toBe("remote edit");
1108
+ expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe("local edit");
1109
+ const journal = journalAt(journalPath);
1110
+ expect(journal.files["drifted.md"]).toMatchObject({
1111
+ hash: hashFile(testFile),
1112
+ remoteEtag: "remote-new-etag",
1113
+ direction: "down",
1114
+ });
1115
+ const index = conflictIndexAt(tmpDir);
1116
+ expect(index.conflicts).toHaveLength(1);
1117
+ expect(index.conflicts[0]).toMatchObject({
1118
+ originalPath: "companies/acme/drifted.md",
1119
+ preserved: "local",
1120
+ });
1121
+ expect(fs.readFileSync(path.join(tmpDir, index.conflicts[0].conflictPath), "utf-8"))
1122
+ .toBe("local edit");
771
1123
  });
772
1124
  it("cloud-authoritative: never pushes a stale local ontology/.last-run over the server value (one-sided-change clobber guard)", async () => {
773
1125
  // Regression for the gardener watermark clobber. A second HQ root sharing this
@@ -789,7 +1141,7 @@ describe("share", () => {
789
1141
  // assert it is never consulted below — the skip is cheap, and a leftover
790
1142
  // mockResolvedValueOnce would otherwise leak into a later test's HEAD queue.
791
1143
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
792
- fs.writeFileSync(journalPath, JSON.stringify({
1144
+ seedJournalAt(journalPath, ({
793
1145
  version: "1",
794
1146
  lastSync: new Date().toISOString(),
795
1147
  files: {
@@ -835,9 +1187,9 @@ describe("share", () => {
835
1187
  //
836
1188
  // Fix: even on first-time upload, if HEAD shows remote exists and
837
1189
  // its content (etag) differs from what we'd put, emit a conflict.
838
- // Under keep, the existing pull-side mirror routine downloads the
839
- // remote bytes to \`<orig>.conflict-<ts>-<short>\` so both versions
840
- // survive on disk; under abort, the company aborts before PUT.
1190
+ // Under keep, cloud takes the original name and local moves to
1191
+ // \`<orig>.conflict-<ts>-<short>\` so both versions survive on disk;
1192
+ // under abort, the company aborts before PUT.
841
1193
  const companyRoot = path.join(tmpDir, "companies", "acme");
842
1194
  fs.mkdirSync(companyRoot, { recursive: true });
843
1195
  const testFile = path.join(companyRoot, "v7b-fresh.txt");
@@ -849,6 +1201,10 @@ describe("share", () => {
849
1201
  etag: '"remote-version-etag"',
850
1202
  size: 40,
851
1203
  });
1204
+ vi.mocked(downloadFile).mockImplementation(async (_ctx, _key, dest) => {
1205
+ fs.writeFileSync(dest, "peer remote content");
1206
+ return undefined;
1207
+ });
852
1208
  // No journal file written — first-time upload contract.
853
1209
  const events = [];
854
1210
  const result = await share({
@@ -860,14 +1216,19 @@ describe("share", () => {
860
1216
  onEvent: (e) => events.push(e),
861
1217
  });
862
1218
  expect(result.conflictPaths).toEqual(["v7b-fresh.txt"]);
863
- // Under keep, local stays as-is (no upload) and the mirror records
864
- // the remote bytes so neither version is lost.
1219
+ // Under keep, no upload occurs: the remote bytes take the working name
1220
+ // and the local body is preserved beside them.
865
1221
  expect(result.filesUploaded).toBe(0);
866
1222
  const conflicts = events.filter((e) => typeof e === "object" && e !== null && e.type === "conflict");
867
1223
  expect(conflicts).toHaveLength(1);
868
1224
  expect(conflicts[0].path).toBe("v7b-fresh.txt");
869
- // Local file unmodified — it must still contain the local content.
870
- expect(fs.readFileSync(testFile, "utf-8")).toBe("macos-fresh-collision-content");
1225
+ expect(fs.readFileSync(testFile, "utf-8")).toBe("peer remote content");
1226
+ expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe("macos-fresh-collision-content");
1227
+ const index = conflictIndexAt(tmpDir);
1228
+ expect(index.conflicts[0]).toMatchObject({
1229
+ originalPath: "companies/acme/v7b-fresh.txt",
1230
+ preserved: "local",
1231
+ });
871
1232
  });
872
1233
  it("first-time-upload-with-cloud-collision: aborts company under --on-conflict abort (Bug #7)", async () => {
873
1234
  // Symmetric to the keep case: when --on-conflict abort is passed, a
@@ -895,11 +1256,11 @@ describe("share", () => {
895
1256
  expect(result.filesUploaded).toBe(0);
896
1257
  });
897
1258
  it("HQ-DESKTOP-54: a privilege refusal while writing the push conflict mirror is a skip, not an error", async () => {
898
- // The keep-mirror also materializes the remote object (to a .conflict-*
899
- // file). On an unprivileged Windows host that throws the typed privilege
1259
+ // Push keep also materializes the remote object (at the original name).
1260
+ // On an unprivileged Windows host that throws the typed privilege
900
1261
  // error; it must be downgraded to the deliberate skip so the push stays
901
1262
  // complete and the runner exits 0 — never type:error. The conflict itself
902
- // is still recorded and the local file is untouched.
1263
+ // is still surfaced and the local rename is rolled back.
903
1264
  const companyRoot = path.join(tmpDir, "companies", "acme");
904
1265
  fs.mkdirSync(companyRoot, { recursive: true });
905
1266
  const testFile = path.join(companyRoot, "v7b-priv.txt");
@@ -910,7 +1271,7 @@ describe("share", () => {
910
1271
  size: 40,
911
1272
  });
912
1273
  // On a real unprivileged host BOTH the fresh-collision reconciled probe and
913
- // the keep-mirror materialize the remote symlink, so both downloadFile calls
1274
+ // the keep adoption materialize the remote symlink, so both downloadFile calls
914
1275
  // refuse. The probe's catch returns "differs" (conflict) without erroring;
915
1276
  // the mirror must downgrade the refusal to the deliberate skip.
916
1277
  vi.mocked(downloadFile)
@@ -931,7 +1292,7 @@ describe("share", () => {
931
1292
  target: "../target.md",
932
1293
  }));
933
1294
  expect(events.some((e) => e.type === "error")).toBe(false);
934
- // The conflict is still recorded; the local file is preserved.
1295
+ // The conflict is still surfaced; the failed adoption restores local.
935
1296
  expect(result.conflictPaths).toEqual(["v7b-priv.txt"]);
936
1297
  expect(fs.readFileSync(testFile, "utf-8")).toBe("local-collision-content");
937
1298
  });
@@ -969,7 +1330,7 @@ describe("share", () => {
969
1330
  direction: "push",
970
1331
  });
971
1332
  expect(fs.readdirSync(companyRoot).filter((name) => name.includes(".conflict-"))).toEqual([]);
972
- const journal = JSON.parse(fs.readFileSync(path.join(stateDir, "sync-journal.acme.json"), "utf-8"));
1333
+ const journal = readJournal("acme");
973
1334
  expect(journal.files["kms-scaffold.md"]).toMatchObject({
974
1335
  direction: "up",
975
1336
  remoteEtag: "288bfa91684ae21f9ae27c061254d412",
@@ -1043,7 +1404,7 @@ describe("share", () => {
1043
1404
  etag: '"0123456789abcdef0123456789abcdef-3"',
1044
1405
  size: 32,
1045
1406
  });
1046
- // The probe (and the subsequent keep-mirror download) return DIFFERENT
1407
+ // The probe (and the subsequent keep adoption download) return DIFFERENT
1047
1408
  // remote bytes.
1048
1409
  vi.mocked(downloadFile).mockImplementation(async (_ctx, _key, dest) => {
1049
1410
  fs.writeFileSync(dest, "a-peers-different-version");
@@ -1062,8 +1423,8 @@ describe("share", () => {
1062
1423
  expect(result.filesUploaded).toBe(0);
1063
1424
  const conflicts = events.filter((e) => typeof e === "object" && e !== null && e.type === "conflict");
1064
1425
  expect(conflicts).toHaveLength(1);
1065
- // Local file untouched under keep.
1066
- expect(fs.readFileSync(testFile, "utf-8")).toBe("my-local-version");
1426
+ expect(fs.readFileSync(testFile, "utf-8")).toBe("a-peers-different-version");
1427
+ expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe("my-local-version");
1067
1428
  });
1068
1429
  describe("conditional-write fence (If-Match / If-None-Match / 412)", () => {
1069
1430
  const fence412 = () => Object.assign(new Error("Precondition failed"), {
@@ -1085,7 +1446,7 @@ describe("share", () => {
1085
1446
  size: 5,
1086
1447
  });
1087
1448
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1088
- fs.writeFileSync(journalPath, JSON.stringify({
1449
+ seedJournalAt(journalPath, ({
1089
1450
  version: "1",
1090
1451
  lastSync: syncedAt,
1091
1452
  files: {
@@ -1123,12 +1484,18 @@ describe("share", () => {
1123
1484
  });
1124
1485
  expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "brand-new.md", undefined, { ifNoneMatch: "*" });
1125
1486
  });
1126
- it("412 under keep: conflict event + remote mirrored + ledger entry + NO journal stamp", async () => {
1487
+ it("412 under keep: adopts the racing remote, preserves local, and converges the journal", async () => {
1127
1488
  const companyRoot = path.join(tmpDir, "companies", "acme");
1128
1489
  fs.mkdirSync(companyRoot, { recursive: true });
1129
1490
  const testFile = path.join(companyRoot, "raced.md");
1130
1491
  fs.writeFileSync(testFile, "my local bytes");
1131
- vi.mocked(headRemoteFile).mockResolvedValueOnce(null);
1492
+ vi.mocked(headRemoteFile)
1493
+ .mockResolvedValueOnce(null)
1494
+ .mockResolvedValueOnce({
1495
+ lastModified: new Date(),
1496
+ etag: '"racing-peer-etag"',
1497
+ size: 23,
1498
+ });
1132
1499
  vi.mocked(uploadFile).mockRejectedValueOnce(fence412());
1133
1500
  vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, dest) => {
1134
1501
  fs.writeFileSync(dest, "the racing peer version");
@@ -1148,19 +1515,22 @@ describe("share", () => {
1148
1515
  expect(result.filesUploaded).toBe(0);
1149
1516
  expect(result.aborted).toBe(false);
1150
1517
  expect(events.some((e) => e.type === "conflict" && e.path === "raced.md")).toBe(true);
1151
- // The racing remote bytes were preserved next to the local copy.
1152
- const mirrors = fs
1153
- .readdirSync(companyRoot)
1154
- .filter((f) => f.includes(".conflict-"));
1155
- expect(mirrors).toHaveLength(1);
1156
- // Local copy untouched.
1157
- expect(fs.readFileSync(testFile, "utf-8")).toBe("my local bytes");
1158
- // No journal stamp for the failed PUT — next pass re-evaluates fresh.
1518
+ // The racing remote wins the working name; local remains recoverable.
1519
+ expect(fs.readFileSync(testFile, "utf-8")).toBe("the racing peer version");
1520
+ expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe("my local bytes");
1521
+ // The pull-shaped result is journalled as the remote body, so the next
1522
+ // pass does not revive the same resolved conflict.
1159
1523
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1160
- if (fs.existsSync(journalPath)) {
1161
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1162
- expect(journal.files?.["raced.md"]).toBeUndefined();
1163
- }
1524
+ const journal = journalAt(journalPath);
1525
+ expect(journal.files["raced.md"]).toMatchObject({
1526
+ hash: hashFile(testFile),
1527
+ remoteEtag: "racing-peer-etag",
1528
+ direction: "down",
1529
+ });
1530
+ expect(conflictIndexAt(tmpDir).conflicts[0]).toMatchObject({
1531
+ originalPath: "companies/acme/raced.md",
1532
+ preserved: "local",
1533
+ });
1164
1534
  });
1165
1535
  it("412 under overwrite: retries exactly once WITHOUT the fence (explicit consent)", async () => {
1166
1536
  const companyRoot = path.join(tmpDir, "companies", "acme");
@@ -1239,8 +1609,12 @@ describe("share", () => {
1239
1609
  }
1240
1610
  return { etag: '"should-not-land"' };
1241
1611
  });
1612
+ vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, dest) => {
1613
+ fs.writeFileSync(dest, "reviewed peer body");
1614
+ return undefined;
1615
+ });
1242
1616
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1243
- fs.writeFileSync(journalPath, JSON.stringify({
1617
+ seedJournalAt(journalPath, ({
1244
1618
  version: "1",
1245
1619
  lastSync: new Date(Date.now() - 60_000).toISOString(),
1246
1620
  files: {
@@ -1283,8 +1657,9 @@ describe("share", () => {
1283
1657
  }
1284
1658
  // No unfenced retry under keep.
1285
1659
  expect(vi.mocked(uploadFile).mock.calls.some((c) => c[4] === undefined)).toBe(false);
1286
- // Local stale bytes preserved on disk (keep does not pull).
1287
- expect(fs.readFileSync(testFile, "utf-8")).toBe(staleBytes);
1660
+ // Keep adopts the peer body but retains the stale local body beside it.
1661
+ expect(fs.readFileSync(testFile, "utf-8")).toBe("reviewed peer body");
1662
+ expect(fs.readFileSync(sidecarIn(path.dirname(testFile)), "utf-8")).toBe(staleBytes);
1288
1663
  });
1289
1664
  it("legitimate local-ahead still uploads when remote remains at journal baseline", async () => {
1290
1665
  const companyRoot = path.join(tmpDir, "companies", "acme");
@@ -1300,7 +1675,7 @@ describe("share", () => {
1300
1675
  });
1301
1676
  vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"new-upload-etag"' });
1302
1677
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1303
- fs.writeFileSync(journalPath, JSON.stringify({
1678
+ seedJournalAt(journalPath, ({
1304
1679
  version: "1",
1305
1680
  lastSync: new Date(Date.now() - 60_000).toISOString(),
1306
1681
  files: {
@@ -1334,8 +1709,12 @@ describe("share", () => {
1334
1709
  etag: '"remote-concurrent"',
1335
1710
  size: 20,
1336
1711
  });
1712
+ vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, dest) => {
1713
+ fs.writeFileSync(dest, "remote concurrent");
1714
+ return undefined;
1715
+ });
1337
1716
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1338
- fs.writeFileSync(journalPath, JSON.stringify({
1717
+ seedJournalAt(journalPath, ({
1339
1718
  version: "1",
1340
1719
  lastSync: new Date().toISOString(),
1341
1720
  files: {
@@ -1358,12 +1737,14 @@ describe("share", () => {
1358
1737
  expect(result.conflictPaths).toEqual(["both-changed.md"]);
1359
1738
  expect(result.filesUploaded).toBe(0);
1360
1739
  expect(uploadFile).not.toHaveBeenCalled();
1361
- });
1362
- it("localDiverges under keep: pull-kept divergent local cannot silently become write authority", async () => {
1363
- // After pull --on-conflict keep, journal stamps remoteEtag of the peer
1364
- // version AND localDiverges=true (local never matched that etag). A later
1365
- // push must not treat "remote still at stamped etag" as license to PUT
1366
- // the divergent local body (journal-baseline If-Match would succeed).
1740
+ expect(fs.readFileSync(testFile, "utf-8")).toBe("remote concurrent");
1741
+ expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe("local concurrent");
1742
+ });
1743
+ it("localDiverges under keep: a pull-held divergent local yields to cloud without becoming write authority", async () => {
1744
+ // After a pull-side hold (`skip` or pull-leg `publish-local`), the journal
1745
+ // stamps the peer remoteEtag and localDiverges=true because local never
1746
+ // matched that etag. A later push must not treat "remote still at stamped
1747
+ // etag" as license to PUT the divergent local body.
1367
1748
  const companyRoot = path.join(tmpDir, "companies", "acme");
1368
1749
  fs.mkdirSync(companyRoot, { recursive: true });
1369
1750
  const testFile = path.join(companyRoot, "kept-divergent.md");
@@ -1376,8 +1757,12 @@ describe("share", () => {
1376
1757
  etag: `"${REMOTE}"`,
1377
1758
  size: 40,
1378
1759
  });
1760
+ vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, dest) => {
1761
+ fs.writeFileSync(dest, "peer remote body");
1762
+ return undefined;
1763
+ });
1379
1764
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1380
- fs.writeFileSync(journalPath, JSON.stringify({
1765
+ seedJournalAt(journalPath, ({
1381
1766
  version: "1",
1382
1767
  lastSync: new Date().toISOString(),
1383
1768
  files: {
@@ -1407,7 +1792,10 @@ describe("share", () => {
1407
1792
  expect(events.some((e) => e.type === "conflict" &&
1408
1793
  e.path === "kept-divergent.md" &&
1409
1794
  e.resolution === "keep")).toBe(true);
1410
- expect(fs.readFileSync(testFile, "utf-8")).toBe(localBytes);
1795
+ expect(fs.readFileSync(testFile, "utf-8")).toBe("peer remote body");
1796
+ expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe(localBytes);
1797
+ expect(journalAt(journalPath).files["kept-divergent.md"]?.localDiverges)
1798
+ .toBeFalsy();
1411
1799
  });
1412
1800
  it("localDiverges under overwrite: explicit intent may force-write", async () => {
1413
1801
  const companyRoot = path.join(tmpDir, "companies", "acme");
@@ -1423,7 +1811,7 @@ describe("share", () => {
1423
1811
  });
1424
1812
  vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"forced-etag"' });
1425
1813
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1426
- fs.writeFileSync(journalPath, JSON.stringify({
1814
+ seedJournalAt(journalPath, ({
1427
1815
  version: "1",
1428
1816
  lastSync: new Date().toISOString(),
1429
1817
  files: {
@@ -1450,6 +1838,304 @@ describe("share", () => {
1450
1838
  // fence then retry — either is consent. Must not leave the file unuploaded.
1451
1839
  expect(uploadFile).toHaveBeenCalled();
1452
1840
  });
1841
+ // ── publish-local: fenced promotion of a kept-local copy ────────────────
1842
+ //
1843
+ // `keep` leaves a file permanently unpublishable: push refuses it on the
1844
+ // localDiverges flag, pull never clears the flag (remote is unchanged), and
1845
+ // `keep` re-arms it every pass. The only existing escape — `overwrite` — is
1846
+ // a blunt force-write that will PUT unfenced after a 412, clobbering a
1847
+ // remote that genuinely advanced.
1848
+ //
1849
+ // `publish-local` is the missing middle: the operator explicitly elects
1850
+ // their local copy as the new authority, and the engine accepts it ONLY
1851
+ // while the remote still sits at the etag recorded when the keep happened.
1852
+ // If the remote moved, consent was given against a version that no longer
1853
+ // exists, so the write must fail closed instead of forcing.
1854
+ it("localDiverges under publish-local: promotes local under an If-Match fence on the journal etag", async () => {
1855
+ const companyRoot = path.join(tmpDir, "companies", "acme");
1856
+ fs.mkdirSync(companyRoot, { recursive: true });
1857
+ const testFile = path.join(companyRoot, "publish-divergent.md");
1858
+ const localBytes = "my kept divergent local, now elected authority";
1859
+ fs.writeFileSync(testFile, localBytes);
1860
+ const localHash = hashFile(testFile);
1861
+ const REMOTE = "peer-version-etag";
1862
+ vi.mocked(headRemoteFile).mockResolvedValueOnce({
1863
+ lastModified: new Date(),
1864
+ etag: `"${REMOTE}"`,
1865
+ size: 40,
1866
+ });
1867
+ vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"published-etag"' });
1868
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
1869
+ fs.writeFileSync(journalPath, JSON.stringify({
1870
+ version: "1",
1871
+ lastSync: new Date().toISOString(),
1872
+ files: {
1873
+ "publish-divergent.md": {
1874
+ hash: localHash,
1875
+ size: localBytes.length,
1876
+ syncedAt: new Date().toISOString(),
1877
+ direction: "down",
1878
+ remoteEtag: REMOTE,
1879
+ localDiverges: true,
1880
+ },
1881
+ },
1882
+ }));
1883
+ const result = await share({
1884
+ paths: [testFile],
1885
+ company: "acme",
1886
+ vaultConfig: mockConfig,
1887
+ hqRoot: tmpDir,
1888
+ skipUnchanged: false,
1889
+ onConflict: "publish-local",
1890
+ });
1891
+ expect(result.filesUploaded).toBe(1);
1892
+ // Fenced on the etag the keep recorded — never unfenced, never live-HEAD.
1893
+ expect(uploadFile).toHaveBeenCalledTimes(1);
1894
+ expect(vi.mocked(uploadFile).mock.calls[0][4]).toEqual({ ifMatch: REMOTE });
1895
+ // The entry is rewritten as an ordinary upload: local is now authority,
1896
+ // so the journal-honesty flag must be gone rather than re-armed.
1897
+ const journal = journalAt(journalPath);
1898
+ expect(journal.files["publish-divergent.md"].direction).toBe("up");
1899
+ expect(journal.files["publish-divergent.md"].localDiverges).toBeFalsy();
1900
+ expect(journal.files["publish-divergent.md"].remoteEtag).toBe("published-etag");
1901
+ expect(fs.readFileSync(testFile, "utf-8")).toBe(localBytes);
1902
+ });
1903
+ it("localDiverges under publish-local: a 412 fails closed and never retries unfenced", async () => {
1904
+ const companyRoot = path.join(tmpDir, "companies", "acme");
1905
+ fs.mkdirSync(companyRoot, { recursive: true });
1906
+ const testFile = path.join(companyRoot, "publish-raced.md");
1907
+ const localBytes = "my local, but the remote moved under me";
1908
+ fs.writeFileSync(testFile, localBytes);
1909
+ const localHash = hashFile(testFile);
1910
+ const REMOTE = "etag-at-keep-time";
1911
+ vi.mocked(headRemoteFile).mockResolvedValueOnce({
1912
+ lastModified: new Date(),
1913
+ etag: `"${REMOTE}"`,
1914
+ size: 40,
1915
+ });
1916
+ const precondition = Object.assign(new Error("At least one of the pre-conditions you specified did not hold"), {
1917
+ name: "PreconditionFailed",
1918
+ });
1919
+ vi.mocked(uploadFile).mockRejectedValueOnce(precondition);
1920
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
1921
+ fs.writeFileSync(journalPath, JSON.stringify({
1922
+ version: "1",
1923
+ lastSync: new Date().toISOString(),
1924
+ files: {
1925
+ "publish-raced.md": {
1926
+ hash: localHash,
1927
+ size: localBytes.length,
1928
+ syncedAt: new Date().toISOString(),
1929
+ direction: "down",
1930
+ remoteEtag: REMOTE,
1931
+ localDiverges: true,
1932
+ },
1933
+ },
1934
+ }));
1935
+ const result = await share({
1936
+ paths: [testFile],
1937
+ company: "acme",
1938
+ vaultConfig: mockConfig,
1939
+ hqRoot: tmpDir,
1940
+ skipUnchanged: false,
1941
+ onConflict: "publish-local",
1942
+ });
1943
+ // Exactly one attempt: the fenced one. The `overwrite` escape hatch of
1944
+ // re-PUTting with no precondition must NOT be reachable from here.
1945
+ expect(uploadFile).toHaveBeenCalledTimes(1);
1946
+ expect(result.filesUploaded).toBe(0);
1947
+ expect(result.conflictPaths).toContain("publish-raced.md");
1948
+ // Consent was for a remote that no longer exists, so the flag stays put
1949
+ // and the operator is asked again rather than silently overruled.
1950
+ const journal = journalAt(journalPath);
1951
+ expect(journal.files["publish-raced.md"].localDiverges).toBe(true);
1952
+ expect(fs.readFileSync(testFile, "utf-8")).toBe(localBytes);
1953
+ });
1954
+ // Codex review (PR #356, P1). The sync runner's push leg always calls
1955
+ // share() with `skipUnchanged: true`, and a pull-held entry stores the
1956
+ // CURRENT local hash — that is exactly what the journal-honesty stamp
1957
+ // records. So `existing.hash === localHash` is true for every stuck file,
1958
+ // classifyPushCandidates() returns `skip-unchanged`, and executeUploads()
1959
+ // never runs: no HEAD, no conflict, no upload. Without this bypass the
1960
+ // whole recovery is a no-op in the one mode that can reach personal-vault
1961
+ // files, and the next pull leaves them stuck again. `hash === journal hash`
1962
+ // means "local is unchanged since the stamp", NOT "local matches remote" —
1963
+ // for a divergent entry those are different claims.
1964
+ it("localDiverges bypasses the skip-unchanged fast path so the recovery can run at all", async () => {
1965
+ const companyRoot = path.join(tmpDir, "companies", "acme");
1966
+ fs.mkdirSync(companyRoot, { recursive: true });
1967
+ const testFile = path.join(companyRoot, "stuck-under-skip-unchanged.md");
1968
+ const localBytes = "the body a pull hold stamped and then refused to publish";
1969
+ fs.writeFileSync(testFile, localBytes);
1970
+ const localHash = hashFile(testFile);
1971
+ const REMOTE = "etag-at-keep-time";
1972
+ vi.mocked(headRemoteFile).mockResolvedValueOnce({
1973
+ lastModified: new Date(),
1974
+ etag: `"${REMOTE}"`,
1975
+ size: 40,
1976
+ });
1977
+ vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"published-etag"' });
1978
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
1979
+ fs.writeFileSync(journalPath, JSON.stringify({
1980
+ version: "1",
1981
+ lastSync: new Date().toISOString(),
1982
+ files: {
1983
+ "stuck-under-skip-unchanged.md": {
1984
+ // Deliberately the CURRENT local hash: this is what a pull hold
1985
+ // writes, and it is what makes the fast path swallow the file.
1986
+ hash: localHash,
1987
+ size: localBytes.length,
1988
+ syncedAt: new Date().toISOString(),
1989
+ direction: "down",
1990
+ remoteEtag: REMOTE,
1991
+ localDiverges: true,
1992
+ },
1993
+ },
1994
+ }));
1995
+ const result = await share({
1996
+ paths: [testFile],
1997
+ company: "acme",
1998
+ vaultConfig: mockConfig,
1999
+ hqRoot: tmpDir,
2000
+ skipUnchanged: true,
2001
+ onConflict: "publish-local",
2002
+ });
2003
+ expect(result.filesUploaded).toBe(1);
2004
+ expect(vi.mocked(uploadFile).mock.calls[0][4]).toEqual({ ifMatch: REMOTE });
2005
+ const journal = journalAt(journalPath);
2006
+ expect(journal.files["stuck-under-skip-unchanged.md"].direction).toBe("up");
2007
+ expect(journal.files["stuck-under-skip-unchanged.md"].localDiverges).toBeFalsy();
2008
+ });
2009
+ // Codex review (PR #356, P2). A genuine two-sided conflict is not the
2010
+ // localDiverges recovery: the journal etag is stale BY DEFINITION here
2011
+ // (that is what remoteChanged means), so fencing consent on it guarantees a
2012
+ // 412 and the file can never publish. Consent given at this prompt is
2013
+ // consent against the remote the operator was just shown, so the fence must
2014
+ // be the freshly observed etag.
2015
+ it("publish-local on a fresh two-sided conflict fences on the OBSERVED remote etag, not the stale journal one", async () => {
2016
+ const companyRoot = path.join(tmpDir, "companies", "acme");
2017
+ fs.mkdirSync(companyRoot, { recursive: true });
2018
+ const testFile = path.join(companyRoot, "two-sided.md");
2019
+ fs.writeFileSync(testFile, "my newer local body");
2020
+ const STALE_JOURNAL_ETAG = "etag-from-last-sync";
2021
+ const OBSERVED_REMOTE_ETAG = "etag-the-peer-just-wrote";
2022
+ vi.mocked(headRemoteFile).mockResolvedValueOnce({
2023
+ lastModified: new Date(),
2024
+ etag: `"${OBSERVED_REMOTE_ETAG}"`,
2025
+ size: 40,
2026
+ });
2027
+ vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"published-etag"' });
2028
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
2029
+ fs.writeFileSync(journalPath, JSON.stringify({
2030
+ version: "1",
2031
+ lastSync: new Date().toISOString(),
2032
+ files: {
2033
+ "two-sided.md": {
2034
+ hash: "a-hash-that-is-neither-side",
2035
+ size: 10,
2036
+ syncedAt: new Date().toISOString(),
2037
+ direction: "up",
2038
+ remoteEtag: STALE_JOURNAL_ETAG,
2039
+ },
2040
+ },
2041
+ }));
2042
+ const result = await share({
2043
+ paths: [testFile],
2044
+ company: "acme",
2045
+ vaultConfig: mockConfig,
2046
+ hqRoot: tmpDir,
2047
+ skipUnchanged: false,
2048
+ onConflict: "publish-local",
2049
+ });
2050
+ expect(result.filesUploaded).toBe(1);
2051
+ expect(vi.mocked(uploadFile).mock.calls[0][4]).toEqual({
2052
+ ifMatch: OBSERVED_REMOTE_ETAG,
2053
+ });
2054
+ });
2055
+ // Codex review (PR #356, P2). HEAD returning null skips the whole conflict
2056
+ // block, so the precondition becomes If-None-Match:* and the object is
2057
+ // RECREATED. For a divergent entry that silently overrules a peer's
2058
+ // deletion — the remote is emphatically no longer at the recorded etag, so
2059
+ // the strategy's own fence says fail closed. `keep`'s recreate-once
2060
+ // behaviour for non-divergent entries is unaffected.
2061
+ it("publish-local does not resurrect a remote deleted after the keep", async () => {
2062
+ const companyRoot = path.join(tmpDir, "companies", "acme");
2063
+ fs.mkdirSync(companyRoot, { recursive: true });
2064
+ const testFile = path.join(companyRoot, "peer-deleted.md");
2065
+ const localBytes = "my divergent local body";
2066
+ fs.writeFileSync(testFile, localBytes);
2067
+ const localHash = hashFile(testFile);
2068
+ vi.mocked(headRemoteFile).mockResolvedValueOnce(null);
2069
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
2070
+ fs.writeFileSync(journalPath, JSON.stringify({
2071
+ version: "1",
2072
+ lastSync: new Date().toISOString(),
2073
+ files: {
2074
+ "peer-deleted.md": {
2075
+ hash: localHash,
2076
+ size: localBytes.length,
2077
+ syncedAt: new Date().toISOString(),
2078
+ direction: "down",
2079
+ remoteEtag: "etag-before-the-peer-deleted-it",
2080
+ localDiverges: true,
2081
+ },
2082
+ },
2083
+ }));
2084
+ const result = await share({
2085
+ paths: [testFile],
2086
+ company: "acme",
2087
+ vaultConfig: mockConfig,
2088
+ hqRoot: tmpDir,
2089
+ skipUnchanged: false,
2090
+ onConflict: "publish-local",
2091
+ });
2092
+ expect(uploadFile).not.toHaveBeenCalled();
2093
+ expect(result.filesUploaded).toBe(0);
2094
+ expect(result.conflictPaths).toContain("peer-deleted.md");
2095
+ const journal = journalAt(journalPath);
2096
+ expect(journal.files["peer-deleted.md"].localDiverges).toBe(true);
2097
+ expect(fs.readFileSync(testFile, "utf-8")).toBe(localBytes);
2098
+ });
2099
+ it("publish-local on a non-divergent file behaves like an ordinary fenced upload", async () => {
2100
+ const companyRoot = path.join(tmpDir, "companies", "acme");
2101
+ fs.mkdirSync(companyRoot, { recursive: true });
2102
+ const testFile = path.join(companyRoot, "ordinary.md");
2103
+ fs.writeFileSync(testFile, "locally edited since last sync");
2104
+ const REMOTE = "unchanged-remote-etag";
2105
+ vi.mocked(headRemoteFile).mockResolvedValueOnce({
2106
+ lastModified: new Date(),
2107
+ etag: `"${REMOTE}"`,
2108
+ size: 10,
2109
+ });
2110
+ vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"next-etag"' });
2111
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
2112
+ fs.writeFileSync(journalPath, JSON.stringify({
2113
+ version: "1",
2114
+ lastSync: new Date().toISOString(),
2115
+ files: {
2116
+ "ordinary.md": {
2117
+ hash: "stale-hash-from-a-previous-body",
2118
+ size: 10,
2119
+ syncedAt: new Date().toISOString(),
2120
+ direction: "up",
2121
+ remoteEtag: REMOTE,
2122
+ },
2123
+ },
2124
+ }));
2125
+ const result = await share({
2126
+ paths: [testFile],
2127
+ company: "acme",
2128
+ vaultConfig: mockConfig,
2129
+ hqRoot: tmpDir,
2130
+ skipUnchanged: false,
2131
+ onConflict: "publish-local",
2132
+ });
2133
+ // No localDiverges flag means no special casing: the normal push fence
2134
+ // applies and the file uploads without being routed through conflict.
2135
+ expect(result.filesUploaded).toBe(1);
2136
+ expect(result.conflictPaths ?? []).not.toContain("ordinary.md");
2137
+ expect(vi.mocked(uploadFile).mock.calls[0][4]).toEqual({ ifMatch: REMOTE });
2138
+ });
1453
2139
  // Journal still carries remoteEtag from a prior sync, but the remote key
1454
2140
  // is gone (HEAD null) — e.g. peer delete or operator purge. Recreation
1455
2141
  // must use If-None-Match:* (create-only), NOT If-Match on the stale
@@ -1467,7 +2153,7 @@ describe("share", () => {
1467
2153
  vi.mocked(headRemoteFile).mockResolvedValueOnce(null);
1468
2154
  vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"recreated-etag"' });
1469
2155
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1470
- fs.writeFileSync(journalPath, JSON.stringify({
2156
+ seedJournalAt(journalPath, ({
1471
2157
  version: "1",
1472
2158
  lastSync: new Date(Date.now() - 60_000).toISOString(),
1473
2159
  files: {
@@ -1497,28 +2183,34 @@ describe("share", () => {
1497
2183
  const pc = vi.mocked(uploadFile).mock.calls[0][4];
1498
2184
  expect(pc?.ifMatch).toBeUndefined();
1499
2185
  expect(pc?.ifNoneMatch).toBe("*");
1500
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2186
+ const journal = journalAt(journalPath);
1501
2187
  expect(journal.files["recreate-deleted.md"]).toMatchObject({
1502
2188
  hash: localHash,
1503
2189
  remoteEtag: "recreated-etag",
1504
2190
  direction: "up",
1505
2191
  });
1506
2192
  });
1507
- it("deleted journaled remote + concurrent create 412 under keep: existing conflict semantics", async () => {
2193
+ it("deleted journaled remote + concurrent create 412 under keep: adopts the winner and preserves local", async () => {
1508
2194
  // Same setup as recreation, but a peer creates the key between HEAD and
1509
- // PUT → If-None-Match:* 412s → keep surfaces conflict, no journal stamp.
2195
+ // PUT → If-None-Match:* 412s → keep adopts the peer's winning create.
1510
2196
  const companyRoot = path.join(tmpDir, "companies", "acme");
1511
2197
  fs.mkdirSync(companyRoot, { recursive: true });
1512
2198
  const testFile = path.join(companyRoot, "race-recreate.md");
1513
2199
  fs.writeFileSync(testFile, "my recreated local");
1514
- vi.mocked(headRemoteFile).mockResolvedValueOnce(null);
2200
+ vi.mocked(headRemoteFile)
2201
+ .mockResolvedValueOnce(null)
2202
+ .mockResolvedValueOnce({
2203
+ lastModified: new Date(),
2204
+ etag: '"peer-created-etag"',
2205
+ size: 18,
2206
+ });
1515
2207
  vi.mocked(uploadFile).mockRejectedValueOnce(fence412());
1516
2208
  vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, dest) => {
1517
2209
  fs.writeFileSync(dest, "peer created first");
1518
2210
  return undefined;
1519
2211
  });
1520
2212
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1521
- fs.writeFileSync(journalPath, JSON.stringify({
2213
+ seedJournalAt(journalPath, ({
1522
2214
  version: "1",
1523
2215
  lastSync: new Date(Date.now() - 60_000).toISOString(),
1524
2216
  files: {
@@ -1548,12 +2240,15 @@ describe("share", () => {
1548
2240
  // First (only) attempt must be create-fenced; no unfenced retry under keep.
1549
2241
  expect(uploadFile).toHaveBeenCalledTimes(1);
1550
2242
  expect(vi.mocked(uploadFile).mock.calls[0][4]).toEqual({ ifNoneMatch: "*" });
1551
- // Journal not advanced to a successful upload etag (entry may still be
1552
- // the prior baseline — next pass re-evaluates).
1553
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1554
- expect(journal.files["race-recreate.md"]?.remoteEtag).toBe("etag-before-delete");
1555
- expect(journal.files["race-recreate.md"]?.hash).toBe("old-hash");
1556
- expect(fs.readFileSync(testFile, "utf-8")).toBe("my recreated local");
2243
+ // Journal now describes the peer body at the original name.
2244
+ const journal = journalAt(journalPath);
2245
+ expect(journal.files["race-recreate.md"]).toMatchObject({
2246
+ remoteEtag: "peer-created-etag",
2247
+ hash: hashFile(testFile),
2248
+ direction: "down",
2249
+ });
2250
+ expect(fs.readFileSync(testFile, "utf-8")).toBe("peer created first");
2251
+ expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe("my recreated local");
1557
2252
  });
1558
2253
  });
1559
2254
  it("scoped push (plan-exceeds-grant): syncs the in-scope subset, skips out-of-scope paths, never aborts (feedback_ded09d56)", async () => {
@@ -1874,7 +2569,7 @@ describe("share", () => {
1874
2569
  size: 5,
1875
2570
  });
1876
2571
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1877
- fs.writeFileSync(journalPath, JSON.stringify({
2572
+ seedJournalAt(journalPath, ({
1878
2573
  version: "1",
1879
2574
  lastSync: syncedAt,
1880
2575
  files: {
@@ -1914,7 +2609,7 @@ describe("share", () => {
1914
2609
  size: 5,
1915
2610
  });
1916
2611
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1917
- fs.writeFileSync(journalPath, JSON.stringify({
2612
+ seedJournalAt(journalPath, ({
1918
2613
  version: "1",
1919
2614
  lastSync: syncedAt,
1920
2615
  files: {
@@ -1950,7 +2645,7 @@ describe("share", () => {
1950
2645
  hqRoot: tmpDir,
1951
2646
  });
1952
2647
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1953
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2648
+ const journal = journalAt(journalPath);
1954
2649
  expect(journal.files["fresh.md"].remoteEtag).toBe("new-upload-etag");
1955
2650
  });
1956
2651
  it("forwards UploadAuthor to uploadFile when present (created-by metadata)", async () => {
@@ -1995,7 +2690,7 @@ describe("share", () => {
1995
2690
  const { hashFile } = await import("../journal.js");
1996
2691
  const hash = hashFile(testFile);
1997
2692
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1998
- fs.writeFileSync(journalPath, JSON.stringify({
2693
+ seedJournalAt(journalPath, ({
1999
2694
  version: "1",
2000
2695
  lastSync: new Date().toISOString(),
2001
2696
  files: {
@@ -2215,7 +2910,7 @@ describe("share", () => {
2215
2910
  .update("stable content")
2216
2911
  .digest("hex");
2217
2912
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2218
- fs.writeFileSync(journalPath, JSON.stringify({
2913
+ seedJournalAt(journalPath, ({
2219
2914
  version: "1",
2220
2915
  lastSync: new Date().toISOString(),
2221
2916
  files: {
@@ -2276,7 +2971,7 @@ describe("share", () => {
2276
2971
  // not be queued for delete.
2277
2972
  fs.writeFileSync(path.join(tmpDir, ".hqinclude"), "companies/*/knowledge/\n");
2278
2973
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2279
- fs.writeFileSync(journalPath, JSON.stringify({
2974
+ seedJournalAt(journalPath, ({
2280
2975
  version: "1",
2281
2976
  lastSync: new Date().toISOString(),
2282
2977
  files: {
@@ -2313,7 +3008,7 @@ describe("share", () => {
2313
3008
  operation: "delete",
2314
3009
  });
2315
3010
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "knowledge");
2316
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3011
+ const journal = journalAt(journalPath);
2317
3012
  expect(journal.files["knowledge"]).toBeUndefined();
2318
3013
  });
2319
3014
  it("propagateDeletes: a dangling local symlink is NOT classified as gone (lstat, not existsSync)", async () => {
@@ -2330,7 +3025,7 @@ describe("share", () => {
2330
3025
  const danglingLink = path.join(companyRoot, "dangling-link.md");
2331
3026
  fs.symlinkSync("./missing-target.md", danglingLink);
2332
3027
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2333
- fs.writeFileSync(journalPath, JSON.stringify({
3028
+ seedJournalAt(journalPath, ({
2334
3029
  version: "1",
2335
3030
  lastSync: new Date().toISOString(),
2336
3031
  files: {
@@ -2360,7 +3055,7 @@ describe("share", () => {
2360
3055
  expect(result.filesDeleted).toBe(0);
2361
3056
  expect(deleteRemoteFile).not.toHaveBeenCalled();
2362
3057
  // Journal entry must survive the run.
2363
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3058
+ const journal = journalAt(journalPath);
2364
3059
  expect(journal.files["dangling-link.md"]).toBeDefined();
2365
3060
  });
2366
3061
  it("vault-litter drain: a `.drift-*` marker with direction:'down' and NO delete intent tombstones " +
@@ -2425,7 +3120,7 @@ describe("share", () => {
2425
3120
  });
2426
3121
  expect(result.filesDeleted).toBe(1);
2427
3122
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "personal/scripts/run-project.sh.drift-1779863862-42519");
2428
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3123
+ const journal = journalAt(journalPath);
2429
3124
  expect(journal.files["personal/scripts/run-project.sh.drift-1779863862-42519"]).toBeUndefined();
2430
3125
  }
2431
3126
  finally {
@@ -2498,7 +3193,7 @@ describe("share", () => {
2498
3193
  fs.mkdirSync(companyRoot, { recursive: true });
2499
3194
  // No file on disk under companies/acme/peer-uploaded.md → local missing.
2500
3195
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2501
- fs.writeFileSync(journalPath, JSON.stringify({
3196
+ seedJournalAt(journalPath, ({
2502
3197
  version: "1",
2503
3198
  lastSync: new Date().toISOString(),
2504
3199
  files: {
@@ -2523,7 +3218,7 @@ describe("share", () => {
2523
3218
  });
2524
3219
  expect(result.filesDeleted).toBe(0);
2525
3220
  expect(deleteRemoteFile).not.toHaveBeenCalled();
2526
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3221
+ const journal = journalAt(journalPath);
2527
3222
  expect(journal.files["peer-uploaded.md"]).toBeDefined();
2528
3223
  });
2529
3224
  it("ROLLBACK KNOB: owned-only is honored on a prs_ personal vault — an intent-less entry with a " +
@@ -2614,7 +3309,7 @@ describe("share", () => {
2614
3309
  ]);
2615
3310
  // Journal entry survives, so a later run (or a watcher-minted intent)
2616
3311
  // can still authorize the delete properly.
2617
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3312
+ const journal = journalAt(journalPath);
2618
3313
  expect(journal.files["personal/knowledge/gone.md"]).toBeDefined();
2619
3314
  }
2620
3315
  finally {
@@ -2631,7 +3326,7 @@ describe("share", () => {
2631
3326
  // deleted by the user.
2632
3327
  fs.writeFileSync(path.join(companyRoot, "kept.md"), "still here");
2633
3328
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2634
- fs.writeFileSync(journalPath, JSON.stringify({
3329
+ seedJournalAt(journalPath, ({
2635
3330
  version: "1",
2636
3331
  lastSync: new Date().toISOString(),
2637
3332
  files: {
@@ -2667,7 +3362,7 @@ describe("share", () => {
2667
3362
  expect(deleteRemoteFile).toHaveBeenCalledTimes(1);
2668
3363
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "gone.md");
2669
3364
  // Journal entry for the gone file is removed; the kept entry stays.
2670
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3365
+ const journal = journalAt(journalPath);
2671
3366
  expect(journal.files["gone.md"]).toBeUndefined();
2672
3367
  expect(journal.files["kept.md"]).toBeDefined();
2673
3368
  });
@@ -2675,7 +3370,7 @@ describe("share", () => {
2675
3370
  const companyRoot = path.join(tmpDir, "companies", "acme");
2676
3371
  fs.mkdirSync(companyRoot, { recursive: true });
2677
3372
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2678
- fs.writeFileSync(journalPath, JSON.stringify({
3373
+ seedJournalAt(journalPath, ({
2679
3374
  version: "1",
2680
3375
  lastSync: new Date().toISOString(),
2681
3376
  files: {
@@ -2717,7 +3412,7 @@ describe("share", () => {
2717
3412
  const companyRoot = path.join(tmpDir, "companies", "acme");
2718
3413
  fs.mkdirSync(companyRoot, { recursive: true });
2719
3414
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2720
- fs.writeFileSync(journalPath, JSON.stringify({
3415
+ seedJournalAt(journalPath, ({
2721
3416
  version: "1",
2722
3417
  lastSync: new Date().toISOString(),
2723
3418
  files: {
@@ -2740,7 +3435,7 @@ describe("share", () => {
2740
3435
  expect(result.filesDeleted).toBe(0);
2741
3436
  expect(deleteRemoteFile).not.toHaveBeenCalled();
2742
3437
  // Journal entry survives so the next opt-in run can still propagate.
2743
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3438
+ const journal = journalAt(journalPath);
2744
3439
  expect(journal.files["gone.md"]).toBeDefined();
2745
3440
  });
2746
3441
  it("propagateDeletes: scope is constrained to the supplied paths — sibling deletes are not swept", async () => {
@@ -2748,7 +3443,7 @@ describe("share", () => {
2748
3443
  fs.mkdirSync(path.join(companyRoot, "in-scope"), { recursive: true });
2749
3444
  fs.mkdirSync(path.join(companyRoot, "other"), { recursive: true });
2750
3445
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2751
- fs.writeFileSync(journalPath, JSON.stringify({
3446
+ seedJournalAt(journalPath, ({
2752
3447
  version: "1",
2753
3448
  lastSync: new Date().toISOString(),
2754
3449
  files: {
@@ -2783,7 +3478,7 @@ describe("share", () => {
2783
3478
  expect(result.filesDeleted).toBe(1);
2784
3479
  expect(deleteRemoteFile).toHaveBeenCalledTimes(1);
2785
3480
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "in-scope/gone.md");
2786
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3481
+ const journal = journalAt(journalPath);
2787
3482
  expect(journal.files["in-scope/gone.md"]).toBeUndefined();
2788
3483
  // Sibling tree's journal entry is untouched — `hq share <subtree>` must
2789
3484
  // not act on files outside the named scope.
@@ -2799,7 +3494,7 @@ describe("share", () => {
2799
3494
  fs.mkdirSync(path.join(companyRoot, "in-scope"), { recursive: true });
2800
3495
  fs.mkdirSync(path.join(companyRoot, "other"), { recursive: true });
2801
3496
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2802
- fs.writeFileSync(journalPath, JSON.stringify({
3497
+ seedJournalAt(journalPath, ({
2803
3498
  version: "1",
2804
3499
  lastSync: new Date().toISOString(),
2805
3500
  files: {
@@ -2834,7 +3529,7 @@ describe("share", () => {
2834
3529
  });
2835
3530
  expect(result.filesDeleted).toBe(1);
2836
3531
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "in-scope/gone.md");
2837
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3532
+ const journal = journalAt(journalPath);
2838
3533
  expect(journal.files["in-scope/gone.md"]).toBeUndefined();
2839
3534
  // Scope stays exactly as narrow as the absolute spelling's — no wider.
2840
3535
  expect(journal.files["other/also-gone.md"]).toBeDefined();
@@ -2851,7 +3546,7 @@ describe("share", () => {
2851
3546
  fs.mkdirSync(acmeRepo, { recursive: true });
2852
3547
  fs.symlinkSync(acmeRepo, path.join(companyRoot, "knowledge"));
2853
3548
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2854
- fs.writeFileSync(journalPath, JSON.stringify({
3549
+ seedJournalAt(journalPath, ({
2855
3550
  version: "1",
2856
3551
  lastSync: new Date().toISOString(),
2857
3552
  files: {
@@ -2881,14 +3576,14 @@ describe("share", () => {
2881
3576
  });
2882
3577
  expect(result.filesDeleted).toBe(0);
2883
3578
  expect(deleteRemoteFile).not.toHaveBeenCalled();
2884
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3579
+ const journal = journalAt(journalPath);
2885
3580
  expect(journal.files["knowledge/agents/brief.md"]).toBeDefined();
2886
3581
  });
2887
3582
  it("propagateDeletes: a failed DeleteObject leaves the journal entry intact for retry", async () => {
2888
3583
  const companyRoot = path.join(tmpDir, "companies", "acme");
2889
3584
  fs.mkdirSync(companyRoot, { recursive: true });
2890
3585
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2891
- fs.writeFileSync(journalPath, JSON.stringify({
3586
+ seedJournalAt(journalPath, ({
2892
3587
  version: "1",
2893
3588
  lastSync: new Date().toISOString(),
2894
3589
  files: {
@@ -2921,7 +3616,7 @@ describe("share", () => {
2921
3616
  const errorEvent = events.find((e) => e.type === "error");
2922
3617
  expect(errorEvent).toMatchObject({ path: "flaky.md", message: expect.stringContaining("S3 down") });
2923
3618
  // Entry survives — next run will retry the delete.
2924
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3619
+ const journal = journalAt(journalPath);
2925
3620
  expect(journal.files["flaky.md"]).toBeDefined();
2926
3621
  });
2927
3622
  // ── Delete propagation safety: direction + filter guards (Bug B + C) ──────
@@ -2955,7 +3650,7 @@ describe("share", () => {
2955
3650
  // and one this machine uploaded (direction:'up'). Only the 'up' one should
2956
3651
  // be eligible for delete-propagation.
2957
3652
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2958
- fs.writeFileSync(journalPath, JSON.stringify({
3653
+ seedJournalAt(journalPath, ({
2959
3654
  version: "1",
2960
3655
  lastSync: new Date().toISOString(),
2961
3656
  files: {
@@ -2992,7 +3687,7 @@ describe("share", () => {
2992
3687
  const companyRoot = path.join(tmpDir, "companies", "acme");
2993
3688
  fs.mkdirSync(companyRoot, { recursive: true });
2994
3689
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2995
- fs.writeFileSync(journalPath, JSON.stringify({
3690
+ seedJournalAt(journalPath, ({
2996
3691
  version: "1",
2997
3692
  lastSync: new Date().toISOString(),
2998
3693
  files: {
@@ -3025,7 +3720,7 @@ describe("share", () => {
3025
3720
  // absence is a filter outcome, not a user-intended delete.
3026
3721
  fs.writeFileSync(path.join(tmpDir, ".hqignore"), "companies/acme/legacy/**\n");
3027
3722
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3028
- fs.writeFileSync(journalPath, JSON.stringify({
3723
+ seedJournalAt(journalPath, ({
3029
3724
  version: "1",
3030
3725
  lastSync: new Date().toISOString(),
3031
3726
  files: {
@@ -3084,7 +3779,7 @@ describe("share", () => {
3084
3779
  // Under owned-only this would be stuck on remote forever; currency-gated
3085
3780
  // checks the ETag and propagates the delete cleanly when match holds.
3086
3781
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3087
- fs.writeFileSync(journalPath, JSON.stringify({
3782
+ seedJournalAt(journalPath, ({
3088
3783
  version: "1",
3089
3784
  lastSync: new Date().toISOString(),
3090
3785
  files: {
@@ -3114,7 +3809,7 @@ describe("share", () => {
3114
3809
  });
3115
3810
  expect(result.filesDeleted).toBe(1);
3116
3811
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "core/policies/old.md");
3117
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3812
+ const journal = journalAt(journalPath);
3118
3813
  expect(journal.files["core/policies/old.md"]).toBeUndefined();
3119
3814
  });
3120
3815
  it("currency-gated: propagates the S3 delete for a local-delete tombstone (pull-leg producer hand-off)", async () => {
@@ -3125,7 +3820,7 @@ describe("share", () => {
3125
3820
  const companyRoot = path.join(tmpDir, "companies", "acme");
3126
3821
  fs.mkdirSync(companyRoot, { recursive: true });
3127
3822
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3128
- fs.writeFileSync(journalPath, JSON.stringify({
3823
+ seedJournalAt(journalPath, ({
3129
3824
  version: "2",
3130
3825
  lastSync: new Date().toISOString(),
3131
3826
  files: {
@@ -3161,7 +3856,7 @@ describe("share", () => {
3161
3856
  });
3162
3857
  expect(result.filesDeleted).toBe(1);
3163
3858
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "docs/deleted.md");
3164
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3859
+ const journal = journalAt(journalPath);
3165
3860
  expect(journal.files["docs/deleted.md"]).toBeUndefined();
3166
3861
  });
3167
3862
  it("currency-gated: refuses delete + emits stale-etag event when remote moved since last sync", async () => {
@@ -3170,7 +3865,7 @@ describe("share", () => {
3170
3865
  // Another device modified `shared.md` after this device's last sync.
3171
3866
  // The journal still records the old etag; HEAD returns the new one.
3172
3867
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3173
- fs.writeFileSync(journalPath, JSON.stringify({
3868
+ seedJournalAt(journalPath, ({
3174
3869
  version: "1",
3175
3870
  lastSync: new Date().toISOString(),
3176
3871
  files: {
@@ -3202,7 +3897,7 @@ describe("share", () => {
3202
3897
  // No remote DELETE issued, journal entry preserved (pull will re-pull).
3203
3898
  expect(result.filesDeleted).toBe(0);
3204
3899
  expect(deleteRemoteFile).not.toHaveBeenCalled();
3205
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3900
+ const journal = journalAt(journalPath);
3206
3901
  expect(journal.files["shared.md"]).toBeDefined();
3207
3902
  // Dedicated event fired so UIs surface the refusal. `reason` field
3208
3903
  // discriminates "stale-etag" (real drift) from "legacy-no-etag" so
@@ -3224,7 +3919,7 @@ describe("share", () => {
3224
3919
  fs.mkdirSync(companyRoot, { recursive: true });
3225
3920
  const recreatedPath = path.join(companyRoot, "racy.md");
3226
3921
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3227
- fs.writeFileSync(journalPath, JSON.stringify({
3922
+ seedJournalAt(journalPath, ({
3228
3923
  version: "2",
3229
3924
  lastSync: new Date().toISOString(),
3230
3925
  files: {
@@ -3260,7 +3955,7 @@ describe("share", () => {
3260
3955
  expect(result.filesDeleted).toBe(0);
3261
3956
  expect(deleteRemoteFile).not.toHaveBeenCalled();
3262
3957
  expect(fs.readFileSync(recreatedPath, "utf-8")).toBe("recreated after planning");
3263
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3958
+ const journal = journalAt(journalPath);
3264
3959
  expect(journal.files["racy.md"]).toBeDefined();
3265
3960
  expect(events).toContainEqual(expect.objectContaining({
3266
3961
  type: "delete-refused-stale-etag",
@@ -3282,7 +3977,7 @@ describe("share", () => {
3282
3977
  // genuine local==remote sync, so propagating the delete would destroy the
3283
3978
  // (different) remote version. Must be REFUSED, not propagated.
3284
3979
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3285
- fs.writeFileSync(journalPath, JSON.stringify({
3980
+ seedJournalAt(journalPath, ({
3286
3981
  version: "2",
3287
3982
  lastSync: new Date().toISOString(),
3288
3983
  files: {
@@ -3317,7 +4012,7 @@ describe("share", () => {
3317
4012
  // Refusal short-circuits before the currency HEAD — no network needed.
3318
4013
  expect(headRemoteFile).not.toHaveBeenCalled();
3319
4014
  // Journal entry preserved, not silently dropped.
3320
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4015
+ const journal = journalAt(journalPath);
3321
4016
  expect(journal.files["core/policies/kept.md"]).toBeDefined();
3322
4017
  // Surfaced as a refusal with the divergent-local reason.
3323
4018
  const refused = events.find((e) => e.type === "delete-refused-stale-etag");
@@ -3334,7 +4029,7 @@ describe("share", () => {
3334
4029
  // S3 console). Local copy also missing. Currency-gated should drop the
3335
4030
  // journal entry without issuing a DELETE — the remote is already gone.
3336
4031
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3337
- fs.writeFileSync(journalPath, JSON.stringify({
4032
+ seedJournalAt(journalPath, ({
3338
4033
  version: "1",
3339
4034
  lastSync: new Date().toISOString(),
3340
4035
  files: {
@@ -3367,7 +4062,7 @@ describe("share", () => {
3367
4062
  // from real deletes without re-counting events.
3368
4063
  expect(result.filesTombstoned).toBe(1);
3369
4064
  expect(result.filesRefusedStale).toBe(0);
3370
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4065
+ const journal = journalAt(journalPath);
3371
4066
  expect(journal.files["removed-out-of-band.md"]).toBeUndefined();
3372
4067
  // Synthetic progress event carries the tombstone marker. Without this,
3373
4068
  // the tty stream renders tombstones identically to real deletes — operator
@@ -3388,7 +4083,7 @@ describe("share", () => {
3388
4083
  // against. Refuse the delete in the safe direction; a future sync with
3389
4084
  // a recorded etag can re-evaluate.
3390
4085
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3391
- fs.writeFileSync(journalPath, JSON.stringify({
4086
+ seedJournalAt(journalPath, ({
3392
4087
  version: "1",
3393
4088
  lastSync: new Date().toISOString(),
3394
4089
  files: {
@@ -3442,7 +4137,7 @@ describe("share", () => {
3442
4137
  const companyRoot = path.join(tmpDir, "companies", "acme");
3443
4138
  fs.mkdirSync(companyRoot, { recursive: true });
3444
4139
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3445
- fs.writeFileSync(journalPath, JSON.stringify({
4140
+ seedJournalAt(journalPath, ({
3446
4141
  version: "1",
3447
4142
  lastSync: new Date().toISOString(),
3448
4143
  files: {
@@ -3494,7 +4189,7 @@ describe("share", () => {
3494
4189
  const companyRoot = path.join(tmpDir, "companies", "acme");
3495
4190
  fs.mkdirSync(companyRoot, { recursive: true });
3496
4191
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3497
- fs.writeFileSync(journalPath, JSON.stringify({
4192
+ seedJournalAt(journalPath, ({
3498
4193
  version: "1",
3499
4194
  lastSync: new Date().toISOString(),
3500
4195
  files: {
@@ -3528,7 +4223,7 @@ describe("share", () => {
3528
4223
  const companyRoot = path.join(tmpDir, "companies", "acme");
3529
4224
  fs.mkdirSync(companyRoot, { recursive: true });
3530
4225
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3531
- fs.writeFileSync(journalPath, JSON.stringify({
4226
+ seedJournalAt(journalPath, ({
3532
4227
  version: "1",
3533
4228
  lastSync: new Date().toISOString(),
3534
4229
  files: {
@@ -3578,7 +4273,7 @@ describe("share", () => {
3578
4273
  fs.mkdirSync(companyRoot, { recursive: true });
3579
4274
  const recreatedPath = path.join(companyRoot, "notes", "gone.md");
3580
4275
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3581
- fs.writeFileSync(journalPath, JSON.stringify({
4276
+ seedJournalAt(journalPath, ({
3582
4277
  version: "1",
3583
4278
  lastSync: new Date().toISOString(),
3584
4279
  files: {
@@ -3626,7 +4321,7 @@ describe("share", () => {
3626
4321
  const companyRoot = path.join(tmpDir, "companies", "acme");
3627
4322
  fs.mkdirSync(companyRoot, { recursive: true });
3628
4323
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3629
- fs.writeFileSync(journalPath, JSON.stringify({
4324
+ seedJournalAt(journalPath, ({
3630
4325
  version: "1",
3631
4326
  lastSync: new Date().toISOString(),
3632
4327
  files: {
@@ -3656,14 +4351,14 @@ describe("share", () => {
3656
4351
  type: "delete-refused-stale-etag",
3657
4352
  reason: "legacy-no-etag",
3658
4353
  }));
3659
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4354
+ const journal = journalAt(journalPath);
3660
4355
  expect(journal.files["notes/legacy.md"]).toBeDefined();
3661
4356
  });
3662
4357
  it("owned-only still requires an intent — it is the documented rollback knob", async () => {
3663
4358
  const companyRoot = path.join(tmpDir, "companies", "acme");
3664
4359
  fs.mkdirSync(companyRoot, { recursive: true });
3665
4360
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3666
- fs.writeFileSync(journalPath, JSON.stringify({
4361
+ seedJournalAt(journalPath, ({
3667
4362
  version: "1",
3668
4363
  lastSync: new Date().toISOString(),
3669
4364
  files: {
@@ -3850,7 +4545,7 @@ describe("share", () => {
3850
4545
  const companyRoot = path.join(tmpDir, "companies", "acme");
3851
4546
  fs.mkdirSync(companyRoot, { recursive: true });
3852
4547
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3853
- fs.writeFileSync(journalPath, JSON.stringify({
4548
+ seedJournalAt(journalPath, ({
3854
4549
  version: "1",
3855
4550
  lastSync: new Date().toISOString(),
3856
4551
  files: {
@@ -3891,7 +4586,7 @@ describe("share", () => {
3891
4586
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "regular.md");
3892
4587
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "CLAUDE.md.conflict-2026-05-13T19-40-40Z-e5797a.md");
3893
4588
  // Both journal entries are removed by the delete loop's removeEntry call.
3894
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4589
+ const journal = journalAt(journalPath);
3895
4590
  expect(journal.files["CLAUDE.md.conflict-2026-05-13T19-40-40Z-e5797a.md"]).toBeUndefined();
3896
4591
  expect(journal.files["regular.md"]).toBeUndefined();
3897
4592
  });
@@ -3906,7 +4601,7 @@ describe("share", () => {
3906
4601
  process.env.HQ_STATE_DIR = stateDirPersonal;
3907
4602
  try {
3908
4603
  const journalPath = path.join(stateDirPersonal, "sync-journal.__hq_personal_vault__.json");
3909
- fs.writeFileSync(journalPath, JSON.stringify({
4604
+ seedJournalAt(journalPath, ({
3910
4605
  version: "1",
3911
4606
  lastSync: new Date().toISOString(),
3912
4607
  files: {
@@ -3932,7 +4627,7 @@ describe("share", () => {
3932
4627
  });
3933
4628
  expect(result.filesDeleted).toBe(1);
3934
4629
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "personal/.obsidian/graph.json.drift-1779863862-42519");
3935
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4630
+ const journal = journalAt(journalPath);
3936
4631
  expect(journal.files["personal/.obsidian/graph.json.drift-1779863862-42519"]).toBeUndefined();
3937
4632
  }
3938
4633
  finally {
@@ -3955,7 +4650,7 @@ describe("share", () => {
3955
4650
  hash: "h", size: 1, syncedAt: new Date().toISOString(),
3956
4651
  direction: "up",
3957
4652
  });
3958
- fs.writeFileSync(journalPath, JSON.stringify({
4653
+ seedJournalAt(journalPath, ({
3959
4654
  version: "1",
3960
4655
  lastSync: new Date().toISOString(),
3961
4656
  files: {
@@ -4003,7 +4698,7 @@ describe("share", () => {
4003
4698
  expect(pruned[0].byReason["ignore-filtered"]).toBe(3);
4004
4699
  expect(pruned[0].byReason["excluded-top-level"]).toBe(1);
4005
4700
  // The persisted journal converged: excluded rows gone, legit row kept.
4006
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4701
+ const journal = journalAt(journalPath);
4007
4702
  expect(Object.keys(journal.files)).toContain("personal/keep.md");
4008
4703
  expect(journal.files[".claude/worktrees/wt/a.ts"]).toBeUndefined();
4009
4704
  expect(journal.files["personal/node_modules/pkg/index.js"]).toBeUndefined();
@@ -4035,7 +4730,7 @@ describe("share", () => {
4035
4730
  const key = "deleted-top-level.md";
4036
4731
  const journalSlug = "personal-scoped-delete";
4037
4732
  const journalPath = path.join(stateDir, `sync-journal.${journalSlug}.json`);
4038
- fs.writeFileSync(journalPath, JSON.stringify({
4733
+ seedJournalAt(journalPath, ({
4039
4734
  version: "2",
4040
4735
  lastSync: new Date().toISOString(),
4041
4736
  pulls: [],
@@ -4067,7 +4762,7 @@ describe("share", () => {
4067
4762
  });
4068
4763
  expect(result.filesDeleted).toBe(1);
4069
4764
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), key);
4070
- expect(JSON.parse(fs.readFileSync(journalPath, "utf8")).files[key]).toBeUndefined();
4765
+ expect(journalAt(journalPath).files[key]).toBeUndefined();
4071
4766
  });
4072
4767
  // ── Decommission prefixes ──────────────────────────────────────────────────
4073
4768
  //
@@ -4122,7 +4817,7 @@ describe("share", () => {
4122
4817
  // Local file remains on disk — decommission is a remote-only sweep.
4123
4818
  expect(fs.existsSync(path.join(tmpDir, "companies", "foo", "notes.md"))).toBe(true);
4124
4819
  // Journal entry pruned in lockstep with the remote delete.
4125
- const journalAfter = JSON.parse(fs.readFileSync(personalJournalPath, "utf-8"));
4820
+ const journalAfter = journalAt(personalJournalPath);
4126
4821
  expect(journalAfter.files["companies/foo/notes.md"]).toBeUndefined();
4127
4822
  expect(journalAfter.files["knowledge/x.md"]).toBeDefined();
4128
4823
  });
@@ -4158,7 +4853,7 @@ describe("share", () => {
4158
4853
  expect(deleteRemoteFile).not.toHaveBeenCalled();
4159
4854
  // Journal entry preserved — the pulled-from-peer record stays so a
4160
4855
  // future pull can still reconcile it correctly.
4161
- const journalAfter = JSON.parse(fs.readFileSync(personalJournalPath, "utf-8"));
4856
+ const journalAfter = journalAt(personalJournalPath);
4162
4857
  expect(journalAfter.files["companies/foo/from-peer.md"]).toBeDefined();
4163
4858
  });
4164
4859
  it("decommissionPrefixes: dedupes with propagateDeletes plan — a key in both still emits one DeleteObject", async () => {
@@ -4278,7 +4973,7 @@ describe("share", () => {
4278
4973
  expect(result.filesTombstoned).toBe(1);
4279
4974
  expect(result.filesDeleted).toBe(0);
4280
4975
  // Journal entry is gone.
4281
- const journalAfter = JSON.parse(fs.readFileSync(personalJournalPath, "utf-8"));
4976
+ const journalAfter = journalAt(personalJournalPath);
4282
4977
  expect(journalAfter.files["companies/foo/gone.md"]).toBeUndefined();
4283
4978
  });
4284
4979
  describe("personalMode", () => {
@@ -4319,7 +5014,7 @@ describe("share", () => {
4319
5014
  const acmeJournalPath = path.join(stateDir, "sync-journal.acme.json");
4320
5015
  expect(fs.existsSync(personalJournalPath)).toBe(true);
4321
5016
  expect(fs.existsSync(acmeJournalPath)).toBe(false);
4322
- const journal = JSON.parse(fs.readFileSync(personalJournalPath, "utf-8"));
5017
+ const journal = journalAt(personalJournalPath);
4323
5018
  expect(journal.files["knowledge/notes.md"]).toBeDefined();
4324
5019
  });
4325
5020
  it("personalMode=true accepts files outside companies/<slug>/ (companion to the company-folder rejection)", async () => {
@@ -4607,7 +5302,7 @@ describe("share", () => {
4607
5302
  .createHash("sha256")
4608
5303
  .update(target)
4609
5304
  .digest("hex");
4610
- fs.writeFileSync(journalPath, JSON.stringify({
5305
+ seedJournalAt(journalPath, ({
4611
5306
  version: "1",
4612
5307
  lastSync: new Date().toISOString(),
4613
5308
  files: {
@@ -5010,7 +5705,7 @@ describe("share", () => {
5010
5705
  });
5011
5706
  expect(pushed.filesUploaded).toBe(1);
5012
5707
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5013
- const afterPush = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5708
+ const afterPush = journalAt(journalPath);
5014
5709
  expect(Object.keys(afterPush.files)).toContain("knowledge/agents/brief.md");
5015
5710
  vi.mocked(deleteRemoteFile).mockClear();
5016
5711
  const walked = await share({
@@ -5024,7 +5719,7 @@ describe("share", () => {
5024
5719
  expect(walked.filesDeleted).toBe(0);
5025
5720
  const deletedKeys = vi.mocked(deleteRemoteFile).mock.calls.map((c) => c[1]);
5026
5721
  expect(deletedKeys).not.toContain("knowledge/agents/brief.md");
5027
- const afterWalk = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5722
+ const afterWalk = journalAt(journalPath);
5028
5723
  expect(Object.keys(afterWalk.files)).toContain("knowledge/agents/brief.md");
5029
5724
  });
5030
5725
  });
@@ -5068,7 +5763,7 @@ describe("share", () => {
5068
5763
  }
5069
5764
  }
5070
5765
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5071
- fs.writeFileSync(journalPath, JSON.stringify({
5766
+ seedJournalAt(journalPath, ({
5072
5767
  version: "1",
5073
5768
  lastSync: new Date().toISOString(),
5074
5769
  files,
@@ -5126,7 +5821,7 @@ describe("share", () => {
5126
5821
  expect(summary.samplePaths.length).toBeGreaterThan(0);
5127
5822
  expect(summary.samplePaths.length).toBeLessThanOrEqual(10);
5128
5823
  // Journal entries for the missing files SURVIVE — guard never mutates.
5129
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5824
+ const journal = journalAt(journalPath);
5130
5825
  expect(Object.keys(journal.files).length).toBe(100);
5131
5826
  // Per-key delete-refused-stale-etag events for each candidate.
5132
5827
  const perKeyRefusals = events.filter((e) => e.type === "delete-refused-stale-etag" && e.reason === "bulk-asymmetry");
@@ -5176,7 +5871,7 @@ describe("share", () => {
5176
5871
  expect(headedKeys).not.toContain(`f-${i.toString().padStart(4, "0")}.md`);
5177
5872
  }
5178
5873
  // Journal untouched — the entries stay recoverable.
5179
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5874
+ const journal = journalAt(journalPath);
5180
5875
  expect(Object.keys(journal.files).length).toBe(100);
5181
5876
  });
5182
5877
  // The core of the #227 regression, isolated: pre-#227 a trip emptied
@@ -5207,7 +5902,7 @@ describe("share", () => {
5207
5902
  const perKeyRefusals = events.filter((e) => e.type === "delete-refused-stale-etag" &&
5208
5903
  e.reason === "bulk-asymmetry");
5209
5904
  expect(perKeyRefusals.length).toBe(11);
5210
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5905
+ const journal = journalAt(journalPath);
5211
5906
  expect(Object.keys(journal.files).length).toBe(100);
5212
5907
  });
5213
5908
  it("vault litter still drains when the breaker trips", async () => {
@@ -5221,7 +5916,7 @@ describe("share", () => {
5221
5916
  withIntents: false,
5222
5917
  });
5223
5918
  const litterKey = "CLAUDE.md.conflict-2026-05-13T19-40-40Z-e5797a.md";
5224
- const seeded = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5919
+ const seeded = journalAt(journalPath);
5225
5920
  seeded.files[litterKey] = {
5226
5921
  hash: "h",
5227
5922
  size: 5,
@@ -5230,7 +5925,7 @@ describe("share", () => {
5230
5925
  remoteEtag: "litter-etag",
5231
5926
  kind: "file",
5232
5927
  };
5233
- fs.writeFileSync(journalPath, JSON.stringify(seeded));
5928
+ seedJournalAt(journalPath, seeded);
5234
5929
  vi.mocked(headRemoteFile).mockImplementation(async (_ctx, key) => ({
5235
5930
  etag: `etag-${parseInt(key.replace(/[^0-9]/g, ""), 10)}`,
5236
5931
  lastModified: new Date(),
@@ -5370,7 +6065,7 @@ describe("share", () => {
5370
6065
  expect(summary).toBeDefined();
5371
6066
  expect(summary.ratio).toBeCloseTo(1.0, 5);
5372
6067
  // Journal untouched.
5373
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
6068
+ const journal = journalAt(journalPath);
5374
6069
  expect(Object.keys(journal.files).length).toBe(50);
5375
6070
  });
5376
6071
  it("small absolute count (9/9 = 100% ratio but < MIN_ABS=10) does NOT trip: all 9 delete normally", async () => {
@@ -5414,7 +6109,7 @@ describe("share", () => {
5414
6109
  expect(deleteRemoteFile).not.toHaveBeenCalled();
5415
6110
  expect(events.find((e) => e.type === "delete-refused-bulk-asymmetry")).toBeDefined();
5416
6111
  // Journal entries survive.
5417
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
6112
+ const journal = journalAt(journalPath);
5418
6113
  expect(Object.keys(journal.files).length).toBe(100);
5419
6114
  });
5420
6115
  it("direction:'down' entries don't count toward the bulk numerator (owned-only silently skips them)", async () => {
@@ -5442,7 +6137,7 @@ describe("share", () => {
5442
6137
  localDeleteIntent: deleteIntent("up-etag", "h"),
5443
6138
  };
5444
6139
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5445
- fs.writeFileSync(journalPath, JSON.stringify({ version: "1", lastSync: now, files }));
6140
+ seedJournalAt(journalPath, { version: "1", lastSync: now, files });
5446
6141
  const result = await share({
5447
6142
  paths: [companyRoot],
5448
6143
  company: "acme",
@@ -5565,7 +6260,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5565
6260
  fs.mkdirSync(companyRoot, { recursive: true });
5566
6261
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5567
6262
  // v2-shaped fixture — same field set the production journal uses.
5568
- fs.writeFileSync(journalPath, JSON.stringify({
6263
+ seedJournalAt(journalPath, ({
5569
6264
  version: "2",
5570
6265
  lastSync: new Date().toISOString(),
5571
6266
  files: {
@@ -5611,7 +6306,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5611
6306
  const { hashFile } = await import("../journal.js");
5612
6307
  const baselineHash = hashFile(testFile);
5613
6308
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5614
- fs.writeFileSync(journalPath, JSON.stringify({
6309
+ seedJournalAt(journalPath, ({
5615
6310
  version: "2",
5616
6311
  lastSync: new Date().toISOString(),
5617
6312
  files: {
@@ -5652,7 +6347,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5652
6347
  const originalLstat = fs.lstatSync(testFile);
5653
6348
  // The recorded hash intentionally differs from the current bytes.
5654
6349
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5655
- fs.writeFileSync(journalPath, JSON.stringify({
6350
+ seedJournalAt(journalPath, ({
5656
6351
  version: "2",
5657
6352
  lastSync: new Date().toISOString(),
5658
6353
  files: {
@@ -5699,7 +6394,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5699
6394
  const { hashFile } = await import("../journal.js");
5700
6395
  const realHash = hashFile(testFile);
5701
6396
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5702
- fs.writeFileSync(journalPath, JSON.stringify({
6397
+ seedJournalAt(journalPath, ({
5703
6398
  version: "2",
5704
6399
  lastSync: new Date().toISOString(),
5705
6400
  files: {
@@ -5749,7 +6444,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5749
6444
  const { hashFile } = await import("../journal.js");
5750
6445
  const realHash = hashFile(testFile);
5751
6446
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5752
- fs.writeFileSync(journalPath, JSON.stringify({
6447
+ seedJournalAt(journalPath, ({
5753
6448
  version: "2",
5754
6449
  lastSync: new Date().toISOString(),
5755
6450
  files: {
@@ -5781,7 +6476,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5781
6476
  expect(uploadFile).not.toHaveBeenCalled();
5782
6477
  // Journal mtimeMs is now the bumped value, while content hashing remains
5783
6478
  // authoritative on the next sync.
5784
- const journalAfter = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
6479
+ const journalAfter = journalAt(journalPath);
5785
6480
  expect(journalAfter.files["touched-then-fast.md"].mtimeMs).toBe(bumpedLstat.mtimeMs);
5786
6481
  expect(journalAfter.files["touched-then-fast.md"].size).toBe(bumpedLstat.size);
5787
6482
  // Content hash untouched — the file genuinely didn't change.
@@ -5801,7 +6496,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5801
6496
  });
5802
6497
  expect(result.filesUploaded).toBe(1);
5803
6498
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5804
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
6499
+ const journal = journalAt(journalPath);
5805
6500
  expect(journal.files["fresh.md"]).toBeDefined();
5806
6501
  expect(journal.files["fresh.md"].mtimeMs).toBe(lstat.mtimeMs);
5807
6502
  expect(journal.files["fresh.md"].size).toBe(lstat.size);
@@ -5818,7 +6513,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5818
6513
  const realSize = fs.statSync(testFile).size;
5819
6514
  // Real hash but NO mtimeMs. Hash matches → skip upload.
5820
6515
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5821
- fs.writeFileSync(journalPath, JSON.stringify({
6516
+ seedJournalAt(journalPath, ({
5822
6517
  version: "1",
5823
6518
  lastSync: new Date().toISOString(),
5824
6519
  files: {
@@ -6377,7 +7072,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
6377
7072
  // AND lost their etag/content-hash/stat journal metadata.
6378
7073
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
6379
7074
  expect(fs.existsSync(journalPath)).toBe(true);
6380
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
7075
+ const journal = journalAt(journalPath);
6381
7076
  const stampedKeys = Object.keys(journal.files);
6382
7077
  expect(stampedKeys.length).toBe(FILE_COUNT - 1);
6383
7078
  // The failed file is NOT in the journal (no successful upload).
@@ -6525,7 +7220,7 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
6525
7220
  const companyRoot = path.join(tmpDir, "companies", "acme");
6526
7221
  fs.mkdirSync(companyRoot, { recursive: true });
6527
7222
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
6528
- fs.writeFileSync(journalPath, JSON.stringify({
7223
+ seedJournalAt(journalPath, ({
6529
7224
  version: "1",
6530
7225
  lastSync: new Date().toISOString(),
6531
7226
  files: {
@@ -6554,7 +7249,7 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
6554
7249
  expect(headRemoteFile).not.toHaveBeenCalled();
6555
7250
  expect(deleteRemoteFile).not.toHaveBeenCalled();
6556
7251
  expect(uploadFile).not.toHaveBeenCalled();
6557
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
7252
+ const journal = journalAt(journalPath);
6558
7253
  expect(journal.files["companies/acme/knowledge/poison.md"]).toBeUndefined();
6559
7254
  });
6560
7255
  it("defers an unreadable scope-invalid symlink tombstone without a fatal error", async () => {
@@ -6566,7 +7261,7 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
6566
7261
  fs.mkdirSync(targetDir, { recursive: true });
6567
7262
  fs.symlinkSync(targetDir, linkPath, "dir");
6568
7263
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
6569
- fs.writeFileSync(journalPath, JSON.stringify({
7264
+ seedJournalAt(journalPath, ({
6570
7265
  version: "1",
6571
7266
  lastSync: new Date().toISOString(),
6572
7267
  files: {
@@ -6611,7 +7306,7 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
6611
7306
  samplePaths: [linkKey],
6612
7307
  });
6613
7308
  expect(events.some((event) => event.type === "error")).toBe(false);
6614
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
7309
+ const journal = journalAt(journalPath);
6615
7310
  expect(journal.files[linkKey]).toBeDefined();
6616
7311
  }
6617
7312
  finally {