@indigoai-us/hq-cloud 6.14.2 → 6.14.4

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 (48) hide show
  1. package/dist/bin/sync-runner-company.d.ts +1 -0
  2. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  3. package/dist/bin/sync-runner-company.js +44 -19
  4. package/dist/bin/sync-runner-company.js.map +1 -1
  5. package/dist/bin/sync-runner.d.ts.map +1 -1
  6. package/dist/bin/sync-runner.js +25 -36
  7. package/dist/bin/sync-runner.js.map +1 -1
  8. package/dist/bin/sync-runner.test.js +128 -4
  9. package/dist/bin/sync-runner.test.js.map +1 -1
  10. package/dist/cli/reindex.d.ts.map +1 -1
  11. package/dist/cli/reindex.js +226 -2
  12. package/dist/cli/reindex.js.map +1 -1
  13. package/dist/cli/reindex.test.js +134 -0
  14. package/dist/cli/reindex.test.js.map +1 -1
  15. package/dist/cli/sync.d.ts.map +1 -1
  16. package/dist/cli/sync.js +16 -14
  17. package/dist/cli/sync.js.map +1 -1
  18. package/dist/cli/sync.test.js +73 -0
  19. package/dist/cli/sync.test.js.map +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/personal-vault.d.ts +29 -1
  25. package/dist/personal-vault.d.ts.map +1 -1
  26. package/dist/personal-vault.js +48 -2
  27. package/dist/personal-vault.js.map +1 -1
  28. package/dist/personal-vault.test.js +44 -1
  29. package/dist/personal-vault.test.js.map +1 -1
  30. package/dist/watcher.d.ts +1 -1
  31. package/dist/watcher.d.ts.map +1 -1
  32. package/dist/watcher.js +20 -7
  33. package/dist/watcher.js.map +1 -1
  34. package/dist/watcher.test.js +73 -0
  35. package/dist/watcher.test.js.map +1 -1
  36. package/package.json +1 -1
  37. package/src/bin/sync-runner-company.ts +50 -22
  38. package/src/bin/sync-runner.test.ts +147 -4
  39. package/src/bin/sync-runner.ts +49 -54
  40. package/src/cli/reindex.test.ts +162 -0
  41. package/src/cli/reindex.ts +219 -2
  42. package/src/cli/sync.test.ts +90 -0
  43. package/src/cli/sync.ts +16 -14
  44. package/src/index.ts +2 -0
  45. package/src/personal-vault.test.ts +57 -0
  46. package/src/personal-vault.ts +48 -2
  47. package/src/watcher.test.ts +89 -0
  48. package/src/watcher.ts +23 -8
@@ -26,7 +26,12 @@
26
26
  * the routing source-of-truth.)
27
27
  * - `repos/`, `workspace/`: per user directive — heavy local-only
28
28
  * content (cloned remotes, session threads) that has no business in
29
- * the personal vault.
29
+ * the personal vault. Three explicit sub-paths pierce this exclusion and
30
+ * DO round-trip: `workspace/threads/handoff.json` (+ its active thread) via
31
+ * {@link computeContinuityPointerPaths}, `workspace/agency/` via
32
+ * {@link computeAgencySyncPaths}, and `workspace/.session-logs/` (the
33
+ * reindex-captured Claude Code transcripts) via
34
+ * {@link computeSessionLogsSyncPaths}.
30
35
  *
31
36
  * Note: `core/`, `data/`, and `personal/` were previously excluded but are
32
37
  * INCLUDED as of user directive 2026-05-13. `core/` ships the hq-core
@@ -134,7 +139,15 @@ export function computePersonalVaultPaths(
134
139
  );
135
140
  const continuity = computeContinuityPointerPaths(hqRoot);
136
141
  const agency = computeAgencySyncPaths(hqRoot);
137
- return [...topLevel, ...manifest, ...companySubdirs, ...continuity, ...agency];
142
+ const sessionLogs = computeSessionLogsSyncPaths(hqRoot);
143
+ return [
144
+ ...topLevel,
145
+ ...manifest,
146
+ ...companySubdirs,
147
+ ...continuity,
148
+ ...agency,
149
+ ...sessionLogs,
150
+ ];
138
151
  }
139
152
 
140
153
  /**
@@ -261,6 +274,39 @@ export function computeAgencySyncPaths(hqRoot: string): string[] {
261
274
  return [];
262
275
  }
263
276
 
277
+ /**
278
+ * Fixed relative path (forward-slash, hq-root-relative) of the session-logs
279
+ * subtree. See {@link computeSessionLogsSyncPaths}.
280
+ */
281
+ export const SESSION_LOGS_SYNC_REL = "workspace/.session-logs";
282
+
283
+ /**
284
+ * Compute the absolute path of the session-logs carve-out:
285
+ * `workspace/.session-logs/` (the whole subtree).
286
+ *
287
+ * `hq reindex` copies this HQ root's Claude Code session transcripts here (see
288
+ * reindex's copySessionLogs). Like the agency carve-out above, this pierces the
289
+ * `workspace/` top-level exclusion for ONE specific subtree so those raw
290
+ * transcripts round-trip to the personal vault / S3 — the operator's explicit
291
+ * directive is that the session logs travel with the vault. The rest of
292
+ * `workspace/` (session scratch, locks, reports, worktrees) stays machine-local.
293
+ *
294
+ * Returns the directory path when it exists; share()'s per-file walk handles
295
+ * recursion, and the nested personal-vault exclusions still apply to files
296
+ * inside it. Fail-soft: a missing or unreadable `workspace/.session-logs/`
297
+ * returns []. Callers tolerate empty arrays — same contract as the manifest,
298
+ * continuity, and agency special-cases.
299
+ */
300
+ export function computeSessionLogsSyncPaths(hqRoot: string): string[] {
301
+ const sessionLogsDir = path.join(hqRoot, "workspace", ".session-logs");
302
+ try {
303
+ if (fs.statSync(sessionLogsDir).isDirectory()) return [sessionLogsDir];
304
+ } catch {
305
+ // No session logs captured on this machine yet — nothing to carry.
306
+ }
307
+ return [];
308
+ }
309
+
264
310
  /**
265
311
  * Discover `companies/{slug}/` subdirs that should sync to the personal
266
312
  * bucket as a fallback for companies the operator has not designated as
@@ -12,6 +12,7 @@ import {
12
12
  import { StaticFlagProvider } from "./sync/feature-flags.js";
13
13
  import type { PushEvent } from "./sync/push-event.js";
14
14
  import type { PushTransport } from "./sync/push-transport.js";
15
+ import type { TelemetryEventsBatch } from "./vault-client.js";
15
16
 
16
17
  /**
17
18
  * US-001 — Phase 1 test harness: watch-triggered push seam + latency assertion.
@@ -371,6 +372,36 @@ describe("US-002: createWatchPathFilter — personal-vault exclusions", () => {
371
372
  expect(personal(path.join(ROOT, "workspace/worktrees/x/a.ts"))).toBe(false);
372
373
  });
373
374
 
375
+ // ── workspace/.session-logs subtree carve-out (personal mode) ────────────
376
+ // The personal push uploads workspace/.session-logs/** (reindex-captured
377
+ // Claude Code transcripts, computeSessionLogsSyncPaths), so the personal
378
+ // watcher must emit those changes event-driven. Mirrors the agency carve-out.
379
+ it("DOES emit for workspace/.session-logs files in personalMode (synced subtree)", () => {
380
+ expect(
381
+ personal(path.join(ROOT, "workspace/.session-logs/session.jsonl")),
382
+ ).toBe(true);
383
+ expect(
384
+ personal(path.join(ROOT, "workspace/.session-logs/tool-results/r.txt")),
385
+ ).toBe(true);
386
+ // The subtree root itself, as a file event, also emits.
387
+ expect(personal(path.join(ROOT, "workspace/.session-logs"))).toBe(true);
388
+ });
389
+
390
+ it("allows chokidar to DESCEND to workspace/.session-logs in personalMode (dir probes)", () => {
391
+ expect(personal(path.join(ROOT, "workspace"), true)).toBe(true);
392
+ expect(personal(path.join(ROOT, "workspace/.session-logs"), true)).toBe(true);
393
+ expect(
394
+ personal(path.join(ROOT, "workspace/.session-logs/tool-results"), true),
395
+ ).toBe(true);
396
+ });
397
+
398
+ it("session-logs carve-out is exact-segment (does NOT leak to a similarly-named sibling)", () => {
399
+ expect(
400
+ personal(path.join(ROOT, "workspace/.session-logs-old/foo.txt")),
401
+ ).toBe(false);
402
+ expect(personal(path.join(ROOT, "workspace/.session-logsX.md"))).toBe(false);
403
+ });
404
+
374
405
  it("does not apply the carve-out in non-personal mode", () => {
375
406
  // In non-personal mode the excluded-top-level layer never runs, so the
376
407
  // carve-out is irrelevant; handoff.json passes via the ordinary ignore
@@ -706,4 +737,62 @@ describe("PushEventEmitter — directory and delete tombstone handling", () => {
706
737
  expect(published[0]).not.toHaveProperty("contentHash");
707
738
  expect(published[0]).not.toHaveProperty("mtime");
708
739
  });
740
+
741
+ it("emits push telemetry only when publication fails", async () => {
742
+ const posts: TelemetryEventsBatch[] = [];
743
+ const telemetryClient = {
744
+ postTelemetryEvents: vi.fn(async (batch: TelemetryEventsBatch) => {
745
+ posts.push(batch);
746
+ return { ok: true, written: batch.events.length, skipped: [] };
747
+ }),
748
+ };
749
+ const changed = path.join(dir, "changed.md");
750
+ fs.writeFileSync(changed, "changed");
751
+ const success = new PushEventEmitter({
752
+ originTenantId: "tenant-indigo",
753
+ originDeviceId: "device-a",
754
+ transport: {
755
+ start: async () => {},
756
+ dispose: async () => {},
757
+ connected: true,
758
+ publish: async () => {},
759
+ },
760
+ flagProvider: new StaticFlagProvider(["tenant-indigo"]),
761
+ telemetryClient,
762
+ });
763
+
764
+ await success.emitForBatch({ paths: new Map([[changed, "changed.md"]]) });
765
+ await flushImmediate();
766
+ expect(posts).toEqual([]);
767
+
768
+ const failed = new PushEventEmitter({
769
+ originTenantId: "tenant-indigo",
770
+ originDeviceId: "device-a",
771
+ transport: {
772
+ start: async () => {},
773
+ dispose: async () => {},
774
+ connected: true,
775
+ publish: async () => {
776
+ throw new Error("network down");
777
+ },
778
+ },
779
+ flagProvider: new StaticFlagProvider(["tenant-indigo"]),
780
+ telemetryClient,
781
+ onError: vi.fn(),
782
+ });
783
+
784
+ await failed.emitForBatch({ paths: new Map([[changed, "changed.md"]]) });
785
+ await flushImmediate();
786
+ expect(posts.flatMap((post) => post.events)).toEqual([
787
+ expect.objectContaining({
788
+ eventName: "push_event_failed",
789
+ properties: {
790
+ kind: "upsert",
791
+ count: 1,
792
+ status: "failure",
793
+ stage: "publish",
794
+ },
795
+ }),
796
+ ]);
797
+ });
709
798
  });
package/src/watcher.ts CHANGED
@@ -16,6 +16,7 @@ import { isPersonalVaultExcluded } from "./personal-vault-exclusions.js";
16
16
  import {
17
17
  AGENCY_SYNC_REL,
18
18
  CONTINUITY_POINTER_REL,
19
+ SESSION_LOGS_SYNC_REL,
19
20
  PERSONAL_VAULT_EXCLUDED_TOP_LEVEL,
20
21
  } from "./personal-vault.js";
21
22
  import type { PushEvent } from "./sync/push-event.js";
@@ -466,6 +467,22 @@ export function createWatchPathFilter(
466
467
  }
467
468
  if (isDir && AGENCY_SYNC_REL.startsWith(rel + "/")) return true;
468
469
 
470
+ // Session-logs carve-out: `workspace/.session-logs/` (the whole subtree)
471
+ // IS synced via computeSessionLogsSyncPaths (mirrored on the push side),
472
+ // so it must re-include BEFORE the top-level `workspace/` rejection below
473
+ // — identical shape to the agency carve-out just above. Without this, a
474
+ // `--personal --watch` runner never emits reindex-captured session-log
475
+ // changes and they wait for the 60s poll. Paths at/under it emit; the
476
+ // subtree root and its ancestor dir (`workspace`) return true for the dir
477
+ // probe so chokidar descends to reach them instead of pruning `workspace/`.
478
+ if (
479
+ rel === SESSION_LOGS_SYNC_REL ||
480
+ rel.startsWith(SESSION_LOGS_SYNC_REL + "/")
481
+ ) {
482
+ return true;
483
+ }
484
+ if (isDir && SESSION_LOGS_SYNC_REL.startsWith(rel + "/")) return true;
485
+
469
486
  // Layer 3: excluded top-level buckets (.git/companies/repos/workspace).
470
487
  const topLevel = rel.split("/")[0];
471
488
  if (excludedTopLevel.has(topLevel)) return false;
@@ -972,7 +989,7 @@ export class PushEventEmitter {
972
989
  this.onError(err instanceof Error ? err : new Error(String(err)), {
973
990
  relativePath,
974
991
  });
975
- this.emitPublishTelemetry("push_event_failed", "unknown", "capture");
992
+ this.emitPublishFailureTelemetry("unknown", "capture");
976
993
  return;
977
994
  }
978
995
 
@@ -1008,31 +1025,29 @@ export class PushEventEmitter {
1008
1025
 
1009
1026
  try {
1010
1027
  await this.transport.publish(event);
1011
- this.emitPublishTelemetry("push_event_published", event.kind, "publish");
1012
1028
  } catch (err) {
1013
1029
  // Push failure (network / non-2xx / timeout). The cadence poll is the
1014
1030
  // safety net — log + continue, never throw.
1015
1031
  this.onError(err instanceof Error ? err : new Error(String(err)), {
1016
1032
  relativePath,
1017
1033
  });
1018
- this.emitPublishTelemetry("push_event_failed", event.kind, "publish");
1034
+ this.emitPublishFailureTelemetry(event.kind, "publish");
1019
1035
  }
1020
1036
  }
1021
1037
 
1022
- private emitPublishTelemetry(
1023
- eventName: "push_event_published" | "push_event_failed",
1038
+ private emitPublishFailureTelemetry(
1024
1039
  kind: string,
1025
- stage: string,
1040
+ stage: "capture" | "publish",
1026
1041
  ): void {
1027
1042
  void emitCloudTelemetry(
1028
1043
  this.telemetryClient,
1029
1044
  {
1030
- eventName,
1045
+ eventName: "push_event_failed",
1031
1046
  source: "watcher",
1032
1047
  properties: {
1033
1048
  kind,
1034
1049
  count: 1,
1035
- result: eventName === "push_event_published" ? "success" : "failure",
1050
+ status: "failure",
1036
1051
  stage,
1037
1052
  },
1038
1053
  },