@indigoai-us/hq-cloud 6.14.18 → 6.14.20

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 (101) hide show
  1. package/.github/workflows/ci.yml +35 -0
  2. package/dist/bin/sync-runner-company.d.ts +7 -0
  3. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  4. package/dist/bin/sync-runner-company.js +21 -1
  5. package/dist/bin/sync-runner-company.js.map +1 -1
  6. package/dist/bin/sync-runner-planning.d.ts.map +1 -1
  7. package/dist/bin/sync-runner-planning.js +8 -16
  8. package/dist/bin/sync-runner-planning.js.map +1 -1
  9. package/dist/bin/sync-runner-planning.test.js +138 -0
  10. package/dist/bin/sync-runner-planning.test.js.map +1 -1
  11. package/dist/bin/sync-runner-watch-loop.d.ts +10 -2
  12. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  13. package/dist/bin/sync-runner-watch-loop.js +201 -25
  14. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  15. package/dist/bin/sync-runner-watch-routes.d.ts +1 -1
  16. package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
  17. package/dist/bin/sync-runner-watch-routes.js +14 -2
  18. package/dist/bin/sync-runner-watch-routes.js.map +1 -1
  19. package/dist/bin/sync-runner.d.ts +17 -2
  20. package/dist/bin/sync-runner.d.ts.map +1 -1
  21. package/dist/bin/sync-runner.js +36 -6
  22. package/dist/bin/sync-runner.js.map +1 -1
  23. package/dist/bin/sync-runner.test.js +550 -3
  24. package/dist/bin/sync-runner.test.js.map +1 -1
  25. package/dist/cli/rescue-core.js +27 -1
  26. package/dist/cli/rescue-core.js.map +1 -1
  27. package/dist/cli/rescue-drop-dir-symlink.test.d.ts +2 -0
  28. package/dist/cli/rescue-drop-dir-symlink.test.d.ts.map +1 -0
  29. package/dist/cli/rescue-drop-dir-symlink.test.js +206 -0
  30. package/dist/cli/rescue-drop-dir-symlink.test.js.map +1 -0
  31. package/dist/cli/share.d.ts +15 -0
  32. package/dist/cli/share.d.ts.map +1 -1
  33. package/dist/cli/share.js +100 -55
  34. package/dist/cli/share.js.map +1 -1
  35. package/dist/cli/share.test.js +119 -6
  36. package/dist/cli/share.test.js.map +1 -1
  37. package/dist/cli/sync.d.ts.map +1 -1
  38. package/dist/cli/sync.js +19 -25
  39. package/dist/cli/sync.js.map +1 -1
  40. package/dist/cli/sync.test.js +121 -0
  41. package/dist/cli/sync.test.js.map +1 -1
  42. package/dist/journal.d.ts +3 -1
  43. package/dist/journal.d.ts.map +1 -1
  44. package/dist/journal.js +13 -2
  45. package/dist/journal.js.map +1 -1
  46. package/dist/journal.test.js +26 -1
  47. package/dist/journal.test.js.map +1 -1
  48. package/dist/personal-vault.d.ts +12 -0
  49. package/dist/personal-vault.d.ts.map +1 -1
  50. package/dist/personal-vault.js +20 -0
  51. package/dist/personal-vault.js.map +1 -1
  52. package/dist/qmd-reindex.d.ts +88 -11
  53. package/dist/qmd-reindex.d.ts.map +1 -1
  54. package/dist/qmd-reindex.js +354 -65
  55. package/dist/qmd-reindex.js.map +1 -1
  56. package/dist/qmd-reindex.test.d.ts +4 -0
  57. package/dist/qmd-reindex.test.d.ts.map +1 -1
  58. package/dist/qmd-reindex.test.js +285 -3
  59. package/dist/qmd-reindex.test.js.map +1 -1
  60. package/dist/s3.d.ts +13 -0
  61. package/dist/s3.d.ts.map +1 -1
  62. package/dist/s3.js +34 -1
  63. package/dist/s3.js.map +1 -1
  64. package/dist/s3.test.js +60 -1
  65. package/dist/s3.test.js.map +1 -1
  66. package/dist/telemetry.d.ts +22 -0
  67. package/dist/telemetry.d.ts.map +1 -1
  68. package/dist/telemetry.js +79 -0
  69. package/dist/telemetry.js.map +1 -1
  70. package/dist/telemetry.test.js +117 -1
  71. package/dist/telemetry.test.js.map +1 -1
  72. package/dist/watcher.d.ts +26 -1
  73. package/dist/watcher.d.ts.map +1 -1
  74. package/dist/watcher.js +75 -12
  75. package/dist/watcher.js.map +1 -1
  76. package/package.json +1 -1
  77. package/src/bin/sync-runner-company.ts +29 -1
  78. package/src/bin/sync-runner-planning.test.ts +153 -0
  79. package/src/bin/sync-runner-planning.ts +10 -17
  80. package/src/bin/sync-runner-watch-loop.ts +322 -32
  81. package/src/bin/sync-runner-watch-routes.ts +14 -1
  82. package/src/bin/sync-runner.test.ts +662 -6
  83. package/src/bin/sync-runner.ts +65 -7
  84. package/src/cli/rescue-core.ts +26 -1
  85. package/src/cli/rescue-drop-dir-symlink.test.ts +224 -0
  86. package/src/cli/share.test.ts +150 -6
  87. package/src/cli/share.ts +143 -48
  88. package/src/cli/sync.test.ts +150 -1
  89. package/src/cli/sync.ts +25 -29
  90. package/src/journal.test.ts +45 -0
  91. package/src/journal.ts +18 -1
  92. package/src/personal-vault.ts +23 -0
  93. package/src/qmd-reindex.test.ts +324 -4
  94. package/src/qmd-reindex.ts +414 -76
  95. package/src/s3.test.ts +79 -0
  96. package/src/s3.ts +48 -1
  97. package/src/telemetry.test.ts +128 -0
  98. package/src/telemetry.ts +80 -0
  99. package/src/watcher.ts +111 -14
  100. package/test/e2e/watcher-real-chokidar.test.ts +62 -2
  101. package/test/e2e/watcher-recursive-backend.test.ts +67 -1
@@ -1653,6 +1653,11 @@ describe("share", () => {
1653
1653
  propagateDeletePolicy: "owned-only",
1654
1654
  });
1655
1655
  expect(result.filesDeleted).toBe(1);
1656
+ expect(result.pathResults).toContainEqual({
1657
+ path: "knowledge",
1658
+ status: "accepted",
1659
+ operation: "delete",
1660
+ });
1656
1661
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "knowledge");
1657
1662
  const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1658
1663
  expect(journal.files["knowledge"]).toBeUndefined();
@@ -1794,6 +1799,34 @@ describe("share", () => {
1794
1799
  expect(uploadSymlink).not.toHaveBeenCalled();
1795
1800
  expect(uploadFile).toHaveBeenCalledWith(expect.anything(), path.join(localSkill, "SKILL.md"), ".claude/skills/local-skill/SKILL.md", undefined, expect.anything());
1796
1801
  });
1802
+ it("personalMode excludes generated core mirror symlinks but keeps canonical core files", async () => {
1803
+ const packagePolicy = path.join(tmpDir, "core", "packages", "hq-pack-example", "policies", "generated.md");
1804
+ const generatedMirror = path.join(tmpDir, "core", "policies", "generated.md");
1805
+ const canonicalCoreFile = path.join(tmpDir, "core", "policies", "shipped.md");
1806
+ fs.mkdirSync(path.dirname(packagePolicy), { recursive: true });
1807
+ fs.mkdirSync(path.dirname(generatedMirror), { recursive: true });
1808
+ fs.writeFileSync(packagePolicy, "generated package policy\n");
1809
+ fs.symlinkSync(packagePolicy, generatedMirror);
1810
+ fs.writeFileSync(canonicalCoreFile, "shipped core policy\n");
1811
+ const result = await share({
1812
+ paths: [path.join(tmpDir, "core")],
1813
+ entityContext: makeEntityContext({
1814
+ uid: "prs_01PERSONALTEST",
1815
+ slug: "__hq_personal_vault__",
1816
+ bucketName: "hq-vault-prs-01PERSONALTEST",
1817
+ }),
1818
+ hqRoot: tmpDir,
1819
+ personalMode: true,
1820
+ journalSlug: "personal-generated-core-mirror",
1821
+ });
1822
+ expect(result.filesExcludedByPolicy).toBe(1);
1823
+ expect(result.filesUploaded).toBe(2);
1824
+ expect(vi.mocked(uploadSymlink)).not.toHaveBeenCalled();
1825
+ expect(vi.mocked(uploadFile).mock.calls.map((call) => call[2]).sort()).toEqual([
1826
+ "core/packages/hq-pack-example/policies/generated.md",
1827
+ "core/policies/shipped.md",
1828
+ ]);
1829
+ });
1797
1830
  it("personal-vault override does NOT apply to company vaults: `owned-only` on cmp_ entity still " +
1798
1831
  "refuses to tombstone a direction:'down' entry (multi-user curation intact)", async () => {
1799
1832
  // Sibling guard for the previous test: the override is scoped strictly
@@ -2492,7 +2525,7 @@ describe("share", () => {
2492
2525
  message: expect.stringContaining("tombstone"),
2493
2526
  });
2494
2527
  });
2495
- it("currency-gated: refuses delete for legacy journal entry with no recorded remoteEtag", async () => {
2528
+ it("daemon-stopped deletion has no watcher intent and is safely refused", async () => {
2496
2529
  const companyRoot = path.join(tmpDir, "companies", "acme");
2497
2530
  fs.mkdirSync(companyRoot, { recursive: true });
2498
2531
  // Journal entry from before remoteEtag tracking — no etag to compare
@@ -2532,6 +2565,12 @@ describe("share", () => {
2532
2565
  reason: "missing-delete-intent",
2533
2566
  });
2534
2567
  expect(result.filesRefusedStale).toBe(1);
2568
+ expect(result.pathResults).toContainEqual({
2569
+ path: "legacy-no-etag.md",
2570
+ status: "refused",
2571
+ operation: "delete",
2572
+ reason: "missing-delete-intent",
2573
+ });
2535
2574
  });
2536
2575
  it("currency-gated: real-world /update-hq scenario — direction:'down' delete propagates when current", async () => {
2537
2576
  // Regression for the exact bug that motivated the 5.24 default flip:
@@ -2742,6 +2781,44 @@ describe("share", () => {
2742
2781
  // exclusion list itself is enforced by the runner (sync-runner.ts) by only
2743
2782
  // passing in the allowed top-level directories — share() trusts its
2744
2783
  // `paths` input.
2784
+ it("plans an intent-backed personal delete from an explicit missing scope root", async () => {
2785
+ const key = "deleted-top-level.md";
2786
+ const journalSlug = "personal-scoped-delete";
2787
+ const journalPath = path.join(stateDir, `sync-journal.${journalSlug}.json`);
2788
+ fs.writeFileSync(journalPath, JSON.stringify({
2789
+ version: "2",
2790
+ lastSync: new Date().toISOString(),
2791
+ pulls: [],
2792
+ files: {
2793
+ [key]: {
2794
+ hash: "captured-hash",
2795
+ size: 8,
2796
+ syncedAt: new Date().toISOString(),
2797
+ direction: "up",
2798
+ remoteEtag: "captured-etag",
2799
+ kind: "file",
2800
+ localDeleteIntent: deleteIntent("captured-etag", "captured-hash"),
2801
+ },
2802
+ },
2803
+ }));
2804
+ const result = await share({
2805
+ // computePersonalVaultPaths() has no path to contribute after this
2806
+ // top-level file is gone. The watcher-captured root is the sole scope.
2807
+ paths: [],
2808
+ deleteScopeRoots: [key],
2809
+ company: "acme",
2810
+ vaultConfig: mockConfig,
2811
+ hqRoot: tmpDir,
2812
+ personalMode: true,
2813
+ journalSlug,
2814
+ skipUnchanged: true,
2815
+ propagateDeletes: true,
2816
+ propagateDeletePolicy: "owned-only",
2817
+ });
2818
+ expect(result.filesDeleted).toBe(1);
2819
+ expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), key);
2820
+ expect(JSON.parse(fs.readFileSync(journalPath, "utf8")).files[key]).toBeUndefined();
2821
+ });
2745
2822
  // ── Decommission prefixes ──────────────────────────────────────────────────
2746
2823
  //
2747
2824
  // `decommissionPrefixes` is the "company was promoted to its own team
@@ -3312,7 +3389,7 @@ describe("share", () => {
3312
3389
  // entries as absent locally (the rest get a real file on disk).
3313
3390
  // `direction` default "up" so the owned-only branch accepts every
3314
3391
  // candidate (direction:"down" silently skips and never trips the guard).
3315
- function setupJournal({ total, missing, direction = "up", }) {
3392
+ function setupJournal({ total, missing, direction = "up", withIntents = true, }) {
3316
3393
  const companyRoot = path.join(tmpDir, "companies", "acme");
3317
3394
  fs.mkdirSync(companyRoot, { recursive: true });
3318
3395
  const files = {};
@@ -3325,7 +3402,9 @@ describe("share", () => {
3325
3402
  direction,
3326
3403
  remoteEtag: `etag-${i}`,
3327
3404
  kind: "file",
3328
- localDeleteIntent: deleteIntent(`etag-${i}`, "h"),
3405
+ ...(withIntents
3406
+ ? { localDeleteIntent: deleteIntent(`etag-${i}`, "h") }
3407
+ : {}),
3329
3408
  };
3330
3409
  if (i >= missing) {
3331
3410
  fs.writeFileSync(path.join(companyRoot, key), "hi");
@@ -3343,7 +3422,11 @@ describe("share", () => {
3343
3422
  delete process.env.HQ_SYNC_DELETE_BULK_OVERRIDE;
3344
3423
  });
3345
3424
  it("trips at the 11/100 boundary (11% > 10%, abs >= 10): refuses every delete and emits bulk-asymmetry event", async () => {
3346
- const { companyRoot, journalPath } = setupJournal({ total: 100, missing: 11 });
3425
+ const { companyRoot, journalPath } = setupJournal({
3426
+ total: 100,
3427
+ missing: 11,
3428
+ withIntents: false,
3429
+ });
3347
3430
  // currency-gated: remote HEAD would match every journal etag — guard
3348
3431
  // must trip BEFORE any HEAD round-trip occurs.
3349
3432
  vi.mocked(headRemoteFile).mockImplementation(async (_ctx, key) => ({
@@ -3392,6 +3475,28 @@ describe("share", () => {
3392
3475
  const perKeyRefusals = events.filter((e) => e.type === "delete-refused-stale-etag" && e.reason === "bulk-asymmetry");
3393
3476
  expect(perKeyRefusals.length).toBe(11);
3394
3477
  });
3478
+ it("intent-backed 11/100 subtree disappearance bypasses the breaker and deletes every covered path", async () => {
3479
+ const { companyRoot } = setupJournal({ total: 100, missing: 11 });
3480
+ vi.mocked(headRemoteFile).mockImplementation(async (_ctx, key) => ({
3481
+ etag: `etag-${parseInt(key.replace(/[^0-9]/g, ""), 10)}`,
3482
+ lastModified: new Date(),
3483
+ size: 5,
3484
+ }));
3485
+ const events = [];
3486
+ const result = await share({
3487
+ paths: [companyRoot],
3488
+ company: "acme",
3489
+ vaultConfig: mockConfig,
3490
+ hqRoot: tmpDir,
3491
+ skipUnchanged: true,
3492
+ propagateDeletes: true,
3493
+ propagateDeletePolicy: "currency-gated",
3494
+ onEvent: (e) => events.push(e),
3495
+ });
3496
+ expect(result.filesDeleted).toBe(11);
3497
+ expect(deleteRemoteFile).toHaveBeenCalledTimes(11);
3498
+ expect(events.find((e) => e.type === "delete-refused-bulk-asymmetry")).toBeUndefined();
3499
+ });
3395
3500
  it("does NOT trip at 9/100 (below ratio): all candidates delete normally", async () => {
3396
3501
  const { companyRoot } = setupJournal({ total: 100, missing: 9 });
3397
3502
  vi.mocked(headRemoteFile).mockImplementation(async (_ctx, key) => ({
@@ -3449,7 +3554,11 @@ describe("share", () => {
3449
3554
  expect(deleteRemoteFile).toHaveBeenCalledTimes(11);
3450
3555
  });
3451
3556
  it("fully-empty mirror (100/100) trips the guard — the canonical mass-delete failure mode", async () => {
3452
- const { companyRoot, journalPath } = setupJournal({ total: 50, missing: 50 });
3557
+ const { companyRoot, journalPath } = setupJournal({
3558
+ total: 50,
3559
+ missing: 50,
3560
+ withIntents: false,
3561
+ });
3453
3562
  vi.mocked(headRemoteFile).mockImplementation(async (_ctx, key) => ({
3454
3563
  etag: `etag-${parseInt(key.replace(/[^0-9]/g, ""), 10)}`,
3455
3564
  lastModified: new Date(),
@@ -3497,7 +3606,11 @@ describe("share", () => {
3497
3606
  expect(deleteRemoteFile).toHaveBeenCalledTimes(9);
3498
3607
  });
3499
3608
  it("owned-only policy also goes through the guard: 11/100 missing direction:'up' entries refused", async () => {
3500
- const { companyRoot, journalPath } = setupJournal({ total: 100, missing: 11 });
3609
+ const { companyRoot, journalPath } = setupJournal({
3610
+ total: 100,
3611
+ missing: 11,
3612
+ withIntents: false,
3613
+ });
3501
3614
  const events = [];
3502
3615
  const result = await share({
3503
3616
  paths: [companyRoot],