@uzysjung/agent-harness 26.172.0 → 26.173.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.
package/dist/index.js CHANGED
@@ -101,7 +101,7 @@ import {
101
101
  targetsInclude,
102
102
  upsertHarnessImport,
103
103
  writeInstallLog
104
- } from "./chunk-T2HBTG32.js";
104
+ } from "./chunk-JABFWW6X.js";
105
105
 
106
106
  // node_modules/sisteransi/src/index.js
107
107
  var require_src = __commonJS({
@@ -3713,8 +3713,8 @@ function padDisplay(s, width) {
3713
3713
 
3714
3714
  // src/install-writes.ts
3715
3715
  init_esm_shims();
3716
- import { existsSync as existsSync6, mkdirSync, readFileSync as readFileSync4, statSync, writeFileSync as writeFileSync3 } from "fs";
3717
- import { dirname, join as join7, relative, sep } from "path";
3716
+ import { existsSync as existsSync6, mkdirSync, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
3717
+ import { dirname, join as join8, relative, sep } from "path";
3718
3718
 
3719
3719
  // src/env-files.ts
3720
3720
  init_esm_shims();
@@ -3804,7 +3804,8 @@ var LINES = {
3804
3804
  newInRelease: "wrote (new in this release)",
3805
3805
  refreshed: "refreshed",
3806
3806
  sameAsHarness: "kept \u2014 already the same as the harness version (left yours in place)",
3807
- hadFile: "had a file with this name \u2014 saved as <file>.backup-<time>",
3807
+ // 비켜 둔 설치자 파일은 하네스가 그 자리를 떠날 때(uninstall · 회수) 되돌린다(`displaced` · #782) — 그 약속을 같은 줄에
3808
+ hadFile: "had a file with this name \u2014 saved as <file>.backup-<time> \u2014 yours, put back on uninstall",
3808
3809
  noChecksum: "no checksum on record \u2014 saved a copy once",
3809
3810
  edited: "you edited it \u2014 saved as <file>.backup-<time>",
3810
3811
  removed: "removed",
@@ -3817,6 +3818,9 @@ var LINES = {
3817
3818
  scaffoldKept: "kept \u2014 already there (yours)",
3818
3819
  leftForYou: "left for you (yours now)"
3819
3820
  };
3821
+ function withBackup(line, backup) {
3822
+ return line.replace("<file>.backup-<time>", backup);
3823
+ }
3820
3824
  function unreadable(adapter, op) {
3821
3825
  const what = op === "write" ? "harness part not added" : "harness part not removed";
3822
3826
  return `could not read it (${ADAPTERS[adapter].unreadable}) \u2014 ${what}`;
@@ -3935,7 +3939,8 @@ var PRIOR_MCP_SERVERS = [
3935
3939
  // #766 — `@supabase/mcp-server` 는 npm 에 없는 이름이라(404) 서버가 뜬 적이 없다
3936
3940
  name: "supabase",
3937
3941
  value: { type: "stdio", command: "npx", args: ["-y", "@supabase/mcp-server"] },
3938
- updated: "now Supabase's remote server \u2014 sign in on first use, see NEXT"
3942
+ updated: "now Supabase's remote server \u2014 sign in on first use, see NEXT",
3943
+ dead: `runs @supabase/mcp-server, which is not on npm \u2014 this server never started. To use Supabase's official remote server, set it to { "type": "http", "url": "https://mcp.supabase.com/mcp" } (signs in on first use)`
3939
3944
  },
3940
3945
  {
3941
3946
  name: "chrome-devtools",
@@ -4116,7 +4121,7 @@ function recordedMcpAssetIds(log) {
4116
4121
  // src/recorded.ts
4117
4122
  init_esm_shims();
4118
4123
  import { existsSync as existsSync5 } from "fs";
4119
- import { join as join6 } from "path";
4124
+ import { join as join7 } from "path";
4120
4125
 
4121
4126
  // src/cli-ownership.ts
4122
4127
  init_esm_shims();
@@ -4171,6 +4176,42 @@ function removableFor(cli, remaining) {
4171
4176
  return { exclusive, sharedNowUnowned };
4172
4177
  }
4173
4178
 
4179
+ // src/foreign-slot.ts
4180
+ init_esm_shims();
4181
+ import { lstatSync, realpathSync, statSync } from "fs";
4182
+ import { join as join6 } from "path";
4183
+ var SKILL_SLOT = /^((?:\.claude|\.agents)\/skills\/[^/]+)(?:\/|$)/;
4184
+ function occupiedByNonDirectory(path) {
4185
+ const stat = lstatSync(path, { throwIfNoEntry: false });
4186
+ return stat !== void 0 && !stat.isDirectory();
4187
+ }
4188
+ function linksToProjectSharedSkill(projectDir, id) {
4189
+ const slot = join6(projectDir, ".claude/skills", id);
4190
+ if (lstatSync(slot, { throwIfNoEntry: false })?.isSymbolicLink() !== true) return false;
4191
+ try {
4192
+ const real = realpathSync(slot);
4193
+ return real === join6(realpathSync(projectDir), ".agents/skills", id) && statSync(real).isDirectory();
4194
+ } catch {
4195
+ return false;
4196
+ }
4197
+ }
4198
+ function foreignOwnedTarget(projectDir, rel) {
4199
+ const slot = SKILL_SLOT.exec(rel)?.[1];
4200
+ if (slot === void 0) return null;
4201
+ if (occupiedByNonDirectory(join6(projectDir, slot))) return slot;
4202
+ if (rel !== slot) {
4203
+ const parts = rel.slice(slot.length + 1).split("/");
4204
+ let cursor = slot;
4205
+ for (const part of parts.slice(0, -1)) {
4206
+ cursor = `${cursor}/${part}`;
4207
+ if (occupiedByNonDirectory(join6(projectDir, cursor))) return cursor;
4208
+ }
4209
+ const leaf = lstatSync(join6(projectDir, rel), { throwIfNoEntry: false });
4210
+ if (leaf !== void 0 && !leaf.isFile()) return rel;
4211
+ }
4212
+ return null;
4213
+ }
4214
+
4174
4215
  // src/recorded.ts
4175
4216
  var CLAUDE_DIR = ".claude/";
4176
4217
  var ADVISORY_PATHS = [".env.example", ".github/workflows/", "skills-lock.json"];
@@ -4190,6 +4231,12 @@ function ownersOf(path) {
4190
4231
  )
4191
4232
  );
4192
4233
  }
4234
+ function usersOf(path, projectDir) {
4235
+ const owners = ownersOf(path);
4236
+ const id = /^\.agents\/skills\/([^/]+)\//.exec(path)?.[1];
4237
+ const linked = id !== void 0 && linksToProjectSharedSkill(projectDir, id);
4238
+ return linked && !owners.includes("claude") ? [...owners, "claude"] : owners;
4239
+ }
4193
4240
  function bundlePath(path) {
4194
4241
  return path.startsWith(".agents/skills/") ? `${SKILLS_DIR}${path.slice(".agents/skills/".length)}` : path;
4195
4242
  }
@@ -4202,9 +4249,9 @@ function recordedTrackTargets(log) {
4202
4249
  selectedInternalSkills: INTERNAL_BUNDLED_SKILL_IDS
4203
4250
  };
4204
4251
  const entries = buildManifest(spec).filter((e2) => e2.applies(spec));
4205
- const templates = join6(resolveBundleRoot(), "templates");
4252
+ const templates = join7(resolveBundleRoot(), "templates");
4206
4253
  return (path) => entries.some(
4207
- (e2) => e2.type === "dir" ? path.startsWith(`${e2.target}/`) && existsSync5(join6(templates, e2.source, path.slice(e2.target.length + 1))) : e2.target === path
4254
+ (e2) => e2.type === "dir" ? path.startsWith(`${e2.target}/`) && existsSync5(join7(templates, e2.source, path.slice(e2.target.length + 1))) : e2.target === path
4208
4255
  );
4209
4256
  }
4210
4257
  function recordedTrackSpots(log) {
@@ -4212,13 +4259,13 @@ function recordedTrackSpots(log) {
4212
4259
  tracks: log.spec.tracks.filter(isTrack),
4213
4260
  selectedInternalSkills: INTERNAL_BUNDLED_SKILL_IDS
4214
4261
  };
4215
- const templates = join6(resolveBundleRoot(), "templates");
4262
+ const templates = join7(resolveBundleRoot(), "templates");
4216
4263
  const out = [];
4217
4264
  for (const e2 of buildManifest(spec)) {
4218
4265
  if (!e2.applies(spec) || !e2.target.startsWith(CLAUDE_DIR)) continue;
4219
4266
  if (e2.type === "file") out.push(e2.target);
4220
4267
  else
4221
- for (const rel of listFilesRecursive(join6(templates, e2.source)))
4268
+ for (const rel of listFilesRecursive(join7(templates, e2.source)))
4222
4269
  out.push(`${e2.target}/${rel}`);
4223
4270
  }
4224
4271
  for (const p2 of RETIRED_PATHS) out.push(`${CLAUDE_DIR}${p2}`);
@@ -4297,7 +4344,6 @@ function isScaffoldPath(path) {
4297
4344
  // src/install-writes.ts
4298
4345
  var CLAUDE_DIR2 = ".claude/";
4299
4346
  var SKILLS_DIR2 = ".claude/skills/";
4300
- var BACKUP_PLACEHOLDER = "<file>.backup-<time>";
4301
4347
  function createInstallWriter(args) {
4302
4348
  const { projectDir, previousLog, excluded } = args;
4303
4349
  const now = args.now ?? /* @__PURE__ */ new Date();
@@ -4322,16 +4368,17 @@ function createInstallWriter(args) {
4322
4368
  else external.set(path, sha256);
4323
4369
  }
4324
4370
  function harness(path, from, opts = {}) {
4325
- const abs = join7(projectDir, path);
4371
+ const abs = join8(projectDir, path);
4326
4372
  if (outside.skip(abs)) return null;
4327
4373
  const next = "source" in from ? readFileSync4(from.source, "utf8") : from.content;
4328
4374
  const disk = existsSync6(abs) ? readFileSync4(abs, "utf8") : null;
4329
4375
  const rec = recorded(previousLog, path, opts);
4330
4376
  const diskSha = rec.state === "no-sha" && disk !== null ? hashContent(disk) : null;
4377
+ const judgedRec = diskSha !== null && isLegacyHarnessFile(path, diskSha) ? { state: "sha", sha256: diskSha } : rec;
4331
4378
  const j4 = judge({
4332
4379
  op: "write",
4333
4380
  kind: "harness",
4334
- rec: diskSha !== null && isLegacyHarnessFile(path, diskSha) ? { state: "sha", sha256: diskSha } : rec,
4381
+ rec: judgedRec,
4335
4382
  disk,
4336
4383
  next,
4337
4384
  ...args.run === void 0 ? {} : { run: args.run }
@@ -4376,11 +4423,19 @@ function createInstallWriter(args) {
4376
4423
  default:
4377
4424
  throw new Error(`install: '${j4.record}' is not a write record (${path})`);
4378
4425
  }
4379
- const line = backup === void 0 ? j4.line : j4.line.replace(BACKUP_PLACEHOLDER, backup);
4426
+ const line = backup === void 0 ? j4.line : withBackup(j4.line, backup);
4380
4427
  const backupPair = backup === void 0 || backupAbs === void 0 ? {} : { backup, backupAbs };
4381
- writes.push({ path, verdict: j4.verdict, record: j4.record, line, ...backupPair });
4428
+ const state = judgedRec.state;
4429
+ writes.push({
4430
+ path,
4431
+ verdict: j4.verdict,
4432
+ record: j4.record,
4433
+ recorded: state,
4434
+ line,
4435
+ ...backupPair
4436
+ });
4382
4437
  if (j4.line === "" || j4.verdict === "create" || j4.verdict === "overwrite") return null;
4383
- const out = { path, verdict: j4.verdict, line, ...backupPair };
4438
+ const out = { path, verdict: j4.verdict, recorded: state, line, ...backupPair };
4384
4439
  judged.push(out);
4385
4440
  return out;
4386
4441
  }
@@ -4388,7 +4443,7 @@ function createInstallWriter(args) {
4388
4443
  const file = SHARED_FILES[path];
4389
4444
  if (file === void 0) throw new Error(`install: ${path} is not a shared file`);
4390
4445
  const adapter = ADAPTERS[file.adapter];
4391
- const abs = join7(projectDir, path);
4446
+ const abs = join8(projectDir, path);
4392
4447
  const disk = existsSync6(abs) ? readFileSync4(abs, "utf8") : null;
4393
4448
  const own = new Map(
4394
4449
  (previousLog?.portions ?? []).filter((p2) => p2.path === path).map((p2) => [p2.key, p2.sha256])
@@ -4570,9 +4625,9 @@ function retiredClaims(previous, projectDir, kept) {
4570
4625
  const out = [];
4571
4626
  for (const p2 of RETIRED_PATHS) {
4572
4627
  if (have.has(p2) || recorded(previous, `${CLAUDE_DIR2}${p2}`).state === "none") continue;
4573
- const abs = join7(projectDir, CLAUDE_DIR2, p2);
4628
+ const abs = join8(projectDir, CLAUDE_DIR2, p2);
4574
4629
  try {
4575
- if (!statSync(abs).isFile()) continue;
4630
+ if (!statSync2(abs).isFile()) continue;
4576
4631
  out.push({ path: p2, sha256: hashContent(readFileSync4(abs, "utf8")) });
4577
4632
  } catch {
4578
4633
  }
@@ -4677,7 +4732,7 @@ function legacySettingsSeed(text, render, projectDir, opts = {}) {
4677
4732
  } catch {
4678
4733
  return seed;
4679
4734
  }
4680
- const claudeDir = join7(projectDir, ".claude");
4735
+ const claudeDir = join8(projectDir, ".claude");
4681
4736
  const candidates = new Set([...render.keys()].filter((k2) => k2.startsWith("hooks.")));
4682
4737
  const legacyLines = /* @__PURE__ */ new Set();
4683
4738
  const hooks = isObject2(root) && isObject2(root.hooks) ? root.hooks : {};
@@ -4794,12 +4849,12 @@ function legacyGitignoreSeed(text, render, previous) {
4794
4849
  // src/installer.ts
4795
4850
  init_esm_shims();
4796
4851
  import { chmodSync as chmodSync3, existsSync as existsSync22, readdirSync as readdirSync5, readFileSync as readFileSync23, writeFileSync as writeFileSync8 } from "fs";
4797
- import { basename as basename4, join as join31, relative as relative5, resolve, sep as sep6 } from "path";
4852
+ import { basename as basename4, join as join31, relative as relative6, resolve, sep as sep7 } from "path";
4798
4853
 
4799
4854
  // src/anchor-seed.ts
4800
4855
  init_esm_shims();
4801
4856
  import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
4802
- import { join as join8 } from "path";
4857
+ import { join as join9 } from "path";
4803
4858
 
4804
4859
  // src/agents-md-render.ts
4805
4860
  init_esm_shims();
@@ -4906,7 +4961,7 @@ function compose(body, into) {
4906
4961
  ${trimmed.join("\n")}`;
4907
4962
  }
4908
4963
  function seedAgentsMdProjectContext(projectDir) {
4909
- const root = readIfExists(join8(projectDir, "CLAUDE.md"));
4964
+ const root = readIfExists(join9(projectDir, "CLAUDE.md"));
4910
4965
  if (root === null) return null;
4911
4966
  const withoutImport = stripHarnessImport(root) ?? root;
4912
4967
  const body = dropHarnessQuoteBlocks(dropTitle(withoutImport.split("\n")));
@@ -4914,9 +4969,9 @@ function seedAgentsMdProjectContext(projectDir) {
4914
4969
  return compose(body, "agents-md");
4915
4970
  }
4916
4971
  function seedRootClaudeProjectContext(projectDir, harnessRoot) {
4917
- const agents = readIfExists(join8(projectDir, "AGENTS.md"));
4972
+ const agents = readIfExists(join9(projectDir, "AGENTS.md"));
4918
4973
  if (agents === null) return null;
4919
- const template = readIfExists(join8(harnessRoot, AGENTS_MD_TEMPLATE));
4974
+ const template = readIfExists(join9(harnessRoot, AGENTS_MD_TEMPLATE));
4920
4975
  if (template === null) return null;
4921
4976
  const installer = projectContextInstallerLines({ existing: agents, template });
4922
4977
  if (installer === null) return null;
@@ -4928,7 +4983,7 @@ function seedRootClaudeProjectContext(projectDir, harnessRoot) {
4928
4983
  // src/ci-scaffold.ts
4929
4984
  init_esm_shims();
4930
4985
  import { existsSync as existsSync8 } from "fs";
4931
- import { join as join9 } from "path";
4986
+ import { join as join10 } from "path";
4932
4987
  var PYTHON_TRACKS = "data|csr-fastapi|full";
4933
4988
  var VARIANTS = [
4934
4989
  {
@@ -4952,12 +5007,12 @@ function installCiScaffold(ctx) {
4952
5007
  for (const v2 of VARIANTS) {
4953
5008
  if (!v2.applies(ctx.tracks)) continue;
4954
5009
  const relTarget = `.github/workflows/${v2.target}`;
4955
- const target = join9(ctx.projectDir, relTarget);
5010
+ const target = join10(ctx.projectDir, relTarget);
4956
5011
  if (existsSync8(target)) {
4957
5012
  report.skippedExisting.push(relTarget);
4958
5013
  continue;
4959
5014
  }
4960
- copyFile(join9(ctx.harnessRoot, "templates/github-workflows", v2.source), target);
5015
+ copyFile(join10(ctx.harnessRoot, "templates/github-workflows", v2.source), target);
4961
5016
  report.written.push(relTarget);
4962
5017
  }
4963
5018
  return report;
@@ -4975,7 +5030,7 @@ import { basename, join as join14 } from "path";
4975
5030
 
4976
5031
  // src/agents-skill-targets.ts
4977
5032
  init_esm_shims();
4978
- import { join as join10, sep as sep2 } from "path";
5033
+ import { join as join11, sep as sep2 } from "path";
4979
5034
  function bundledSkillTargets(projectDir, log = readInstallLog(projectDir)) {
4980
5035
  if (log === null) return [];
4981
5036
  const tracks = log.spec.tracks.filter(
@@ -4989,7 +5044,7 @@ function bundledSkillTargets(projectDir, log = readInstallLog(projectDir)) {
4989
5044
  }
4990
5045
  function allowSkillCreation(writer, projectDir, ids) {
4991
5046
  if (ids.length === 0) return writer;
4992
- const roots = ids.map((id) => `${join10(projectDir, ".agents", "skills", id)}${sep2}`);
5047
+ const roots = ids.map((id) => `${join11(projectDir, ".agents", "skills", id)}${sep2}`);
4993
5048
  return {
4994
5049
  write(absPath, content, opts) {
4995
5050
  const inTarget = roots.some((root) => absPath.startsWith(root));
@@ -5012,48 +5067,11 @@ function agentsSkillSlot(params) {
5012
5067
  init_esm_shims();
5013
5068
  import { existsSync as existsSync9, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
5014
5069
  import { dirname as dirname2, relative as relative2, sep as sep3 } from "path";
5015
-
5016
- // src/foreign-slot.ts
5017
- init_esm_shims();
5018
- import { lstatSync, realpathSync, statSync as statSync2 } from "fs";
5019
- import { join as join11 } from "path";
5020
- var SKILL_SLOT = /^((?:\.claude|\.agents)\/skills\/[^/]+)(?:\/|$)/;
5021
- function occupiedByNonDirectory(path) {
5022
- const stat = lstatSync(path, { throwIfNoEntry: false });
5023
- return stat !== void 0 && !stat.isDirectory();
5024
- }
5025
- function linksToProjectSharedSkill(projectDir, id) {
5026
- const slot = join11(projectDir, ".claude/skills", id);
5027
- if (lstatSync(slot, { throwIfNoEntry: false })?.isSymbolicLink() !== true) return false;
5028
- try {
5029
- const real = realpathSync(slot);
5030
- return real === join11(realpathSync(projectDir), ".agents/skills", id) && statSync2(real).isDirectory();
5031
- } catch {
5032
- return false;
5033
- }
5034
- }
5035
- function foreignOwnedTarget(projectDir, rel) {
5036
- const slot = SKILL_SLOT.exec(rel)?.[1];
5037
- if (slot === void 0) return null;
5038
- if (occupiedByNonDirectory(join11(projectDir, slot))) return slot;
5039
- if (rel !== slot) {
5040
- const parts = rel.slice(slot.length + 1).split("/");
5041
- let cursor = slot;
5042
- for (const part of parts.slice(0, -1)) {
5043
- cursor = `${cursor}/${part}`;
5044
- if (occupiedByNonDirectory(join11(projectDir, cursor))) return cursor;
5045
- }
5046
- const leaf = lstatSync(join11(projectDir, rel), { throwIfNoEntry: false });
5047
- if (leaf !== void 0 && !leaf.isFile()) return rel;
5048
- }
5049
- return null;
5050
- }
5051
-
5052
- // src/owned-write.ts
5053
5070
  function createOwnedWriter(projectDir, baseline, options = {}) {
5054
5071
  const { now = /* @__PURE__ */ new Date(), refreshOnly = false, journal, owned = () => false } = options;
5055
5072
  const written = /* @__PURE__ */ new Map();
5056
5073
  const backedUp = [];
5074
+ const judged = [];
5057
5075
  const backupPaths = [];
5058
5076
  const foreignOwned = [];
5059
5077
  const restored = [];
@@ -5078,14 +5096,30 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
5078
5096
  const disk = existsSync9(absPath) ? readFileSync6(absPath, "utf8") : null;
5079
5097
  if (disk === null && refreshOnly && !opts?.createInRefresh && rec.state !== "sha")
5080
5098
  return false;
5081
- const { verdict, record } = judge({ op: "write", kind: "harness", rec, disk, next: content });
5099
+ const { verdict, record, line } = judge({
5100
+ op: "write",
5101
+ kind: "harness",
5102
+ rec,
5103
+ disk,
5104
+ next: content
5105
+ });
5082
5106
  let backupRel = null;
5083
5107
  if (verdict === "backup+overwrite" || verdict === "overwrite" && opts?.backupFirst === true) {
5084
5108
  const backup = backupFile(absPath, now);
5085
5109
  backupPaths.push(backup);
5086
5110
  journal?.backup(backup);
5087
5111
  backupRel = relative2(projectDir, backup).split(sep3).join("/");
5088
- if (verdict === "backup+overwrite" && rec.state !== "none") backedUp.push(rel);
5112
+ if (verdict === "backup+overwrite") {
5113
+ if (rec.state !== "none") backedUp.push(rel);
5114
+ judged.push({
5115
+ path: rel,
5116
+ verdict,
5117
+ recorded: rec.state,
5118
+ line: withBackup(line, backupRel),
5119
+ backup: backupRel,
5120
+ backupAbs: backup
5121
+ });
5122
+ }
5089
5123
  }
5090
5124
  if (record === "displaced" || record === "displaced+sha") {
5091
5125
  displace({ path: rel, change: "displaced", notes: backupRel === null ? [] : [backupRel] });
@@ -5111,6 +5145,7 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
5111
5145
  outside: outside.list(),
5112
5146
  files: [...written].map(([path, sha256]) => ({ path, sha256 })),
5113
5147
  backedUp: [...backedUp],
5148
+ judged: [...judged],
5114
5149
  backupPaths: [...backupPaths],
5115
5150
  updated,
5116
5151
  restored: [...restored],
@@ -6240,6 +6275,7 @@ function runCliTransforms(params) {
6240
6275
  const externalFiles = [];
6241
6276
  const externalBackups = [];
6242
6277
  const externalBackedUp = [];
6278
+ const externalJudged = [];
6243
6279
  const externalForeignOwned = [];
6244
6280
  const displaced = /* @__PURE__ */ new Map();
6245
6281
  let externalOutside = [];
@@ -6261,6 +6297,7 @@ function runCliTransforms(params) {
6261
6297
  }
6262
6298
  externalBackups.push(...report.ownership.backupPaths);
6263
6299
  externalBackedUp.push(...report.ownership.backedUp);
6300
+ externalJudged.push(...report.ownership.judged ?? []);
6264
6301
  for (const f of report.ownership.displaced ?? [])
6265
6302
  if (!displaced.has(f.path)) displaced.set(f.path, f);
6266
6303
  for (const f of report.ownership.foreignOwned) {
@@ -6368,6 +6405,7 @@ function runCliTransforms(params) {
6368
6405
  externalBackups,
6369
6406
  externalUpdated,
6370
6407
  externalBackedUp,
6408
+ externalJudged,
6371
6409
  displaced: [...displaced.values()],
6372
6410
  externalForeignOwned,
6373
6411
  externalOutside,
@@ -6690,14 +6728,15 @@ function refreshExternalSkills(projectDir, deps = {}) {
6690
6728
  }
6691
6729
  throw e2;
6692
6730
  }
6693
- const { report, displaced } = settle(ran);
6731
+ const { report, displaced, judged } = settle(ran);
6694
6732
  recordRefreshedFiles(projectDir, log, report, displaced, deps.writeLog ?? writeInstallLog);
6695
6733
  return {
6696
6734
  attempted: report.attempted.length,
6697
6735
  refreshed: report.succeeded,
6698
6736
  failed: report.attempted.filter((r) => !r.ok).map((r) => ({ id: r.asset.id, message: r.message ?? "failed" })),
6699
6737
  notInCatalog,
6700
- unknown: false
6738
+ unknown: false,
6739
+ judged
6701
6740
  };
6702
6741
  }
6703
6742
  function holdSkillSlots(projectDir, hold) {
@@ -6730,7 +6769,11 @@ function holdSkillSlots(projectDir, hold) {
6730
6769
  hold.clis,
6731
6770
  { log: hold.log, scope: hold.scope }
6732
6771
  );
6733
- const out = { backups: [], displaced: [] };
6772
+ const out = {
6773
+ backups: [],
6774
+ displaced: [],
6775
+ judged: []
6776
+ };
6734
6777
  const back = /* @__PURE__ */ new Set();
6735
6778
  const failed = [];
6736
6779
  for (const f of moved) {
@@ -6743,6 +6786,16 @@ function holdSkillSlots(projectDir, hold) {
6743
6786
  }
6744
6787
  out.backups.push(r.backup);
6745
6788
  if (r.displaced !== null) out.displaced.push(r.displaced);
6789
+ const backup = toProjectRel(projectDir, r.backup);
6790
+ const line = !f.recorded ? LINES.hadFile : r.gone ? LINES.editedRemoved : LINES.edited;
6791
+ out.judged.push({
6792
+ path: f.rel,
6793
+ verdict: r.gone ? "backup+remove" : "backup+overwrite",
6794
+ recorded: f.recorded ? "sha" : "none",
6795
+ line: withBackup(line, backup),
6796
+ backup,
6797
+ backupAbs: r.backup
6798
+ });
6746
6799
  } catch (e2) {
6747
6800
  failed.push(`${f.rel} (${e2 instanceof Error ? e2.message : String(e2)})`);
6748
6801
  }
@@ -6820,9 +6873,10 @@ function settleAside(projectDir, f, restore) {
6820
6873
  }
6821
6874
  const backup = backupFromCopy(f.abs, f.aside);
6822
6875
  rmSync3(f.aside, { force: true });
6823
- if (f.recorded || now === null) return { backup, displaced: null };
6876
+ if (f.recorded || now === null) return { backup, gone: now === null, displaced: null };
6824
6877
  return {
6825
6878
  backup,
6879
+ gone: false,
6826
6880
  displaced: { path: f.rel, change: "displaced", notes: [toProjectRel(projectDir, backup)] }
6827
6881
  };
6828
6882
  }
@@ -7074,6 +7128,8 @@ function refreshLinkedSkillBodies(params) {
7074
7128
  notOurs: ids.filter((id) => !baseline.has(bodyKey(id))),
7075
7129
  files: result.files,
7076
7130
  backupPaths: result.backupPaths,
7131
+ backedUp: result.backedUp,
7132
+ judged: result.judged ?? [],
7077
7133
  displaced: result.displaced ?? [],
7078
7134
  foreignOwned: result.foreignOwned,
7079
7135
  outside: result.outside ?? []
@@ -7239,7 +7295,7 @@ import {
7239
7295
  unlinkSync as unlinkSync2,
7240
7296
  writeFileSync as writeFileSync7
7241
7297
  } from "fs";
7242
- import { basename as basename3, dirname as dirname7, join as join30 } from "path";
7298
+ import { basename as basename3, dirname as dirname7, join as join30, relative as relative5, sep as sep6 } from "path";
7243
7299
  var LEGACY_ANCHOR_FILE2 = ".claude/CLAUDE.md";
7244
7300
  function buildUpdateSpec(projectDir, tracks, only) {
7245
7301
  const log = readInstallLog(projectDir);
@@ -7301,11 +7357,12 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
7301
7357
  const logAtStart = readInstallLog(projectDir);
7302
7358
  const claudeManaged = logAtStart === null || installedClis(logAtStart).includes("claude");
7303
7359
  report.claudeUnrecorded = !claudeManaged && logAtStart !== null && existsSync21(claudeDir) ? recordClaudeCommand(logAtStart, installedTracks(projectDir)) : null;
7304
- const harness = logAtStart === null ? { displaced: [], restored: [], foreignOwned: [], outside: [] } : updateHarnessFiles({ projectDir, templatesDir, log: logAtStart, wants, outside, report });
7360
+ const harness = logAtStart === null ? { displaced: [], restored: [], foreignOwned: [], outside: [], judged: [] } : updateHarnessFiles({ projectDir, templatesDir, log: logAtStart, wants, outside, report });
7305
7361
  const claudeDisplaced = harness.displaced;
7362
+ const judged = [...harness.judged];
7306
7363
  const prunedRestored = harness.restored;
7307
7364
  if (existsSync21(claudeDir) && claudeManaged) {
7308
- if (wants("anchor")) syncHarnessAnchor(projectDir, templatesDir, report, outside);
7365
+ if (wants("anchor")) syncHarnessAnchor(projectDir, templatesDir, report, outside, judged);
7309
7366
  }
7310
7367
  const harnessMcp = renderHarnessMcp(harnessRoot, {
7311
7368
  spec: { tracks: installedTracks(projectDir), options: DEFAULT_OPTIONS },
@@ -7339,9 +7396,16 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
7339
7396
  if (wants("hooks")) report.mcpAllowlistRetired = retireMcpAllowlist(projectDir);
7340
7397
  report.retiredAgents = staleAgentFiles(claudeDir);
7341
7398
  report.demotedAgents = demotedAgentFiles(claudeDir, installedTracks(projectDir));
7342
- const external = wants("external") ? refreshExternalCli(projectDir, harnessRoot, legacyDroppedKeys(logAtStart), harnessMcp) : {
7399
+ const external = wants("external") ? refreshExternalCli(
7400
+ projectDir,
7401
+ harnessRoot,
7402
+ legacyDroppedKeys(logAtStart),
7403
+ harnessMcp,
7404
+ report.skillsSkippedLinks.filter((id) => linksToProjectSharedSkill(projectDir, id))
7405
+ ) : {
7343
7406
  externalUpdated: 0,
7344
7407
  externalBackedUp: [],
7408
+ externalJudged: [],
7345
7409
  displaced: [],
7346
7410
  externalForeignOwned: [],
7347
7411
  externalOutside: [],
@@ -7452,10 +7516,17 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
7452
7516
  const displacedSame = displacedAll.filter((f) => f.notes.length === 0).map((f) => f.path);
7453
7517
  if (displacedMoved.length > 0) report.displaced = displacedMoved;
7454
7518
  if (displacedSame.length > 0) report.displacedSame = displacedSame;
7455
- report.backups = collectRunBackups(projectDir, startedAt, backupsAtStart);
7456
- const onRecord = checksumOnRecord(logAtStart);
7457
- const firstContact = new Set(displacedMoved);
7458
- const noChecksum = report.backups.map((b2) => b2.path).filter((p2) => !onRecord(p2) && !firstContact.has(p2));
7519
+ const deadMcp = claudeManaged ? deadMcpLeft(projectDir) : [];
7520
+ if (deadMcp.length > 0) report.deadMcp = deadMcp;
7521
+ judged.push(...external.externalJudged, ...skillRefresh.judged ?? []);
7522
+ if (judged.length > 0) report.judged = judged;
7523
+ const unmeasured = new Set(
7524
+ judged.filter((j4) => j4.recorded === "no-sha" && j4.backup !== void 0).map((j4) => j4.path)
7525
+ );
7526
+ report.backups = collectRunBackups(projectDir, startedAt, backupsAtStart).map(
7527
+ (b2) => existsSync21(join30(projectDir, b2.path)) ? b2 : { ...b2, removed: true }
7528
+ );
7529
+ const noChecksum = report.backups.map((b2) => b2.path).filter((p2) => unmeasured.has(p2));
7459
7530
  if (noChecksum.length > 0) report.noChecksum = noChecksum;
7460
7531
  writeBackupList(projectDir, report.backups, new Set(noChecksum));
7461
7532
  return report;
@@ -7533,19 +7604,16 @@ function reclaimOutOfTrackRecorded(projectDir, outside) {
7533
7604
  }
7534
7605
  return { reclaim: result, displaced };
7535
7606
  }
7536
- function checksumOnRecord(log) {
7537
- if (log === null) return () => false;
7538
- const paths = /* @__PURE__ */ new Set([
7539
- ...(log.policyFiles ?? []).map((f) => `.claude/${f.path}`),
7540
- ...(log.skillFiles ?? []).map((f) => `.claude/skills/${f.path}`),
7541
- ...(log.externalFiles ?? []).map((f) => f.path),
7542
- ...log.assets.flatMap((a) => a.files ?? []).map((f) => f.path),
7543
- // #800 — 함께 쓰는 파일은 몫마다 sha 가 있다 — 그 백업은 설치자 편집이 확실하다
7544
- ...(log.portions ?? []).map((p2) => p2.path)
7545
- ]);
7546
- const anchor = log.templates.rootClaudeMd;
7547
- if (anchor !== void 0) paths.add(anchor.path);
7548
- return (path) => paths.has(path);
7607
+ function deadMcpLeft(projectDir) {
7608
+ let servers;
7609
+ try {
7610
+ servers = JSON.parse(readFileSync22(join30(projectDir, ".mcp.json"), "utf8")).mcpServers;
7611
+ } catch {
7612
+ return [];
7613
+ }
7614
+ return PRIOR_MCP_SERVERS.flatMap(
7615
+ (p2) => p2.dead !== void 0 && servers?.[p2.name] !== void 0 && jsonSha(servers[p2.name]) === jsonSha(toClaudeServer(p2.value)) ? [{ name: p2.name, note: p2.dead }] : []
7616
+ );
7549
7617
  }
7550
7618
  function retiredFeaturesPart(projectDir, f) {
7551
7619
  if (f.path !== ".codex/config.toml") return {};
@@ -7739,6 +7807,7 @@ function updateHarnessTargets(projectDir, templatesDir, log, wants, report, fore
7739
7807
  }
7740
7808
  function updateHarnessFiles(args) {
7741
7809
  const { projectDir, templatesDir, log, wants, outside, report } = args;
7810
+ const judged = [];
7742
7811
  const now = /* @__PURE__ */ new Date();
7743
7812
  const claude = installedClis(log).includes("claude");
7744
7813
  const claims = retiredClaims(log, projectDir, log.policyFiles ?? []);
@@ -7759,7 +7828,7 @@ function updateHarnessFiles(args) {
7759
7828
  if (!claude || !wants(dir === "hooks" ? "hooks" : "rules")) continue;
7760
7829
  const label = `.claude/${dir}`;
7761
7830
  report.updated[label] = 0;
7762
- const ctx = { prefix: dir, outside, recordOf };
7831
+ const ctx = { prefix: dir, outside, recordOf, projectDir, judged };
7763
7832
  report.pruned[label] = pruneOrphans(
7764
7833
  join30(projectDir, label),
7765
7834
  join30(templatesDir, dir),
@@ -7776,7 +7845,8 @@ function updateHarnessFiles(args) {
7776
7845
  targets.refreshedSkills,
7777
7846
  outside,
7778
7847
  recordOf,
7779
- now
7848
+ now,
7849
+ judged
7780
7850
  );
7781
7851
  report.skillsPruned = skillPrune.pruned;
7782
7852
  report.skillsBackedUp.push(...skillPrune.backedUp);
@@ -7785,6 +7855,11 @@ function updateHarnessFiles(args) {
7785
7855
  write: true
7786
7856
  });
7787
7857
  const ledger = writer.ledger();
7858
+ judged.unshift(
7859
+ ...ledger.writes.flatMap(
7860
+ ({ path, verdict, recorded: recorded2, line, backup, backupAbs }) => backup === void 0 || backupAbs === void 0 ? [] : [{ path, verdict, recorded: recorded2, line, backup, backupAbs }]
7861
+ )
7862
+ );
7788
7863
  recordHarnessWrites(projectDir, ledger, {
7789
7864
  claims,
7790
7865
  prunedPolicy,
@@ -7815,10 +7890,25 @@ function updateHarnessFiles(args) {
7815
7890
  displaced: ledger.rootFiles.filter((f) => f.change === "displaced"),
7816
7891
  restored,
7817
7892
  foreignOwned,
7818
- outside: ledger.outside
7893
+ outside: ledger.outside,
7894
+ judged
7895
+ };
7896
+ }
7897
+ function removedRow(projectDir, path, why, judged, backupAbs) {
7898
+ const { line, recorded: recorded2 } = judged;
7899
+ if (backupAbs === null) return { path, verdict: "remove", recorded: recorded2, line: `${why} \u2014 ${line}` };
7900
+ const backup = relative5(projectDir, backupAbs).split(sep6).join("/");
7901
+ return {
7902
+ path,
7903
+ verdict: "backup+remove",
7904
+ recorded: recorded2,
7905
+ line: `${why} \u2014 ${withBackup(line, backup)}`,
7906
+ backup,
7907
+ backupAbs
7819
7908
  };
7820
7909
  }
7821
- function pruneSkillExtras(projectDir, templatesDir, ids, outside, recordOf, now) {
7910
+ var NOT_RECORDED_REMOVED = "not on the install record \u2014 saved as <file>.backup-<time>, removed from live";
7911
+ function pruneSkillExtras(projectDir, templatesDir, ids, outside, recordOf, now, judged) {
7822
7912
  const pruned = [];
7823
7913
  const backedUp = [];
7824
7914
  for (const id of ids) {
@@ -7829,11 +7919,18 @@ function pruneSkillExtras(projectDir, templatesDir, ids, outside, recordOf, now)
7829
7919
  if (foreignOwnedTarget(projectDir, `.claude/skills/${id}/${rel}`) !== null) continue;
7830
7920
  const targetFile = join30(targetSkill, rel);
7831
7921
  if (outside.skip(targetFile)) continue;
7832
- const rec = recordOf(`.claude/skills/${id}/${rel}`);
7833
- if (rec.state !== "sha" || hashContent(readFileSync22(targetFile, "utf8")) !== rec.sha256) {
7834
- backupFile(targetFile, now);
7922
+ const path = `.claude/skills/${id}/${rel}`;
7923
+ const rec = recordOf(path);
7924
+ const disk = rec.state === "sha" ? readFileSync22(targetFile, "utf8") : "";
7925
+ const line = rec.state === "none" ? NOT_RECORDED_REMOVED : judge({ op: "remove", kind: "harness", rec, disk, next: null }).line;
7926
+ let backupAbs = null;
7927
+ if (rec.state !== "sha" || hashContent(disk) !== rec.sha256) {
7928
+ backupAbs = backupFile(targetFile, now);
7835
7929
  backedUp.push(`${id}/${rel}`);
7836
7930
  }
7931
+ judged.push(
7932
+ removedRow(projectDir, path, "not in bundle", { line, recorded: rec.state }, backupAbs)
7933
+ );
7837
7934
  unlinkSync2(targetFile);
7838
7935
  pruned.push(`${id}/${rel}`);
7839
7936
  for (let d2 = dirname7(targetFile); d2 !== targetSkill && readdirSync4(d2).length === 0; d2 = dirname7(d2))
@@ -7908,7 +8005,7 @@ function writeBackupList(projectDir, backups, noChecksum = /* @__PURE__ */ new S
7908
8005
  `${JSON.stringify(
7909
8006
  {
7910
8007
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
7911
- note: "update \uAC00 \uB0A8\uAE34 \uBC31\uC5C5 \uC30D. \uC6D0\uBCF8(path)\uC740 \uCD5C\uC2E0 \uBC88\uB4E4\uD310, \uBC31\uC5C5(backup)\uC740 \uBC14\uAFB8\uAE30 \uC804 \uD310. noChecksum \uC774 \uC5C6\uB294 \uC30D\uC740 \uAE30\uB85D\uACFC \uB2EC\uB77C\uC9C4 \uD30C\uC77C = \uB2F9\uC2E0\uC758 \uD3B8\uC9D1\uBD84\uC774\uB2E4 \u2014 \uC0C8 \uD310\uC5D0 \uB2E4\uC2DC \uC5B9\uC73C\uB824\uBA74 \uB450 \uD30C\uC77C\uC758 diff \uB97C \uBCF4\uB77C. noChecksum: true \uC778 \uC30D\uC740 \uC61B \uD310\uC774 \uCCB4\uD06C\uC12C\uC744 \uAE30\uB85D\uD558\uC9C0 \uC54A\uC544 \uBE44\uAD50 \uC5C6\uC774 \uD55C \uBC88 \uBC31\uC5C5\uD55C \uAC83 \u2014 \uB300\uAC1C \uB9B4\uB9AC\uC988 \uC0AC\uC774 \uD15C\uD50C\uB9BF \uBCC0\uACBD\uBD84\uC774\uB77C \uB2F9\uC2E0\uC758 \uD3B8\uC9D1\uC774 \uC544\uB2D0 \uC218 \uC788\uB2E4.",
8008
+ note: "update \uAC00 \uB0A8\uAE34 \uBC31\uC5C5 \uC30D. \uC6D0\uBCF8(path)\uC740 \uCD5C\uC2E0 \uBC88\uB4E4\uD310, \uBC31\uC5C5(backup)\uC740 \uBC14\uAFB8\uAE30 \uC804 \uD310. noChecksum \uC774 \uC5C6\uB294 \uC30D\uC740 \uAE30\uB85D\uACFC \uB2EC\uB77C\uC9C4 \uD30C\uC77C = \uB2F9\uC2E0\uC758 \uD3B8\uC9D1\uBD84\uC774\uB2E4 \u2014 \uC0C8 \uD310\uC5D0 \uB2E4\uC2DC \uC5B9\uC73C\uB824\uBA74 \uB450 \uD30C\uC77C\uC758 diff \uB97C \uBCF4\uB77C. noChecksum: true \uC778 \uC30D\uC740 \uC61B \uD310\uC774 \uCCB4\uD06C\uC12C\uC744 \uAE30\uB85D\uD558\uC9C0 \uC54A\uC544 \uBE44\uAD50 \uC5C6\uC774 \uD55C \uBC88 \uBC31\uC5C5\uD55C \uAC83 \u2014 \uB300\uAC1C \uB9B4\uB9AC\uC988 \uC0AC\uC774 \uD15C\uD50C\uB9BF \uBCC0\uACBD\uBD84\uC774\uB77C \uB2F9\uC2E0\uC758 \uD3B8\uC9D1\uC774 \uC544\uB2D0 \uC218 \uC788\uB2E4. removed: true \uC778 \uC30D\uC740 \uADF8 \uD30C\uC77C\uC744 \uC9C0\uC6B0\uAE30 \uC804\uC5D0 \uB0A8\uAE34 \uAC83(\uC740\uD1F4\uD55C \uD558\uB124\uC2A4 \uD30C\uC77C \xB7 \uBC88\uB4E4\uC5D0 \uC5C6\uB294 \uC2A4\uD0AC \uD30C\uC77C \uB4F1) \u2014 \uC6D0\uBCF8 \uC790\uB9AC\uC5D0 \uC0C8 \uD310\uC774 \uC5C6\uB2E4. \uB2F9\uC2E0 \uD30C\uC77C\uC774\uC5C8\uB2E4\uBA74 \uBC31\uC5C5\uC744 \uC6D0\uB798 \uC774\uB984\uC73C\uB85C \uB418\uB3CC\uB9AC\uBA74 \uB41C\uB2E4.",
7912
8009
  backups: backups.map((b2) => noChecksum.has(b2.path) ? { ...b2, noChecksum: true } : b2)
7913
8010
  },
7914
8011
  null,
@@ -7927,7 +8024,7 @@ function trackOnlyFileAssets(tracks) {
7927
8024
  (e2) => e2.type === "file" && e2.applies(optedOut) && e2.applies(optedIn)
7928
8025
  );
7929
8026
  }
7930
- function syncHarnessAnchor(projectDir, templatesDir, report, outside, now = /* @__PURE__ */ new Date()) {
8027
+ function syncHarnessAnchor(projectDir, templatesDir, report, outside, judged, now = /* @__PURE__ */ new Date()) {
7931
8028
  if (existsSync21(join30(projectDir, LEGACY_ANCHOR_FILE2))) {
7932
8029
  report.legacyAnchor = LEGACY_ANCHOR_FILE2;
7933
8030
  }
@@ -7942,8 +8039,20 @@ function syncHarnessAnchor(projectDir, templatesDir, report, outside, now = /* @
7942
8039
  const recorded2 = readInstallLog(projectDir)?.templates.rootClaudeMd;
7943
8040
  const owned = recorded2?.path === HARNESS_ANCHOR_FILE && hashContent(current) === recorded2.sha256;
7944
8041
  if (!owned) {
7945
- backupFile(anchor, now);
8042
+ const backup = backupFile(anchor, now);
7946
8043
  report.anchorBackedUp = true;
8044
+ const rel = relative5(projectDir, backup).split(sep6).join("/");
8045
+ const rec = recorded2?.path === HARNESS_ANCHOR_FILE ? { state: "sha", sha256: recorded2.sha256 } : { state: "no-sha" };
8046
+ const next = readFileSync22(templateMd, "utf8");
8047
+ const { line } = judge({ op: "write", kind: "harness", rec, disk: current, next });
8048
+ judged.push({
8049
+ path: HARNESS_ANCHOR_FILE,
8050
+ verdict: "backup+overwrite",
8051
+ recorded: rec.state,
8052
+ line: withBackup(line, rel),
8053
+ backup: rel,
8054
+ backupAbs: backup
8055
+ });
7947
8056
  }
7948
8057
  copyFileSync(templateMd, anchor);
7949
8058
  report.claudeMdUpdated = true;
@@ -8107,7 +8216,7 @@ function changedRemoteMcp(writes) {
8107
8216
  const changed = /* @__PURE__ */ new Set([...mcp.added, ...mcp.replaced ?? []]);
8108
8217
  return (mcp.remote ?? []).filter((r) => changed.has(r.name));
8109
8218
  }
8110
- function refreshExternalCli(projectDir, harnessRoot, droppedKeys, harnessMcp) {
8219
+ function refreshExternalCli(projectDir, harnessRoot, droppedKeys, harnessMcp, linkedIds) {
8111
8220
  const log = readInstallLog(projectDir);
8112
8221
  const presentBefore = sharedSkillIdsOnDisk(projectDir);
8113
8222
  const rulesBefore = /* @__PURE__ */ new Set();
@@ -8139,11 +8248,23 @@ function refreshExternalCli(projectDir, harnessRoot, droppedKeys, harnessMcp) {
8139
8248
  // 앞 단계(하네스 파일 쓰기)가 헬퍼만 든 `externalFiles` 를 적어도 같은 답이다 — 헬퍼는 외부 자리로 세지 않는다(B-2)
8140
8249
  owned: recordedOwner(log)
8141
8250
  });
8251
+ const linked = refreshLinkedSkillBodies({
8252
+ harnessRoot,
8253
+ projectDir,
8254
+ ids: linkedIds.filter(
8255
+ (id) => !result.externalFiles.some((f) => f.path === `${SHARED_SKILLS_DIR}/${id}/SKILL.md`)
8256
+ ),
8257
+ baseline: new Map(
8258
+ mergeExternalFiles(log?.externalFiles, result.externalFiles).map((f) => [f.path, f.sha256])
8259
+ )
8260
+ });
8261
+ const written = [...result.externalFiles, ...linked.files];
8262
+ const displaced = [...result.displaced ?? [], ...linked.displaced];
8142
8263
  const anchorPath = result.anchorRetired?.action === "removed" ? result.anchorRetired.path : null;
8143
8264
  const anchorDisplaced = log === null || anchorPath === null ? [] : restoreDisplaced(log, projectDir, { has: (p2) => p2 === anchorPath, write: true });
8144
8265
  if (log) {
8145
8266
  const retiredPath = result.anchorRetired?.forget === true ? result.anchorRetired.path : null;
8146
- const merged = mergeExternalFiles(log.externalFiles, result.externalFiles).filter(
8267
+ const merged = mergeExternalFiles(log.externalFiles, written).filter(
8147
8268
  (f) => f.path !== retiredPath
8148
8269
  );
8149
8270
  const next = retiredPath === null ? { ...log } : { ...log, spec: { ...log.spec, clis: [...installedClis(log)] } };
@@ -8157,7 +8278,7 @@ function refreshExternalCli(projectDir, harnessRoot, droppedKeys, harnessMcp) {
8157
8278
  if (portions.length > 0) next.portions = portions;
8158
8279
  else delete next.portions;
8159
8280
  const created = result.sharedFiles.filter((f) => f.createdAsRoot === true).map((f) => ({ path: f.path, change: "created", notes: ["\uD558\uB124\uC2A4 \uBAAB\uB9CC(\uBE14\uB85D)"] }));
8160
- const roots = [...created, ...withoutLostBackups(log.rootFiles, result.displaced ?? [])];
8281
+ const roots = [...created, ...withoutLostBackups(log.rootFiles, displaced)];
8161
8282
  if (roots.length > 0) next.rootFiles = mergeRootFiles(log.rootFiles, roots);
8162
8283
  if (anchorDisplaced.some((d2) => d2.forget))
8163
8284
  next.rootFiles = withoutRestored(next.rootFiles, anchorDisplaced);
@@ -8216,11 +8337,12 @@ function refreshExternalCli(projectDir, harnessRoot, droppedKeys, harnessMcp) {
8216
8337
  const dropped = new Set(droppedKeys);
8217
8338
  return {
8218
8339
  externalUpdated: result.externalUpdated,
8219
- externalBackedUp: result.externalBackedUp,
8220
- displaced: result.displaced ?? [],
8221
- externalForeignOwned: result.externalForeignOwned,
8222
- externalOutside: result.externalOutside,
8223
- written: result.externalFiles.map((f) => f.path),
8340
+ externalBackedUp: [...result.externalBackedUp, ...linked.backedUp],
8341
+ externalJudged: [...result.externalJudged, ...linked.judged],
8342
+ displaced,
8343
+ externalForeignOwned: [...result.externalForeignOwned, ...linked.foreignOwned],
8344
+ externalOutside: [...result.externalOutside, ...linked.outside],
8345
+ written: written.map((f) => f.path),
8224
8346
  skillsInstalled,
8225
8347
  skillsRestored,
8226
8348
  rulesRestored,
@@ -8275,14 +8397,24 @@ function pruneOrphans(target, source, ext, ctx, now) {
8275
8397
  for (const file of readdirSync4(target)) {
8276
8398
  if (!file.endsWith(ext) || existsSync21(join30(source, file))) continue;
8277
8399
  const targetFile = join30(target, file);
8278
- const rec = ctx.recordOf(`.claude/${ctx.prefix}/${file}`);
8400
+ const path = `.claude/${ctx.prefix}/${file}`;
8401
+ const rec = ctx.recordOf(path);
8279
8402
  try {
8280
8403
  const disk = readFileSync22(targetFile, "utf8");
8281
- const { verdict } = judge({ op: "remove", kind: "harness", rec, disk, next: null });
8404
+ const { verdict, line } = judge({ op: "remove", kind: "harness", rec, disk, next: null });
8282
8405
  if (verdict === "leave" || ctx.outside.skip(targetFile)) continue;
8283
- if (verdict === "backup+remove") backupFile(targetFile, now);
8406
+ const backupAbs = verdict === "backup+remove" ? backupFile(targetFile, now) : null;
8284
8407
  unlinkSync2(targetFile);
8285
8408
  removed.push(file);
8409
+ ctx.judged.push(
8410
+ removedRow(
8411
+ ctx.projectDir,
8412
+ path,
8413
+ "retired from the harness",
8414
+ { line, recorded: rec.state },
8415
+ backupAbs
8416
+ )
8417
+ );
8286
8418
  } catch {
8287
8419
  }
8288
8420
  }
@@ -8346,7 +8478,7 @@ function runInstall(ctx) {
8346
8478
  JSON.parse(readFileSync23(settingsPath, "utf8"));
8347
8479
  } catch {
8348
8480
  throw new Error(
8349
- `${relative5(process.cwd(), settingsPath)} is not valid JSON \u2014 left untouched, so no hooks were wired. Fix or delete that file, then run the install again`
8481
+ `${relative6(process.cwd(), settingsPath)} is not valid JSON \u2014 left untouched, so no hooks were wired. Fix or delete that file, then run the install again`
8350
8482
  );
8351
8483
  }
8352
8484
  }
@@ -8415,6 +8547,7 @@ function runInstallStages(ctx, args) {
8415
8547
  externalBackups,
8416
8548
  externalUpdated: _externalUpdated,
8417
8549
  externalBackedUp: _externalBackedUp,
8550
+ externalJudged: _externalJudged,
8418
8551
  displaced: cliDisplaced = [],
8419
8552
  externalForeignOwned,
8420
8553
  externalOutside,
@@ -8967,7 +9100,7 @@ function recordInterruptedInstall(ctx, error, run) {
8967
9100
  const codexTrust = registeredTrust(journal.codexOptIn, ctx.projectDir);
8968
9101
  const written = writtenSoFar(ledger, cliFiles, rootFiles, stage.rootImportWritten);
8969
9102
  if (codexTrust) written.push(`${codexTrust.configPath} (Codex trust entry for this folder)`);
8970
- const projectRel = (abs) => relative5(ctx.projectDir, abs).split(sep6).join("/");
9103
+ const projectRel = (abs) => relative6(ctx.projectDir, abs).split(sep7).join("/");
8971
9104
  const writtenSet = new Set(written);
8972
9105
  const backups = [...own.backups, ...journal.backups].map((abs) => {
8973
9106
  const original = projectRel(backupOriginal(abs) ?? abs);
@@ -9144,7 +9277,7 @@ function listHookFiles(hookDir) {
9144
9277
  init_esm_shims();
9145
9278
  import { homedir as homedir3 } from "os";
9146
9279
  import { existsSync as existsSync24, readdirSync as readdirSync6, readFileSync as readFileSync25 } from "fs";
9147
- import { join as join33, relative as relative6 } from "path";
9280
+ import { join as join33, relative as relative7 } from "path";
9148
9281
 
9149
9282
  // src/preset-recommend.ts
9150
9283
  init_esm_shims();
@@ -9372,9 +9505,11 @@ function renderUpdateSummary(log, spec, report) {
9372
9505
  });
9373
9506
  const backups = report.updateMode?.backups ?? [];
9374
9507
  if (backups.length > 0) {
9375
- const unmeasured = report.updateMode?.noChecksum?.length ?? 0;
9376
- const unmeasuredPart = unmeasured === 0 ? "" : unmeasured === backups.length ? " \u2014 no checksum on record for any of them (installed before checksums were kept), so they may not be your edits" : ` \u2014 ${unmeasured} had no checksum on record (marked noChecksum in the list), so those may not be your edits`;
9377
- const removedBefore = report.updateMode?.outOfTrack?.backedUp.length ?? 0;
9508
+ const replaced = backups.filter((b2) => b2.removed !== true);
9509
+ const removedBefore = backups.length - replaced.length;
9510
+ const noChecksum = new Set(report.updateMode?.noChecksum ?? []);
9511
+ const unmeasured = replaced.filter((b2) => noChecksum.has(b2.path)).length;
9512
+ const unmeasuredPart = unmeasured === 0 ? "" : unmeasured === backups.length ? " \u2014 no checksum on record for any of them (installed before checksums were kept), so they may not be your edits" : ` \u2014 ${unmeasured} ${removedBefore > 0 ? "replaced " : ""}had no checksum on record (marked noChecksum in the list), so those may not be your edits`;
9378
9513
  const how = removedBefore === 0 ? "saved before replacing" : removedBefore === backups.length ? "saved before removing" : `saved before replacing (${removedBefore} before removing)`;
9379
9514
  log(
9380
9515
  infoRow(
@@ -9382,12 +9517,13 @@ function renderUpdateSummary(log, spec, report) {
9382
9517
  `${backups.length} file(s) ${how}, as *.backup-<time>${unmeasuredPart} \xB7 list: .uzys-agent-harness/update-backups.json`
9383
9518
  )
9384
9519
  );
9385
- const displacedN = report.updateMode?.displaced?.length ?? 0;
9386
- if (removedBefore + displacedN < backups.length)
9520
+ const displaced = new Set(report.updateMode?.displaced ?? []);
9521
+ const carry = replaced.filter((b2) => !displaced.has(b2.path)).length;
9522
+ if (carry > 0)
9387
9523
  log(
9388
9524
  infoRow(
9389
9525
  "NEXT",
9390
- unmeasured === backups.length ? 'nothing to carry over unless you remember editing one of them \u2014 then ask audit-harness-fit: "update \uBC31\uC5C5\uBCF8\uC758 \uB0B4 \uD3B8\uC9D1\uC744 \uB0B4 \uD30C\uC77C\uB85C \uC62E\uACA8\uC918"' : 'backups are not loaded \u2014 to keep your edits across updates, ask audit-harness-fit: "update \uBC31\uC5C5\uBCF8\uC758 \uB0B4 \uD3B8\uC9D1\uC744 \uB0B4 \uD30C\uC77C\uB85C \uC62E\uACA8\uC918" (update replaces harness files every time, so edits put back inside them return as backups)'
9526
+ unmeasured === carry ? 'nothing to carry over unless you remember editing one of them \u2014 then ask audit-harness-fit: "update \uBC31\uC5C5\uBCF8\uC758 \uB0B4 \uD3B8\uC9D1\uC744 \uB0B4 \uD30C\uC77C\uB85C \uC62E\uACA8\uC918"' : 'backups are not loaded \u2014 to keep your edits across updates, ask audit-harness-fit: "update \uBC31\uC5C5\uBCF8\uC758 \uB0B4 \uD3B8\uC9D1\uC744 \uB0B4 \uD30C\uC77C\uB85C \uC62E\uACA8\uC918" (update replaces harness files every time, so edits put back inside them return as backups)'
9391
9527
  )
9392
9528
  );
9393
9529
  }
@@ -9779,25 +9915,8 @@ function linkedSkillRows(log, updated, notOurs) {
9779
9915
  );
9780
9916
  }
9781
9917
  }
9782
- function hadFileRow(log, area, paths) {
9783
- if (paths.length === 0) return;
9784
- log(
9785
- assetRow(
9786
- "skip",
9787
- `${area} had files with these names`,
9788
- `${paths.length} backed up as *.backup-<time> \u2014 yours, put back on uninstall \xB7 ${paths.join(", ")}`
9789
- )
9790
- );
9791
- }
9792
9918
  function renderPhase1Rows(log, baseline, verbose = false) {
9793
9919
  if (baseline.updateMode) {
9794
- const noChecksum = new Set(baseline.updateMode.noChecksum ?? []);
9795
- const displaced = baseline.updateMode.displaced ?? [];
9796
- const displacedSkills = displaced.filter((p2) => p2.startsWith(".claude/skills/"));
9797
- const displacedPolicy = displaced.filter(
9798
- (p2) => p2.startsWith(".claude/") && !p2.startsWith(".claude/skills/")
9799
- );
9800
- const displacedExternal = displaced.filter((p2) => !p2.startsWith(".claude/"));
9801
9920
  for (const [dir, count] of Object.entries(baseline.updateMode.updated)) {
9802
9921
  if (count > 0) log(assetRow("success", dir, `${count} files updated`));
9803
9922
  }
@@ -9854,6 +9973,8 @@ function renderPhase1Rows(log, baseline, verbose = false) {
9854
9973
  const row = sharedFileRow(f);
9855
9974
  if (row !== null) log(row);
9856
9975
  }
9976
+ for (const d2 of baseline.updateMode.deadMcp ?? [])
9977
+ log(assetRow("skip", ".mcp.json", `${d2.name} \u2014 ${d2.note}`));
9857
9978
  for (const f of baseline.updateMode.sharedLeft ?? []) {
9858
9979
  log(sharedLeftRow(f));
9859
9980
  if (f.retiredFeatures !== void 0) log(retiredFeaturesRow(f.path, f.retiredFeatures));
@@ -9867,11 +9988,7 @@ function renderPhase1Rows(log, baseline, verbose = false) {
9867
9988
  if (legacy2 !== null) log(legacy2);
9868
9989
  for (const row of legacyReleasedCatalogRows(baseline.updateMode.legacyReleasedCatalog ?? []))
9869
9990
  log(row);
9870
- for (const [dir, removed] of Object.entries(baseline.updateMode.pruned)) {
9871
- if (removed.length > 0) {
9872
- log(assetRow("skip", `${dir} orphan prune`, `${removed.length} removed`));
9873
- }
9874
- }
9991
+ for (const j4 of baseline.updateMode.judged ?? []) log(judgedRow(j4));
9875
9992
  if (baseline.updateMode.skippedGroups.length > 0) {
9876
9993
  log(
9877
9994
  assetRow(
@@ -9884,15 +10001,6 @@ function renderPhase1Rows(log, baseline, verbose = false) {
9884
10001
  if (baseline.updateMode.claudeMdUpdated) {
9885
10002
  log(assetRow("success", HARNESS_ANCHOR_FILE, "refreshed from template"));
9886
10003
  }
9887
- if (baseline.updateMode.anchorBackedUp && !noChecksum.has(HARNESS_ANCHOR_FILE)) {
9888
- log(
9889
- assetRow(
9890
- "skip",
9891
- `${HARNESS_ANCHOR_FILE} edited`,
9892
- "your edits backed up as *.backup-<time> \u2014 latest template is now active"
9893
- )
9894
- );
9895
- }
9896
10004
  if (baseline.updateMode.anchorCreated) {
9897
10005
  log(assetRow("success", HARNESS_ANCHOR_FILE, "created from template (anchor migration)"));
9898
10006
  }
@@ -9920,28 +10028,6 @@ function renderPhase1Rows(log, baseline, verbose = false) {
9920
10028
  if (baseline.updateMode.legacyAnchor) {
9921
10029
  log(assetRow("skip", baseline.updateMode.legacyAnchor, LEGACY_ANCHOR_NOTE));
9922
10030
  }
9923
- const skillsEdited = baseline.updateMode.skillsBackedUp.filter(
9924
- (p2) => !noChecksum.has(`.claude/skills/${p2}`)
9925
- );
9926
- if (skillsEdited.length > 0) {
9927
- log(
9928
- assetRow(
9929
- "skip",
9930
- ".claude/skills edited files",
9931
- `${skillsEdited.length} backed up as *.backup-<time>`
9932
- )
9933
- );
9934
- }
9935
- hadFileRow(log, ".claude/skills", displacedSkills);
9936
- if (baseline.updateMode.skillsPruned.length > 0) {
9937
- log(
9938
- assetRow(
9939
- "success",
9940
- ".claude/skills not in bundle",
9941
- `${baseline.updateMode.skillsPruned.length} files deleted \xB7 ${baseline.updateMode.skillsPruned.join(", ")}`
9942
- )
9943
- );
9944
- }
9945
10031
  linkedSkillRows(
9946
10032
  log,
9947
10033
  baseline.updateMode.skillsLinked ?? [],
@@ -9995,29 +10081,6 @@ function renderPhase1Rows(log, baseline, verbose = false) {
9995
10081
  )
9996
10082
  );
9997
10083
  }
9998
- const policyEdited = baseline.updateMode.policyBackedUp.filter(
9999
- (p2) => !noChecksum.has(`.claude/${p2}`)
10000
- );
10001
- if (policyEdited.length > 0) {
10002
- log(
10003
- assetRow(
10004
- "skip",
10005
- "edited policy files",
10006
- `${policyEdited.length} backed up as *.backup-<time>`
10007
- )
10008
- );
10009
- }
10010
- hadFileRow(log, "policy files", displacedPolicy);
10011
- const unmeasured = [...noChecksum].filter((p2) => p2 !== ".mcp-allowlist");
10012
- if (unmeasured.length > 0) {
10013
- log(
10014
- assetRow(
10015
- "skip",
10016
- "no checksum on record",
10017
- `${unmeasured.length} backed up once as *.backup-<time> \u2014 installed before checksums were kept, so these may not be your edits (usually what changed between releases); later updates compare precisely`
10018
- )
10019
- );
10020
- }
10021
10084
  const retiredRefs = baseline.updateMode.retiredHookRefsRemoved ?? [];
10022
10085
  if (retiredRefs.length > 0) {
10023
10086
  log(
@@ -10067,17 +10130,6 @@ function renderPhase1Rows(log, baseline, verbose = false) {
10067
10130
  )
10068
10131
  );
10069
10132
  }
10070
- const externalEdited = baseline.updateMode.externalBackedUp.filter((p2) => !noChecksum.has(p2));
10071
- if (externalEdited.length > 0) {
10072
- log(
10073
- assetRow(
10074
- "skip",
10075
- "edited external CLI files",
10076
- `${externalEdited.length} backed up as *.backup-<time>`
10077
- )
10078
- );
10079
- }
10080
- hadFileRow(log, "external CLI files", displacedExternal);
10081
10133
  const displacedSame = baseline.updateMode.displacedSame ?? [];
10082
10134
  if (displacedSame.length > 0) {
10083
10135
  log(
@@ -10538,7 +10590,7 @@ function renderInterruptedInstall(err, e2, cwd = process.cwd()) {
10538
10590
  }
10539
10591
  const { path, error } = e2.record;
10540
10592
  err(
10541
- path !== null ? ` Already in place before it stopped \u2014 recorded in ${relative6(cwd, path) || path}:` : " Already in place before it stopped:"
10593
+ path !== null ? ` Already in place before it stopped \u2014 recorded in ${relative7(cwd, path) || path}:` : " Already in place before it stopped:"
10542
10594
  );
10543
10595
  for (const [key, n] of groups) {
10544
10596
  err(` ${key.endsWith("/") ? `${key} (${n} file${n === 1 ? "" : "s"})` : key}`);
@@ -11011,7 +11063,7 @@ import {
11011
11063
  statSync as statSync5,
11012
11064
  writeFileSync as writeFileSync9
11013
11065
  } from "fs";
11014
- import { dirname as dirname8, join as join35, relative as relative7, resolve as resolve4, sep as sep7 } from "path";
11066
+ import { dirname as dirname8, join as join35, relative as relative8, resolve as resolve4, sep as sep8 } from "path";
11015
11067
 
11016
11068
  // src/uninstall-interactive.ts
11017
11069
  init_esm_shims();
@@ -12940,12 +12992,7 @@ function settleLog(ctx, io) {
12940
12992
  const recordDir = join35(projectDir, INSTALL_LOG_DIR);
12941
12993
  if (outsideLinkAt(projectDir, INSTALL_LOG_DIR) !== null) io.rm(recordDir);
12942
12994
  else {
12943
- for (const rel of [
12944
- `${INSTALL_LOG_DIR}/${INSTALL_LOG_FILENAME}`,
12945
- ...HARNESS_RUNTIME_IGNORES
12946
- ]) {
12947
- io.rm(join35(projectDir, rel));
12948
- }
12995
+ for (const rel of RECORD_FILES) io.rm(join35(projectDir, rel));
12949
12996
  pruneEmptyDirsUpward(projectDir, recordDir);
12950
12997
  }
12951
12998
  if (outsideLinkAt(projectDir, LEGACY_INSTALL_LOG_DIR) === null) {
@@ -13131,13 +13178,16 @@ function resolveCliTarget(ctx, io) {
13131
13178
  function underAny(path, owned) {
13132
13179
  return owned.some((o) => o.endsWith("/") ? path.startsWith(o) : path === o);
13133
13180
  }
13134
- function planCliRemoval(target, remaining) {
13181
+ function planCliRemoval(target, remaining, log, projectDir) {
13135
13182
  const { exclusive, sharedNowUnowned } = removableFor(target, remaining);
13136
13183
  const owned = [...exclusive, ...sharedNowUnowned];
13137
13184
  const prefixes = owned.filter((o) => o.kind === "recorded" || o.kind === "anchor").map((o) => o.path);
13138
- const has = (path) => underAny(path, prefixes);
13185
+ const used = (path) => usersOf(path, projectDir).some((cli) => remaining.includes(cli));
13186
+ const has = (path) => underAny(path, prefixes) && !used(path);
13187
+ const kept = harnessCandidates(log).filter((p2) => underAny(p2, prefixes) && used(p2)).map((p2) => p2.split("/").slice(0, 3).join("/"));
13139
13188
  return {
13140
13189
  target,
13190
+ kept: [...new Set(kept)].sort(),
13141
13191
  scope: {
13142
13192
  has,
13143
13193
  // `.claude/settings.json` 은 claude 의 `.claude/` 아래 · `.codex/config.toml` 은 codex 의 `.codex/` 아래 · `AGENTS.md` ·
@@ -13151,6 +13201,11 @@ function planCliRemoval(target, remaining) {
13151
13201
  }
13152
13202
  };
13153
13203
  }
13204
+ function keptForRemainingLines(plan, preview) {
13205
+ if (plan.kept.length === 0) return [];
13206
+ const what = `${plan.kept.join(" \xB7 ")} \u2014 still used by Claude Code through its .claude/skills/ link(s)`;
13207
+ return [preview ? ` \u25CB leave ${what}` : ` ${c.yellow("\u2298")} left ${what}`];
13208
+ }
13154
13209
  function removeCliHeader(target, installed, remaining) {
13155
13210
  return [
13156
13211
  "",
@@ -13172,6 +13227,7 @@ function removeCliDryRunLines(plan, ctx, remaining) {
13172
13227
  harnessRoot
13173
13228
  });
13174
13229
  lines2.push(...harnessRemovalLines(preview, installLog, projectDir, plan.scope, true));
13230
+ lines2.push(...keptForRemainingLines(plan, true));
13175
13231
  lines2.push(...assetsLeftLines(installLog, projectDir, plan.scope));
13176
13232
  const rerender = agentsMdRerenderSet(plan.target, remaining, installLog, projectDir);
13177
13233
  if (rerender !== null) {
@@ -13281,7 +13337,7 @@ function removeCliAction(ctx, io) {
13281
13337
  const { options, installLog, projectDir, harnessRoot } = ctx;
13282
13338
  const installed = installedClis(installLog);
13283
13339
  const remaining = installed.filter((cli) => cli !== target);
13284
- const plan = planCliRemoval(target, remaining);
13340
+ const plan = planCliRemoval(target, remaining, installLog, projectDir);
13285
13341
  for (const line of removeCliHeader(target, installed, remaining)) io.log(line);
13286
13342
  if (options.dryRun) {
13287
13343
  for (const line of removeCliDryRunLines(plan, ctx, remaining)) io.log(line);
@@ -13296,6 +13352,7 @@ function removeCliAction(ctx, io) {
13296
13352
  for (const line of harnessRemovalLines(done, installLog, projectDir, plan.scope, false)) {
13297
13353
  io.log(line);
13298
13354
  }
13355
+ for (const line of keptForRemainingLines(plan, false)) io.log(line);
13299
13356
  for (const line of assetsLeftLines(installLog, projectDir, plan.scope)) io.log(line);
13300
13357
  const rerenderSet = agentsMdRerenderSet(target, remaining, installLog, projectDir);
13301
13358
  const rerender = rerenderSet === null ? null : rerenderAgentsMd(ctx, rerenderSet);
@@ -13561,11 +13618,16 @@ var LEGACY_LOG = `${LEGACY_INSTALL_LOG_DIR}/${INSTALL_LOG_FILENAME}`;
13561
13618
  var SKILLS_DIR3 = ".claude/skills/";
13562
13619
  var CLI_FOLDERS2 = [".claude", ".codex", ".opencode"];
13563
13620
  var LEFT_FILES_SHOWN = 3;
13621
+ var RECORD_FILES = [
13622
+ `${INSTALL_LOG_DIR}/${INSTALL_LOG_FILENAME}`,
13623
+ ...HARNESS_RUNTIME_IGNORES
13624
+ ];
13564
13625
  var FULL_SCOPE = {
13565
13626
  has: () => true,
13566
13627
  shared: UNINSTALL_SHARED,
13567
13628
  skeleton: PROJECT_SKELETON_DIRS,
13568
- folders: CLI_FOLDERS2,
13629
+ // #817 — 기록 폴더도 — 헬퍼는 파일 하나씩 회수하고(#812) 설치자 파일은 남으니, 폴더가 왜 남았는지 같은 줄로 말한다
13630
+ folders: [...CLI_FOLDERS2, INSTALL_LOG_DIR],
13569
13631
  importBlock: true,
13570
13632
  anchor: true
13571
13633
  };
@@ -13722,7 +13784,7 @@ function removeRecordedFile(path, ctx) {
13722
13784
  if (verdict.verdict !== "remove" && verdict.verdict !== "backup+remove") return;
13723
13785
  let backup = null;
13724
13786
  if (verdict.verdict === "backup+remove") {
13725
- backup = io.write ? relative7(projectDir, backupFile(effective)).split(sep7).join("/") : `${relative7(projectDir, effective).split(sep7).join("/")}.backup-<time>`;
13787
+ backup = io.write ? relative8(projectDir, backupFile(effective)).split(sep8).join("/") : `${relative8(projectDir, effective).split(sep8).join("/")}.backup-<time>`;
13726
13788
  }
13727
13789
  if (io.write) {
13728
13790
  if (effective !== abs) io.rm(abs);
@@ -13742,7 +13804,7 @@ function legacyFileCandidates() {
13742
13804
  }
13743
13805
  function mayReclaimLegacy(log, projectDir, path) {
13744
13806
  const clis = installedClis(log);
13745
- if (!ownersOf(path).some((c3) => clis.includes(c3))) return false;
13807
+ if (!usersOf(path, projectDir).some((c3) => clis.includes(c3))) return false;
13746
13808
  if (isExcludedPath(bundlePath(path), excludedIds(log))) return false;
13747
13809
  return !(log.rootFiles ?? []).some(
13748
13810
  (f) => f.path === path && f.change === "displaced" && !isLegacyDisplaced(log, projectDir, f)
@@ -14025,7 +14087,7 @@ function leftFolderLines(r, log, projectDir, scope, preview) {
14025
14087
  const entries = /* @__PURE__ */ new Map();
14026
14088
  for (const n of yoursUnder(projectDir, dir)) {
14027
14089
  const full = `${dir}/${n.name}`;
14028
- if (full === LEGACY_LOG) continue;
14090
+ if (full === LEGACY_LOG || RECORD_FILES.includes(full)) continue;
14029
14091
  if (!gone.has(full) && !assetFiles.has(full) && !said(full)) entries.set(n.name, n.shown);
14030
14092
  }
14031
14093
  for (const p2 of back)
@@ -14125,7 +14187,7 @@ function removeAnchor(log, projectDir, io) {
14125
14187
  ]
14126
14188
  };
14127
14189
  }
14128
- const backup = edited ? relative7(projectDir, backupFile(abs)).split(sep7).join("/") : null;
14190
+ const backup = edited ? relative8(projectDir, backupFile(abs)).split(sep8).join("/") : null;
14129
14191
  io.rm(abs);
14130
14192
  return {
14131
14193
  removed: true,
@@ -14188,7 +14250,7 @@ function removeLegacyAgentsMd(path, abs, effective, strip, ctx) {
14188
14250
  let backup = `${path}.backup-<time>`;
14189
14251
  try {
14190
14252
  if (io.write) {
14191
- backup = relative7(projectDir, backupFile(effective)).split(sep7).join("/");
14253
+ backup = relative8(projectDir, backupFile(effective)).split(sep8).join("/");
14192
14254
  if (strip.remainder !== null) {
14193
14255
  writeFileSync9(effective, neutralizeScaffoldBanner(strip.remainder), "utf8");
14194
14256
  } else {
@@ -14234,7 +14296,7 @@ function removeToolFiles(files, projectDir, rm) {
14234
14296
  try {
14235
14297
  if (hashContent(readFileSync27(abs, "utf8")) !== sha256) {
14236
14298
  const backup = backupFile(abs);
14237
- notes.push(`backed up ${path} \u2192 ${relative7(projectDir, backup)} (changed since install)`);
14299
+ notes.push(`backed up ${path} \u2192 ${relative8(projectDir, backup)} (changed since install)`);
14238
14300
  }
14239
14301
  rm(abs);
14240
14302
  removed.push(path);
@@ -14249,8 +14311,8 @@ function removeToolFiles(files, projectDir, rm) {
14249
14311
  function pruneEmptyDirsUpward(projectDir, startDir) {
14250
14312
  let cursor = startDir;
14251
14313
  while (true) {
14252
- const rel = relative7(projectDir, cursor);
14253
- if (rel === "" || rel.startsWith(`..${sep7}`) || rel === "..") return;
14314
+ const rel = relative8(projectDir, cursor);
14315
+ if (rel === "" || rel.startsWith(`..${sep8}`) || rel === "..") return;
14254
14316
  try {
14255
14317
  if (readdirSync7(cursor).length > 0) return;
14256
14318
  rmdirSync3(cursor);