@indigoai-us/hq-cloud 6.14.50 → 6.15.1

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 (340) hide show
  1. package/dist/bin/sync-mutation.d.ts +14 -0
  2. package/dist/bin/sync-mutation.d.ts.map +1 -0
  3. package/dist/bin/sync-mutation.js +60 -0
  4. package/dist/bin/sync-mutation.js.map +1 -0
  5. package/dist/bin/sync-mutation.test.d.ts +2 -0
  6. package/dist/bin/sync-mutation.test.d.ts.map +1 -0
  7. package/dist/bin/sync-mutation.test.js +64 -0
  8. package/dist/bin/sync-mutation.test.js.map +1 -0
  9. package/dist/bin/sync-runner-company.d.ts +8 -0
  10. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  11. package/dist/bin/sync-runner-company.js +16 -0
  12. package/dist/bin/sync-runner-company.js.map +1 -1
  13. package/dist/bin/sync-runner-company.test.d.ts +2 -0
  14. package/dist/bin/sync-runner-company.test.d.ts.map +1 -0
  15. package/dist/bin/sync-runner-company.test.js +36 -0
  16. package/dist/bin/sync-runner-company.test.js.map +1 -0
  17. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  18. package/dist/bin/sync-runner-watch-loop.js +98 -8
  19. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  20. package/dist/bin/sync-runner.d.ts +17 -0
  21. package/dist/bin/sync-runner.d.ts.map +1 -1
  22. package/dist/bin/sync-runner.js.map +1 -1
  23. package/dist/bin/sync-runner.test.js +109 -0
  24. package/dist/bin/sync-runner.test.js.map +1 -1
  25. package/dist/cli/conflict-recovery.test.d.ts +2 -0
  26. package/dist/cli/conflict-recovery.test.d.ts.map +1 -0
  27. package/dist/cli/conflict-recovery.test.js +201 -0
  28. package/dist/cli/conflict-recovery.test.js.map +1 -0
  29. package/dist/cli/conflict.d.ts +60 -0
  30. package/dist/cli/conflict.d.ts.map +1 -1
  31. package/dist/cli/conflict.js +333 -0
  32. package/dist/cli/conflict.js.map +1 -1
  33. package/dist/cli/reindex-knowledge.d.ts +7 -0
  34. package/dist/cli/reindex-knowledge.d.ts.map +1 -0
  35. package/dist/cli/reindex-knowledge.js +389 -0
  36. package/dist/cli/reindex-knowledge.js.map +1 -0
  37. package/dist/cli/reindex-knowledge.test.d.ts +12 -0
  38. package/dist/cli/reindex-knowledge.test.d.ts.map +1 -0
  39. package/dist/cli/reindex-knowledge.test.js +260 -0
  40. package/dist/cli/reindex-knowledge.test.js.map +1 -0
  41. package/dist/cli/reindex.d.ts.map +1 -1
  42. package/dist/cli/reindex.js +8 -0
  43. package/dist/cli/reindex.js.map +1 -1
  44. package/dist/cli/rescue-settings-reconcile.test.js +47 -0
  45. package/dist/cli/rescue-settings-reconcile.test.js.map +1 -1
  46. package/dist/cli/sync.d.ts +27 -0
  47. package/dist/cli/sync.d.ts.map +1 -1
  48. package/dist/cli/sync.js +52 -0
  49. package/dist/cli/sync.js.map +1 -1
  50. package/dist/cli/sync.test.js +31 -1
  51. package/dist/cli/sync.test.js.map +1 -1
  52. package/dist/index.d.ts +2 -0
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +1 -0
  55. package/dist/index.js.map +1 -1
  56. package/dist/skill-telemetry.d.ts +6 -0
  57. package/dist/skill-telemetry.d.ts.map +1 -1
  58. package/dist/skill-telemetry.js +14 -2
  59. package/dist/skill-telemetry.js.map +1 -1
  60. package/dist/skill-telemetry.test.js +79 -0
  61. package/dist/skill-telemetry.test.js.map +1 -1
  62. package/dist/sync/candidate-uploader.d.ts +88 -0
  63. package/dist/sync/candidate-uploader.d.ts.map +1 -0
  64. package/dist/sync/candidate-uploader.js +212 -0
  65. package/dist/sync/candidate-uploader.js.map +1 -0
  66. package/dist/sync/candidate-uploader.test.d.ts +2 -0
  67. package/dist/sync/candidate-uploader.test.d.ts.map +1 -0
  68. package/dist/sync/candidate-uploader.test.js +132 -0
  69. package/dist/sync/candidate-uploader.test.js.map +1 -0
  70. package/dist/sync/delta-client.d.ts +73 -0
  71. package/dist/sync/delta-client.d.ts.map +1 -0
  72. package/dist/sync/delta-client.js +201 -0
  73. package/dist/sync/delta-client.js.map +1 -0
  74. package/dist/sync/delta-client.test.d.ts +2 -0
  75. package/dist/sync/delta-client.test.d.ts.map +1 -0
  76. package/dist/sync/delta-client.test.js +97 -0
  77. package/dist/sync/delta-client.test.js.map +1 -0
  78. package/dist/sync/durable-apply.d.ts +76 -0
  79. package/dist/sync/durable-apply.d.ts.map +1 -0
  80. package/dist/sync/durable-apply.js +530 -0
  81. package/dist/sync/durable-apply.js.map +1 -0
  82. package/dist/sync/durable-apply.test.d.ts +2 -0
  83. package/dist/sync/durable-apply.test.d.ts.map +1 -0
  84. package/dist/sync/durable-apply.test.js +180 -0
  85. package/dist/sync/durable-apply.test.js.map +1 -0
  86. package/dist/sync/event-sync.d.ts +33 -1
  87. package/dist/sync/event-sync.d.ts.map +1 -1
  88. package/dist/sync/event-sync.js +149 -1
  89. package/dist/sync/event-sync.js.map +1 -1
  90. package/dist/sync/event-sync.test.js +142 -1
  91. package/dist/sync/event-sync.test.js.map +1 -1
  92. package/dist/sync/index.d.ts +2 -0
  93. package/dist/sync/index.d.ts.map +1 -1
  94. package/dist/sync/index.js +1 -0
  95. package/dist/sync/index.js.map +1 -1
  96. package/dist/sync/multipart-uploader.d.ts +99 -0
  97. package/dist/sync/multipart-uploader.d.ts.map +1 -0
  98. package/dist/sync/multipart-uploader.js +447 -0
  99. package/dist/sync/multipart-uploader.js.map +1 -0
  100. package/dist/sync/multipart-uploader.test.d.ts +2 -0
  101. package/dist/sync/multipart-uploader.test.d.ts.map +1 -0
  102. package/dist/sync/multipart-uploader.test.js +119 -0
  103. package/dist/sync/multipart-uploader.test.js.map +1 -0
  104. package/dist/sync/mutation-client.d.ts +82 -0
  105. package/dist/sync/mutation-client.d.ts.map +1 -0
  106. package/dist/sync/mutation-client.js +221 -0
  107. package/dist/sync/mutation-client.js.map +1 -0
  108. package/dist/sync/mutation-client.test.d.ts +2 -0
  109. package/dist/sync/mutation-client.test.d.ts.map +1 -0
  110. package/dist/sync/mutation-client.test.js +51 -0
  111. package/dist/sync/mutation-client.test.js.map +1 -0
  112. package/dist/sync/push-receiver.d.ts +45 -0
  113. package/dist/sync/push-receiver.d.ts.map +1 -1
  114. package/dist/sync/push-receiver.js +101 -0
  115. package/dist/sync/push-receiver.js.map +1 -1
  116. package/dist/sync/push-receiver.test.js +54 -2
  117. package/dist/sync/push-receiver.test.js.map +1 -1
  118. package/dist/sync/scope-inventory-client.d.ts +69 -0
  119. package/dist/sync/scope-inventory-client.d.ts.map +1 -0
  120. package/dist/sync/scope-inventory-client.js +210 -0
  121. package/dist/sync/scope-inventory-client.js.map +1 -0
  122. package/dist/sync/scope-inventory-client.test.d.ts +2 -0
  123. package/dist/sync/scope-inventory-client.test.d.ts.map +1 -0
  124. package/dist/sync/scope-inventory-client.test.js +94 -0
  125. package/dist/sync/scope-inventory-client.test.js.map +1 -0
  126. package/dist/sync/snapshot-client.d.ts +98 -0
  127. package/dist/sync/snapshot-client.d.ts.map +1 -0
  128. package/dist/sync/snapshot-client.js +402 -0
  129. package/dist/sync/snapshot-client.js.map +1 -0
  130. package/dist/sync/snapshot-client.test.d.ts +2 -0
  131. package/dist/sync/snapshot-client.test.d.ts.map +1 -0
  132. package/dist/sync/snapshot-client.test.js +169 -0
  133. package/dist/sync/snapshot-client.test.js.map +1 -0
  134. package/dist/sync/uploader-finalization.d.ts +97 -0
  135. package/dist/sync/uploader-finalization.d.ts.map +1 -0
  136. package/dist/sync/uploader-finalization.js +273 -0
  137. package/dist/sync/uploader-finalization.js.map +1 -0
  138. package/dist/sync/uploader-finalization.test.d.ts +2 -0
  139. package/dist/sync/uploader-finalization.test.d.ts.map +1 -0
  140. package/dist/sync/uploader-finalization.test.js +92 -0
  141. package/dist/sync/uploader-finalization.test.js.map +1 -0
  142. package/dist/telemetry.d.ts +11 -1
  143. package/dist/telemetry.d.ts.map +1 -1
  144. package/dist/telemetry.js +21 -2
  145. package/dist/telemetry.js.map +1 -1
  146. package/dist/telemetry.test.js +80 -0
  147. package/dist/telemetry.test.js.map +1 -1
  148. package/package.json +6 -1
  149. package/.claude/policies/hq-cloud-esm-cannot-spy-fs-builtins.md +0 -30
  150. package/.claude/policies/hq-cloud-strip-types-no-parameter-properties.md +0 -22
  151. package/.github/workflows/ci.yml +0 -84
  152. package/.github/workflows/publish.yml +0 -56
  153. package/.github/workflows/unreleased-commits-nag.yml +0 -256
  154. package/eslint.config.js +0 -67
  155. package/pnpm-workspace.yaml +0 -2
  156. package/scripts/presign-transport-e2e.mjs +0 -250
  157. package/scripts/vault-rebaseline.sh +0 -323
  158. package/scripts/vault-rescue.sh +0 -332
  159. package/src/active-company.test.ts +0 -188
  160. package/src/active-company.ts +0 -168
  161. package/src/agent-codex-instructions.test.ts +0 -332
  162. package/src/agent-codex-instructions.ts +0 -309
  163. package/src/auth.ts +0 -146
  164. package/src/backup-prune.test.ts +0 -98
  165. package/src/backup-prune.ts +0 -182
  166. package/src/bin/backup-prune-runner.ts +0 -33
  167. package/src/bin/rescue-runner.ts +0 -25
  168. package/src/bin/sync-runner-company.ts +0 -695
  169. package/src/bin/sync-runner-events.test.ts +0 -143
  170. package/src/bin/sync-runner-events.ts +0 -55
  171. package/src/bin/sync-runner-planning.test.ts +0 -311
  172. package/src/bin/sync-runner-planning.ts +0 -258
  173. package/src/bin/sync-runner-rollup.test.ts +0 -37
  174. package/src/bin/sync-runner-rollup.ts +0 -97
  175. package/src/bin/sync-runner-telemetry.ts +0 -15
  176. package/src/bin/sync-runner-watch-loop.ts +0 -1235
  177. package/src/bin/sync-runner-watch-routes.test.ts +0 -71
  178. package/src/bin/sync-runner-watch-routes.ts +0 -184
  179. package/src/bin/sync-runner.test.ts +0 -8767
  180. package/src/bin/sync-runner.ts +0 -2190
  181. package/src/cli/accept.ts +0 -124
  182. package/src/cli/conflict.ts +0 -119
  183. package/src/cli/doctor.test.ts +0 -581
  184. package/src/cli/doctor.ts +0 -642
  185. package/src/cli/index.ts +0 -49
  186. package/src/cli/invite.test.ts +0 -250
  187. package/src/cli/invite.ts +0 -214
  188. package/src/cli/promote.ts +0 -157
  189. package/src/cli/reindex.test.ts +0 -957
  190. package/src/cli/reindex.ts +0 -970
  191. package/src/cli/rescue-classify-ordering.test.ts +0 -548
  192. package/src/cli/rescue-clone-diagnostics.test.ts +0 -120
  193. package/src/cli/rescue-core.ts +0 -3011
  194. package/src/cli/rescue-drift-reconcile.test.ts +0 -179
  195. package/src/cli/rescue-drop-dir-symlink.test.ts +0 -224
  196. package/src/cli/rescue-exec-bit-preserve.test.ts +0 -187
  197. package/src/cli/rescue-hq-root-guard.test.ts +0 -232
  198. package/src/cli/rescue-journal-reconcile.test.ts +0 -215
  199. package/src/cli/rescue-mtime-preserve.test.ts +0 -203
  200. package/src/cli/rescue-settings-reconcile.test.ts +0 -583
  201. package/src/cli/rescue-snapshot.test.ts +0 -57
  202. package/src/cli/rescue-snapshot.ts +0 -51
  203. package/src/cli/rescue.reindex.test.ts +0 -63
  204. package/src/cli/rescue.test.ts +0 -131
  205. package/src/cli/rescue.ts +0 -182
  206. package/src/cli/share.test.ts +0 -7843
  207. package/src/cli/share.ts +0 -3663
  208. package/src/cli/sync-scope.test.ts +0 -652
  209. package/src/cli/sync.test.ts +0 -5207
  210. package/src/cli/sync.ts +0 -3470
  211. package/src/cli/tombstones.ts +0 -106
  212. package/src/cli/watch-event-push-conflict.test.ts +0 -234
  213. package/src/client-info.test.ts +0 -214
  214. package/src/client-info.ts +0 -121
  215. package/src/cognito-auth.test.ts +0 -712
  216. package/src/cognito-auth.ts +0 -1422
  217. package/src/company-resolver.test.ts +0 -618
  218. package/src/company-resolver.ts +0 -521
  219. package/src/context.test.ts +0 -583
  220. package/src/context.ts +0 -378
  221. package/src/daemon-worker.ts +0 -26
  222. package/src/daemon.ts +0 -99
  223. package/src/entity-resolver.test.ts +0 -315
  224. package/src/entity-resolver.ts +0 -180
  225. package/src/ignore.test.ts +0 -466
  226. package/src/ignore.ts +0 -469
  227. package/src/index.ts +0 -439
  228. package/src/journal.test.ts +0 -968
  229. package/src/journal.ts +0 -765
  230. package/src/lib/cloud-authoritative.test.ts +0 -45
  231. package/src/lib/cloud-authoritative.ts +0 -59
  232. package/src/lib/conflict-file.ts +0 -86
  233. package/src/lib/conflict-index.ts +0 -289
  234. package/src/lib/conflict.test.ts +0 -348
  235. package/src/lib/describe-error.test.ts +0 -100
  236. package/src/lib/describe-error.ts +0 -58
  237. package/src/lib/exit-codes.ts +0 -24
  238. package/src/lib/machine-id.test.ts +0 -231
  239. package/src/lib/machine-id.ts +0 -175
  240. package/src/lib/net-errors.test.ts +0 -65
  241. package/src/lib/net-errors.ts +0 -86
  242. package/src/lib/readlink-safe.test.ts +0 -43
  243. package/src/lib/readlink-safe.ts +0 -29
  244. package/src/local-path-codec.test.ts +0 -138
  245. package/src/local-path-codec.ts +0 -161
  246. package/src/machine-auth.test.ts +0 -1323
  247. package/src/manifest-reconcile.test.ts +0 -1123
  248. package/src/manifest-reconcile.ts +0 -518
  249. package/src/object-io.test.ts +0 -1221
  250. package/src/object-io.ts +0 -1306
  251. package/src/operation-lock.test.ts +0 -484
  252. package/src/operation-lock.ts +0 -680
  253. package/src/outcome-telemetry.test.ts +0 -498
  254. package/src/outcome-telemetry.ts +0 -639
  255. package/src/personal-vault-exclusions.test.ts +0 -308
  256. package/src/personal-vault-exclusions.ts +0 -354
  257. package/src/personal-vault.test.ts +0 -756
  258. package/src/personal-vault.ts +0 -496
  259. package/src/prefix-coalesce.test.ts +0 -240
  260. package/src/prefix-coalesce.ts +0 -273
  261. package/src/public-surface.test.ts +0 -117
  262. package/src/qmd-reindex.test.ts +0 -877
  263. package/src/qmd-reindex.ts +0 -842
  264. package/src/read-only-state-dir.test.ts +0 -188
  265. package/src/remote-pull.test.ts +0 -1130
  266. package/src/remote-pull.ts +0 -618
  267. package/src/s3.symlink-materialize.test.ts +0 -492
  268. package/src/s3.test.ts +0 -1789
  269. package/src/s3.ts +0 -1532
  270. package/src/schemas/signal-types.test.ts +0 -82
  271. package/src/schemas/signal-types.ts +0 -38
  272. package/src/schemas/source-channels.test.ts +0 -82
  273. package/src/schemas/source-channels.ts +0 -53
  274. package/src/scope-shrink.test.ts +0 -633
  275. package/src/scope-shrink.ts +0 -481
  276. package/src/signals/get.test.ts +0 -310
  277. package/src/signals/get.ts +0 -75
  278. package/src/signals/internals.ts +0 -195
  279. package/src/signals/list.test.ts +0 -420
  280. package/src/signals/list.ts +0 -79
  281. package/src/signals/parse.ts +0 -8
  282. package/src/signals/types.ts +0 -91
  283. package/src/skill-telemetry.test.ts +0 -1825
  284. package/src/skill-telemetry.ts +0 -1439
  285. package/src/sources/get.test.ts +0 -293
  286. package/src/sources/get.ts +0 -66
  287. package/src/sources/internals.ts +0 -198
  288. package/src/sources/list.test.ts +0 -402
  289. package/src/sources/list.ts +0 -84
  290. package/src/sources/parse.ts +0 -43
  291. package/src/sources/types.ts +0 -84
  292. package/src/sync/event-sync.test.ts +0 -594
  293. package/src/sync/event-sync.ts +0 -545
  294. package/src/sync/feature-flags.test.ts +0 -378
  295. package/src/sync/feature-flags.ts +0 -62
  296. package/src/sync/index.ts +0 -76
  297. package/src/sync/lease-client.test.ts +0 -128
  298. package/src/sync/lease-client.ts +0 -207
  299. package/src/sync/logger.test.ts +0 -242
  300. package/src/sync/logger.ts +0 -79
  301. package/src/sync/metrics.test.ts +0 -462
  302. package/src/sync/metrics.ts +0 -213
  303. package/src/sync/pull-scope.ts +0 -265
  304. package/src/sync/push-event.test.ts +0 -266
  305. package/src/sync/push-event.ts +0 -224
  306. package/src/sync/push-receiver.test.ts +0 -566
  307. package/src/sync/push-receiver.ts +0 -1048
  308. package/src/sync/push-transport.ts +0 -231
  309. package/src/sync/realtime-rollout.test.ts +0 -86
  310. package/src/sync/realtime-rollout.ts +0 -262
  311. package/src/sync/state-store.test.ts +0 -194
  312. package/src/sync/state-store.ts +0 -727
  313. package/src/sync-core.ts +0 -58
  314. package/src/sync-progress.test.ts +0 -94
  315. package/src/sync-progress.ts +0 -140
  316. package/src/telemetry-events.test.ts +0 -88
  317. package/src/telemetry-events.ts +0 -205
  318. package/src/telemetry.test.ts +0 -1280
  319. package/src/telemetry.ts +0 -1109
  320. package/src/types.ts +0 -314
  321. package/src/vault-client.test.ts +0 -1380
  322. package/src/vault-client.ts +0 -1694
  323. package/src/version.ts +0 -24
  324. package/src/watch-roots.test.ts +0 -278
  325. package/src/watch-roots.ts +0 -162
  326. package/src/watcher-event-gate.test.ts +0 -212
  327. package/src/watcher.test.ts +0 -1079
  328. package/src/watcher.ts +0 -1741
  329. package/test/e2e/sync/cross-tenant-isolation.test.ts +0 -630
  330. package/test/e2e/sync/skill-telemetry-oversized-transcript.test.ts +0 -124
  331. package/test/e2e/sync/transient-company-leg.test.ts +0 -384
  332. package/test/e2e/sync/windows-unreadable-link-leg.test.ts +0 -191
  333. package/test/e2e/watcher-real-chokidar.test.ts +0 -165
  334. package/test/e2e/watcher-recursive-backend.test.ts +0 -181
  335. package/test/e2e/watcher-scoped-coverage.test.ts +0 -381
  336. package/test/invite-flow.integration.test.ts +0 -244
  337. package/test/joiner-manifest-reconcile.integration.test.ts +0 -322
  338. package/test/share-sync.integration.test.ts +0 -213
  339. package/tsconfig.json +0 -19
  340. package/vitest.config.ts +0 -22
@@ -1,521 +0,0 @@
1
- /**
2
- * cwd → owning-company resolution for telemetry edge attribution
3
- * (surface-hq-console-telemetry US-002).
4
- *
5
- * Both telemetry collectors (`./telemetry.ts`, `./skill-telemetry.ts`) stamp an
6
- * optional `companyUid` on each event so the server can attribute usage to the
7
- * company that owns the repo the event was produced in. The mapping comes from
8
- * `<hqRoot>/companies/manifest.yaml`, whose shape is:
9
- *
10
- * companies:
11
- * indigo:
12
- * repos:
13
- * - repos/private/hq-cloud
14
- * - repos/public/hq-core
15
- * cloud_uid: cmp_01KQWRSQTCTES1SEFKXKDZAAK1
16
- * liverecover:
17
- * repos:
18
- * - repos/private/liverecover-site
19
- * # (no cloud_uid — not cloud-backed)
20
- *
21
- * companyUid source: the value stamped is the manifest's `cloud_uid`. That is a
22
- * `cmp_*` entity uid — the SAME value the server membership-keys on
23
- * (`membershipKey = '<personUid>#<companyUid>'`, where companyUid is the cmp_*
24
- * uid; see hq-pro `vault-service/handlers/usage.ts` + `skill-invocations.ts`).
25
- * So no slug→uid round-trip is needed: the manifest already carries the uid the
26
- * server expects. A company entry WITHOUT a `cloud_uid` (not cloud-backed, e.g.
27
- * `liverecover`/`temple`) contributes no mapping — its repos resolve to no
28
- * companyUid and the event is sent unattributed (field omitted).
29
- *
30
- * The reserved sentinel `unattributed` is NEVER produced here — "no match"
31
- * means "return undefined / omit the field", which the server treats as
32
- * unattributed/personal.
33
- *
34
- * Cost: `buildRepoCompanyMap()` parses the manifest ONCE per sync run; the
35
- * resulting map is reused for every event via `resolveCompanyForCwd()`. No
36
- * per-event manifest reads.
37
- *
38
- * Privacy: this module only ever returns a `cmp_*` uid (or undefined). It never
39
- * adds a raw path to any payload — the collectors add exactly one new field,
40
- * `companyUid`.
41
- */
42
-
43
- import { promises as fs } from "node:fs";
44
- import * as path from "node:path";
45
-
46
- import yaml from "js-yaml";
47
-
48
- /**
49
- * A repo-path → companyUid lookup, built once per run. Keys are ABSOLUTE,
50
- * normalized repo roots (`<hqRoot>/repos/private/hq-cloud`); values are the
51
- * owning company's `cmp_*` uid. An event `cwd` is attributed to the entry whose
52
- * repo root is the longest prefix of (or equal to) the cwd.
53
- */
54
- export interface RepoCompanyMap {
55
- /**
56
- * Absolute, normalized repo root → owning company `cmp_*` uid. `matchKey` is
57
- * the precomputed `canonicalPath(repoRoot)` used for the containment test, so
58
- * per-event matching stays a plain string compare.
59
- */
60
- entries: Array<{ repoRoot: string; matchKey: string; companyUid: string }>;
61
- /**
62
- * Company SLUG → owning company `cmp_*` uid, from the manifest keys. Backs
63
- * skill-based attribution: a company skill is invoked as `<slug>:<skill>`
64
- * (mirroring `companies/<slug>/skills/`), so its usage belongs to the company
65
- * named by that slug regardless of the cwd it ran in. Manifest keys are unique,
66
- * so a slug resolves to exactly one cloud-backed company here.
67
- */
68
- bySlug: Map<string, string>;
69
- /**
70
- * Whether the HQ filesystem folds case, PROBED at build time rather than
71
- * inferred from `process.platform`. APFS can be case-sensitive on macOS and
72
- * Windows supports per-directory case sensitivity, so the OS is only a hint;
73
- * getting this wrong in the folding direction risks cross-tenant
74
- * misattribution. Defaults to `false` (exact matching) when the probe fails.
75
- */
76
- foldsCase: boolean;
77
- /**
78
- * Match keys that two or more companies claimed. They are excluded from
79
- * `entries` (fail closed), and resolution must ALSO refuse to reach them via
80
- * the worktree fallback — otherwise an ambiguous `foo-wt-team` would collapse
81
- * to `foo` and hand the event to whichever company owns `foo`, quietly
82
- * undoing the protection.
83
- */
84
- ambiguous: Set<string>;
85
- }
86
-
87
- interface ManifestCompany {
88
- repos?: unknown;
89
- cloud_uid?: unknown;
90
- /** HQ-root-relative company directory (e.g. `companies/indigo`). */
91
- path?: unknown;
92
- }
93
-
94
- interface ManifestShape {
95
- companies?: Record<string, ManifestCompany>;
96
- }
97
-
98
- /** Drop a trailing slash (keeping a bare "/"). Mirrors the scope-path
99
- * normalization in `./skill-telemetry.ts` so prefix matching is consistent. */
100
- function normalizePath(p: string): string {
101
- return p.length > 1 ? p.replace(/\/+$/, "") : p;
102
- }
103
-
104
- /**
105
- * Canonical comparison key for a filesystem path.
106
- *
107
- * Both sides of the containment test (`repoRoot` and the event `cwd`) run
108
- * through this so a match does not hinge on the emitting machine's path
109
- * dialect. Real prod `cwd` values include Windows roots (`C:\Users\eggsa\HQ`)
110
- * alongside POSIX ones, and the manifest side is built with `path.resolve`,
111
- * which emits the HOST separator. Comparing those raw meant Windows clients
112
- * could never match.
113
- *
114
- * Two normalizations, both deliberate:
115
- * - Separators collapse to `/` so `C:\Users\x\HQ` and `C:/Users/x/HQ` agree.
116
- * - Case is folded only when the HQ filesystem actually folds case, as
117
- * PROBED by `probeCaseFolding`. Where it does, `.../HQ` and `.../hq` are
118
- * the same directory and not folding would drop real attribution. Where it
119
- * does not, they are different directories and folding could attribute a
120
- * cwd to a company that does not own it.
121
- *
122
- * Correct-but-unattributed always beats attributed-to-the-wrong-company, so the
123
- * probe defaults to NOT folding whenever it cannot determine the answer.
124
- */
125
- function canonicalPath(p: string, foldsCase: boolean): string {
126
- const normalized = normalizePath(p.replace(/\\/g, "/"));
127
- return foldsCase ? normalized.toLowerCase() : normalized;
128
- }
129
-
130
- /**
131
- * Determine whether the filesystem folds case for entries INSIDE `hqRoot`, by
132
- * asking it.
133
- *
134
- * `process.platform` is only a hint: APFS volumes can be created case-SENSITIVE
135
- * on macOS, and Windows supports per-directory case sensitivity — in both cases
136
- * the platform still reads `darwin`/`win32`. Since folding on a case-sensitive
137
- * filesystem risks cross-tenant misattribution, we probe instead of assume.
138
- *
139
- * The probe deliberately targets a CHILD of `hqRoot` (`companies/`, which must
140
- * exist — the manifest was just read from inside it) rather than `hqRoot`
141
- * itself. Under Windows per-directory case sensitivity, the lookup of a
142
- * directory's own name is governed by its PARENT, so probing `hqRoot` would
143
- * report the parent's behaviour while every path we actually compare lives
144
- * inside `hqRoot`. Probing a child measures the directory that matters.
145
- *
146
- * The probe stats the child with its basename case inverted and compares inode
147
- * identity. Any failure (missing, permission denied, no inode) returns `false`,
148
- * the conservative answer.
149
- */
150
- async function probeDirFoldsCase(dir: string): Promise<boolean> {
151
- const base = path.basename(dir);
152
- const flippedBase = base
153
- .split("")
154
- .map((c) => (c === c.toLowerCase() ? c.toUpperCase() : c.toLowerCase()))
155
- .join("");
156
- if (flippedBase === base) return false; // no cased characters to test with
157
- try {
158
- const [a, b] = await Promise.all([
159
- fs.stat(dir),
160
- fs.stat(path.join(path.dirname(dir), flippedBase)),
161
- ]);
162
- // Same file identity via the flipped-case name ⇒ this directory folds case.
163
- return a.ino !== 0 && a.ino === b.ino && a.dev === b.dev;
164
- } catch {
165
- return false;
166
- }
167
- }
168
-
169
- /**
170
- * Whether case may be folded for EVERY directory we actually match under.
171
- *
172
- * Windows configures case sensitivity PER DIRECTORY, so one probe cannot speak
173
- * for the whole tree: `companies/`, `repos/private/`, and an external symlink
174
- * target can each behave differently. Folding a path whose parent is
175
- * case-sensitive is the direction that risks stamping the wrong `companyUid`,
176
- * so this requires unanimity — if any probe says case-sensitive, or there is
177
- * nothing to probe, matching stays case-exact everywhere.
178
- *
179
- * Pass the ROOTS, not their parents. Case behaviour for a name is a property of
180
- * the directory the name is looked up IN, so flipping a root's basename
181
- * measures that root's PARENT — precisely the directory we match under.
182
- * Flipping the parent's own basename would instead measure the grandparent and
183
- * could report folding for a parent that is actually case-sensitive.
184
- *
185
- * The cost is one `stat` pair per distinct root, concurrently, once per run.
186
- */
187
- async function probeCaseFolding(roots: string[]): Promise<boolean> {
188
- const distinct = [...new Set(roots)];
189
- if (distinct.length === 0) return false;
190
- const results = await Promise.all(distinct.map(probeDirFoldsCase));
191
- return results.every(Boolean);
192
- }
193
-
194
- /**
195
- * Expand one manifest `repos` entry into the absolute roots it may live at.
196
- *
197
- * The manifest records repos as BARE NAMES (`hq-cloud`, `hq-console`) — see
198
- * `companies/manifest.yaml` — while the checkout lives under
199
- * `<hqRoot>/repos/{private,public}/<name>`. The original implementation did a
200
- * plain `path.resolve(hqRoot, repo)`, producing `<hqRoot>/hq-cloud`, a path
201
- * that exists for no repo. Every entry therefore matched no cwd and NO event
202
- * was ever attributed (verified in prod: 0 of 3200 sampled usage events across
203
- * 9 people carried a `companyUid`).
204
- *
205
- * An entry that already contains a separator is treated as an HQ-root-relative
206
- * path verbatim, so the documented `repos/private/<name>` form keeps working.
207
- */
208
- function repoRootCandidates(hqRoot: string, repo: string): string[] {
209
- const trimmed = repo.trim().replace(/^\.\//, "");
210
- if (trimmed.includes("/") || trimmed.includes("\\")) {
211
- return [path.resolve(hqRoot, trimmed)];
212
- }
213
- return [
214
- path.resolve(hqRoot, "repos", "private", trimmed),
215
- path.resolve(hqRoot, "repos", "public", trimmed),
216
- ];
217
- }
218
-
219
- /**
220
- * Sibling git-worktree checkouts of `repoRoot`, per HQ's `<repo>-wt-<branch>`
221
- * convention (e.g. `repos/private/hq-pro-wt-feature-x`).
222
- *
223
- * These matter a lot in practice: a large share of HQ work happens INSIDE a
224
- * worktree rather than the primary checkout, and the resolver's slash boundary
225
- * deliberately rejects `…/hq-pro-wt-feature-x` as a mere string-prefix sibling
226
- * of `…/hq-pro`. Without this expansion every worktree session stays
227
- * unattributed — which is exactly the emptiness this whole change is fixing.
228
- *
229
- * The prefix test honours `foldsCase`: `readdir` returns the casing stored on
230
- * disk while `name` comes from the manifest, so on a case-folding filesystem a
231
- * manifest entry `hq-cloud` must still find a checkout stored as
232
- * `HQ-Cloud-wt-x`. Using a case-sensitive comparison there would drop the
233
- * worktree even though the later match treats those casings as the same path.
234
- *
235
- * Best-effort: an unreadable parent directory yields no worktrees rather than
236
- * failing the run.
237
- */
238
- async function worktreeSiblings(repoRoot: string, foldsCase: boolean): Promise<string[]> {
239
- const parent = path.dirname(repoRoot);
240
- const prefix = `${path.basename(repoRoot)}-wt-`;
241
- const wanted = foldsCase ? prefix.toLowerCase() : prefix;
242
- try {
243
- const dirents = await fs.readdir(parent, { withFileTypes: true });
244
- return dirents
245
- .filter((d) => {
246
- if (!d.isDirectory() && !d.isSymbolicLink()) return false;
247
- const candidate = foldsCase ? d.name.toLowerCase() : d.name;
248
- return candidate.startsWith(wanted);
249
- })
250
- .map((d) => path.join(parent, d.name));
251
- } catch {
252
- return [];
253
- }
254
- }
255
-
256
- /**
257
- * The physical path behind `p`, when it differs from the lexical one.
258
- *
259
- * HQ's knowledge-repo layout uses symlinked checkouts, and a process launched
260
- * inside one can report the RESOLVED path as its `cwd`. Matching only the
261
- * lexical path would then miss the event entirely, so both are registered.
262
- * Returns `null` when the path does not exist or already is its own realpath.
263
- */
264
- /**
265
- * Whether `candidate` is strictly beneath `root`.
266
- *
267
- * Guards manifest-supplied paths. The HQ root itself is NOT inside itself: it
268
- * spans every tenant, so claiming it for one company would misattribute all of
269
- * them.
270
- */
271
- function isInsideRoot(root: string, candidate: string): boolean {
272
- const rel = path.relative(root, candidate);
273
- return rel.length > 0 && !rel.startsWith("..") && !path.isAbsolute(rel);
274
- }
275
-
276
- async function physicalAlias(p: string): Promise<string | null> {
277
- try {
278
- const real = await fs.realpath(p);
279
- return real !== p ? real : null;
280
- } catch {
281
- return null;
282
- }
283
- }
284
-
285
- /** Whether the path exists at all (used to keep absent candidates out of the
286
- * case probe, where they would look indistinguishable from case-sensitive). */
287
- async function physicalExists(p: string): Promise<boolean> {
288
- try {
289
- await fs.stat(p);
290
- return true;
291
- } catch {
292
- return false;
293
- }
294
- }
295
-
296
- /**
297
- * Parse `<hqRoot>/companies/manifest.yaml` ONCE and build the repo-path →
298
- * companyUid lookup. Best-effort: a missing/unparseable manifest yields an
299
- * empty map (every event then resolves to no company → unattributed), so a
300
- * telemetry run never fails on a manifest problem.
301
- *
302
- * Only companies that carry a `cloud_uid` (`cmp_*`) contribute entries — a
303
- * company without one is not cloud-backed and its repos stay unattributed.
304
- */
305
- export async function buildRepoCompanyMap(hqRoot: string): Promise<RepoCompanyMap> {
306
- const manifestPath = path.join(hqRoot, "companies", "manifest.yaml");
307
- const empty = (foldsCase = false): RepoCompanyMap => ({
308
- entries: [],
309
- bySlug: new Map(),
310
- foldsCase,
311
- ambiguous: new Set(),
312
- });
313
-
314
- let doc: ManifestShape;
315
- try {
316
- const raw = await fs.readFile(manifestPath, "utf-8");
317
- const parsed = yaml.load(raw);
318
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return empty();
319
- doc = parsed as ManifestShape;
320
- } catch {
321
- // Missing / unreadable / unparseable — no attribution this run.
322
- return empty();
323
- }
324
-
325
- const companies = doc.companies;
326
- if (!companies || typeof companies !== "object") return empty();
327
-
328
- const bySlug = new Map<string, string>();
329
- // Roots are collected BEFORE anything is canonicalized, because the case
330
- // probe needs to know which parent directories we will actually match under.
331
- const rootsToExpand: Array<{ root: string; companyUid: string }> = [];
332
-
333
- for (const [slug, company] of Object.entries(companies)) {
334
- if (!company || typeof company !== "object") continue;
335
- const cloudUid = company.cloud_uid;
336
- // Only cloud-backed companies (with a cmp_* uid) attribute events.
337
- if (typeof cloudUid !== "string" || !cloudUid.startsWith("cmp_")) continue;
338
- // Slug → uid for skill-based attribution (a `<slug>:<skill>` invocation).
339
- if (typeof slug === "string" && slug.length > 0) bySlug.set(slug, cloudUid);
340
-
341
- // The company's own HQ directory (`companies/<slug>`) is company-owned work
342
- // too — skills, knowledge, and project files all live there, and in prod a
343
- // large share of sessions run from inside the HQ root rather than a repo
344
- // checkout. Without this, that work is silently unattributed.
345
- const declaredPath =
346
- typeof company.path === "string" && company.path.trim().length > 0
347
- ? company.path.trim()
348
- : `companies/${slug}`;
349
- // `path` is documented as HQ-root-RELATIVE, but the manifest is synced and
350
- // hand-editable. An absolute path, `.`, or `..` would otherwise register the
351
- // HQ root — or an arbitrary directory — as owned by this company, stamping
352
- // its uid on every event beneath. Anything that escapes the root is dropped.
353
- const companyDir = path.resolve(hqRoot, declaredPath);
354
- if (isInsideRoot(hqRoot, companyDir)) {
355
- rootsToExpand.push({ root: companyDir, companyUid: cloudUid });
356
- }
357
-
358
- const repos = company.repos;
359
- if (!Array.isArray(repos)) continue;
360
- for (const repo of repos) {
361
- if (typeof repo !== "string" || repo.trim().length === 0) continue;
362
- // Manifest `repos` are BARE NAMES; expand to the real checkout roots
363
- // under `repos/{private,public}/`. See `repoRootCandidates`.
364
- for (const candidate of repoRootCandidates(hqRoot, repo)) {
365
- rootsToExpand.push({ root: candidate, companyUid: cloudUid });
366
- }
367
- }
368
- }
369
-
370
- // Discover BEFORE probing, so the probe can cover every directory we will
371
- // actually match under — including the parents of external symlink targets,
372
- // which are unknown until the aliases are resolved.
373
- //
374
- // Worktree discovery matches its prefix case-INSENSITIVELY here regardless of
375
- // the filesystem. That deliberately yields a superset: claims are still keyed
376
- // by the probed `foldsCase` below, so a case-variant that the filesystem
377
- // considers distinct simply becomes its own (possibly ambiguous) claim rather
378
- // than being silently missed at discovery time.
379
- const expansions = await Promise.all(
380
- rootsToExpand.map(async ({ root, companyUid }) => {
381
- const [worktrees, real] = await Promise.all([
382
- worktreeSiblings(root, true),
383
- physicalAlias(root),
384
- ]);
385
- const paths = real ? [...worktrees, real] : [...worktrees];
386
- const worktreeReals = await Promise.all(worktrees.map(physicalAlias));
387
- for (const wr of worktreeReals) if (wr) paths.push(wr);
388
- return { paths, companyUid };
389
- }),
390
- );
391
-
392
- const allPaths = [
393
- ...rootsToExpand.map((r) => r.root),
394
- ...expansions.flatMap((e) => e.paths),
395
- ];
396
-
397
- // Probe the actual filesystem rather than trusting `process.platform`, over
398
- // every path we may match under. Only paths that EXIST are probed: a bare
399
- // repo name expands to both `repos/private` and `repos/public` and normally
400
- // only one is real, so including the missing one would report
401
- // case-sensitivity for a directory that is simply absent and wrongly force
402
- // the whole map exact.
403
- const existing = await Promise.all(
404
- allPaths.map(async (p) => ((await physicalExists(p)) ? p : null)),
405
- );
406
- const foldsCase = await probeCaseFolding(existing.filter((p): p is string => p !== null));
407
-
408
- // Collect claims first, resolve conflicts second. A path claimed by two
409
- // DIFFERENT companies is ambiguous and must not be attributed to whichever
410
- // one the manifest happened to list first.
411
- const claims = new Map<string, { repoRoot: string; companyUids: Set<string> }>();
412
- const claim = (absPath: string, companyUid: string): void => {
413
- const repoRoot = normalizePath(absPath);
414
- const matchKey = canonicalPath(repoRoot, foldsCase);
415
- const existingClaim = claims.get(matchKey);
416
- if (existingClaim) existingClaim.companyUids.add(companyUid);
417
- else claims.set(matchKey, { repoRoot, companyUids: new Set([companyUid]) });
418
- };
419
- for (const { root, companyUid } of rootsToExpand) claim(root, companyUid);
420
- for (const { paths, companyUid } of expansions) {
421
- for (const p of paths) claim(p, companyUid);
422
- }
423
-
424
- const entries: Array<{ repoRoot: string; matchKey: string; companyUid: string }> = [];
425
- const ambiguous = new Set<string>();
426
- for (const [matchKey, { repoRoot, companyUids }] of claims) {
427
- // Fail CLOSED on ambiguity. Two companies listing the same bare repo name,
428
- // or two roots colliding under case folding, cannot be told apart here —
429
- // and attributing to the wrong tenant is far worse than not attributing.
430
- if (companyUids.size !== 1) {
431
- ambiguous.add(matchKey);
432
- continue;
433
- }
434
- entries.push({ repoRoot, matchKey, companyUid: [...companyUids][0] });
435
- }
436
-
437
- // Longest root first so the first containment match is the most specific
438
- // (handles nested layouts where one repo path prefixes another — notably a
439
- // worktree root, which must win over the company dir that contains it).
440
- entries.sort((a, b) => b.matchKey.length - a.matchKey.length);
441
- return { entries, bySlug, foldsCase, ambiguous };
442
- }
443
-
444
- /**
445
- * Resolve a skill INVOCATION to its owning company's `cmp_*` uid, or `undefined`
446
- * when the skill is not a cloud-backed company's skill.
447
- *
448
- * A company skill is invoked as `<slug>:<skill>` (the namespaced form of
449
- * `companies/<slug>/skills/<skill>`); the leading `<slug>` names the owning
450
- * company. We look that slug up in the manifest's slug→uid map, so a company
451
- * skill attributes to ITS company no matter which cwd it ran in — this is what
452
- * surfaces company-skill usage in the company console. A core / `personal:` /
453
- * `hq-pack-*:` / `anthropic-skills:` skill has no matching manifest company, so
454
- * it resolves to `undefined` (the cwd resolver remains the fallback).
455
- */
456
- export function resolveCompanyForSkill(
457
- skill: string | undefined,
458
- map: RepoCompanyMap,
459
- ): string | undefined {
460
- if (typeof skill !== "string") return undefined;
461
- const idx = skill.indexOf(":");
462
- if (idx <= 0) return undefined; // un-namespaced (core) skill
463
- return map.bySlug.get(skill.slice(0, idx));
464
- }
465
-
466
- /**
467
- * Resolve an event's `cwd` to the owning company's `cmp_*` uid, or `undefined`
468
- * when the cwd is inside no cloud-backed company repo (→ omit `companyUid`;
469
- * the server treats absence as unattributed/personal).
470
- *
471
- * Matching is by path containment: the cwd must equal a repo root or sit
472
- * beneath it (`<repoRoot>/...`). A trailing-slash boundary prevents a sibling
473
- * that merely shares a string prefix (`<repoRoot>-other`) from matching.
474
- * Entries are pre-sorted longest-first, so the first match is the most
475
- * specific.
476
- *
477
- * If nothing matches, one lexical retry strips HQ's `-wt-<branch>` worktree
478
- * suffix from the path. `buildRepoCompanyMap` discovers worktrees by reading
479
- * the directory, which only sees the ones that exist RIGHT NOW — but telemetry
480
- * is read from a cursor and a branch worktree is routinely deleted before the
481
- * next pass. Those pending rows still carry the worktree cwd, and because a
482
- * successful POST advances the cursor permanently, failing to resolve them
483
- * loses that attribution for good. The suffix is documented convention, so
484
- * deriving it from the recorded path needs no directory to still exist.
485
- */
486
- export function resolveCompanyForCwd(
487
- cwd: string | undefined,
488
- map: RepoCompanyMap,
489
- ): string | undefined {
490
- if (typeof cwd !== "string" || cwd.length === 0) return undefined;
491
- const foldsCase = map.foldsCase === true;
492
- const c = canonicalPath(cwd, foldsCase);
493
-
494
- const match = (candidate: string): string | undefined => {
495
- for (const { matchKey, companyUid } of map.entries) {
496
- if (candidate === matchKey || candidate.startsWith(`${matchKey}/`)) {
497
- return companyUid;
498
- }
499
- }
500
- return undefined;
501
- };
502
-
503
- const direct = match(c);
504
- if (direct) return direct;
505
-
506
- // The path may have been dropped for AMBIGUITY (claimed by two companies).
507
- // Falling back would collapse e.g. an ambiguous `foo-wt-team` to `foo` and
508
- // hand the event to whoever owns `foo`, silently undoing the fail-closed
509
- // protection. An ambiguous path stays unattributed, full stop.
510
- const ambiguous = map.ambiguous;
511
- if (ambiguous) {
512
- for (const key of ambiguous) {
513
- if (c === key || c.startsWith(`${key}/`)) return undefined;
514
- }
515
- }
516
-
517
- // `<...>/hq-pro-wt-feature-x/src` → `<...>/hq-pro/src`. Only the FIRST such
518
- // segment is rewritten; `-wt-` deeper in the path is left alone.
519
- const collapsed = c.replace(/^(.*?\/[^/]+?)-wt-[^/]*(\/.*)?$/, "$1$2");
520
- return collapsed !== c ? match(collapsed) : undefined;
521
- }