@indigoai-us/hq-cloud 6.14.17 → 6.14.19

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 (124) hide show
  1. package/.github/workflows/ci.yml +35 -0
  2. package/dist/active-company.d.ts +43 -0
  3. package/dist/active-company.d.ts.map +1 -0
  4. package/dist/active-company.js +132 -0
  5. package/dist/active-company.js.map +1 -0
  6. package/dist/active-company.test.d.ts +2 -0
  7. package/dist/active-company.test.d.ts.map +1 -0
  8. package/dist/active-company.test.js +149 -0
  9. package/dist/active-company.test.js.map +1 -0
  10. package/dist/bin/sync-runner-company.d.ts +7 -0
  11. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  12. package/dist/bin/sync-runner-company.js +21 -1
  13. package/dist/bin/sync-runner-company.js.map +1 -1
  14. package/dist/bin/sync-runner-planning.d.ts +27 -1
  15. package/dist/bin/sync-runner-planning.d.ts.map +1 -1
  16. package/dist/bin/sync-runner-planning.js +33 -4
  17. package/dist/bin/sync-runner-planning.js.map +1 -1
  18. package/dist/bin/sync-runner-planning.test.d.ts +2 -0
  19. package/dist/bin/sync-runner-planning.test.d.ts.map +1 -0
  20. package/dist/bin/sync-runner-planning.test.js +115 -0
  21. package/dist/bin/sync-runner-planning.test.js.map +1 -0
  22. package/dist/bin/sync-runner-watch-loop.d.ts +10 -2
  23. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  24. package/dist/bin/sync-runner-watch-loop.js +201 -25
  25. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  26. package/dist/bin/sync-runner-watch-routes.d.ts +1 -1
  27. package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
  28. package/dist/bin/sync-runner-watch-routes.js +14 -2
  29. package/dist/bin/sync-runner-watch-routes.js.map +1 -1
  30. package/dist/bin/sync-runner.d.ts +39 -9
  31. package/dist/bin/sync-runner.d.ts.map +1 -1
  32. package/dist/bin/sync-runner.js +114 -20
  33. package/dist/bin/sync-runner.js.map +1 -1
  34. package/dist/bin/sync-runner.test.js +852 -8
  35. package/dist/bin/sync-runner.test.js.map +1 -1
  36. package/dist/cli/rescue-core.js +27 -1
  37. package/dist/cli/rescue-core.js.map +1 -1
  38. package/dist/cli/rescue-drop-dir-symlink.test.d.ts +2 -0
  39. package/dist/cli/rescue-drop-dir-symlink.test.d.ts.map +1 -0
  40. package/dist/cli/rescue-drop-dir-symlink.test.js +206 -0
  41. package/dist/cli/rescue-drop-dir-symlink.test.js.map +1 -0
  42. package/dist/cli/share.d.ts +15 -0
  43. package/dist/cli/share.d.ts.map +1 -1
  44. package/dist/cli/share.js +100 -55
  45. package/dist/cli/share.js.map +1 -1
  46. package/dist/cli/share.test.js +119 -6
  47. package/dist/cli/share.test.js.map +1 -1
  48. package/dist/cli/sync.d.ts.map +1 -1
  49. package/dist/cli/sync.js +19 -25
  50. package/dist/cli/sync.js.map +1 -1
  51. package/dist/cli/sync.test.js +121 -0
  52. package/dist/cli/sync.test.js.map +1 -1
  53. package/dist/journal.d.ts +3 -1
  54. package/dist/journal.d.ts.map +1 -1
  55. package/dist/journal.js +13 -2
  56. package/dist/journal.js.map +1 -1
  57. package/dist/journal.test.js +26 -1
  58. package/dist/journal.test.js.map +1 -1
  59. package/dist/manifest-reconcile.d.ts +118 -5
  60. package/dist/manifest-reconcile.d.ts.map +1 -1
  61. package/dist/manifest-reconcile.js +319 -62
  62. package/dist/manifest-reconcile.js.map +1 -1
  63. package/dist/manifest-reconcile.test.js +824 -2
  64. package/dist/manifest-reconcile.test.js.map +1 -1
  65. package/dist/personal-vault.d.ts +12 -0
  66. package/dist/personal-vault.d.ts.map +1 -1
  67. package/dist/personal-vault.js +20 -0
  68. package/dist/personal-vault.js.map +1 -1
  69. package/dist/qmd-reindex.d.ts +88 -11
  70. package/dist/qmd-reindex.d.ts.map +1 -1
  71. package/dist/qmd-reindex.js +354 -65
  72. package/dist/qmd-reindex.js.map +1 -1
  73. package/dist/qmd-reindex.test.d.ts +4 -0
  74. package/dist/qmd-reindex.test.d.ts.map +1 -1
  75. package/dist/qmd-reindex.test.js +285 -3
  76. package/dist/qmd-reindex.test.js.map +1 -1
  77. package/dist/s3.d.ts +13 -0
  78. package/dist/s3.d.ts.map +1 -1
  79. package/dist/s3.js +34 -1
  80. package/dist/s3.js.map +1 -1
  81. package/dist/s3.test.js +60 -1
  82. package/dist/s3.test.js.map +1 -1
  83. package/dist/telemetry.d.ts +22 -0
  84. package/dist/telemetry.d.ts.map +1 -1
  85. package/dist/telemetry.js +79 -0
  86. package/dist/telemetry.js.map +1 -1
  87. package/dist/telemetry.test.js +117 -1
  88. package/dist/telemetry.test.js.map +1 -1
  89. package/dist/watcher.d.ts +26 -1
  90. package/dist/watcher.d.ts.map +1 -1
  91. package/dist/watcher.js +75 -12
  92. package/dist/watcher.js.map +1 -1
  93. package/package.json +1 -1
  94. package/pnpm-workspace.yaml +1 -1
  95. package/src/active-company.test.ts +188 -0
  96. package/src/active-company.ts +168 -0
  97. package/src/bin/sync-runner-company.ts +29 -1
  98. package/src/bin/sync-runner-planning.test.ts +131 -0
  99. package/src/bin/sync-runner-planning.ts +60 -7
  100. package/src/bin/sync-runner-watch-loop.ts +322 -32
  101. package/src/bin/sync-runner-watch-routes.ts +14 -1
  102. package/src/bin/sync-runner.test.ts +1021 -16
  103. package/src/bin/sync-runner.ts +179 -28
  104. package/src/cli/rescue-core.ts +26 -1
  105. package/src/cli/rescue-drop-dir-symlink.test.ts +224 -0
  106. package/src/cli/share.test.ts +150 -6
  107. package/src/cli/share.ts +143 -48
  108. package/src/cli/sync.test.ts +150 -1
  109. package/src/cli/sync.ts +25 -29
  110. package/src/journal.test.ts +45 -0
  111. package/src/journal.ts +18 -1
  112. package/src/manifest-reconcile.test.ts +1019 -3
  113. package/src/manifest-reconcile.ts +424 -66
  114. package/src/personal-vault.ts +23 -0
  115. package/src/qmd-reindex.test.ts +324 -4
  116. package/src/qmd-reindex.ts +414 -76
  117. package/src/s3.test.ts +79 -0
  118. package/src/s3.ts +48 -1
  119. package/src/telemetry.test.ts +128 -0
  120. package/src/telemetry.ts +80 -0
  121. package/src/watcher.ts +111 -14
  122. package/test/e2e/watcher-real-chokidar.test.ts +62 -2
  123. package/test/e2e/watcher-recursive-backend.test.ts +67 -1
  124. package/test/joiner-manifest-reconcile.integration.test.ts +283 -0
@@ -84,7 +84,10 @@ const mockVendResponse = {
84
84
  };
85
85
 
86
86
  function setupFetchMock(
87
- opts: { tombstones?: Array<{ key: string; deletedAt: string }> } = {},
87
+ opts: {
88
+ tombstones?: Array<{ key: string; deletedAt: string }>;
89
+ tombstoneFailure?: boolean;
90
+ } = {},
88
91
  ) {
89
92
  const fetchMock = vi.fn().mockImplementation(async (url: string) => {
90
93
  const urlStr = String(url);
@@ -92,6 +95,7 @@ function setupFetchMock(
92
95
  // every legacy test exercises the no-suppression path; tests that need
93
96
  // suppression pass `{ tombstones }`.
94
97
  if (urlStr.includes("/v1/files/tombstones")) {
98
+ if (opts.tombstoneFailure) throw new Error("tombstone lookup unavailable");
95
99
  return {
96
100
  ok: true,
97
101
  status: 200,
@@ -1085,6 +1089,53 @@ describe("sync", () => {
1085
1089
  expect(journal.files[key]?.removedAt).toBeUndefined();
1086
1090
  });
1087
1091
 
1092
+ it("tombstone lookup failure does not resurrect a current intent-backed delete", async () => {
1093
+ const key = "docs/watcher-deleted.md";
1094
+ const localPath = path.join(tmpDir, "companies", "acme", key);
1095
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
1096
+ { key, size: 42, lastModified: new Date(), etag: '"cur123"' },
1097
+ ]);
1098
+ setupFetchMock({ tombstoneFailure: true });
1099
+ fs.writeFileSync(
1100
+ journalPath,
1101
+ JSON.stringify({
1102
+ version: "2",
1103
+ lastSync: new Date().toISOString(),
1104
+ files: {
1105
+ [key]: {
1106
+ hash: "synced-hash",
1107
+ size: 42,
1108
+ syncedAt: new Date().toISOString(),
1109
+ direction: "down",
1110
+ kind: "file",
1111
+ remoteEtag: "cur123",
1112
+ localDeleteIntent: {
1113
+ version: 1,
1114
+ remoteEtag: "cur123",
1115
+ localHash: "synced-hash",
1116
+ localKind: "file",
1117
+ },
1118
+ },
1119
+ },
1120
+ pulls: [],
1121
+ }),
1122
+ );
1123
+
1124
+ await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
1125
+
1126
+ expect(s3Module.downloadFile).not.toHaveBeenCalledWith(
1127
+ expect.anything(),
1128
+ key,
1129
+ expect.anything(),
1130
+ expect.anything(),
1131
+ );
1132
+ expect(fs.existsSync(localPath)).toBe(false);
1133
+ expect(
1134
+ JSON.parse(fs.readFileSync(journalPath, "utf8")).files[key]
1135
+ ?.removedReason,
1136
+ ).toBe("local-delete");
1137
+ });
1138
+
1088
1139
  it("pull producer: a BULK disappearance is drift-restored, NOT tombstoned (ridge-incident guard in the pull leg)", async () => {
1089
1140
  // 12 clean files vanish at once (corrupt mirror / bulk rm / checkout). The
1090
1141
  // bulk-asymmetry guard refuses to treat this as intent — every file is
@@ -3480,6 +3531,104 @@ describe("sync", () => {
3480
3531
  });
3481
3532
  });
3482
3533
 
3534
+ it("converges after reindex prunes a downloaded generated core mirror (DEV-1974)", async () => {
3535
+ // Legacy personal-vault objects may be generated links below core/<type>.
3536
+ // A pull materializes the link, then reindex removes it because its
3537
+ // canonical source is elsewhere. The second pull must recognize the
3538
+ // journaled symlink as generated state rather than downloading it again.
3539
+ const linkKey = "core/policies/legacy-mirror.md";
3540
+ const linkPath = path.join(tmpDir, linkKey);
3541
+ const linkTarget = "../../personal/policies/legacy-mirror.md";
3542
+ const remote = {
3543
+ key: linkKey,
3544
+ size: 48,
3545
+ lastModified: new Date(),
3546
+ etag: '"legacy-mirror"',
3547
+ };
3548
+
3549
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValue([remote]);
3550
+ vi.mocked(s3Module.downloadFile).mockImplementation(
3551
+ async (_ctx: unknown, _key: string, localPath: string) => {
3552
+ fs.mkdirSync(path.dirname(localPath), { recursive: true });
3553
+ fs.symlinkSync(linkTarget, localPath);
3554
+ return { metadata: {} };
3555
+ },
3556
+ );
3557
+
3558
+ const first = await sync({
3559
+ company: "acme",
3560
+ vaultConfig: mockConfig,
3561
+ hqRoot: tmpDir,
3562
+ personalMode: true,
3563
+ });
3564
+ expect(first.filesDownloaded).toBe(1);
3565
+ expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true);
3566
+
3567
+ // The real post-sync reindex removes this retired mirror. The unit test
3568
+ // mocks reindex globally, so make that lifecycle transition explicit.
3569
+ fs.unlinkSync(linkPath);
3570
+
3571
+ const second = await sync({
3572
+ company: "acme",
3573
+ vaultConfig: mockConfig,
3574
+ hqRoot: tmpDir,
3575
+ personalMode: true,
3576
+ });
3577
+
3578
+ expect(second.filesDownloaded).toBe(0);
3579
+ expect(vi.mocked(s3Module.downloadFile)).toHaveBeenCalledTimes(1);
3580
+ });
3581
+
3582
+ it("pulls a changed canonical object after a generated core mirror journal entry", async () => {
3583
+ const key = "core/policies/legacy-mirror.md";
3584
+ const localPath = path.join(tmpDir, key);
3585
+ const legacyMirror = {
3586
+ key,
3587
+ size: 48,
3588
+ lastModified: new Date(),
3589
+ etag: '"legacy-mirror"',
3590
+ };
3591
+ const canonicalReplacement = {
3592
+ ...legacyMirror,
3593
+ lastModified: new Date(Date.now() + 1_000),
3594
+ etag: '"canonical-replacement"',
3595
+ };
3596
+
3597
+ vi.mocked(s3Module.listRemoteFiles)
3598
+ .mockResolvedValueOnce([legacyMirror])
3599
+ .mockResolvedValueOnce([canonicalReplacement]);
3600
+ vi.mocked(s3Module.downloadFile)
3601
+ .mockImplementationOnce(async (_ctx: unknown, _key: string, destination: string) => {
3602
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
3603
+ fs.symlinkSync("../../personal/policies/legacy-mirror.md", destination);
3604
+ return { metadata: {} };
3605
+ })
3606
+ .mockImplementationOnce(async (_ctx: unknown, _key: string, destination: string) => {
3607
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
3608
+ fs.writeFileSync(destination, "canonical replacement\n");
3609
+ return { metadata: {} };
3610
+ });
3611
+
3612
+ await sync({
3613
+ company: "acme",
3614
+ vaultConfig: mockConfig,
3615
+ hqRoot: tmpDir,
3616
+ personalMode: true,
3617
+ });
3618
+ fs.unlinkSync(localPath);
3619
+
3620
+ const second = await sync({
3621
+ company: "acme",
3622
+ vaultConfig: mockConfig,
3623
+ hqRoot: tmpDir,
3624
+ personalMode: true,
3625
+ });
3626
+
3627
+ expect(second.filesDownloaded).toBe(1);
3628
+ expect(fs.readFileSync(localPath, "utf8")).toBe("canonical replacement\n");
3629
+ expect(vi.mocked(s3Module.downloadFile)).toHaveBeenCalledTimes(2);
3630
+ });
3631
+
3483
3632
  it("does not crash when local is a real directory at a remote-key path (EISDIR guard)", async () => {
3484
3633
  // Codex round-9 P2 follow-up: pre-fix, the pull planner's
3485
3634
  // post-lstat branch always called hashFile (readFileSync) on a
package/src/cli/sync.ts CHANGED
@@ -48,7 +48,10 @@ import {
48
48
  PERSONAL_VAULT_JOURNAL_SLUG,
49
49
  migratePersonalVaultJournal,
50
50
  } from "../journal.js";
51
- import { PERSONAL_VAULT_MANIFEST_KEY } from "../personal-vault.js";
51
+ import {
52
+ isGeneratedCoreMirrorKey,
53
+ PERSONAL_VAULT_MANIFEST_KEY,
54
+ } from "../personal-vault.js";
52
55
  import { isPersonalVaultExcluded } from "../personal-vault-exclusions.js";
53
56
  import {
54
57
  buildScopeShrinkPlan,
@@ -2122,33 +2125,6 @@ interface PullPlan {
2122
2125
  const PULL_INTENTIONAL_DELETE_MIN_ABS = 10;
2123
2126
  const PULL_INTENTIONAL_DELETE_RATIO = 0.1;
2124
2127
 
2125
- // The four release-shipped `core/<type>/` namespaces the personal-overlay
2126
- // mirror (reindex) materializes into. A stale mirror symlink pushed to the
2127
- // vault lands as a single object at exactly `core/<type>/<...>/<entry>`, and if
2128
- // a later release ships a REAL directory of the same name, pull sees a
2129
- // (local real dir) vs (remote single object) collision at that key forever.
2130
- const OVERLAY_MIRROR_CORE_TYPES = ["knowledge", "policies", "workers", "settings"];
2131
-
2132
- /**
2133
- * True when `key` is under a release-shipped personal-overlay-mirror core
2134
- * namespace (`core/{knowledge,policies,workers,settings}/<entry>/...`).
2135
- *
2136
- * A remote LIST entry is always a single S3 object. A REAL release-shipped core
2137
- * directory only ever syncs as child objects UNDER its key
2138
- * (`core/knowledge/public/agent-browser/INDEX.md`), never as one object AT the
2139
- * directory's own key — so a single object at such a key, colliding with a
2140
- * local real directory, is unambiguously a stale overlay symlink marker, not
2141
- * live core content. Pure path test — no I/O — safe inside the sync planner.
2142
- */
2143
- function isOverlayMirrorCoreKey(key: string): boolean {
2144
- const parts = key.split("/");
2145
- return (
2146
- parts.length >= 3 &&
2147
- parts[0] === "core" &&
2148
- OVERLAY_MIRROR_CORE_TYPES.includes(parts[1])
2149
- );
2150
- }
2151
-
2152
2128
  function computePullPlan(
2153
2129
  remoteFiles: RemoteFile[],
2154
2130
  journal: SyncJournal,
@@ -2305,6 +2281,26 @@ function computePullPlan(
2305
2281
 
2306
2282
  const journalEntry = getEntry(journal, remoteFile.key);
2307
2283
 
2284
+ // Reindex and package wiring own symlinks under these `core/<type>`
2285
+ // surfaces; their canonical sources are elsewhere. A prior pull records
2286
+ // the remote object as a symlink, then reindex removes the generated link.
2287
+ // Without this classification the next pull sees only an absent local path
2288
+ // and downloads the same marker forever. Retain regular core files: the
2289
+ // journal kind is the discriminator, because LIST exposes no object type.
2290
+ // An unchanged ETag is also required: a remote writer can replace a legacy
2291
+ // symlink with canonical file content at the same key, and that replacement
2292
+ // must still be pulled.
2293
+ if (
2294
+ personalMode &&
2295
+ journalEntry?.kind === "symlink" &&
2296
+ isGeneratedCoreMirrorKey(remoteFile.key) &&
2297
+ journalEntry.remoteEtag !== undefined &&
2298
+ !hasRemoteChanged(remoteFile, journalEntry)
2299
+ ) {
2300
+ items.push({ action: "skip-excluded-policy", remoteFile, localPath });
2301
+ continue;
2302
+ }
2303
+
2308
2304
  // ── FILE_TOMBSTONE consult (delete-resync) ───────────────────────────────
2309
2305
  // A remote object present in the LIST may be an intentionally-deleted key
2310
2306
  // (a peer re-pushed it, or its delete-marker hasn't propagated to this
@@ -2414,7 +2410,7 @@ function computePullPlan(
2414
2410
  // "rm -rf the local directory" advice below is actively dangerous here
2415
2411
  // (it would destroy release-shipped core content to pull inert derived
2416
2412
  // state), and it recurred on every sync. feedback_ce9aeede.
2417
- if (isOverlayMirrorCoreKey(remoteFile.key)) {
2413
+ if (isGeneratedCoreMirrorKey(remoteFile.key)) {
2418
2414
  console.error(
2419
2415
  ` Note: ${remoteFile.key} is a stale personal-overlay marker ` +
2420
2416
  `shadowing a release-shipped core directory; keeping the local ` +
@@ -29,6 +29,7 @@ import {
29
29
  PERSONAL_VAULT_JOURNAL_SLUG,
30
30
  migratePersonalVaultJournal,
31
31
  listJournals,
32
+ markLocalDeleteIntent,
32
33
  } from "./journal.js";
33
34
  import type { SyncJournal, PullRecord } from "./types.js";
34
35
 
@@ -745,6 +746,50 @@ describe("journal", () => {
745
746
  // successful sync because it read one path keyed off the HQ-ROOT, while the
746
747
  // engine writes slug-sharded journals (personal vault + per-company).
747
748
  // `listJournals` is the all-shard enumeration the status surface now uses.
749
+ it("binds a local delete intent to the captured remote revision", () => {
750
+ const journal: SyncJournal = {
751
+ version: "2",
752
+ lastSync: "",
753
+ pulls: [],
754
+ files: {
755
+ "docs/a.md": {
756
+ hash: "local-hash",
757
+ size: 1,
758
+ syncedAt: new Date().toISOString(),
759
+ direction: "down",
760
+ kind: "file",
761
+ remoteEtag: "new-etag",
762
+ },
763
+ },
764
+ };
765
+
766
+ expect(
767
+ markLocalDeleteIntent(
768
+ journal,
769
+ "docs/a.md",
770
+ "local-hash",
771
+ "file",
772
+ "old-etag",
773
+ ),
774
+ ).toBe(false);
775
+ expect(journal.files["docs/a.md"]?.localDeleteIntent).toBeUndefined();
776
+
777
+ expect(
778
+ markLocalDeleteIntent(
779
+ journal,
780
+ "docs/a.md",
781
+ "local-hash",
782
+ "file",
783
+ "new-etag",
784
+ ),
785
+ ).toBe(true);
786
+ expect(journal.files["docs/a.md"]?.localDeleteIntent).toMatchObject({
787
+ remoteEtag: "new-etag",
788
+ localHash: "local-hash",
789
+ localKind: "file",
790
+ });
791
+ });
792
+
748
793
  describe("listJournals", () => {
749
794
  function seed(slug: string, files: SyncJournal["files"]): void {
750
795
  writeJournal(slug, {
package/src/journal.ts CHANGED
@@ -607,9 +607,15 @@ export function markLocalDeleteIntent(
607
607
  relativePath: string,
608
608
  localHash: string,
609
609
  localKind: "file" | "symlink",
610
+ expectedRemoteEtag: string,
610
611
  ): boolean {
611
612
  const entry = journal.files[relativePath];
612
- if (!entry?.remoteEtag || entry.hash !== localHash || entry.kind !== localKind) {
613
+ if (
614
+ !entry?.remoteEtag ||
615
+ entry.remoteEtag !== normalizeEtag(expectedRemoteEtag) ||
616
+ entry.hash !== localHash ||
617
+ entry.kind !== localKind
618
+ ) {
613
619
  return false;
614
620
  }
615
621
  entry.localDeleteIntent = {
@@ -621,6 +627,17 @@ export function markLocalDeleteIntent(
621
627
  return true;
622
628
  }
623
629
 
630
+ /** Invalidate a prior delete authorization after a watcher-observed recreate. */
631
+ export function clearLocalDeleteIntent(
632
+ journal: SyncJournal,
633
+ relativePath: string,
634
+ ): boolean {
635
+ const entry = journal.files[relativePath];
636
+ if (!entry?.localDeleteIntent) return false;
637
+ delete entry.localDeleteIntent;
638
+ return true;
639
+ }
640
+
624
641
  /** True if the entry is a tombstone (set by `tombstoneEntry`). */
625
642
  export function isTombstone(entry: JournalEntry | undefined): boolean {
626
643
  return !!entry && typeof entry.removedAt === "string";