@indigoai-us/hq-cloud 6.14.23 → 6.14.25

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 (131) hide show
  1. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  2. package/dist/bin/sync-runner-company.js +11 -0
  3. package/dist/bin/sync-runner-company.js.map +1 -1
  4. package/dist/bin/sync-runner-planning.d.ts +11 -0
  5. package/dist/bin/sync-runner-planning.d.ts.map +1 -1
  6. package/dist/bin/sync-runner-planning.js +37 -2
  7. package/dist/bin/sync-runner-planning.js.map +1 -1
  8. package/dist/bin/sync-runner-planning.test.js +1 -0
  9. package/dist/bin/sync-runner-planning.test.js.map +1 -1
  10. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  11. package/dist/bin/sync-runner-watch-loop.js +17 -1
  12. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  13. package/dist/bin/sync-runner.d.ts +6 -0
  14. package/dist/bin/sync-runner.d.ts.map +1 -1
  15. package/dist/bin/sync-runner.js +14 -2
  16. package/dist/bin/sync-runner.js.map +1 -1
  17. package/dist/bin/sync-runner.test.js +155 -1
  18. package/dist/bin/sync-runner.test.js.map +1 -1
  19. package/dist/cli/doctor.d.ts +119 -0
  20. package/dist/cli/doctor.d.ts.map +1 -0
  21. package/dist/cli/doctor.js +485 -0
  22. package/dist/cli/doctor.js.map +1 -0
  23. package/dist/cli/doctor.test.d.ts +13 -0
  24. package/dist/cli/doctor.test.d.ts.map +1 -0
  25. package/dist/cli/doctor.test.js +485 -0
  26. package/dist/cli/doctor.test.js.map +1 -0
  27. package/dist/cli/index.d.ts +2 -0
  28. package/dist/cli/index.d.ts.map +1 -1
  29. package/dist/cli/index.js +4 -0
  30. package/dist/cli/index.js.map +1 -1
  31. package/dist/cli/share.js +6 -1
  32. package/dist/cli/share.js.map +1 -1
  33. package/dist/cli/sync.d.ts +29 -0
  34. package/dist/cli/sync.d.ts.map +1 -1
  35. package/dist/cli/sync.js +140 -2
  36. package/dist/cli/sync.js.map +1 -1
  37. package/dist/cli/sync.test.js +223 -1
  38. package/dist/cli/sync.test.js.map +1 -1
  39. package/dist/cognito-auth.test.js +7 -0
  40. package/dist/cognito-auth.test.js.map +1 -1
  41. package/dist/company-resolver.d.ts +31 -1
  42. package/dist/company-resolver.d.ts.map +1 -1
  43. package/dist/company-resolver.js +329 -21
  44. package/dist/company-resolver.js.map +1 -1
  45. package/dist/company-resolver.test.js +371 -1
  46. package/dist/company-resolver.test.js.map +1 -1
  47. package/dist/index.d.ts +2 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +3 -0
  50. package/dist/index.js.map +1 -1
  51. package/dist/lib/conflict-file.d.ts.map +1 -1
  52. package/dist/lib/conflict-file.js +5 -1
  53. package/dist/lib/conflict-file.js.map +1 -1
  54. package/dist/lib/conflict-index.d.ts.map +1 -1
  55. package/dist/lib/conflict-index.js +7 -2
  56. package/dist/lib/conflict-index.js.map +1 -1
  57. package/dist/lib/machine-id.test.js +14 -0
  58. package/dist/lib/machine-id.test.js.map +1 -1
  59. package/dist/local-path-codec.d.ts +18 -0
  60. package/dist/local-path-codec.d.ts.map +1 -1
  61. package/dist/local-path-codec.js +63 -0
  62. package/dist/local-path-codec.js.map +1 -1
  63. package/dist/local-path-codec.test.d.ts +14 -0
  64. package/dist/local-path-codec.test.d.ts.map +1 -0
  65. package/dist/local-path-codec.test.js +102 -0
  66. package/dist/local-path-codec.test.js.map +1 -0
  67. package/dist/machine-auth.test.js +7 -0
  68. package/dist/machine-auth.test.js.map +1 -1
  69. package/dist/s3.d.ts +5 -0
  70. package/dist/s3.d.ts.map +1 -1
  71. package/dist/s3.js +129 -3
  72. package/dist/s3.js.map +1 -1
  73. package/dist/s3.symlink-materialize.test.d.ts +18 -0
  74. package/dist/s3.symlink-materialize.test.d.ts.map +1 -0
  75. package/dist/s3.symlink-materialize.test.js +394 -0
  76. package/dist/s3.symlink-materialize.test.js.map +1 -0
  77. package/dist/s3.test.js +19 -0
  78. package/dist/s3.test.js.map +1 -1
  79. package/dist/skill-telemetry.d.ts.map +1 -1
  80. package/dist/skill-telemetry.js +24 -3
  81. package/dist/skill-telemetry.js.map +1 -1
  82. package/dist/skill-telemetry.test.js +17 -0
  83. package/dist/skill-telemetry.test.js.map +1 -1
  84. package/dist/telemetry.d.ts +10 -1
  85. package/dist/telemetry.d.ts.map +1 -1
  86. package/dist/telemetry.js +95 -4
  87. package/dist/telemetry.js.map +1 -1
  88. package/dist/telemetry.test.js +177 -0
  89. package/dist/telemetry.test.js.map +1 -1
  90. package/dist/vault-client.d.ts +42 -0
  91. package/dist/vault-client.d.ts.map +1 -1
  92. package/dist/vault-client.js +40 -0
  93. package/dist/vault-client.js.map +1 -1
  94. package/dist/watcher.d.ts.map +1 -1
  95. package/dist/watcher.js +10 -1
  96. package/dist/watcher.js.map +1 -1
  97. package/dist/watcher.test.js +28 -0
  98. package/dist/watcher.test.js.map +1 -1
  99. package/package.json +1 -1
  100. package/src/bin/sync-runner-company.ts +10 -0
  101. package/src/bin/sync-runner-planning.test.ts +1 -0
  102. package/src/bin/sync-runner-planning.ts +46 -3
  103. package/src/bin/sync-runner-watch-loop.ts +19 -1
  104. package/src/bin/sync-runner.test.ts +174 -0
  105. package/src/bin/sync-runner.ts +17 -1
  106. package/src/cli/doctor.test.ts +581 -0
  107. package/src/cli/doctor.ts +640 -0
  108. package/src/cli/index.ts +14 -0
  109. package/src/cli/share.ts +6 -1
  110. package/src/cli/sync.test.ts +281 -1
  111. package/src/cli/sync.ts +184 -2
  112. package/src/cognito-auth.test.ts +5 -0
  113. package/src/company-resolver.test.ts +432 -1
  114. package/src/company-resolver.ts +362 -23
  115. package/src/index.ts +10 -0
  116. package/src/lib/conflict-file.ts +6 -1
  117. package/src/lib/conflict-index.ts +7 -2
  118. package/src/lib/machine-id.test.ts +10 -0
  119. package/src/local-path-codec.test.ts +138 -0
  120. package/src/local-path-codec.ts +66 -0
  121. package/src/machine-auth.test.ts +5 -0
  122. package/src/s3.symlink-materialize.test.ts +492 -0
  123. package/src/s3.test.ts +24 -0
  124. package/src/s3.ts +148 -4
  125. package/src/skill-telemetry.test.ts +19 -0
  126. package/src/skill-telemetry.ts +32 -3
  127. package/src/telemetry.test.ts +241 -0
  128. package/src/telemetry.ts +124 -6
  129. package/src/vault-client.ts +68 -0
  130. package/src/watcher.test.ts +33 -0
  131. package/src/watcher.ts +10 -1
@@ -0,0 +1,119 @@
1
+ /**
2
+ * `hq sync doctor` — bounded skill-key dedupe + local farm GC (US-003,
3
+ * hq-sync-windows-skill-symlink-collision).
4
+ *
5
+ * The mixed-version amplifier (see `canonicalVaultKeySpelling` in
6
+ * local-path-codec.ts) leaves the vault holding several percent-encoded
7
+ * GENERATIONS of every `.claude/skills/<ns>:<skill>` wrapper key (`:`, `%3A`,
8
+ * `%253A`, `%25253A`, …) and the local farm holding junk-named wrapper dirs.
9
+ * US-001/US-002 stop NEW generations from being minted; this doctor is the
10
+ * one-shot reconcile that collapses the families already in the wild:
11
+ *
12
+ * 1. Vault dedupe — list keys under `.claude/skills/`, group them by their
13
+ * decode-to-fixpoint canonical form, let the newest-bodied spelling win,
14
+ * (re)write the canonical `:` key when it is missing or stale, and delete
15
+ * every non-canonical spelling. Deletion is gated hard: only bodies that
16
+ * start with the `hq-symlink:` marker, or that are byte-identical to the
17
+ * canonical twin, are auto-deleted — anything else is surfaced for manual
18
+ * review, never silently removed.
19
+ * 2. Local farm GC — remove `.claude/skills/` entries whose names decode to
20
+ * a canonical wrapper that already exists on disk and whose content is
21
+ * exclusively symlink-farm material. A real file anywhere is a refusal.
22
+ *
23
+ * Dry-run is the DEFAULT: the full keep/rewrite/tombstone/rm plan is printed
24
+ * (counts + every key/path) and nothing is written until `--yes`
25
+ * (`options.yes`). Remote deletes respect the sync-delete-bulk-asymmetry-guard
26
+ * policy — same thresholds, env override, and refusal semantics as
27
+ * `computeDeletePlan` in share.ts. Per-item failures are loud warnings, never
28
+ * process-fatal (hq-sync-deliberate-skip-not-fatal-error-exit2).
29
+ */
30
+ import { type ObjectIO } from "../object-io.js";
31
+ import type { VaultServiceConfig } from "../types.js";
32
+ /** Vault prefix the doctor is bounded to — the skill wrapper link farm. */
33
+ export declare const SKILLS_KEY_PREFIX = ".claude/skills/";
34
+ /**
35
+ * The narrow object-store seam the doctor needs. Structurally satisfied by
36
+ * every `ObjectIO` transport; tests inject an in-memory fake (the same
37
+ * dependency-injection convention the s3.ts suites use via
38
+ * `setObjectIOFactory`).
39
+ */
40
+ export type DoctorStore = Pick<ObjectIO, "listObjects" | "getObject" | "putObject" | "deleteObject">;
41
+ export interface SyncDoctorOptions {
42
+ /** Entity whose vault to reconcile (prs_/agt_ uid, cmp_ uid, or slug). */
43
+ entity: string;
44
+ /** Vault service config (auth + region), as for `sync()`/`share()`. */
45
+ vaultConfig: VaultServiceConfig;
46
+ /** HQ root whose local `.claude/skills/` farm the GC pass walks. */
47
+ hqRoot: string;
48
+ /** Apply the plan. Default false = dry-run: print the plan, write nothing. */
49
+ yes?: boolean;
50
+ /**
51
+ * Explicit bulk-delete override (the `--force-bulk` CLI flag). Equivalent to
52
+ * `HQ_SYNC_DELETE_BULK_OVERRIDE=1`; without either, a tombstone plan that
53
+ * trips the asymmetry thresholds is refused (writes and local GC still run).
54
+ */
55
+ bulkDeleteOverride?: boolean;
56
+ /** DI seam for tests; defaults to the entity's resolved ObjectIO. */
57
+ store?: DoctorStore;
58
+ /**
59
+ * Platform override for local farm-name encoding (local-path-codec
60
+ * convention). Decode-to-fixpoint always uses win32 semantics — junk
61
+ * spellings are a key-space phenomenon, not a host one — but the canonical
62
+ * LOCAL wrapper name a junk dir must collapse into is host-specific
63
+ * (`ns%3Askill` on win32, `ns:skill` on POSIX).
64
+ */
65
+ win32?: boolean;
66
+ /**
67
+ * The doctor takes the same per-root operation lock as `sync()`. Set when a
68
+ * caller (runner/daemon) already holds it for this root.
69
+ */
70
+ operationLockAlreadyHeld?: boolean;
71
+ }
72
+ /** One canonical key the doctor will (re)write, and the spelling it copies. */
73
+ export interface SkillKeyRewrite {
74
+ key: string;
75
+ /** Winning (newest-bodied) spelling whose body becomes the canonical body. */
76
+ fromSpelling: string;
77
+ }
78
+ export interface DoctorManualReviewItem {
79
+ key: string;
80
+ reason: string;
81
+ }
82
+ export interface LocalGcManualReviewItem {
83
+ path: string;
84
+ reason: string;
85
+ }
86
+ export interface SyncDoctorPlan {
87
+ /** Every listed `.claude/skills/` key (the bulk-guard denominator). */
88
+ inScopeKeys: number;
89
+ /** Canonical-spelling keys that need no action. */
90
+ keep: string[];
91
+ /** Canonical keys to (re)write from their winning spelling's body. */
92
+ rewrites: SkillKeyRewrite[];
93
+ /** Non-canonical spellings safe to delete (marker body or byte-identical). */
94
+ tombstones: string[];
95
+ /** Keys the safety gate refused to auto-delete — surfaced, never removed. */
96
+ manualReview: DoctorManualReviewItem[];
97
+ /** Junk-named local farm entries (symlink-farm content only) to remove. */
98
+ localRemovals: string[];
99
+ /** Junk-named local entries the GC refused to touch — surfaced only. */
100
+ localManualReview: LocalGcManualReviewItem[];
101
+ /** True when the tombstone plan tripped the bulk-asymmetry breaker. */
102
+ bulkRefused: boolean;
103
+ }
104
+ export interface SyncDoctorResult {
105
+ plan: SyncDoctorPlan;
106
+ /** False on dry-run (the default) and when `--yes` was absent. */
107
+ applied: boolean;
108
+ keysRewritten: number;
109
+ keysDeleted: number;
110
+ dirsRemoved: number;
111
+ /** Loud non-fatal warnings emitted during plan + apply. */
112
+ warnings: number;
113
+ }
114
+ /**
115
+ * Run the doctor: build the dedupe + GC plan, print it in full, and — only
116
+ * with `yes: true` — apply it. See the module doc for semantics.
117
+ */
118
+ export declare function syncDoctor(options: SyncDoctorOptions): Promise<SyncDoctorResult>;
119
+ //# sourceMappingURL=doctor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/cli/doctor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAKH,OAAO,EAAmB,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AASjE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD,2EAA2E;AAC3E,eAAO,MAAM,iBAAiB,oBAAoB,CAAC;AAqBnD;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,IAAI,CAC5B,QAAQ,EACR,aAAa,GAAG,WAAW,GAAG,WAAW,GAAG,cAAc,CAC3D,CAAC;AAEF,MAAM,WAAW,iBAAiB;IAChC,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,WAAW,EAAE,kBAAkB,CAAC;IAChC,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,GAAG,CAAC,EAAE,OAAO,CAAC;IACd;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,qEAAqE;IACrE,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;OAGG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC;AAED,+EAA+E;AAC/E,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,8EAA8E;IAC9E,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,sBAAsB;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,uEAAuE;IACvE,WAAW,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,sEAAsE;IACtE,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,8EAA8E;IAC9E,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,6EAA6E;IAC7E,YAAY,EAAE,sBAAsB,EAAE,CAAC;IACvC,2EAA2E;IAC3E,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,wEAAwE;IACxE,iBAAiB,EAAE,uBAAuB,EAAE,CAAC;IAC7C,uEAAuE;IACvE,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,cAAc,CAAC;IACrB,kEAAkE;IAClE,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC;CAClB;AAwMD;;;GAGG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,gBAAgB,CAAC,CAO3B"}
@@ -0,0 +1,485 @@
1
+ /**
2
+ * `hq sync doctor` — bounded skill-key dedupe + local farm GC (US-003,
3
+ * hq-sync-windows-skill-symlink-collision).
4
+ *
5
+ * The mixed-version amplifier (see `canonicalVaultKeySpelling` in
6
+ * local-path-codec.ts) leaves the vault holding several percent-encoded
7
+ * GENERATIONS of every `.claude/skills/<ns>:<skill>` wrapper key (`:`, `%3A`,
8
+ * `%253A`, `%25253A`, …) and the local farm holding junk-named wrapper dirs.
9
+ * US-001/US-002 stop NEW generations from being minted; this doctor is the
10
+ * one-shot reconcile that collapses the families already in the wild:
11
+ *
12
+ * 1. Vault dedupe — list keys under `.claude/skills/`, group them by their
13
+ * decode-to-fixpoint canonical form, let the newest-bodied spelling win,
14
+ * (re)write the canonical `:` key when it is missing or stale, and delete
15
+ * every non-canonical spelling. Deletion is gated hard: only bodies that
16
+ * start with the `hq-symlink:` marker, or that are byte-identical to the
17
+ * canonical twin, are auto-deleted — anything else is surfaced for manual
18
+ * review, never silently removed.
19
+ * 2. Local farm GC — remove `.claude/skills/` entries whose names decode to
20
+ * a canonical wrapper that already exists on disk and whose content is
21
+ * exclusively symlink-farm material. A real file anywhere is a refusal.
22
+ *
23
+ * Dry-run is the DEFAULT: the full keep/rewrite/tombstone/rm plan is printed
24
+ * (counts + every key/path) and nothing is written until `--yes`
25
+ * (`options.yes`). Remote deletes respect the sync-delete-bulk-asymmetry-guard
26
+ * policy — same thresholds, env override, and refusal semantics as
27
+ * `computeDeletePlan` in share.ts. Per-item failures are loud warnings, never
28
+ * process-fatal (hq-sync-deliberate-skip-not-fatal-error-exit2).
29
+ */
30
+ import * as fs from "fs";
31
+ import * as path from "path";
32
+ import { resolveEntityContext } from "../context.js";
33
+ import { resolveObjectIO } from "../object-io.js";
34
+ import { SYMLINK_BODY_PREFIX } from "../s3.js";
35
+ import { canonicalVaultKeySpelling, canonicalVaultSegmentSpelling, encodeLocalVaultSegment, } from "../local-path-codec.js";
36
+ import { withOperationLock } from "../operation-lock.js";
37
+ /** Vault prefix the doctor is bounded to — the skill wrapper link farm. */
38
+ export const SKILLS_KEY_PREFIX = ".claude/skills/";
39
+ /**
40
+ * Bulk-asymmetry circuit-breaker, per the sync-delete-bulk-asymmetry-guard
41
+ * policy: any new delete-propagation site must ship the guard inline with the
42
+ * SAME threshold defaults and env-var name as the reference implementation
43
+ * (share.ts `BULK_ASYMMETRY_*` + `isBulkAsymmetryOverride`). Here the
44
+ * numerator is the planned tombstone count and the denominator is every
45
+ * in-scope `.claude/skills/` key — a dedupe pass that wants to delete a large
46
+ * slice of the farm at once must be explicitly confirmed, because that shape
47
+ * is indistinguishable from a planner bug eating the vault.
48
+ */
49
+ const BULK_ASYMMETRY_RATIO = 0.10;
50
+ const BULK_ASYMMETRY_MIN_ABS = 10;
51
+ function isBulkDeleteOverride(explicit) {
52
+ if (explicit)
53
+ return true;
54
+ const v = (process.env.HQ_SYNC_DELETE_BULK_OVERRIDE ?? "").toLowerCase();
55
+ return v === "1" || v === "true" || v === "yes";
56
+ }
57
+ function isSymlinkMarkerBody(body) {
58
+ return (body.subarray(0, SYMLINK_BODY_PREFIX.length).toString("utf-8") ===
59
+ SYMLINK_BODY_PREFIX);
60
+ }
61
+ /** Exhaustively page the skills prefix (the transport caps pages at ~1000). */
62
+ async function listSkillObjects(store) {
63
+ const objects = [];
64
+ let continuationToken;
65
+ do {
66
+ const page = await store.listObjects({
67
+ prefix: SKILLS_KEY_PREFIX,
68
+ continuationToken,
69
+ });
70
+ objects.push(...page.objects);
71
+ continuationToken = page.nextContinuationToken;
72
+ } while (continuationToken);
73
+ return objects;
74
+ }
75
+ /**
76
+ * Winner ordering: newest lastModified first ("newest-bodied spelling wins");
77
+ * ties prefer the canonical spelling, then the lexicographically smallest key
78
+ * so the plan is deterministic under equal timestamps.
79
+ */
80
+ function pickWinner(members, canonicalKey) {
81
+ return members.reduce((best, m) => {
82
+ if (m.lastModified.getTime() !== best.lastModified.getTime()) {
83
+ return m.lastModified.getTime() > best.lastModified.getTime() ? m : best;
84
+ }
85
+ if ((m.key === canonicalKey) !== (best.key === canonicalKey)) {
86
+ return m.key === canonicalKey ? m : best;
87
+ }
88
+ return m.key < best.key ? m : best;
89
+ });
90
+ }
91
+ /**
92
+ * True when every entry under `dir` (recursively) is symlink-farm material:
93
+ * symlinks, or directories that themselves contain only symlink-farm
94
+ * material. Any regular file — or anything that is neither a symlink nor a
95
+ * directory (fifo, socket) — makes the tree untouchable.
96
+ */
97
+ function containsOnlySymlinkFarmContent(dir) {
98
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
99
+ for (const entry of entries) {
100
+ if (entry.isSymbolicLink())
101
+ continue;
102
+ if (entry.isDirectory()) {
103
+ if (!containsOnlySymlinkFarmContent(path.join(dir, entry.name))) {
104
+ return false;
105
+ }
106
+ continue;
107
+ }
108
+ return false;
109
+ }
110
+ return true;
111
+ }
112
+ /** lstat-based existence probe — a broken symlink wrapper still "exists". */
113
+ function entryExists(p) {
114
+ try {
115
+ fs.lstatSync(p);
116
+ return true;
117
+ }
118
+ catch {
119
+ return false;
120
+ }
121
+ }
122
+ /**
123
+ * Plan the local `.claude/skills/` farm GC. Pure planning — no removal here.
124
+ * An entry qualifies when its name percent-decodes (to fixpoint) to a
125
+ * canonical wrapper name DIFFERENT from its own spelling, that canonical
126
+ * wrapper already exists on disk, and the entry is a symlink or a directory
127
+ * of exclusively symlink-farm content. Everything else that is junk-named is
128
+ * listed for manual review with the reason it was refused.
129
+ */
130
+ function planLocalSkillFarmGc(skillsDir, win32, warn) {
131
+ const removals = [];
132
+ const manualReview = [];
133
+ if (!entryExists(skillsDir))
134
+ return { removals, manualReview };
135
+ let entries;
136
+ try {
137
+ entries = fs.readdirSync(skillsDir, { withFileTypes: true });
138
+ }
139
+ catch (err) {
140
+ warn(`local GC skipped — cannot read ${skillsDir}: ${String(err)}`);
141
+ return { removals, manualReview };
142
+ }
143
+ for (const entry of entries) {
144
+ // Fixpoint-decode the name (win32 key-space semantics unconditionally —
145
+ // a mac's literal `ns%3Askill` junk dir is junk too), then re-encode for
146
+ // THIS host to get the canonical local wrapper spelling.
147
+ const canonicalName = canonicalVaultSegmentSpelling(entry.name);
148
+ // A no-op canonicalization means this name is NOT an amplifier
149
+ // generation: generated wrapper generations always decode back to a
150
+ // `<ns>:<skill>` name, and canonicalVaultSegmentSpelling deliberately
151
+ // returns colon-free names unchanged (`foo%25bar` is a legitimate
152
+ // wrapper for the canonical skill name `foo%bar`, distinct from a
153
+ // literal `foo%bar` dir). Encoding such a name for the host would
154
+ // re-escape it into a different spelling (`foo%2525bar` on win32) and,
155
+ // if that double-encoded sibling happens to exist, `--yes` would delete
156
+ // the legitimate wrapper. Bail before the host encode — the amplifier
157
+ // family this GC exists to collapse always changes spelling here.
158
+ if (canonicalName === entry.name)
159
+ continue;
160
+ const canonicalLocalName = encodeLocalVaultSegment(canonicalName, win32);
161
+ if (entry.name === canonicalLocalName)
162
+ continue; // already canonical
163
+ const fullPath = path.join(skillsDir, entry.name);
164
+ if (!entryExists(path.join(skillsDir, canonicalLocalName))) {
165
+ manualReview.push({
166
+ path: fullPath,
167
+ reason: `canonical wrapper '${canonicalLocalName}' missing on disk — junk-named entry may be the only copy`,
168
+ });
169
+ continue;
170
+ }
171
+ if (entry.isSymbolicLink()) {
172
+ removals.push(fullPath);
173
+ continue;
174
+ }
175
+ if (entry.isDirectory()) {
176
+ let farmOnly = false;
177
+ try {
178
+ farmOnly = containsOnlySymlinkFarmContent(fullPath);
179
+ }
180
+ catch (err) {
181
+ warn(`local GC cannot inspect ${fullPath}: ${String(err)}`);
182
+ manualReview.push({ path: fullPath, reason: "unreadable — not inspected" });
183
+ continue;
184
+ }
185
+ if (farmOnly) {
186
+ removals.push(fullPath);
187
+ }
188
+ else {
189
+ manualReview.push({
190
+ path: fullPath,
191
+ reason: "contains real (non-symlink) files — never auto-deleted",
192
+ });
193
+ }
194
+ continue;
195
+ }
196
+ manualReview.push({
197
+ path: fullPath,
198
+ reason: "junk-named regular file — never auto-deleted",
199
+ });
200
+ }
201
+ return { removals, manualReview };
202
+ }
203
+ /** Remove one farm entry: unlink symlinks (rmdir for win32 dir links), rm dirs. */
204
+ function removeFarmEntry(fullPath) {
205
+ const st = fs.lstatSync(fullPath);
206
+ if (st.isSymbolicLink()) {
207
+ try {
208
+ fs.unlinkSync(fullPath);
209
+ }
210
+ catch (err) {
211
+ const code = err.code;
212
+ // A Windows directory symlink is a directory reparse point; unlink()
213
+ // refuses it (EPERM/EISDIR) and rmdir removes the LINK, not the target.
214
+ if (code === "EPERM" || code === "EISDIR") {
215
+ fs.rmdirSync(fullPath);
216
+ }
217
+ else {
218
+ throw err;
219
+ }
220
+ }
221
+ return;
222
+ }
223
+ fs.rmSync(fullPath, { recursive: true });
224
+ }
225
+ /**
226
+ * Run the doctor: build the dedupe + GC plan, print it in full, and — only
227
+ * with `yes: true` — apply it. See the module doc for semantics.
228
+ */
229
+ export async function syncDoctor(options) {
230
+ if (options.operationLockAlreadyHeld) {
231
+ return syncDoctorWithOperationLockHeld(options);
232
+ }
233
+ return withOperationLock(options.hqRoot, "sync-doctor", () => syncDoctorWithOperationLockHeld(options));
234
+ }
235
+ async function syncDoctorWithOperationLockHeld(options) {
236
+ let warnings = 0;
237
+ const warn = (message) => {
238
+ warnings++;
239
+ console.error(` ! ${message}`);
240
+ };
241
+ const win32 = options.win32 ?? process.platform === "win32";
242
+ const store = options.store ??
243
+ resolveObjectIO(await resolveEntityContext(options.entity, options.vaultConfig));
244
+ // ── Vault leg: plan ────────────────────────────────────────────────────────
245
+ const listed = await listSkillObjects(store);
246
+ const groups = new Map();
247
+ for (const obj of listed) {
248
+ const canonicalKey = canonicalVaultKeySpelling(obj.key);
249
+ const members = groups.get(canonicalKey);
250
+ if (members) {
251
+ members.push(obj);
252
+ }
253
+ else {
254
+ groups.set(canonicalKey, [obj]);
255
+ }
256
+ }
257
+ const keep = [];
258
+ const rewrites = [];
259
+ const rewritePayloads = [];
260
+ const tombstones = [];
261
+ const manualReview = [];
262
+ for (const [canonicalKey, members] of groups) {
263
+ // Fast path: one spelling and it is already canonical — nothing to do,
264
+ // and no body fetch (the common case must stay O(list), not O(get)).
265
+ if (members.length === 1 && members[0].key === canonicalKey) {
266
+ keep.push(canonicalKey);
267
+ continue;
268
+ }
269
+ // Actionable family: fetch every member's body. A fetch failure demotes
270
+ // that spelling to manual review (unknown bytes are never deleted) and
271
+ // never aborts the run.
272
+ const fetched = [];
273
+ for (const member of members) {
274
+ try {
275
+ const res = await store.getObject(member.key);
276
+ fetched.push({
277
+ key: member.key,
278
+ lastModified: member.lastModified,
279
+ body: res.body,
280
+ metadata: res.metadata,
281
+ });
282
+ }
283
+ catch (err) {
284
+ warn(`body fetch failed for ${member.key}: ${String(err)} — left for manual review`);
285
+ manualReview.push({
286
+ key: member.key,
287
+ reason: "body fetch failed — bytes unknown, not touched",
288
+ });
289
+ }
290
+ }
291
+ if (fetched.length === 0)
292
+ continue; // every member failed; all warned above
293
+ // A canonical spelling that is LISTED but whose body fetch failed means
294
+ // the canonical bytes are unknown — rewriting from a junk sibling could
295
+ // overwrite a non-marker object behind a transient read fault. Park the
296
+ // whole family: no rewrite, no deletes; re-run once the read recovers.
297
+ const canonicalListed = members.some((m) => m.key === canonicalKey);
298
+ const canonicalMember = fetched.find((m) => m.key === canonicalKey);
299
+ if (canonicalListed && canonicalMember === undefined) {
300
+ for (const member of fetched) {
301
+ manualReview.push({
302
+ key: member.key,
303
+ reason: "canonical body unknown (fetch failed) — family not touched",
304
+ });
305
+ }
306
+ continue;
307
+ }
308
+ const winner = pickWinner(fetched, canonicalKey);
309
+ // The body the canonical key will hold after this pass: the newest-bodied
310
+ // spelling's. (Re)write when the canonical key is missing entirely or its
311
+ // bytes are stale relative to the winner.
312
+ const finalBody = winner.body;
313
+ if (!canonicalMember || !canonicalMember.body.equals(finalBody)) {
314
+ rewrites.push({ key: canonicalKey, fromSpelling: winner.key });
315
+ // CAS fence (same contract as the push path): if the canonical key was
316
+ // listed, the PUT lands only while its ETag is unchanged; if it was
317
+ // absent, only while it is still absent. A concurrent upload from
318
+ // another machine turns the rewrite into a 412 warning (and the
319
+ // failed-rewrite guard below keeps the family's tombstones), never a
320
+ // clobber of bytes this run has not seen.
321
+ const listedCanonical = members.find((m) => m.key === canonicalKey);
322
+ rewritePayloads.push({
323
+ key: canonicalKey,
324
+ body: finalBody,
325
+ metadata: winner.metadata,
326
+ ...(listedCanonical
327
+ ? { ifMatch: listedCanonical.etag }
328
+ : { ifNoneMatch: "*" }),
329
+ });
330
+ }
331
+ else {
332
+ keep.push(canonicalKey);
333
+ }
334
+ // Safety gate, per spelling: auto-delete ONLY symlink-marker bodies or
335
+ // bodies byte-identical to the canonical twin's final content.
336
+ for (const member of fetched) {
337
+ if (member.key === canonicalKey)
338
+ continue;
339
+ if (isSymlinkMarkerBody(member.body) || member.body.equals(finalBody)) {
340
+ tombstones.push(member.key);
341
+ }
342
+ else {
343
+ manualReview.push({
344
+ key: member.key,
345
+ reason: "body is neither a symlink marker nor byte-identical to the canonical key — not auto-deleted",
346
+ });
347
+ }
348
+ }
349
+ }
350
+ // ── Bulk-asymmetry decision (policy: sync-delete-bulk-asymmetry-guard) ────
351
+ const bulkRefused = !isBulkDeleteOverride(options.bulkDeleteOverride) &&
352
+ tombstones.length >= BULK_ASYMMETRY_MIN_ABS &&
353
+ listed.length > 0 &&
354
+ tombstones.length / listed.length >= BULK_ASYMMETRY_RATIO;
355
+ // ── Local leg: plan ───────────────────────────────────────────────────────
356
+ const skillsDir = path.join(options.hqRoot, ".claude", "skills");
357
+ const localGc = planLocalSkillFarmGc(skillsDir, win32, warn);
358
+ const plan = {
359
+ inScopeKeys: listed.length,
360
+ keep,
361
+ rewrites,
362
+ tombstones,
363
+ manualReview,
364
+ localRemovals: localGc.removals,
365
+ localManualReview: localGc.manualReview,
366
+ bulkRefused,
367
+ };
368
+ printPlan(plan);
369
+ const result = {
370
+ plan,
371
+ applied: false,
372
+ keysRewritten: 0,
373
+ keysDeleted: 0,
374
+ dirsRemoved: 0,
375
+ warnings,
376
+ };
377
+ // ── Dry-run is the default: full plan printed, nothing written. ───────────
378
+ if (!options.yes) {
379
+ console.log("\nDry-run (default): nothing written. Re-run with --yes to apply.");
380
+ result.warnings = warnings;
381
+ return result;
382
+ }
383
+ // ── Apply: canonical rewrites first (additive), then gated deletes, then
384
+ // local GC. Per-item failures warn and continue — never fatal. ─────────
385
+ // Canonical keys whose rewrite failed: their families' tombstones must NOT
386
+ // be deleted below — when the canonical body was missing or stale, the junk
387
+ // spelling may hold the newest (or only) remote copy, and deleting it after
388
+ // a failed rewrite would drop that body permanently.
389
+ const failedRewriteCanonicals = new Set();
390
+ for (const payload of rewritePayloads) {
391
+ try {
392
+ await store.putObject({
393
+ key: payload.key,
394
+ body: payload.body,
395
+ // Content-type is not consulted by the engine's read path; marker
396
+ // records upload as octet-stream (s3.ts uploadSymlink convention).
397
+ contentType: "application/octet-stream",
398
+ metadata: payload.metadata,
399
+ ifMatch: payload.ifMatch,
400
+ ifNoneMatch: payload.ifNoneMatch,
401
+ });
402
+ result.keysRewritten++;
403
+ }
404
+ catch (err) {
405
+ failedRewriteCanonicals.add(payload.key);
406
+ warn(`canonical rewrite failed for ${payload.key}: ${String(err)}`);
407
+ }
408
+ }
409
+ if (bulkRefused) {
410
+ // Refusal is event-only (policy req #4): nothing deleted, nothing
411
+ // journaled away — re-run with the override once the plan is reviewed.
412
+ console.error(` ! bulk-asymmetry circuit-breaker tripped: ${tombstones.length}/${listed.length} in-scope skill keys planned for deletion.\n` +
413
+ ` Refusing the delete leg — review the plan above, then re-run with --force-bulk (or HQ_SYNC_DELETE_BULK_OVERRIDE=1) to apply it.`);
414
+ }
415
+ else {
416
+ for (const key of tombstones) {
417
+ if (failedRewriteCanonicals.has(canonicalVaultKeySpelling(key))) {
418
+ warn(`skipping delete of ${key} — its canonical rewrite failed; re-run the doctor once the write path recovers`);
419
+ continue;
420
+ }
421
+ try {
422
+ await store.deleteObject(key);
423
+ result.keysDeleted++;
424
+ }
425
+ catch (err) {
426
+ warn(`delete failed for ${key}: ${String(err)}`);
427
+ }
428
+ }
429
+ }
430
+ for (const fullPath of plan.localRemovals) {
431
+ try {
432
+ // Re-verify at apply time: the entry may have changed between plan and
433
+ // apply (sync pass, user edit). Positive check — remove ONLY what is
434
+ // still a symlink or a farm-only directory; anything else (a regular
435
+ // file that replaced the planned entry, a dir that grew real files) is
436
+ // refused rather than trusted from the plan.
437
+ const st = fs.lstatSync(fullPath);
438
+ const stillFarmEntry = st.isSymbolicLink() ||
439
+ (st.isDirectory() && containsOnlySymlinkFarmContent(fullPath));
440
+ if (!stillFarmEntry) {
441
+ warn(`skipped ${fullPath} — no longer symlink-farm content (changed since planning)`);
442
+ continue;
443
+ }
444
+ removeFarmEntry(fullPath);
445
+ result.dirsRemoved++;
446
+ }
447
+ catch (err) {
448
+ warn(`local removal failed for ${fullPath}: ${String(err)}`);
449
+ }
450
+ }
451
+ result.applied = true;
452
+ result.warnings = warnings;
453
+ console.log(`\nApplied: ${result.keysRewritten} canonical key(s) written, ${result.keysDeleted} spelling(s) deleted, ${result.dirsRemoved} local entr(ies) removed.`);
454
+ return result;
455
+ }
456
+ /** Print the full plan: counts plus every key/path in each bucket. */
457
+ function printPlan(plan) {
458
+ console.log(`hq sync doctor — skill-key dedupe plan (${SKILLS_KEY_PREFIX})`);
459
+ console.log(` in-scope keys: ${plan.inScopeKeys}`);
460
+ console.log(` keep (canonical, no action): ${plan.keep.length}`);
461
+ for (const key of plan.keep)
462
+ console.log(` = ${key}`);
463
+ console.log(` rewrite canonical: ${plan.rewrites.length}`);
464
+ for (const r of plan.rewrites) {
465
+ console.log(` + ${r.key} (body from ${r.fromSpelling})`);
466
+ }
467
+ console.log(` tombstone (delete non-canonical spelling): ${plan.tombstones.length}`);
468
+ for (const key of plan.tombstones)
469
+ console.log(` - ${key}`);
470
+ console.log(` manual review (NOT deleted): ${plan.manualReview.length}`);
471
+ for (const item of plan.manualReview) {
472
+ console.log(` ? ${item.key} — ${item.reason}`);
473
+ }
474
+ console.log(` local farm GC — remove: ${plan.localRemovals.length}`);
475
+ for (const p of plan.localRemovals)
476
+ console.log(` - ${p}`);
477
+ console.log(` local farm GC — manual review: ${plan.localManualReview.length}`);
478
+ for (const item of plan.localManualReview) {
479
+ console.log(` ? ${item.path} — ${item.reason}`);
480
+ }
481
+ if (plan.bulkRefused) {
482
+ console.log(` bulk-asymmetry: delete leg WOULD BE REFUSED (${plan.tombstones.length}/${plan.inScopeKeys} keys; override with --force-bulk or HQ_SYNC_DELETE_BULK_OVERRIDE=1)`);
483
+ }
484
+ }
485
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../src/cli/doctor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Unit tests for `hq sync doctor` (US-003,
3
+ * hq-sync-windows-skill-symlink-collision): skill-key family dedupe in the
4
+ * vault + junk-wrapper GC in the local `.claude/skills/` farm.
5
+ *
6
+ * The object store is dependency-injected (`SyncDoctorOptions.store`) with an
7
+ * in-memory fake, so no test touches a network or a real vault. Local-farm
8
+ * cases run against a mkdtemp root with real symlinks (the same approach as
9
+ * s3.symlink-materialize.test.ts) and pin win32 naming via the injectable
10
+ * `win32` option rather than the host platform.
11
+ */
12
+ export {};
13
+ //# sourceMappingURL=doctor.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.test.d.ts","sourceRoot":"","sources":["../../src/cli/doctor.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}