@indigoai-us/hq-cloud 6.4.0 → 6.6.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 (50) hide show
  1. package/dist/bin/sync-runner.d.ts +4 -35
  2. package/dist/bin/sync-runner.d.ts.map +1 -1
  3. package/dist/bin/sync-runner.js +14 -104
  4. package/dist/bin/sync-runner.js.map +1 -1
  5. package/dist/bin/sync-runner.test.js +19 -0
  6. package/dist/bin/sync-runner.test.js.map +1 -1
  7. package/dist/cli/rescue-classify-ordering.test.d.ts +2 -0
  8. package/dist/cli/rescue-classify-ordering.test.d.ts.map +1 -0
  9. package/dist/cli/rescue-classify-ordering.test.js +241 -0
  10. package/dist/cli/rescue-classify-ordering.test.js.map +1 -0
  11. package/dist/cli/rescue-core.js +68 -22
  12. package/dist/cli/rescue-core.js.map +1 -1
  13. package/dist/cli/sync-scope.test.js +67 -0
  14. package/dist/cli/sync-scope.test.js.map +1 -1
  15. package/dist/cli/sync.d.ts +19 -0
  16. package/dist/cli/sync.d.ts.map +1 -1
  17. package/dist/cli/sync.js +62 -19
  18. package/dist/cli/sync.js.map +1 -1
  19. package/dist/index.d.ts +4 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +7 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/s3.d.ts.map +1 -1
  24. package/dist/s3.js +15 -5
  25. package/dist/s3.js.map +1 -1
  26. package/dist/s3.test.js +71 -2
  27. package/dist/s3.test.js.map +1 -1
  28. package/dist/scope-shrink.d.ts +70 -7
  29. package/dist/scope-shrink.d.ts.map +1 -1
  30. package/dist/scope-shrink.js +102 -23
  31. package/dist/scope-shrink.js.map +1 -1
  32. package/dist/scope-shrink.test.js +63 -0
  33. package/dist/scope-shrink.test.js.map +1 -1
  34. package/dist/sync/pull-scope.d.ts +50 -0
  35. package/dist/sync/pull-scope.d.ts.map +1 -0
  36. package/dist/sync/pull-scope.js +129 -0
  37. package/dist/sync/pull-scope.js.map +1 -0
  38. package/package.json +1 -1
  39. package/src/bin/sync-runner.test.ts +23 -0
  40. package/src/bin/sync-runner.ts +19 -116
  41. package/src/cli/rescue-classify-ordering.test.ts +263 -0
  42. package/src/cli/rescue-core.ts +82 -26
  43. package/src/cli/sync-scope.test.ts +84 -0
  44. package/src/cli/sync.ts +90 -17
  45. package/src/index.ts +11 -0
  46. package/src/s3.test.ts +91 -1
  47. package/src/s3.ts +15 -5
  48. package/src/scope-shrink.test.ts +71 -0
  49. package/src/scope-shrink.ts +164 -20
  50. package/src/sync/pull-scope.ts +161 -0
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Integration tests for the rescue classifier's fail-safe ordering and its
3
+ * handling of nested directory symlinks (src/cli/rescue-core.ts).
4
+ *
5
+ * Regression for DEV-1767 (feedback_aa768683): `hq rescue` choked on a
6
+ * symlinked DIRECTORY under core/ — a reindex artifact such as
7
+ * `core/knowledge/ad-creative-engine -> ../../personal/knowledge/...` — throwing
8
+ * `Error: Path is a directory` mid-pass *after* it had already deleted ~10 core
9
+ * scaffold files, leaving HQ half-applied. `--check` classified the same tree
10
+ * cleanly, so the dry-run gave false confidence.
11
+ *
12
+ * Two invariants are locked here:
13
+ * 1. A directory symlink under core/ is classified as a droppable reindex
14
+ * symlink (never followed, never file-read, never a crash) — exactly like
15
+ * a file symlink. The personal/ target it points at is left untouched.
16
+ * 2. Classification is read-only and runs to completion BEFORE any destructive
17
+ * apply. A classifier exception therefore deletes NOTHING (no half-applied
18
+ * wipe), and the same classify code path backs both the live run and the
19
+ * `--dry-run` plan (parity).
20
+ *
21
+ * Like the sibling rescue tests, the source clone is shimmed to a local fixture
22
+ * repo and the rescue runs in-process so we can assert its on-disk effects.
23
+ */
24
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
25
+ import { execFileSync } from "child_process";
26
+ import * as fs from "fs";
27
+ import * as os from "os";
28
+ import * as path from "path";
29
+ import { runRescue } from "./rescue-core.js";
30
+ function hasGit() {
31
+ try {
32
+ execFileSync("git", ["--version"], { stdio: "ignore" });
33
+ return true;
34
+ }
35
+ catch {
36
+ return false;
37
+ }
38
+ }
39
+ const gitAvailable = hasGit();
40
+ function runRescueCapture(argv, env) {
41
+ let stdout = "";
42
+ let stderr = "";
43
+ const origOut = process.stdout.write.bind(process.stdout);
44
+ const origErr = process.stderr.write.bind(process.stderr);
45
+ process.stdout.write = ((chunk) => {
46
+ stdout += String(chunk);
47
+ return true;
48
+ });
49
+ process.stderr.write = ((chunk) => {
50
+ stderr += String(chunk);
51
+ return true;
52
+ });
53
+ let status;
54
+ let threw;
55
+ try {
56
+ status = runRescue(argv, { env }).status;
57
+ }
58
+ catch (e) {
59
+ threw = e;
60
+ }
61
+ finally {
62
+ process.stdout.write = origOut;
63
+ process.stderr.write = origErr;
64
+ }
65
+ return { status, stdout, stderr, threw };
66
+ }
67
+ const lexists = (p) => {
68
+ try {
69
+ fs.lstatSync(p);
70
+ return true;
71
+ }
72
+ catch {
73
+ return false;
74
+ }
75
+ };
76
+ describe.skipIf(!gitAvailable)("rescue classify-before-delete + dir-symlink handling", () => {
77
+ let workDir;
78
+ let upstream;
79
+ let shimDir;
80
+ let floorSha;
81
+ let baseEnv;
82
+ let caseSeq = 0;
83
+ const git = (cwd, ...args) => execFileSync("git", args, {
84
+ cwd,
85
+ stdio: ["ignore", "pipe", "pipe"],
86
+ env: {
87
+ ...process.env,
88
+ GIT_AUTHOR_NAME: "t",
89
+ GIT_AUTHOR_EMAIL: "t@t",
90
+ GIT_COMMITTER_NAME: "t",
91
+ GIT_COMMITTER_EMAIL: "t@t",
92
+ },
93
+ })
94
+ .toString()
95
+ .trim();
96
+ beforeAll(() => {
97
+ workDir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-rescue-ordering-"));
98
+ // Upstream: floor has core/a.md="v1"; HEAD advances it to "v2". core/b.md is
99
+ // stable across both. The symlink fixtures never appear upstream — they only
100
+ // exist in the local HQ root as reindex artifacts.
101
+ upstream = path.join(workDir, "upstream");
102
+ fs.mkdirSync(path.join(upstream, "core"), { recursive: true });
103
+ git(workDir, "init", "-b", "main", "upstream");
104
+ fs.writeFileSync(path.join(upstream, "core/a.md"), "v1\n");
105
+ fs.writeFileSync(path.join(upstream, "core/b.md"), "beta\n");
106
+ git(upstream, "add", "-A");
107
+ git(upstream, "commit", "-m", "floor");
108
+ floorSha = git(upstream, "rev-parse", "HEAD");
109
+ fs.writeFileSync(path.join(upstream, "core/a.md"), "v2\n");
110
+ git(upstream, "add", "-A");
111
+ git(upstream, "commit", "-m", "head");
112
+ const realGit = execFileSync("bash", ["-lc", "command -v git"]).toString().trim() || "/usr/bin/git";
113
+ shimDir = path.join(workDir, "shim");
114
+ fs.mkdirSync(shimDir, { recursive: true });
115
+ const shim = `#!/usr/bin/env bash
116
+ if [ "$1" = "clone" ]; then
117
+ args=()
118
+ for a in "$@"; do
119
+ case "$a" in
120
+ https://github.com/*) a=${JSON.stringify(upstream)} ;;
121
+ esac
122
+ args+=("$a")
123
+ done
124
+ exec ${JSON.stringify(realGit)} "\${args[@]}"
125
+ fi
126
+ exec ${JSON.stringify(realGit)} "$@"
127
+ `;
128
+ fs.writeFileSync(path.join(shimDir, "git"), shim, { mode: 0o755 });
129
+ baseEnv = { ...process.env, PATH: `${shimDir}:${process.env.PATH ?? ""}` };
130
+ });
131
+ afterAll(() => {
132
+ if (workDir)
133
+ fs.rmSync(workDir, { recursive: true, force: true });
134
+ });
135
+ /**
136
+ * Fresh HQ root per case (live runs mutate). core/a.md matches the floor
137
+ * (unchanged vs baseline, differs from HEAD -> "delete + replace"); core/b.md
138
+ * is byte-identical to upstream ("unchanged, preserved in place"). The caller
139
+ * adds whatever symlink/extra files the case needs.
140
+ */
141
+ function makeHqRoot() {
142
+ caseSeq += 1;
143
+ const hqRoot = path.join(workDir, `hq-${caseSeq}`);
144
+ fs.mkdirSync(path.join(hqRoot, "core"), { recursive: true });
145
+ fs.mkdirSync(path.join(hqRoot, "companies"), { recursive: true });
146
+ fs.mkdirSync(path.join(hqRoot, "personal/knowledge"), { recursive: true });
147
+ fs.writeFileSync(path.join(hqRoot, "core/a.md"), "v1\n");
148
+ fs.writeFileSync(path.join(hqRoot, "core/b.md"), "beta\n");
149
+ return hqRoot;
150
+ }
151
+ const liveArgs = (hqRoot, extra = []) => [
152
+ "--hq-root", hqRoot,
153
+ "--source", "test/repo",
154
+ "--ref", "main",
155
+ "--floor-sha", floorSha,
156
+ "--paths", "core",
157
+ "--yes",
158
+ "--no-backup",
159
+ ...extra,
160
+ ];
161
+ it("drops a DIRECTORY symlink under core/ as a reindex artifact (no crash, no half-apply)", () => {
162
+ const hqRoot = makeHqRoot();
163
+ // The exact DEV-1767 shape: a directory symlink nested under core/ pointing
164
+ // into personal/. Its target exists and must survive untouched.
165
+ const target = path.join(hqRoot, "personal/knowledge/ad-creative-engine");
166
+ fs.mkdirSync(target, { recursive: true });
167
+ fs.writeFileSync(path.join(target, "keep.md"), "owned by personal\n");
168
+ fs.mkdirSync(path.join(hqRoot, "core/knowledge"), { recursive: true });
169
+ const link = path.join(hqRoot, "core/knowledge/ad-creative-engine");
170
+ fs.symlinkSync("../../personal/knowledge/ad-creative-engine", link);
171
+ const r = runRescueCapture(liveArgs(hqRoot), baseEnv);
172
+ const out = `${r.stdout}\n${r.stderr}`;
173
+ expect(r.threw, out).toBeUndefined();
174
+ expect(r.status, out).toBe(0);
175
+ // The bash-era failure signature must never appear.
176
+ expect(out).not.toMatch(/Path is a directory/);
177
+ expect(out).not.toMatch(/EISDIR/);
178
+ // Classified + dropped as exactly one reindex symlink.
179
+ expect(out).toMatch(/reindex symlinks dropped:\s+1 entries/);
180
+ // Symlink itself is gone; its personal/ target is untouched.
181
+ expect(lexists(link)).toBe(false);
182
+ expect(fs.readFileSync(path.join(target, "keep.md"), "utf-8")).toBe("owned by personal\n");
183
+ // Real core files still classified correctly around it.
184
+ expect(fs.existsSync(path.join(hqRoot, "core/a.md"))).toBe(true);
185
+ expect(fs.existsSync(path.join(hqRoot, "core/b.md"))).toBe(true);
186
+ });
187
+ it("drops a FILE symlink under core/ the same way (file-symlink behavior unchanged)", () => {
188
+ const hqRoot = makeHqRoot();
189
+ fs.writeFileSync(path.join(hqRoot, "personal/knowledge/note.md"), "personal note\n");
190
+ const link = path.join(hqRoot, "core/note.md");
191
+ fs.symlinkSync("../personal/knowledge/note.md", link);
192
+ const r = runRescueCapture(liveArgs(hqRoot), baseEnv);
193
+ const out = `${r.stdout}\n${r.stderr}`;
194
+ expect(r.threw, out).toBeUndefined();
195
+ expect(r.status, out).toBe(0);
196
+ expect(out).toMatch(/reindex symlinks dropped:\s+1 entries/);
197
+ expect(lexists(link)).toBe(false);
198
+ expect(fs.readFileSync(path.join(hqRoot, "personal/knowledge/note.md"), "utf-8")).toBe("personal note\n");
199
+ });
200
+ it("a classifier exception deletes NOTHING (classify fully before any destructive apply)", () => {
201
+ const hqRoot = makeHqRoot();
202
+ // core/a.md is classified BEFORE core/zzz.md (sorted walk) as a
203
+ // "delete + replace". core/zzz.md then throws during classification. If the
204
+ // wipe were interleaved (old behavior), a.md would already be gone.
205
+ fs.writeFileSync(path.join(hqRoot, "core/zzz.md"), "trigger\n");
206
+ const r = runRescueCapture(liveArgs(hqRoot), {
207
+ ...baseEnv,
208
+ HQ_RESCUE_FAULT_AT_REL: "core/zzz.md",
209
+ });
210
+ const out = `${r.stdout}\n${r.stderr}`;
211
+ // The injected classifier fault aborts the run.
212
+ expect(r.threw, out).toBeDefined();
213
+ expect(String(r.threw)).toMatch(/fault injected at core\/zzz\.md/);
214
+ // Crucially: the file that was already classified for deletion still exists,
215
+ // untouched — no half-applied wipe.
216
+ expect(fs.existsSync(path.join(hqRoot, "core/a.md"))).toBe(true);
217
+ expect(fs.readFileSync(path.join(hqRoot, "core/a.md"), "utf-8")).toBe("v1\n");
218
+ expect(fs.existsSync(path.join(hqRoot, "core/b.md"))).toBe(true);
219
+ expect(fs.existsSync(path.join(hqRoot, "core/zzz.md"))).toBe(true);
220
+ });
221
+ it("dry-run classifies the dir-symlink identically to the live run and changes nothing", () => {
222
+ const hqRoot = makeHqRoot();
223
+ const target = path.join(hqRoot, "personal/knowledge/ad-creative-engine");
224
+ fs.mkdirSync(target, { recursive: true });
225
+ fs.mkdirSync(path.join(hqRoot, "core/knowledge"), { recursive: true });
226
+ const link = path.join(hqRoot, "core/knowledge/ad-creative-engine");
227
+ fs.symlinkSync("../../personal/knowledge/ad-creative-engine", link);
228
+ const r = runRescueCapture(liveArgs(hqRoot, ["--dry-run"]), baseEnv);
229
+ const out = `${r.stdout}\n${r.stderr}`;
230
+ expect(r.threw, out).toBeUndefined();
231
+ expect(r.status, out).toBe(0);
232
+ // Dry-run surfaces the SAME classification the live pass acts on.
233
+ expect(out).toContain("drop reindex symlink: core/knowledge/ad-creative-engine");
234
+ expect(out).toMatch(/reindex symlinks dropped:\s+1 entries/);
235
+ expect(out).toMatch(/DRY RUN complete\. No filesystem changes made\./);
236
+ // ...but touches nothing: the symlink (and everything else) is still on disk.
237
+ expect(lexists(link)).toBe(true);
238
+ expect(fs.existsSync(path.join(hqRoot, "core/a.md"))).toBe(true);
239
+ });
240
+ });
241
+ //# sourceMappingURL=rescue-classify-ordering.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rescue-classify-ordering.test.js","sourceRoot":"","sources":["../../src/cli/rescue-classify-ordering.test.ts"],"names":[],"mappings":"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"}
@@ -609,6 +609,7 @@ function doRescue(cfg, env, out, err, setTmp) {
609
609
  unchangedList,
610
610
  appendLog,
611
611
  out,
612
+ actions: [],
612
613
  };
613
614
  // --- Pre-operation safety snapshot (BEFORE any destructive op) ---
614
615
  let backupDir = "";
@@ -636,7 +637,17 @@ function doRescue(cfg, env, out, err, setTmp) {
636
637
  }
637
638
  out(` snapshot complete (restore any file: cp "${backupDir}/<relpath>" "${hqRoot}/<relpath>")\n`);
638
639
  }
639
- // --- Walk + classify ---
640
+ // --- Walk + classify (PHASE 1: read-only) ---
641
+ // Classification mutates nothing on disk: every destructive op (delete,
642
+ // rescue-move, conflict-quarantine, symlink-drop) is recorded as a deferred
643
+ // closure in `ctx.actions` and executed later (PHASE 2). This is the
644
+ // ordering-safety invariant: if classification throws partway through the
645
+ // wipe set (e.g. an unreadable entry, a future unhandled file shape), it
646
+ // aborts HERE — before a single file has been touched — instead of leaving a
647
+ // half-applied wipe like the pre-port bash classifier did (DEV-1767:
648
+ // `Error: Path is a directory` on a nested dir-symlink, thrown after ~10 core
649
+ // files were already deleted). Dry-run runs this exact same pass and then
650
+ // returns, so the `--check` plan can never miss a condition the live pass hits.
640
651
  out("\n");
641
652
  if (wipeToplevel.length === 0) {
642
653
  out("==> Wipe set is empty; nothing to process or overlay.\n");
@@ -708,6 +719,13 @@ function doRescue(cfg, env, out, err, setTmp) {
708
719
  out("==> DRY RUN complete. No filesystem changes made.\n");
709
720
  return { status: 0 };
710
721
  }
722
+ // --- Apply classified actions (PHASE 2: destructive) ---
723
+ // The full wipe set classified without throwing, so it is now safe to mutate.
724
+ // Actions run in classification (walk) order — the same order the old
725
+ // interleaved walk mutated in — so per-file output and on-disk results are
726
+ // unchanged versus before the two-phase split.
727
+ for (const act of ctx.actions)
728
+ act();
711
729
  // --- Back up preserve-subpaths to a mktemp shuttle ---
712
730
  const shuttle = path.join(tmpdir, "preserve");
713
731
  fs.mkdirSync(shuttle, { recursive: true });
@@ -1139,10 +1157,21 @@ function conflictOne(ctx, rel) {
1139
1157
  ctx.appendLog(`conflicted\t${rel}\t-> ${destRel}\n`);
1140
1158
  }
1141
1159
  // --- classify + act on one file (the per-file workhorse) ---
1160
+ //
1161
+ // Read-only by contract: this records intent (counts + dry-run plan lines +
1162
+ // deferred `ctx.actions` closures) but never mutates the filesystem itself. The
1163
+ // closures are run later by the PHASE 2 apply loop, only once the whole wipe
1164
+ // set has classified without throwing. Keep it that way — a stray `fs.rmSync`
1165
+ // here re-opens the half-applied-wipe hole this split closed (DEV-1767).
1142
1166
  function processOne(ctx, rel) {
1143
1167
  const { cfg } = ctx;
1144
1168
  const localPath = path.join(ctx.hqRoot, rel);
1145
1169
  const srcPath = path.join(ctx.srcDir, rel);
1170
+ // Test-only fault seam: prove the classify-before-delete ordering invariant
1171
+ // by forcing a classifier throw at a chosen entry. Never set in production.
1172
+ if (ctx.env.HQ_RESCUE_FAULT_AT_REL && rel === ctx.env.HQ_RESCUE_FAULT_AT_REL) {
1173
+ throw new Error(`rescue classifier fault injected at ${rel} (HQ_RESCUE_FAULT_AT_REL)`);
1174
+ }
1146
1175
  if (isUnderPreserve(cfg, rel))
1147
1176
  return;
1148
1177
  // Conflict-resolution artifacts (`<name>.conflict-<ts>-<peer>.<ext>`).
@@ -1152,7 +1181,7 @@ function processOne(ctx, rel) {
1152
1181
  ctx.out(` drop conflict artifact: ${rel}\n`);
1153
1182
  }
1154
1183
  else {
1155
- fs.rmSync(localPath, { force: true });
1184
+ ctx.actions.push(() => fs.rmSync(localPath, { force: true }));
1156
1185
  }
1157
1186
  return;
1158
1187
  }
@@ -1162,7 +1191,7 @@ function processOne(ctx, rel) {
1162
1191
  ctx.out(` skip script-managed (rewrites at stamp step): ${rel}\n`);
1163
1192
  }
1164
1193
  else {
1165
- fs.rmSync(localPath, { force: true });
1194
+ ctx.actions.push(() => fs.rmSync(localPath, { force: true }));
1166
1195
  }
1167
1196
  return;
1168
1197
  }
@@ -1182,8 +1211,10 @@ function processOne(ctx, rel) {
1182
1211
  ctx.out(` drop reindex symlink: ${rel} -> ${tgt}\n`);
1183
1212
  }
1184
1213
  else {
1185
- fs.rmSync(localPath, { force: true });
1186
- ctx.appendLog(`symlink-dropped\t${rel}\t(reindex regenerable)\n`);
1214
+ ctx.actions.push(() => {
1215
+ fs.rmSync(localPath, { force: true });
1216
+ ctx.appendLog(`symlink-dropped\t${rel}\t(reindex regenerable)\n`);
1217
+ });
1187
1218
  }
1188
1219
  }
1189
1220
  return;
@@ -1233,8 +1264,10 @@ function processOne(ctx, rel) {
1233
1264
  ctx.out(` cloud-symlink reconciled (unchanged): ${rel} (hq-symlink: marker matches upstream target)\n`);
1234
1265
  }
1235
1266
  else {
1236
- fs.rmSync(localPath, { force: true });
1237
- ctx.appendLog(`cloud-symlink-reconciled\t${rel}\t(hq-symlink: marker matches upstream target; overlay re-lays symlink)\n`);
1267
+ ctx.actions.push(() => {
1268
+ fs.rmSync(localPath, { force: true });
1269
+ ctx.appendLog(`cloud-symlink-reconciled\t${rel}\t(hq-symlink: marker matches upstream target; overlay re-lays symlink)\n`);
1270
+ });
1238
1271
  }
1239
1272
  return;
1240
1273
  }
@@ -1245,8 +1278,10 @@ function processOne(ctx, rel) {
1245
1278
  ctx.out(` drift reconciled (identical to upstream HEAD; no rescue): ${rel}\n`);
1246
1279
  }
1247
1280
  else {
1248
- fs.rmSync(localPath, { force: true });
1249
- ctx.appendLog(`drift-reconciled\t${rel}\t(identical to upstream HEAD; drifted from floor only — no personal/ copy)\n`);
1281
+ ctx.actions.push(() => {
1282
+ fs.rmSync(localPath, { force: true });
1283
+ ctx.appendLog(`drift-reconciled\t${rel}\t(identical to upstream HEAD; drifted from floor only — no personal/ copy)\n`);
1284
+ });
1250
1285
  }
1251
1286
  return;
1252
1287
  }
@@ -1271,18 +1306,20 @@ function processOne(ctx, rel) {
1271
1306
  }
1272
1307
  else {
1273
1308
  if (rel === ".claude/CLAUDE.md") {
1274
- rescueOne(ctx, rel);
1309
+ ctx.actions.push(() => rescueOne(ctx, rel));
1275
1310
  }
1276
1311
  else if (isOverwriteSafe(rel)) {
1277
- fs.rmSync(localPath, { force: true });
1278
- ctx.counts.userOverwrite += 1;
1279
- ctx.appendLog(`overwritten\t${rel}\t(overwrite-safe; upstream wins, no copy preserved)\n`);
1312
+ ctx.actions.push(() => {
1313
+ fs.rmSync(localPath, { force: true });
1314
+ ctx.counts.userOverwrite += 1;
1315
+ ctx.appendLog(`overwritten\t${rel}\t(overwrite-safe; upstream wins, no copy preserved)\n`);
1316
+ });
1280
1317
  }
1281
1318
  else if (isConflictClass(rel)) {
1282
- conflictOne(ctx, rel);
1319
+ ctx.actions.push(() => conflictOne(ctx, rel));
1283
1320
  }
1284
1321
  else {
1285
- rescueOne(ctx, rel);
1322
+ ctx.actions.push(() => rescueOne(ctx, rel));
1286
1323
  }
1287
1324
  }
1288
1325
  }
@@ -1294,8 +1331,10 @@ function processOne(ctx, rel) {
1294
1331
  ctx.out(` unchanged (preserved in place): ${rel}\n`);
1295
1332
  }
1296
1333
  else {
1297
- fs.appendFileSync(ctx.unchangedList, `/${rel}\n`);
1298
- ctx.appendLog(`unchanged\t${rel}\t(identical to upstream; left in place, mtime preserved)\n`);
1334
+ ctx.actions.push(() => {
1335
+ fs.appendFileSync(ctx.unchangedList, `/${rel}\n`);
1336
+ ctx.appendLog(`unchanged\t${rel}\t(identical to upstream; left in place, mtime preserved)\n`);
1337
+ });
1299
1338
  }
1300
1339
  }
1301
1340
  else {
@@ -1303,8 +1342,10 @@ function processOne(ctx, rel) {
1303
1342
  ctx.out(` unchanged (delete + replace): ${rel}\n`);
1304
1343
  }
1305
1344
  else {
1306
- fs.rmSync(localPath, { force: true });
1307
- ctx.appendLog(`deleted\t${rel}\t(unchanged vs baseline; re-laid by overlay if still upstream)\n`);
1345
+ ctx.actions.push(() => {
1346
+ fs.rmSync(localPath, { force: true });
1347
+ ctx.appendLog(`deleted\t${rel}\t(unchanged vs baseline; re-laid by overlay if still upstream)\n`);
1348
+ });
1308
1349
  }
1309
1350
  }
1310
1351
  }
@@ -1321,7 +1362,10 @@ function walkAndProcess(ctx, rootRel) {
1321
1362
  ctx.out(" wholesale-replace: companies/_template (template carve-out)\n");
1322
1363
  }
1323
1364
  else {
1324
- fs.rmSync(path.join(ctx.hqRoot, "companies", "_template"), { recursive: true, force: true });
1365
+ ctx.actions.push(() => fs.rmSync(path.join(ctx.hqRoot, "companies", "_template"), {
1366
+ recursive: true,
1367
+ force: true,
1368
+ }));
1325
1369
  }
1326
1370
  return;
1327
1371
  }
@@ -1341,8 +1385,10 @@ function walkAndProcess(ctx, rootRel) {
1341
1385
  ctx.out(` drop reindex symlink: ${rootRel} -> ${tgt}\n`);
1342
1386
  }
1343
1387
  else {
1344
- fs.rmSync(rootAbs, { force: true });
1345
- ctx.appendLog(`symlink-dropped\t${rootRel}\t(reindex regenerable)\n`);
1388
+ ctx.actions.push(() => {
1389
+ fs.rmSync(rootAbs, { force: true });
1390
+ ctx.appendLog(`symlink-dropped\t${rootRel}\t(reindex regenerable)\n`);
1391
+ });
1346
1392
  }
1347
1393
  }
1348
1394
  return;