@indigoai-us/hq-cloud 6.15.78 → 6.15.80

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 (95) hide show
  1. package/dist/bin/area-ledger-migration.d.ts +13 -0
  2. package/dist/bin/area-ledger-migration.d.ts.map +1 -0
  3. package/dist/bin/area-ledger-migration.js +64 -0
  4. package/dist/bin/area-ledger-migration.js.map +1 -0
  5. package/dist/bin/area-ledger-migration.test.d.ts +2 -0
  6. package/dist/bin/area-ledger-migration.test.d.ts.map +1 -0
  7. package/dist/bin/area-ledger-migration.test.js +55 -0
  8. package/dist/bin/area-ledger-migration.test.js.map +1 -0
  9. package/dist/bin/sync-runner-company.d.ts +6 -0
  10. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  11. package/dist/bin/sync-runner-company.js +6 -0
  12. package/dist/bin/sync-runner-company.js.map +1 -1
  13. package/dist/bin/sync-runner-company.test.js +47 -0
  14. package/dist/bin/sync-runner-company.test.js.map +1 -1
  15. package/dist/cli/share.d.ts +8 -0
  16. package/dist/cli/share.d.ts.map +1 -1
  17. package/dist/cli/share.js +184 -10
  18. package/dist/cli/share.js.map +1 -1
  19. package/dist/cli/share.test.js +330 -7
  20. package/dist/cli/share.test.js.map +1 -1
  21. package/dist/cli/sync.d.ts +7 -0
  22. package/dist/cli/sync.d.ts.map +1 -1
  23. package/dist/cli/sync.js +11 -1
  24. package/dist/cli/sync.js.map +1 -1
  25. package/dist/cli/sync.test.js +25 -0
  26. package/dist/cli/sync.test.js.map +1 -1
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +3 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/index.test.js +11 -1
  32. package/dist/index.test.js.map +1 -1
  33. package/dist/journal-store.test.js +42 -1
  34. package/dist/journal-store.test.js.map +1 -1
  35. package/dist/journal.d.ts +13 -1
  36. package/dist/journal.d.ts.map +1 -1
  37. package/dist/journal.js +104 -3
  38. package/dist/journal.js.map +1 -1
  39. package/dist/sync/area-ledger-migration.d.ts +116 -0
  40. package/dist/sync/area-ledger-migration.d.ts.map +1 -0
  41. package/dist/sync/area-ledger-migration.js +505 -0
  42. package/dist/sync/area-ledger-migration.js.map +1 -0
  43. package/dist/sync/area-ledger-migration.test.d.ts +2 -0
  44. package/dist/sync/area-ledger-migration.test.d.ts.map +1 -0
  45. package/dist/sync/area-ledger-migration.test.js +274 -0
  46. package/dist/sync/area-ledger-migration.test.js.map +1 -0
  47. package/dist/sync/area-ledger.d.ts +93 -0
  48. package/dist/sync/area-ledger.d.ts.map +1 -0
  49. package/dist/sync/area-ledger.js +460 -0
  50. package/dist/sync/area-ledger.js.map +1 -0
  51. package/dist/sync/area-ledger.test.d.ts +2 -0
  52. package/dist/sync/area-ledger.test.d.ts.map +1 -0
  53. package/dist/sync/area-ledger.test.js +199 -0
  54. package/dist/sync/area-ledger.test.js.map +1 -0
  55. package/dist/sync/event-sync.d.ts +14 -4
  56. package/dist/sync/event-sync.d.ts.map +1 -1
  57. package/dist/sync/event-sync.js +177 -61
  58. package/dist/sync/event-sync.js.map +1 -1
  59. package/dist/sync/event-sync.test.js +140 -10
  60. package/dist/sync/event-sync.test.js.map +1 -1
  61. package/dist/sync/index.d.ts +2 -0
  62. package/dist/sync/index.d.ts.map +1 -1
  63. package/dist/sync/index.js +1 -0
  64. package/dist/sync/index.js.map +1 -1
  65. package/dist/sync/parallel-sync-coordinator.d.ts.map +1 -1
  66. package/dist/sync/parallel-sync-coordinator.js +22 -4
  67. package/dist/sync/parallel-sync-coordinator.js.map +1 -1
  68. package/dist/sync/parallel-sync-coordinator.test.js +45 -0
  69. package/dist/sync/parallel-sync-coordinator.test.js.map +1 -1
  70. package/dist/sync/reconcile-cursor.d.ts +97 -0
  71. package/dist/sync/reconcile-cursor.d.ts.map +1 -0
  72. package/dist/sync/reconcile-cursor.js +215 -0
  73. package/dist/sync/reconcile-cursor.js.map +1 -0
  74. package/dist/sync/reconcile-cursor.test.d.ts +2 -0
  75. package/dist/sync/reconcile-cursor.test.d.ts.map +1 -0
  76. package/dist/sync/reconcile-cursor.test.js +139 -0
  77. package/dist/sync/reconcile-cursor.test.js.map +1 -0
  78. package/dist/sync/state-store.d.ts +76 -0
  79. package/dist/sync/state-store.d.ts.map +1 -1
  80. package/dist/sync/state-store.js +317 -3
  81. package/dist/sync/state-store.js.map +1 -1
  82. package/dist/sync/state-store.test.js +39 -1
  83. package/dist/sync/state-store.test.js.map +1 -1
  84. package/dist/sync/sync-work.d.ts +2 -1
  85. package/dist/sync/sync-work.d.ts.map +1 -1
  86. package/dist/sync/sync-work.js.map +1 -1
  87. package/dist/types.d.ts +6 -0
  88. package/dist/types.d.ts.map +1 -1
  89. package/dist/vault-client.d.ts +9 -2
  90. package/dist/vault-client.d.ts.map +1 -1
  91. package/dist/vault-client.js +3 -2
  92. package/dist/vault-client.js.map +1 -1
  93. package/dist/vault-client.test.js +26 -0
  94. package/dist/vault-client.test.js.map +1 -1
  95. package/package.json +3 -2
@@ -56,6 +56,7 @@ import { share, ServerOwnedPushPathsError, UnreachablePushPathsError, Unregister
56
56
  import { deleteRemoteFile, downloadFile, headRemoteFile, primeObjectTransport, primeUploads, uploadFile, uploadSymlink, WindowsSymlinkPrivilegeError, } from "../s3.js";
57
57
  import { VaultAuthError, } from "../vault-client.js";
58
58
  import { hashFile, readJournal, writeJournal } from "../journal.js";
59
+ import { ReconcileCursorStore } from "../sync/reconcile-cursor.js";
59
60
  /**
60
61
  * Read a journal the way production does.
61
62
  *
@@ -144,6 +145,8 @@ describe("company skill identity preflight", () => {
144
145
  stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-share-skill-state-"));
145
146
  process.env.HQ_STATE_DIR = stateDir;
146
147
  vi.mocked(uploadFile).mockClear();
148
+ vi.mocked(headRemoteFile).mockReset();
149
+ vi.mocked(headRemoteFile).mockResolvedValue(null);
147
150
  });
148
151
  afterEach(() => {
149
152
  fs.rmSync(hqRoot, { recursive: true, force: true });
@@ -157,13 +160,16 @@ describe("company skill identity preflight", () => {
157
160
  return file;
158
161
  }
159
162
  function registered(input, uid = "skl_PREFLIGHT1") {
163
+ const stamped = parseRegisteredCompanySkillUid(input.content)
164
+ ? input.content
165
+ : input.content.replace("---\n", `---\nskill_uid: ${uid}\n`);
160
166
  return {
161
167
  ok: true,
162
168
  skillUid: uid,
163
169
  path: input.path,
164
170
  vaultPath: input.path,
165
- content: input.content.replace("---\n", `---\nskill_uid: ${uid}\n`),
166
- action: "created",
171
+ content: stamped,
172
+ action: parseRegisteredCompanySkillUid(input.content) ? "reused" : "created",
167
173
  accessPolicy: "open",
168
174
  skill: { name: "Call Prep", description: "Prepare calls", tags: [] },
169
175
  };
@@ -177,7 +183,7 @@ describe("company skill identity preflight", () => {
177
183
  entityContext: makeEntityContext(),
178
184
  registerCompanySkill: registrar,
179
185
  });
180
- expect(registrar).toHaveBeenCalledWith({
186
+ expect(registrar).toHaveBeenNthCalledWith(1, {
181
187
  companySlug: "acme",
182
188
  path: "skills/call-prep/SKILL.md",
183
189
  content: expect.stringContaining("name: Call Prep"),
@@ -185,6 +191,12 @@ describe("company skill identity preflight", () => {
185
191
  expect(parseRegisteredCompanySkillUid(fs.readFileSync(file, "utf8"))).toBe("skl_PREFLIGHT1");
186
192
  expect(result.filesUploaded).toBe(1);
187
193
  expect(uploadFile).toHaveBeenCalledWith(expect.anything(), file, "skills/call-prep/SKILL.md", undefined, expect.anything());
194
+ expect(registrar).toHaveBeenNthCalledWith(2, {
195
+ companySlug: "acme",
196
+ path: "skills/call-prep/SKILL.md",
197
+ content: expect.stringContaining("skill_uid: skl_PREFLIGHT1"),
198
+ landed: true,
199
+ });
188
200
  });
189
201
  it("fails closed without an authenticated registration seam", async () => {
190
202
  const file = createSkill("manual", "---\nname: Manual\ndescription: Missing id\n---\n");
@@ -196,17 +208,276 @@ describe("company skill identity preflight", () => {
196
208
  expect(uploadFile).not.toHaveBeenCalled();
197
209
  expect(parseRegisteredCompanySkillUid(fs.readFileSync(file, "utf8"))).toBeUndefined();
198
210
  });
199
- it("does not re-register an already stamped canonical skill", async () => {
211
+ it("reconciles an already stamped canonical skill after upload", async () => {
200
212
  const file = createSkill("existing", "---\nname: Existing\ndescription: Ready\nskill_uid: skl_EXISTING1\n---\n");
201
- const registrar = vi.fn();
213
+ const registrar = vi.fn(async (input) => registered(input, "skl_EXISTING1"));
202
214
  const result = await share({
203
215
  paths: [file],
204
216
  hqRoot,
205
217
  entityContext: makeEntityContext(),
206
218
  registerCompanySkill: registrar,
207
219
  });
220
+ expect(registrar).toHaveBeenCalledOnce();
221
+ expect(registrar).toHaveBeenCalledWith({
222
+ companySlug: "acme",
223
+ path: "skills/existing/SKILL.md",
224
+ content: expect.stringContaining("skill_uid: skl_EXISTING1"),
225
+ landed: true,
226
+ });
227
+ expect(result.filesUploaded).toBe(1);
228
+ });
229
+ it("does not reconcile bytes edited while the upload is in flight", async () => {
230
+ const original = "---\nname: Existing\ndescription: Ready\nskill_uid: skl_EXISTING1\ntags: [sales]\n---\n";
231
+ const file = createSkill("existing", original);
232
+ const registrar = vi.fn(async (input) => registered(input, "skl_EXISTING1"));
233
+ vi.mocked(uploadFile).mockImplementationOnce(async () => {
234
+ fs.writeFileSync(file, original.replace("sales", "reporting"));
235
+ return { etag: '"upload-etag"' };
236
+ });
237
+ const result = await share({
238
+ paths: [file],
239
+ hqRoot,
240
+ entityContext: makeEntityContext(),
241
+ registerCompanySkill: registrar,
242
+ });
243
+ expect(result.filesUploaded).toBe(1);
208
244
  expect(registrar).not.toHaveBeenCalled();
245
+ });
246
+ it("retries a failed post-upload metadata reconciliation without re-uploading", async () => {
247
+ const file = createSkill("existing", "---\nname: Existing\ndescription: Ready\nskill_uid: skl_EXISTING1\ntags: [sales]\n---\n");
248
+ const failedRegistrar = vi.fn(async () => {
249
+ throw new Error("registry temporarily unavailable");
250
+ });
251
+ const first = await share({
252
+ paths: [file],
253
+ hqRoot,
254
+ entityContext: makeEntityContext(),
255
+ registerCompanySkill: failedRegistrar,
256
+ });
257
+ expect(first.filesUploaded).toBe(1);
258
+ expect(readJournal("acme").files["skills/existing/SKILL.md"])
259
+ .toMatchObject({ skillMetadataPending: true });
260
+ vi.mocked(uploadFile).mockClear();
261
+ vi.mocked(headRemoteFile).mockResolvedValue({
262
+ lastModified: new Date(),
263
+ etag: '"upload-etag"',
264
+ size: fs.statSync(file).size,
265
+ });
266
+ const healingRegistrar = vi.fn(async (input) => registered(input, "skl_EXISTING1"));
267
+ const second = await share({
268
+ paths: [file],
269
+ hqRoot,
270
+ entityContext: makeEntityContext(),
271
+ registerCompanySkill: healingRegistrar,
272
+ skipUnchanged: true,
273
+ });
274
+ expect(second.filesUploaded).toBe(0);
275
+ expect(uploadFile).not.toHaveBeenCalled();
276
+ expect(healingRegistrar).toHaveBeenCalledWith(expect.objectContaining({ landed: true }));
277
+ expect(readJournal("acme").files["skills/existing/SKILL.md"]
278
+ ?.skillMetadataPending).toBeUndefined();
279
+ });
280
+ it("durably records pending skill metadata before starting reconciliation", async () => {
281
+ const file = createSkill("existing", "---\nname: Existing\ndescription: Ready\nskill_uid: skl_EXISTING1\ntags: [sales]\n---\n");
282
+ let pendingAtReconcile;
283
+ const registrar = vi.fn(async (input) => {
284
+ pendingAtReconcile = readJournal("acme").files["skills/existing/SKILL.md"];
285
+ return registered(input, "skl_EXISTING1");
286
+ });
287
+ await share({
288
+ paths: [file],
289
+ hqRoot,
290
+ entityContext: makeEntityContext(),
291
+ registerCompanySkill: registrar,
292
+ });
293
+ expect(registrar).toHaveBeenCalledOnce();
294
+ expect(pendingAtReconcile)
295
+ .toMatchObject({ skillMetadataPending: true, remoteEtag: "upload-etag" });
296
+ });
297
+ it("reconciles skill metadata when restart finds identical remote bytes without a journal", async () => {
298
+ const content = "---\nname: Existing\ndescription: Ready\nskill_uid: skl_EXISTING1\ntags: [sales]\n---\n";
299
+ const file = createSkill("existing", content);
300
+ vi.mocked(headRemoteFile).mockResolvedValueOnce({
301
+ lastModified: new Date(),
302
+ etag: '"remote-etag"',
303
+ size: Buffer.byteLength(content),
304
+ });
305
+ vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, destination) => {
306
+ fs.writeFileSync(destination, content);
307
+ return undefined;
308
+ });
309
+ const registrar = vi.fn(async (input) => registered(input, "skl_EXISTING1"));
310
+ const result = await share({
311
+ paths: [file],
312
+ hqRoot,
313
+ entityContext: makeEntityContext(),
314
+ registerCompanySkill: registrar,
315
+ });
316
+ expect(result.filesUploaded).toBe(0);
317
+ expect(uploadFile).not.toHaveBeenCalled();
318
+ expect(registrar).toHaveBeenCalledWith(expect.objectContaining({
319
+ path: "skills/existing/SKILL.md",
320
+ content,
321
+ landed: true,
322
+ }));
323
+ expect(readJournal("acme").files["skills/existing/SKILL.md"])
324
+ .not.toHaveProperty("skillMetadataPending", true);
325
+ });
326
+ it("restarts instead of reconciling skill bytes edited after the equality proof", async () => {
327
+ const original = "---\nname: Existing\ndescription: Ready\nskill_uid: skl_EXISTING1\ntags: [sales]\n---\n";
328
+ const changed = original.replace("[sales]", "[reporting]");
329
+ const file = createSkill("existing", original);
330
+ vi.mocked(headRemoteFile).mockResolvedValue({
331
+ lastModified: new Date(),
332
+ etag: '"remote-etag"',
333
+ size: Buffer.byteLength(original),
334
+ });
335
+ vi.mocked(downloadFile).mockImplementation(async (_ctx, _key, destination) => {
336
+ fs.writeFileSync(destination, original);
337
+ return undefined;
338
+ });
339
+ const realReadFileSync = fs.readFileSync.bind(fs);
340
+ let skillReads = 0;
341
+ const readSpy = vi.spyOn(fs, "readFileSync").mockImplementation(((pathname, options) => {
342
+ if (pathname === file && options === undefined) {
343
+ skillReads++;
344
+ if (skillReads === 2)
345
+ fs.writeFileSync(file, changed);
346
+ }
347
+ return realReadFileSync(pathname, options);
348
+ }));
349
+ const registrar = vi.fn(async (input) => registered(input, "skl_EXISTING1"));
350
+ let result;
351
+ try {
352
+ result = await share({
353
+ paths: [file],
354
+ hqRoot,
355
+ entityContext: makeEntityContext(),
356
+ registerCompanySkill: registrar,
357
+ onConflict: "overwrite",
358
+ });
359
+ }
360
+ finally {
361
+ readSpy.mockRestore();
362
+ }
363
+ expect(result.filesUploaded).toBe(1);
364
+ expect(uploadFile).toHaveBeenCalledOnce();
365
+ expect(registrar).toHaveBeenCalledOnce();
366
+ expect(registrar).toHaveBeenCalledWith(expect.objectContaining({
367
+ content: changed,
368
+ landed: true,
369
+ }));
370
+ });
371
+ it("reconciles current remote metadata when the landed upload was replaced", async () => {
372
+ const file = createSkill("existing", "---\nname: Existing\ndescription: Ready\nskill_uid: skl_EXISTING1\ntags: [sales]\n---\n");
373
+ const failedRegistrar = vi.fn(async () => {
374
+ throw new Error("registry temporarily unavailable");
375
+ });
376
+ await share({
377
+ paths: [file],
378
+ hqRoot,
379
+ entityContext: makeEntityContext(),
380
+ registerCompanySkill: failedRegistrar,
381
+ });
382
+ expect(readJournal("acme").files["skills/existing/SKILL.md"])
383
+ .toMatchObject({ skillMetadataPending: true, remoteEtag: "upload-etag" });
384
+ vi.mocked(uploadFile).mockClear();
385
+ vi.mocked(headRemoteFile).mockResolvedValue({
386
+ lastModified: new Date(),
387
+ etag: '"peer-overwrite"',
388
+ size: fs.statSync(file).size,
389
+ });
390
+ const remote = fs.readFileSync(file, "utf8").replace("[sales]", "[remote]");
391
+ vi.mocked(downloadFile).mockImplementation(async (_ctx, _key, destination) => {
392
+ fs.writeFileSync(destination, remote);
393
+ return {};
394
+ });
395
+ const healingRegistrar = vi.fn(async (input) => registered(input, "skl_EXISTING1"));
396
+ const result = await share({
397
+ paths: [file],
398
+ hqRoot,
399
+ entityContext: makeEntityContext(),
400
+ registerCompanySkill: healingRegistrar,
401
+ skipUnchanged: true,
402
+ onConflict: "keep",
403
+ });
404
+ expect(healingRegistrar).toHaveBeenCalledWith(expect.objectContaining({ content: remote, landed: true }));
405
+ expect(readJournal("acme").files["skills/existing/SKILL.md"]
406
+ ?.skillMetadataPending).toBeUndefined();
407
+ expect(result.conflictPaths).toEqual(["skills/existing/SKILL.md"]);
408
+ expect(fs.readFileSync(file, "utf8")).toBe(remote);
409
+ });
410
+ it("preserves entityContext-only uploads of already registered skills", async () => {
411
+ const file = createSkill("existing", "---\nname: Existing\nskill_uid: skl_EXISTING1\n---\n");
412
+ const result = await share({
413
+ paths: [file],
414
+ hqRoot,
415
+ entityContext: makeEntityContext(),
416
+ });
209
417
  expect(result.filesUploaded).toBe(1);
418
+ expect(readJournal("acme").files["skills/existing/SKILL.md"])
419
+ .not.toHaveProperty("skillMetadataPending", true);
420
+ });
421
+ it("invalidates a replaced remote baseline even when metadata recovery fails", async () => {
422
+ const file = createSkill("existing", "---\nname: Existing\nskill_uid: skl_EXISTING1\n---\n");
423
+ await share({
424
+ paths: [file],
425
+ hqRoot,
426
+ entityContext: makeEntityContext(),
427
+ registerCompanySkill: vi.fn(async () => {
428
+ throw new Error("registry unavailable");
429
+ }),
430
+ });
431
+ vi.mocked(headRemoteFile).mockResolvedValue({
432
+ lastModified: new Date(),
433
+ etag: '"peer-overwrite"',
434
+ size: fs.statSync(file).size,
435
+ });
436
+ vi.mocked(downloadFile).mockRejectedValueOnce(new Error("download failed"));
437
+ await expect(share({
438
+ paths: [file],
439
+ hqRoot,
440
+ entityContext: makeEntityContext(),
441
+ registerCompanySkill: vi.fn(async (input) => registered(input, "skl_EXISTING1")),
442
+ skipUnchanged: true,
443
+ })).rejects.toThrow("download failed");
444
+ expect(readJournal("acme").files["skills/existing/SKILL.md"])
445
+ .toBeUndefined();
446
+ });
447
+ it("uploads a local change before reconciling metadata when it lands during remote verification", async () => {
448
+ const original = "---\nname: Existing\ndescription: Ready\nskill_uid: skl_EXISTING1\ntags: [sales]\n---\n";
449
+ const file = createSkill("existing", original);
450
+ const failedRegistrar = vi.fn(async () => {
451
+ throw new Error("registry temporarily unavailable");
452
+ });
453
+ await share({
454
+ paths: [file],
455
+ hqRoot,
456
+ entityContext: makeEntityContext(),
457
+ registerCompanySkill: failedRegistrar,
458
+ });
459
+ vi.mocked(uploadFile).mockClear();
460
+ vi.mocked(headRemoteFile).mockImplementationOnce(async () => {
461
+ fs.writeFileSync(file, original.replace("[sales]", "[reporting]"));
462
+ return {
463
+ lastModified: new Date(),
464
+ etag: '"upload-etag"',
465
+ size: Buffer.byteLength(original),
466
+ };
467
+ });
468
+ const healingRegistrar = vi.fn(async (input) => registered(input, "skl_EXISTING1"));
469
+ await share({
470
+ paths: [file],
471
+ hqRoot,
472
+ entityContext: makeEntityContext(),
473
+ registerCompanySkill: healingRegistrar,
474
+ skipUnchanged: true,
475
+ });
476
+ expect(uploadFile).toHaveBeenCalledTimes(1);
477
+ expect(healingRegistrar).toHaveBeenCalledTimes(1);
478
+ expect(vi.mocked(uploadFile).mock.invocationCallOrder[0]).toBeLessThan(healingRegistrar.mock.invocationCallOrder[0]);
479
+ expect(readJournal("acme").files["skills/existing/SKILL.md"]
480
+ ?.skillMetadataPending).toBeUndefined();
210
481
  });
211
482
  it("recognizes quoted identities in CRLF frontmatter", () => {
212
483
  expect(parseRegisteredCompanySkillUid("---\r\nname: Existing\r\nskill_uid: \"skl_WINDOWS1\"\r\n---\r\n")).toBe("skl_WINDOWS1");
@@ -1710,7 +1981,7 @@ describe("share", () => {
1710
1981
  path: "v7b-priv.txt",
1711
1982
  target: "../target.md",
1712
1983
  }));
1713
- expect(events.some((e) => e.type === "error")).toBe(false);
1984
+ expect(events.filter((e) => e.type === "error")).toEqual([]);
1714
1985
  // The conflict is still surfaced; the failed adoption restores local.
1715
1986
  expect(result.conflictPaths).toEqual(["v7b-priv.txt"]);
1716
1987
  expect(fs.readFileSync(testFile, "utf-8")).toBe("local-collision-content");
@@ -7697,12 +7968,23 @@ describe("nested companies directories in a company vault", () => {
7697
7968
  const midPath = path.join(companyRoot, "audit", "companies", "notes.md");
7698
7969
  fs.mkdirSync(path.dirname(midPath), { recursive: true });
7699
7970
  fs.writeFileSync(midPath, "ordinary segment");
7971
+ const skillRegistrar = async (input) => ({
7972
+ ok: true,
7973
+ skillUid: "skl_SIGNALS1",
7974
+ path: input.path,
7975
+ vaultPath: input.path,
7976
+ content: input.content,
7977
+ action: "reused",
7978
+ accessPolicy: "preserved",
7979
+ skill: { name: "Signals", description: "", tags: [] },
7980
+ });
7700
7981
  const events = [];
7701
7982
  const result = await share({
7702
7983
  paths: [companyRoot],
7703
7984
  company: "acme",
7704
7985
  vaultConfig: mockConfig,
7705
7986
  hqRoot: tmpDir,
7987
+ registerCompanySkill: skillRegistrar,
7706
7988
  onEvent: (e) => events.push(e),
7707
7989
  });
7708
7990
  expect(result.aborted).toBe(false);
@@ -7716,7 +7998,7 @@ describe("nested companies directories in a company vault", () => {
7716
7998
  "audit/companies/notes.md",
7717
7999
  ]));
7718
8000
  expect(events.filter((e) => e.type === "skip-invalid-scoped-key")).toEqual([]);
7719
- expect(events.some((e) => e.type === "error")).toBe(false);
8001
+ expect(events.filter((e) => e.type === "error")).toEqual([]);
7720
8002
  });
7721
8003
  it("never plans an upload for macOS Finder Icon\\r files — silent skip, valid files still ship", async () => {
7722
8004
  // The incident: Finder writes an `Icon\r` file into every directory it
@@ -7993,4 +8275,45 @@ describe("defaultConsoleLogger — delete refusal wording per reason", () => {
7993
8275
  expect(lines.join("\n")).toContain("ignore-filtered=2, excluded-top-level=1");
7994
8276
  });
7995
8277
  });
8278
+ describe("share reconcile apply checkpoint", () => {
8279
+ let hqRoot;
8280
+ let stateDir;
8281
+ beforeEach(() => {
8282
+ hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-share-reconcile-"));
8283
+ stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-share-reconcile-state-"));
8284
+ process.env.HQ_STATE_DIR = stateDir;
8285
+ vi.mocked(uploadFile).mockReset();
8286
+ vi.mocked(uploadFile).mockRejectedValue(new Error("temporary upload failure"));
8287
+ vi.mocked(headRemoteFile).mockResolvedValue(null);
8288
+ vi.mocked(primeUploads).mockResolvedValue(undefined);
8289
+ vi.mocked(primeObjectTransport).mockResolvedValue(undefined);
8290
+ });
8291
+ afterEach(() => {
8292
+ fs.rmSync(hqRoot, { recursive: true, force: true });
8293
+ fs.rmSync(stateDir, { recursive: true, force: true });
8294
+ delete process.env.HQ_STATE_DIR;
8295
+ });
8296
+ it("withholds an apply checkpoint after a per-path upload failure", async () => {
8297
+ const localPath = path.join(hqRoot, "companies", "acme", "notes", "retry.md");
8298
+ fs.mkdirSync(path.dirname(localPath), { recursive: true });
8299
+ fs.writeFileSync(localPath, "retry me");
8300
+ const store = ReconcileCursorStore.open({
8301
+ rootDir: stateDir,
8302
+ journalSlug: "acme",
8303
+ layoutEpoch: 1,
8304
+ areaId: "root:notes",
8305
+ });
8306
+ const result = await share({
8307
+ paths: [localPath],
8308
+ hqRoot,
8309
+ entityContext: makeEntityContext(),
8310
+ reconcileApply: {
8311
+ store,
8312
+ cursor: { routerVersion: 1, epoch: "epoch-1", areaId: "root:notes", phase: "apply", journalAfterKey: "notes/retry.md" },
8313
+ },
8314
+ });
8315
+ expect(result.filesUploaded).toBe(0);
8316
+ expect(store.read().cursor).toBeUndefined();
8317
+ });
8318
+ });
7996
8319
  //# sourceMappingURL=share.test.js.map