@indigoai-us/hq-cloud 5.48.4 → 6.0.0

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 (51) hide show
  1. package/dist/cli/index.d.ts +2 -2
  2. package/dist/cli/index.d.ts.map +1 -1
  3. package/dist/cli/index.js +2 -2
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/cli/reindex.d.ts +23 -0
  6. package/dist/cli/reindex.d.ts.map +1 -0
  7. package/dist/cli/reindex.js +45 -0
  8. package/dist/cli/reindex.js.map +1 -0
  9. package/dist/cli/reindex.test.d.ts +11 -0
  10. package/dist/cli/reindex.test.d.ts.map +1 -0
  11. package/dist/cli/{master-sync.test.js → reindex.test.js} +15 -15
  12. package/dist/cli/reindex.test.js.map +1 -0
  13. package/dist/cli/rescue.d.ts.map +1 -1
  14. package/dist/cli/rescue.js +16 -1
  15. package/dist/cli/rescue.js.map +1 -1
  16. package/dist/cli/rescue.reindex.test.d.ts +2 -0
  17. package/dist/cli/rescue.reindex.test.d.ts.map +1 -0
  18. package/dist/cli/rescue.reindex.test.js +41 -0
  19. package/dist/cli/rescue.reindex.test.js.map +1 -0
  20. package/dist/cli/share.test.js +2 -2
  21. package/dist/cli/share.test.js.map +1 -1
  22. package/dist/cli/sync.d.ts +9 -0
  23. package/dist/cli/sync.d.ts.map +1 -1
  24. package/dist/cli/sync.js +17 -0
  25. package/dist/cli/sync.js.map +1 -1
  26. package/dist/cli/sync.test.js +20 -0
  27. package/dist/cli/sync.test.js.map +1 -1
  28. package/dist/index.d.ts +2 -2
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +2 -2
  31. package/dist/index.js.map +1 -1
  32. package/package.json +1 -1
  33. package/scripts/{master-sync.sh → reindex.sh} +10 -10
  34. package/scripts/replace-rescue.sh +20 -20
  35. package/src/cli/index.ts +3 -3
  36. package/src/cli/{master-sync.test.ts → reindex.test.ts} +14 -14
  37. package/src/cli/reindex.ts +57 -0
  38. package/src/cli/rescue.reindex.test.ts +46 -0
  39. package/src/cli/rescue.ts +15 -1
  40. package/src/cli/share.test.ts +2 -2
  41. package/src/cli/sync.test.ts +23 -0
  42. package/src/cli/sync.ts +27 -0
  43. package/src/index.ts +3 -3
  44. package/dist/cli/master-sync.d.ts +0 -22
  45. package/dist/cli/master-sync.d.ts.map +0 -1
  46. package/dist/cli/master-sync.js +0 -44
  47. package/dist/cli/master-sync.js.map +0 -1
  48. package/dist/cli/master-sync.test.d.ts +0 -11
  49. package/dist/cli/master-sync.test.d.ts.map +0 -1
  50. package/dist/cli/master-sync.test.js.map +0 -1
  51. package/src/cli/master-sync.ts +0 -56
@@ -1,16 +1,16 @@
1
1
  #!/bin/bash
2
- # master-sync.sh — surfaces namespace skills as Claude Code skills under
2
+ # reindex.sh — surfaces namespace skills as Claude Code skills under
3
3
  # .claude/skills/<ns>:<skill>/ with every file in the source skill folder
4
4
  # mirrored as a symlink. Also mirrors personal/{knowledge,policies,workers,
5
5
  # settings}/* into core/<type>/<name>.
6
6
  #
7
7
  # This script ships inside the @indigoai-us/hq-cloud package and is invoked via
8
- # `hq master-sync` (which passes the HQ root as $1). It is idempotent and cheap
8
+ # `hq reindex` (which passes the HQ root as $1). It is idempotent and cheap
9
9
  # to re-run, so it doesn't gate on whether personal/ was actually touched. Real
10
10
  # files/dirs already at the link path are left untouched.
11
11
  #
12
12
  # REPO_ROOT resolution (in priority order):
13
- # 1. $1 — passed by `hq master-sync` (the HQ root)
13
+ # 1. $1 — passed by `hq reindex` (the HQ root)
14
14
  # 2. $HQ_REPO_ROOT — env override
15
15
  # 3. $PWD — fallback when invoked directly
16
16
  #
@@ -40,7 +40,7 @@ set -uo pipefail
40
40
 
41
41
  REPO_ROOT="${1:-${HQ_REPO_ROOT:-$PWD}}"
42
42
  if [ ! -d "$REPO_ROOT" ]; then
43
- echo "master-sync: REPO_ROOT '$REPO_ROOT' is not a directory" >&2
43
+ echo "reindex: REPO_ROOT '$REPO_ROOT' is not a directory" >&2
44
44
  exit 1
45
45
  fi
46
46
  REPO_ROOT="$(cd "$REPO_ROOT" && pwd)"
@@ -128,7 +128,7 @@ while [ "$i" -lt "${#namespaces[@]}" ]; do
128
128
  fi
129
129
  done
130
130
  if [ "$already" -eq 1 ]; then
131
- echo "master-sync: namespace '$ns' already claimed by an earlier source; skipping $src_rel" >&2
131
+ echo "reindex: namespace '$ns' already claimed by an earlier source; skipping $src_rel" >&2
132
132
  continue
133
133
  fi
134
134
  seen+=("$ns")
@@ -147,7 +147,7 @@ while [ "$i" -lt "${#namespaces[@]}" ]; do
147
147
 
148
148
  # If something non-directory occupies the slot, bail.
149
149
  if [ -e "$wrapper" ] && [ ! -L "$wrapper" ] && [ ! -d "$wrapper" ]; then
150
- echo "master-sync: $wrapper exists and is not a directory; skipping" >&2
150
+ echo "reindex: $wrapper exists and is not a directory; skipping" >&2
151
151
  continue
152
152
  fi
153
153
  # If it's a symlink (e.g. legacy directory-symlink form from earlier
@@ -171,10 +171,10 @@ while [ "$i" -lt "${#namespaces[@]}" ]; do
171
171
  if [ "$current" = "$relative_target" ]; then
172
172
  continue
173
173
  fi
174
- echo "master-sync: .claude/skills/$wrapper_name/$entry already points to '$current' (expected '$relative_target'); leaving alone" >&2
174
+ echo "reindex: .claude/skills/$wrapper_name/$entry already points to '$current' (expected '$relative_target'); leaving alone" >&2
175
175
  continue
176
176
  elif [ -e "$link_path" ]; then
177
- echo "master-sync: $link_path already exists and is not a symlink; skipping" >&2
177
+ echo "reindex: $link_path already exists and is not a symlink; skipping" >&2
178
178
  continue
179
179
  fi
180
180
 
@@ -298,10 +298,10 @@ for type in knowledge policies workers settings; do
298
298
  if [ "$current" = "$relative_target" ]; then
299
299
  continue
300
300
  fi
301
- echo "master-sync: core/$type/$entry already points to '$current' (expected '$relative_target'); leaving alone" >&2
301
+ echo "reindex: core/$type/$entry already points to '$current' (expected '$relative_target'); leaving alone" >&2
302
302
  continue
303
303
  elif [ -e "$link_path" ]; then
304
- echo "master-sync: core/$type/$entry already exists and is not a symlink; skipping" >&2
304
+ echo "reindex: core/$type/$entry already exists and is not a symlink; skipping" >&2
305
305
  continue
306
306
  fi
307
307
 
@@ -77,7 +77,7 @@
77
77
  # not a customization — saving it under personal/ or .hq-conflicts/ just
78
78
  # accumulates noise.
79
79
  #
80
- # Symlinks generated by `master-sync.sh` (targets resolving into
80
+ # Symlinks generated by `reindex.sh` (targets resolving into
81
81
  # `personal/`) are DELETED during the walk rather than preserved. They
82
82
  # rebuild deterministically on the next Stop-hook pass — keeping the old
83
83
  # link risks shadowing the upstream file the overlay writes at the same
@@ -245,7 +245,7 @@ BACKUP_DIR="" # resolved at snapshot time (BACKUP_ROOT/pre-update-<RUN_TS>)
245
245
  # personal + installed-pack workers. Its content is a function of the
246
246
  # user's install state, not the upstream tree, so an overlay that
247
247
  # replaces it produces a registry that doesn't match what's on disk
248
- # (and `master-sync` regenerates it on the next Stop hook anyway).
248
+ # (and `reindex` regenerates it on the next Stop hook anyway).
249
249
  # Shuttling preserves the up-to-date local version across the rescue.
250
250
  # Why .claude/settings.local.json: per-user local settings (denied paths,
251
251
  # env vars, allow-listed tool patterns) authored by the user via the CLI
@@ -436,7 +436,7 @@ if [ "${#PRESERVE_SUBPATHS[@]}" -ne 0 ]; then
436
436
  fi
437
437
  done
438
438
  fi
439
- echo "==> Drift policy: rescue user-edited files to personal/; route .agents|.codex|.obsidian|MIGRATION.md user-edits to .hq-conflicts/rescue-$RUN_TS/; silently overwrite AGENTS.md|USER-GUIDE.md|core/policies/_digest.md (regenerable); leave user-only files untouched; drop master-sync symlinks (regenerated by master-sync.sh)"
439
+ echo "==> Drift policy: rescue user-edited files to personal/; route .agents|.codex|.obsidian|MIGRATION.md user-edits to .hq-conflicts/rescue-$RUN_TS/; silently overwrite AGENTS.md|USER-GUIDE.md|core/policies/_digest.md (regenerable); leave user-only files untouched; drop reindex symlinks (regenerated by reindex.sh)"
440
440
  if [ "$CLOUD_UPDATE" = "1" ]; then
441
441
  echo "==> Cloud-update mode: ON (recognize \`hq-symlink:<target>\` flat files as upstream-symlink-equivalent — reconciled as UNCHANGED)"
442
442
  fi
@@ -459,7 +459,7 @@ if [ "$ASSUME_YES" != "1" ] && [ "$DRY_RUN" != "1" ]; then
459
459
  if [ "$CLOUD_UPDATE" = "1" ]; then
460
460
  _cloud_line="\n * reconcile \`hq-symlink:<target>\` flat files against upstream symlinks (cloud-update mode),"
461
461
  fi
462
- printf "\nThis will:\n%s\n * rescue user-edited files (vs the last sync) into personal/,\n * route user-edited .agents/.codex/.obsidian/MIGRATION.md into .hq-conflicts/rescue-%s/ for review,\n * silently overwrite AGENTS.md / USER-GUIDE.md / core/policies/_digest.md (regenerable from upstream or master-sync — no copy preserved),%b\n * drop master-sync-generated symlinks (regenerated by master-sync.sh on next Stop hook),\n * leave user-only files (not in upstream) untouched,\n * delete upstream files unchanged since last sync, then unpack %s@%s on top.\nType 'yes' to proceed: " "$_backup_line" "$RUN_TS" "$_cloud_line" "$SOURCE_REPO" "$REF"
462
+ printf "\nThis will:\n%s\n * rescue user-edited files (vs the last sync) into personal/,\n * route user-edited .agents/.codex/.obsidian/MIGRATION.md into .hq-conflicts/rescue-%s/ for review,\n * silently overwrite AGENTS.md / USER-GUIDE.md / core/policies/_digest.md (regenerable from upstream or reindex — no copy preserved),%b\n * drop reindex-generated symlinks (regenerated by reindex.sh on next Stop hook),\n * leave user-only files (not in upstream) untouched,\n * delete upstream files unchanged since last sync, then unpack %s@%s on top.\nType 'yes' to proceed: " "$_backup_line" "$RUN_TS" "$_cloud_line" "$SOURCE_REPO" "$REF"
463
463
  read -r confirm
464
464
  [ "$confirm" = "yes" ] || { echo "Aborted."; exit 4; }
465
465
  fi
@@ -767,7 +767,7 @@ rescue_one() {
767
767
  #
768
768
  # Routing them to .hq-conflicts/rescue-<RUN_TS>/<rel> keeps them quarantined
769
769
  # under a timestamped bucket the user can inspect and discard, without
770
- # polluting the personal/ overlay tree (where master-sync would then try
770
+ # polluting the personal/ overlay tree (where reindex would then try
771
771
  # to re-mirror them into core/).
772
772
  is_conflict_class() {
773
773
  local rel="$1"
@@ -814,7 +814,7 @@ conflict_one() {
814
814
  # core/docs/hq/USER-GUIDE.md — current release doc; always re-shipped.
815
815
  # core/policies/_digest.md — generated by `core/scripts/build-policy-digest.sh`
816
816
  # from the union of policy files; rebuilt on
817
- # the next Stop-hook master-sync pass.
817
+ # the next Stop-hook reindex pass.
818
818
  # Same rationale as core/workers/registry.yaml,
819
819
  # minus the carve-out (digest is small + cheap
820
820
  # to regenerate, so just let the overlay win).
@@ -894,11 +894,11 @@ is_cloud_flattened_symlink_equiv() {
894
894
  [ "$local_target" = "$upstream_target" ]
895
895
  }
896
896
 
897
- # True when $local_path is a symlink generated by master-sync.sh — those
897
+ # True when $local_path is a symlink generated by reindex.sh — those
898
898
  # point into personal/ (e.g. `core/<type>/<entry>` -> `../../personal/...`,
899
899
  # `.claude/skills/<ns>:<entry>` -> `../../../personal/skills/<entry>/...`,
900
900
  # `.claude/commands/<ns>/<entry>.md` -> `../../../personal/skills/<entry>/SKILL.md`).
901
- # They are deterministically rebuilt on the next master-sync.sh run, so the
901
+ # They are deterministically rebuilt on the next reindex.sh run, so the
902
902
  # safe move during rescue is to delete rather than preserve — a leftover
903
903
  # symlink can shadow an overlaid-from-upstream real file at the same path.
904
904
  is_master_sync_symlink() {
@@ -958,20 +958,20 @@ process_one() {
958
958
  ;;
959
959
  esac
960
960
 
961
- # Symlinks (mid-tree). master-sync.sh deterministically rebuilds the
961
+ # Symlinks (mid-tree). reindex.sh deterministically rebuilds the
962
962
  # personal/-targeted ones on its next Stop-hook pass, so the safe move
963
963
  # here is to delete them — a leftover symlink at a path the overlay
964
964
  # also writes to creates an ambiguous "is this the symlink or the
965
- # overlaid file?" state on disk. Non-master-sync symlinks (user-created,
965
+ # overlaid file?" state on disk. Non-reindex symlinks (user-created,
966
966
  # absolute targets, or targets outside personal/) are left alone.
967
967
  if [ -L "$local_path" ]; then
968
968
  if is_master_sync_symlink "$local_path"; then
969
969
  COUNT_SYMLINK_DROPPED=$((COUNT_SYMLINK_DROPPED + 1))
970
970
  if [ "$DRY_RUN" = "1" ]; then
971
- echo " drop master-sync symlink: $rel -> $(readlink "$local_path" 2>/dev/null || true)"
971
+ echo " drop reindex symlink: $rel -> $(readlink "$local_path" 2>/dev/null || true)"
972
972
  else
973
973
  rm -f "$local_path"
974
- printf 'symlink-dropped\t%s\t(master-sync regenerable)\n' "$rel" >> "$RESCUE_LOG" 2>/dev/null || true
974
+ printf 'symlink-dropped\t%s\t(reindex regenerable)\n' "$rel" >> "$RESCUE_LOG" 2>/dev/null || true
975
975
  fi
976
976
  fi
977
977
  return 0
@@ -1038,7 +1038,7 @@ process_one() {
1038
1038
 
1039
1039
  # Convergence guard: in history_floor mode a file is flagged USER-EDIT when
1040
1040
  # its blob differs from the *old* baseline (the floor). But HQ's own Stop
1041
- # hooks (master-sync symlinking personal/->core/, autocommit, registry/
1041
+ # hooks (reindex symlinking personal/->core/, autocommit, registry/
1042
1042
  # _digest/core.yaml regeneration) rewrite scaffold files every session, so a
1043
1043
  # file routinely drifts from the floor yet lands byte-for-byte identical to
1044
1044
  # the *new* upstream HEAD this overlay is about to write. Rescuing it into
@@ -1123,16 +1123,16 @@ walk_and_process() {
1123
1123
  # convenience aliases the overlay overwrites cleanly (rsync -a preserves
1124
1124
  # link semantics from source). Master-sync personal-overlay symlinks at
1125
1125
  # the top level — rare but possible — are dropped here so the overlay
1126
- # can land a real file at the same path without ambiguity; master-sync
1126
+ # can land a real file at the same path without ambiguity; reindex
1127
1127
  # regenerates the link on its next Stop-hook pass.
1128
1128
  if [ -L "$root_abs" ]; then
1129
1129
  if is_master_sync_symlink "$root_abs"; then
1130
1130
  COUNT_SYMLINK_DROPPED=$((COUNT_SYMLINK_DROPPED + 1))
1131
1131
  if [ "$DRY_RUN" = "1" ]; then
1132
- echo " drop master-sync symlink: $root_rel -> $(readlink "$root_abs" 2>/dev/null || true)"
1132
+ echo " drop reindex symlink: $root_rel -> $(readlink "$root_abs" 2>/dev/null || true)"
1133
1133
  else
1134
1134
  rm -f "$root_abs"
1135
- printf 'symlink-dropped\t%s\t(master-sync regenerable)\n' "$root_rel" >> "$RESCUE_LOG" 2>/dev/null || true
1135
+ printf 'symlink-dropped\t%s\t(reindex regenerable)\n' "$root_rel" >> "$RESCUE_LOG" 2>/dev/null || true
1136
1136
  fi
1137
1137
  fi
1138
1138
  return 0
@@ -1146,7 +1146,7 @@ walk_and_process() {
1146
1146
 
1147
1147
  # Top-level directory: walk recursively, pruning node_modules + nested
1148
1148
  # .git (v0.1.103 perf + correctness guard). Symlinks are surfaced via
1149
- # `-type l` so master-sync-generated links (targets resolving into
1149
+ # `-type l` so reindex-generated links (targets resolving into
1150
1150
  # personal/) can be dropped — see process_one + is_master_sync_symlink.
1151
1151
  # Under -P (the default), find does not descend INTO directory symlinks,
1152
1152
  # which keeps the walk bounded even when those links exist.
@@ -1217,7 +1217,7 @@ if [ "$DRY_RUN" = "1" ]; then
1217
1217
  echo " user-edit (overwrite-safe): $COUNT_USER_OVERWRITE files"
1218
1218
  echo " cloud-symlink reconciled: $COUNT_CLOUD_SYMLINK_RECONCILED files"
1219
1219
  echo " drift reconciled (== upstream): $COUNT_DRIFT_RECONCILED files"
1220
- echo " master-sync symlinks dropped: $COUNT_SYMLINK_DROPPED entries"
1220
+ echo " reindex symlinks dropped: $COUNT_SYMLINK_DROPPED entries"
1221
1221
  if [ "$COUNT_CLAUDE_DIFF_APPEND" -gt 0 ]; then
1222
1222
  echo " of which .claude/CLAUDE.md would diff-append to personal/CLAUDE.md"
1223
1223
  fi
@@ -1361,7 +1361,7 @@ echo " user-edits (conflict quarantine): $COUNT_USER_CONFLICT files"
1361
1361
  echo " user-edits (overwrite-safe): $COUNT_USER_OVERWRITE files"
1362
1362
  echo " cloud-symlink reconciled: $COUNT_CLOUD_SYMLINK_RECONCILED files"
1363
1363
  echo " drift reconciled (== upstream): $COUNT_DRIFT_RECONCILED files"
1364
- echo " master-sync symlinks dropped: $COUNT_SYMLINK_DROPPED entries"
1364
+ echo " reindex symlinks dropped: $COUNT_SYMLINK_DROPPED entries"
1365
1365
  if [ "$COUNT_CLAUDE_DIFF_APPEND" -gt 0 ]; then
1366
1366
  echo " of which .claude/CLAUDE.md diff-appended to personal/CLAUDE.md"
1367
1367
  fi
@@ -1396,7 +1396,7 @@ if [ "$DO_BACKUP" = "1" ] && [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ]; then
1396
1396
  echo " user-edit (overwrite-safe): $COUNT_USER_OVERWRITE"
1397
1397
  echo " cloud-symlink reconciled: $COUNT_CLOUD_SYMLINK_RECONCILED"
1398
1398
  echo " drift reconciled (== upstream HEAD): $COUNT_DRIFT_RECONCILED"
1399
- echo " master-sync symlinks dropped: $COUNT_SYMLINK_DROPPED"
1399
+ echo " reindex symlinks dropped: $COUNT_SYMLINK_DROPPED"
1400
1400
  if [ "$COUNT_USER_CONFLICT" -gt 0 ]; then
1401
1401
  echo " conflict bucket: .hq-conflicts/rescue-$RUN_TS/"
1402
1402
  fi
package/src/cli/index.ts CHANGED
@@ -25,9 +25,9 @@ export { promote } from "./promote.js";
25
25
  export type { PromoteOptions, PromoteResult } from "./promote.js";
26
26
 
27
27
  // Skill/personal-overlay mirroring + workers-registry regen (formerly the
28
- // hq-core master-sync.sh hook).
29
- export { masterSync, masterSyncScriptPath } from "./master-sync.js";
30
- export type { MasterSyncOptions, MasterSyncResult } from "./master-sync.js";
28
+ // hq-core master-sync.sh hook; `hq reindex`).
29
+ export { reindex, reindexScriptPath } from "./reindex.js";
30
+ export type { ReindexOptions, ReindexResult } from "./reindex.js";
31
31
 
32
32
  // Drift-preserving HQ-core re-sync (formerly bundled only inside the HQ Sync
33
33
  // menubar app). Now shared between the app and `hq rescue`.
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Unit tests for `hq master-sync` (the formerly-bash hq-core hook, now shipped
2
+ * Unit tests for `hq reindex` (the formerly-bash hq-core hook, now shipped
3
3
  * in this package and invoked via the CLI).
4
4
  *
5
- * The logic itself lives in scripts/master-sync.sh; these tests exercise it via
6
- * the masterSync() wrapper against a temp HQ tree, asserting the observable
5
+ * The logic itself lives in scripts/reindex.sh; these tests exercise it via
6
+ * the reindex() wrapper against a temp HQ tree, asserting the observable
7
7
  * filesystem outcomes (skill wrappers, personal-overlay mirroring) rather than
8
8
  * re-deriving the bash internals.
9
9
  */
@@ -12,17 +12,17 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest";
12
12
  import * as fs from "fs";
13
13
  import * as path from "path";
14
14
  import * as os from "os";
15
- import { masterSync, masterSyncScriptPath } from "./master-sync.js";
15
+ import { reindex, reindexScriptPath } from "./reindex.js";
16
16
 
17
- describe("masterSyncScriptPath", () => {
17
+ describe("reindexScriptPath", () => {
18
18
  it("resolves to the bundled script and the file exists", () => {
19
- const p = masterSyncScriptPath();
20
- expect(p.endsWith(path.join("scripts", "master-sync.sh"))).toBe(true);
19
+ const p = reindexScriptPath();
20
+ expect(p.endsWith(path.join("scripts", "reindex.sh"))).toBe(true);
21
21
  expect(fs.existsSync(p)).toBe(true);
22
22
  });
23
23
  });
24
24
 
25
- describe("masterSync", () => {
25
+ describe("reindex", () => {
26
26
  let root: string;
27
27
 
28
28
  beforeEach(() => {
@@ -44,7 +44,7 @@ describe("masterSync", () => {
44
44
  fs.writeFileSync(path.join(root, "core/skills/demo/helper.md"), "h\n");
45
45
  writeSkill("companies/acme/skills/widget");
46
46
 
47
- const { status } = masterSync({ repoRoot: root });
47
+ const { status } = reindex({ repoRoot: root });
48
48
  expect(status).toBe(0);
49
49
 
50
50
  const coreWrapper = path.join(root, ".claude/skills/core:demo");
@@ -65,7 +65,7 @@ describe("masterSync", () => {
65
65
  fs.mkdirSync(path.join(root, "personal/policies"), { recursive: true });
66
66
  fs.writeFileSync(path.join(root, "personal/policies/myrule.md"), "rule\n");
67
67
 
68
- const { status } = masterSync({ repoRoot: root });
68
+ const { status } = reindex({ repoRoot: root });
69
69
  expect(status).toBe(0);
70
70
 
71
71
  const link = path.join(root, "core/policies/myrule.md");
@@ -75,18 +75,18 @@ describe("masterSync", () => {
75
75
 
76
76
  it("prunes orphan managed wrappers when the source skill disappears", () => {
77
77
  writeSkill("core/skills/demo");
78
- masterSync({ repoRoot: root });
78
+ reindex({ repoRoot: root });
79
79
  expect(fs.existsSync(path.join(root, ".claude/skills/core:demo"))).toBe(true);
80
80
 
81
81
  fs.rmSync(path.join(root, "core/skills/demo"), { recursive: true, force: true });
82
- masterSync({ repoRoot: root });
82
+ reindex({ repoRoot: root });
83
83
  expect(fs.existsSync(path.join(root, ".claude/skills/core:demo"))).toBe(false);
84
84
  });
85
85
 
86
86
  it("is idempotent — a second run is a no-op", () => {
87
87
  writeSkill("core/skills/demo");
88
- expect(masterSync({ repoRoot: root }).status).toBe(0);
89
- expect(masterSync({ repoRoot: root }).status).toBe(0);
88
+ expect(reindex({ repoRoot: root }).status).toBe(0);
89
+ expect(reindex({ repoRoot: root }).status).toBe(0);
90
90
  expect(
91
91
  fs.readlinkSync(path.join(root, ".claude/skills/core:demo/SKILL.md")),
92
92
  ).toBe("../../../core/skills/demo/SKILL.md");
@@ -0,0 +1,57 @@
1
+ /**
2
+ * hq reindex — surfaces namespaced skills as Claude Code skill wrappers,
3
+ * mirrors personal/{knowledge,policies,workers,settings} into core/, prunes
4
+ * orphan wrappers, and regenerates the workers registry.
5
+ *
6
+ * The implementation lives in scripts/reindex.sh, shipped with this package.
7
+ * This module resolves that script relative to the package (dist/cli → package
8
+ * root) and execs it against the caller's HQ root. Historically the script ran
9
+ * directly as a Claude Code hook inside hq-core (named "master-sync"); it now
10
+ * lives here so a single copy is maintained, and the hq-core hook is a thin
11
+ * shim over `hq reindex`.
12
+ */
13
+ import { spawnSync } from "child_process";
14
+ import { fileURLToPath } from "url";
15
+ import path from "path";
16
+
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = path.dirname(__filename);
19
+
20
+ /**
21
+ * Absolute path to the bundled reindex.sh. From the compiled module at
22
+ * dist/cli/reindex.js, the package root is two levels up; the script lives
23
+ * at <package-root>/scripts/reindex.sh.
24
+ */
25
+ export function reindexScriptPath(): string {
26
+ return path.resolve(__dirname, "..", "..", "scripts", "reindex.sh");
27
+ }
28
+
29
+ export interface ReindexOptions {
30
+ /** HQ root to operate on. Defaults to process.cwd(). */
31
+ repoRoot?: string;
32
+ }
33
+
34
+ export interface ReindexResult {
35
+ /** Exit status of the underlying script (0 = success). */
36
+ status: number;
37
+ }
38
+
39
+ /**
40
+ * Run reindex against an HQ root. Synchronous — the script is cheap and
41
+ * idempotent, and callers (the hook shim, sync/rescue, tests) want the exit
42
+ * status. stdout/stderr from the script are forwarded to stderr so the
43
+ * caller's stdout stays clean (hooks must not emit stdout that the agent
44
+ * interprets).
45
+ */
46
+ export function reindex(opts: ReindexOptions = {}): ReindexResult {
47
+ const repoRoot = opts.repoRoot ?? process.cwd();
48
+ const script = reindexScriptPath();
49
+ const res = spawnSync("bash", [script, repoRoot], {
50
+ stdio: ["ignore", "inherit", "inherit"],
51
+ });
52
+ if (res.error) {
53
+ process.stderr.write(`reindex: failed to run ${script}: ${res.error.message}\n`);
54
+ return { status: 1 };
55
+ }
56
+ return { status: res.status ?? 1 };
57
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Wiring tests: `rescue()` runs `reindex()` after a successful, non-dry-run
3
+ * rescue so the generated skill wrappers / personal mirrors / workers registry
4
+ * are refreshed once core/ has been re-laid-down.
5
+ *
6
+ * child_process is mocked so the real replace-rescue.sh never runs, and
7
+ * ./reindex.js is mocked to a spy so we assert the call without spawning.
8
+ */
9
+ import { describe, it, expect, vi, beforeEach } from "vitest";
10
+
11
+ vi.mock("child_process", () => ({
12
+ spawnSync: vi.fn(() => ({ status: 0 })),
13
+ }));
14
+ vi.mock("./reindex.js", () => ({
15
+ reindex: vi.fn(() => ({ status: 0 })),
16
+ }));
17
+
18
+ import { spawnSync } from "child_process";
19
+ import { reindex } from "./reindex.js";
20
+ import { rescue } from "./rescue.js";
21
+
22
+ describe("rescue → reindex", () => {
23
+ beforeEach(() => {
24
+ vi.clearAllMocks();
25
+ (spawnSync as unknown as ReturnType<typeof vi.fn>).mockReturnValue({ status: 0 });
26
+ });
27
+
28
+ it("refreshes via reindex after a successful rescue", () => {
29
+ const r = rescue({ hqRoot: "/tmp/hq", assumeYes: true });
30
+ expect(r.status).toBe(0);
31
+ expect(reindex).toHaveBeenCalledTimes(1);
32
+ expect(reindex).toHaveBeenCalledWith({ repoRoot: "/tmp/hq" });
33
+ });
34
+
35
+ it("does NOT run reindex on a dry-run", () => {
36
+ rescue({ hqRoot: "/tmp/hq", dryRun: true });
37
+ expect(reindex).not.toHaveBeenCalled();
38
+ });
39
+
40
+ it("does NOT run reindex when the rescue script fails", () => {
41
+ (spawnSync as unknown as ReturnType<typeof vi.fn>).mockReturnValueOnce({ status: 1 });
42
+ const r = rescue({ hqRoot: "/tmp/hq", assumeYes: true });
43
+ expect(r.status).toBe(1);
44
+ expect(reindex).not.toHaveBeenCalled();
45
+ });
46
+ });
package/src/cli/rescue.ts CHANGED
@@ -17,6 +17,7 @@
17
17
  import { spawnSync } from "child_process";
18
18
  import { fileURLToPath } from "url";
19
19
  import path from "path";
20
+ import { reindex } from "./reindex.js";
20
21
 
21
22
  const __filename = fileURLToPath(import.meta.url);
22
23
  const __dirname = path.dirname(__filename);
@@ -118,5 +119,18 @@ export function rescue(opts: RescueOptions = {}): RescueResult {
118
119
  process.stderr.write(`rescue: failed to run ${script}: ${res.error.message}\n`);
119
120
  return { status: 1 };
120
121
  }
121
- return { status: res.status ?? 1 };
122
+ const status = res.status ?? 1;
123
+ // A successful, non-dry-run rescue re-lays-down core/, so refresh the
124
+ // generated skill wrappers, personal-overlay mirrors, and workers registry.
125
+ // Best-effort + idempotent — never overrides the rescue's own exit status.
126
+ // repoRoot falls back to process.cwd() (reindex's default) when hqRoot is
127
+ // omitted, matching the rescue script's own cwd-based default.
128
+ if (status === 0 && !opts.dryRun) {
129
+ try {
130
+ reindex({ repoRoot: opts.hqRoot });
131
+ } catch {
132
+ // best-effort
133
+ }
134
+ }
135
+ return { status };
122
136
  }
@@ -2290,7 +2290,7 @@ describe("share", () => {
2290
2290
  // target's bytes under the link's key while a nested symlink was
2291
2291
  // silently dropped from every push. The link topology never survived a
2292
2292
  // round trip — fresh-machine pulls landed in a state where overlay
2293
- // symlinks just didn't exist until master-sync.sh recreated them
2293
+ // symlinks just didn't exist until reindex.sh recreated them
2294
2294
  // locally. The fix detects symlinks via lstat / Dirent.isSymbolicLink
2295
2295
  // and routes them to a new uploadSymlink primitive that PUTs a
2296
2296
  // zero-byte object with x-amz-meta-hq-symlink-target carrying the
@@ -2334,7 +2334,7 @@ describe("share", () => {
2334
2334
  const realPolicy = path.join(policiesDir, "real.md");
2335
2335
  fs.writeFileSync(realPolicy, "real content");
2336
2336
  const linkPolicy = path.join(policiesDir, "link.md");
2337
- // Mirrors the master-sync.sh overlay shape: relative target pointing
2337
+ // Mirrors the reindex.sh overlay shape: relative target pointing
2338
2338
  // to a sibling in the same dir.
2339
2339
  fs.symlinkSync("real.md", linkPolicy);
2340
2340
 
@@ -36,8 +36,15 @@ vi.mock("../s3.js", async () => {
36
36
  };
37
37
  });
38
38
 
39
+ // Mock the post-sync refresh so tests neither spawn the real reindex.sh nor
40
+ // mutate the temp hqRoot, and we can assert the wiring directly.
41
+ vi.mock("./reindex.js", () => ({
42
+ reindex: vi.fn(() => ({ status: 0 })),
43
+ }));
44
+
39
45
  import { sync } from "./sync.js";
40
46
  import * as s3Module from "../s3.js";
47
+ import { reindex } from "./reindex.js";
41
48
 
42
49
  const mockConfig: VaultServiceConfig = {
43
50
  apiUrl: "https://vault-api.test",
@@ -114,6 +121,22 @@ describe("sync", () => {
114
121
  delete process.env.HQ_STATE_DIR;
115
122
  });
116
123
 
124
+ it("runs reindex against hqRoot after a sync that downloaded files", async () => {
125
+ await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
126
+ expect(reindex).toHaveBeenCalledWith({ repoRoot: tmpDir });
127
+ });
128
+
129
+ it("skips reindex when skipReindex is set", async () => {
130
+ const result = await sync({
131
+ company: "acme",
132
+ vaultConfig: mockConfig,
133
+ hqRoot: tmpDir,
134
+ skipReindex: true,
135
+ });
136
+ expect(result.filesDownloaded).toBe(2);
137
+ expect(reindex).not.toHaveBeenCalled();
138
+ });
139
+
117
140
  it("downloads remote files under companies/{slug}/ so two companies don't collide", async () => {
118
141
  const result = await sync({
119
142
  company: "acme",
package/src/cli/sync.ts CHANGED
@@ -49,6 +49,7 @@ import {
49
49
  readShortMachineId,
50
50
  } from "../lib/conflict-file.js";
51
51
  import { appendConflictEntry } from "../lib/conflict-index.js";
52
+ import { reindex } from "./reindex.js";
52
53
 
53
54
  /**
54
55
  * Per-file events emitted by `sync()` as it progresses.
@@ -329,6 +330,15 @@ export interface SyncOptions {
329
330
  * tombstoned. Mirrors `hq sync narrow --force`.
330
331
  */
331
332
  forceScopeShrink?: boolean;
333
+ /**
334
+ * Skip the post-sync `reindex()` refresh (skill wrappers + personal overlay
335
+ * mirrors + workers registry). By default, when a sync changes on-disk
336
+ * sources (downloads, tombstones, or scope-orphan removals), `sync()`
337
+ * re-runs reindex so the generated `.claude/skills/<ns>:<skill>` wrappers
338
+ * stay in sync. An orchestrator syncing many companies in one pass can set
339
+ * this and run `reindex()` once itself instead of per-company.
340
+ */
341
+ skipReindex?: boolean;
332
342
  }
333
343
 
334
344
  export interface SyncResult {
@@ -1263,6 +1273,23 @@ export async function sync(options: SyncOptions): Promise<SyncResult> {
1263
1273
  journal.lastSync = new Date().toISOString();
1264
1274
  writeJournal(journalSlug, journal);
1265
1275
 
1276
+ // When the pull actually changed on-disk sources (new files, tombstoned
1277
+ // removals, or scope-orphan cleanups), refresh the generated skill wrappers,
1278
+ // personal-overlay mirrors, and workers registry. reindex is idempotent and
1279
+ // best-effort — it must never fail a sync, and is skipped on no-op syncs
1280
+ // (the common daemon case) and when the caller opts out via skipReindex.
1281
+ const changedOnDisk =
1282
+ filesDownloaded > 0 ||
1283
+ filesTombstoned > 0 ||
1284
+ shrinkResult.cleanRemoved > 0;
1285
+ if (!options.skipReindex && changedOnDisk) {
1286
+ try {
1287
+ reindex({ repoRoot: hqRoot });
1288
+ } catch {
1289
+ // best-effort: a post-sync refresh failure never fails the sync
1290
+ }
1291
+ }
1292
+
1266
1293
  return {
1267
1294
  filesDownloaded,
1268
1295
  bytesDownloaded,
package/src/index.ts CHANGED
@@ -212,9 +212,9 @@ export type { AcceptOptions, AcceptResult } from "./cli/index.js";
212
212
  export { promote } from "./cli/index.js";
213
213
  export type { PromoteOptions, PromoteResult } from "./cli/index.js";
214
214
 
215
- // Skill/personal-overlay mirroring + workers-registry regen (`hq master-sync`).
216
- export { masterSync, masterSyncScriptPath } from "./cli/index.js";
217
- export type { MasterSyncOptions, MasterSyncResult } from "./cli/index.js";
215
+ // Skill/personal-overlay mirroring + workers-registry regen (`hq reindex`).
216
+ export { reindex, reindexScriptPath } from "./cli/index.js";
217
+ export type { ReindexOptions, ReindexResult } from "./cli/index.js";
218
218
 
219
219
  // Drift-preserving HQ-core re-sync — shared by the HQ Sync app and `hq rescue`.
220
220
  export { rescue, rescueScriptPath, buildRescueArgs } from "./cli/index.js";
@@ -1,22 +0,0 @@
1
- /**
2
- * Absolute path to the bundled master-sync.sh. From the compiled module at
3
- * dist/cli/master-sync.js, the package root is two levels up; the script lives
4
- * at <package-root>/scripts/master-sync.sh.
5
- */
6
- export declare function masterSyncScriptPath(): string;
7
- export interface MasterSyncOptions {
8
- /** HQ root to operate on. Defaults to process.cwd(). */
9
- repoRoot?: string;
10
- }
11
- export interface MasterSyncResult {
12
- /** Exit status of the underlying script (0 = success). */
13
- status: number;
14
- }
15
- /**
16
- * Run master-sync against an HQ root. Synchronous — the script is cheap and
17
- * idempotent, and callers (the hook shim, tests) want the exit status.
18
- * stdout/stderr from the script are forwarded to stderr so the caller's stdout
19
- * stays clean (hooks must not emit stdout that the agent interprets).
20
- */
21
- export declare function masterSync(opts?: MasterSyncOptions): MasterSyncResult;
22
- //# sourceMappingURL=master-sync.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"master-sync.d.ts","sourceRoot":"","sources":["../../src/cli/master-sync.ts"],"names":[],"mappings":"AAmBA;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C;AAED,MAAM,WAAW,iBAAiB;IAChC,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,GAAE,iBAAsB,GAAG,gBAAgB,CAWzE"}
@@ -1,44 +0,0 @@
1
- /**
2
- * hq master-sync — surfaces namespaced skills as Claude Code skill wrappers,
3
- * mirrors personal/{knowledge,policies,workers,settings} into core/, prunes
4
- * orphan wrappers, and regenerates the workers registry.
5
- *
6
- * The implementation lives in scripts/master-sync.sh, shipped with this
7
- * package. This module resolves that script relative to the package (dist/cli
8
- * → package root) and execs it against the caller's HQ root. Historically the
9
- * script ran directly as a Claude Code hook inside hq-core; it now lives here
10
- * so a single copy is maintained, and the hq-core hook is a thin shim over
11
- * `hq master-sync`.
12
- */
13
- import { spawnSync } from "child_process";
14
- import { fileURLToPath } from "url";
15
- import path from "path";
16
- const __filename = fileURLToPath(import.meta.url);
17
- const __dirname = path.dirname(__filename);
18
- /**
19
- * Absolute path to the bundled master-sync.sh. From the compiled module at
20
- * dist/cli/master-sync.js, the package root is two levels up; the script lives
21
- * at <package-root>/scripts/master-sync.sh.
22
- */
23
- export function masterSyncScriptPath() {
24
- return path.resolve(__dirname, "..", "..", "scripts", "master-sync.sh");
25
- }
26
- /**
27
- * Run master-sync against an HQ root. Synchronous — the script is cheap and
28
- * idempotent, and callers (the hook shim, tests) want the exit status.
29
- * stdout/stderr from the script are forwarded to stderr so the caller's stdout
30
- * stays clean (hooks must not emit stdout that the agent interprets).
31
- */
32
- export function masterSync(opts = {}) {
33
- const repoRoot = opts.repoRoot ?? process.cwd();
34
- const script = masterSyncScriptPath();
35
- const res = spawnSync("bash", [script, repoRoot], {
36
- stdio: ["ignore", "inherit", "inherit"],
37
- });
38
- if (res.error) {
39
- process.stderr.write(`master-sync: failed to run ${script}: ${res.error.message}\n`);
40
- return { status: 1 };
41
- }
42
- return { status: res.status ?? 1 };
43
- }
44
- //# sourceMappingURL=master-sync.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"master-sync.js","sourceRoot":"","sources":["../../src/cli/master-sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AACpC,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;AAE3C;;;;GAIG;AACH,MAAM,UAAU,oBAAoB;IAClC,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,gBAAgB,CAAC,CAAC;AAC1E,CAAC;AAYD;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,OAA0B,EAAE;IACrD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAChD,MAAM,MAAM,GAAG,oBAAoB,EAAE,CAAC;IACtC,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE;QAChD,KAAK,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,SAAS,CAAC;KACxC,CAAC,CAAC;IACH,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;QACd,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,8BAA8B,MAAM,KAAK,GAAG,CAAC,KAAK,CAAC,OAAO,IAAI,CAAC,CAAC;QACrF,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;IACvB,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;AACrC,CAAC"}