@indigoai-us/hq-cloud 6.12.4 → 6.12.5
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.
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +11 -0
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +13 -0
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +42 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/lib/cloud-authoritative.d.ts +45 -0
- package/dist/lib/cloud-authoritative.d.ts.map +1 -0
- package/dist/lib/cloud-authoritative.js +58 -0
- package/dist/lib/cloud-authoritative.js.map +1 -0
- package/dist/lib/cloud-authoritative.test.d.ts +2 -0
- package/dist/lib/cloud-authoritative.test.d.ts.map +1 -0
- package/dist/lib/cloud-authoritative.test.js +42 -0
- package/dist/lib/cloud-authoritative.test.js.map +1 -0
- package/package.json +1 -1
- package/src/cli/share.ts +11 -0
- package/src/cli/sync.test.ts +52 -0
- package/src/cli/sync.ts +14 -0
- package/src/lib/cloud-authoritative.test.ts +45 -0
- package/src/lib/cloud-authoritative.ts +59 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { isCloudAuthoritative } from "./cloud-authoritative.js";
|
|
3
|
+
describe("isCloudAuthoritative", () => {
|
|
4
|
+
it("matches server-regenerated root files (both path forms)", () => {
|
|
5
|
+
for (const p of [
|
|
6
|
+
"companies/indigo/company-brief.md",
|
|
7
|
+
"company-brief.md",
|
|
8
|
+
"companies/acme/board.json",
|
|
9
|
+
"board.json",
|
|
10
|
+
]) {
|
|
11
|
+
expect(isCloudAuthoritative(p)).toBe(true);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
it("matches the pipeline-authored dirs", () => {
|
|
15
|
+
for (const p of [
|
|
16
|
+
"companies/indigo/ontology/entities/concept/grok.md",
|
|
17
|
+
"ontology/entities/person/x.md",
|
|
18
|
+
"companies/indigo/signals/slack-dm/action_item/a.md",
|
|
19
|
+
"signals/decision/b.md",
|
|
20
|
+
"companies/indigo/sources/slack/T1-2.md",
|
|
21
|
+
"sources/meetings/m.md",
|
|
22
|
+
]) {
|
|
23
|
+
expect(isCloudAuthoritative(p)).toBe(true);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
it("does NOT match client-owned files", () => {
|
|
27
|
+
for (const p of [
|
|
28
|
+
"companies/indigo/skills/work-mesh/SKILL.md",
|
|
29
|
+
"companies/indigo/projects/company-agents/release/index.html",
|
|
30
|
+
"companies/indigo/workers/hq-notify/emails/notice.template.html",
|
|
31
|
+
"companies/indigo/knowledge/readme.md",
|
|
32
|
+
"company-brief.md.conflict-2026-06-18T00-00-00Z-501bc8.md", // a mirror, not the brief
|
|
33
|
+
"docs/handoff.md",
|
|
34
|
+
]) {
|
|
35
|
+
expect(isCloudAuthoritative(p)).toBe(false);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
it("normalizes windows separators", () => {
|
|
39
|
+
expect(isCloudAuthoritative("companies\\indigo\\ontology\\entities\\x.md")).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
//# sourceMappingURL=cloud-authoritative.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloud-authoritative.test.js","sourceRoot":"","sources":["../../src/lib/cloud-authoritative.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAEhE,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,KAAK,MAAM,CAAC,IAAI;YACd,mCAAmC;YACnC,kBAAkB;YAClB,2BAA2B;YAC3B,YAAY;SACb,EAAE,CAAC;YACF,MAAM,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7C,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,KAAK,MAAM,CAAC,IAAI;YACd,oDAAoD;YACpD,+BAA+B;YAC/B,oDAAoD;YACpD,uBAAuB;YACvB,wCAAwC;YACxC,uBAAuB;SACxB,EAAE,CAAC;YACF,MAAM,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7C,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC3C,KAAK,MAAM,CAAC,IAAI;YACd,4CAA4C;YAC5C,6DAA6D;YAC7D,gEAAgE;YAChE,sCAAsC;YACtC,0DAA0D,EAAE,0BAA0B;YACtF,iBAAiB;SAClB,EAAE,CAAC;YACF,MAAM,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9C,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACvC,MAAM,CAAC,oBAAoB,CAAC,6CAA6C,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzF,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/package.json
CHANGED
package/src/cli/share.ts
CHANGED
|
@@ -64,6 +64,7 @@ import {
|
|
|
64
64
|
readShortMachineId,
|
|
65
65
|
} from "../lib/conflict-file.js";
|
|
66
66
|
import { appendConflictEntry } from "../lib/conflict-index.js";
|
|
67
|
+
import { isCloudAuthoritative } from "../lib/cloud-authoritative.js";
|
|
67
68
|
|
|
68
69
|
/**
|
|
69
70
|
* Push-side fresh-collision convergence probe.
|
|
@@ -1260,6 +1261,16 @@ async function executeUploads(
|
|
|
1260
1261
|
}
|
|
1261
1262
|
|
|
1262
1263
|
if ((localChanged && remoteChanged) || isFreshCollision) {
|
|
1264
|
+
// Cloud-authoritative paths (server-regenerated: company-brief.md,
|
|
1265
|
+
// board.json, ontology/ signals/ sources/) are PULL-WINS. Never push the
|
|
1266
|
+
// local copy over the server-owned version and never mint a `.conflict-`
|
|
1267
|
+
// mirror — the pull leg overwrites local from cloud. Declining here is
|
|
1268
|
+
// what stops the per-sync conflict-mirror loop on these files.
|
|
1269
|
+
if (isCloudAuthoritative(relativePath)) {
|
|
1270
|
+
run.emit({ type: "reconciled", path: relativePath, direction: "push" });
|
|
1271
|
+
counters.filesSkipped++;
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1263
1274
|
conflictPaths.push(relativePath);
|
|
1264
1275
|
|
|
1265
1276
|
const resolution = await resolveConflictSerialized({
|
package/src/cli/sync.test.ts
CHANGED
|
@@ -451,6 +451,58 @@ describe("sync", () => {
|
|
|
451
451
|
expect(litter).toHaveLength(1);
|
|
452
452
|
});
|
|
453
453
|
|
|
454
|
+
it("cloud-authoritative file is PULL-WINS on conflict: overwrites local, no mirror, no false-stamp", async () => {
|
|
455
|
+
// company-brief.md is server-regenerated (gardener), so a divergence is not
|
|
456
|
+
// a real conflict — the cloud copy wins. Even under --on-conflict keep, the
|
|
457
|
+
// conflict path must short-circuit to a plain overwrite: NO `.conflict-`
|
|
458
|
+
// mirror (the loop) and NO journal stamp of the remote etag over divergent
|
|
459
|
+
// local content (Finding B). Contrast the docs/handoff.md test above, which
|
|
460
|
+
// (correctly) keeps local + mirrors for a client-owned file.
|
|
461
|
+
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
462
|
+
fs.mkdirSync(companyRoot, { recursive: true });
|
|
463
|
+
fs.writeFileSync(path.join(companyRoot, "company-brief.md"), "local richer brief");
|
|
464
|
+
|
|
465
|
+
vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
|
|
466
|
+
{ key: "company-brief.md", size: 17, lastModified: new Date(), etag: '"newbrief"' },
|
|
467
|
+
]);
|
|
468
|
+
// Default downloadFile mock writes "mock file content" — the cloud copy.
|
|
469
|
+
|
|
470
|
+
fs.writeFileSync(
|
|
471
|
+
journalPath,
|
|
472
|
+
JSON.stringify({
|
|
473
|
+
version: "1",
|
|
474
|
+
lastSync: new Date().toISOString(),
|
|
475
|
+
files: {
|
|
476
|
+
"company-brief.md": {
|
|
477
|
+
hash: "stale-hash",
|
|
478
|
+
size: 18,
|
|
479
|
+
syncedAt: new Date(Date.now() - 3600000).toISOString(),
|
|
480
|
+
direction: "down",
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
}),
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
const result = await sync({
|
|
487
|
+
company: "acme",
|
|
488
|
+
onConflict: "keep",
|
|
489
|
+
vaultConfig: mockConfig,
|
|
490
|
+
hqRoot: tmpDir,
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
// Cloud wins: not counted as a conflict; local overwritten with the cloud copy.
|
|
494
|
+
expect(result.conflicts).toBe(0);
|
|
495
|
+
expect(result.conflictPaths).toEqual([]);
|
|
496
|
+
expect(
|
|
497
|
+
fs.readFileSync(path.join(companyRoot, "company-brief.md"), "utf-8"),
|
|
498
|
+
).toBe("mock file content");
|
|
499
|
+
// No conflict mirror littered.
|
|
500
|
+
const litter = fs
|
|
501
|
+
.readdirSync(companyRoot)
|
|
502
|
+
.filter((f) => f.includes(".conflict-"));
|
|
503
|
+
expect(litter).toHaveLength(0);
|
|
504
|
+
});
|
|
505
|
+
|
|
454
506
|
it("pre-primes new-file GET presigns before per-file created-by HEADs (avoids the presign-burst breaker trip)", async () => {
|
|
455
507
|
vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
|
|
456
508
|
{ key: "alpha.md", size: 10, lastModified: new Date(), etag: '"a1"' },
|
package/src/cli/sync.ts
CHANGED
|
@@ -69,6 +69,7 @@ import {
|
|
|
69
69
|
readShortMachineId,
|
|
70
70
|
} from "../lib/conflict-file.js";
|
|
71
71
|
import { appendConflictEntry } from "../lib/conflict-index.js";
|
|
72
|
+
import { isCloudAuthoritative } from "../lib/cloud-authoritative.js";
|
|
72
73
|
import { reindex } from "./reindex.js";
|
|
73
74
|
import { withOperationLock } from "../operation-lock.js";
|
|
74
75
|
import {
|
|
@@ -1048,6 +1049,19 @@ async function executeConflictItem(
|
|
|
1048
1049
|
): Promise<SyncResult | null> {
|
|
1049
1050
|
const { remoteFile, localPath } = item;
|
|
1050
1051
|
|
|
1052
|
+
// Cloud-authoritative paths (server-regenerated: company-brief.md, board.json,
|
|
1053
|
+
// ontology/ signals/ sources/) are PULL-WINS: the cloud copy is the source of
|
|
1054
|
+
// truth, so a divergence here is not a real conflict. Resolve it as a normal
|
|
1055
|
+
// overwrite-from-cloud — queue a plain download (which stamps the journal with
|
|
1056
|
+
// the REMOTE hash/etag) and skip the conflict path entirely. This removes both
|
|
1057
|
+
// the `.conflict-` mirror loop AND the journal false-stamp (remote etag over
|
|
1058
|
+
// divergent local content) that the keep path produced for these files.
|
|
1059
|
+
if (isCloudAuthoritative(path.relative(run.hqRoot, localPath))) {
|
|
1060
|
+
downloadItems.push({ action: "download", remoteFile, localPath, isNew: false });
|
|
1061
|
+
run.emit({ type: "reconciled", path: remoteFile.key, direction: "pull" });
|
|
1062
|
+
return null;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1051
1065
|
await refreshRunContextIfExpiring(run);
|
|
1052
1066
|
|
|
1053
1067
|
const detectedAt = new Date().toISOString();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { isCloudAuthoritative } from "./cloud-authoritative.js";
|
|
3
|
+
|
|
4
|
+
describe("isCloudAuthoritative", () => {
|
|
5
|
+
it("matches server-regenerated root files (both path forms)", () => {
|
|
6
|
+
for (const p of [
|
|
7
|
+
"companies/indigo/company-brief.md",
|
|
8
|
+
"company-brief.md",
|
|
9
|
+
"companies/acme/board.json",
|
|
10
|
+
"board.json",
|
|
11
|
+
]) {
|
|
12
|
+
expect(isCloudAuthoritative(p)).toBe(true);
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("matches the pipeline-authored dirs", () => {
|
|
17
|
+
for (const p of [
|
|
18
|
+
"companies/indigo/ontology/entities/concept/grok.md",
|
|
19
|
+
"ontology/entities/person/x.md",
|
|
20
|
+
"companies/indigo/signals/slack-dm/action_item/a.md",
|
|
21
|
+
"signals/decision/b.md",
|
|
22
|
+
"companies/indigo/sources/slack/T1-2.md",
|
|
23
|
+
"sources/meetings/m.md",
|
|
24
|
+
]) {
|
|
25
|
+
expect(isCloudAuthoritative(p)).toBe(true);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("does NOT match client-owned files", () => {
|
|
30
|
+
for (const p of [
|
|
31
|
+
"companies/indigo/skills/work-mesh/SKILL.md",
|
|
32
|
+
"companies/indigo/projects/company-agents/release/index.html",
|
|
33
|
+
"companies/indigo/workers/hq-notify/emails/notice.template.html",
|
|
34
|
+
"companies/indigo/knowledge/readme.md",
|
|
35
|
+
"company-brief.md.conflict-2026-06-18T00-00-00Z-501bc8.md", // a mirror, not the brief
|
|
36
|
+
"docs/handoff.md",
|
|
37
|
+
]) {
|
|
38
|
+
expect(isCloudAuthoritative(p)).toBe(false);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("normalizes windows separators", () => {
|
|
43
|
+
expect(isCloudAuthoritative("companies\\indigo\\ontology\\entities\\x.md")).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud-authoritative vault paths — files the SERVER regenerates and owns, so
|
|
3
|
+
* on a sync conflict the cloud version always wins (pull-wins) and NO
|
|
4
|
+
* `.conflict-` mirror is minted.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists
|
|
7
|
+
* ---------------
|
|
8
|
+
* Some vault files are written/regenerated server-side on a schedule, not by
|
|
9
|
+
* the client:
|
|
10
|
+
* - `company-brief.md` — rewritten by the ontology gardener's brief-generator
|
|
11
|
+
* on every run.
|
|
12
|
+
* - `board.json` — rewritten by board automation.
|
|
13
|
+
* - `ontology/**`, `signals/**`, `sources/**` — the gardener / sources+signals
|
|
14
|
+
* pipeline are the sole authors; clients only consume.
|
|
15
|
+
*
|
|
16
|
+
* A client that also holds these locally diverges from the server copy on
|
|
17
|
+
* essentially every sync. Under `--on-conflict keep` (what outposts run) that
|
|
18
|
+
* produced two compounding bugs:
|
|
19
|
+
* 1. CONFLICT LOOP — each sync minted a fresh `<file>.conflict-<ts>-<machine>`
|
|
20
|
+
* mirror that was never cleaned up (e.g. ~50 `company-brief.md.conflict-*`
|
|
21
|
+
* piled up on an outpost over a few days).
|
|
22
|
+
* 2. SILENT DRIFT — the keep path stamped the journal with the REMOTE etag
|
|
23
|
+
* while leaving the divergent LOCAL content in place, so the journal
|
|
24
|
+
* reported "in sync" forever and the file never reconciled.
|
|
25
|
+
*
|
|
26
|
+
* Treating these paths as cloud-authoritative resolves both: the conflict path
|
|
27
|
+
* short-circuits to a normal overwrite-from-cloud (correct journal stamp, no
|
|
28
|
+
* mirror), because for a server-owned file the cloud copy is the truth.
|
|
29
|
+
*
|
|
30
|
+
* Extending
|
|
31
|
+
* ---------
|
|
32
|
+
* Add a pattern to {@link CLOUD_AUTHORITATIVE_PATTERNS}. Patterns are matched
|
|
33
|
+
* against the VAULT-RELATIVE path (a leading `companies/<slug>/` is stripped
|
|
34
|
+
* first), so both call sites — the pull leg (hqRoot-relative
|
|
35
|
+
* `companies/<slug>/…`) and the push leg (company-relative `…`) — work.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/** Vault-relative patterns for server-owned, cloud-wins files. */
|
|
39
|
+
export const CLOUD_AUTHORITATIVE_PATTERNS: readonly RegExp[] = [
|
|
40
|
+
/^company-brief\.md$/,
|
|
41
|
+
/^board\.json$/,
|
|
42
|
+
/^ontology\//,
|
|
43
|
+
/^signals\//,
|
|
44
|
+
/^sources\//,
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* True iff `relPath` is a server-regenerated, cloud-authoritative vault file.
|
|
49
|
+
* Accepts either an hqRoot-relative path (`companies/<slug>/…`) or a
|
|
50
|
+
* vault-relative path (`…`); Windows separators are normalized.
|
|
51
|
+
*/
|
|
52
|
+
export function isCloudAuthoritative(relPath: string): boolean {
|
|
53
|
+
const vaultRel = relPath
|
|
54
|
+
.split("\\")
|
|
55
|
+
.join("/")
|
|
56
|
+
.replace(/^\.?\//, "")
|
|
57
|
+
.replace(/^companies\/[^/]+\//, "");
|
|
58
|
+
return CLOUD_AUTHORITATIVE_PATTERNS.some((re) => re.test(vaultRel));
|
|
59
|
+
}
|