@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,756 +0,0 @@
1
- /**
2
- * Unit tests for the personal-vault path-discovery helpers.
3
- *
4
- * These cover the column-anchored `cloud: true` exclusion-marker regex and
5
- * the `companies/{slug}/` enumeration filter — both are pure functions over
6
- * disk + caller-supplied option sets, so we exercise them against tmp
7
- * directories rather than mocking fs. Local (non-cloud) companies sync to
8
- * the personal vault by DEFAULT: a missing/markerless `company.yaml` is
9
- * INCLUDED; only an explicit `cloud: true` marker, membership via
10
- * `teamSyncedSlugs`, or a `cloud_uid: cmp_*` entry in
11
- * `companies/manifest.yaml` opts a company out.
12
- *
13
- * The sync-runner-level integration (team-synced slug derivation,
14
- * decommission of cloud-promoted companies, end-to-end share() invocation)
15
- * lives in `bin/sync-runner.test.ts` — tests G, H, I.
16
- */
17
-
18
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
19
- import * as fs from "fs";
20
- import * as os from "os";
21
- import * as path from "path";
22
- import {
23
- computeContinuityPointerPaths,
24
- computePersonalCompanySubdirs,
25
- computePersonalVaultPaths,
26
- readCloudBackedSlugsFromManifest,
27
- CONTINUITY_POINTER_REL,
28
- computeAgencySyncPaths,
29
- AGENCY_SYNC_REL,
30
- computeSessionLogsSyncPaths,
31
- SESSION_LOGS_SYNC_REL,
32
- PERSONAL_VAULT_COMPANY_EXCLUDED_SLUGS,
33
- PERSONAL_VAULT_EXCLUDED_TOP_LEVEL,
34
- isPersonalVaultDeletionExcluded,
35
- } from "./personal-vault.js";
36
-
37
- describe("personal-vault helpers", () => {
38
- let hqRoot: string;
39
-
40
- beforeEach(() => {
41
- hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-pv-test-"));
42
- });
43
-
44
- afterEach(() => {
45
- fs.rmSync(hqRoot, { recursive: true, force: true });
46
- });
47
-
48
- /** Helper: write `companies/{slug}/company.yaml` with arbitrary content. */
49
- function writeCompany(slug: string, yaml: string | null): void {
50
- const dir = path.join(hqRoot, "companies", slug);
51
- fs.mkdirSync(dir, { recursive: true });
52
- if (yaml !== null) fs.writeFileSync(path.join(dir, "company.yaml"), yaml);
53
- }
54
-
55
- /** Helper: relative-paths-sorted projection of an absolute-path list. */
56
- function rel(abs: string[]): string[] {
57
- return abs.map((p) => path.relative(hqRoot, p)).sort();
58
- }
59
-
60
- // ── Exported constants ─────────────────────────────────────────────────
61
- it("constants: PERSONAL_VAULT_EXCLUDED_TOP_LEVEL contains the canonical four names", () => {
62
- expect([...PERSONAL_VAULT_EXCLUDED_TOP_LEVEL].sort()).toEqual([
63
- ".git",
64
- "companies",
65
- "repos",
66
- "workspace",
67
- ]);
68
- });
69
-
70
- it("constants: PERSONAL_VAULT_COMPANY_EXCLUDED_SLUGS hard-lists `_template`", () => {
71
- expect(PERSONAL_VAULT_COMPANY_EXCLUDED_SLUGS).toContain("_template");
72
- });
73
-
74
- // ── computePersonalCompanySubdirs: default inclusion ───────────────────
75
- // Local companies sync to the personal vault by default. A missing or
76
- // markerless company.yaml is INCLUDED — only an explicit `cloud: true`
77
- // marker (or membership) opts a company OUT.
78
- it("default: includes a company with NO company.yaml", () => {
79
- writeCompany("foo", null);
80
- expect(rel(computePersonalCompanySubdirs(hqRoot))).toEqual([
81
- path.join("companies", "foo"),
82
- ]);
83
- });
84
-
85
- it("default: includes a company with an empty company.yaml", () => {
86
- writeCompany("foo", "");
87
- expect(rel(computePersonalCompanySubdirs(hqRoot))).toEqual([
88
- path.join("companies", "foo"),
89
- ]);
90
- });
91
-
92
- it("default: includes a company explicitly marked `cloud: false`", () => {
93
- writeCompany("foo", "cloud: false\nname: Foo\n");
94
- expect(rel(computePersonalCompanySubdirs(hqRoot))).toEqual([
95
- path.join("companies", "foo"),
96
- ]);
97
- });
98
-
99
- it("default: includes a company.yaml with no `cloud:` key at all", () => {
100
- writeCompany("foo", "name: Foo\nslug: foo\n");
101
- expect(rel(computePersonalCompanySubdirs(hqRoot))).toEqual([
102
- path.join("companies", "foo"),
103
- ]);
104
- });
105
-
106
- // ── computePersonalCompanySubdirs: `cloud: true` exclusion-marker ──────
107
- it("marker: excludes canonical `cloud: true` at column 0", () => {
108
- writeCompany("foo", "cloud: true\nname: Foo\n");
109
- expect(computePersonalCompanySubdirs(hqRoot)).toEqual([]);
110
- });
111
-
112
- it("marker: excludes `cloud:true` (no space after colon)", () => {
113
- writeCompany("foo", "cloud:true\n");
114
- expect(computePersonalCompanySubdirs(hqRoot)).toEqual([]);
115
- });
116
-
117
- it("marker: excludes with a trailing comment after `cloud: true`", () => {
118
- writeCompany("foo", "cloud: true # cloud-backed, synced to team bucket\n");
119
- expect(computePersonalCompanySubdirs(hqRoot)).toEqual([]);
120
- });
121
-
122
- it("marker: excludes with CRLF line endings", () => {
123
- writeCompany("foo", "cloud: true\r\nname: Foo\r\n");
124
- expect(computePersonalCompanySubdirs(hqRoot)).toEqual([]);
125
- });
126
-
127
- it("marker: excludes `cloud: true` even when it is NOT the first line", () => {
128
- writeCompany("foo", "name: Foo\nslug: foo\ncloud: true\n");
129
- expect(computePersonalCompanySubdirs(hqRoot)).toEqual([]);
130
- });
131
-
132
- // ── computePersonalCompanySubdirs: marker fails OPEN (still included) ───
133
- // A marker that isn't the canonical column-0 lowercase `cloud: true`
134
- // shape `/designate-team` emits does NOT exclude — the company stays in
135
- // the personal vault, which is the safe default.
136
- it("marker: nested `cloud: true` does NOT exclude (column-0 anchor)", () => {
137
- writeCompany("foo", "meta:\n cloud: true\n");
138
- expect(rel(computePersonalCompanySubdirs(hqRoot))).toEqual([
139
- path.join("companies", "foo"),
140
- ]);
141
- });
142
-
143
- it("marker: list-item style `- cloud: true` does NOT exclude", () => {
144
- writeCompany("foo", "tags:\n- cloud: true\n");
145
- expect(rel(computePersonalCompanySubdirs(hqRoot))).toEqual([
146
- path.join("companies", "foo"),
147
- ]);
148
- });
149
-
150
- it("marker: flow-mapping `{ cloud: true }` does NOT exclude", () => {
151
- writeCompany("foo", "{ cloud: true }\n");
152
- expect(rel(computePersonalCompanySubdirs(hqRoot))).toEqual([
153
- path.join("companies", "foo"),
154
- ]);
155
- });
156
-
157
- it("marker: YAML-alt boolean spellings (True, TRUE, yes, on) do NOT exclude", () => {
158
- for (const variant of ["cloud: True\n", "cloud: TRUE\n", "cloud: yes\n", "cloud: on\n"]) {
159
- writeCompany("foo", variant);
160
- expect(rel(computePersonalCompanySubdirs(hqRoot))).toEqual([
161
- path.join("companies", "foo"),
162
- ]);
163
- fs.rmSync(path.join(hqRoot, "companies", "foo"), { recursive: true });
164
- }
165
- });
166
-
167
- it("marker: quoted `cloud: \"true\"` does NOT exclude (quote breaks the literal-true match)", () => {
168
- writeCompany("foo", 'cloud: "true"\n');
169
- expect(rel(computePersonalCompanySubdirs(hqRoot))).toEqual([
170
- path.join("companies", "foo"),
171
- ]);
172
- });
173
-
174
- it("marker: `cloud: truthy` does NOT exclude (word-boundary guard)", () => {
175
- writeCompany("foo", "cloud: truthy\n");
176
- expect(rel(computePersonalCompanySubdirs(hqRoot))).toEqual([
177
- path.join("companies", "foo"),
178
- ]);
179
- });
180
-
181
- // ── Subdir-level filters ───────────────────────────────────────────────
182
- it("filter: skips _template even when it would otherwise be included", () => {
183
- writeCompany("_template", null);
184
- writeCompany("real", null);
185
- expect(rel(computePersonalCompanySubdirs(hqRoot))).toEqual([
186
- path.join("companies", "real"),
187
- ]);
188
- });
189
-
190
- it("filter: skips slugs in the teamSyncedSlugs set (cloud-backed)", () => {
191
- writeCompany("free", null);
192
- writeCompany("team-acme", null);
193
- expect(
194
- rel(computePersonalCompanySubdirs(hqRoot, new Set(["team-acme"]))),
195
- ).toEqual([path.join("companies", "free")]);
196
- });
197
-
198
- // Manifest cloud_uid is the durable cloud-backed signal when the runner
199
- // has no membership fanout (agent `--personal` ritual). Without this,
200
- // multi-GB team trees re-enter the personal vault every cycle.
201
- it("filter: skips slugs with cloud_uid in companies/manifest.yaml", () => {
202
- writeCompany("local-only", null);
203
- writeCompany("indigo", null); // no company.yaml cloud:true
204
- fs.mkdirSync(path.join(hqRoot, "companies"), { recursive: true });
205
- fs.writeFileSync(
206
- path.join(hqRoot, "companies", "manifest.yaml"),
207
- [
208
- "companies:",
209
- " indigo:",
210
- " name: Indigo",
211
- " cloud_uid: cmp_01KQ2RYAHXHDPCTY9GPQPTH3DG",
212
- " bucket_name: hq-vault-cmp-01kq2ryahxhdpcty9gpqpth3dg",
213
- " local-only:",
214
- " name: Local Only",
215
- "",
216
- ].join("\n"),
217
- );
218
- expect(rel(computePersonalCompanySubdirs(hqRoot))).toEqual([
219
- path.join("companies", "local-only"),
220
- ]);
221
- expect([...readCloudBackedSlugsFromManifest(hqRoot)].sort()).toEqual([
222
- "indigo",
223
- ]);
224
- });
225
-
226
- it("filter: missing/unreadable manifest fails open (local companies still included)", () => {
227
- writeCompany("foo", null);
228
- // no companies/manifest.yaml
229
- expect(rel(computePersonalCompanySubdirs(hqRoot))).toEqual([
230
- path.join("companies", "foo"),
231
- ]);
232
- expect(readCloudBackedSlugsFromManifest(hqRoot).size).toBe(0);
233
- });
234
-
235
- it("filter: skips stray files under companies/ (only directories considered)", () => {
236
- // Hypothetical: someone drops a README.md at companies/. Should not
237
- // crash, should not be enumerated as a company subdir.
238
- fs.mkdirSync(path.join(hqRoot, "companies"), { recursive: true });
239
- fs.writeFileSync(path.join(hqRoot, "companies", "README.md"), "# README");
240
- writeCompany("real", null);
241
- expect(rel(computePersonalCompanySubdirs(hqRoot))).toEqual([
242
- path.join("companies", "real"),
243
- ]);
244
- });
245
-
246
- it("filter: missing hqRoot/companies/ returns []", () => {
247
- // No `companies/` dir at all (fresh install or atypical layout).
248
- expect(computePersonalCompanySubdirs(hqRoot)).toEqual([]);
249
- });
250
-
251
- // ── computePersonalVaultPaths: top-level + company-subdir composition ──
252
- it("composition: local company subdirs are included by default (no opt-in flag)", () => {
253
- fs.mkdirSync(path.join(hqRoot, ".claude"));
254
- fs.mkdirSync(path.join(hqRoot, "knowledge"));
255
- writeCompany("foo", null);
256
- writeCompany("bar", "cloud: true\n");
257
-
258
- const out = rel(computePersonalVaultPaths(hqRoot));
259
- // Top-level entries present.
260
- expect(out).toContain(".claude");
261
- expect(out).toContain("knowledge");
262
- // Local (non-cloud) company included; cloud-marked one excluded.
263
- expect(out).toContain(path.join("companies", "foo"));
264
- expect(out).not.toContain(path.join("companies", "bar"));
265
- // The companies/ top-level entry itself is never present — only specific
266
- // eligible subdirs. Preserves the invariant that share() never walks
267
- // the whole companies/ tree under personalMode.
268
- expect(out).not.toContain("companies");
269
- });
270
-
271
- it("composition: teamSyncedSlugs filter excludes cloud-backed companies", () => {
272
- writeCompany("foo", null);
273
- writeCompany("synced", null);
274
-
275
- const out = rel(
276
- computePersonalVaultPaths(hqRoot, {
277
- teamSyncedSlugs: new Set(["synced"]),
278
- }),
279
- );
280
- expect(out).toContain(path.join("companies", "foo"));
281
- expect(out).not.toContain(path.join("companies", "synced"));
282
- });
283
-
284
- it("composition: missing hqRoot returns []", () => {
285
- fs.rmSync(hqRoot, { recursive: true });
286
- expect(computePersonalVaultPaths(hqRoot)).toEqual([]);
287
- });
288
-
289
- // ── companies/manifest.yaml inclusion ─────────────────────────────────
290
- //
291
- // The manifest is the routing source-of-truth (which slugs exist, which
292
- // are cloud-backed, what their company UIDs are). The vault needs a
293
- // single canonical copy: without it, a fresh machine joining the
294
- // personal vault has no way to enumerate which non-cloud companies it
295
- // expects to pull. Special-cased because companies/ itself is in
296
- // PERSONAL_VAULT_EXCLUDED_TOP_LEVEL.
297
- it("manifest: includes companies/manifest.yaml when present (default opts)", () => {
298
- fs.mkdirSync(path.join(hqRoot, "companies"));
299
- fs.writeFileSync(
300
- path.join(hqRoot, "companies", "manifest.yaml"),
301
- "companies:\n foo: {}\n",
302
- );
303
- fs.mkdirSync(path.join(hqRoot, ".claude"));
304
-
305
- const out = rel(computePersonalVaultPaths(hqRoot));
306
- expect(out).toContain(path.join("companies", "manifest.yaml"));
307
- expect(out).toContain(".claude");
308
- });
309
-
310
- it("manifest: included even when there are no eligible company subdirs", () => {
311
- fs.mkdirSync(path.join(hqRoot, "companies"));
312
- fs.writeFileSync(
313
- path.join(hqRoot, "companies", "manifest.yaml"),
314
- "companies: {}\n",
315
- );
316
-
317
- const out = rel(computePersonalVaultPaths(hqRoot));
318
- expect(out).toContain(path.join("companies", "manifest.yaml"));
319
- });
320
-
321
- it("manifest: absent file is not included (no crash, no phantom entry)", () => {
322
- fs.mkdirSync(path.join(hqRoot, "companies"));
323
- // No manifest.yaml written.
324
-
325
- const out = rel(computePersonalVaultPaths(hqRoot));
326
- expect(out).not.toContain(path.join("companies", "manifest.yaml"));
327
- });
328
-
329
- it("manifest: missing companies/ directory entirely is not an error", () => {
330
- // Fresh install or atypical layout: no companies/ dir at all.
331
- fs.mkdirSync(path.join(hqRoot, ".claude"));
332
- expect(() => computePersonalVaultPaths(hqRoot)).not.toThrow();
333
- const out = rel(computePersonalVaultPaths(hqRoot));
334
- expect(out).toContain(".claude");
335
- expect(out).not.toContain(path.join("companies", "manifest.yaml"));
336
- });
337
-
338
- it("manifest: ONLY manifest.yaml is special-cased — other companies/ root files stay excluded", () => {
339
- // Anti-test: only manifest.yaml gets the bypass. A stray README.md at
340
- // companies/ root or a directory called `_template` must still be
341
- // filtered (the latter by company-eligibility, the former silently).
342
- fs.mkdirSync(path.join(hqRoot, "companies"));
343
- fs.writeFileSync(
344
- path.join(hqRoot, "companies", "manifest.yaml"),
345
- "companies: {}\n",
346
- );
347
- fs.writeFileSync(
348
- path.join(hqRoot, "companies", "README.md"),
349
- "# companies tree",
350
- );
351
-
352
- const out = rel(computePersonalVaultPaths(hqRoot));
353
- expect(out).toContain(path.join("companies", "manifest.yaml"));
354
- expect(out).not.toContain(path.join("companies", "README.md"));
355
- });
356
-
357
- it("manifest: composes with local company subdirs", () => {
358
- fs.mkdirSync(path.join(hqRoot, "companies"));
359
- fs.writeFileSync(
360
- path.join(hqRoot, "companies", "manifest.yaml"),
361
- "companies:\n foo: {cloud_uid: null}\n team-acme: {cloud_uid: cmp_01XYZ}\n",
362
- );
363
- writeCompany("foo", null);
364
- writeCompany("team-acme", "cloud: true\n");
365
-
366
- const out = rel(
367
- computePersonalVaultPaths(hqRoot, {
368
- teamSyncedSlugs: new Set(["team-acme"]),
369
- }),
370
- );
371
- // Manifest + non-cloud subdir, NOT team-acme.
372
- expect(out).toContain(path.join("companies", "manifest.yaml"));
373
- expect(out).toContain(path.join("companies", "foo"));
374
- expect(out).not.toContain(path.join("companies", "team-acme"));
375
- });
376
-
377
- it("manifest: stray `manifest.yaml.conflict-*` litter is never enumerated as a company", () => {
378
- // Regression guard for DEV-1719's secondary symptom: when the personal
379
- // push accumulated `companies/manifest.yaml.conflict-<ts>.yaml` mirror
380
- // files, those FILES must never be mis-counted as company subdirs (they
381
- // are not directories) nor leak into the push set. The real manifest.yaml
382
- // is still included; the conflict litter is not.
383
- fs.mkdirSync(path.join(hqRoot, "companies"));
384
- fs.writeFileSync(
385
- path.join(hqRoot, "companies", "manifest.yaml"),
386
- "companies: {}\n",
387
- );
388
- fs.writeFileSync(
389
- path.join(hqRoot, "companies", "manifest.yaml.conflict-2026-06-08T10-00-00Z-ab12cd.yaml"),
390
- "companies: {}\n",
391
- );
392
-
393
- // Not treated as a company subdir (only directories are eligible).
394
- expect(computePersonalCompanySubdirs(hqRoot)).toEqual([]);
395
-
396
- // Push set carries the real manifest, never the conflict litter.
397
- const out = rel(computePersonalVaultPaths(hqRoot));
398
- expect(out).toContain(path.join("companies", "manifest.yaml"));
399
- expect(
400
- out.some((p) => p.includes("manifest.yaml.conflict-")),
401
- ).toBe(false);
402
- });
403
- });
404
-
405
- // ─────────────────────────────────────────────────────────────────────────
406
- // DEV-1778 — session-continuity pointer carve-out.
407
- //
408
- // `workspace/` is in PERSONAL_VAULT_EXCLUDED_TOP_LEVEL (machine-local by
409
- // design). The ONE exception is the session pointer
410
- // `workspace/threads/handoff.json` + the single thread file it references via
411
- // `thread_path`, so a `/handoff` on one machine reaches a second machine.
412
- // Mirrors the companies/manifest.yaml special-case: a narrow file-level
413
- // re-include that never broadens the rest of workspace/.
414
- // ─────────────────────────────────────────────────────────────────────────
415
- describe("personal-vault: continuity-pointer carve-out", () => {
416
- let hqRoot: string;
417
-
418
- beforeEach(() => {
419
- hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-cont-test-"));
420
- });
421
-
422
- afterEach(() => {
423
- fs.rmSync(hqRoot, { recursive: true, force: true });
424
- });
425
-
426
- /** Helper: relative-paths-sorted projection of an absolute-path list. */
427
- function rel(abs: string[]): string[] {
428
- return abs.map((p) => path.relative(hqRoot, p)).sort();
429
- }
430
-
431
- const THREADS = path.join("workspace", "threads");
432
-
433
- /** Write workspace/threads/handoff.json with the given object. */
434
- function writeHandoff(obj: unknown, raw?: string): void {
435
- const dir = path.join(hqRoot, THREADS);
436
- fs.mkdirSync(dir, { recursive: true });
437
- fs.writeFileSync(
438
- path.join(dir, "handoff.json"),
439
- raw !== undefined ? raw : JSON.stringify(obj),
440
- );
441
- }
442
-
443
- /** Write a thread file under workspace/threads/ and return its rel path. */
444
- function writeThread(name: string): string {
445
- const dir = path.join(hqRoot, THREADS);
446
- fs.mkdirSync(dir, { recursive: true });
447
- fs.writeFileSync(path.join(dir, name), "{}");
448
- return path.join(THREADS, name).split(path.sep).join("/");
449
- }
450
-
451
- it("constant: CONTINUITY_POINTER_REL is the canonical pointer path", () => {
452
- expect(CONTINUITY_POINTER_REL).toBe("workspace/threads/handoff.json");
453
- });
454
-
455
- it("includes handoff.json + the thread file it points to", () => {
456
- const threadRel = writeThread("T-20260617-1200-demo.json");
457
- writeHandoff({ thread_path: threadRel });
458
-
459
- expect(rel(computeContinuityPointerPaths(hqRoot))).toEqual(
460
- [
461
- path.join(THREADS, "T-20260617-1200-demo.json"),
462
- path.join(THREADS, "handoff.json"),
463
- ].sort(),
464
- );
465
- });
466
-
467
- it("carve-out composes into computePersonalVaultPaths", () => {
468
- const threadRel = writeThread("T-20260617-1200-demo.json");
469
- writeHandoff({ thread_path: threadRel });
470
- fs.mkdirSync(path.join(hqRoot, ".claude"));
471
-
472
- const out = rel(computePersonalVaultPaths(hqRoot));
473
- expect(out).toContain(path.join(THREADS, "handoff.json"));
474
- expect(out).toContain(path.join(THREADS, "T-20260617-1200-demo.json"));
475
- // Sibling top-level dirs still travel; the rest of workspace/ does not.
476
- expect(out).toContain(".claude");
477
- });
478
-
479
- it("does NOT broaden the rest of workspace/ (siblings/other threads stay local)", () => {
480
- const threadRel = writeThread("T-active.json");
481
- writeThread("T-old.json"); // an inactive thread — must NOT sync
482
- writeHandoff({ thread_path: threadRel });
483
- // Unrelated workspace litter that must stay machine-local.
484
- fs.mkdirSync(path.join(hqRoot, "workspace", "locks"), { recursive: true });
485
- fs.writeFileSync(path.join(hqRoot, "workspace", "locks", "x.lock"), "1");
486
- fs.writeFileSync(path.join(hqRoot, "workspace", "threads", "INDEX.md"), "#");
487
-
488
- const out = rel(computePersonalVaultPaths(hqRoot));
489
- expect(out).toContain(path.join(THREADS, "handoff.json"));
490
- expect(out).toContain(path.join(THREADS, "T-active.json"));
491
- expect(out).not.toContain(path.join(THREADS, "T-old.json"));
492
- expect(out).not.toContain(path.join(THREADS, "INDEX.md"));
493
- expect(out.some((p) => p.startsWith(path.join("workspace", "locks")))).toBe(
494
- false,
495
- );
496
- });
497
-
498
- it("handoff.json present but thread_path absent → only the pointer", () => {
499
- writeHandoff({ message: "no pointer field" });
500
- expect(rel(computeContinuityPointerPaths(hqRoot))).toEqual([
501
- path.join(THREADS, "handoff.json"),
502
- ]);
503
- });
504
-
505
- it("handoff.json points to a non-existent thread file → only the pointer", () => {
506
- writeHandoff({ thread_path: "workspace/threads/T-ghost.json" });
507
- expect(rel(computeContinuityPointerPaths(hqRoot))).toEqual([
508
- path.join(THREADS, "handoff.json"),
509
- ]);
510
- });
511
-
512
- it("no handoff.json at all → empty (nothing to carry)", () => {
513
- fs.mkdirSync(path.join(hqRoot, THREADS), { recursive: true });
514
- expect(computeContinuityPointerPaths(hqRoot)).toEqual([]);
515
- });
516
-
517
- it("malformed handoff.json → fail-soft to pointer only (no throw)", () => {
518
- writeHandoff(null, "{ this is : not json ]");
519
- expect(() => computeContinuityPointerPaths(hqRoot)).not.toThrow();
520
- expect(rel(computeContinuityPointerPaths(hqRoot))).toEqual([
521
- path.join(THREADS, "handoff.json"),
522
- ]);
523
- });
524
-
525
- // ── Security: thread_path must never escape workspace/threads/ ──────────
526
- it("rejects an absolute thread_path (only the pointer is included)", () => {
527
- // Put a real file at the absolute target so the ONLY thing rejecting it
528
- // is the containment guard, not a missing-file fallthrough.
529
- const evil = path.join(hqRoot, "secret.txt");
530
- fs.writeFileSync(evil, "top secret");
531
- writeHandoff({ thread_path: evil });
532
- expect(rel(computeContinuityPointerPaths(hqRoot))).toEqual([
533
- path.join(THREADS, "handoff.json"),
534
- ]);
535
- });
536
-
537
- it("rejects a traversal thread_path (../../.env) — no smuggling out of threads/", () => {
538
- fs.writeFileSync(path.join(hqRoot, ".env"), "SECRET=1");
539
- writeHandoff({ thread_path: "workspace/threads/../../.env" });
540
- const out = rel(computeContinuityPointerPaths(hqRoot));
541
- expect(out).toEqual([path.join(THREADS, "handoff.json")]);
542
- expect(out.some((p) => p.endsWith(".env"))).toBe(false);
543
- });
544
-
545
- it("rejects a thread_path under workspace/ but outside threads/", () => {
546
- fs.mkdirSync(path.join(hqRoot, "workspace", "reports"), {
547
- recursive: true,
548
- });
549
- fs.writeFileSync(
550
- path.join(hqRoot, "workspace", "reports", "secret.md"),
551
- "x",
552
- );
553
- writeHandoff({ thread_path: "workspace/reports/secret.md" });
554
- expect(rel(computeContinuityPointerPaths(hqRoot))).toEqual([
555
- path.join(THREADS, "handoff.json"),
556
- ]);
557
- });
558
-
559
- it("rejects a symlink that escapes threads/ (realpath containment)", () => {
560
- // A thread_path that names an in-threads symlink whose target is OUTSIDE
561
- // threads/ must be rejected by the realpath re-check.
562
- fs.writeFileSync(path.join(hqRoot, "outside.txt"), "secret");
563
- fs.mkdirSync(path.join(hqRoot, THREADS), { recursive: true });
564
- const link = path.join(hqRoot, THREADS, "escape.json");
565
- try {
566
- fs.symlinkSync(path.join(hqRoot, "outside.txt"), link);
567
- } catch {
568
- // Platform without symlink support — skip the assertion gracefully.
569
- return;
570
- }
571
- writeHandoff({ thread_path: "workspace/threads/escape.json" });
572
- expect(rel(computeContinuityPointerPaths(hqRoot))).toEqual([
573
- path.join(THREADS, "handoff.json"),
574
- ]);
575
- });
576
-
577
- it("R-F35: rejects threads symlinks below hqRoot while allowing symlinked hqRoot ancestors", () => {
578
- const privateThreads = path.join(hqRoot, "repos", "private");
579
- fs.mkdirSync(privateThreads, { recursive: true });
580
- fs.mkdirSync(path.join(hqRoot, "workspace"), { recursive: true });
581
- fs.writeFileSync(path.join(privateThreads, "handoff.json"), JSON.stringify({
582
- thread_path: "workspace/threads/T-private.json",
583
- }));
584
- fs.writeFileSync(path.join(privateThreads, "T-private.json"), "{}");
585
- fs.symlinkSync(privateThreads, path.join(hqRoot, THREADS), "dir");
586
-
587
- expect(rel(computeContinuityPointerPaths(hqRoot))).toEqual([
588
- path.join(THREADS, "handoff.json"),
589
- ]);
590
-
591
- const realParent = fs.mkdtempSync(path.join(os.tmpdir(), "hq-cont-real-"));
592
- const aliasParent = fs.mkdtempSync(path.join(os.tmpdir(), "hq-cont-alias-"));
593
- const realRoot = path.join(realParent, "root");
594
- const aliasRoot = path.join(aliasParent, "root-link");
595
- try {
596
- fs.mkdirSync(path.join(realRoot, THREADS), { recursive: true });
597
- fs.writeFileSync(
598
- path.join(realRoot, THREADS, "handoff.json"),
599
- JSON.stringify({ thread_path: "workspace/threads/T-active.json" }),
600
- );
601
- fs.writeFileSync(path.join(realRoot, THREADS, "T-active.json"), "{}");
602
- fs.symlinkSync(realRoot, aliasRoot, "dir");
603
-
604
- const aliasRel = computeContinuityPointerPaths(aliasRoot)
605
- .map((p) => path.relative(aliasRoot, p))
606
- .sort();
607
- expect(aliasRel).toEqual(
608
- [
609
- path.join(THREADS, "T-active.json"),
610
- path.join(THREADS, "handoff.json"),
611
- ].sort(),
612
- );
613
- } finally {
614
- fs.rmSync(realParent, { recursive: true, force: true });
615
- fs.rmSync(aliasParent, { recursive: true, force: true });
616
- }
617
- });
618
- });
619
-
620
- // ─────────────────────────────────────────────────────────────────────────
621
- // workspace/agency carve-out — sync the agency-team workspace across machines.
622
- //
623
- // Like the continuity pointer, this pierces the workspace/ top-level exclusion
624
- // for ONE subtree: workspace/agency/<company>/<team>/ (hq-pack-agency chat
625
- // inboxes + roster/state), so an agency team picked up on a second machine sees
626
- // the same conversation it had on the first. The rest of workspace/ stays local.
627
- // ─────────────────────────────────────────────────────────────────────────
628
- describe("personal-vault: agency-workspace carve-out", () => {
629
- let hqRoot: string;
630
- beforeEach(() => {
631
- hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-agency-test-"));
632
- });
633
- afterEach(() => {
634
- fs.rmSync(hqRoot, { recursive: true, force: true });
635
- });
636
- const relAll = (abs: string[]): string[] =>
637
- abs.map((p) => path.relative(hqRoot, p)).sort();
638
-
639
- it("constant: AGENCY_SYNC_REL is workspace/agency", () => {
640
- expect(AGENCY_SYNC_REL).toBe("workspace/agency");
641
- });
642
-
643
- it("includes workspace/agency when it exists", () => {
644
- fs.mkdirSync(path.join(hqRoot, "workspace", "agency", "acme", "growth"), {
645
- recursive: true,
646
- });
647
- expect(relAll(computeAgencySyncPaths(hqRoot))).toEqual([
648
- path.join("workspace", "agency"),
649
- ]);
650
- });
651
-
652
- it("empty when workspace/agency is absent (fail-soft)", () => {
653
- fs.mkdirSync(path.join(hqRoot, "workspace"), { recursive: true });
654
- expect(computeAgencySyncPaths(hqRoot)).toEqual([]);
655
- });
656
-
657
- it("composes into computePersonalVaultPaths without broadening workspace/", () => {
658
- fs.mkdirSync(path.join(hqRoot, "workspace", "agency", "acme", "growth"), {
659
- recursive: true,
660
- });
661
- fs.mkdirSync(path.join(hqRoot, "workspace", "locks"), { recursive: true });
662
- fs.writeFileSync(path.join(hqRoot, "workspace", "locks", "x.lock"), "1");
663
- fs.mkdirSync(path.join(hqRoot, ".claude"), { recursive: true });
664
-
665
- const out = relAll(computePersonalVaultPaths(hqRoot));
666
- expect(out).toContain(path.join("workspace", "agency"));
667
- expect(out).toContain(".claude");
668
- expect(out.some((p) => p.startsWith(path.join("workspace", "locks")))).toBe(
669
- false,
670
- );
671
- });
672
- });
673
-
674
- // ─────────────────────────────────────────────────────────────────────────
675
- // Session-logs carve-out (workspace/.session-logs) — reindex-captured Claude
676
- // Code transcripts pierce the workspace/ exclusion and round-trip to the vault.
677
- // ─────────────────────────────────────────────────────────────────────────
678
- describe("personal-vault: session-logs carve-out", () => {
679
- let hqRoot: string;
680
- beforeEach(() => {
681
- hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-sesslog-test-"));
682
- });
683
- afterEach(() => {
684
- fs.rmSync(hqRoot, { recursive: true, force: true });
685
- });
686
- const relAll = (abs: string[]): string[] =>
687
- abs.map((p) => path.relative(hqRoot, p)).sort();
688
-
689
- it("constant: SESSION_LOGS_SYNC_REL is workspace/.session-logs", () => {
690
- expect(SESSION_LOGS_SYNC_REL).toBe("workspace/.session-logs");
691
- });
692
-
693
- it("includes workspace/.session-logs when it exists", () => {
694
- fs.mkdirSync(path.join(hqRoot, "workspace", ".session-logs", "tool-results"), {
695
- recursive: true,
696
- });
697
- expect(relAll(computeSessionLogsSyncPaths(hqRoot))).toEqual([
698
- path.join("workspace", ".session-logs"),
699
- ]);
700
- });
701
-
702
- it("empty when workspace/.session-logs is absent (fail-soft)", () => {
703
- fs.mkdirSync(path.join(hqRoot, "workspace"), { recursive: true });
704
- expect(computeSessionLogsSyncPaths(hqRoot)).toEqual([]);
705
- });
706
-
707
- it("composes into computePersonalVaultPaths without broadening workspace/", () => {
708
- fs.mkdirSync(path.join(hqRoot, "workspace", ".session-logs"), { recursive: true });
709
- fs.writeFileSync(
710
- path.join(hqRoot, "workspace", ".session-logs", "session.jsonl"),
711
- "l\n",
712
- );
713
- // A sibling under workspace/ that must STAY local.
714
- fs.mkdirSync(path.join(hqRoot, "workspace", "locks"), { recursive: true });
715
- fs.writeFileSync(path.join(hqRoot, "workspace", "locks", "x.lock"), "1");
716
- fs.mkdirSync(path.join(hqRoot, ".claude"), { recursive: true });
717
-
718
- const out = relAll(computePersonalVaultPaths(hqRoot));
719
- expect(out).toContain(path.join("workspace", ".session-logs"));
720
- expect(out).toContain(".claude");
721
- // The carve-out is exactly the .session-logs subtree — no other workspace/
722
- // sibling is pulled in.
723
- expect(out.some((p) => p.startsWith(path.join("workspace", "locks")))).toBe(
724
- false,
725
- );
726
- });
727
- });
728
-
729
- // ─────────────────────────────────────────────────────────────────────────
730
- describe("personal-vault: isPersonalVaultDeletionExcluded", () => {
731
- it("protects release scaffold + reindex-generated skill wrappers", () => {
732
- expect(isPersonalVaultDeletionExcluded("core")).toBe(true);
733
- expect(isPersonalVaultDeletionExcluded("core/policies/x.md")).toBe(true);
734
- expect(isPersonalVaultDeletionExcluded(".claude/skills")).toBe(true);
735
- expect(isPersonalVaultDeletionExcluded(".claude/skills/core:demo/SKILL.md")).toBe(true);
736
- });
737
-
738
- it("protects reindex-generated command links from delete-propagation", () => {
739
- // The generated <ns>:<cmd>.md links reindex surfaces — recreated locally by
740
- // `hq reindex`, so a local absence is not user intent.
741
- expect(isPersonalVaultDeletionExcluded(".claude/commands/core:demo.md")).toBe(true);
742
- expect(isPersonalVaultDeletionExcluded(".claude/commands/indigo:signals.md")).toBe(true);
743
- expect(isPersonalVaultDeletionExcluded(".claude/commands/hq-pack-demo:hello.md")).toBe(true);
744
- // Win32-encoded colon form is protected too.
745
- expect(isPersonalVaultDeletionExcluded(".claude/commands/indigo%3Asignals.md")).toBe(true);
746
- });
747
-
748
- it("does NOT protect hand-authored native commands (deletion IS user intent)", () => {
749
- // A non-namespaced native slash command the user authored directly under
750
- // .claude/commands/ must still delete-propagate, unlike the skill farm.
751
- expect(isPersonalVaultDeletionExcluded(".claude/commands/deploy.md")).toBe(false);
752
- expect(isPersonalVaultDeletionExcluded(".claude/commands")).toBe(false);
753
- // And an ordinary user file is never protected.
754
- expect(isPersonalVaultDeletionExcluded("knowledge/notes.md")).toBe(false);
755
- });
756
- });