@indigoai-us/hq-cloud 6.15.30 → 6.15.31

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 (119) hide show
  1. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  2. package/dist/bin/sync-runner-company.js +24 -13
  3. package/dist/bin/sync-runner-company.js.map +1 -1
  4. package/dist/bin/sync-runner-company.test.js +0 -71
  5. package/dist/bin/sync-runner-company.test.js.map +1 -1
  6. package/dist/bin/sync-runner-watch-loop.d.ts +0 -10
  7. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  8. package/dist/bin/sync-runner-watch-loop.js +13 -170
  9. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  10. package/dist/bin/sync-runner-watch-routes.d.ts +1 -16
  11. package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
  12. package/dist/bin/sync-runner-watch-routes.js +2 -40
  13. package/dist/bin/sync-runner-watch-routes.js.map +1 -1
  14. package/dist/bin/sync-runner-watch-routes.test.js +1 -53
  15. package/dist/bin/sync-runner-watch-routes.test.js.map +1 -1
  16. package/dist/bin/sync-runner.d.ts +3 -15
  17. package/dist/bin/sync-runner.d.ts.map +1 -1
  18. package/dist/bin/sync-runner.js +13 -66
  19. package/dist/bin/sync-runner.js.map +1 -1
  20. package/dist/bin/sync-runner.test.js +13 -433
  21. package/dist/bin/sync-runner.test.js.map +1 -1
  22. package/dist/cli/conflict.d.ts +2 -25
  23. package/dist/cli/conflict.d.ts.map +1 -1
  24. package/dist/cli/conflict.js +4 -29
  25. package/dist/cli/conflict.js.map +1 -1
  26. package/dist/cli/share.d.ts +4 -40
  27. package/dist/cli/share.d.ts.map +1 -1
  28. package/dist/cli/share.js +86 -478
  29. package/dist/cli/share.js.map +1 -1
  30. package/dist/cli/share.test.js +150 -845
  31. package/dist/cli/share.test.js.map +1 -1
  32. package/dist/cli/sync-scope.test.js +18 -147
  33. package/dist/cli/sync-scope.test.js.map +1 -1
  34. package/dist/cli/sync.d.ts +0 -20
  35. package/dist/cli/sync.d.ts.map +1 -1
  36. package/dist/cli/sync.js +29 -261
  37. package/dist/cli/sync.js.map +1 -1
  38. package/dist/cli/sync.test.js +90 -519
  39. package/dist/cli/sync.test.js.map +1 -1
  40. package/dist/journal.d.ts +0 -1
  41. package/dist/journal.d.ts.map +1 -1
  42. package/dist/journal.js +25 -155
  43. package/dist/journal.js.map +1 -1
  44. package/dist/journal.test.js +11 -29
  45. package/dist/journal.test.js.map +1 -1
  46. package/dist/object-io.d.ts +13 -34
  47. package/dist/object-io.d.ts.map +1 -1
  48. package/dist/object-io.js +29 -154
  49. package/dist/object-io.js.map +1 -1
  50. package/dist/outcome-telemetry.d.ts +1 -7
  51. package/dist/outcome-telemetry.d.ts.map +1 -1
  52. package/dist/outcome-telemetry.js +9 -15
  53. package/dist/outcome-telemetry.js.map +1 -1
  54. package/dist/outcome-telemetry.test.js +0 -15
  55. package/dist/outcome-telemetry.test.js.map +1 -1
  56. package/dist/outposts/client.d.ts +2 -2
  57. package/dist/outposts/client.d.ts.map +1 -1
  58. package/dist/outposts/client.js +3 -4
  59. package/dist/outposts/client.js.map +1 -1
  60. package/dist/outposts/command.d.ts.map +1 -1
  61. package/dist/outposts/command.js +6 -9
  62. package/dist/outposts/command.js.map +1 -1
  63. package/dist/outposts/command.test.js +2 -59
  64. package/dist/outposts/command.test.js.map +1 -1
  65. package/dist/outposts/remote-command.d.ts +8 -8
  66. package/dist/outposts/remote-command.d.ts.map +1 -1
  67. package/dist/outposts/remote-command.js +8 -10
  68. package/dist/outposts/remote-command.js.map +1 -1
  69. package/dist/prefix-coalesce.d.ts +0 -14
  70. package/dist/prefix-coalesce.d.ts.map +1 -1
  71. package/dist/prefix-coalesce.js +0 -29
  72. package/dist/prefix-coalesce.js.map +1 -1
  73. package/dist/prefix-coalesce.test.js +1 -22
  74. package/dist/prefix-coalesce.test.js.map +1 -1
  75. package/dist/skill-telemetry.d.ts +1 -6
  76. package/dist/skill-telemetry.d.ts.map +1 -1
  77. package/dist/skill-telemetry.js +8 -14
  78. package/dist/skill-telemetry.js.map +1 -1
  79. package/dist/skill-telemetry.test.js +0 -43
  80. package/dist/skill-telemetry.test.js.map +1 -1
  81. package/dist/sts-credential-refresh.test.js +2 -87
  82. package/dist/sts-credential-refresh.test.js.map +1 -1
  83. package/dist/sync/state-store.d.ts.map +1 -1
  84. package/dist/sync/state-store.js +1 -20
  85. package/dist/sync/state-store.js.map +1 -1
  86. package/dist/sync/state-store.test.js +0 -64
  87. package/dist/sync/state-store.test.js.map +1 -1
  88. package/dist/sync-core.d.ts.map +1 -1
  89. package/dist/sync-core.js +2 -7
  90. package/dist/sync-core.js.map +1 -1
  91. package/dist/sync-core.test.js +0 -36
  92. package/dist/sync-core.test.js.map +1 -1
  93. package/dist/telemetry.d.ts +6 -30
  94. package/dist/telemetry.d.ts.map +1 -1
  95. package/dist/telemetry.js +135 -331
  96. package/dist/telemetry.js.map +1 -1
  97. package/dist/telemetry.test.js +0 -403
  98. package/dist/telemetry.test.js.map +1 -1
  99. package/dist/types.d.ts +3 -26
  100. package/dist/types.d.ts.map +1 -1
  101. package/dist/watcher.d.ts +0 -76
  102. package/dist/watcher.d.ts.map +1 -1
  103. package/dist/watcher.js +3 -105
  104. package/dist/watcher.js.map +1 -1
  105. package/dist/watcher.test.js +1 -165
  106. package/dist/watcher.test.js.map +1 -1
  107. package/package.json +1 -1
  108. package/dist/bandwidth.d.ts +0 -128
  109. package/dist/bandwidth.d.ts.map +0 -1
  110. package/dist/bandwidth.js +0 -254
  111. package/dist/bandwidth.js.map +0 -1
  112. package/dist/bandwidth.test.d.ts +0 -2
  113. package/dist/bandwidth.test.d.ts.map +0 -1
  114. package/dist/bandwidth.test.js +0 -243
  115. package/dist/bandwidth.test.js.map +0 -1
  116. package/dist/journal-delta-writes.test.d.ts +0 -2
  117. package/dist/journal-delta-writes.test.d.ts.map +0 -1
  118. package/dist/journal-delta-writes.test.js +0 -140
  119. package/dist/journal-delta-writes.test.js.map +0 -1
@@ -5,7 +5,6 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
5
5
  import * as fs from "fs";
6
6
  import * as path from "path";
7
7
  import * as os from "os";
8
- import * as crypto from "crypto";
9
8
  import { clearContextCache } from "../context.js";
10
9
  import { lockPathFor } from "../operation-lock.js";
11
10
  // Re-export node:fs as a mutable module so unreadable-link regressions can
@@ -14,13 +13,6 @@ vi.mock("fs", async (importOriginal) => {
14
13
  const actual = await importOriginal();
15
14
  return { ...actual };
16
15
  });
17
- // Same mutable-namespace trick as `fs` above: `skip` is an interactive-only
18
- // resolution, so the only way to exercise its arm is to stand in for the
19
- // prompt. Re-exporting the real module keeps every other call site genuine.
20
- vi.mock("./conflict.js", async (importOriginal) => {
21
- const actual = await importOriginal();
22
- return { ...actual };
23
- });
24
16
  describe("sync mutation stdin boundary", () => {
25
17
  it("accepts only the exact sidecar command and emits one discriminated upsert result", async () => {
26
18
  expect(SYNC_MUTATION_STDIN_COMMAND).toEqual(["sync", "mutation", "--stdin-json"]);
@@ -123,37 +115,10 @@ vi.mock("./reindex.js", () => ({
123
115
  import { sync, reportNewFilesToNotify, applyRealtimeDeltaPage, isSyncMutationStdinCommand, runSyncMutationStdinJson, SYNC_MUTATION_STDIN_COMMAND, win32SymlinkFlavorBroken, } from "./sync.js";
124
116
  import { share } from "./share.js";
125
117
  import * as s3Module from "../s3.js";
126
- import * as conflictModule from "./conflict.js";
127
118
  import { reindex } from "./reindex.js";
128
119
  import { readSyncProgress } from "../sync-progress.js";
129
120
  import { VaultAuthError } from "../vault-client.js";
130
- import { readJournal, getEntry, listJournals } from "../journal.js";
131
- /**
132
- * Read a journal the way production does.
133
- *
134
- * These tests used to `JSON.parse` `sync-journal.<slug>.json` directly, back
135
- * when that file held a full copy of the journal. It is now a small locator —
136
- * the authority is the v3 state store — so the assertions below go through
137
- * `readJournal`, which is what the sync engine itself uses. The slug is
138
- * recovered from the path so each call site keeps naming the journal it means.
139
- */
140
- function journalAt(journalPath) {
141
- const slug = path
142
- .basename(journalPath)
143
- .replace(/^sync-journal\./, "")
144
- .replace(/\.json$/, "");
145
- return readJournal(slug);
146
- }
147
- /**
148
- * The single `.conflict-` sidecar in a directory. Asserting through this rather
149
- * than re-globbing inline keeps "exactly one was minted" enforced everywhere a
150
- * test reaches for the preserved side.
151
- */
152
- function sidecarIn(dir) {
153
- const found = fs.readdirSync(dir).filter((f) => f.includes(".conflict-"));
154
- expect(found).toHaveLength(1);
155
- return path.join(dir, found[0]);
156
- }
121
+ import { readJournal, getEntry } from "../journal.js";
157
122
  const mockConfig = {
158
123
  apiUrl: "https://vault-api.test",
159
124
  authToken: "test-jwt-token",
@@ -501,7 +466,7 @@ describe("sync", () => {
501
466
  const result = await sync({ vaultConfig: mockConfig, hqRoot: tmpDir });
502
467
  expect(result.filesDownloaded).toBe(2);
503
468
  });
504
- it("detects conflicts with local changes and adopts the cloud copy on --on-conflict keep", async () => {
469
+ it("detects conflicts with local changes and keeps local on --on-conflict keep", async () => {
505
470
  const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
506
471
  fs.mkdirSync(companyDocs, { recursive: true });
507
472
  fs.writeFileSync(path.join(companyDocs, "handoff.md"), "local version");
@@ -525,66 +490,10 @@ describe("sync", () => {
525
490
  });
526
491
  expect(result.conflicts).toBe(1);
527
492
  expect(result.conflictPaths).toEqual(["docs/handoff.md"]);
528
- // `keep` adopts the cloud body at the original name…
529
- expect(fs.readFileSync(path.join(companyDocs, "handoff.md"), "utf-8")).toBe("mock file content");
530
- // …and the local one survives beside it.
531
- expect(fs.readFileSync(sidecarIn(companyDocs), "utf-8")).toBe("local version");
532
- });
533
- it("rechecks the current local body when executing a planned conflict", async () => {
534
- const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
535
- fs.mkdirSync(companyDocs, { recursive: true });
536
- const localPath = path.join(companyDocs, "handoff.md");
537
- fs.writeFileSync(localPath, "local body seen by the planner");
538
- fs.writeFileSync(journalPath, JSON.stringify({
539
- version: "1",
540
- lastSync: new Date().toISOString(),
541
- files: {
542
- "docs/handoff.md": {
543
- hash: "common-base-hash",
544
- size: 16,
545
- syncedAt: new Date(Date.now() - 3600000).toISOString(),
546
- direction: "down",
547
- remoteEtag: "common-base-etag",
548
- },
549
- },
550
- }));
551
- vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
552
- {
553
- key: "docs/handoff.md",
554
- size: "mock file content".length,
555
- lastModified: new Date(),
556
- etag: '"new-remote-etag"',
557
- },
558
- ]);
559
- let changedAfterPlan = false;
560
- const result = await sync({
561
- company: "acme",
562
- onConflict: "keep",
563
- vaultConfig: mockConfig,
564
- hqRoot: tmpDir,
565
- onEvent: (event) => {
566
- if (event.type === "plan" && !changedAfterPlan) {
567
- changedAfterPlan = true;
568
- // The local writer converged this path with the cloud after the
569
- // whole-run preview classified it, but before its action executes.
570
- fs.writeFileSync(localPath, "mock file content");
571
- }
572
- },
573
- });
574
- expect(changedAfterPlan).toBe(true);
575
- expect(result.conflicts).toBe(0);
576
- expect(result.conflictPaths).toEqual([]);
577
- expect(fs.readdirSync(companyDocs).filter((name) => name.includes(".conflict-")))
578
- .toEqual([]);
579
- const entry = journalAt(journalPath).files["docs/handoff.md"];
580
- expect(entry).toMatchObject({
581
- hash: crypto.createHash("sha256").update("mock file content").digest("hex"),
582
- remoteEtag: "new-remote-etag",
583
- direction: "down",
584
- });
585
- expect(entry.localDiverges).toBeFalsy();
493
+ expect(result.filesSkipped).toBeGreaterThanOrEqual(1);
494
+ expect(fs.readFileSync(path.join(companyDocs, "handoff.md"), "utf-8")).toBe("local version");
586
495
  });
587
- it("conflict skip marks the entry localDiverges (journal-honesty: the held copy never matched the recorded remote etag)", async () => {
496
+ it("conflict keep marks the entry localDiverges (journal-honesty: kept copy never matched the recorded remote etag)", async () => {
588
497
  const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
589
498
  fs.mkdirSync(companyDocs, { recursive: true });
590
499
  fs.writeFileSync(path.join(companyDocs, "handoff.md"), "local version");
@@ -600,311 +509,20 @@ describe("sync", () => {
600
509
  },
601
510
  },
602
511
  }));
603
- // `keep` no longer holds, so the honesty flag now belongs to the arms that
604
- // still do. `skip` is the interactive one and is exercised through the
605
- // prompt stand-in.
606
- const resolveSpy = vi
607
- .spyOn(conflictModule, "resolveConflict")
608
- .mockResolvedValue("skip");
609
- let result;
610
- try {
611
- result = await sync({
612
- company: "acme",
613
- vaultConfig: mockConfig,
614
- hqRoot: tmpDir,
615
- });
616
- }
617
- finally {
618
- resolveSpy.mockRestore();
619
- }
620
- expect(result.conflicts).toBe(1);
621
- const journal = journalAt(journalPath);
622
- // remoteEtag still recorded so the conflict can't re-fire (#137 invariant)…
623
- expect(journal.files["docs/handoff.md"].remoteEtag).toBeTruthy();
624
- // …but the held local copy is flagged divergent so a later currency-gated
625
- // delete cannot fire on a journal that lies about local==remote.
626
- expect(journal.files["docs/handoff.md"].localDiverges).toBe(true);
627
- });
628
- // `publish-local` reaches the PULL leg too, because the strategy is set once
629
- // for a bidirectional run. It has no meaning here — there is nothing to
630
- // publish while deciding what to do with an incoming object — so it MUST
631
- // degrade to `keep`. If it instead fell through to the download branch it
632
- // would overwrite the very local body the operator elected to publish, which
633
- // is precisely the data loss the strategy exists to prevent.
634
- it("conflict publish-local on the pull leg keeps local and never downloads over it", async () => {
635
- const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
636
- fs.mkdirSync(companyDocs, { recursive: true });
637
- fs.writeFileSync(path.join(companyDocs, "handoff.md"), "my elected local version");
638
- fs.writeFileSync(journalPath, JSON.stringify({
639
- version: "1",
640
- lastSync: new Date().toISOString(),
641
- files: {
642
- "docs/handoff.md": {
643
- hash: "old-hash-from-last-sync",
644
- size: 20,
645
- syncedAt: new Date(Date.now() - 3600000).toISOString(),
646
- direction: "down",
647
- },
648
- },
649
- }));
650
512
  const result = await sync({
651
513
  company: "acme",
652
- onConflict: "publish-local",
514
+ onConflict: "keep",
653
515
  vaultConfig: mockConfig,
654
516
  hqRoot: tmpDir,
655
517
  });
656
518
  expect(result.conflicts).toBe(1);
657
- expect(fs.readFileSync(path.join(companyDocs, "handoff.md"), "utf-8")).toBe("my elected local version");
658
- // Identical bookkeeping to `keep`: the etag is stamped so the conflict
659
- // cannot re-fire, and the honesty flag is armed so the push leg knows this
660
- // is the divergent body it may promote under an If-Match fence.
661
- const journal = journalAt(journalPath);
519
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
520
+ // remoteEtag still recorded so the conflict can't re-fire (#137 invariant)…
662
521
  expect(journal.files["docs/handoff.md"].remoteEtag).toBeTruthy();
522
+ // …but the kept-local copy is flagged divergent so a later currency-gated
523
+ // delete cannot fire on a journal that lies about local==remote.
663
524
  expect(journal.files["docs/handoff.md"].localDiverges).toBe(true);
664
525
  });
665
- // --- `keep` semantics, revised 2026-08-21 -------------------------------
666
- //
667
- // `keep` now means "the CLOUD copy becomes the working copy; my local body is
668
- // preserved beside it as a `.conflict-` sidecar." It used to mean the
669
- // opposite — hold local, stamp the remote etag, arm `localDiverges` — and
670
- // that combination was terminal: push refused the divergent local, pull saw
671
- // the remote unchanged so it never replaced the entry, and every subsequent
672
- // `keep` re-armed the flag. The file could converge in neither direction, and
673
- // because the desktop app hardcodes `--on-conflict keep`, that was the
674
- // default outcome for every real conflict.
675
- //
676
- // Electing the cloud body clears the divergence BY CONSTRUCTION — the entry
677
- // then genuinely records what sits on disk — while the local bytes survive
678
- // under the sidecar for the operator to re-apply. Nothing is destroyed; the
679
- // working copy changes and the old one is renamed.
680
- it("conflict keep takes the cloud body at the original path and preserves local in a `.conflict-` sidecar", async () => {
681
- const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
682
- fs.mkdirSync(companyDocs, { recursive: true });
683
- fs.writeFileSync(path.join(companyDocs, "handoff.md"), "local version");
684
- vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
685
- { key: "docs/handoff.md", size: 42, lastModified: new Date(), etag: '"divergent"' },
686
- ]);
687
- fs.writeFileSync(journalPath, JSON.stringify({
688
- version: "1",
689
- lastSync: new Date().toISOString(),
690
- files: {
691
- "docs/handoff.md": {
692
- hash: "old-hash-from-last-sync",
693
- size: 20,
694
- syncedAt: new Date(Date.now() - 3600000).toISOString(),
695
- direction: "down",
696
- },
697
- },
698
- }));
699
- const result = await sync({
700
- company: "acme",
701
- onConflict: "keep",
702
- vaultConfig: mockConfig,
703
- hqRoot: tmpDir,
704
- });
705
- expect(result.conflicts).toBe(1);
706
- expect(result.conflictPaths).toEqual(["docs/handoff.md"]);
707
- // The cloud body is the working copy now.
708
- expect(fs.readFileSync(path.join(companyDocs, "handoff.md"), "utf-8")).toBe("mock file content");
709
- // …and the local body is not lost — it is sitting beside it.
710
- const sidecars = fs
711
- .readdirSync(companyDocs)
712
- .filter((f) => f.includes(".conflict-"));
713
- expect(sidecars).toHaveLength(1);
714
- expect(fs.readFileSync(path.join(companyDocs, sidecars[0]), "utf-8")).toBe("local version");
715
- });
716
- it("conflict keep clears the fetched sidecar before rename for Windows compatibility", async () => {
717
- const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
718
- fs.mkdirSync(companyDocs, { recursive: true });
719
- const originalPath = path.join(companyDocs, "handoff.md");
720
- fs.writeFileSync(originalPath, "local version");
721
- vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
722
- { key: "docs/handoff.md", size: 42, lastModified: new Date(), etag: '"divergent"' },
723
- ]);
724
- fs.writeFileSync(journalPath, JSON.stringify({
725
- version: "1",
726
- lastSync: new Date().toISOString(),
727
- files: {
728
- "docs/handoff.md": {
729
- hash: "old-hash-from-last-sync",
730
- size: 20,
731
- syncedAt: new Date(Date.now() - 3600000).toISOString(),
732
- direction: "down",
733
- },
734
- },
735
- }));
736
- // Node's Windows rename does not replace an occupied destination. Model
737
- // that rule only for the local→sidecar move; unrelated atomic index and
738
- // journal renames keep using the host filesystem's normal behaviour.
739
- const realRenameSync = fs.renameSync;
740
- const renameSpy = vi.spyOn(fs, "renameSync").mockImplementation((from, to) => {
741
- if (String(from) === originalPath &&
742
- String(to).includes(".conflict-") &&
743
- fs.existsSync(to)) {
744
- throw errnoError("EEXIST");
745
- }
746
- return realRenameSync(from, to);
747
- });
748
- try {
749
- await sync({
750
- company: "acme",
751
- onConflict: "keep",
752
- vaultConfig: mockConfig,
753
- hqRoot: tmpDir,
754
- });
755
- }
756
- finally {
757
- renameSpy.mockRestore();
758
- }
759
- expect(fs.readFileSync(originalPath, "utf-8")).toBe("mock file content");
760
- expect(fs.readFileSync(sidecarIn(companyDocs), "utf-8")).toBe("local version");
761
- });
762
- it("conflict keep converges the journal on the REMOTE body and leaves localDiverges unset", async () => {
763
- const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
764
- fs.mkdirSync(companyDocs, { recursive: true });
765
- fs.writeFileSync(path.join(companyDocs, "handoff.md"), "local version");
766
- vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
767
- { key: "docs/handoff.md", size: 42, lastModified: new Date(), etag: '"divergent"' },
768
- ]);
769
- fs.writeFileSync(journalPath, JSON.stringify({
770
- version: "1",
771
- lastSync: new Date().toISOString(),
772
- files: {
773
- "docs/handoff.md": {
774
- hash: "old-hash-from-last-sync",
775
- size: 20,
776
- syncedAt: new Date(Date.now() - 3600000).toISOString(),
777
- direction: "down",
778
- },
779
- },
780
- }));
781
- await sync({
782
- company: "acme",
783
- onConflict: "keep",
784
- vaultConfig: mockConfig,
785
- hqRoot: tmpDir,
786
- });
787
- const entry = journalAt(journalPath).files["docs/handoff.md"];
788
- expect(entry.remoteEtag).toBeTruthy();
789
- // The whole point of the change: the entry describes the body that is
790
- // actually on disk, so the honesty flag has nothing to report. A set flag
791
- // here would re-create the deadlock this test exists to prevent.
792
- expect(entry.localDiverges).toBeFalsy();
793
- });
794
- it("conflict keep records the REMOTE body's hash, so the entry is pushable instead of permanently divergent", async () => {
795
- // This is the deadlock in one assertion. The old `keep` wrote the LOCAL
796
- // hash next to the REMOTE etag, which is a self-contradictory entry: the
797
- // push leg refuses to send a body the journal calls divergent, and the pull
798
- // leg sees a remote whose etag already matches and leaves it alone. Writing
799
- // the remote hash is what makes the entry describable — and therefore
800
- // sendable — again.
801
- const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
802
- fs.mkdirSync(companyDocs, { recursive: true });
803
- fs.writeFileSync(path.join(companyDocs, "handoff.md"), "local version");
804
- vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
805
- { key: "docs/handoff.md", size: 42, lastModified: new Date(), etag: '"divergent"' },
806
- ]);
807
- fs.writeFileSync(journalPath, JSON.stringify({
808
- version: "1",
809
- lastSync: new Date().toISOString(),
810
- files: {
811
- "docs/handoff.md": {
812
- hash: "old-hash-from-last-sync",
813
- size: 20,
814
- syncedAt: new Date(Date.now() - 3600000).toISOString(),
815
- direction: "down",
816
- },
817
- },
818
- }));
819
- await sync({
820
- company: "acme",
821
- onConflict: "keep",
822
- vaultConfig: mockConfig,
823
- hqRoot: tmpDir,
824
- });
825
- // Pinned to the REMOTE body specifically, not to "whatever is on disk" —
826
- // the old behaviour also left those two agreeing (it recorded the local
827
- // hash and left the local file in place), so a disk-relative assertion
828
- // would pass against the very bug this guards.
829
- const remoteHash = crypto
830
- .createHash("sha256")
831
- .update("mock file content")
832
- .digest("hex");
833
- const entry = journalAt(journalPath).files["docs/handoff.md"];
834
- expect(entry.hash).toBe(remoteHash);
835
- });
836
- it("conflict keep indexes the sidecar as the LOCAL body so /resolve-conflicts offers the right side", async () => {
837
- const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
838
- fs.mkdirSync(companyDocs, { recursive: true });
839
- fs.writeFileSync(path.join(companyDocs, "handoff.md"), "local version");
840
- vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
841
- { key: "docs/handoff.md", size: 42, lastModified: new Date(), etag: '"divergent"' },
842
- ]);
843
- fs.writeFileSync(journalPath, JSON.stringify({
844
- version: "1",
845
- lastSync: new Date().toISOString(),
846
- files: {
847
- "docs/handoff.md": {
848
- hash: "old-hash-from-last-sync",
849
- size: 20,
850
- syncedAt: new Date(Date.now() - 3600000).toISOString(),
851
- direction: "down",
852
- },
853
- },
854
- }));
855
- await sync({
856
- company: "acme",
857
- onConflict: "keep",
858
- vaultConfig: mockConfig,
859
- hqRoot: tmpDir,
860
- });
861
- const index = JSON.parse(fs.readFileSync(path.join(tmpDir, ".hq-conflicts", "index.json"), "utf-8"));
862
- expect(index.conflicts).toHaveLength(1);
863
- const entry = index.conflicts[0];
864
- expect(entry.originalPath).toBe("companies/acme/docs/handoff.md");
865
- expect(entry.preserved).toBe("local");
866
- // The sidecar it points at holds the LOCAL body — the side the operator
867
- // may want back. Reading the recorded path is the contract /resolve-conflicts
868
- // relies on, so assert through it rather than re-globbing the directory.
869
- expect(fs.readFileSync(path.join(tmpDir, entry.conflictPath), "utf-8")).toBe("local version");
870
- });
871
- it("interactive `skip` still leaves local in place — skip is not keep", async () => {
872
- // `skip` means "do not touch this one, I will deal with it later". It is a
873
- // separate arm from `keep` and must NOT adopt the cloud body; a strategy
874
- // that says "leave it alone" may not rename the operator's file.
875
- const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
876
- fs.mkdirSync(companyDocs, { recursive: true });
877
- fs.writeFileSync(path.join(companyDocs, "handoff.md"), "local version");
878
- vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
879
- { key: "docs/handoff.md", size: 42, lastModified: new Date(), etag: '"divergent"' },
880
- ]);
881
- fs.writeFileSync(journalPath, JSON.stringify({
882
- version: "1",
883
- lastSync: new Date().toISOString(),
884
- files: {
885
- "docs/handoff.md": {
886
- hash: "old-hash-from-last-sync",
887
- size: 20,
888
- syncedAt: new Date(Date.now() - 3600000).toISOString(),
889
- direction: "down",
890
- },
891
- },
892
- }));
893
- const resolveSpy = vi
894
- .spyOn(conflictModule, "resolveConflict")
895
- .mockResolvedValue("skip");
896
- try {
897
- await sync({
898
- company: "acme",
899
- vaultConfig: mockConfig,
900
- hqRoot: tmpDir,
901
- });
902
- }
903
- finally {
904
- resolveSpy.mockRestore();
905
- }
906
- expect(fs.readFileSync(path.join(companyDocs, "handoff.md"), "utf-8")).toBe("local version");
907
- });
908
526
  it("a genuine download clears a stale localDiverges flag (entry is now truly local==remote)", async () => {
909
527
  const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
910
528
  fs.mkdirSync(companyDocs, { recursive: true });
@@ -930,7 +548,7 @@ describe("sync", () => {
930
548
  ]);
931
549
  await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
932
550
  expect(fs.existsSync(path.join(companyDocs, "handoff.md"))).toBe(true);
933
- const journal = journalAt(journalPath);
551
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
934
552
  expect(journal.files["docs/handoff.md"]?.localDiverges).toBeFalsy();
935
553
  });
936
554
  it("leaves a downloaded unreadable link unjournaled without emitting a fatal error", async () => {
@@ -969,7 +587,7 @@ describe("sync", () => {
969
587
  samplePaths: [linkKey],
970
588
  });
971
589
  expect(events.some((event) => event.type === "error")).toBe(false);
972
- const journal = journalAt(journalPath);
590
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
973
591
  expect(journal.files[linkKey]).toBeUndefined();
974
592
  }
975
593
  finally {
@@ -1067,7 +685,7 @@ describe("sync", () => {
1067
685
  expect(litter).toEqual([]);
1068
686
  // Journal re-stamped off the stale baseline (hash now matches local,
1069
687
  // remoteEtag recorded) so the phantom conflict can't re-fire.
1070
- const journalAfter = journalAt(journalPath);
688
+ const journalAfter = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1071
689
  expect(journalAfter.files["docs/handoff.md"].hash).not.toBe("stale-hash");
1072
690
  expect(journalAfter.files["docs/handoff.md"].remoteEtag).toBeTruthy();
1073
691
  });
@@ -1125,7 +743,7 @@ describe("sync", () => {
1125
743
  });
1126
744
  expect(events.some((event) => event.type === "error")).toBe(false);
1127
745
  expect(fs.readFileSync(localPath, "utf-8")).toBe("local version");
1128
- const journal = journalAt(journalPath);
746
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1129
747
  expect(journal.files[linkKey].hash).toBe("stale-hash");
1130
748
  }
1131
749
  finally {
@@ -1164,10 +782,13 @@ describe("sync", () => {
1164
782
  });
1165
783
  expect(result.conflicts).toBe(1);
1166
784
  expect(result.conflictPaths).toEqual(["docs/handoff.md"]);
1167
- // Cloud body adopted at the original name on keep…
1168
- expect(fs.readFileSync(path.join(companyDocs, "handoff.md"), "utf-8")).toBe("mock file content");
1169
- // …and exactly one sidecar retained, holding the displaced local body.
1170
- expect(fs.readFileSync(sidecarIn(companyDocs), "utf-8")).toBe("local version");
785
+ // Local preserved on keep.
786
+ expect(fs.readFileSync(path.join(companyDocs, "handoff.md"), "utf-8")).toBe("local version");
787
+ // Mirror retained for inspection.
788
+ const litter = fs
789
+ .readdirSync(companyDocs)
790
+ .filter((f) => f.includes(".conflict-"));
791
+ expect(litter).toHaveLength(1);
1171
792
  });
1172
793
  it("cloud-authoritative file is PULL-WINS on conflict: overwrites local, no mirror, no false-stamp", async () => {
1173
794
  // company-brief.md is server-regenerated (gardener), so a divergence is not
@@ -1280,12 +901,10 @@ describe("sync", () => {
1280
901
  // Surfaced as a conflict, NOT a silent download.
1281
902
  expect(result.conflicts).toBe(1);
1282
903
  expect(result.conflictPaths).toEqual(["docs/handoff.md"]);
1283
- expect(result.filesDownloaded).toBe(1);
1284
- // The newer local bytes are not lost — the rollback is survivable because
1285
- // `keep` renames them aside rather than discarding them. The guard's job is
1286
- // to route this through conflict resolution instead of a silent download;
1287
- // which side ends up at the original name is the strategy's call.
1288
- expect(fs.readFileSync(sidecarIn(path.dirname(localPath)), "utf-8")).toBe(localContent);
904
+ expect(result.filesDownloaded).toBe(0);
905
+ // The newer local bytes are intact — nothing was rolled back.
906
+ expect(fs.readFileSync(localPath, "utf-8")).toBe(localContent);
907
+ // Remote preserved as an inspection mirror rather than overwriting local.
1289
908
  const litter = fs
1290
909
  .readdirSync(companyDocs)
1291
910
  .filter((f) => f.includes(".conflict-"));
@@ -1345,8 +964,8 @@ describe("sync", () => {
1345
964
  });
1346
965
  expect(result.conflicts).toBe(1);
1347
966
  expect(result.conflictPaths).toEqual(["docs/handoff.md"]);
1348
- expect(result.filesDownloaded).toBe(1);
1349
- expect(fs.readFileSync(sidecarIn(path.dirname(localPath)), "utf-8")).toBe(localEdit);
967
+ expect(result.filesDownloaded).toBe(0);
968
+ expect(fs.readFileSync(localPath, "utf-8")).toBe(localEdit);
1350
969
  });
1351
970
  it("anti-rollback guard is direction-gated: a SMALLER remote on a clean CONSUMER copy (direction=down) downloads, not conflict", async () => {
1352
971
  // The device only ever PULLED this file (direction: "down") — a consumer
@@ -1470,16 +1089,18 @@ describe("sync", () => {
1470
1089
  // Surfaced as a conflict, NOT a silent download.
1471
1090
  expect(result.conflicts).toBe(1);
1472
1091
  expect(result.conflictPaths).toEqual(["docs/handoff.md"]);
1473
- expect(result.filesDownloaded).toBe(1);
1474
- // The unjournaled local body is not lost — it is renamed aside, which is
1475
- // what makes adopting the cloud copy survivable for a file that had no
1476
- // sync history at all.
1477
- expect(fs.readFileSync(sidecarIn(path.dirname(localPath)), "utf-8")).toBe(localContent);
1478
- // The cloud body took the original name.
1479
- expect(fs.readFileSync(localPath, "utf-8")).toBe("mock file content");
1092
+ expect(result.filesDownloaded).toBe(0);
1093
+ // The local edit is intact.
1094
+ expect(fs.readFileSync(localPath, "utf-8")).toBe(localContent);
1095
+ // Cloud copy preserved alongside it for inspection / `/resolve-conflicts`.
1096
+ const litter = fs
1097
+ .readdirSync(companyDocs)
1098
+ .filter((f) => f.includes(".conflict-"));
1099
+ expect(litter).toHaveLength(1);
1100
+ expect(fs.readFileSync(path.join(companyDocs, litter[0]), "utf-8")).toBe("mock file content");
1480
1101
  // And a durable record exists so the overwrite can never be silent.
1481
1102
  const index = JSON.parse(fs.readFileSync(path.join(tmpDir, ".hq-conflicts", "index.json"), "utf-8"));
1482
- expect(index.conflicts.some((c) => c.originalPath === "companies/acme/docs/handoff.md" && c.preserved === "local")).toBe(true);
1103
+ expect(index.conflicts.some((c) => c.originalPath === "companies/acme/docs/handoff.md")).toBe(true);
1483
1104
  });
1484
1105
  it("untracked-local guard is free on first pull: an unjournaled local file that already MATCHES the cloud reconciles silently", async () => {
1485
1106
  // The guard must not manufacture conflicts for the common case — a fresh
@@ -1510,7 +1131,7 @@ describe("sync", () => {
1510
1131
  .filter((f) => f.includes(".conflict-"));
1511
1132
  expect(litter).toHaveLength(0);
1512
1133
  // Journal stamped, so the next pass is a plain unchanged-skip.
1513
- const journal = journalAt(journalPath);
1134
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1514
1135
  expect(journal.files["docs/handoff.md"]).toBeDefined();
1515
1136
  });
1516
1137
  it("RF-F02EXEC: refuses a download whose parent symlink appears after planning", async () => {
@@ -1670,7 +1291,7 @@ describe("sync", () => {
1670
1291
  });
1671
1292
  expect(fs.readFileSync(untrackedPath, "utf-8")).toBe("brand new local work");
1672
1293
  expect(fs.existsSync(trackedPath)).toBe(false);
1673
- const journal = journalAt(journalPath);
1294
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1674
1295
  expect(journal.files[untrackedKey]).toBeUndefined();
1675
1296
  expect(journal.files[trackedKey]).toBeUndefined();
1676
1297
  });
@@ -1743,7 +1364,7 @@ describe("sync", () => {
1743
1364
  await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
1744
1365
  expect(s3Module.downloadFile).toHaveBeenCalledWith(expect.anything(), key, expect.anything(), expect.anything());
1745
1366
  expect(fs.readFileSync(localPath, "utf-8")).toBe("mock file content");
1746
- const journal = journalAt(journalPath);
1367
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1747
1368
  expect(journal.files[key]?.removedAt).toBeUndefined();
1748
1369
  });
1749
1370
  it("does not canonicalize ignored remote-missing journal keys in the tombstone sweep", async () => {
@@ -1791,7 +1412,7 @@ describe("sync", () => {
1791
1412
  // Equivalence: an ignored key is never tombstoned — the local file and its
1792
1413
  // journal entry both survive, exactly as before the fix.
1793
1414
  expect(fs.existsSync(ignoredLocal)).toBe(true);
1794
- const sweptJournal = journalAt(journalPath);
1415
+ const sweptJournal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1795
1416
  expect(sweptJournal.files[ignoredKey]).toBeDefined();
1796
1417
  });
1797
1418
  it("tombstone lookup failure does not resurrect a current intent-backed delete", async () => {
@@ -1825,7 +1446,7 @@ describe("sync", () => {
1825
1446
  await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
1826
1447
  expect(s3Module.downloadFile).not.toHaveBeenCalledWith(expect.anything(), key, expect.anything(), expect.anything());
1827
1448
  expect(fs.existsSync(localPath)).toBe(false);
1828
- expect(journalAt(journalPath).files[key]
1449
+ expect(JSON.parse(fs.readFileSync(journalPath, "utf8")).files[key]
1829
1450
  ?.removedReason).toBe("local-delete");
1830
1451
  });
1831
1452
  it("pull producer: a BULK disappearance is drift-restored, NOT tombstoned (ridge-incident guard in the pull leg)", async () => {
@@ -1861,7 +1482,7 @@ describe("sync", () => {
1861
1482
  await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
1862
1483
  // All restored, none tombstoned.
1863
1484
  expect(s3Module.downloadFile).toHaveBeenCalledTimes(12);
1864
- const journal = journalAt(journalPath);
1485
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1865
1486
  for (const k of keys) {
1866
1487
  expect(journal.files[k]?.removedAt).toBeUndefined();
1867
1488
  }
@@ -1902,7 +1523,7 @@ describe("sync", () => {
1902
1523
  await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
1903
1524
  expect(s3Module.downloadFile).not.toHaveBeenCalledWith(expect.anything(), key, expect.anything());
1904
1525
  expect(fs.existsSync(localPath)).toBe(false);
1905
- const journal = journalAt(journalPath);
1526
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1906
1527
  // Entry PRESERVED as a tombstone, not removed.
1907
1528
  expect(journal.files[key]?.removedAt).toBeTruthy();
1908
1529
  expect(journal.files[key]?.removedReason).toBe("local-delete");
@@ -1934,7 +1555,7 @@ describe("sync", () => {
1934
1555
  it("stamps journal.lastSync on every successful run, even when nothing transferred", async () => {
1935
1556
  // First run downloads both remote files and stamps lastSync.
1936
1557
  await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
1937
- const firstStamp = journalAt(journalPath).lastSync;
1558
+ const firstStamp = JSON.parse(fs.readFileSync(journalPath, "utf-8")).lastSync;
1938
1559
  expect(firstStamp).not.toBe("");
1939
1560
  // Second run: same remote, same local — Stage-2 plan is all-skip, so
1940
1561
  // updateEntry never fires. lastSync must still advance, otherwise the
@@ -1942,7 +1563,7 @@ describe("sync", () => {
1942
1563
  await new Promise((r) => setTimeout(r, 5));
1943
1564
  const result = await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
1944
1565
  expect(result.filesDownloaded).toBe(0);
1945
- const secondStamp = journalAt(journalPath).lastSync;
1566
+ const secondStamp = JSON.parse(fs.readFileSync(journalPath, "utf-8")).lastSync;
1946
1567
  expect(new Date(secondStamp).getTime()).toBeGreaterThan(new Date(firstStamp).getTime());
1947
1568
  });
1948
1569
  it("journalSlug: 'personal' routes journal I/O to sync-journal.personal.json", async () => {
@@ -2033,9 +1654,13 @@ describe("sync", () => {
2033
1654
  expect(fs.existsSync(path.join(tmpDir, "companies", "foo", "bar.md"))).toBe(false);
2034
1655
  // The whole point: it now gets a journal baseline, so the push side stops
2035
1656
  // re-firing a transient conflict every sync (the bug this fix closes).
2036
- // Enumerate through the API: the per-slug JSON files are locators now, so
2037
- // reading them directly finds no `files` map at all.
2038
- const journaledManifest = listJournals().some((j) => j.journal.files?.["companies/manifest.yaml"] != null);
1657
+ const journaledManifest = fs
1658
+ .readdirSync(stateDir)
1659
+ .filter((f) => f.startsWith("sync-journal."))
1660
+ .some((f) => {
1661
+ const j = JSON.parse(fs.readFileSync(path.join(stateDir, f), "utf8"));
1662
+ return j.files?.["companies/manifest.yaml"] != null;
1663
+ });
2039
1664
  expect(journaledManifest).toBe(true);
2040
1665
  });
2041
1666
  it("personalMode pull lands the session-continuity pointer + active thread file under <hqRoot>/workspace/threads/ so a handoff resumes on machine B (DEV-1778)", async () => {
@@ -2129,10 +1754,9 @@ describe("sync", () => {
2129
1754
  });
2130
1755
  expect(result.conflicts).toBe(1);
2131
1756
  expect(result.conflictPaths).toEqual(["workspace/threads/handoff.json"]);
2132
- // B's newer local pointer is preserved verbatim — renamed aside rather than
2133
- // destroyed. DEV-1778 was about the pointer being LOST; a recoverable
2134
- // sidecar satisfies that, and the machine can still resume from it.
2135
- const kept = JSON.parse(fs.readFileSync(sidecarIn(threadsLocal), "utf-8"));
1757
+ expect(result.filesSkipped).toBeGreaterThanOrEqual(1);
1758
+ // B's newer local pointer is preserved verbatim — not clobbered by A's.
1759
+ const kept = JSON.parse(fs.readFileSync(path.join(threadsLocal, "handoff.json"), "utf-8"));
2136
1760
  expect(kept.message).toBe("newer local from B");
2137
1761
  expect(kept.thread_path).toBe("workspace/threads/T-machineB.json");
2138
1762
  });
@@ -2289,7 +1913,7 @@ describe("sync", () => {
2289
1913
  expect(result.filesTombstoned).toBeGreaterThanOrEqual(1);
2290
1914
  // Journal entry for the deleted path must be gone too — otherwise
2291
1915
  // the next sync would tombstone the same key on every run.
2292
- const journal = journalAt(journalPath);
1916
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2293
1917
  expect(journal.files["docs/deleted-by-peer.md"]).toBeUndefined();
2294
1918
  expect(journal.files["docs/kept.md"]).toBeDefined();
2295
1919
  });
@@ -2354,7 +1978,7 @@ describe("sync", () => {
2354
1978
  expect(result.filesTombstoned).toBe(0);
2355
1979
  // The journal is healed: the malformed key is gone, the canonical POSIX key
2356
1980
  // points at the live file (so a future pull stays converged, not poisoned).
2357
- const journal = journalAt(journalPath);
1981
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2358
1982
  expect(journal.files[backslashKey]).toBeUndefined();
2359
1983
  expect(journal.files[posixKey]).toBeDefined();
2360
1984
  expect(Object.keys(journal.files).some((k) => k.includes("\\"))).toBe(false);
@@ -2407,7 +2031,7 @@ describe("sync", () => {
2407
2031
  expect(result.filesTombstoned).toBe(0);
2408
2032
  // Journal entry retained — next sync (push) will re-evaluate and
2409
2033
  // either re-upload the divergent local or surface as a conflict.
2410
- const journal = journalAt(journalPath);
2034
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2411
2035
  expect(journal.files["docs/edited-locally.md"]).toBeDefined();
2412
2036
  expect(journal.files["docs/edited-locally.md"].hash).toBe(baselineHash);
2413
2037
  });
@@ -2451,7 +2075,7 @@ describe("sync", () => {
2451
2075
  expect(result.filesTombstoned).toBe(0);
2452
2076
  expect(fs.existsSync(racedPath)).toBe(true);
2453
2077
  expect(fs.readFileSync(racedPath, "utf-8")).toBe("concurrent local edit");
2454
- const journal = journalAt(journalPath);
2078
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2455
2079
  expect(journal.files["docs/racy-delete.md"]).toBeDefined();
2456
2080
  expect(journal.files["docs/racy-delete.md"].hash).toBe(baselineHash);
2457
2081
  });
@@ -2488,7 +2112,7 @@ describe("sync", () => {
2488
2112
  expect(result.filesTombstoned).toBe(0);
2489
2113
  expect(result.conflicts).toBe(1);
2490
2114
  expect(fs.readFileSync(localPath, "utf-8")).toBe(baseline);
2491
- const journal = journalAt(journalPath);
2115
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2492
2116
  expect(journal.files["docs/created-after-plan.md"]).toBeDefined();
2493
2117
  });
2494
2118
  it("CAS: preserves a concurrent local edit when a staged download is ready to replace it", async () => {
@@ -2572,7 +2196,7 @@ describe("sync", () => {
2572
2196
  propagateDeletePolicy: "all",
2573
2197
  });
2574
2198
  expect(s3Module.deleteRemoteFile).not.toHaveBeenCalled();
2575
- const journal = journalAt(personalJournalPath);
2199
+ const journal = JSON.parse(fs.readFileSync(personalJournalPath, "utf-8"));
2576
2200
  expect(journal.files["core/core.yaml"]).toBeDefined();
2577
2201
  expect(journal.files[".claude/skills/core:demo/SKILL.md"]).toBeDefined();
2578
2202
  });
@@ -2606,7 +2230,7 @@ describe("sync", () => {
2606
2230
  });
2607
2231
  expect(s3Module.deleteRemoteFile).not.toHaveBeenCalled();
2608
2232
  expect(result.filesRefusedStale).toBe(2);
2609
- const journal = journalAt(journalPath);
2233
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2610
2234
  expect(journal.files["docs/a.md"]).toBeDefined();
2611
2235
  expect(journal.files["docs/b.md"]).toBeDefined();
2612
2236
  });
@@ -2634,16 +2258,10 @@ describe("sync", () => {
2634
2258
  },
2635
2259
  pulls: [],
2636
2260
  }));
2637
- vi.mocked(s3Module.headRemoteFile)
2638
- .mockResolvedValueOnce({
2261
+ vi.mocked(s3Module.headRemoteFile).mockResolvedValueOnce({
2639
2262
  etag: "old-etag",
2640
2263
  size: baseline.length,
2641
2264
  lastModified: new Date(),
2642
- })
2643
- .mockResolvedValueOnce({
2644
- etag: "peer-etag",
2645
- size: "mock file content".length,
2646
- lastModified: new Date(),
2647
2265
  });
2648
2266
  const preconditionError = Object.assign(new Error("remote changed"), {
2649
2267
  name: "PreconditionFailed",
@@ -2659,15 +2277,7 @@ describe("sync", () => {
2659
2277
  onConflict: "keep",
2660
2278
  });
2661
2279
  expect(result.conflictPaths).toContain("board.json");
2662
- // The conditional PUT still fails closed, but keep is cloud-wins: the
2663
- // peer body takes the working name and the local append remains in the
2664
- // sidecar for explicit resolution.
2665
- expect(fs.readFileSync(boardPath, "utf-8")).toBe("mock file content");
2666
- expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe(localAppend);
2667
- expect(journalAt(journalPath).files["board.json"]).toMatchObject({
2668
- remoteEtag: "peer-etag",
2669
- direction: "down",
2670
- });
2280
+ expect(fs.readFileSync(boardPath, "utf-8")).toBe(localAppend);
2671
2281
  expect(s3Module.uploadFile).toHaveBeenCalledWith(expect.anything(), boardPath, "board.json", undefined, { ifMatch: "old-etag" });
2672
2282
  });
2673
2283
  it("does NOT tombstone symlinks whose readlink target has diverged from the journal (Codex P1 round 4)", async () => {
@@ -2712,7 +2322,7 @@ describe("sync", () => {
2712
2322
  expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true);
2713
2323
  expect(fs.readlinkSync(linkPath)).toBe("new-target.md");
2714
2324
  expect(result.filesTombstoned).toBe(0);
2715
- const journal = journalAt(journalPath);
2325
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2716
2326
  expect(journal.files["docs/link.md"]).toBeDefined();
2717
2327
  expect(journal.files["docs/link.md"].hash).toBe(baselineHash);
2718
2328
  });
@@ -2779,7 +2389,7 @@ describe("sync", () => {
2779
2389
  // The really-deleted file MUST be tombstoned.
2780
2390
  expect(fs.existsSync(reallyDeletedPath)).toBe(false);
2781
2391
  expect(result.filesTombstoned).toBe(1);
2782
- const journal = journalAt(journalPath);
2392
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2783
2393
  // Journal entry retained for the invisible key (so next sync at
2784
2394
  // broader scope can re-evaluate).
2785
2395
  expect(journal.files["docs/invisible-to-list.md"]).toBeDefined();
@@ -2853,7 +2463,7 @@ describe("sync", () => {
2853
2463
  expect(result.filesDownloaded).toBe(0);
2854
2464
  expect(result.filesTombstoned).toBeGreaterThanOrEqual(1);
2855
2465
  // Journal entry dropped so it converges (no re-tombstone next run).
2856
- const journal = journalAt(journalPath);
2466
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2857
2467
  expect(journal.files["docs/shared.md"]).toBeUndefined();
2858
2468
  });
2859
2469
  it("delete-resync: a re-created object NEWER than the tombstone DOES sync (tombstone is not permanent)", async () => {
@@ -2945,7 +2555,7 @@ describe("sync", () => {
2945
2555
  expect(result.conflicts).toBeGreaterThanOrEqual(1);
2946
2556
  expect(result.conflictPaths).toContain("docs/edited.md");
2947
2557
  expect(fs.existsSync(editedPath)).toBe(true);
2948
- expect(fs.readFileSync(sidecarIn(path.dirname(editedPath)), "utf-8")).toBe("my local edits");
2558
+ expect(fs.readFileSync(editedPath, "utf-8")).toBe("my local edits");
2949
2559
  expect(result.filesTombstoned).toBe(0);
2950
2560
  });
2951
2561
  it("delete-resync: suppression is per-key and the fetch is company-scoped (no cross-tenant bleed)", async () => {
@@ -3017,7 +2627,7 @@ describe("sync", () => {
3017
2627
  });
3018
2628
  expect(fs.existsSync(deniedPath)).toBe(true);
3019
2629
  expect(result.filesTombstoned).toBe(0);
3020
- const journal = journalAt(journalPath);
2630
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3021
2631
  expect(journal.files["docs/out-of-scope.md"]).toBeDefined();
3022
2632
  });
3023
2633
  it("retains the journal entry when tombstone unlink fails with EACCES (Codex P1 — Bug #9 follow-up)", async () => {
@@ -3065,7 +2675,7 @@ describe("sync", () => {
3065
2675
  // Tombstone NOT counted — the delete wasn't honored.
3066
2676
  expect(result.filesTombstoned).toBe(0);
3067
2677
  // Journal entry MUST be retained so the next sync retries.
3068
- const journal = journalAt(journalPath);
2678
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3069
2679
  expect(journal.files["docs/locked-by-peer.md"]).toBeDefined();
3070
2680
  expect(journal.files["docs/locked-by-peer.md"].hash).toBe("locked-hash");
3071
2681
  }
@@ -3292,7 +2902,7 @@ describe("sync", () => {
3292
2902
  expect(s3Module.downloadFile).not.toHaveBeenCalled();
3293
2903
  expect(fs.existsSync(escapedPath)).toBe(false);
3294
2904
  expect(fs.existsSync(path.join(companyRoot, traversalKey))).toBe(false);
3295
- const journal = journalAt(journalPath);
2905
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3296
2906
  expect(journal.files[traversalKey]).toBeUndefined();
3297
2907
  }
3298
2908
  finally {
@@ -3326,7 +2936,7 @@ describe("sync", () => {
3326
2936
  expect(s3Module.downloadFile).not.toHaveBeenCalled();
3327
2937
  expect(fs.existsSync(escapedPath)).toBe(false);
3328
2938
  expect(fs.existsSync(path.join(companyRoot, remoteKey))).toBe(false);
3329
- const journal = journalAt(journalPath);
2939
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3330
2940
  expect(journal.files[remoteKey]).toBeUndefined();
3331
2941
  }
3332
2942
  finally {
@@ -3401,46 +3011,10 @@ describe("sync", () => {
3401
3011
  vaultConfig: mockConfig,
3402
3012
  hqRoot: tmpDir,
3403
3013
  });
3404
- const journal = journalAt(journalPath);
3014
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3405
3015
  expect(journal.files["docs/handoff.md"].remoteEtag).toBe("abc123");
3406
3016
  expect(journal.files["knowledge/readme.md"].remoteEtag).toBe("def456");
3407
3017
  });
3408
- it("persists each downloaded hash before starting the next file", async () => {
3409
- process.env.HQ_SYNC_TRANSFER_CONCURRENCY = "1";
3410
- const remoteFiles = [
3411
- { key: "ordered/first.md", size: 18, lastModified: new Date(), etag: '"first-etag"' },
3412
- { key: "ordered/second.md", size: 18, lastModified: new Date(), etag: '"second-etag"' },
3413
- ];
3414
- const defaultDownload = vi.mocked(s3Module.downloadFile).getMockImplementation();
3415
- if (!defaultDownload)
3416
- throw new Error("missing default download mock");
3417
- try {
3418
- vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce(remoteFiles);
3419
- vi.mocked(s3Module.downloadFile).mockImplementation(async (ctx, key, localPath, options) => {
3420
- if (key === "ordered/second.md") {
3421
- const first = journalAt(journalPath).files["ordered/first.md"];
3422
- expect(first).toMatchObject({
3423
- hash: crypto.createHash("sha256").update("mock file content").digest("hex"),
3424
- remoteEtag: "first-etag",
3425
- direction: "down",
3426
- });
3427
- expect(first.localDiverges).toBeFalsy();
3428
- }
3429
- return defaultDownload(ctx, key, localPath, options);
3430
- });
3431
- await sync({
3432
- company: "acme",
3433
- vaultConfig: mockConfig,
3434
- hqRoot: tmpDir,
3435
- skipReindex: true,
3436
- onEvent: () => undefined,
3437
- });
3438
- }
3439
- finally {
3440
- vi.mocked(s3Module.downloadFile).mockImplementation(defaultDownload);
3441
- delete process.env.HQ_SYNC_TRANSFER_CONCURRENCY;
3442
- }
3443
- });
3444
3018
  it("checkpoints completed downloads so a failed bulk pass resumes only its tail", async () => {
3445
3019
  process.env.HQ_SYNC_TRANSFER_CONCURRENCY = "1";
3446
3020
  const remoteFiles = Array.from({ length: 9 }, (_, index) => ({
@@ -3458,7 +3032,7 @@ describe("sync", () => {
3458
3032
  if (key === "bulk/file-8.md") {
3459
3033
  // This models the next transfer crashing the process. The first
3460
3034
  // eight entries must already be durable before the final writer.
3461
- const checkpoint = journalAt(journalPath);
3035
+ const checkpoint = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3462
3036
  expect(Object.keys(checkpoint.files)).toHaveLength(8);
3463
3037
  throw new Error("simulated crash after checkpoint");
3464
3038
  }
@@ -3472,7 +3046,7 @@ describe("sync", () => {
3472
3046
  onEvent: () => undefined,
3473
3047
  });
3474
3048
  expect(first.filesDownloaded).toBe(8);
3475
- const checkpoint = journalAt(journalPath);
3049
+ const checkpoint = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3476
3050
  expect(Object.keys(checkpoint.files)).toHaveLength(8);
3477
3051
  vi.mocked(s3Module.downloadFile).mockImplementation(defaultDownload);
3478
3052
  vi.mocked(s3Module.downloadFile).mockClear();
@@ -3923,7 +3497,7 @@ describe("sync", () => {
3923
3497
  // skip-unchanged works on the next tick.
3924
3498
  const journalMod = await import("../journal.js");
3925
3499
  const expectedHash = journalMod.hashSymlinkTarget(linkTarget);
3926
- const journal = journalAt(journalPath);
3500
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3927
3501
  expect(journal.files[linkKey]).toMatchObject({
3928
3502
  hash: expectedHash,
3929
3503
  size: 0,
@@ -4255,7 +3829,7 @@ describe("sync", () => {
4255
3829
  expect(errOutput).toContain("manual");
4256
3830
  expect(errOutput).not.toContain("directory-overlay marker");
4257
3831
  });
4258
- it("shared-mode leg withholds out-of-scope keys at LIST time (scoped listing supersedes the feedback_d2082110 gap advisory)", async () => {
3832
+ it("shared-mode leg surfaces the materialization gap and names the shared→all lever (feedback_d2082110)", async () => {
4259
3833
  // Reporter feedback_d2082110 (cody@jonesroadbeauty.com): four memberships
4260
3834
  // were configured as `shared` rather than `all`, so not all vault content
4261
3835
  // materialized on the second device — a SILENT gap the reporter had to
@@ -4288,19 +3862,16 @@ describe("sync", () => {
4288
3862
  onEvent: (e) => events.push(e),
4289
3863
  });
4290
3864
  // The in-scope key pulled; the out-of-scope key was NOT materialized.
4291
- // BEHAVIOR CHANGE (targeted-sync scoped listing): the shared leg lists
4292
- // ONLY its in-scope prefixes now, so the out-of-scope key never reaches
4293
- // the plan — filesOutOfScope is 0 and the scope-materialization-gap
4294
- // advisory (which counted plan-visible withheld keys) has nothing to
4295
- // report. The advisory's classification logic remains for full listings;
4296
- // the memory win of not materializing the whole vault LIST supersedes the
4297
- // withheld-count telemetry on scoped legs.
4298
3865
  expect(result.filesDownloaded).toBe(1);
4299
- expect(result.filesOutOfScope).toBe(0);
3866
+ expect(result.filesOutOfScope).toBe(1);
4300
3867
  expect(fs.existsSync(path.join(tmpDir, "companies", "acme", "knowledge", "present.md"))).toBe(true);
4301
3868
  expect(fs.existsSync(path.join(tmpDir, "companies", "acme", "projects", "secret.md"))).toBe(false);
3869
+ // The gap is now SURFACED with the count, the active mode, and the lever.
4302
3870
  const gap = events.find((e) => e.type === "scope-materialization-gap");
4303
- expect(gap).toBeUndefined();
3871
+ expect(gap).toBeDefined();
3872
+ expect(gap.count).toBe(1);
3873
+ expect(gap.syncMode).toBe("shared");
3874
+ expect(gap.samplePaths).toEqual(["projects/secret.md"]);
4304
3875
  });
4305
3876
  it("all-mode leg (default) never emits a materialization gap — no scoping, no noise", async () => {
4306
3877
  // Boundary for the feedback_d2082110 gap surface: in `all` mode every key is
@@ -4573,7 +4144,7 @@ describe("sync", () => {
4573
4144
  // The journal MUST be updated to reflect the user's keep
4574
4145
  // decision. Pre-fix: catch swallowed the ENOENT, journal stayed
4575
4146
  // at oldHash + old etag, every subsequent sync re-fired.
4576
- const journal = journalAt(journalPath);
4147
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4577
4148
  const entry = journal.files[linkKey];
4578
4149
  expect(entry.hash).toBe(expectedNewHash);
4579
4150
  expect(entry.size).toBe(0);
@@ -4872,7 +4443,7 @@ describe("scope-invalid key hardening (companies/ prefix in a company vault —
4872
4443
  // Cleaned, not re-uploaded: local doubled-tree copy gone, journal dropped.
4873
4444
  expect(fs.existsSync(path.join(doubled, "poison.md"))).toBe(false);
4874
4445
  expect(result.filesTombstoned).toBeGreaterThanOrEqual(1);
4875
- const journal = journalAt(journalPath);
4446
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4876
4447
  expect(journal.files["companies/acme/knowledge/poison.md"]).toBeUndefined();
4877
4448
  // No HEAD verify against the invalid key — the server validator would
4878
4449
  // reject it (INVALID_KEY_COMPANIES_SCOPED) and defer the cleanup forever.
@@ -5000,7 +4571,7 @@ describe("junk key-spelling guard (US-002 mixed-version amplifier)", () => {
5000
4571
  expect(events.filter((e) => e.type === "error")).toHaveLength(0);
5001
4572
  expect(result.aborted).toBe(false);
5002
4573
  expect(result.filesSkipped).toBe(1);
5003
- const journalAfter = journalAt(journalPath);
4574
+ const journalAfter = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5004
4575
  expect(Object.keys(journalAfter.files)).not.toContain(junkKey);
5005
4576
  });
5006
4577
  it("still pulls a junk-spelling key that is ALREADY journaled under that exact spelling", async () => {
@@ -5045,7 +4616,7 @@ describe("junk key-spelling guard (US-002 mixed-version amplifier)", () => {
5045
4616
  // Junk twin still gated; the canonical journal entry tombstones out.
5046
4617
  expect(events.filter((e) => e.type === "skip-junk-key-spelling")).toHaveLength(1);
5047
4618
  expect(result.filesTombstoned).toBe(1);
5048
- const journalAfter = journalAt(journalPath);
4619
+ const journalAfter = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5049
4620
  expect(Object.keys(journalAfter.files)).not.toContain(canonicalKey);
5050
4621
  });
5051
4622
  it("always downloads a CANONICAL remote key even when the journal holds only its junk twin (post-doctor convergence)", async () => {
@@ -5062,7 +4633,7 @@ describe("junk key-spelling guard (US-002 mixed-version amplifier)", () => {
5062
4633
  expect(vi.mocked(s3Module.downloadFile)).toHaveBeenCalledTimes(1);
5063
4634
  expect(vi.mocked(s3Module.downloadFile).mock.calls[0][1]).toBe(canonicalKey);
5064
4635
  expect(result.filesDownloaded).toBe(1);
5065
- const journalAfter = journalAt(journalPath);
4636
+ const journalAfter = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5066
4637
  expect(Object.keys(journalAfter.files)).toContain(canonicalKey);
5067
4638
  });
5068
4639
  it("does not tombstone a journaled canonical key when the LIST holds only its junk twin", async () => {
@@ -5078,7 +4649,7 @@ describe("junk key-spelling guard (US-002 mixed-version amplifier)", () => {
5078
4649
  const { events, result } = await runSync();
5079
4650
  // Junk twin skipped, canonical entry NOT tombstoned.
5080
4651
  expect(events.filter((e) => e.type === "skip-junk-key-spelling")).toHaveLength(1);
5081
- const journalAfter = journalAt(journalPath);
4652
+ const journalAfter = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5082
4653
  expect(Object.keys(journalAfter.files)).toContain(canonicalKey);
5083
4654
  expect(result.filesTombstoned ?? 0).toBe(0);
5084
4655
  expect(events.filter((e) => e.type === "error")).toHaveLength(0);