@indigoai-us/hq-cloud 6.14.49 → 6.15.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 (126) hide show
  1. package/.github/workflows/unreleased-commits-nag.yml +256 -0
  2. package/dist/bin/sync-runner-watch-loop.d.ts +12 -0
  3. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  4. package/dist/bin/sync-runner-watch-loop.js +153 -25
  5. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  6. package/dist/bin/sync-runner-watch-routes.d.ts +17 -0
  7. package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
  8. package/dist/bin/sync-runner-watch-routes.js +11 -0
  9. package/dist/bin/sync-runner-watch-routes.js.map +1 -1
  10. package/dist/bin/sync-runner-watch-routes.test.d.ts +2 -0
  11. package/dist/bin/sync-runner-watch-routes.test.d.ts.map +1 -0
  12. package/dist/bin/sync-runner-watch-routes.test.js +51 -0
  13. package/dist/bin/sync-runner-watch-routes.test.js.map +1 -0
  14. package/dist/bin/sync-runner.d.ts +11 -12
  15. package/dist/bin/sync-runner.d.ts.map +1 -1
  16. package/dist/bin/sync-runner.js.map +1 -1
  17. package/dist/bin/sync-runner.test.js +173 -52
  18. package/dist/bin/sync-runner.test.js.map +1 -1
  19. package/dist/cli/reindex-knowledge.d.ts +7 -0
  20. package/dist/cli/reindex-knowledge.d.ts.map +1 -0
  21. package/dist/cli/reindex-knowledge.js +389 -0
  22. package/dist/cli/reindex-knowledge.js.map +1 -0
  23. package/dist/cli/reindex-knowledge.test.d.ts +12 -0
  24. package/dist/cli/reindex-knowledge.test.d.ts.map +1 -0
  25. package/dist/cli/reindex-knowledge.test.js +260 -0
  26. package/dist/cli/reindex-knowledge.test.js.map +1 -0
  27. package/dist/cli/reindex.d.ts.map +1 -1
  28. package/dist/cli/reindex.js +8 -0
  29. package/dist/cli/reindex.js.map +1 -1
  30. package/dist/cli/rescue-classify-ordering.test.js +8 -2
  31. package/dist/cli/rescue-classify-ordering.test.js.map +1 -1
  32. package/dist/cli/rescue-core.d.ts +85 -0
  33. package/dist/cli/rescue-core.d.ts.map +1 -1
  34. package/dist/cli/rescue-core.js +390 -1
  35. package/dist/cli/rescue-core.js.map +1 -1
  36. package/dist/cli/rescue-settings-reconcile.test.d.ts +2 -0
  37. package/dist/cli/rescue-settings-reconcile.test.d.ts.map +1 -0
  38. package/dist/cli/rescue-settings-reconcile.test.js +523 -0
  39. package/dist/cli/rescue-settings-reconcile.test.js.map +1 -0
  40. package/dist/index.d.ts +1 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +1 -1
  43. package/dist/index.js.map +1 -1
  44. package/dist/journal.d.ts.map +1 -1
  45. package/dist/journal.js +81 -51
  46. package/dist/journal.js.map +1 -1
  47. package/dist/journal.test.js +11 -19
  48. package/dist/journal.test.js.map +1 -1
  49. package/dist/sync/event-sync.d.ts +8 -25
  50. package/dist/sync/event-sync.d.ts.map +1 -1
  51. package/dist/sync/event-sync.js +23 -48
  52. package/dist/sync/event-sync.js.map +1 -1
  53. package/dist/sync/event-sync.test.js +6 -32
  54. package/dist/sync/event-sync.test.js.map +1 -1
  55. package/dist/sync/feature-flags.d.ts +20 -117
  56. package/dist/sync/feature-flags.d.ts.map +1 -1
  57. package/dist/sync/feature-flags.js +24 -147
  58. package/dist/sync/feature-flags.js.map +1 -1
  59. package/dist/sync/feature-flags.test.js +15 -61
  60. package/dist/sync/feature-flags.test.js.map +1 -1
  61. package/dist/sync/index.d.ts +1 -1
  62. package/dist/sync/index.d.ts.map +1 -1
  63. package/dist/sync/index.js +1 -1
  64. package/dist/sync/index.js.map +1 -1
  65. package/dist/sync/lease-client.d.ts +67 -0
  66. package/dist/sync/lease-client.d.ts.map +1 -0
  67. package/dist/sync/lease-client.js +158 -0
  68. package/dist/sync/lease-client.js.map +1 -0
  69. package/dist/sync/lease-client.test.d.ts +2 -0
  70. package/dist/sync/lease-client.test.d.ts.map +1 -0
  71. package/dist/sync/lease-client.test.js +119 -0
  72. package/dist/sync/lease-client.test.js.map +1 -0
  73. package/dist/sync/push-receiver.d.ts +8 -19
  74. package/dist/sync/push-receiver.d.ts.map +1 -1
  75. package/dist/sync/push-receiver.js +6 -50
  76. package/dist/sync/push-receiver.js.map +1 -1
  77. package/dist/sync/push-receiver.test.js +6 -57
  78. package/dist/sync/push-receiver.test.js.map +1 -1
  79. package/dist/sync/realtime-rollout.d.ts +85 -0
  80. package/dist/sync/realtime-rollout.d.ts.map +1 -0
  81. package/dist/sync/realtime-rollout.js +206 -0
  82. package/dist/sync/realtime-rollout.js.map +1 -0
  83. package/dist/sync/realtime-rollout.test.d.ts +2 -0
  84. package/dist/sync/realtime-rollout.test.d.ts.map +1 -0
  85. package/dist/sync/realtime-rollout.test.js +71 -0
  86. package/dist/sync/realtime-rollout.test.js.map +1 -0
  87. package/dist/sync/state-store.d.ts +40 -0
  88. package/dist/sync/state-store.d.ts.map +1 -0
  89. package/dist/sync/state-store.js +680 -0
  90. package/dist/sync/state-store.js.map +1 -0
  91. package/dist/sync/state-store.test.d.ts +2 -0
  92. package/dist/sync/state-store.test.d.ts.map +1 -0
  93. package/dist/sync/state-store.test.js +159 -0
  94. package/dist/sync/state-store.test.js.map +1 -0
  95. package/dist/types.d.ts +12 -0
  96. package/dist/types.d.ts.map +1 -1
  97. package/package.json +1 -1
  98. package/src/bin/sync-runner-watch-loop.ts +176 -32
  99. package/src/bin/sync-runner-watch-routes.test.ts +71 -0
  100. package/src/bin/sync-runner-watch-routes.ts +27 -0
  101. package/src/bin/sync-runner.test.ts +188 -53
  102. package/src/bin/sync-runner.ts +11 -12
  103. package/src/cli/reindex-knowledge.test.ts +307 -0
  104. package/src/cli/reindex-knowledge.ts +450 -0
  105. package/src/cli/reindex.ts +9 -0
  106. package/src/cli/rescue-classify-ordering.test.ts +9 -3
  107. package/src/cli/rescue-core.ts +459 -1
  108. package/src/cli/rescue-settings-reconcile.test.ts +637 -0
  109. package/src/index.ts +2 -4
  110. package/src/journal.test.ts +19 -21
  111. package/src/journal.ts +87 -54
  112. package/src/sync/event-sync.test.ts +9 -44
  113. package/src/sync/event-sync.ts +26 -52
  114. package/src/sync/feature-flags.test.ts +22 -72
  115. package/src/sync/feature-flags.ts +32 -199
  116. package/src/sync/index.ts +2 -4
  117. package/src/sync/lease-client.test.ts +128 -0
  118. package/src/sync/lease-client.ts +207 -0
  119. package/src/sync/push-receiver.test.ts +6 -58
  120. package/src/sync/push-receiver.ts +9 -74
  121. package/src/sync/realtime-rollout.test.ts +86 -0
  122. package/src/sync/realtime-rollout.ts +262 -0
  123. package/src/sync/state-store.test.ts +194 -0
  124. package/src/sync/state-store.ts +727 -0
  125. package/src/types.ts +14 -0
  126. package/test/e2e/sync/transient-company-leg.test.ts +8 -4
@@ -97,5 +97,90 @@ export type RescueDecision = {
97
97
  kind: "wholesale-replace-template";
98
98
  rel: "companies/_template";
99
99
  };
100
+ type JsonValue = string | number | boolean | null | JsonValue[] | {
101
+ [k: string]: JsonValue;
102
+ };
103
+ /** Structural deep equality for JSON values (stable, order-sensitive for objects). */
104
+ export declare function jsonDeepEqual(a: JsonValue, b: JsonValue): boolean;
105
+ /**
106
+ * Compute the drift of `ours` relative to `base` (the three-way "ours − base").
107
+ * Returns a partial object holding only the values `ours` added or changed
108
+ * versus `base`, suitable for merging into settings.local.json so Claude Code's
109
+ * higher-precedence Local scope re-supplies them on top of the release baseline.
110
+ *
111
+ * - object: recurse per key; a key absent from `base` is drift wholesale.
112
+ * - array: drift = the elements present in `ours` but not deep-contained in
113
+ * `base`. This matches Claude Code's union merge for permissions/hooks, so a
114
+ * relocated allow/deny entry unions back into the effective settings.
115
+ * - scalar / type-mismatch: drift = ours's value when not deep-equal.
116
+ *
117
+ * `excludePath` names dotted key paths that are NEVER drift (env.PATH — a
118
+ * machine-local value re-composed onto the release baseline, not relocated).
119
+ * Returns `undefined` when there is no drift at this node.
120
+ */
121
+ export declare function settingsDrift(ours: JsonValue, base: JsonValue | undefined, excludePath?: string[], pathPrefix?: string): JsonValue | undefined;
122
+ /**
123
+ * Deep-merge `patch` into `target` in place and return it. Objects merge
124
+ * per-key; arrays union (append patch elements not already deep-present, so a
125
+ * re-run is idempotent and no duplicate permission/hook entries accumulate);
126
+ * scalars are added ONLY when the key is absent from `target`. A scalar the
127
+ * user already set in Local is the higher-precedence value and MUST NOT be
128
+ * clobbered by the lower-precedence Project drift (e.g. Project `model: opus` +
129
+ * Local `model: sonnet` must stay `sonnet` after rescue). Mirrors how Claude
130
+ * Code's Local scope overrides/unions over Project.
131
+ */
132
+ export declare function deepMergeSettings(target: {
133
+ [k: string]: JsonValue;
134
+ }, patch: {
135
+ [k: string]: JsonValue;
136
+ }): {
137
+ [k: string]: JsonValue;
138
+ };
139
+ /**
140
+ * Apply the user's array REMOVALS (elements present in `base` but absent from
141
+ * `ours`) to the corresponding arrays in `theirs`, in place. A union-style Local
142
+ * overlay cannot express a removal, so a permission/hook a user deliberately
143
+ * deleted from the generated settings.json would otherwise be restored by the
144
+ * release baseline and never removable. Filtering `theirs` — the Project file we
145
+ * now own — is the only representation of that intent. Recurses through objects;
146
+ * only acts when all three nodes are arrays (or objects to recurse). Requires a
147
+ * REAL floor base: never call with base = theirs (HEAD), or release-only entries
148
+ * would be mistaken for removals.
149
+ */
150
+ export declare function applyArrayRemovals(theirs: {
151
+ [k: string]: JsonValue;
152
+ }, ours: {
153
+ [k: string]: JsonValue;
154
+ }, base: {
155
+ [k: string]: JsonValue;
156
+ }): void;
157
+ interface ReconcileCtx {
158
+ hqRoot: string;
159
+ srcDir: string;
160
+ env: NodeJS.ProcessEnv;
161
+ historyFloor: string;
162
+ baselineMode: "history_floor" | "head_compare";
163
+ /** cfg.preserveSubpaths — used to honor an explicit `--preserve-subpath` of
164
+ * settings.json (or the whole `.claude` tree). */
165
+ preserveSubpaths: string[];
166
+ }
167
+ /**
168
+ * Post-overlay reconcile for the machine-local, generated `.claude/settings.json`.
169
+ *
170
+ * At entry the overlay has already re-laid the release template (theirs) over
171
+ * settings.json, and the user's settings.local.json has been restored from the
172
+ * preserve shuttle. This function:
173
+ * 1. re-composes env.PATH onto theirs (machine-specific; deliverable 3), and
174
+ * 2. extracts ours's drift versus the last-sync floor (base) — excluding
175
+ * env.PATH — and merges it into settings.local.json (deliverable 2),
176
+ * so the EFFECTIVE merged settings (Project ⊕ Local, per Claude Code's rules)
177
+ * are preserved while the Project base returns to the shipped composition with
178
+ * any newly-released hook registrations wired in (deliverable 1).
179
+ *
180
+ * Best-effort and non-fatal: unreadable/invalid JSON degrades to a
181
+ * PATH-recompose-only pass (never worse than the old behavior), matching
182
+ * rescue's posture elsewhere.
183
+ */
184
+ export declare function reconcileClaudeSettings(ctx: ReconcileCtx, preRescueSettingsJson: string | null, out: (s: string) => void, err: (s: string) => void): void;
100
185
  export {};
101
186
  //# sourceMappingURL=rescue-core.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rescue-core.d.ts","sourceRoot":"","sources":["../../src/cli/rescue-core.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAa7B,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;CAChB;AA0CD,UAAU,MAAM;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,OAAO,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AA2BD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAS3D;AA0FD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAwJtD;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;CAAO,GACrC,eAAe,CAkCjB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,GAAE,IAAI,CAAC,YAAmB,GAAG,OAAO,CAGhG;AAu2BD,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC7B;IAAE,IAAI,EAAE,wBAAwB,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,qBAAqB,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,sBAAsB,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC7D;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,0BAA0B,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACzC;IAAE,IAAI,EAAE,uBAAuB,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,0BAA0B,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,oBAAoB,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC3C;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,oBAAoB,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC3C;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACzC;IAAE,IAAI,EAAE,4BAA4B,CAAC;IAAC,GAAG,EAAE,qBAAqB,CAAA;CAAE,CAAC"}
1
+ {"version":3,"file":"rescue-core.d.ts","sourceRoot":"","sources":["../../src/cli/rescue-core.ts"],"names":[],"mappings":"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"}
@@ -466,6 +466,25 @@ function doRescue(cfg, env, out, err, setTmp) {
466
466
  // which merely starts but exits non-zero used to trigger an enormous cp -a
467
467
  // fallback, then fail later at overlay. Stop before classification/allocation.
468
468
  requireWorkingRsync(env, err);
469
+ // --- Capture the pre-rescue .claude/settings.json (BEFORE the wipe) ---
470
+ // This is `ours` for the post-overlay settings reconcile. It must be read now
471
+ // because the overlay-safe classification deletes/overwrites it before
472
+ // reconcileClaudeSettings runs. `null` when absent (fresh install / narrow
473
+ // mode that excludes .claude).
474
+ const claudeSettingsRel = ".claude/settings.json";
475
+ const claudeSettingsInScope = cfg.narrowPaths.length === 0 || cfg.narrowPaths.includes(".claude");
476
+ let preRescueSettingsJson = null;
477
+ if (claudeSettingsInScope) {
478
+ const p = path.join(hqRoot, claudeSettingsRel);
479
+ if (isFileFollow(p)) {
480
+ try {
481
+ preRescueSettingsJson = fs.readFileSync(p, "utf-8");
482
+ }
483
+ catch {
484
+ preRescueSettingsJson = null;
485
+ }
486
+ }
487
+ }
469
488
  // --- Read prior sync-point metadata (BEFORE the wipe) ---
470
489
  let prevSyncSha = "";
471
490
  let prevSyncSource = "";
@@ -957,6 +976,16 @@ function doRescue(cfg, env, out, err, setTmp) {
957
976
  out(` restored ${rel}\n`);
958
977
  }
959
978
  }
979
+ // --- Reconcile the generated .claude/settings.json ---
980
+ // Runs AFTER the overlay (which re-laid the release template, wiring in any
981
+ // newly-released hook registrations) and AFTER the preserve-subpath restore
982
+ // (which brought back the user's .claude/settings.local.json). Re-composes the
983
+ // machine env.PATH onto the release baseline and relocates genuine user drift
984
+ // (vs the last-sync floor) into settings.local.json so the effective merged
985
+ // settings are preserved. No-op in narrow mode that excludes .claude.
986
+ if (claudeSettingsInScope) {
987
+ reconcileClaudeSettings({ hqRoot, srcDir, env, historyFloor, baselineMode, preserveSubpaths: cfg.preserveSubpaths }, preRescueSettingsJson, out, err);
988
+ }
960
989
  // --- Restore executable bit on shipped shebang scripts (defense-in-depth) ---
961
990
  // rsync -a already propagated each file's upstream git tree mode; this guards
962
991
  // the case where upstream itself committed an executable script as 0644 -- a
@@ -1344,7 +1373,17 @@ function isOverwriteSafe(rel) {
1344
1373
  return (rel === "AGENTS.md" ||
1345
1374
  rel === "USER-GUIDE.md" ||
1346
1375
  rel === "core/docs/hq/USER-GUIDE.md" ||
1347
- rel === "core/policies/_digest.md");
1376
+ rel === "core/policies/_digest.md" ||
1377
+ // `.claude/settings.json` is a machine-local *generated* file (setup.sh
1378
+ // step 3b composes env.PATH into the release template). It always differs
1379
+ // from every release blob because of that PATH, so the generic classifier
1380
+ // would rescue it into personal/ and lay a PATH-less template on top —
1381
+ // dropping the machine PATH and (depending on drift/history state) failing
1382
+ // to wire in newly-released hook registrations. Instead let the overlay
1383
+ // always re-lay the release template (no personal/ copy), then
1384
+ // reconcileClaudeSettings() re-composes env.PATH and relocates genuine user
1385
+ // drift into settings.local.json. See reconcileClaudeSettings.
1386
+ rel === ".claude/settings.json");
1348
1387
  }
1349
1388
  function masterSyncSymlinkTarget(localPath) {
1350
1389
  const st = lstatOrNull(localPath);
@@ -1509,6 +1548,356 @@ function diffAppendClaudeMd(ctx) {
1509
1548
  ctx.out(" diff-appended: .claude/CLAUDE.md additions -> personal/CLAUDE.md\n");
1510
1549
  ctx.counts.claudeDiffAppend += 1;
1511
1550
  }
1551
+ function isPlainObject(v) {
1552
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1553
+ }
1554
+ /** Structural deep equality for JSON values (stable, order-sensitive for objects). */
1555
+ export function jsonDeepEqual(a, b) {
1556
+ if (a === b)
1557
+ return true;
1558
+ if (Array.isArray(a) && Array.isArray(b)) {
1559
+ if (a.length !== b.length)
1560
+ return false;
1561
+ return a.every((x, i) => jsonDeepEqual(x, b[i]));
1562
+ }
1563
+ if (isPlainObject(a) && isPlainObject(b)) {
1564
+ const ak = Object.keys(a);
1565
+ const bk = Object.keys(b);
1566
+ if (ak.length !== bk.length)
1567
+ return false;
1568
+ return ak.every((k) => k in b && jsonDeepEqual(a[k], b[k]));
1569
+ }
1570
+ return false;
1571
+ }
1572
+ function jsonParseOrNull(text) {
1573
+ if (text == null)
1574
+ return null;
1575
+ try {
1576
+ return JSON.parse(text);
1577
+ }
1578
+ catch {
1579
+ return null;
1580
+ }
1581
+ }
1582
+ /**
1583
+ * Compute the drift of `ours` relative to `base` (the three-way "ours − base").
1584
+ * Returns a partial object holding only the values `ours` added or changed
1585
+ * versus `base`, suitable for merging into settings.local.json so Claude Code's
1586
+ * higher-precedence Local scope re-supplies them on top of the release baseline.
1587
+ *
1588
+ * - object: recurse per key; a key absent from `base` is drift wholesale.
1589
+ * - array: drift = the elements present in `ours` but not deep-contained in
1590
+ * `base`. This matches Claude Code's union merge for permissions/hooks, so a
1591
+ * relocated allow/deny entry unions back into the effective settings.
1592
+ * - scalar / type-mismatch: drift = ours's value when not deep-equal.
1593
+ *
1594
+ * `excludePath` names dotted key paths that are NEVER drift (env.PATH — a
1595
+ * machine-local value re-composed onto the release baseline, not relocated).
1596
+ * Returns `undefined` when there is no drift at this node.
1597
+ */
1598
+ export function settingsDrift(ours, base, excludePath = ["env.PATH"], pathPrefix = "") {
1599
+ const here = pathPrefix;
1600
+ if (excludePath.includes(here))
1601
+ return undefined;
1602
+ if (base === undefined) {
1603
+ // Whole subtree is new in ours — but still honor exclusions nested under it.
1604
+ if (isPlainObject(ours)) {
1605
+ const out = {};
1606
+ let any = false;
1607
+ for (const k of Object.keys(ours)) {
1608
+ const child = settingsDrift(ours[k], undefined, excludePath, here ? `${here}.${k}` : k);
1609
+ if (child !== undefined) {
1610
+ out[k] = child;
1611
+ any = true;
1612
+ }
1613
+ }
1614
+ return any ? out : undefined;
1615
+ }
1616
+ return ours;
1617
+ }
1618
+ if (isPlainObject(ours) && isPlainObject(base)) {
1619
+ const out = {};
1620
+ let any = false;
1621
+ for (const k of Object.keys(ours)) {
1622
+ const child = settingsDrift(ours[k], k in base ? base[k] : undefined, excludePath, here ? `${here}.${k}` : k);
1623
+ if (child !== undefined) {
1624
+ out[k] = child;
1625
+ any = true;
1626
+ }
1627
+ }
1628
+ return any ? out : undefined;
1629
+ }
1630
+ if (Array.isArray(ours) && Array.isArray(base)) {
1631
+ const extra = ours.filter((el) => !base.some((b) => jsonDeepEqual(el, b)));
1632
+ return extra.length > 0 ? extra : undefined;
1633
+ }
1634
+ return jsonDeepEqual(ours, base) ? undefined : ours;
1635
+ }
1636
+ /**
1637
+ * Deep-merge `patch` into `target` in place and return it. Objects merge
1638
+ * per-key; arrays union (append patch elements not already deep-present, so a
1639
+ * re-run is idempotent and no duplicate permission/hook entries accumulate);
1640
+ * scalars are added ONLY when the key is absent from `target`. A scalar the
1641
+ * user already set in Local is the higher-precedence value and MUST NOT be
1642
+ * clobbered by the lower-precedence Project drift (e.g. Project `model: opus` +
1643
+ * Local `model: sonnet` must stay `sonnet` after rescue). Mirrors how Claude
1644
+ * Code's Local scope overrides/unions over Project.
1645
+ */
1646
+ export function deepMergeSettings(target, patch) {
1647
+ for (const k of Object.keys(patch)) {
1648
+ const pv = patch[k];
1649
+ const tv = target[k];
1650
+ if (Array.isArray(pv) && Array.isArray(tv)) {
1651
+ for (const el of pv) {
1652
+ if (!tv.some((x) => jsonDeepEqual(x, el)))
1653
+ tv.push(el);
1654
+ }
1655
+ }
1656
+ else if (isPlainObject(pv) && isPlainObject(tv)) {
1657
+ deepMergeSettings(tv, pv);
1658
+ }
1659
+ else if (!(k in target)) {
1660
+ // Key absent from Local — safe to introduce the relocated drift value.
1661
+ target[k] = pv;
1662
+ }
1663
+ // else: Local already defines this scalar (or a differently-typed value);
1664
+ // it is the higher-precedence override and is left untouched.
1665
+ }
1666
+ return target;
1667
+ }
1668
+ /**
1669
+ * Apply the user's array REMOVALS (elements present in `base` but absent from
1670
+ * `ours`) to the corresponding arrays in `theirs`, in place. A union-style Local
1671
+ * overlay cannot express a removal, so a permission/hook a user deliberately
1672
+ * deleted from the generated settings.json would otherwise be restored by the
1673
+ * release baseline and never removable. Filtering `theirs` — the Project file we
1674
+ * now own — is the only representation of that intent. Recurses through objects;
1675
+ * only acts when all three nodes are arrays (or objects to recurse). Requires a
1676
+ * REAL floor base: never call with base = theirs (HEAD), or release-only entries
1677
+ * would be mistaken for removals.
1678
+ */
1679
+ export function applyArrayRemovals(theirs, ours, base) {
1680
+ for (const k of Object.keys(base)) {
1681
+ const bv = base[k];
1682
+ const ov = ours[k];
1683
+ const tv = theirs[k];
1684
+ if (Array.isArray(bv) && Array.isArray(ov) && Array.isArray(tv)) {
1685
+ const removed = bv.filter((b) => !ov.some((o) => jsonDeepEqual(o, b)));
1686
+ if (removed.length > 0) {
1687
+ theirs[k] = tv.filter((t) => !removed.some((r) => jsonDeepEqual(r, t)));
1688
+ }
1689
+ }
1690
+ else if (isPlainObject(bv) && isPlainObject(ov) && isPlainObject(tv)) {
1691
+ applyArrayRemovals(tv, ov, bv);
1692
+ }
1693
+ }
1694
+ }
1695
+ /**
1696
+ * Re-compose the machine env.PATH for the freshly-overlaid release template.
1697
+ * Runs the SAME composer setup.sh uses (core/scripts/compose-settings-path.sh)
1698
+ * with the machine's own PATH as the base — preferring the pre-rescue snapshot
1699
+ * so a working PATH is never degraded by a possibly-truncated rescue-process
1700
+ * PATH — so the value is always machine-specific and never a hardcoded release
1701
+ * PATH (the release template ships none). Prefers the release clone's composer,
1702
+ * falls back to the overlaid one; if neither runs, carries `basePath` verbatim.
1703
+ */
1704
+ function composeSettingsPath(srcDir, hqRoot, basePath, env) {
1705
+ const candidates = [
1706
+ path.join(srcDir, "core", "scripts", "compose-settings-path.sh"),
1707
+ path.join(hqRoot, "core", "scripts", "compose-settings-path.sh"),
1708
+ ];
1709
+ for (const script of candidates) {
1710
+ if (!isFileFollow(script))
1711
+ continue;
1712
+ const r = run("bash", [script, basePath], { env });
1713
+ const composed = r.stdout.replace(/\n+$/, "");
1714
+ if (r.status === 0 && composed)
1715
+ return composed;
1716
+ }
1717
+ return basePath;
1718
+ }
1719
+ /** True when `.claude/settings.json` is covered by an EXPLICIT preserve subpath
1720
+ * (the caller asked rescue to keep it, so the reconcile must not rewrite it).
1721
+ * The always-on carve-outs never cover settings.json, so any match here is a
1722
+ * deliberate caller request. */
1723
+ function settingsExplicitlyPreserved(preserveSubpaths) {
1724
+ const rel = ".claude/settings.json";
1725
+ for (const sp of preserveSubpaths) {
1726
+ const s = sp.replace(/\/$/, "");
1727
+ if (s === rel || s === ".claude" || rel.startsWith(s + "/"))
1728
+ return true;
1729
+ }
1730
+ return false;
1731
+ }
1732
+ /**
1733
+ * Post-overlay reconcile for the machine-local, generated `.claude/settings.json`.
1734
+ *
1735
+ * At entry the overlay has already re-laid the release template (theirs) over
1736
+ * settings.json, and the user's settings.local.json has been restored from the
1737
+ * preserve shuttle. This function:
1738
+ * 1. re-composes env.PATH onto theirs (machine-specific; deliverable 3), and
1739
+ * 2. extracts ours's drift versus the last-sync floor (base) — excluding
1740
+ * env.PATH — and merges it into settings.local.json (deliverable 2),
1741
+ * so the EFFECTIVE merged settings (Project ⊕ Local, per Claude Code's rules)
1742
+ * are preserved while the Project base returns to the shipped composition with
1743
+ * any newly-released hook registrations wired in (deliverable 1).
1744
+ *
1745
+ * Best-effort and non-fatal: unreadable/invalid JSON degrades to a
1746
+ * PATH-recompose-only pass (never worse than the old behavior), matching
1747
+ * rescue's posture elsewhere.
1748
+ */
1749
+ export function reconcileClaudeSettings(ctx, preRescueSettingsJson, out, err) {
1750
+ const { hqRoot, srcDir, env, historyFloor, baselineMode, preserveSubpaths } = ctx;
1751
+ const settingsPath = path.join(hqRoot, ".claude", "settings.json");
1752
+ const localPath = path.join(hqRoot, ".claude", "settings.local.json");
1753
+ // The caller explicitly asked to keep settings.json (via `--preserve-subpath
1754
+ // .claude/settings.json` or `--preserve-subpath .claude`). The shuttle already
1755
+ // restored their file; honor that and do not rewrite it (nor land new
1756
+ // registrations — that is the point of an explicit preserve).
1757
+ if (settingsExplicitlyPreserved(preserveSubpaths)) {
1758
+ out("==> Skipped .claude/settings.json reconcile (explicitly preserved by --preserve-subpath)\n");
1759
+ return;
1760
+ }
1761
+ const overlaidTemplateExists = isFileFollow(settingsPath);
1762
+ if (!overlaidTemplateExists) {
1763
+ // The release no longer ships .claude/settings.json, but the overwrite-safe
1764
+ // classification already deleted the local file. Restore the pre-rescue copy
1765
+ // so the machine PATH and every user customization survive (critical under
1766
+ // --no-backup, where no snapshot exists to recover from).
1767
+ if (preRescueSettingsJson !== null) {
1768
+ try {
1769
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
1770
+ fs.writeFileSync(settingsPath, preRescueSettingsJson);
1771
+ out("==> Release no longer ships .claude/settings.json; restored your existing settings.json (no reconcile).\n");
1772
+ }
1773
+ catch {
1774
+ err(" WARN: could not restore pre-rescue .claude/settings.json after the release dropped the template\n");
1775
+ }
1776
+ }
1777
+ return;
1778
+ }
1779
+ const theirsRaw = (() => {
1780
+ try {
1781
+ return fs.readFileSync(settingsPath, "utf-8");
1782
+ }
1783
+ catch {
1784
+ return null;
1785
+ }
1786
+ })();
1787
+ const theirs = jsonParseOrNull(theirsRaw);
1788
+ if (!isPlainObject(theirs)) {
1789
+ err(" WARN: overlaid .claude/settings.json is not valid JSON; skipping settings reconcile\n");
1790
+ return;
1791
+ }
1792
+ const ours = jsonParseOrNull(preRescueSettingsJson);
1793
+ const oursIsValid = isPlainObject(ours);
1794
+ // The pre-rescue settings.json existed but was not parseable, and the overlay
1795
+ // already replaced it. Preserve the raw bytes under personal/ (the same place
1796
+ // a user-edit rescue would have put them) so a temporarily-malformed file is
1797
+ // never destroyed by a --no-backup run. Then continue with a PATH-recompose
1798
+ // pass so the machine still works.
1799
+ if (preRescueSettingsJson !== null && !oursIsValid) {
1800
+ const backup = path.join(hqRoot, "personal", ".claude", "settings.json");
1801
+ try {
1802
+ fs.mkdirSync(path.dirname(backup), { recursive: true });
1803
+ fs.writeFileSync(backup, preRescueSettingsJson);
1804
+ err(" WARN: pre-rescue .claude/settings.json was not valid JSON; preserved a copy at personal/.claude/settings.json\n");
1805
+ }
1806
+ catch {
1807
+ err(" WARN: pre-rescue .claude/settings.json was not valid JSON and could not be backed up\n");
1808
+ }
1809
+ }
1810
+ const oursEnvPath = oursIsValid && isPlainObject(ours.env) && typeof ours.env.PATH === "string"
1811
+ ? ours.env.PATH
1812
+ : undefined;
1813
+ // 1) Re-compose env.PATH onto the release baseline. Base = the machine's own
1814
+ // prior PATH snapshot when present, else this process's PATH.
1815
+ const basePath = oursEnvPath ?? env.PATH ?? "";
1816
+ const composedPath = composeSettingsPath(srcDir, hqRoot, basePath, env);
1817
+ if (composedPath) {
1818
+ const envObj = isPlainObject(theirs.env) ? theirs.env : (theirs.env = {});
1819
+ envObj.PATH = composedPath;
1820
+ }
1821
+ // 2) Relocate genuine user drift (ours vs floor baseline) into settings.local,
1822
+ // and apply user array removals to the Project baseline.
1823
+ let driftKeys = 0;
1824
+ let relocationFailed = false;
1825
+ if (oursIsValid) {
1826
+ // realBase = settings.json at the last-sync floor (only trustworthy in
1827
+ // history_floor mode). Removals require it; additions fall back to theirs.
1828
+ let realBase;
1829
+ if (baselineMode === "history_floor" && historyFloor) {
1830
+ const show = run("git", ["show", `${historyFloor}:.claude/settings.json`], {
1831
+ cwd: srcDir,
1832
+ env,
1833
+ });
1834
+ if (show.status === 0)
1835
+ realBase = jsonParseOrNull(show.stdout);
1836
+ }
1837
+ // Removals: only with a REAL floor base (never theirs — release-only entries
1838
+ // would look like removals).
1839
+ if (isPlainObject(realBase)) {
1840
+ applyArrayRemovals(theirs, ours, realBase);
1841
+ }
1842
+ // Additions drift: base = realBase when available, else theirs (conservative
1843
+ // head_compare fallback — never loses a customization). See design note.
1844
+ const additionsBase = isPlainObject(realBase) ? realBase : theirs;
1845
+ const drift = settingsDrift(ours, additionsBase);
1846
+ if (isPlainObject(drift) && Object.keys(drift).length > 0) {
1847
+ const existingLocal = jsonParseOrNull((() => {
1848
+ try {
1849
+ return isFileFollow(localPath) ? fs.readFileSync(localPath, "utf-8") : null;
1850
+ }
1851
+ catch {
1852
+ return null;
1853
+ }
1854
+ })());
1855
+ const localObj = isPlainObject(existingLocal) ? existingLocal : {};
1856
+ deepMergeSettings(localObj, drift);
1857
+ driftKeys = Object.keys(drift).length;
1858
+ try {
1859
+ fs.mkdirSync(path.dirname(localPath), { recursive: true });
1860
+ fs.writeFileSync(localPath, JSON.stringify(localObj, null, 2) + "\n");
1861
+ }
1862
+ catch {
1863
+ relocationFailed = true;
1864
+ err(" WARN: could not write .claude/settings.local.json during settings reconcile\n");
1865
+ }
1866
+ }
1867
+ }
1868
+ // If drift relocation into settings.local.json failed, DO NOT overwrite
1869
+ // settings.json with the release baseline — that would drop the drift with no
1870
+ // on-disk copy under --no-backup. Restore the pre-rescue file so nothing is
1871
+ // lost; the update simply isn't applied to settings this run (the user can
1872
+ // retry once settings.local.json is writable).
1873
+ if (relocationFailed) {
1874
+ if (preRescueSettingsJson !== null) {
1875
+ try {
1876
+ fs.writeFileSync(settingsPath, preRescueSettingsJson);
1877
+ }
1878
+ catch {
1879
+ err(" WARN: could not restore pre-rescue .claude/settings.json after a failed drift relocation\n");
1880
+ }
1881
+ }
1882
+ err(" WARN: kept your original .claude/settings.json (settings update not applied — settings.local.json was unwritable)\n");
1883
+ return;
1884
+ }
1885
+ // Write the re-composed release baseline back to settings.json.
1886
+ try {
1887
+ fs.writeFileSync(settingsPath, JSON.stringify(theirs, null, 2) + "\n");
1888
+ }
1889
+ catch {
1890
+ err(" WARN: could not write re-composed .claude/settings.json during reconcile\n");
1891
+ return;
1892
+ }
1893
+ if (driftKeys > 0) {
1894
+ out(`==> Reconciled .claude/settings.json to release baseline (env.PATH re-composed; ` +
1895
+ `relocated ${driftKeys} drift key(s) -> .claude/settings.local.json)\n`);
1896
+ }
1897
+ else {
1898
+ out("==> Reconciled .claude/settings.json to release baseline (env.PATH re-composed; no local drift to relocate)\n");
1899
+ }
1900
+ }
1512
1901
  /**
1513
1902
  * Defense-in-depth for executable scaffold scripts. `rescue` rebuilds the tree
1514
1903
  * with `git clone` + `rsync -a`, faithfully propagating each file's git tree