@indigoai-us/hq-cloud 6.15.0 → 6.15.2

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 (329) hide show
  1. package/dist/bin/sync-mutation.d.ts +16 -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 +165 -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/sync.d.ts +35 -0
  34. package/dist/cli/sync.d.ts.map +1 -1
  35. package/dist/cli/sync.js +100 -0
  36. package/dist/cli/sync.js.map +1 -1
  37. package/dist/cli/sync.test.js +85 -1
  38. package/dist/cli/sync.test.js.map +1 -1
  39. package/dist/index.d.ts +2 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +1 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/skill-telemetry.d.ts +6 -0
  44. package/dist/skill-telemetry.d.ts.map +1 -1
  45. package/dist/skill-telemetry.js +14 -2
  46. package/dist/skill-telemetry.js.map +1 -1
  47. package/dist/skill-telemetry.test.js +79 -0
  48. package/dist/skill-telemetry.test.js.map +1 -1
  49. package/dist/sync/candidate-uploader.d.ts +88 -0
  50. package/dist/sync/candidate-uploader.d.ts.map +1 -0
  51. package/dist/sync/candidate-uploader.js +212 -0
  52. package/dist/sync/candidate-uploader.js.map +1 -0
  53. package/dist/sync/candidate-uploader.test.d.ts +2 -0
  54. package/dist/sync/candidate-uploader.test.d.ts.map +1 -0
  55. package/dist/sync/candidate-uploader.test.js +132 -0
  56. package/dist/sync/candidate-uploader.test.js.map +1 -0
  57. package/dist/sync/delta-client.d.ts +73 -0
  58. package/dist/sync/delta-client.d.ts.map +1 -0
  59. package/dist/sync/delta-client.js +201 -0
  60. package/dist/sync/delta-client.js.map +1 -0
  61. package/dist/sync/delta-client.test.d.ts +2 -0
  62. package/dist/sync/delta-client.test.d.ts.map +1 -0
  63. package/dist/sync/delta-client.test.js +97 -0
  64. package/dist/sync/delta-client.test.js.map +1 -0
  65. package/dist/sync/durable-apply.d.ts +76 -0
  66. package/dist/sync/durable-apply.d.ts.map +1 -0
  67. package/dist/sync/durable-apply.js +530 -0
  68. package/dist/sync/durable-apply.js.map +1 -0
  69. package/dist/sync/durable-apply.test.d.ts +2 -0
  70. package/dist/sync/durable-apply.test.d.ts.map +1 -0
  71. package/dist/sync/durable-apply.test.js +180 -0
  72. package/dist/sync/durable-apply.test.js.map +1 -0
  73. package/dist/sync/event-sync.d.ts +33 -1
  74. package/dist/sync/event-sync.d.ts.map +1 -1
  75. package/dist/sync/event-sync.js +149 -1
  76. package/dist/sync/event-sync.js.map +1 -1
  77. package/dist/sync/event-sync.test.js +142 -1
  78. package/dist/sync/event-sync.test.js.map +1 -1
  79. package/dist/sync/index.d.ts +2 -0
  80. package/dist/sync/index.d.ts.map +1 -1
  81. package/dist/sync/index.js +1 -0
  82. package/dist/sync/index.js.map +1 -1
  83. package/dist/sync/multipart-uploader.d.ts +99 -0
  84. package/dist/sync/multipart-uploader.d.ts.map +1 -0
  85. package/dist/sync/multipart-uploader.js +447 -0
  86. package/dist/sync/multipart-uploader.js.map +1 -0
  87. package/dist/sync/multipart-uploader.test.d.ts +2 -0
  88. package/dist/sync/multipart-uploader.test.d.ts.map +1 -0
  89. package/dist/sync/multipart-uploader.test.js +119 -0
  90. package/dist/sync/multipart-uploader.test.js.map +1 -0
  91. package/dist/sync/mutation-client.d.ts +85 -0
  92. package/dist/sync/mutation-client.d.ts.map +1 -0
  93. package/dist/sync/mutation-client.js +245 -0
  94. package/dist/sync/mutation-client.js.map +1 -0
  95. package/dist/sync/mutation-client.test.d.ts +2 -0
  96. package/dist/sync/mutation-client.test.d.ts.map +1 -0
  97. package/dist/sync/mutation-client.test.js +51 -0
  98. package/dist/sync/mutation-client.test.js.map +1 -0
  99. package/dist/sync/push-receiver.d.ts +45 -0
  100. package/dist/sync/push-receiver.d.ts.map +1 -1
  101. package/dist/sync/push-receiver.js +101 -0
  102. package/dist/sync/push-receiver.js.map +1 -1
  103. package/dist/sync/push-receiver.test.js +54 -2
  104. package/dist/sync/push-receiver.test.js.map +1 -1
  105. package/dist/sync/scope-inventory-client.d.ts +69 -0
  106. package/dist/sync/scope-inventory-client.d.ts.map +1 -0
  107. package/dist/sync/scope-inventory-client.js +210 -0
  108. package/dist/sync/scope-inventory-client.js.map +1 -0
  109. package/dist/sync/scope-inventory-client.test.d.ts +2 -0
  110. package/dist/sync/scope-inventory-client.test.d.ts.map +1 -0
  111. package/dist/sync/scope-inventory-client.test.js +94 -0
  112. package/dist/sync/scope-inventory-client.test.js.map +1 -0
  113. package/dist/sync/snapshot-client.d.ts +98 -0
  114. package/dist/sync/snapshot-client.d.ts.map +1 -0
  115. package/dist/sync/snapshot-client.js +402 -0
  116. package/dist/sync/snapshot-client.js.map +1 -0
  117. package/dist/sync/snapshot-client.test.d.ts +2 -0
  118. package/dist/sync/snapshot-client.test.d.ts.map +1 -0
  119. package/dist/sync/snapshot-client.test.js +169 -0
  120. package/dist/sync/snapshot-client.test.js.map +1 -0
  121. package/dist/sync/uploader-finalization.d.ts +97 -0
  122. package/dist/sync/uploader-finalization.d.ts.map +1 -0
  123. package/dist/sync/uploader-finalization.js +273 -0
  124. package/dist/sync/uploader-finalization.js.map +1 -0
  125. package/dist/sync/uploader-finalization.test.d.ts +2 -0
  126. package/dist/sync/uploader-finalization.test.d.ts.map +1 -0
  127. package/dist/sync/uploader-finalization.test.js +92 -0
  128. package/dist/sync/uploader-finalization.test.js.map +1 -0
  129. package/dist/telemetry.d.ts +11 -1
  130. package/dist/telemetry.d.ts.map +1 -1
  131. package/dist/telemetry.js +21 -2
  132. package/dist/telemetry.js.map +1 -1
  133. package/dist/telemetry.test.js +80 -0
  134. package/dist/telemetry.test.js.map +1 -1
  135. package/package.json +6 -1
  136. package/.claude/policies/hq-cloud-esm-cannot-spy-fs-builtins.md +0 -30
  137. package/.claude/policies/hq-cloud-strip-types-no-parameter-properties.md +0 -22
  138. package/.github/workflows/ci.yml +0 -84
  139. package/.github/workflows/publish.yml +0 -56
  140. package/.github/workflows/unreleased-commits-nag.yml +0 -256
  141. package/eslint.config.js +0 -67
  142. package/pnpm-workspace.yaml +0 -2
  143. package/scripts/presign-transport-e2e.mjs +0 -250
  144. package/scripts/vault-rebaseline.sh +0 -323
  145. package/scripts/vault-rescue.sh +0 -332
  146. package/src/active-company.test.ts +0 -188
  147. package/src/active-company.ts +0 -168
  148. package/src/agent-codex-instructions.test.ts +0 -332
  149. package/src/agent-codex-instructions.ts +0 -309
  150. package/src/auth.ts +0 -146
  151. package/src/backup-prune.test.ts +0 -98
  152. package/src/backup-prune.ts +0 -182
  153. package/src/bin/backup-prune-runner.ts +0 -33
  154. package/src/bin/rescue-runner.ts +0 -25
  155. package/src/bin/sync-runner-company.ts +0 -695
  156. package/src/bin/sync-runner-events.test.ts +0 -143
  157. package/src/bin/sync-runner-events.ts +0 -55
  158. package/src/bin/sync-runner-planning.test.ts +0 -311
  159. package/src/bin/sync-runner-planning.ts +0 -258
  160. package/src/bin/sync-runner-rollup.test.ts +0 -37
  161. package/src/bin/sync-runner-rollup.ts +0 -97
  162. package/src/bin/sync-runner-telemetry.ts +0 -15
  163. package/src/bin/sync-runner-watch-loop.ts +0 -1235
  164. package/src/bin/sync-runner-watch-routes.test.ts +0 -71
  165. package/src/bin/sync-runner-watch-routes.ts +0 -184
  166. package/src/bin/sync-runner.test.ts +0 -8767
  167. package/src/bin/sync-runner.ts +0 -2190
  168. package/src/cli/accept.ts +0 -124
  169. package/src/cli/conflict.ts +0 -119
  170. package/src/cli/doctor.test.ts +0 -581
  171. package/src/cli/doctor.ts +0 -642
  172. package/src/cli/index.ts +0 -49
  173. package/src/cli/invite.test.ts +0 -250
  174. package/src/cli/invite.ts +0 -214
  175. package/src/cli/promote.ts +0 -157
  176. package/src/cli/reindex-knowledge.test.ts +0 -307
  177. package/src/cli/reindex-knowledge.ts +0 -450
  178. package/src/cli/reindex.test.ts +0 -957
  179. package/src/cli/reindex.ts +0 -979
  180. package/src/cli/rescue-classify-ordering.test.ts +0 -548
  181. package/src/cli/rescue-clone-diagnostics.test.ts +0 -120
  182. package/src/cli/rescue-core.ts +0 -3011
  183. package/src/cli/rescue-drift-reconcile.test.ts +0 -179
  184. package/src/cli/rescue-drop-dir-symlink.test.ts +0 -224
  185. package/src/cli/rescue-exec-bit-preserve.test.ts +0 -187
  186. package/src/cli/rescue-hq-root-guard.test.ts +0 -232
  187. package/src/cli/rescue-journal-reconcile.test.ts +0 -215
  188. package/src/cli/rescue-mtime-preserve.test.ts +0 -203
  189. package/src/cli/rescue-settings-reconcile.test.ts +0 -637
  190. package/src/cli/rescue-snapshot.test.ts +0 -57
  191. package/src/cli/rescue-snapshot.ts +0 -51
  192. package/src/cli/rescue.reindex.test.ts +0 -63
  193. package/src/cli/rescue.test.ts +0 -131
  194. package/src/cli/rescue.ts +0 -182
  195. package/src/cli/share.test.ts +0 -7843
  196. package/src/cli/share.ts +0 -3663
  197. package/src/cli/sync-scope.test.ts +0 -652
  198. package/src/cli/sync.test.ts +0 -5207
  199. package/src/cli/sync.ts +0 -3470
  200. package/src/cli/tombstones.ts +0 -106
  201. package/src/cli/watch-event-push-conflict.test.ts +0 -234
  202. package/src/client-info.test.ts +0 -214
  203. package/src/client-info.ts +0 -121
  204. package/src/cognito-auth.test.ts +0 -712
  205. package/src/cognito-auth.ts +0 -1422
  206. package/src/company-resolver.test.ts +0 -618
  207. package/src/company-resolver.ts +0 -521
  208. package/src/context.test.ts +0 -583
  209. package/src/context.ts +0 -378
  210. package/src/daemon-worker.ts +0 -26
  211. package/src/daemon.ts +0 -99
  212. package/src/entity-resolver.test.ts +0 -315
  213. package/src/entity-resolver.ts +0 -180
  214. package/src/ignore.test.ts +0 -466
  215. package/src/ignore.ts +0 -469
  216. package/src/index.ts +0 -439
  217. package/src/journal.test.ts +0 -968
  218. package/src/journal.ts +0 -765
  219. package/src/lib/cloud-authoritative.test.ts +0 -45
  220. package/src/lib/cloud-authoritative.ts +0 -59
  221. package/src/lib/conflict-file.ts +0 -86
  222. package/src/lib/conflict-index.ts +0 -289
  223. package/src/lib/conflict.test.ts +0 -348
  224. package/src/lib/describe-error.test.ts +0 -100
  225. package/src/lib/describe-error.ts +0 -58
  226. package/src/lib/exit-codes.ts +0 -24
  227. package/src/lib/machine-id.test.ts +0 -231
  228. package/src/lib/machine-id.ts +0 -175
  229. package/src/lib/net-errors.test.ts +0 -65
  230. package/src/lib/net-errors.ts +0 -86
  231. package/src/lib/readlink-safe.test.ts +0 -43
  232. package/src/lib/readlink-safe.ts +0 -29
  233. package/src/local-path-codec.test.ts +0 -138
  234. package/src/local-path-codec.ts +0 -161
  235. package/src/machine-auth.test.ts +0 -1323
  236. package/src/manifest-reconcile.test.ts +0 -1123
  237. package/src/manifest-reconcile.ts +0 -518
  238. package/src/object-io.test.ts +0 -1221
  239. package/src/object-io.ts +0 -1306
  240. package/src/operation-lock.test.ts +0 -484
  241. package/src/operation-lock.ts +0 -680
  242. package/src/outcome-telemetry.test.ts +0 -498
  243. package/src/outcome-telemetry.ts +0 -639
  244. package/src/personal-vault-exclusions.test.ts +0 -308
  245. package/src/personal-vault-exclusions.ts +0 -354
  246. package/src/personal-vault.test.ts +0 -756
  247. package/src/personal-vault.ts +0 -496
  248. package/src/prefix-coalesce.test.ts +0 -240
  249. package/src/prefix-coalesce.ts +0 -273
  250. package/src/public-surface.test.ts +0 -117
  251. package/src/qmd-reindex.test.ts +0 -877
  252. package/src/qmd-reindex.ts +0 -842
  253. package/src/read-only-state-dir.test.ts +0 -188
  254. package/src/remote-pull.test.ts +0 -1130
  255. package/src/remote-pull.ts +0 -618
  256. package/src/s3.symlink-materialize.test.ts +0 -492
  257. package/src/s3.test.ts +0 -1789
  258. package/src/s3.ts +0 -1532
  259. package/src/schemas/signal-types.test.ts +0 -82
  260. package/src/schemas/signal-types.ts +0 -38
  261. package/src/schemas/source-channels.test.ts +0 -82
  262. package/src/schemas/source-channels.ts +0 -53
  263. package/src/scope-shrink.test.ts +0 -633
  264. package/src/scope-shrink.ts +0 -481
  265. package/src/signals/get.test.ts +0 -310
  266. package/src/signals/get.ts +0 -75
  267. package/src/signals/internals.ts +0 -195
  268. package/src/signals/list.test.ts +0 -420
  269. package/src/signals/list.ts +0 -79
  270. package/src/signals/parse.ts +0 -8
  271. package/src/signals/types.ts +0 -91
  272. package/src/skill-telemetry.test.ts +0 -1825
  273. package/src/skill-telemetry.ts +0 -1439
  274. package/src/sources/get.test.ts +0 -293
  275. package/src/sources/get.ts +0 -66
  276. package/src/sources/internals.ts +0 -198
  277. package/src/sources/list.test.ts +0 -402
  278. package/src/sources/list.ts +0 -84
  279. package/src/sources/parse.ts +0 -43
  280. package/src/sources/types.ts +0 -84
  281. package/src/sync/event-sync.test.ts +0 -594
  282. package/src/sync/event-sync.ts +0 -545
  283. package/src/sync/feature-flags.test.ts +0 -378
  284. package/src/sync/feature-flags.ts +0 -62
  285. package/src/sync/index.ts +0 -76
  286. package/src/sync/lease-client.test.ts +0 -128
  287. package/src/sync/lease-client.ts +0 -207
  288. package/src/sync/logger.test.ts +0 -242
  289. package/src/sync/logger.ts +0 -79
  290. package/src/sync/metrics.test.ts +0 -462
  291. package/src/sync/metrics.ts +0 -213
  292. package/src/sync/pull-scope.ts +0 -265
  293. package/src/sync/push-event.test.ts +0 -266
  294. package/src/sync/push-event.ts +0 -224
  295. package/src/sync/push-receiver.test.ts +0 -566
  296. package/src/sync/push-receiver.ts +0 -1048
  297. package/src/sync/push-transport.ts +0 -231
  298. package/src/sync/realtime-rollout.test.ts +0 -86
  299. package/src/sync/realtime-rollout.ts +0 -262
  300. package/src/sync/state-store.test.ts +0 -194
  301. package/src/sync/state-store.ts +0 -727
  302. package/src/sync-core.ts +0 -58
  303. package/src/sync-progress.test.ts +0 -94
  304. package/src/sync-progress.ts +0 -140
  305. package/src/telemetry-events.test.ts +0 -88
  306. package/src/telemetry-events.ts +0 -205
  307. package/src/telemetry.test.ts +0 -1280
  308. package/src/telemetry.ts +0 -1109
  309. package/src/types.ts +0 -314
  310. package/src/vault-client.test.ts +0 -1380
  311. package/src/vault-client.ts +0 -1694
  312. package/src/version.ts +0 -24
  313. package/src/watch-roots.test.ts +0 -278
  314. package/src/watch-roots.ts +0 -162
  315. package/src/watcher-event-gate.test.ts +0 -212
  316. package/src/watcher.test.ts +0 -1079
  317. package/src/watcher.ts +0 -1741
  318. package/test/e2e/sync/cross-tenant-isolation.test.ts +0 -630
  319. package/test/e2e/sync/skill-telemetry-oversized-transcript.test.ts +0 -124
  320. package/test/e2e/sync/transient-company-leg.test.ts +0 -384
  321. package/test/e2e/sync/windows-unreadable-link-leg.test.ts +0 -191
  322. package/test/e2e/watcher-real-chokidar.test.ts +0 -165
  323. package/test/e2e/watcher-recursive-backend.test.ts +0 -181
  324. package/test/e2e/watcher-scoped-coverage.test.ts +0 -381
  325. package/test/invite-flow.integration.test.ts +0 -244
  326. package/test/joiner-manifest-reconcile.integration.test.ts +0 -322
  327. package/test/share-sync.integration.test.ts +0 -213
  328. package/tsconfig.json +0 -19
  329. 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
- }