@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.
- package/dist/bin/sync-mutation.d.ts +16 -0
- package/dist/bin/sync-mutation.d.ts.map +1 -0
- package/dist/bin/sync-mutation.js +60 -0
- package/dist/bin/sync-mutation.js.map +1 -0
- package/dist/bin/sync-mutation.test.d.ts +2 -0
- package/dist/bin/sync-mutation.test.d.ts.map +1 -0
- package/dist/bin/sync-mutation.test.js +165 -0
- package/dist/bin/sync-mutation.test.js.map +1 -0
- package/dist/bin/sync-runner-company.d.ts +8 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +16 -0
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-company.test.d.ts +2 -0
- package/dist/bin/sync-runner-company.test.d.ts.map +1 -0
- package/dist/bin/sync-runner-company.test.js +36 -0
- package/dist/bin/sync-runner-company.test.js.map +1 -0
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +98 -8
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +17 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +109 -0
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/conflict-recovery.test.d.ts +2 -0
- package/dist/cli/conflict-recovery.test.d.ts.map +1 -0
- package/dist/cli/conflict-recovery.test.js +201 -0
- package/dist/cli/conflict-recovery.test.js.map +1 -0
- package/dist/cli/conflict.d.ts +60 -0
- package/dist/cli/conflict.d.ts.map +1 -1
- package/dist/cli/conflict.js +333 -0
- package/dist/cli/conflict.js.map +1 -1
- package/dist/cli/sync.d.ts +35 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +100 -0
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +85 -1
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/skill-telemetry.d.ts +6 -0
- package/dist/skill-telemetry.d.ts.map +1 -1
- package/dist/skill-telemetry.js +14 -2
- package/dist/skill-telemetry.js.map +1 -1
- package/dist/skill-telemetry.test.js +79 -0
- package/dist/skill-telemetry.test.js.map +1 -1
- package/dist/sync/candidate-uploader.d.ts +88 -0
- package/dist/sync/candidate-uploader.d.ts.map +1 -0
- package/dist/sync/candidate-uploader.js +212 -0
- package/dist/sync/candidate-uploader.js.map +1 -0
- package/dist/sync/candidate-uploader.test.d.ts +2 -0
- package/dist/sync/candidate-uploader.test.d.ts.map +1 -0
- package/dist/sync/candidate-uploader.test.js +132 -0
- package/dist/sync/candidate-uploader.test.js.map +1 -0
- package/dist/sync/delta-client.d.ts +73 -0
- package/dist/sync/delta-client.d.ts.map +1 -0
- package/dist/sync/delta-client.js +201 -0
- package/dist/sync/delta-client.js.map +1 -0
- package/dist/sync/delta-client.test.d.ts +2 -0
- package/dist/sync/delta-client.test.d.ts.map +1 -0
- package/dist/sync/delta-client.test.js +97 -0
- package/dist/sync/delta-client.test.js.map +1 -0
- package/dist/sync/durable-apply.d.ts +76 -0
- package/dist/sync/durable-apply.d.ts.map +1 -0
- package/dist/sync/durable-apply.js +530 -0
- package/dist/sync/durable-apply.js.map +1 -0
- package/dist/sync/durable-apply.test.d.ts +2 -0
- package/dist/sync/durable-apply.test.d.ts.map +1 -0
- package/dist/sync/durable-apply.test.js +180 -0
- package/dist/sync/durable-apply.test.js.map +1 -0
- package/dist/sync/event-sync.d.ts +33 -1
- package/dist/sync/event-sync.d.ts.map +1 -1
- package/dist/sync/event-sync.js +149 -1
- package/dist/sync/event-sync.js.map +1 -1
- package/dist/sync/event-sync.test.js +142 -1
- package/dist/sync/event-sync.test.js.map +1 -1
- package/dist/sync/index.d.ts +2 -0
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +1 -0
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/multipart-uploader.d.ts +99 -0
- package/dist/sync/multipart-uploader.d.ts.map +1 -0
- package/dist/sync/multipart-uploader.js +447 -0
- package/dist/sync/multipart-uploader.js.map +1 -0
- package/dist/sync/multipart-uploader.test.d.ts +2 -0
- package/dist/sync/multipart-uploader.test.d.ts.map +1 -0
- package/dist/sync/multipart-uploader.test.js +119 -0
- package/dist/sync/multipart-uploader.test.js.map +1 -0
- package/dist/sync/mutation-client.d.ts +85 -0
- package/dist/sync/mutation-client.d.ts.map +1 -0
- package/dist/sync/mutation-client.js +245 -0
- package/dist/sync/mutation-client.js.map +1 -0
- package/dist/sync/mutation-client.test.d.ts +2 -0
- package/dist/sync/mutation-client.test.d.ts.map +1 -0
- package/dist/sync/mutation-client.test.js +51 -0
- package/dist/sync/mutation-client.test.js.map +1 -0
- package/dist/sync/push-receiver.d.ts +45 -0
- package/dist/sync/push-receiver.d.ts.map +1 -1
- package/dist/sync/push-receiver.js +101 -0
- package/dist/sync/push-receiver.js.map +1 -1
- package/dist/sync/push-receiver.test.js +54 -2
- package/dist/sync/push-receiver.test.js.map +1 -1
- package/dist/sync/scope-inventory-client.d.ts +69 -0
- package/dist/sync/scope-inventory-client.d.ts.map +1 -0
- package/dist/sync/scope-inventory-client.js +210 -0
- package/dist/sync/scope-inventory-client.js.map +1 -0
- package/dist/sync/scope-inventory-client.test.d.ts +2 -0
- package/dist/sync/scope-inventory-client.test.d.ts.map +1 -0
- package/dist/sync/scope-inventory-client.test.js +94 -0
- package/dist/sync/scope-inventory-client.test.js.map +1 -0
- package/dist/sync/snapshot-client.d.ts +98 -0
- package/dist/sync/snapshot-client.d.ts.map +1 -0
- package/dist/sync/snapshot-client.js +402 -0
- package/dist/sync/snapshot-client.js.map +1 -0
- package/dist/sync/snapshot-client.test.d.ts +2 -0
- package/dist/sync/snapshot-client.test.d.ts.map +1 -0
- package/dist/sync/snapshot-client.test.js +169 -0
- package/dist/sync/snapshot-client.test.js.map +1 -0
- package/dist/sync/uploader-finalization.d.ts +97 -0
- package/dist/sync/uploader-finalization.d.ts.map +1 -0
- package/dist/sync/uploader-finalization.js +273 -0
- package/dist/sync/uploader-finalization.js.map +1 -0
- package/dist/sync/uploader-finalization.test.d.ts +2 -0
- package/dist/sync/uploader-finalization.test.d.ts.map +1 -0
- package/dist/sync/uploader-finalization.test.js +92 -0
- package/dist/sync/uploader-finalization.test.js.map +1 -0
- package/dist/telemetry.d.ts +11 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +21 -2
- package/dist/telemetry.js.map +1 -1
- package/dist/telemetry.test.js +80 -0
- package/dist/telemetry.test.js.map +1 -1
- package/package.json +6 -1
- package/.claude/policies/hq-cloud-esm-cannot-spy-fs-builtins.md +0 -30
- package/.claude/policies/hq-cloud-strip-types-no-parameter-properties.md +0 -22
- package/.github/workflows/ci.yml +0 -84
- package/.github/workflows/publish.yml +0 -56
- package/.github/workflows/unreleased-commits-nag.yml +0 -256
- package/eslint.config.js +0 -67
- package/pnpm-workspace.yaml +0 -2
- package/scripts/presign-transport-e2e.mjs +0 -250
- package/scripts/vault-rebaseline.sh +0 -323
- package/scripts/vault-rescue.sh +0 -332
- package/src/active-company.test.ts +0 -188
- package/src/active-company.ts +0 -168
- package/src/agent-codex-instructions.test.ts +0 -332
- package/src/agent-codex-instructions.ts +0 -309
- package/src/auth.ts +0 -146
- package/src/backup-prune.test.ts +0 -98
- package/src/backup-prune.ts +0 -182
- package/src/bin/backup-prune-runner.ts +0 -33
- package/src/bin/rescue-runner.ts +0 -25
- package/src/bin/sync-runner-company.ts +0 -695
- package/src/bin/sync-runner-events.test.ts +0 -143
- package/src/bin/sync-runner-events.ts +0 -55
- package/src/bin/sync-runner-planning.test.ts +0 -311
- package/src/bin/sync-runner-planning.ts +0 -258
- package/src/bin/sync-runner-rollup.test.ts +0 -37
- package/src/bin/sync-runner-rollup.ts +0 -97
- package/src/bin/sync-runner-telemetry.ts +0 -15
- package/src/bin/sync-runner-watch-loop.ts +0 -1235
- package/src/bin/sync-runner-watch-routes.test.ts +0 -71
- package/src/bin/sync-runner-watch-routes.ts +0 -184
- package/src/bin/sync-runner.test.ts +0 -8767
- package/src/bin/sync-runner.ts +0 -2190
- package/src/cli/accept.ts +0 -124
- package/src/cli/conflict.ts +0 -119
- package/src/cli/doctor.test.ts +0 -581
- package/src/cli/doctor.ts +0 -642
- package/src/cli/index.ts +0 -49
- package/src/cli/invite.test.ts +0 -250
- package/src/cli/invite.ts +0 -214
- package/src/cli/promote.ts +0 -157
- package/src/cli/reindex-knowledge.test.ts +0 -307
- package/src/cli/reindex-knowledge.ts +0 -450
- package/src/cli/reindex.test.ts +0 -957
- package/src/cli/reindex.ts +0 -979
- package/src/cli/rescue-classify-ordering.test.ts +0 -548
- package/src/cli/rescue-clone-diagnostics.test.ts +0 -120
- package/src/cli/rescue-core.ts +0 -3011
- package/src/cli/rescue-drift-reconcile.test.ts +0 -179
- package/src/cli/rescue-drop-dir-symlink.test.ts +0 -224
- package/src/cli/rescue-exec-bit-preserve.test.ts +0 -187
- package/src/cli/rescue-hq-root-guard.test.ts +0 -232
- package/src/cli/rescue-journal-reconcile.test.ts +0 -215
- package/src/cli/rescue-mtime-preserve.test.ts +0 -203
- package/src/cli/rescue-settings-reconcile.test.ts +0 -637
- package/src/cli/rescue-snapshot.test.ts +0 -57
- package/src/cli/rescue-snapshot.ts +0 -51
- package/src/cli/rescue.reindex.test.ts +0 -63
- package/src/cli/rescue.test.ts +0 -131
- package/src/cli/rescue.ts +0 -182
- package/src/cli/share.test.ts +0 -7843
- package/src/cli/share.ts +0 -3663
- package/src/cli/sync-scope.test.ts +0 -652
- package/src/cli/sync.test.ts +0 -5207
- package/src/cli/sync.ts +0 -3470
- package/src/cli/tombstones.ts +0 -106
- package/src/cli/watch-event-push-conflict.test.ts +0 -234
- package/src/client-info.test.ts +0 -214
- package/src/client-info.ts +0 -121
- package/src/cognito-auth.test.ts +0 -712
- package/src/cognito-auth.ts +0 -1422
- package/src/company-resolver.test.ts +0 -618
- package/src/company-resolver.ts +0 -521
- package/src/context.test.ts +0 -583
- package/src/context.ts +0 -378
- package/src/daemon-worker.ts +0 -26
- package/src/daemon.ts +0 -99
- package/src/entity-resolver.test.ts +0 -315
- package/src/entity-resolver.ts +0 -180
- package/src/ignore.test.ts +0 -466
- package/src/ignore.ts +0 -469
- package/src/index.ts +0 -439
- package/src/journal.test.ts +0 -968
- package/src/journal.ts +0 -765
- package/src/lib/cloud-authoritative.test.ts +0 -45
- package/src/lib/cloud-authoritative.ts +0 -59
- package/src/lib/conflict-file.ts +0 -86
- package/src/lib/conflict-index.ts +0 -289
- package/src/lib/conflict.test.ts +0 -348
- package/src/lib/describe-error.test.ts +0 -100
- package/src/lib/describe-error.ts +0 -58
- package/src/lib/exit-codes.ts +0 -24
- package/src/lib/machine-id.test.ts +0 -231
- package/src/lib/machine-id.ts +0 -175
- package/src/lib/net-errors.test.ts +0 -65
- package/src/lib/net-errors.ts +0 -86
- package/src/lib/readlink-safe.test.ts +0 -43
- package/src/lib/readlink-safe.ts +0 -29
- package/src/local-path-codec.test.ts +0 -138
- package/src/local-path-codec.ts +0 -161
- package/src/machine-auth.test.ts +0 -1323
- package/src/manifest-reconcile.test.ts +0 -1123
- package/src/manifest-reconcile.ts +0 -518
- package/src/object-io.test.ts +0 -1221
- package/src/object-io.ts +0 -1306
- package/src/operation-lock.test.ts +0 -484
- package/src/operation-lock.ts +0 -680
- package/src/outcome-telemetry.test.ts +0 -498
- package/src/outcome-telemetry.ts +0 -639
- package/src/personal-vault-exclusions.test.ts +0 -308
- package/src/personal-vault-exclusions.ts +0 -354
- package/src/personal-vault.test.ts +0 -756
- package/src/personal-vault.ts +0 -496
- package/src/prefix-coalesce.test.ts +0 -240
- package/src/prefix-coalesce.ts +0 -273
- package/src/public-surface.test.ts +0 -117
- package/src/qmd-reindex.test.ts +0 -877
- package/src/qmd-reindex.ts +0 -842
- package/src/read-only-state-dir.test.ts +0 -188
- package/src/remote-pull.test.ts +0 -1130
- package/src/remote-pull.ts +0 -618
- package/src/s3.symlink-materialize.test.ts +0 -492
- package/src/s3.test.ts +0 -1789
- package/src/s3.ts +0 -1532
- package/src/schemas/signal-types.test.ts +0 -82
- package/src/schemas/signal-types.ts +0 -38
- package/src/schemas/source-channels.test.ts +0 -82
- package/src/schemas/source-channels.ts +0 -53
- package/src/scope-shrink.test.ts +0 -633
- package/src/scope-shrink.ts +0 -481
- package/src/signals/get.test.ts +0 -310
- package/src/signals/get.ts +0 -75
- package/src/signals/internals.ts +0 -195
- package/src/signals/list.test.ts +0 -420
- package/src/signals/list.ts +0 -79
- package/src/signals/parse.ts +0 -8
- package/src/signals/types.ts +0 -91
- package/src/skill-telemetry.test.ts +0 -1825
- package/src/skill-telemetry.ts +0 -1439
- package/src/sources/get.test.ts +0 -293
- package/src/sources/get.ts +0 -66
- package/src/sources/internals.ts +0 -198
- package/src/sources/list.test.ts +0 -402
- package/src/sources/list.ts +0 -84
- package/src/sources/parse.ts +0 -43
- package/src/sources/types.ts +0 -84
- package/src/sync/event-sync.test.ts +0 -594
- package/src/sync/event-sync.ts +0 -545
- package/src/sync/feature-flags.test.ts +0 -378
- package/src/sync/feature-flags.ts +0 -62
- package/src/sync/index.ts +0 -76
- package/src/sync/lease-client.test.ts +0 -128
- package/src/sync/lease-client.ts +0 -207
- package/src/sync/logger.test.ts +0 -242
- package/src/sync/logger.ts +0 -79
- package/src/sync/metrics.test.ts +0 -462
- package/src/sync/metrics.ts +0 -213
- package/src/sync/pull-scope.ts +0 -265
- package/src/sync/push-event.test.ts +0 -266
- package/src/sync/push-event.ts +0 -224
- package/src/sync/push-receiver.test.ts +0 -566
- package/src/sync/push-receiver.ts +0 -1048
- package/src/sync/push-transport.ts +0 -231
- package/src/sync/realtime-rollout.test.ts +0 -86
- package/src/sync/realtime-rollout.ts +0 -262
- package/src/sync/state-store.test.ts +0 -194
- package/src/sync/state-store.ts +0 -727
- package/src/sync-core.ts +0 -58
- package/src/sync-progress.test.ts +0 -94
- package/src/sync-progress.ts +0 -140
- package/src/telemetry-events.test.ts +0 -88
- package/src/telemetry-events.ts +0 -205
- package/src/telemetry.test.ts +0 -1280
- package/src/telemetry.ts +0 -1109
- package/src/types.ts +0 -314
- package/src/vault-client.test.ts +0 -1380
- package/src/vault-client.ts +0 -1694
- package/src/version.ts +0 -24
- package/src/watch-roots.test.ts +0 -278
- package/src/watch-roots.ts +0 -162
- package/src/watcher-event-gate.test.ts +0 -212
- package/src/watcher.test.ts +0 -1079
- package/src/watcher.ts +0 -1741
- package/test/e2e/sync/cross-tenant-isolation.test.ts +0 -630
- package/test/e2e/sync/skill-telemetry-oversized-transcript.test.ts +0 -124
- package/test/e2e/sync/transient-company-leg.test.ts +0 -384
- package/test/e2e/sync/windows-unreadable-link-leg.test.ts +0 -191
- package/test/e2e/watcher-real-chokidar.test.ts +0 -165
- package/test/e2e/watcher-recursive-backend.test.ts +0 -181
- package/test/e2e/watcher-scoped-coverage.test.ts +0 -381
- package/test/invite-flow.integration.test.ts +0 -244
- package/test/joiner-manifest-reconcile.integration.test.ts +0 -322
- package/test/share-sync.integration.test.ts +0 -213
- package/tsconfig.json +0 -19
- package/vitest.config.ts +0 -22
|
@@ -1,45 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,59 +0,0 @@
|
|
|
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
|
-
}
|
package/src/lib/conflict-file.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Conflict file naming + writing.
|
|
3
|
-
*
|
|
4
|
-
* When share/sync detects divergence, the cloud's version of the file is
|
|
5
|
-
* written next to the original with a name encoding the timestamp and the
|
|
6
|
-
* machine that detected the conflict. Lets multiple machines independently
|
|
7
|
-
* surface their own conflicts without name collisions, and lets the user
|
|
8
|
-
* (or the `/resolve-conflicts` HQ skill) see local + cloud side-by-side
|
|
9
|
-
* in their file browser.
|
|
10
|
-
*
|
|
11
|
-
* Machine-id provisioning lives in `./machine-id.ts` — hq-cloud owns the
|
|
12
|
-
* source-of-truth file `<hqRoot>/.hq/machine-id` so every sync host
|
|
13
|
-
* (including Linux outposts with no menubar app) gets a stable id. This
|
|
14
|
-
* module re-exports `readShortMachineId` for back-compat with existing
|
|
15
|
-
* callers; new callers should import directly from `./machine-id.js`.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import * as fs from "fs";
|
|
19
|
-
import * as path from "path";
|
|
20
|
-
|
|
21
|
-
import { localPathForVaultKey } from "../local-path-codec.js";
|
|
22
|
-
|
|
23
|
-
export { readShortMachineId, getOrCreateMachineId } from "./machine-id.js";
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Build the conflict file path for an original. ISO uses `-` instead of
|
|
27
|
-
* `:` so the result is filesystem-safe on every OS, and the original
|
|
28
|
-
* extension is preserved at the end so editors syntax-highlight correctly.
|
|
29
|
-
*
|
|
30
|
-
* knowledge/notes.md, 2026-04-27T22:05:14Z, abc123
|
|
31
|
-
* → knowledge/notes.md.conflict-2026-04-27T22-05-14Z-abc123.md
|
|
32
|
-
*
|
|
33
|
-
* projects/foo/prd.json, ..., abc123
|
|
34
|
-
* → projects/foo/prd.json.conflict-...-abc123.json
|
|
35
|
-
*
|
|
36
|
-
* Files without an extension get the `.conflict-...` suffix appended verbatim.
|
|
37
|
-
*/
|
|
38
|
-
export function buildConflictPath(
|
|
39
|
-
originalRelative: string,
|
|
40
|
-
detectedAt: string,
|
|
41
|
-
shortMachineId: string,
|
|
42
|
-
): string {
|
|
43
|
-
const safeTs = detectedAt.replace(/:/g, "-").replace(/\.\d+/, "");
|
|
44
|
-
const ext = path.extname(originalRelative); // ".md" or "" if none
|
|
45
|
-
// The full original path is preserved (extension and all) so users can
|
|
46
|
-
// visually pair `notes.md` with `notes.md.conflict-…md` in their file
|
|
47
|
-
// browser. The trailing `<ext>` after the timestamp keeps the file
|
|
48
|
-
// syntax-highlighted in editors that key off the final extension.
|
|
49
|
-
const suffix = `.conflict-${safeTs}-${shortMachineId}${ext}`;
|
|
50
|
-
return `${originalRelative}${suffix}`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Write the cloud-side bytes to the conflict path. Creates parent dirs as
|
|
55
|
-
* needed (the conflict file always lives next to the original, so the
|
|
56
|
-
* parent already exists in the steady-state — but defense-in-depth).
|
|
57
|
-
*/
|
|
58
|
-
export function writeConflictFile(
|
|
59
|
-
hqRoot: string,
|
|
60
|
-
conflictRelative: string,
|
|
61
|
-
contents: Buffer,
|
|
62
|
-
): void {
|
|
63
|
-
// Codec boundary: conflict paths are canonical (hq-root-relative,
|
|
64
|
-
// forward-slash) keys, so the mirror must land at the encoded local name
|
|
65
|
-
// on win32 — a raw join of a colon key can never be created there.
|
|
66
|
-
const abs = localPathForVaultKey(hqRoot, conflictRelative);
|
|
67
|
-
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
68
|
-
fs.writeFileSync(abs, contents);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Stable conflict ID — used to dedupe re-detections of the same conflict.
|
|
73
|
-
* Re-running sync after a conflict but before the user has resolved should
|
|
74
|
-
* NOT pile up duplicate entries. The id is derived from the original path
|
|
75
|
-
* and the detection timestamp; if the same original conflicts twice with
|
|
76
|
-
* the user resolving in between, that's a new id (different timestamp),
|
|
77
|
-
* which is correct.
|
|
78
|
-
*/
|
|
79
|
-
export function buildConflictId(
|
|
80
|
-
originalRelative: string,
|
|
81
|
-
detectedAt: string,
|
|
82
|
-
): string {
|
|
83
|
-
const safeTs = detectedAt.replace(/:/g, "-").replace(/\.\d+/, "");
|
|
84
|
-
const safePath = originalRelative.replace(/[/\\.]/g, "-");
|
|
85
|
-
return `${safePath}-${safeTs}`;
|
|
86
|
-
}
|
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Conflict index — durable record of pending divergences awaiting resolution.
|
|
3
|
-
*
|
|
4
|
-
* Lives at `<hq_root>/.hq-conflicts/index.json` (inside HQ content, NOT in
|
|
5
|
-
* `~/.hq/`). Two reasons it sits in HQ content rather than in the state dir:
|
|
6
|
-
* 1. The `/resolve-conflicts` HQ skill discovers it relative to the user's
|
|
7
|
-
* HQ folder — that's the user's mental anchor for "where my files are."
|
|
8
|
-
* 2. The conflict-side files themselves live in HQ content, so the index
|
|
9
|
-
* and the files it references stay co-located. If the user moves HQ,
|
|
10
|
-
* the index moves with it.
|
|
11
|
-
*
|
|
12
|
-
* Excluded from cross-machine sync via `.hqignore` — each machine resolves
|
|
13
|
-
* its own queue. We never propagate conflict files (they'd just create more
|
|
14
|
-
* conflicts on the other side).
|
|
15
|
-
*
|
|
16
|
-
* Writes are atomic (tmp + rename). The resolution skill mutates this file
|
|
17
|
-
* mid-walk; a torn write would corrupt the only record of pending conflicts
|
|
18
|
-
* and could lose track of files we'd written to disk. Higher stakes than the
|
|
19
|
-
* journal, which the next sync can rebuild.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import * as crypto from "crypto";
|
|
23
|
-
import * as fs from "fs";
|
|
24
|
-
import * as path from "path";
|
|
25
|
-
import { localPathForVaultKey } from "../local-path-codec.js";
|
|
26
|
-
import type { ConflictIndex, ConflictIndexEntry } from "../types.js";
|
|
27
|
-
|
|
28
|
-
const CONFLICTS_DIR = ".hq-conflicts";
|
|
29
|
-
const INDEX_FILENAME = "index.json";
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Absolute path to the conflict index for a given HQ root.
|
|
33
|
-
*/
|
|
34
|
-
export function getConflictIndexPath(hqRoot: string): string {
|
|
35
|
-
return path.join(hqRoot, CONFLICTS_DIR, INDEX_FILENAME);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Read the conflict index. Returns an empty index if the file doesn't exist
|
|
40
|
-
* yet (first-conflict-ever case).
|
|
41
|
-
*
|
|
42
|
-
* Throws on corrupt JSON — we deliberately don't auto-repair, since the only
|
|
43
|
-
* record of pending conflicts is too important to silently overwrite. The
|
|
44
|
-
* `/resolve-conflicts` skill surfaces this case to the user with a manual
|
|
45
|
-
* inspection prompt.
|
|
46
|
-
*/
|
|
47
|
-
export function readConflictIndex(hqRoot: string): ConflictIndex {
|
|
48
|
-
const indexPath = getConflictIndexPath(hqRoot);
|
|
49
|
-
if (!fs.existsSync(indexPath)) {
|
|
50
|
-
return { version: 1, conflicts: [] };
|
|
51
|
-
}
|
|
52
|
-
const raw = fs.readFileSync(indexPath, "utf-8");
|
|
53
|
-
const parsed = JSON.parse(raw) as ConflictIndex;
|
|
54
|
-
// Defensive: an empty file or wrong-shape JSON shouldn't crash callers.
|
|
55
|
-
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.conflicts)) {
|
|
56
|
-
return { version: 1, conflicts: [] };
|
|
57
|
-
}
|
|
58
|
-
return parsed;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Atomically write the conflict index. Writes to `<index>.tmp.<random>` then
|
|
63
|
-
* renames into place — `rename(2)` is atomic on POSIX, so a crash mid-write
|
|
64
|
-
* leaves either the old file or the new one, never a half-written one.
|
|
65
|
-
*
|
|
66
|
-
* Always sorts conflicts by `detectedAt` ascending before writing — keeps
|
|
67
|
-
* the file diff-friendly across runs and makes "oldest-first walk" the
|
|
68
|
-
* natural read order in the resolution skill.
|
|
69
|
-
*/
|
|
70
|
-
export function writeConflictIndex(
|
|
71
|
-
hqRoot: string,
|
|
72
|
-
index: ConflictIndex,
|
|
73
|
-
): void {
|
|
74
|
-
const indexPath = getConflictIndexPath(hqRoot);
|
|
75
|
-
fs.mkdirSync(path.dirname(indexPath), { recursive: true });
|
|
76
|
-
|
|
77
|
-
const sorted: ConflictIndex = {
|
|
78
|
-
version: index.version,
|
|
79
|
-
conflicts: [...index.conflicts].sort((a, b) =>
|
|
80
|
-
a.detectedAt.localeCompare(b.detectedAt),
|
|
81
|
-
),
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
// Random suffix in tmp name avoids collision if two sync runs ever overlap
|
|
85
|
-
// (shouldn't happen — the runner serializes — but cheap insurance).
|
|
86
|
-
const tmpPath = `${indexPath}.tmp.${crypto.randomBytes(6).toString("hex")}`;
|
|
87
|
-
fs.writeFileSync(tmpPath, JSON.stringify(sorted, null, 2));
|
|
88
|
-
fs.renameSync(tmpPath, indexPath);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Idempotent append. If an entry with the same `id` already exists (same
|
|
93
|
-
* original path, same detection timestamp), update it in place rather than
|
|
94
|
-
* duplicating. This matters because re-running sync after a conflict but
|
|
95
|
-
* before resolution will re-detect the same divergence — without dedup the
|
|
96
|
-
* index would grow unboundedly.
|
|
97
|
-
*
|
|
98
|
-
* The "update in place" path also covers the case where the cloud advanced
|
|
99
|
-
* again between detections: we want the latest `remoteVersionId` and
|
|
100
|
-
* `remoteHash` so the resolution skill shows the user the *current* cloud
|
|
101
|
-
* state, not stale data from the first detection.
|
|
102
|
-
*/
|
|
103
|
-
export function appendConflictEntry(
|
|
104
|
-
hqRoot: string,
|
|
105
|
-
entry: ConflictIndexEntry,
|
|
106
|
-
): void {
|
|
107
|
-
const index = readConflictIndex(hqRoot);
|
|
108
|
-
const existingIdx = index.conflicts.findIndex((c) => c.id === entry.id);
|
|
109
|
-
if (existingIdx >= 0) {
|
|
110
|
-
index.conflicts[existingIdx] = entry;
|
|
111
|
-
} else {
|
|
112
|
-
index.conflicts.push(entry);
|
|
113
|
-
}
|
|
114
|
-
writeConflictIndex(hqRoot, index);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Remove an entry by id. Used by the `/resolve-conflicts` skill after the
|
|
119
|
-
* user picks a resolution and the conflict file is cleaned up. No-op if the
|
|
120
|
-
* id isn't present (e.g. user manually removed the file then re-ran the
|
|
121
|
-
* skill — we want that to be a clean exit, not an error).
|
|
122
|
-
*/
|
|
123
|
-
export function removeConflictEntry(hqRoot: string, id: string): void {
|
|
124
|
-
const index = readConflictIndex(hqRoot);
|
|
125
|
-
const filtered = index.conflicts.filter((c) => c.id !== id);
|
|
126
|
-
if (filtered.length === index.conflicts.length) return;
|
|
127
|
-
writeConflictIndex(hqRoot, { version: index.version, conflicts: filtered });
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Summary of what a {@link pruneConflictIndex} pass reclaimed. */
|
|
131
|
-
export interface PruneConflictIndexResult {
|
|
132
|
-
/** Rows dropped because their `.conflict-*` mirror no longer exists. */
|
|
133
|
-
prunedOrphans: number;
|
|
134
|
-
/** Rows dropped because the original file and its mirror are byte-identical. */
|
|
135
|
-
prunedIdentical: number;
|
|
136
|
-
/** Byte-identical mirror files deleted from disk during the pass. */
|
|
137
|
-
removedMirrors: number;
|
|
138
|
-
/** Rows kept (genuine divergence, or unprovable — fail-safe retained). */
|
|
139
|
-
kept: number;
|
|
140
|
-
/**
|
|
141
|
-
* Up to 10 ORIGINAL (non-mirror) paths of the kept rows, for a
|
|
142
|
-
* post-sync reconcile surface (`conflicts-remaining`). Empty when
|
|
143
|
-
* `kept === 0`. Lets a caller name the preserved conflict variants a human
|
|
144
|
-
* still has to resolve without re-reading the index. feedback_d2082110.
|
|
145
|
-
*/
|
|
146
|
-
keptSamplePaths: string[];
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Byte-identity check used by the prune pass. Symlink-aware: two symlinks are
|
|
151
|
-
* identical iff their link targets match (mirroring how the detector hashes a
|
|
152
|
-
* symlink record — `hashSymlinkTarget`, not the followed content). A symlink
|
|
153
|
-
* compared against a regular file is never identical. Regular files reject on a
|
|
154
|
-
* size mismatch first (cheap), then compare full bytes.
|
|
155
|
-
*
|
|
156
|
-
* Throws if either side can't be read — callers treat that as "unprovable" and
|
|
157
|
-
* keep the entry rather than risk dropping a real conflict.
|
|
158
|
-
*/
|
|
159
|
-
function entryFilesAreIdentical(
|
|
160
|
-
aPath: string,
|
|
161
|
-
aStat: fs.Stats,
|
|
162
|
-
bPath: string,
|
|
163
|
-
bStat: fs.Stats,
|
|
164
|
-
): boolean {
|
|
165
|
-
const aLink = aStat.isSymbolicLink();
|
|
166
|
-
const bLink = bStat.isSymbolicLink();
|
|
167
|
-
if (aLink || bLink) {
|
|
168
|
-
if (aLink !== bLink) return false;
|
|
169
|
-
return fs.readlinkSync(aPath) === fs.readlinkSync(bPath);
|
|
170
|
-
}
|
|
171
|
-
if (aStat.size !== bStat.size) return false;
|
|
172
|
-
return fs.readFileSync(aPath).equals(fs.readFileSync(bPath));
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Garbage-collect the conflict index so it self-heals instead of growing
|
|
177
|
-
* without bound. Before this pass existed the ledger only ever shrank when a
|
|
178
|
-
* human resolved a conflict via `/resolve-conflicts`; every false positive and
|
|
179
|
-
* every orphaned row lingered forever, so the index over-reported the real
|
|
180
|
-
* pending-conflict count (the menubar's journal-derived count stayed correct,
|
|
181
|
-
* but `.hq-conflicts/index.json` did not).
|
|
182
|
-
*
|
|
183
|
-
* Two classes of entry are dropped — both provably not a pending conflict:
|
|
184
|
-
*
|
|
185
|
-
* 1. **Orphaned** — the `.conflict-*` mirror file is gone from disk. The
|
|
186
|
-
* mirror is the only artifact a human resolves against; once it's missing
|
|
187
|
-
* the row can never be acted on, so it's pure litter (the "missing-cloud"
|
|
188
|
-
* rows operators reported climbing into the dozens).
|
|
189
|
-
*
|
|
190
|
-
* 2. **Byte-identical false positives** — the original file and its conflict
|
|
191
|
-
* mirror both still exist and are byte-for-byte identical. The mirror
|
|
192
|
-
* holds the remote bytes captured at detection time, so identical bytes
|
|
193
|
-
* mean there was never a real divergence (this is exactly the manual
|
|
194
|
-
* safe-purge operators have been doing by hand). The stale mirror file is
|
|
195
|
-
* deleted and the row dropped.
|
|
196
|
-
*
|
|
197
|
-
* Conservative by construction — an entry is kept whenever it might be a real
|
|
198
|
-
* conflict: the original file is missing (a genuine local-delete-vs-remote
|
|
199
|
-
* divergence), the bytes differ, or either side can't be read. The index file
|
|
200
|
-
* is only rewritten when at least one row is actually dropped, so a clean
|
|
201
|
-
* ledger keeps its mtime untouched.
|
|
202
|
-
*/
|
|
203
|
-
export function pruneConflictIndex(hqRoot: string): PruneConflictIndexResult {
|
|
204
|
-
const result: PruneConflictIndexResult = {
|
|
205
|
-
prunedOrphans: 0,
|
|
206
|
-
prunedIdentical: 0,
|
|
207
|
-
removedMirrors: 0,
|
|
208
|
-
kept: 0,
|
|
209
|
-
keptSamplePaths: [],
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
const index = readConflictIndex(hqRoot);
|
|
213
|
-
if (index.conflicts.length === 0) return result;
|
|
214
|
-
|
|
215
|
-
const kept: ConflictIndexEntry[] = [];
|
|
216
|
-
const mirrorsToRemove: string[] = [];
|
|
217
|
-
|
|
218
|
-
for (const entry of index.conflicts) {
|
|
219
|
-
// Codec boundary: index rows store canonical (hq-root-relative) keys, so
|
|
220
|
-
// both probes must resolve through the codec — a raw join of a win32
|
|
221
|
-
// colon key always lstat-fails, which would mis-classify a live encoded
|
|
222
|
-
// mirror as an orphaned row and silently drop it.
|
|
223
|
-
const mirrorAbs = localPathForVaultKey(hqRoot, entry.conflictPath);
|
|
224
|
-
const originalAbs = localPathForVaultKey(hqRoot, entry.originalPath);
|
|
225
|
-
|
|
226
|
-
let mirrorStat: fs.Stats;
|
|
227
|
-
try {
|
|
228
|
-
mirrorStat = fs.lstatSync(mirrorAbs);
|
|
229
|
-
} catch {
|
|
230
|
-
// Mirror is gone — orphaned row, drop it. Nothing on disk to clean up.
|
|
231
|
-
result.prunedOrphans++;
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Both sides must exist to compare. A missing original is a real
|
|
236
|
-
// local-delete divergence — keep it for the human to resolve.
|
|
237
|
-
let originalStat: fs.Stats;
|
|
238
|
-
try {
|
|
239
|
-
originalStat = fs.lstatSync(originalAbs);
|
|
240
|
-
} catch {
|
|
241
|
-
kept.push(entry);
|
|
242
|
-
result.kept++;
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
let identical: boolean;
|
|
247
|
-
try {
|
|
248
|
-
identical = entryFilesAreIdentical(
|
|
249
|
-
originalAbs,
|
|
250
|
-
originalStat,
|
|
251
|
-
mirrorAbs,
|
|
252
|
-
mirrorStat,
|
|
253
|
-
);
|
|
254
|
-
} catch {
|
|
255
|
-
// Couldn't read one side — fail safe, keep the row.
|
|
256
|
-
kept.push(entry);
|
|
257
|
-
result.kept++;
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (identical) {
|
|
262
|
-
result.prunedIdentical++;
|
|
263
|
-
mirrorsToRemove.push(mirrorAbs);
|
|
264
|
-
} else {
|
|
265
|
-
kept.push(entry);
|
|
266
|
-
result.kept++;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Name the preserved divergences for the post-sync reconcile surface.
|
|
271
|
-
result.keptSamplePaths = kept.slice(0, 10).map((e) => e.originalPath);
|
|
272
|
-
|
|
273
|
-
// No row dropped → leave the file (and its mtime) untouched.
|
|
274
|
-
if (kept.length === index.conflicts.length) return result;
|
|
275
|
-
|
|
276
|
-
// Delete the byte-identical mirror litter. Best-effort: a leftover identical
|
|
277
|
-
// mirror is cosmetic, never corrupting, so a failed unlink doesn't abort.
|
|
278
|
-
for (const mirrorAbs of mirrorsToRemove) {
|
|
279
|
-
try {
|
|
280
|
-
fs.rmSync(mirrorAbs, { force: true });
|
|
281
|
-
result.removedMirrors++;
|
|
282
|
-
} catch {
|
|
283
|
-
/* best-effort cleanup */
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
writeConflictIndex(hqRoot, { version: index.version, conflicts: kept });
|
|
288
|
-
return result;
|
|
289
|
-
}
|