@uzysjung/agent-harness 26.172.0 → 26.174.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
@@ -96,12 +96,13 @@ import {
96
96
  resolveScope,
97
97
  scaffoldBanner,
98
98
  sharedPathOfKeyId,
99
+ shouldInstallAsset,
99
100
  stripHarnessImport,
100
101
  summarizeContextCost,
101
102
  targetsInclude,
102
103
  upsertHarnessImport,
103
104
  writeInstallLog
104
- } from "./chunk-T2HBTG32.js";
105
+ } from "./chunk-LAWKDFKZ.js";
105
106
 
106
107
  // node_modules/sisteransi/src/index.js
107
108
  var require_src = __commonJS({
@@ -3713,8 +3714,8 @@ function padDisplay(s, width) {
3713
3714
 
3714
3715
  // src/install-writes.ts
3715
3716
  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";
3717
+ import { existsSync as existsSync6, mkdirSync, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
3718
+ import { dirname, join as join8, relative, sep } from "path";
3718
3719
 
3719
3720
  // src/env-files.ts
3720
3721
  init_esm_shims();
@@ -3804,7 +3805,8 @@ var LINES = {
3804
3805
  newInRelease: "wrote (new in this release)",
3805
3806
  refreshed: "refreshed",
3806
3807
  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>",
3808
+ // 비켜 둔 설치자 파일은 하네스가 그 자리를 떠날 때(uninstall · 회수) 되돌린다(`displaced` · #782) — 그 약속을 같은 줄에
3809
+ hadFile: "had a file with this name \u2014 saved as <file>.backup-<time> \u2014 yours, put back on uninstall",
3808
3810
  noChecksum: "no checksum on record \u2014 saved a copy once",
3809
3811
  edited: "you edited it \u2014 saved as <file>.backup-<time>",
3810
3812
  removed: "removed",
@@ -3817,6 +3819,9 @@ var LINES = {
3817
3819
  scaffoldKept: "kept \u2014 already there (yours)",
3818
3820
  leftForYou: "left for you (yours now)"
3819
3821
  };
3822
+ function withBackup(line, backup) {
3823
+ return line.replace("<file>.backup-<time>", backup);
3824
+ }
3820
3825
  function unreadable(adapter, op) {
3821
3826
  const what = op === "write" ? "harness part not added" : "harness part not removed";
3822
3827
  return `could not read it (${ADAPTERS[adapter].unreadable}) \u2014 ${what}`;
@@ -3935,7 +3940,8 @@ var PRIOR_MCP_SERVERS = [
3935
3940
  // #766 — `@supabase/mcp-server` 는 npm 에 없는 이름이라(404) 서버가 뜬 적이 없다
3936
3941
  name: "supabase",
3937
3942
  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"
3943
+ updated: "now Supabase's remote server \u2014 sign in on first use, see NEXT",
3944
+ 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
3945
  },
3940
3946
  {
3941
3947
  name: "chrome-devtools",
@@ -4116,7 +4122,7 @@ function recordedMcpAssetIds(log) {
4116
4122
  // src/recorded.ts
4117
4123
  init_esm_shims();
4118
4124
  import { existsSync as existsSync5 } from "fs";
4119
- import { join as join6 } from "path";
4125
+ import { join as join7 } from "path";
4120
4126
 
4121
4127
  // src/cli-ownership.ts
4122
4128
  init_esm_shims();
@@ -4171,6 +4177,42 @@ function removableFor(cli, remaining) {
4171
4177
  return { exclusive, sharedNowUnowned };
4172
4178
  }
4173
4179
 
4180
+ // src/foreign-slot.ts
4181
+ init_esm_shims();
4182
+ import { lstatSync, realpathSync, statSync } from "fs";
4183
+ import { join as join6 } from "path";
4184
+ var SKILL_SLOT = /^((?:\.claude|\.agents)\/skills\/[^/]+)(?:\/|$)/;
4185
+ function occupiedByNonDirectory(path) {
4186
+ const stat = lstatSync(path, { throwIfNoEntry: false });
4187
+ return stat !== void 0 && !stat.isDirectory();
4188
+ }
4189
+ function linksToProjectSharedSkill(projectDir, id) {
4190
+ const slot = join6(projectDir, ".claude/skills", id);
4191
+ if (lstatSync(slot, { throwIfNoEntry: false })?.isSymbolicLink() !== true) return false;
4192
+ try {
4193
+ const real = realpathSync(slot);
4194
+ return real === join6(realpathSync(projectDir), ".agents/skills", id) && statSync(real).isDirectory();
4195
+ } catch {
4196
+ return false;
4197
+ }
4198
+ }
4199
+ function foreignOwnedTarget(projectDir, rel) {
4200
+ const slot = SKILL_SLOT.exec(rel)?.[1];
4201
+ if (slot === void 0) return null;
4202
+ if (occupiedByNonDirectory(join6(projectDir, slot))) return slot;
4203
+ if (rel !== slot) {
4204
+ const parts = rel.slice(slot.length + 1).split("/");
4205
+ let cursor = slot;
4206
+ for (const part of parts.slice(0, -1)) {
4207
+ cursor = `${cursor}/${part}`;
4208
+ if (occupiedByNonDirectory(join6(projectDir, cursor))) return cursor;
4209
+ }
4210
+ const leaf = lstatSync(join6(projectDir, rel), { throwIfNoEntry: false });
4211
+ if (leaf !== void 0 && !leaf.isFile()) return rel;
4212
+ }
4213
+ return null;
4214
+ }
4215
+
4174
4216
  // src/recorded.ts
4175
4217
  var CLAUDE_DIR = ".claude/";
4176
4218
  var ADVISORY_PATHS = [".env.example", ".github/workflows/", "skills-lock.json"];
@@ -4190,6 +4232,12 @@ function ownersOf(path) {
4190
4232
  )
4191
4233
  );
4192
4234
  }
4235
+ function usersOf(path, projectDir) {
4236
+ const owners = ownersOf(path);
4237
+ const id = /^\.agents\/skills\/([^/]+)\//.exec(path)?.[1];
4238
+ const linked = id !== void 0 && linksToProjectSharedSkill(projectDir, id);
4239
+ return linked && !owners.includes("claude") ? [...owners, "claude"] : owners;
4240
+ }
4193
4241
  function bundlePath(path) {
4194
4242
  return path.startsWith(".agents/skills/") ? `${SKILLS_DIR}${path.slice(".agents/skills/".length)}` : path;
4195
4243
  }
@@ -4202,9 +4250,9 @@ function recordedTrackTargets(log) {
4202
4250
  selectedInternalSkills: INTERNAL_BUNDLED_SKILL_IDS
4203
4251
  };
4204
4252
  const entries = buildManifest(spec).filter((e2) => e2.applies(spec));
4205
- const templates = join6(resolveBundleRoot(), "templates");
4253
+ const templates = join7(resolveBundleRoot(), "templates");
4206
4254
  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
4255
+ (e2) => e2.type === "dir" ? path.startsWith(`${e2.target}/`) && existsSync5(join7(templates, e2.source, path.slice(e2.target.length + 1))) : e2.target === path
4208
4256
  );
4209
4257
  }
4210
4258
  function recordedTrackSpots(log) {
@@ -4212,13 +4260,13 @@ function recordedTrackSpots(log) {
4212
4260
  tracks: log.spec.tracks.filter(isTrack),
4213
4261
  selectedInternalSkills: INTERNAL_BUNDLED_SKILL_IDS
4214
4262
  };
4215
- const templates = join6(resolveBundleRoot(), "templates");
4263
+ const templates = join7(resolveBundleRoot(), "templates");
4216
4264
  const out = [];
4217
4265
  for (const e2 of buildManifest(spec)) {
4218
4266
  if (!e2.applies(spec) || !e2.target.startsWith(CLAUDE_DIR)) continue;
4219
4267
  if (e2.type === "file") out.push(e2.target);
4220
4268
  else
4221
- for (const rel of listFilesRecursive(join6(templates, e2.source)))
4269
+ for (const rel of listFilesRecursive(join7(templates, e2.source)))
4222
4270
  out.push(`${e2.target}/${rel}`);
4223
4271
  }
4224
4272
  for (const p2 of RETIRED_PATHS) out.push(`${CLAUDE_DIR}${p2}`);
@@ -4297,7 +4345,6 @@ function isScaffoldPath(path) {
4297
4345
  // src/install-writes.ts
4298
4346
  var CLAUDE_DIR2 = ".claude/";
4299
4347
  var SKILLS_DIR2 = ".claude/skills/";
4300
- var BACKUP_PLACEHOLDER = "<file>.backup-<time>";
4301
4348
  function createInstallWriter(args) {
4302
4349
  const { projectDir, previousLog, excluded } = args;
4303
4350
  const now = args.now ?? /* @__PURE__ */ new Date();
@@ -4322,16 +4369,17 @@ function createInstallWriter(args) {
4322
4369
  else external.set(path, sha256);
4323
4370
  }
4324
4371
  function harness(path, from, opts = {}) {
4325
- const abs = join7(projectDir, path);
4372
+ const abs = join8(projectDir, path);
4326
4373
  if (outside.skip(abs)) return null;
4327
4374
  const next = "source" in from ? readFileSync4(from.source, "utf8") : from.content;
4328
4375
  const disk = existsSync6(abs) ? readFileSync4(abs, "utf8") : null;
4329
4376
  const rec = recorded(previousLog, path, opts);
4330
4377
  const diskSha = rec.state === "no-sha" && disk !== null ? hashContent(disk) : null;
4378
+ const judgedRec = diskSha !== null && isLegacyHarnessFile(path, diskSha) ? { state: "sha", sha256: diskSha } : rec;
4331
4379
  const j4 = judge({
4332
4380
  op: "write",
4333
4381
  kind: "harness",
4334
- rec: diskSha !== null && isLegacyHarnessFile(path, diskSha) ? { state: "sha", sha256: diskSha } : rec,
4382
+ rec: judgedRec,
4335
4383
  disk,
4336
4384
  next,
4337
4385
  ...args.run === void 0 ? {} : { run: args.run }
@@ -4376,11 +4424,19 @@ function createInstallWriter(args) {
4376
4424
  default:
4377
4425
  throw new Error(`install: '${j4.record}' is not a write record (${path})`);
4378
4426
  }
4379
- const line = backup === void 0 ? j4.line : j4.line.replace(BACKUP_PLACEHOLDER, backup);
4427
+ const line = backup === void 0 ? j4.line : withBackup(j4.line, backup);
4380
4428
  const backupPair = backup === void 0 || backupAbs === void 0 ? {} : { backup, backupAbs };
4381
- writes.push({ path, verdict: j4.verdict, record: j4.record, line, ...backupPair });
4429
+ const state = judgedRec.state;
4430
+ writes.push({
4431
+ path,
4432
+ verdict: j4.verdict,
4433
+ record: j4.record,
4434
+ recorded: state,
4435
+ line,
4436
+ ...backupPair
4437
+ });
4382
4438
  if (j4.line === "" || j4.verdict === "create" || j4.verdict === "overwrite") return null;
4383
- const out = { path, verdict: j4.verdict, line, ...backupPair };
4439
+ const out = { path, verdict: j4.verdict, recorded: state, line, ...backupPair };
4384
4440
  judged.push(out);
4385
4441
  return out;
4386
4442
  }
@@ -4388,7 +4444,7 @@ function createInstallWriter(args) {
4388
4444
  const file = SHARED_FILES[path];
4389
4445
  if (file === void 0) throw new Error(`install: ${path} is not a shared file`);
4390
4446
  const adapter = ADAPTERS[file.adapter];
4391
- const abs = join7(projectDir, path);
4447
+ const abs = join8(projectDir, path);
4392
4448
  const disk = existsSync6(abs) ? readFileSync4(abs, "utf8") : null;
4393
4449
  const own = new Map(
4394
4450
  (previousLog?.portions ?? []).filter((p2) => p2.path === path).map((p2) => [p2.key, p2.sha256])
@@ -4570,9 +4626,9 @@ function retiredClaims(previous, projectDir, kept) {
4570
4626
  const out = [];
4571
4627
  for (const p2 of RETIRED_PATHS) {
4572
4628
  if (have.has(p2) || recorded(previous, `${CLAUDE_DIR2}${p2}`).state === "none") continue;
4573
- const abs = join7(projectDir, CLAUDE_DIR2, p2);
4629
+ const abs = join8(projectDir, CLAUDE_DIR2, p2);
4574
4630
  try {
4575
- if (!statSync(abs).isFile()) continue;
4631
+ if (!statSync2(abs).isFile()) continue;
4576
4632
  out.push({ path: p2, sha256: hashContent(readFileSync4(abs, "utf8")) });
4577
4633
  } catch {
4578
4634
  }
@@ -4677,7 +4733,7 @@ function legacySettingsSeed(text, render, projectDir, opts = {}) {
4677
4733
  } catch {
4678
4734
  return seed;
4679
4735
  }
4680
- const claudeDir = join7(projectDir, ".claude");
4736
+ const claudeDir = join8(projectDir, ".claude");
4681
4737
  const candidates = new Set([...render.keys()].filter((k2) => k2.startsWith("hooks.")));
4682
4738
  const legacyLines = /* @__PURE__ */ new Set();
4683
4739
  const hooks = isObject2(root) && isObject2(root.hooks) ? root.hooks : {};
@@ -4794,12 +4850,12 @@ function legacyGitignoreSeed(text, render, previous) {
4794
4850
  // src/installer.ts
4795
4851
  init_esm_shims();
4796
4852
  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";
4853
+ import { basename as basename4, join as join31, relative as relative6, resolve, sep as sep7 } from "path";
4798
4854
 
4799
4855
  // src/anchor-seed.ts
4800
4856
  init_esm_shims();
4801
4857
  import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
4802
- import { join as join8 } from "path";
4858
+ import { join as join9 } from "path";
4803
4859
 
4804
4860
  // src/agents-md-render.ts
4805
4861
  init_esm_shims();
@@ -4906,7 +4962,7 @@ function compose(body, into) {
4906
4962
  ${trimmed.join("\n")}`;
4907
4963
  }
4908
4964
  function seedAgentsMdProjectContext(projectDir) {
4909
- const root = readIfExists(join8(projectDir, "CLAUDE.md"));
4965
+ const root = readIfExists(join9(projectDir, "CLAUDE.md"));
4910
4966
  if (root === null) return null;
4911
4967
  const withoutImport = stripHarnessImport(root) ?? root;
4912
4968
  const body = dropHarnessQuoteBlocks(dropTitle(withoutImport.split("\n")));
@@ -4914,9 +4970,9 @@ function seedAgentsMdProjectContext(projectDir) {
4914
4970
  return compose(body, "agents-md");
4915
4971
  }
4916
4972
  function seedRootClaudeProjectContext(projectDir, harnessRoot) {
4917
- const agents = readIfExists(join8(projectDir, "AGENTS.md"));
4973
+ const agents = readIfExists(join9(projectDir, "AGENTS.md"));
4918
4974
  if (agents === null) return null;
4919
- const template = readIfExists(join8(harnessRoot, AGENTS_MD_TEMPLATE));
4975
+ const template = readIfExists(join9(harnessRoot, AGENTS_MD_TEMPLATE));
4920
4976
  if (template === null) return null;
4921
4977
  const installer = projectContextInstallerLines({ existing: agents, template });
4922
4978
  if (installer === null) return null;
@@ -4928,7 +4984,7 @@ function seedRootClaudeProjectContext(projectDir, harnessRoot) {
4928
4984
  // src/ci-scaffold.ts
4929
4985
  init_esm_shims();
4930
4986
  import { existsSync as existsSync8 } from "fs";
4931
- import { join as join9 } from "path";
4987
+ import { join as join10 } from "path";
4932
4988
  var PYTHON_TRACKS = "data|csr-fastapi|full";
4933
4989
  var VARIANTS = [
4934
4990
  {
@@ -4952,12 +5008,12 @@ function installCiScaffold(ctx) {
4952
5008
  for (const v2 of VARIANTS) {
4953
5009
  if (!v2.applies(ctx.tracks)) continue;
4954
5010
  const relTarget = `.github/workflows/${v2.target}`;
4955
- const target = join9(ctx.projectDir, relTarget);
5011
+ const target = join10(ctx.projectDir, relTarget);
4956
5012
  if (existsSync8(target)) {
4957
5013
  report.skippedExisting.push(relTarget);
4958
5014
  continue;
4959
5015
  }
4960
- copyFile(join9(ctx.harnessRoot, "templates/github-workflows", v2.source), target);
5016
+ copyFile(join10(ctx.harnessRoot, "templates/github-workflows", v2.source), target);
4961
5017
  report.written.push(relTarget);
4962
5018
  }
4963
5019
  return report;
@@ -4975,7 +5031,7 @@ import { basename, join as join14 } from "path";
4975
5031
 
4976
5032
  // src/agents-skill-targets.ts
4977
5033
  init_esm_shims();
4978
- import { join as join10, sep as sep2 } from "path";
5034
+ import { join as join11, sep as sep2 } from "path";
4979
5035
  function bundledSkillTargets(projectDir, log = readInstallLog(projectDir)) {
4980
5036
  if (log === null) return [];
4981
5037
  const tracks = log.spec.tracks.filter(
@@ -4989,7 +5045,7 @@ function bundledSkillTargets(projectDir, log = readInstallLog(projectDir)) {
4989
5045
  }
4990
5046
  function allowSkillCreation(writer, projectDir, ids) {
4991
5047
  if (ids.length === 0) return writer;
4992
- const roots = ids.map((id) => `${join10(projectDir, ".agents", "skills", id)}${sep2}`);
5048
+ const roots = ids.map((id) => `${join11(projectDir, ".agents", "skills", id)}${sep2}`);
4993
5049
  return {
4994
5050
  write(absPath, content, opts) {
4995
5051
  const inTarget = roots.some((root) => absPath.startsWith(root));
@@ -5001,7 +5057,7 @@ function allowSkillCreation(writer, projectDir, ids) {
5001
5057
  }
5002
5058
  function agentsSkillSlot(params) {
5003
5059
  const { projectDir, selectedInternalSkills, refreshOnly, writer } = params;
5004
- const creatable = refreshOnly ? bundledSkillTargets(projectDir) : [];
5060
+ const creatable = refreshOnly ? bundledSkillTargets(projectDir, params.log) : [];
5005
5061
  return {
5006
5062
  skillIds: creatable.length === 0 ? selectedInternalSkills : [.../* @__PURE__ */ new Set([...selectedInternalSkills, ...creatable])],
5007
5063
  skillWriter: allowSkillCreation(writer, projectDir, creatable)
@@ -5012,48 +5068,11 @@ function agentsSkillSlot(params) {
5012
5068
  init_esm_shims();
5013
5069
  import { existsSync as existsSync9, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
5014
5070
  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
5071
  function createOwnedWriter(projectDir, baseline, options = {}) {
5054
5072
  const { now = /* @__PURE__ */ new Date(), refreshOnly = false, journal, owned = () => false } = options;
5055
5073
  const written = /* @__PURE__ */ new Map();
5056
5074
  const backedUp = [];
5075
+ const judged = [];
5057
5076
  const backupPaths = [];
5058
5077
  const foreignOwned = [];
5059
5078
  const restored = [];
@@ -5078,14 +5097,30 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
5078
5097
  const disk = existsSync9(absPath) ? readFileSync6(absPath, "utf8") : null;
5079
5098
  if (disk === null && refreshOnly && !opts?.createInRefresh && rec.state !== "sha")
5080
5099
  return false;
5081
- const { verdict, record } = judge({ op: "write", kind: "harness", rec, disk, next: content });
5100
+ const { verdict, record, line } = judge({
5101
+ op: "write",
5102
+ kind: "harness",
5103
+ rec,
5104
+ disk,
5105
+ next: content
5106
+ });
5082
5107
  let backupRel = null;
5083
5108
  if (verdict === "backup+overwrite" || verdict === "overwrite" && opts?.backupFirst === true) {
5084
5109
  const backup = backupFile(absPath, now);
5085
5110
  backupPaths.push(backup);
5086
5111
  journal?.backup(backup);
5087
5112
  backupRel = relative2(projectDir, backup).split(sep3).join("/");
5088
- if (verdict === "backup+overwrite" && rec.state !== "none") backedUp.push(rel);
5113
+ if (verdict === "backup+overwrite") {
5114
+ if (rec.state !== "none") backedUp.push(rel);
5115
+ judged.push({
5116
+ path: rel,
5117
+ verdict,
5118
+ recorded: rec.state,
5119
+ line: withBackup(line, backupRel),
5120
+ backup: backupRel,
5121
+ backupAbs: backup
5122
+ });
5123
+ }
5089
5124
  }
5090
5125
  if (record === "displaced" || record === "displaced+sha") {
5091
5126
  displace({ path: rel, change: "displaced", notes: backupRel === null ? [] : [backupRel] });
@@ -5111,6 +5146,7 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
5111
5146
  outside: outside.list(),
5112
5147
  files: [...written].map(([path, sha256]) => ({ path, sha256 })),
5113
5148
  backedUp: [...backedUp],
5149
+ judged: [...judged],
5114
5150
  backupPaths: [...backupPaths],
5115
5151
  updated,
5116
5152
  restored: [...restored],
@@ -5451,7 +5487,8 @@ function runAgentsMdTransform(params) {
5451
5487
  projectDir,
5452
5488
  selectedInternalSkills: params.selectedInternalSkills,
5453
5489
  refreshOnly,
5454
- writer
5490
+ writer,
5491
+ ...params.log === void 0 ? {} : { log: params.log }
5455
5492
  });
5456
5493
  const creates = !exists2 && (!refreshOnly || params.installedByRecord === true);
5457
5494
  const legacy = params.legacyContext ?? null;
@@ -6240,6 +6277,7 @@ function runCliTransforms(params) {
6240
6277
  const externalFiles = [];
6241
6278
  const externalBackups = [];
6242
6279
  const externalBackedUp = [];
6280
+ const externalJudged = [];
6243
6281
  const externalForeignOwned = [];
6244
6282
  const displaced = /* @__PURE__ */ new Map();
6245
6283
  let externalOutside = [];
@@ -6261,6 +6299,7 @@ function runCliTransforms(params) {
6261
6299
  }
6262
6300
  externalBackups.push(...report.ownership.backupPaths);
6263
6301
  externalBackedUp.push(...report.ownership.backedUp);
6302
+ externalJudged.push(...report.ownership.judged ?? []);
6264
6303
  for (const f of report.ownership.displaced ?? [])
6265
6304
  if (!displaced.has(f.path)) displaced.set(f.path, f);
6266
6305
  for (const f of report.ownership.foreignOwned) {
@@ -6368,6 +6407,7 @@ function runCliTransforms(params) {
6368
6407
  externalBackups,
6369
6408
  externalUpdated,
6370
6409
  externalBackedUp,
6410
+ externalJudged,
6371
6411
  displaced: [...displaced.values()],
6372
6412
  externalForeignOwned,
6373
6413
  externalOutside,
@@ -6690,14 +6730,15 @@ function refreshExternalSkills(projectDir, deps = {}) {
6690
6730
  }
6691
6731
  throw e2;
6692
6732
  }
6693
- const { report, displaced } = settle(ran);
6733
+ const { report, displaced, judged } = settle(ran);
6694
6734
  recordRefreshedFiles(projectDir, log, report, displaced, deps.writeLog ?? writeInstallLog);
6695
6735
  return {
6696
6736
  attempted: report.attempted.length,
6697
6737
  refreshed: report.succeeded,
6698
6738
  failed: report.attempted.filter((r) => !r.ok).map((r) => ({ id: r.asset.id, message: r.message ?? "failed" })),
6699
6739
  notInCatalog,
6700
- unknown: false
6740
+ unknown: false,
6741
+ judged
6701
6742
  };
6702
6743
  }
6703
6744
  function holdSkillSlots(projectDir, hold) {
@@ -6730,7 +6771,11 @@ function holdSkillSlots(projectDir, hold) {
6730
6771
  hold.clis,
6731
6772
  { log: hold.log, scope: hold.scope }
6732
6773
  );
6733
- const out = { backups: [], displaced: [] };
6774
+ const out = {
6775
+ backups: [],
6776
+ displaced: [],
6777
+ judged: []
6778
+ };
6734
6779
  const back = /* @__PURE__ */ new Set();
6735
6780
  const failed = [];
6736
6781
  for (const f of moved) {
@@ -6743,6 +6788,16 @@ function holdSkillSlots(projectDir, hold) {
6743
6788
  }
6744
6789
  out.backups.push(r.backup);
6745
6790
  if (r.displaced !== null) out.displaced.push(r.displaced);
6791
+ const backup = toProjectRel(projectDir, r.backup);
6792
+ const line = !f.recorded ? LINES.hadFile : r.gone ? LINES.editedRemoved : LINES.edited;
6793
+ out.judged.push({
6794
+ path: f.rel,
6795
+ verdict: r.gone ? "backup+remove" : "backup+overwrite",
6796
+ recorded: f.recorded ? "sha" : "none",
6797
+ line: withBackup(line, backup),
6798
+ backup,
6799
+ backupAbs: r.backup
6800
+ });
6746
6801
  } catch (e2) {
6747
6802
  failed.push(`${f.rel} (${e2 instanceof Error ? e2.message : String(e2)})`);
6748
6803
  }
@@ -6820,9 +6875,10 @@ function settleAside(projectDir, f, restore) {
6820
6875
  }
6821
6876
  const backup = backupFromCopy(f.abs, f.aside);
6822
6877
  rmSync3(f.aside, { force: true });
6823
- if (f.recorded || now === null) return { backup, displaced: null };
6878
+ if (f.recorded || now === null) return { backup, gone: now === null, displaced: null };
6824
6879
  return {
6825
6880
  backup,
6881
+ gone: false,
6826
6882
  displaced: { path: f.rel, change: "displaced", notes: [toProjectRel(projectDir, backup)] }
6827
6883
  };
6828
6884
  }
@@ -7074,6 +7130,8 @@ function refreshLinkedSkillBodies(params) {
7074
7130
  notOurs: ids.filter((id) => !baseline.has(bodyKey(id))),
7075
7131
  files: result.files,
7076
7132
  backupPaths: result.backupPaths,
7133
+ backedUp: result.backedUp,
7134
+ judged: result.judged ?? [],
7077
7135
  displaced: result.displaced ?? [],
7078
7136
  foreignOwned: result.foreignOwned,
7079
7137
  outside: result.outside ?? []
@@ -7239,7 +7297,7 @@ import {
7239
7297
  unlinkSync as unlinkSync2,
7240
7298
  writeFileSync as writeFileSync7
7241
7299
  } from "fs";
7242
- import { basename as basename3, dirname as dirname7, join as join30 } from "path";
7300
+ import { basename as basename3, dirname as dirname7, join as join30, relative as relative5, sep as sep6 } from "path";
7243
7301
  var LEGACY_ANCHOR_FILE2 = ".claude/CLAUDE.md";
7244
7302
  function buildUpdateSpec(projectDir, tracks, only) {
7245
7303
  const log = readInstallLog(projectDir);
@@ -7301,11 +7359,12 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
7301
7359
  const logAtStart = readInstallLog(projectDir);
7302
7360
  const claudeManaged = logAtStart === null || installedClis(logAtStart).includes("claude");
7303
7361
  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 });
7362
+ const harness = logAtStart === null ? { displaced: [], restored: [], foreignOwned: [], outside: [], judged: [] } : updateHarnessFiles({ projectDir, templatesDir, log: logAtStart, wants, outside, report });
7305
7363
  const claudeDisplaced = harness.displaced;
7364
+ const judged = [...harness.judged];
7306
7365
  const prunedRestored = harness.restored;
7307
7366
  if (existsSync21(claudeDir) && claudeManaged) {
7308
- if (wants("anchor")) syncHarnessAnchor(projectDir, templatesDir, report, outside);
7367
+ if (wants("anchor")) syncHarnessAnchor(projectDir, templatesDir, report, outside, judged);
7309
7368
  }
7310
7369
  const harnessMcp = renderHarnessMcp(harnessRoot, {
7311
7370
  spec: { tracks: installedTracks(projectDir), options: DEFAULT_OPTIONS },
@@ -7339,9 +7398,16 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
7339
7398
  if (wants("hooks")) report.mcpAllowlistRetired = retireMcpAllowlist(projectDir);
7340
7399
  report.retiredAgents = staleAgentFiles(claudeDir);
7341
7400
  report.demotedAgents = demotedAgentFiles(claudeDir, installedTracks(projectDir));
7342
- const external = wants("external") ? refreshExternalCli(projectDir, harnessRoot, legacyDroppedKeys(logAtStart), harnessMcp) : {
7401
+ const external = wants("external") ? refreshExternalCli(
7402
+ projectDir,
7403
+ harnessRoot,
7404
+ legacyDroppedKeys(logAtStart),
7405
+ harnessMcp,
7406
+ report.skillsSkippedLinks.filter((id) => linksToProjectSharedSkill(projectDir, id))
7407
+ ) : {
7343
7408
  externalUpdated: 0,
7344
7409
  externalBackedUp: [],
7410
+ externalJudged: [],
7345
7411
  displaced: [],
7346
7412
  externalForeignOwned: [],
7347
7413
  externalOutside: [],
@@ -7452,10 +7518,17 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
7452
7518
  const displacedSame = displacedAll.filter((f) => f.notes.length === 0).map((f) => f.path);
7453
7519
  if (displacedMoved.length > 0) report.displaced = displacedMoved;
7454
7520
  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));
7521
+ const deadMcp = claudeManaged ? deadMcpLeft(projectDir) : [];
7522
+ if (deadMcp.length > 0) report.deadMcp = deadMcp;
7523
+ judged.push(...external.externalJudged, ...skillRefresh.judged ?? []);
7524
+ if (judged.length > 0) report.judged = judged;
7525
+ const unmeasured = new Set(
7526
+ judged.filter((j4) => j4.recorded === "no-sha" && j4.backup !== void 0).map((j4) => j4.path)
7527
+ );
7528
+ report.backups = collectRunBackups(projectDir, startedAt, backupsAtStart).map(
7529
+ (b2) => existsSync21(join30(projectDir, b2.path)) ? b2 : { ...b2, removed: true }
7530
+ );
7531
+ const noChecksum = report.backups.map((b2) => b2.path).filter((p2) => unmeasured.has(p2));
7459
7532
  if (noChecksum.length > 0) report.noChecksum = noChecksum;
7460
7533
  writeBackupList(projectDir, report.backups, new Set(noChecksum));
7461
7534
  return report;
@@ -7533,19 +7606,16 @@ function reclaimOutOfTrackRecorded(projectDir, outside) {
7533
7606
  }
7534
7607
  return { reclaim: result, displaced };
7535
7608
  }
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);
7609
+ function deadMcpLeft(projectDir) {
7610
+ let servers;
7611
+ try {
7612
+ servers = JSON.parse(readFileSync22(join30(projectDir, ".mcp.json"), "utf8")).mcpServers;
7613
+ } catch {
7614
+ return [];
7615
+ }
7616
+ return PRIOR_MCP_SERVERS.flatMap(
7617
+ (p2) => p2.dead !== void 0 && servers?.[p2.name] !== void 0 && jsonSha(servers[p2.name]) === jsonSha(toClaudeServer(p2.value)) ? [{ name: p2.name, note: p2.dead }] : []
7618
+ );
7549
7619
  }
7550
7620
  function retiredFeaturesPart(projectDir, f) {
7551
7621
  if (f.path !== ".codex/config.toml") return {};
@@ -7739,6 +7809,7 @@ function updateHarnessTargets(projectDir, templatesDir, log, wants, report, fore
7739
7809
  }
7740
7810
  function updateHarnessFiles(args) {
7741
7811
  const { projectDir, templatesDir, log, wants, outside, report } = args;
7812
+ const judged = [];
7742
7813
  const now = /* @__PURE__ */ new Date();
7743
7814
  const claude = installedClis(log).includes("claude");
7744
7815
  const claims = retiredClaims(log, projectDir, log.policyFiles ?? []);
@@ -7759,7 +7830,7 @@ function updateHarnessFiles(args) {
7759
7830
  if (!claude || !wants(dir === "hooks" ? "hooks" : "rules")) continue;
7760
7831
  const label = `.claude/${dir}`;
7761
7832
  report.updated[label] = 0;
7762
- const ctx = { prefix: dir, outside, recordOf };
7833
+ const ctx = { prefix: dir, outside, recordOf, projectDir, judged };
7763
7834
  report.pruned[label] = pruneOrphans(
7764
7835
  join30(projectDir, label),
7765
7836
  join30(templatesDir, dir),
@@ -7776,7 +7847,8 @@ function updateHarnessFiles(args) {
7776
7847
  targets.refreshedSkills,
7777
7848
  outside,
7778
7849
  recordOf,
7779
- now
7850
+ now,
7851
+ judged
7780
7852
  );
7781
7853
  report.skillsPruned = skillPrune.pruned;
7782
7854
  report.skillsBackedUp.push(...skillPrune.backedUp);
@@ -7785,6 +7857,11 @@ function updateHarnessFiles(args) {
7785
7857
  write: true
7786
7858
  });
7787
7859
  const ledger = writer.ledger();
7860
+ judged.unshift(
7861
+ ...ledger.writes.flatMap(
7862
+ ({ path, verdict, recorded: recorded2, line, backup, backupAbs }) => backup === void 0 || backupAbs === void 0 ? [] : [{ path, verdict, recorded: recorded2, line, backup, backupAbs }]
7863
+ )
7864
+ );
7788
7865
  recordHarnessWrites(projectDir, ledger, {
7789
7866
  claims,
7790
7867
  prunedPolicy,
@@ -7815,10 +7892,25 @@ function updateHarnessFiles(args) {
7815
7892
  displaced: ledger.rootFiles.filter((f) => f.change === "displaced"),
7816
7893
  restored,
7817
7894
  foreignOwned,
7818
- outside: ledger.outside
7895
+ outside: ledger.outside,
7896
+ judged
7897
+ };
7898
+ }
7899
+ function removedRow(projectDir, path, why, judged, backupAbs) {
7900
+ const { line, recorded: recorded2 } = judged;
7901
+ if (backupAbs === null) return { path, verdict: "remove", recorded: recorded2, line: `${why} \u2014 ${line}` };
7902
+ const backup = relative5(projectDir, backupAbs).split(sep6).join("/");
7903
+ return {
7904
+ path,
7905
+ verdict: "backup+remove",
7906
+ recorded: recorded2,
7907
+ line: `${why} \u2014 ${withBackup(line, backup)}`,
7908
+ backup,
7909
+ backupAbs
7819
7910
  };
7820
7911
  }
7821
- function pruneSkillExtras(projectDir, templatesDir, ids, outside, recordOf, now) {
7912
+ var NOT_RECORDED_REMOVED = "not on the install record \u2014 saved as <file>.backup-<time>, removed from live";
7913
+ function pruneSkillExtras(projectDir, templatesDir, ids, outside, recordOf, now, judged) {
7822
7914
  const pruned = [];
7823
7915
  const backedUp = [];
7824
7916
  for (const id of ids) {
@@ -7829,11 +7921,18 @@ function pruneSkillExtras(projectDir, templatesDir, ids, outside, recordOf, now)
7829
7921
  if (foreignOwnedTarget(projectDir, `.claude/skills/${id}/${rel}`) !== null) continue;
7830
7922
  const targetFile = join30(targetSkill, rel);
7831
7923
  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);
7924
+ const path = `.claude/skills/${id}/${rel}`;
7925
+ const rec = recordOf(path);
7926
+ const disk = rec.state === "sha" ? readFileSync22(targetFile, "utf8") : "";
7927
+ const line = rec.state === "none" ? NOT_RECORDED_REMOVED : judge({ op: "remove", kind: "harness", rec, disk, next: null }).line;
7928
+ let backupAbs = null;
7929
+ if (rec.state !== "sha" || hashContent(disk) !== rec.sha256) {
7930
+ backupAbs = backupFile(targetFile, now);
7835
7931
  backedUp.push(`${id}/${rel}`);
7836
7932
  }
7933
+ judged.push(
7934
+ removedRow(projectDir, path, "not in bundle", { line, recorded: rec.state }, backupAbs)
7935
+ );
7837
7936
  unlinkSync2(targetFile);
7838
7937
  pruned.push(`${id}/${rel}`);
7839
7938
  for (let d2 = dirname7(targetFile); d2 !== targetSkill && readdirSync4(d2).length === 0; d2 = dirname7(d2))
@@ -7908,7 +8007,7 @@ function writeBackupList(projectDir, backups, noChecksum = /* @__PURE__ */ new S
7908
8007
  `${JSON.stringify(
7909
8008
  {
7910
8009
  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.",
8010
+ 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
8011
  backups: backups.map((b2) => noChecksum.has(b2.path) ? { ...b2, noChecksum: true } : b2)
7913
8012
  },
7914
8013
  null,
@@ -7927,7 +8026,7 @@ function trackOnlyFileAssets(tracks) {
7927
8026
  (e2) => e2.type === "file" && e2.applies(optedOut) && e2.applies(optedIn)
7928
8027
  );
7929
8028
  }
7930
- function syncHarnessAnchor(projectDir, templatesDir, report, outside, now = /* @__PURE__ */ new Date()) {
8029
+ function syncHarnessAnchor(projectDir, templatesDir, report, outside, judged, now = /* @__PURE__ */ new Date()) {
7931
8030
  if (existsSync21(join30(projectDir, LEGACY_ANCHOR_FILE2))) {
7932
8031
  report.legacyAnchor = LEGACY_ANCHOR_FILE2;
7933
8032
  }
@@ -7942,8 +8041,20 @@ function syncHarnessAnchor(projectDir, templatesDir, report, outside, now = /* @
7942
8041
  const recorded2 = readInstallLog(projectDir)?.templates.rootClaudeMd;
7943
8042
  const owned = recorded2?.path === HARNESS_ANCHOR_FILE && hashContent(current) === recorded2.sha256;
7944
8043
  if (!owned) {
7945
- backupFile(anchor, now);
8044
+ const backup = backupFile(anchor, now);
7946
8045
  report.anchorBackedUp = true;
8046
+ const rel = relative5(projectDir, backup).split(sep6).join("/");
8047
+ const rec = recorded2?.path === HARNESS_ANCHOR_FILE ? { state: "sha", sha256: recorded2.sha256 } : { state: "no-sha" };
8048
+ const next = readFileSync22(templateMd, "utf8");
8049
+ const { line } = judge({ op: "write", kind: "harness", rec, disk: current, next });
8050
+ judged.push({
8051
+ path: HARNESS_ANCHOR_FILE,
8052
+ verdict: "backup+overwrite",
8053
+ recorded: rec.state,
8054
+ line: withBackup(line, rel),
8055
+ backup: rel,
8056
+ backupAbs: backup
8057
+ });
7947
8058
  }
7948
8059
  copyFileSync(templateMd, anchor);
7949
8060
  report.claudeMdUpdated = true;
@@ -8010,7 +8121,7 @@ function excludedSkillOf(log) {
8010
8121
  const excluded = excludedIds(log);
8011
8122
  return (id) => excluded.has(id) || isBaselineExcluded(`.claude/skills/${id}`, excluded);
8012
8123
  }
8013
- function recordedRenderInputs(projectDir, log) {
8124
+ function recordedRenderInputs(projectDir, log, tracks = installedTracks(projectDir)) {
8014
8125
  const excluded = excludedIds(log);
8015
8126
  return {
8016
8127
  // 스킬은 **기록 + 이번 릴리즈 기본**만 넘긴다(#761 — `recordedBundledSkills`). 스킬 *파일*은 전체 목록을 넘겨도
@@ -8023,7 +8134,7 @@ function recordedRenderInputs(projectDir, log) {
8023
8134
  ),
8024
8135
  // #601 — install 과 같은 SSOT(resolveRules)를 설치된 트랙으로. 렌더 경로(AGENTS.md 절 · antigravity 룰 생성)는
8025
8136
  // 디스크 게이트가 없어, data 트랙 설치에서 update 만으로 cli-development(제6룰)가 생겼다.
8026
- rules: resolveRules({ tracks: installedTracks(projectDir) }).filter(
8137
+ rules: resolveRules({ tracks }).filter(
8027
8138
  (r) => !isBaselineExcluded(`.claude/rules/${r}.md`, excluded)
8028
8139
  )
8029
8140
  };
@@ -8107,7 +8218,7 @@ function changedRemoteMcp(writes) {
8107
8218
  const changed = /* @__PURE__ */ new Set([...mcp.added, ...mcp.replaced ?? []]);
8108
8219
  return (mcp.remote ?? []).filter((r) => changed.has(r.name));
8109
8220
  }
8110
- function refreshExternalCli(projectDir, harnessRoot, droppedKeys, harnessMcp) {
8221
+ function refreshExternalCli(projectDir, harnessRoot, droppedKeys, harnessMcp, linkedIds) {
8111
8222
  const log = readInstallLog(projectDir);
8112
8223
  const presentBefore = sharedSkillIdsOnDisk(projectDir);
8113
8224
  const rulesBefore = /* @__PURE__ */ new Set();
@@ -8139,11 +8250,23 @@ function refreshExternalCli(projectDir, harnessRoot, droppedKeys, harnessMcp) {
8139
8250
  // 앞 단계(하네스 파일 쓰기)가 헬퍼만 든 `externalFiles` 를 적어도 같은 답이다 — 헬퍼는 외부 자리로 세지 않는다(B-2)
8140
8251
  owned: recordedOwner(log)
8141
8252
  });
8253
+ const linked = refreshLinkedSkillBodies({
8254
+ harnessRoot,
8255
+ projectDir,
8256
+ ids: linkedIds.filter(
8257
+ (id) => !result.externalFiles.some((f) => f.path === `${SHARED_SKILLS_DIR}/${id}/SKILL.md`)
8258
+ ),
8259
+ baseline: new Map(
8260
+ mergeExternalFiles(log?.externalFiles, result.externalFiles).map((f) => [f.path, f.sha256])
8261
+ )
8262
+ });
8263
+ const written = [...result.externalFiles, ...linked.files];
8264
+ const displaced = [...result.displaced ?? [], ...linked.displaced];
8142
8265
  const anchorPath = result.anchorRetired?.action === "removed" ? result.anchorRetired.path : null;
8143
8266
  const anchorDisplaced = log === null || anchorPath === null ? [] : restoreDisplaced(log, projectDir, { has: (p2) => p2 === anchorPath, write: true });
8144
8267
  if (log) {
8145
8268
  const retiredPath = result.anchorRetired?.forget === true ? result.anchorRetired.path : null;
8146
- const merged = mergeExternalFiles(log.externalFiles, result.externalFiles).filter(
8269
+ const merged = mergeExternalFiles(log.externalFiles, written).filter(
8147
8270
  (f) => f.path !== retiredPath
8148
8271
  );
8149
8272
  const next = retiredPath === null ? { ...log } : { ...log, spec: { ...log.spec, clis: [...installedClis(log)] } };
@@ -8157,7 +8280,7 @@ function refreshExternalCli(projectDir, harnessRoot, droppedKeys, harnessMcp) {
8157
8280
  if (portions.length > 0) next.portions = portions;
8158
8281
  else delete next.portions;
8159
8282
  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 ?? [])];
8283
+ const roots = [...created, ...withoutLostBackups(log.rootFiles, displaced)];
8161
8284
  if (roots.length > 0) next.rootFiles = mergeRootFiles(log.rootFiles, roots);
8162
8285
  if (anchorDisplaced.some((d2) => d2.forget))
8163
8286
  next.rootFiles = withoutRestored(next.rootFiles, anchorDisplaced);
@@ -8216,11 +8339,12 @@ function refreshExternalCli(projectDir, harnessRoot, droppedKeys, harnessMcp) {
8216
8339
  const dropped = new Set(droppedKeys);
8217
8340
  return {
8218
8341
  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),
8342
+ externalBackedUp: [...result.externalBackedUp, ...linked.backedUp],
8343
+ externalJudged: [...result.externalJudged, ...linked.judged],
8344
+ displaced,
8345
+ externalForeignOwned: [...result.externalForeignOwned, ...linked.foreignOwned],
8346
+ externalOutside: [...result.externalOutside, ...linked.outside],
8347
+ written: written.map((f) => f.path),
8224
8348
  skillsInstalled,
8225
8349
  skillsRestored,
8226
8350
  rulesRestored,
@@ -8275,14 +8399,24 @@ function pruneOrphans(target, source, ext, ctx, now) {
8275
8399
  for (const file of readdirSync4(target)) {
8276
8400
  if (!file.endsWith(ext) || existsSync21(join30(source, file))) continue;
8277
8401
  const targetFile = join30(target, file);
8278
- const rec = ctx.recordOf(`.claude/${ctx.prefix}/${file}`);
8402
+ const path = `.claude/${ctx.prefix}/${file}`;
8403
+ const rec = ctx.recordOf(path);
8279
8404
  try {
8280
8405
  const disk = readFileSync22(targetFile, "utf8");
8281
- const { verdict } = judge({ op: "remove", kind: "harness", rec, disk, next: null });
8406
+ const { verdict, line } = judge({ op: "remove", kind: "harness", rec, disk, next: null });
8282
8407
  if (verdict === "leave" || ctx.outside.skip(targetFile)) continue;
8283
- if (verdict === "backup+remove") backupFile(targetFile, now);
8408
+ const backupAbs = verdict === "backup+remove" ? backupFile(targetFile, now) : null;
8284
8409
  unlinkSync2(targetFile);
8285
8410
  removed.push(file);
8411
+ ctx.judged.push(
8412
+ removedRow(
8413
+ ctx.projectDir,
8414
+ path,
8415
+ "retired from the harness",
8416
+ { line, recorded: rec.state },
8417
+ backupAbs
8418
+ )
8419
+ );
8286
8420
  } catch {
8287
8421
  }
8288
8422
  }
@@ -8346,7 +8480,7 @@ function runInstall(ctx) {
8346
8480
  JSON.parse(readFileSync23(settingsPath, "utf8"));
8347
8481
  } catch {
8348
8482
  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`
8483
+ `${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
8484
  );
8351
8485
  }
8352
8486
  }
@@ -8415,6 +8549,7 @@ function runInstallStages(ctx, args) {
8415
8549
  externalBackups,
8416
8550
  externalUpdated: _externalUpdated,
8417
8551
  externalBackedUp: _externalBackedUp,
8552
+ externalJudged: _externalJudged,
8418
8553
  displaced: cliDisplaced = [],
8419
8554
  externalForeignOwned,
8420
8555
  externalOutside,
@@ -8967,7 +9102,7 @@ function recordInterruptedInstall(ctx, error, run) {
8967
9102
  const codexTrust = registeredTrust(journal.codexOptIn, ctx.projectDir);
8968
9103
  const written = writtenSoFar(ledger, cliFiles, rootFiles, stage.rootImportWritten);
8969
9104
  if (codexTrust) written.push(`${codexTrust.configPath} (Codex trust entry for this folder)`);
8970
- const projectRel = (abs) => relative5(ctx.projectDir, abs).split(sep6).join("/");
9105
+ const projectRel = (abs) => relative6(ctx.projectDir, abs).split(sep7).join("/");
8971
9106
  const writtenSet = new Set(written);
8972
9107
  const backups = [...own.backups, ...journal.backups].map((abs) => {
8973
9108
  const original = projectRel(backupOriginal(abs) ?? abs);
@@ -9144,7 +9279,7 @@ function listHookFiles(hookDir) {
9144
9279
  init_esm_shims();
9145
9280
  import { homedir as homedir3 } from "os";
9146
9281
  import { existsSync as existsSync24, readdirSync as readdirSync6, readFileSync as readFileSync25 } from "fs";
9147
- import { join as join33, relative as relative6 } from "path";
9282
+ import { join as join33, relative as relative7 } from "path";
9148
9283
 
9149
9284
  // src/preset-recommend.ts
9150
9285
  init_esm_shims();
@@ -9372,9 +9507,11 @@ function renderUpdateSummary(log, spec, report) {
9372
9507
  });
9373
9508
  const backups = report.updateMode?.backups ?? [];
9374
9509
  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;
9510
+ const replaced = backups.filter((b2) => b2.removed !== true);
9511
+ const removedBefore = backups.length - replaced.length;
9512
+ const noChecksum = new Set(report.updateMode?.noChecksum ?? []);
9513
+ const unmeasured = replaced.filter((b2) => noChecksum.has(b2.path)).length;
9514
+ 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
9515
  const how = removedBefore === 0 ? "saved before replacing" : removedBefore === backups.length ? "saved before removing" : `saved before replacing (${removedBefore} before removing)`;
9379
9516
  log(
9380
9517
  infoRow(
@@ -9382,12 +9519,13 @@ function renderUpdateSummary(log, spec, report) {
9382
9519
  `${backups.length} file(s) ${how}, as *.backup-<time>${unmeasuredPart} \xB7 list: .uzys-agent-harness/update-backups.json`
9383
9520
  )
9384
9521
  );
9385
- const displacedN = report.updateMode?.displaced?.length ?? 0;
9386
- if (removedBefore + displacedN < backups.length)
9522
+ const displaced = new Set(report.updateMode?.displaced ?? []);
9523
+ const carry = replaced.filter((b2) => !displaced.has(b2.path)).length;
9524
+ if (carry > 0)
9387
9525
  log(
9388
9526
  infoRow(
9389
9527
  "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)'
9528
+ 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
9529
  )
9392
9530
  );
9393
9531
  }
@@ -9779,25 +9917,8 @@ function linkedSkillRows(log, updated, notOurs) {
9779
9917
  );
9780
9918
  }
9781
9919
  }
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
9920
  function renderPhase1Rows(log, baseline, verbose = false) {
9793
9921
  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
9922
  for (const [dir, count] of Object.entries(baseline.updateMode.updated)) {
9802
9923
  if (count > 0) log(assetRow("success", dir, `${count} files updated`));
9803
9924
  }
@@ -9854,6 +9975,8 @@ function renderPhase1Rows(log, baseline, verbose = false) {
9854
9975
  const row = sharedFileRow(f);
9855
9976
  if (row !== null) log(row);
9856
9977
  }
9978
+ for (const d2 of baseline.updateMode.deadMcp ?? [])
9979
+ log(assetRow("skip", ".mcp.json", `${d2.name} \u2014 ${d2.note}`));
9857
9980
  for (const f of baseline.updateMode.sharedLeft ?? []) {
9858
9981
  log(sharedLeftRow(f));
9859
9982
  if (f.retiredFeatures !== void 0) log(retiredFeaturesRow(f.path, f.retiredFeatures));
@@ -9867,11 +9990,7 @@ function renderPhase1Rows(log, baseline, verbose = false) {
9867
9990
  if (legacy2 !== null) log(legacy2);
9868
9991
  for (const row of legacyReleasedCatalogRows(baseline.updateMode.legacyReleasedCatalog ?? []))
9869
9992
  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
- }
9993
+ for (const j4 of baseline.updateMode.judged ?? []) log(judgedRow(j4));
9875
9994
  if (baseline.updateMode.skippedGroups.length > 0) {
9876
9995
  log(
9877
9996
  assetRow(
@@ -9884,15 +10003,6 @@ function renderPhase1Rows(log, baseline, verbose = false) {
9884
10003
  if (baseline.updateMode.claudeMdUpdated) {
9885
10004
  log(assetRow("success", HARNESS_ANCHOR_FILE, "refreshed from template"));
9886
10005
  }
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
10006
  if (baseline.updateMode.anchorCreated) {
9897
10007
  log(assetRow("success", HARNESS_ANCHOR_FILE, "created from template (anchor migration)"));
9898
10008
  }
@@ -9920,28 +10030,6 @@ function renderPhase1Rows(log, baseline, verbose = false) {
9920
10030
  if (baseline.updateMode.legacyAnchor) {
9921
10031
  log(assetRow("skip", baseline.updateMode.legacyAnchor, LEGACY_ANCHOR_NOTE));
9922
10032
  }
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
10033
  linkedSkillRows(
9946
10034
  log,
9947
10035
  baseline.updateMode.skillsLinked ?? [],
@@ -9995,29 +10083,6 @@ function renderPhase1Rows(log, baseline, verbose = false) {
9995
10083
  )
9996
10084
  );
9997
10085
  }
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
10086
  const retiredRefs = baseline.updateMode.retiredHookRefsRemoved ?? [];
10022
10087
  if (retiredRefs.length > 0) {
10023
10088
  log(
@@ -10067,17 +10132,6 @@ function renderPhase1Rows(log, baseline, verbose = false) {
10067
10132
  )
10068
10133
  );
10069
10134
  }
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
10135
  const displacedSame = baseline.updateMode.displacedSame ?? [];
10082
10136
  if (displacedSame.length > 0) {
10083
10137
  log(
@@ -10538,7 +10592,7 @@ function renderInterruptedInstall(err, e2, cwd = process.cwd()) {
10538
10592
  }
10539
10593
  const { path, error } = e2.record;
10540
10594
  err(
10541
- path !== null ? ` Already in place before it stopped \u2014 recorded in ${relative6(cwd, path) || path}:` : " Already in place before it stopped:"
10595
+ path !== null ? ` Already in place before it stopped \u2014 recorded in ${relative7(cwd, path) || path}:` : " Already in place before it stopped:"
10542
10596
  );
10543
10597
  for (const [key, n] of groups) {
10544
10598
  err(` ${key.endsWith("/") ? `${key} (${n} file${n === 1 ? "" : "s"})` : key}`);
@@ -11011,7 +11065,56 @@ import {
11011
11065
  statSync as statSync5,
11012
11066
  writeFileSync as writeFileSync9
11013
11067
  } from "fs";
11014
- import { dirname as dirname8, join as join35, relative as relative7, resolve as resolve4, sep as sep7 } from "path";
11068
+ import { dirname as dirname8, join as join35, relative as relative8, resolve as resolve4, sep as sep8 } from "path";
11069
+
11070
+ // src/track-removal.ts
11071
+ init_esm_shims();
11072
+ function trackRemoval(log, target) {
11073
+ const known = log.spec.tracks.filter(isTrack);
11074
+ const remaining = known.filter((t) => t !== target);
11075
+ const stays = claudeTargets(remaining);
11076
+ const only = [...claudeTargets(known)].filter((p2) => !stays.has(p2));
11077
+ const has = (path) => only.some((t) => claudeSpot(path) === t || claudeSpot(path).startsWith(`${t}/`));
11078
+ return { target, known, remaining, has };
11079
+ }
11080
+ function claudeSpot(path) {
11081
+ return bundlePath(path).replace(/^\.agents\/rules\//, ".claude/rules/");
11082
+ }
11083
+ function trackRemovalNames(removed, target) {
11084
+ const groups = /* @__PURE__ */ new Map();
11085
+ for (const p2 of removed) {
11086
+ const m = /^\.claude\/([^/]+)\/([^/]+?)(?:\.md)?(?:\/|$)/.exec(claudeSpot(p2));
11087
+ const [kind, name] = m ? [m[1] ?? "", m[2] ?? ""] : ["files", p2];
11088
+ groups.set(kind, (groups.get(kind) ?? /* @__PURE__ */ new Set()).add(name));
11089
+ }
11090
+ const names = [...groups].sort(([a], [b2]) => a.localeCompare(b2)).map(([kind, names2]) => `${kind} ${[...names2].sort().join(", ")}`);
11091
+ return `removes: ${names.join(" \xB7 ") || `nothing only ${target} installs`}`;
11092
+ }
11093
+ function trackLeftLines(log, r, harnessRoot) {
11094
+ const recorded2 = new Set(log.assets.map((a) => a.id));
11095
+ const def = (a, tracks) => shouldInstallAsset(a, { tracks, options: DEFAULT_OPTIONS });
11096
+ const mine = (a) => def(a, r.known) && !def(a, r.remaining);
11097
+ const lines2 = EXTERNAL_ASSETS.filter(
11098
+ (a) => a.method.kind !== "internal" && recorded2.has(a.id) && mine(a)
11099
+ ).map(
11100
+ (a) => `asset ${a.id} (a default of ${r.target}) \u2014 remove with: agent-harness uninstall --only ${a.id}`
11101
+ );
11102
+ const keys = (log.portions ?? []).filter((p2) => p2.path === ".mcp.json").map((p2) => p2.key);
11103
+ const staysMcp = new Set(trackDefaultMcpAssetIds(harnessRoot, r.remaining));
11104
+ const mcp = trackDefaultMcpAssetIds(harnessRoot, r.known).filter(
11105
+ (id) => !staysMcp.has(id) && keys.includes(`mcpServers.${id}`)
11106
+ );
11107
+ if (mcp.length === 0) return lines2;
11108
+ const flags = [
11109
+ r.remaining.map((t) => `--track ${t}`),
11110
+ installedClis(log).map((c3) => `--cli ${c3}`)
11111
+ ];
11112
+ const command = `agent-harness install ${[...flags.flat(), ...mcp.map((id) => `--without ${id}`)].join(" ")}`;
11113
+ return [
11114
+ ...lines2,
11115
+ `MCP servers ${mcp.join(" \xB7 ")} (defaults of ${r.target}, kept because your install record has them) \u2014 drop: ${command} (this re-runs the whole install: harness files refreshed, edited ones backed up; --without stays on record, so add --with <id> to get one back later)`
11116
+ ];
11117
+ }
11015
11118
 
11016
11119
  // src/uninstall-interactive.ts
11017
11120
  init_esm_shims();
@@ -12057,7 +12160,7 @@ function buildRouterChoices(_state, record) {
12057
12160
  {
12058
12161
  value: "uninstall",
12059
12162
  label: "Uninstall",
12060
- hint: "remove everything, one CLI, or single assets",
12163
+ hint: "remove everything, one CLI, one track, or single assets",
12061
12164
  enabled: true
12062
12165
  },
12063
12166
  {
@@ -12420,7 +12523,7 @@ function hintFor(asset) {
12420
12523
  return 'no automatic reverse path \u2014 only "Remove everything" clears it';
12421
12524
  }
12422
12525
  }
12423
- function buildUninstallModeChoices(clis, assetCount) {
12526
+ function buildUninstallModeChoices(clis, assetCount, tracks) {
12424
12527
  return [
12425
12528
  {
12426
12529
  value: "cli",
@@ -12428,6 +12531,7 @@ function buildUninstallModeChoices(clis, assetCount) {
12428
12531
  hint: clis.length > 1 ? "that CLI's own files go; files shared with a remaining CLI stay; your text stays" : 'only one CLI here \u2014 use "Remove everything"',
12429
12532
  enabled: clis.length > 1
12430
12533
  },
12534
+ ...tracks === void 0 ? [] : [trackMode(tracks.length > 1)],
12431
12535
  {
12432
12536
  value: "selected",
12433
12537
  label: "Remove selected assets",
@@ -12442,6 +12546,12 @@ function buildUninstallModeChoices(clis, assetCount) {
12442
12546
  }
12443
12547
  ];
12444
12548
  }
12549
+ var trackMode = (many) => ({
12550
+ value: "track",
12551
+ label: "Remove one track",
12552
+ hint: many ? "only that track's own files go" : 'only one track here \u2014 use "Remove everything"',
12553
+ enabled: many
12554
+ });
12445
12555
  var CLI_FOLDERS = [".claude/", ".codex/", ".opencode/"];
12446
12556
  var FOLDER_CLI = {
12447
12557
  ".claude/": "claude",
@@ -12522,7 +12632,7 @@ async function runInteractiveUninstall(projectDir, deps = {}) {
12522
12632
  const rows2 = buildRemovableRows(log.assets);
12523
12633
  const clis = installedClis(log);
12524
12634
  const mode = await prompts.selectMode(
12525
- buildUninstallModeChoices(clis, rows2.length),
12635
+ buildUninstallModeChoices(clis, rows2.length, log.spec.tracks.filter(isTrack)),
12526
12636
  `uzys-agent-harness \xB7 uninstall installed: ${clis.join(", ") || "(none)"} \xB7 assets ${rows2.length}`
12527
12637
  );
12528
12638
  if (mode === null) {
@@ -12530,6 +12640,7 @@ async function runInteractiveUninstall(projectDir, deps = {}) {
12530
12640
  return { ok: false, reason: "cancelled" };
12531
12641
  }
12532
12642
  if (mode === "cli") return pickCli(projectDir, clis, prompts);
12643
+ if (mode === "track") return pickTrack(projectDir, log, prompts);
12533
12644
  if (mode === "all") {
12534
12645
  const ok2 = await prompts.confirm(
12535
12646
  [
@@ -12592,6 +12703,26 @@ async function pickCli(projectDir, clis, prompts) {
12592
12703
  }
12593
12704
  return { ok: true, options: { projectDir, cli } };
12594
12705
  }
12706
+ async function pickTrack(projectDir, log, prompts) {
12707
+ const rows2 = log.spec.tracks.filter(isTrack).map((t) => ({ value: t, label: TRACK_LABELS[t], hint: t }));
12708
+ const track = await prompts.selectTrack(rows2);
12709
+ const left = rows2.map((r) => r.value).filter((t) => t !== track);
12710
+ const ok = track !== null && await prompts.confirm(
12711
+ [
12712
+ `Remove ${track} (remaining: ${left.join(", ")})? Same as: agent-harness uninstall --track ${track}`,
12713
+ ...previewTrackRemoval(
12714
+ { installLog: log, projectDir, harnessRoot: resolveBundleRoot() },
12715
+ track
12716
+ ).map((l) => ` \xB7 ${l}`),
12717
+ ` \xB7 file counts: agent-harness uninstall --track ${track} --dry-run`
12718
+ ].join("\n")
12719
+ );
12720
+ if (!ok) {
12721
+ prompts.cancel("Cancelled.");
12722
+ return { ok: false, reason: "cancelled" };
12723
+ }
12724
+ return { ok: true, options: { projectDir, track } };
12725
+ }
12595
12726
  function defaultUninstallPrompts() {
12596
12727
  return {
12597
12728
  intro: (m) => ge(m),
@@ -12616,6 +12747,13 @@ function defaultUninstallPrompts() {
12616
12747
  });
12617
12748
  return q(r) ? null : r;
12618
12749
  },
12750
+ selectTrack: async (rows2) => {
12751
+ const r = await Ee({
12752
+ message: 'Which track? (the last remaining track cannot be removed this way \u2014 use "Remove everything")',
12753
+ options: rows2.map((x) => ({ value: x.value, label: x.label, hint: x.hint }))
12754
+ });
12755
+ return q(r) ? null : r;
12756
+ },
12619
12757
  selectAssets: async (rows2) => {
12620
12758
  if (rows2.length === 0) return [];
12621
12759
  const r = await ve({
@@ -12688,6 +12826,13 @@ function uninstallAction(options, deps = {}) {
12688
12826
  exit(1);
12689
12827
  return;
12690
12828
  }
12829
+ if (options.track !== void 0) {
12830
+ removeTrackAction(
12831
+ { options, installLog, projectDir, harnessRoot },
12832
+ { log, err, exit, rm, writeLog }
12833
+ );
12834
+ return;
12835
+ }
12691
12836
  if (options.cli !== void 0) {
12692
12837
  removeCliAction(
12693
12838
  { options, installLog, projectDir, harnessRoot },
@@ -12940,12 +13085,7 @@ function settleLog(ctx, io) {
12940
13085
  const recordDir = join35(projectDir, INSTALL_LOG_DIR);
12941
13086
  if (outsideLinkAt(projectDir, INSTALL_LOG_DIR) !== null) io.rm(recordDir);
12942
13087
  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
- }
13088
+ for (const rel of RECORD_FILES) io.rm(join35(projectDir, rel));
12949
13089
  pruneEmptyDirsUpward(projectDir, recordDir);
12950
13090
  }
12951
13091
  if (outsideLinkAt(projectDir, LEGACY_INSTALL_LOG_DIR) === null) {
@@ -13094,50 +13234,53 @@ var TEMPLATE_DIR_FIELD = {
13094
13234
  codex: "codexDir",
13095
13235
  opencode: "opencodeDir"
13096
13236
  };
13097
- function resolveCliTarget(ctx, io) {
13098
- const { options, installLog } = ctx;
13099
- if (options.only !== void 0) {
13100
- io.err(status.failure(c.red("ERROR: --cli \uB294 --only \uC640 \uD568\uAED8 \uC4F8 \uC218 \uC5C6\uB2E4")));
13101
- io.err(
13102
- c.dim(
13103
- " CLI \uD558\uB098 \uC81C\uAC70: uninstall --cli <name> / \uC790\uC0B0 \uD558\uB098 \uC81C\uAC70: uninstall --only <id>"
13104
- )
13105
- );
13237
+ function resolveScopeTarget(io, s) {
13238
+ const reject = (msg, hint) => {
13239
+ io.err(status.failure(c.red(`ERROR: ${msg}`)));
13240
+ io.err(c.dim(` ${hint}`));
13106
13241
  io.exit(1);
13107
13242
  return null;
13243
+ };
13244
+ if (s.conflict) {
13245
+ return reject(
13246
+ `--${s.flag} \uB294 --only \xB7 --cli \xB7 --track \uC911 \uD558\uB098\uB9CC \uC4F8 \uC218 \uC788\uB2E4`,
13247
+ "CLI \uD558\uB098: uninstall --cli <name> / \uD2B8\uB799 \uD558\uB098: uninstall --track <name> / \uC790\uC0B0: uninstall --only <id>"
13248
+ );
13108
13249
  }
13109
- const target = (options.cli ?? "").trim();
13110
- if (!isCliBase(target)) {
13111
- io.err(status.failure(c.red(`ERROR: Invalid --cli value: ${target || "(empty)"}`)));
13112
- io.err(c.dim(` Must be one of: ${CLI_BASES.join(" | ")}`));
13113
- io.exit(1);
13114
- return null;
13250
+ const target = String(s.value ?? "").trim();
13251
+ if (!s.vocab.includes(target)) {
13252
+ return reject(
13253
+ `Invalid --${s.flag} value: ${target || "(empty)"}`,
13254
+ `Must be one of: ${s.vocab.join(" | ")}`
13255
+ );
13115
13256
  }
13116
- const installed = installedClis(installLog);
13117
- if (!installed.includes(target)) {
13118
- io.err(status.failure(c.red(`ERROR: ${target} is not installed in this project`)));
13119
- io.err(c.dim(` installed: ${installed.join(", ") || "(none)"}`));
13120
- io.exit(1);
13121
- return null;
13257
+ if (!s.installed.includes(target)) {
13258
+ return reject(
13259
+ `${target} is not installed in this project`,
13260
+ `installed: ${s.installed.join(", ") || "(none)"}`
13261
+ );
13122
13262
  }
13123
- if (installed.length <= 1) {
13124
- io.err(status.failure(c.red(`ERROR: ${target} \uB294 \uC774 \uD504\uB85C\uC81D\uD2B8\uC758 \uB9C8\uC9C0\uB9C9 CLI \uB2E4`)));
13125
- io.err(c.dim(" \uC804\uB7C9 \uC81C\uAC70\uB294 `agent-harness uninstall` \uC744 \uC4F4\uB2E4 (\uC790\uC0B0\xB7\uC124\uCE58 \uAE30\uB85D\uAE4C\uC9C0 \uD568\uAED8)"));
13126
- io.exit(1);
13127
- return null;
13263
+ if (s.installed.length <= 1) {
13264
+ return reject(
13265
+ `${target} \uB294 \uC774 \uD504\uB85C\uC81D\uD2B8\uC758 \uB9C8\uC9C0\uB9C9 ${s.flag === "cli" ? "CLI \uB2E4" : "\uD2B8\uB799\uC774\uB2E4"}`,
13266
+ "\uC804\uB7C9 \uC81C\uAC70\uB294 `agent-harness uninstall` \uC744 \uC4F4\uB2E4 (\uC790\uC0B0\xB7\uC124\uCE58 \uAE30\uB85D\uAE4C\uC9C0 \uD568\uAED8)"
13267
+ );
13128
13268
  }
13129
13269
  return target;
13130
13270
  }
13131
13271
  function underAny(path, owned) {
13132
13272
  return owned.some((o) => o.endsWith("/") ? path.startsWith(o) : path === o);
13133
13273
  }
13134
- function planCliRemoval(target, remaining) {
13274
+ function planCliRemoval(target, remaining, log, projectDir) {
13135
13275
  const { exclusive, sharedNowUnowned } = removableFor(target, remaining);
13136
13276
  const owned = [...exclusive, ...sharedNowUnowned];
13137
13277
  const prefixes = owned.filter((o) => o.kind === "recorded" || o.kind === "anchor").map((o) => o.path);
13138
- const has = (path) => underAny(path, prefixes);
13278
+ const used = (path) => usersOf(path, projectDir).some((cli) => remaining.includes(cli));
13279
+ const has = (path) => underAny(path, prefixes) && !used(path);
13280
+ const kept = harnessCandidates(log).filter((p2) => underAny(p2, prefixes) && used(p2)).map((p2) => p2.split("/").slice(0, 3).join("/"));
13139
13281
  return {
13140
13282
  target,
13283
+ kept: [...new Set(kept)].sort(),
13141
13284
  scope: {
13142
13285
  has,
13143
13286
  // `.claude/settings.json` 은 claude 의 `.claude/` 아래 · `.codex/config.toml` 은 codex 의 `.codex/` 아래 · `AGENTS.md` ·
@@ -13151,40 +13294,16 @@ function planCliRemoval(target, remaining) {
13151
13294
  }
13152
13295
  };
13153
13296
  }
13154
- function removeCliHeader(target, installed, remaining) {
13155
- return [
13156
- "",
13157
- c.bold(`uzys-agent-harness \xB7 uninstall --cli ${target}`),
13158
- "",
13159
- c.dim(` installed: ${installed.join(", ")}`),
13160
- c.dim(` removing: ${target}`),
13161
- c.dim(` remaining: ${remaining.join(", ")}`),
13162
- ""
13163
- ];
13164
- }
13165
- function removeCliDryRunLines(plan, ctx, remaining) {
13166
- const { installLog, projectDir, harnessRoot } = ctx;
13167
- const lines2 = [c.yellow("[DRY RUN] CLI \uC81C\uAC70 \uBBF8\uB9AC\uBCF4\uAE30 (\uC2E4\uC81C \uBCC0\uACBD \uC5C6\uC74C):"), ""];
13168
- const preview = removeHarnessFiles(installLog, projectDir, plan.scope, {
13169
- rm: () => {
13170
- },
13171
- write: false,
13172
- harnessRoot
13173
- });
13174
- lines2.push(...harnessRemovalLines(preview, installLog, projectDir, plan.scope, true));
13175
- lines2.push(...assetsLeftLines(installLog, projectDir, plan.scope));
13176
- const rerender = agentsMdRerenderSet(plan.target, remaining, installLog, projectDir);
13177
- if (rerender !== null) {
13178
- lines2.push(` \u25CB would re-render AGENTS.md for ${displayNames(rerender)} (your sections kept)`);
13179
- }
13180
- lines2.push(` \u25CB install log updated (clis: ${remaining.join(", ")})`, "");
13181
- return lines2;
13297
+ function keptForRemainingLines(plan, preview) {
13298
+ if (plan.kept.length === 0) return [];
13299
+ const what = `${plan.kept.join(" \xB7 ")} \u2014 still used by Claude Code through its .claude/skills/ link(s)`;
13300
+ return [preview ? ` \u25CB leave ${what}` : ` ${c.yellow("\u2298")} left ${what}`];
13182
13301
  }
13183
13302
  function displayNames(clis) {
13184
13303
  return clis.map((c3) => CLI_DISPLAY_NAMES[c3]).join(" \xB7 ");
13185
13304
  }
13186
13305
  function agentsMdRerenderSet(target, remaining, log, projectDir) {
13187
- if (!isAgentsMdCli(target)) return null;
13306
+ if (target !== null && !isAgentsMdCli(target)) return null;
13188
13307
  const set = agentsMdClis(remaining);
13189
13308
  if (set.length === 0) return null;
13190
13309
  const recorded2 = (log.externalFiles ?? []).some((f) => f.path === AGENTS_MD2) || (log.portions ?? []).some((p2) => p2.path === AGENTS_MD2);
@@ -13201,17 +13320,18 @@ function readTextOrEmpty(abs) {
13201
13320
  return "";
13202
13321
  }
13203
13322
  }
13204
- function rerenderAgentsMd(ctx, set) {
13205
- const { installLog, projectDir, harnessRoot } = ctx;
13323
+ function rerenderAgentsMd(ctx, set, log) {
13324
+ const { projectDir, harnessRoot } = ctx;
13206
13325
  return runAgentsMdTransform({
13207
13326
  harnessRoot,
13208
13327
  projectDir,
13209
13328
  clis: agentsMdClis(set),
13210
- ...recordedRenderInputs(projectDir, installLog),
13211
- baseline: new Map((installLog.externalFiles ?? []).map((f) => [f.path, f.sha256])),
13329
+ ...recordedRenderInputs(projectDir, log, log.spec.tracks.filter(isTrack)),
13330
+ baseline: new Map((log.externalFiles ?? []).map((f) => [f.path, f.sha256])),
13212
13331
  refreshOnly: true,
13213
13332
  installedByRecord: true,
13214
- shared: { portions: installLog.portions ?? [], excluded: [...excludedIds(installLog)] }
13333
+ shared: { portions: log.portions ?? [], excluded: [...excludedIds(log)] },
13334
+ log
13215
13335
  });
13216
13336
  }
13217
13337
  function withRerender(next, r) {
@@ -13260,65 +13380,197 @@ function settleCliLog(ctx, target, remaining, done, anchorRemoved) {
13260
13380
  delete next.policyFiles;
13261
13381
  delete next.skillFiles;
13262
13382
  }
13263
- const survivors = (installLog.externalFiles ?? []).filter((f) => !done.forgotten.has(f.path));
13264
- if (survivors.length > 0) next.externalFiles = survivors;
13265
- else delete next.externalFiles;
13266
- if (installLog.rootFiles !== void 0) {
13267
- next.rootFiles = withoutRestored(installLog.rootFiles, done.displaced);
13268
- }
13383
+ return forgetRemoved(next, done);
13384
+ }
13385
+ function forgetRemoved(next, done) {
13386
+ const out = { ...next };
13387
+ const kept = (prefix) => (f) => !done.forgotten.has(`${prefix}${f.path}`);
13388
+ if (out.policyFiles !== void 0) out.policyFiles = out.policyFiles.filter(kept(CLAUDE_DIR3));
13389
+ if (out.skillFiles !== void 0) out.skillFiles = out.skillFiles.filter(kept(SKILLS_DIR3));
13390
+ const survivors = (out.externalFiles ?? []).filter(kept(""));
13391
+ if (survivors.length > 0) out.externalFiles = survivors;
13392
+ else delete out.externalFiles;
13393
+ if (out.rootFiles !== void 0) out.rootFiles = withoutRestored(out.rootFiles, done.displaced);
13269
13394
  const touched = new Set(done.shared.map((r) => r.path));
13270
13395
  const portions = [
13271
- ...(installLog.portions ?? []).filter((p2) => !touched.has(p2.path)),
13396
+ ...(out.portions ?? []).filter((p2) => !touched.has(p2.path)),
13272
13397
  ...done.shared.flatMap((r) => r.portions)
13273
13398
  ];
13274
- if (portions.length > 0) next.portions = portions;
13275
- else delete next.portions;
13276
- return next;
13399
+ if (portions.length > 0) out.portions = portions;
13400
+ else delete out.portions;
13401
+ return out;
13277
13402
  }
13278
- function removeCliAction(ctx, io) {
13279
- const target = resolveCliTarget(ctx, io);
13280
- if (target === null) return;
13403
+ function runScopedRemoval(ctx, io, plan) {
13281
13404
  const { options, installLog, projectDir, harnessRoot } = ctx;
13282
- const installed = installedClis(installLog);
13283
- const remaining = installed.filter((cli) => cli !== target);
13284
- const plan = planCliRemoval(target, remaining);
13285
- for (const line of removeCliHeader(target, installed, remaining)) io.log(line);
13286
- if (options.dryRun) {
13287
- for (const line of removeCliDryRunLines(plan, ctx, remaining)) io.log(line);
13288
- io.exit(0);
13289
- return;
13290
- }
13405
+ const preview = options.dryRun === true;
13406
+ const remaining = plan.remaining.join(", ");
13407
+ const recordNote = `${plan.flag === "cli" ? "clis" : "tracks"}: ${remaining}`;
13408
+ const header = [
13409
+ "",
13410
+ c.bold(`uzys-agent-harness \xB7 uninstall --${plan.flag} ${plan.target}`),
13411
+ "",
13412
+ c.dim(` installed: ${plan.installed.join(", ")}`),
13413
+ c.dim(` removing: ${plan.target}`),
13414
+ c.dim(` remaining: ${remaining}`),
13415
+ ""
13416
+ ];
13417
+ for (const line of header) io.log(line);
13291
13418
  const done = removeHarnessFiles(installLog, projectDir, plan.scope, {
13292
- rm: io.rm,
13293
- write: true,
13419
+ rm: preview ? () => {
13420
+ } : io.rm,
13421
+ write: !preview,
13294
13422
  harnessRoot
13295
13423
  });
13296
- for (const line of harnessRemovalLines(done, installLog, projectDir, plan.scope, false)) {
13297
- io.log(line);
13298
- }
13299
- for (const line of assetsLeftLines(installLog, projectDir, plan.scope)) io.log(line);
13300
- const rerenderSet = agentsMdRerenderSet(target, remaining, installLog, projectDir);
13301
- const rerender = rerenderSet === null ? null : rerenderAgentsMd(ctx, rerenderSet);
13302
- if (rerender !== null) for (const line of rerenderLines(rerender)) io.log(line);
13303
- const settled = settleCliLog(ctx, target, remaining, done, done.anchor.removed);
13424
+ const what = plan.flag === "cli" ? "CLI" : "\uD2B8\uB799";
13425
+ const lines2 = [
13426
+ ...preview ? [c.yellow(`[DRY RUN] ${what} \uC81C\uAC70 \uBBF8\uB9AC\uBCF4\uAE30 (\uC2E4\uC81C \uBCC0\uACBD \uC5C6\uC74C):`), ""] : [],
13427
+ ...harnessRemovalLines(done, installLog, projectDir, plan.scope, preview),
13428
+ ...plan.leftLines(preview, done)
13429
+ ];
13430
+ if (preview) {
13431
+ if (plan.rerenderSet !== null) {
13432
+ lines2.push(
13433
+ ` \u25CB would re-render AGENTS.md for ${displayNames(plan.rerenderSet)} (your sections kept)`
13434
+ );
13435
+ }
13436
+ for (const line of [...lines2, ` \u25CB install log updated (${recordNote})`, ""]) io.log(line);
13437
+ io.exit(0);
13438
+ return;
13439
+ }
13440
+ for (const line of lines2) io.log(line);
13441
+ const settled = plan.settle(done);
13442
+ const agentsBefore = readTextOrEmpty(join35(projectDir, AGENTS_MD2));
13443
+ const rerender = plan.rerenderSet === null ? null : rerenderAgentsMd(ctx, plan.rerenderSet, settled);
13444
+ const rewrote = rerender !== null && readTextOrEmpty(join35(projectDir, AGENTS_MD2)) !== agentsBefore;
13445
+ if (rewrote) for (const line of rerenderLines(rerender)) io.log(line);
13304
13446
  const next = rerender === null ? settled : withRerender(settled, rerender);
13305
13447
  try {
13306
13448
  io.writeLog(projectDir, next);
13307
- io.log(` ${status.success(`install log updated (clis: ${remaining.join(", ")})`)}`);
13449
+ io.log(` ${status.success(`install log updated (${recordNote})`)}`);
13308
13450
  } catch (e2) {
13309
13451
  io.err(status.failure(c.red(`ERROR: install log \uAC31\uC2E0 \uC2E4\uD328 \u2014 ${installLogPath(projectDir)}`)));
13310
13452
  io.err(c.dim(` ${e2 instanceof Error ? e2.message : String(e2)}`));
13311
13453
  io.log("");
13312
13454
  io.log(
13313
- c.yellow(`${target} \uB294 \uC81C\uAC70\uB410\uC73C\uB098 install log \uB97C \uAC31\uC2E0\uD558\uC9C0 \uBABB\uD588\uB2E4 (\uAE30\uB85D\uC774 \uC2E4\uC81C\uC640 \uB2E4\uB974\uB2E4)`)
13455
+ c.yellow(
13456
+ `${plan.target} \uB294 \uC81C\uAC70\uB410\uC73C\uB098 install log \uB97C \uAC31\uC2E0\uD558\uC9C0 \uBABB\uD588\uB2E4 (\uAE30\uB85D\uC774 \uC2E4\uC81C\uC640 \uB2E4\uB974\uB2E4)`
13457
+ )
13314
13458
  );
13315
13459
  io.exit(1);
13316
13460
  return;
13317
13461
  }
13318
13462
  io.log("");
13319
- io.log(status.success(c.green(`${target} removed (remaining: ${remaining.join(", ")})`)));
13463
+ io.log(status.success(c.green(`${plan.target} removed (remaining: ${remaining})`)));
13320
13464
  io.exit(0);
13321
13465
  }
13466
+ function removeCliAction(ctx, io) {
13467
+ const { options, installLog, projectDir } = ctx;
13468
+ const installed = installedClis(installLog);
13469
+ const target = resolveScopeTarget(io, {
13470
+ flag: "cli",
13471
+ value: options.cli,
13472
+ conflict: options.only !== void 0,
13473
+ vocab: CLI_BASES,
13474
+ installed
13475
+ });
13476
+ if (target === null) return;
13477
+ const remaining = installed.filter((cli) => cli !== target);
13478
+ const plan = planCliRemoval(target, remaining, installLog, projectDir);
13479
+ runScopedRemoval(ctx, io, {
13480
+ flag: "cli",
13481
+ target,
13482
+ installed,
13483
+ remaining,
13484
+ scope: plan.scope,
13485
+ leftLines: (preview) => [
13486
+ ...keptForRemainingLines(plan, preview),
13487
+ ...assetsLeftLines(installLog, projectDir, plan.scope)
13488
+ ],
13489
+ rerenderSet: agentsMdRerenderSet(target, remaining, installLog, projectDir),
13490
+ settle: (done) => settleCliLog(ctx, target, remaining, done, done.anchor.removed)
13491
+ });
13492
+ }
13493
+ function removeTrackAction(ctx, io) {
13494
+ const { options, installLog, projectDir } = ctx;
13495
+ const target = resolveScopeTarget(io, {
13496
+ flag: "track",
13497
+ value: options.track,
13498
+ conflict: options.only !== void 0 || options.cli !== void 0,
13499
+ vocab: TRACKS,
13500
+ installed: installLog.spec.tracks.filter(isTrack)
13501
+ });
13502
+ if (target === null) return;
13503
+ const removal = trackRemoval(installLog, target);
13504
+ runScopedRemoval(ctx, io, {
13505
+ flag: "track",
13506
+ target,
13507
+ installed: removal.known,
13508
+ remaining: removal.remaining,
13509
+ scope: { ...TRACK_SCOPE, has: removal.has },
13510
+ leftLines: (preview, done) => preview ? trackNotes(ctx, removal, done, false).map((l) => ` \u25CB ${l}`) : trackNotes(ctx, removal, done, true),
13511
+ rerenderSet: agentsMdRerenderSet(null, installedClis(installLog), installLog, projectDir),
13512
+ settle: (done) => settleTrackLog(ctx, target, removal.remaining, done)
13513
+ });
13514
+ }
13515
+ var TRACK_SCOPE = { shared: [], skeleton: [], folders: [], importBlock: false, anchor: false };
13516
+ function trackNotes(ctx, r, d2, write) {
13517
+ const md = join35(ctx.projectDir, "CLAUDE.md");
13518
+ const block = rootBlockNext(ctx, new Set(d2.removed));
13519
+ if (write && block !== null) {
13520
+ backupIfLossyUtf8(md);
13521
+ writeFileSync9(md, block);
13522
+ }
13523
+ const left = trackLeftLines(ctx.installLog, r, ctx.harnessRoot);
13524
+ if (write) {
13525
+ const refreshed = block === null ? [] : ["CLAUDE.md \u2014 harness block refreshed (\uBCF8\uBB38 \uBCF4\uC874)"];
13526
+ return [
13527
+ ...refreshed.map((l) => ` ${status.success(l)}`),
13528
+ ...left.map((l) => ` \u2298 left ${l}`)
13529
+ ];
13530
+ }
13531
+ const would = block === null ? [] : ["would refresh the harness block in CLAUDE.md (\uBCF8\uBB38 \uBCF4\uC874)"];
13532
+ return [trackRemovalNames(d2.removed, r.target), ...would, ...left.map((l) => `leave ${l}`)];
13533
+ }
13534
+ function previewTrackRemoval(ctx, target) {
13535
+ const r = trackRemoval(ctx.installLog, target);
13536
+ const io = { rm: () => {
13537
+ }, write: false, harnessRoot: ctx.harnessRoot };
13538
+ const scope = { ...TRACK_SCOPE, has: r.has };
13539
+ const done = removeHarnessFiles(ctx.installLog, ctx.projectDir, scope, io);
13540
+ const counts = harnessRemovalLines(done, ctx.installLog, ctx.projectDir, scope, true);
13541
+ return [...counts.map((l) => l.replace(/^\s*○ /, "")), ...trackNotes(ctx, r, done, false)];
13542
+ }
13543
+ function rootBlockNext({ installLog, projectDir }, gone) {
13544
+ const anchor = installLog.templates?.rootClaudeMd?.path;
13545
+ const md = join35(projectDir, "CLAUDE.md");
13546
+ const text = safeExists(md) && outsideProjectTarget(projectDir, md) === null ? readTextOrEmpty(md) : "";
13547
+ if (anchor === void 0 || !safeExists(join35(projectDir, anchor))) return null;
13548
+ if (stripHarnessImport(text) === null) return null;
13549
+ const stays = (id) => listFilesRecursive(join35(projectDir, SKILLS_DIR3, id)).some(
13550
+ (f) => !gone.has(`${SKILLS_DIR3}${id}/${f}`)
13551
+ );
13552
+ const continuousSkills = CONTINUOUS_SKILLS.map((s) => s.id).filter(stays);
13553
+ const next = upsertHarnessImport(text, { projectName: "", tracks: [], continuousSkills });
13554
+ return next === text ? null : next;
13555
+ }
13556
+ function settleTrackLog(ctx, target, remaining, done) {
13557
+ const { installLog, projectDir } = ctx;
13558
+ const tracks = installLog.spec.tracks.filter((t) => t !== target);
13559
+ const next = forgetRemoved({ ...installLog, spec: { ...installLog.spec, tracks } }, done);
13560
+ const abs = join35(projectDir, INSTALLED_TRACKS);
13561
+ const key = INSTALLED_TRACKS.slice(CLAUDE_DIR3.length);
13562
+ const prior = installLog.policyFiles?.find((f) => f.path === key)?.sha256;
13563
+ const edited = prior !== void 0 && safeExists(abs) && hashContent(readTextOrEmpty(abs)) !== prior;
13564
+ if (!installedClis(installLog).includes("claude") || edited) return next;
13565
+ if (outsideProjectTarget(projectDir, abs) !== null) return next;
13566
+ const content = installedTracksText(remaining);
13567
+ writeFileSync9(abs, content);
13568
+ if (next.policyFiles !== void 0) {
13569
+ const sha256 = hashContent(content);
13570
+ next.policyFiles = [...next.policyFiles.filter((f) => f.path !== key), { path: key, sha256 }];
13571
+ }
13572
+ return next;
13573
+ }
13322
13574
  var UNINSTALL_SHARED = Object.keys(SHARED_FILES).filter(
13323
13575
  (p2) => p2 !== "CLAUDE.md"
13324
13576
  );
@@ -13561,11 +13813,16 @@ var LEGACY_LOG = `${LEGACY_INSTALL_LOG_DIR}/${INSTALL_LOG_FILENAME}`;
13561
13813
  var SKILLS_DIR3 = ".claude/skills/";
13562
13814
  var CLI_FOLDERS2 = [".claude", ".codex", ".opencode"];
13563
13815
  var LEFT_FILES_SHOWN = 3;
13816
+ var RECORD_FILES = [
13817
+ `${INSTALL_LOG_DIR}/${INSTALL_LOG_FILENAME}`,
13818
+ ...HARNESS_RUNTIME_IGNORES
13819
+ ];
13564
13820
  var FULL_SCOPE = {
13565
13821
  has: () => true,
13566
13822
  shared: UNINSTALL_SHARED,
13567
13823
  skeleton: PROJECT_SKELETON_DIRS,
13568
- folders: CLI_FOLDERS2,
13824
+ // #817 — 기록 폴더도 — 헬퍼는 파일 하나씩 회수하고(#812) 설치자 파일은 남으니, 폴더가 왜 남았는지 같은 줄로 말한다
13825
+ folders: [...CLI_FOLDERS2, INSTALL_LOG_DIR],
13569
13826
  importBlock: true,
13570
13827
  anchor: true
13571
13828
  };
@@ -13722,7 +13979,7 @@ function removeRecordedFile(path, ctx) {
13722
13979
  if (verdict.verdict !== "remove" && verdict.verdict !== "backup+remove") return;
13723
13980
  let backup = null;
13724
13981
  if (verdict.verdict === "backup+remove") {
13725
- backup = io.write ? relative7(projectDir, backupFile(effective)).split(sep7).join("/") : `${relative7(projectDir, effective).split(sep7).join("/")}.backup-<time>`;
13982
+ backup = io.write ? relative8(projectDir, backupFile(effective)).split(sep8).join("/") : `${relative8(projectDir, effective).split(sep8).join("/")}.backup-<time>`;
13726
13983
  }
13727
13984
  if (io.write) {
13728
13985
  if (effective !== abs) io.rm(abs);
@@ -13742,7 +13999,7 @@ function legacyFileCandidates() {
13742
13999
  }
13743
14000
  function mayReclaimLegacy(log, projectDir, path) {
13744
14001
  const clis = installedClis(log);
13745
- if (!ownersOf(path).some((c3) => clis.includes(c3))) return false;
14002
+ if (!usersOf(path, projectDir).some((c3) => clis.includes(c3))) return false;
13746
14003
  if (isExcludedPath(bundlePath(path), excludedIds(log))) return false;
13747
14004
  return !(log.rootFiles ?? []).some(
13748
14005
  (f) => f.path === path && f.change === "displaced" && !isLegacyDisplaced(log, projectDir, f)
@@ -14025,7 +14282,7 @@ function leftFolderLines(r, log, projectDir, scope, preview) {
14025
14282
  const entries = /* @__PURE__ */ new Map();
14026
14283
  for (const n of yoursUnder(projectDir, dir)) {
14027
14284
  const full = `${dir}/${n.name}`;
14028
- if (full === LEGACY_LOG) continue;
14285
+ if (full === LEGACY_LOG || RECORD_FILES.includes(full)) continue;
14029
14286
  if (!gone.has(full) && !assetFiles.has(full) && !said(full)) entries.set(n.name, n.shown);
14030
14287
  }
14031
14288
  for (const p2 of back)
@@ -14125,7 +14382,7 @@ function removeAnchor(log, projectDir, io) {
14125
14382
  ]
14126
14383
  };
14127
14384
  }
14128
- const backup = edited ? relative7(projectDir, backupFile(abs)).split(sep7).join("/") : null;
14385
+ const backup = edited ? relative8(projectDir, backupFile(abs)).split(sep8).join("/") : null;
14129
14386
  io.rm(abs);
14130
14387
  return {
14131
14388
  removed: true,
@@ -14188,7 +14445,7 @@ function removeLegacyAgentsMd(path, abs, effective, strip, ctx) {
14188
14445
  let backup = `${path}.backup-<time>`;
14189
14446
  try {
14190
14447
  if (io.write) {
14191
- backup = relative7(projectDir, backupFile(effective)).split(sep7).join("/");
14448
+ backup = relative8(projectDir, backupFile(effective)).split(sep8).join("/");
14192
14449
  if (strip.remainder !== null) {
14193
14450
  writeFileSync9(effective, neutralizeScaffoldBanner(strip.remainder), "utf8");
14194
14451
  } else {
@@ -14234,7 +14491,7 @@ function removeToolFiles(files, projectDir, rm) {
14234
14491
  try {
14235
14492
  if (hashContent(readFileSync27(abs, "utf8")) !== sha256) {
14236
14493
  const backup = backupFile(abs);
14237
- notes.push(`backed up ${path} \u2192 ${relative7(projectDir, backup)} (changed since install)`);
14494
+ notes.push(`backed up ${path} \u2192 ${relative8(projectDir, backup)} (changed since install)`);
14238
14495
  }
14239
14496
  rm(abs);
14240
14497
  removed.push(path);
@@ -14249,8 +14506,8 @@ function removeToolFiles(files, projectDir, rm) {
14249
14506
  function pruneEmptyDirsUpward(projectDir, startDir) {
14250
14507
  let cursor = startDir;
14251
14508
  while (true) {
14252
- const rel = relative7(projectDir, cursor);
14253
- if (rel === "" || rel.startsWith(`..${sep7}`) || rel === "..") return;
14509
+ const rel = relative8(projectDir, cursor);
14510
+ if (rel === "" || rel.startsWith(`..${sep8}`) || rel === "..") return;
14254
14511
  try {
14255
14512
  if (readdirSync7(cursor).length > 0) return;
14256
14513
  rmdirSync3(cursor);
@@ -14305,6 +14562,9 @@ function registerUninstallCommand(cli) {
14305
14562
  ).option(
14306
14563
  "--cli <name>",
14307
14564
  `[Scope] Remove one CLI only (${CLI_BASES.join(" | ")}). Shared files stay until the last user leaves`
14565
+ ).option(
14566
+ "--track <name>",
14567
+ `[Scope] Remove one track only (${TRACKS.join(" | ")}). Files another recorded track also installs stay`
14308
14568
  ).option("--yes", "[Mode] Skip the interactive picker and remove everything (non-interactive)").action(async (options) => {
14309
14569
  await dispatchUninstall(options);
14310
14570
  });
@@ -14313,7 +14573,7 @@ function shouldRunInteractive(options, isTty) {
14313
14573
  if (!isTty) return false;
14314
14574
  if (options.yes || options.dryRun) return false;
14315
14575
  if (options.keepTemplates) return false;
14316
- return options.only === void 0 && options.cli === void 0;
14576
+ return options.only === void 0 && options.cli === void 0 && options.track === void 0;
14317
14577
  }
14318
14578
  async function dispatchUninstall(options) {
14319
14579
  const projectDir = resolve4(options.projectDir ?? process.cwd());
@@ -14326,7 +14586,7 @@ async function dispatchUninstall(options) {
14326
14586
  console.error(
14327
14587
  status.failure(
14328
14588
  c.red(
14329
- "ERROR: no terminal and no flag saying what to remove \u2014 nothing was done. Pass --yes (remove everything), --only <ids>, --cli <name>, or --dry-run."
14589
+ "ERROR: no terminal and no flag saying what to remove \u2014 nothing was done. Pass --yes (remove everything), --only <ids>, --cli <name>, --track <name>, or --dry-run."
14330
14590
  )
14331
14591
  )
14332
14592
  );
@@ -14339,7 +14599,7 @@ async function dispatchUninstall(options) {
14339
14599
  await runUninstallScreen(projectDir);
14340
14600
  }
14341
14601
  function lacksRemovalIntent(options) {
14342
- return options.yes === void 0 && options.dryRun === void 0 && options.only === void 0 && options.cli === void 0;
14602
+ return options.yes === void 0 && options.dryRun === void 0 && options.only === void 0 && options.cli === void 0 && options.track === void 0;
14343
14603
  }
14344
14604
  async function runUninstallScreen(projectDir, opts = {}) {
14345
14605
  const picked = await runInteractiveUninstall(projectDir, opts);