@uzysjung/agent-harness 26.160.0 → 26.161.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
@@ -4,6 +4,7 @@ import {
4
4
  CATEGORIES,
5
5
  CATEGORY_TITLES,
6
6
  CLI_BASES,
7
+ CLI_BASE_SORT_ORDER,
7
8
  CONTINUOUS_SKILLS,
8
9
  CONTINUOUS_SKILLS_HEADING,
9
10
  DEFAULT_OPTIONS,
@@ -11,6 +12,7 @@ import {
11
12
  HARNESS_ANCHOR_FILE,
12
13
  HARNESS_IMPORT_LINE,
13
14
  INSTALL_LOG_DIR,
15
+ INSTALL_LOG_FILENAME,
14
16
  INTERNAL_BUNDLED_SKILL_IDS,
15
17
  POLICY_DIRS,
16
18
  RENAMED_SKILL_IDS,
@@ -47,6 +49,7 @@ import {
47
49
  hashContent,
48
50
  init_esm_shims,
49
51
  installLogPath,
52
+ installedClis,
50
53
  isAssetSelected,
51
54
  isCliBase,
52
55
  isCliNeutralTarget,
@@ -57,6 +60,7 @@ import {
57
60
  legacyInstallLogPath,
58
61
  listFilesRecursive,
59
62
  mergeExternalFiles,
63
+ parseCliTargets,
60
64
  readInstallLog,
61
65
  renderContinuousSkillsNote,
62
66
  renderFillScaffold,
@@ -64,12 +68,14 @@ import {
64
68
  resolveBundleRoot,
65
69
  resolveRules,
66
70
  resolveScope,
71
+ scaffoldBanner,
67
72
  stripHarnessImport,
68
73
  summarizeContextCost,
74
+ targetsInclude,
69
75
  upsertHarnessImport,
70
76
  withContinuousSkillsNote,
71
77
  writeInstallLog
72
- } from "./chunk-ZXWOY5VO.js";
78
+ } from "./chunk-MBRLCU5O.js";
73
79
 
74
80
  // node_modules/sisteransi/src/index.js
75
81
  var require_src = __commonJS({
@@ -754,7 +760,7 @@ var cac = (name = "") => new CAC(name);
754
760
  // package.json
755
761
  var package_default = {
756
762
  name: "@uzysjung/agent-harness",
757
- version: "26.160.0",
763
+ version: "26.161.0",
758
764
  description: "Curate vetted AI-coding skills & plugins by your tech stack \u2014 install only what you need, across Claude Code, Codex, OpenCode & Antigravity",
759
765
  type: "module",
760
766
  publishConfig: {
@@ -799,10 +805,10 @@ var package_default = {
799
805
  devDependencies: {
800
806
  "@biomejs/biome": "^2.4.13",
801
807
  "@types/node": "^25.6.0",
802
- "@vitest/coverage-v8": "^4.1.10",
808
+ "@vitest/coverage-v8": "^4.1.11",
803
809
  tsup: "^8.3.0",
804
810
  typescript: "^5.6.0",
805
- vitest: "^4.1.10"
811
+ vitest: "^4.1.11"
806
812
  },
807
813
  license: "MIT",
808
814
  repository: {
@@ -894,55 +900,6 @@ function withBaselineHints(targets, templatesDir) {
894
900
  });
895
901
  }
896
902
 
897
- // src/cli-targets.ts
898
- init_esm_shims();
899
- var CLI_BASE_SORT_ORDER = {
900
- claude: 0,
901
- codex: 1,
902
- opencode: 2,
903
- antigravity: 3
904
- };
905
- function parseCliTargets(input) {
906
- const items = normalizeInput(input);
907
- if (items.length === 0) {
908
- return { ok: true, targets: ["claude"], warnings: [] };
909
- }
910
- const collected = /* @__PURE__ */ new Set();
911
- const warnings = [];
912
- for (const item of items) {
913
- if (!isCliBase(item)) {
914
- let hint = "";
915
- if (item === "both") {
916
- hint = "\n v0.8.0 removed 'both' alias. Use --cli claude --cli codex.";
917
- } else if (item === "all") {
918
- hint = "\n v0.8.0 removed 'all' alias. Use --cli claude --cli codex --cli opencode.";
919
- } else if (item.includes(",")) {
920
- hint = "\n Tip: comma-separated not supported. Use --cli A --cli B for multiple.";
921
- }
922
- return {
923
- ok: false,
924
- targets: ["claude"],
925
- warnings,
926
- error: `Invalid --cli value: ${item}. Must be one of: ${CLI_BASES.join(" | ")}${hint}`
927
- };
928
- }
929
- collected.add(item);
930
- }
931
- const targets = [...collected].sort((a, b2) => CLI_BASE_SORT_ORDER[a] - CLI_BASE_SORT_ORDER[b2]);
932
- return { ok: true, targets, warnings };
933
- }
934
- function normalizeInput(input) {
935
- if (input === void 0 || input === null) return [];
936
- if (typeof input === "string") {
937
- const trimmed = input.trim();
938
- return trimmed === "" ? [] : [trimmed];
939
- }
940
- return input.filter((s) => typeof s === "string" && s.trim() !== "").map((s) => s.trim());
941
- }
942
- function targetsInclude(targets, base) {
943
- return targets.includes(base);
944
- }
945
-
946
903
  // src/design.ts
947
904
  init_esm_shims();
948
905
  var isColorEnabled = (() => {
@@ -1030,62 +987,18 @@ function padDisplay(s, width) {
1030
987
  init_esm_shims();
1031
988
  import {
1032
989
  chmodSync as chmodSync3,
1033
- existsSync as existsSync14,
990
+ existsSync as existsSync15,
1034
991
  mkdirSync as mkdirSync4,
1035
992
  readdirSync as readdirSync4,
1036
- readFileSync as readFileSync13,
993
+ readFileSync as readFileSync14,
1037
994
  writeFileSync as writeFileSync6
1038
995
  } from "fs";
1039
- import { basename as basename5, dirname as dirname4, join as join13, resolve } from "path";
996
+ import { basename as basename5, dirname as dirname4, join as join16, resolve } from "path";
1040
997
 
1041
- // src/ci-scaffold.ts
998
+ // src/anchor-seed.ts
1042
999
  init_esm_shims();
1043
- import { existsSync as existsSync2 } from "fs";
1000
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
1044
1001
  import { join as join2 } from "path";
1045
- var PYTHON_TRACKS = "data|csr-fastapi|full";
1046
- var VARIANTS = [
1047
- {
1048
- source: "ci-node.yml",
1049
- target: "ci.yml",
1050
- applies: (tracks) => anyTrack(tracks, "csr-*|ssr-*|tooling|full") || !anyTrack(tracks, PYTHON_TRACKS)
1051
- },
1052
- {
1053
- source: "ci-python.yml",
1054
- target: "ci-python.yml",
1055
- applies: (tracks) => anyTrack(tracks, PYTHON_TRACKS)
1056
- },
1057
- {
1058
- source: "e2e.yml",
1059
- target: "e2e.yml",
1060
- applies: (tracks) => hasUiTrack(tracks)
1061
- }
1062
- ];
1063
- function installCiScaffold(ctx) {
1064
- const report = { written: [], skippedExisting: [] };
1065
- for (const v2 of VARIANTS) {
1066
- if (!v2.applies(ctx.tracks)) continue;
1067
- const relTarget = `.github/workflows/${v2.target}`;
1068
- const target = join2(ctx.projectDir, relTarget);
1069
- if (existsSync2(target)) {
1070
- report.skippedExisting.push(relTarget);
1071
- continue;
1072
- }
1073
- copyFile(join2(ctx.harnessRoot, "templates/github-workflows", v2.source), target);
1074
- report.written.push(relTarget);
1075
- }
1076
- return report;
1077
- }
1078
-
1079
- // src/cli-transforms.ts
1080
- init_esm_shims();
1081
-
1082
- // src/antigravity/transform.ts
1083
- init_esm_shims();
1084
- import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
1085
- import { basename, join as join6 } from "path";
1086
-
1087
- // src/codex/agents-md.ts
1088
- init_esm_shims();
1089
1002
 
1090
1003
  // src/agents-md-merge.ts
1091
1004
  init_esm_shims();
@@ -1210,8 +1123,200 @@ function mergeAgentsMd(params) {
1210
1123
  }
1211
1124
  return bodies.size === 0 ? rendered : assemble(rendered, names, bodies);
1212
1125
  }
1126
+ function installerLines(body, m) {
1127
+ if (body === null) return [];
1128
+ const at2 = blockRange(body, m);
1129
+ return at2 === null ? [...body] : [...body.slice(0, at2[0]), ...body.slice(at2[1] + 1)];
1130
+ }
1131
+ function compact(lines) {
1132
+ return lines.map((l) => l.replace(/\r$/, "").trimEnd()).filter((l) => l.trim() !== "");
1133
+ }
1134
+ function trimBlankEdges(lines) {
1135
+ const out = trimTrailingBlanks(lines);
1136
+ while (out.length > 0 && (out[0] ?? "").trim() === "") out.shift();
1137
+ return out;
1138
+ }
1139
+ function projectContextInstallerLines(params) {
1140
+ const { existing, template } = params;
1141
+ const body = sectionBody(existing, sectionNames(template), CONTEXT_SECTION);
1142
+ return body === null ? null : installerLines(body, SKILLS_BLOCK);
1143
+ }
1144
+ function stripHarnessFromAgentsMd(params) {
1145
+ const { existing, template } = params;
1146
+ const lines = existing.split("\n");
1147
+ if (blockRange(lines, ANCHOR_BLOCK) === null && blockRange(lines, SKILLS_BLOCK) === null) {
1148
+ return null;
1149
+ }
1150
+ const names = sectionNames(template);
1151
+ const context = installerLines(sectionBody(existing, names, CONTEXT_SECTION), SKILLS_BLOCK);
1152
+ const rules = installerLines(sectionBody(existing, names, RULES_SECTION), ANCHOR_BLOCK);
1153
+ const scaffold = compact(renderFillScaffold("agents-md").split("\n"));
1154
+ const contextFilled = compact(context).join("\n") !== scaffold.join("\n");
1155
+ const rulesFilled = compact(rules).length > 0;
1156
+ if (!contextFilled && !rulesFilled) return null;
1157
+ const out = [];
1158
+ const title = (lines[0] ?? "").replace(/\r$/, "");
1159
+ if (/^# /.test(title)) out.push(title, "");
1160
+ out.push(`## ${CONTEXT_SECTION}`, "", ...trimBlankEdges(context), "");
1161
+ if (rulesFilled) out.push(`## ${RULES_SECTION}`, "", ...trimBlankEdges(rules), "");
1162
+ return out.join("\n");
1163
+ }
1164
+
1165
+ // src/anchor-seed.ts
1166
+ var SCAFFOLD_MARK = "SCAFFOLD \u2014 not filled in yet";
1167
+ var TRACK_NOTE_MARK = "Active track(s):";
1168
+ function readIfExists(path) {
1169
+ if (!existsSync2(path)) return null;
1170
+ try {
1171
+ return readFileSync2(path, "utf8");
1172
+ } catch {
1173
+ return null;
1174
+ }
1175
+ }
1176
+ function dropHarnessQuoteBlocks(lines) {
1177
+ const out = [];
1178
+ let i = 0;
1179
+ while (i < lines.length) {
1180
+ if (!(lines[i] ?? "").trim().startsWith(">")) {
1181
+ out.push(lines[i] ?? "");
1182
+ i += 1;
1183
+ continue;
1184
+ }
1185
+ let end = i;
1186
+ while (end < lines.length && (lines[end] ?? "").trim().startsWith(">")) end++;
1187
+ const run = lines.slice(i, end);
1188
+ const text = run.join("\n");
1189
+ if (!text.includes(SCAFFOLD_MARK) && !text.includes(TRACK_NOTE_MARK)) out.push(...run);
1190
+ i = end;
1191
+ }
1192
+ return out;
1193
+ }
1194
+ function dropTitle(lines) {
1195
+ const out = [...lines];
1196
+ while (out.length > 0 && (out[0] ?? "").trim() === "") out.shift();
1197
+ if (out.length > 0 && /^#\s/.test(out[0] ?? "")) out.shift();
1198
+ return out;
1199
+ }
1200
+ function compact2(lines) {
1201
+ return lines.map((l) => l.replace(/\r$/, "").trimEnd()).filter((l) => l.trim() !== "");
1202
+ }
1203
+ function isPristineScaffold(body, surface) {
1204
+ const pristine = compact2(dropHarnessQuoteBlocks(renderFillScaffold(surface).split("\n")));
1205
+ return compact2(body).join("\n") === pristine.join("\n");
1206
+ }
1207
+ function compose(body, into) {
1208
+ const trimmed = [...body];
1209
+ while (trimmed.length > 0 && (trimmed.at(-1) ?? "").trim() === "") trimmed.pop();
1210
+ while (trimmed.length > 0 && (trimmed[0] ?? "").trim() === "") trimmed.shift();
1211
+ if (trimmed.length === 0) return null;
1212
+ return `${scaffoldBanner(into)}
1213
+
1214
+ ${trimmed.join("\n")}`;
1215
+ }
1216
+ function seedAgentsMdProjectContext(projectDir) {
1217
+ const root = readIfExists(join2(projectDir, "CLAUDE.md"));
1218
+ if (root === null) return null;
1219
+ const withoutImport = stripHarnessImport(root) ?? root;
1220
+ const body = dropHarnessQuoteBlocks(dropTitle(withoutImport.split("\n")));
1221
+ if (isPristineScaffold(body, "claude")) return null;
1222
+ return compose(body, "agents-md");
1223
+ }
1224
+ function seedRootClaudeProjectContext(projectDir, harnessRoot) {
1225
+ const agents = readIfExists(join2(projectDir, "AGENTS.md"));
1226
+ if (agents === null) return null;
1227
+ const template = readIfExists(join2(harnessRoot, "templates/codex/AGENTS.md.template")) ?? readIfExists(join2(harnessRoot, "templates/opencode/AGENTS.md.template"));
1228
+ if (template === null) return null;
1229
+ const installer = projectContextInstallerLines({ existing: agents, template });
1230
+ if (installer === null) return null;
1231
+ const body = dropHarnessQuoteBlocks(installer);
1232
+ if (isPristineScaffold(body, "agents-md")) return null;
1233
+ return compose(body, "claude");
1234
+ }
1235
+
1236
+ // src/ci-scaffold.ts
1237
+ init_esm_shims();
1238
+ import { existsSync as existsSync3 } from "fs";
1239
+ import { join as join3 } from "path";
1240
+ var PYTHON_TRACKS = "data|csr-fastapi|full";
1241
+ var VARIANTS = [
1242
+ {
1243
+ source: "ci-node.yml",
1244
+ target: "ci.yml",
1245
+ applies: (tracks) => anyTrack(tracks, "csr-*|ssr-*|tooling|full") || !anyTrack(tracks, PYTHON_TRACKS)
1246
+ },
1247
+ {
1248
+ source: "ci-python.yml",
1249
+ target: "ci-python.yml",
1250
+ applies: (tracks) => anyTrack(tracks, PYTHON_TRACKS)
1251
+ },
1252
+ {
1253
+ source: "e2e.yml",
1254
+ target: "e2e.yml",
1255
+ applies: (tracks) => hasUiTrack(tracks)
1256
+ }
1257
+ ];
1258
+ function installCiScaffold(ctx) {
1259
+ const report = { written: [], skippedExisting: [] };
1260
+ for (const v2 of VARIANTS) {
1261
+ if (!v2.applies(ctx.tracks)) continue;
1262
+ const relTarget = `.github/workflows/${v2.target}`;
1263
+ const target = join3(ctx.projectDir, relTarget);
1264
+ if (existsSync3(target)) {
1265
+ report.skippedExisting.push(relTarget);
1266
+ continue;
1267
+ }
1268
+ copyFile(join3(ctx.harnessRoot, "templates/github-workflows", v2.source), target);
1269
+ report.written.push(relTarget);
1270
+ }
1271
+ return report;
1272
+ }
1273
+
1274
+ // src/cli-transforms.ts
1275
+ init_esm_shims();
1276
+
1277
+ // src/antigravity/transform.ts
1278
+ init_esm_shims();
1279
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
1280
+ import { basename, join as join8 } from "path";
1281
+
1282
+ // src/agents-skill-targets.ts
1283
+ init_esm_shims();
1284
+ import { join as join4, sep } from "path";
1285
+ function bundledSkillTargets(projectDir) {
1286
+ const log = readInstallLog(projectDir);
1287
+ if (log === null) return [];
1288
+ const tracks = log.spec.tracks.filter(
1289
+ (t) => TRACKS.includes(t)
1290
+ );
1291
+ if (tracks.length === 0) return [];
1292
+ const skillExcluded = new Set(log.spec.skillExclude ?? []);
1293
+ const baselineExcluded = new Set(log.spec.baselineExclude ?? []);
1294
+ return buildAssetSpec({ tracks, options: DEFAULT_OPTIONS }).selectedInternalSkills.filter(
1295
+ (id) => !skillExcluded.has(id) && !isBaselineExcluded(`.claude/skills/${id}`, baselineExcluded)
1296
+ );
1297
+ }
1298
+ function allowSkillCreation(writer, projectDir, ids) {
1299
+ if (ids.length === 0) return writer;
1300
+ const roots = ids.map((id) => `${join4(projectDir, ".agents", "skills", id)}${sep}`);
1301
+ return {
1302
+ write(absPath, content, opts) {
1303
+ const inTarget = roots.some((root) => absPath.startsWith(root));
1304
+ return writer.write(absPath, content, inTarget ? { ...opts, createInRefresh: true } : opts);
1305
+ },
1306
+ result: () => writer.result()
1307
+ };
1308
+ }
1309
+ function agentsSkillSlot(params) {
1310
+ const { projectDir, selectedInternalSkills, refreshOnly, writer } = params;
1311
+ const creatable = refreshOnly ? bundledSkillTargets(projectDir) : [];
1312
+ return {
1313
+ skillIds: creatable.length === 0 ? selectedInternalSkills : [.../* @__PURE__ */ new Set([...selectedInternalSkills, ...creatable])],
1314
+ skillWriter: allowSkillCreation(writer, projectDir, creatable)
1315
+ };
1316
+ }
1213
1317
 
1214
1318
  // src/codex/agents-md.ts
1319
+ init_esm_shims();
1215
1320
  function renameSlashes(text) {
1216
1321
  return text.replaceAll("/uzys:", "/uzys-");
1217
1322
  }
@@ -1223,8 +1328,8 @@ function renderAgentsMd(params) {
1223
1328
 
1224
1329
  // src/codex/skills.ts
1225
1330
  init_esm_shims();
1226
- import { chmodSync, existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1227
- import { join as join3 } from "path";
1331
+ import { chmodSync, existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
1332
+ import { join as join5 } from "path";
1228
1333
  function renderBundledSkill(source) {
1229
1334
  const trimmed = source.trimEnd();
1230
1335
  const lines = trimmed.split(/\r?\n/);
@@ -1256,14 +1361,14 @@ function writeBundledSkillDirs(params) {
1256
1361
  const { harnessRoot, projectDir, skillIds, writer } = params;
1257
1362
  const written = [];
1258
1363
  for (const id of skillIds) {
1259
- const srcDir = join3(harnessRoot, "templates/skills", id);
1260
- const srcSkill = join3(srcDir, "SKILL.md");
1261
- if (!existsSync3(srcSkill)) {
1364
+ const srcDir = join5(harnessRoot, "templates/skills", id);
1365
+ const srcSkill = join5(srcDir, "SKILL.md");
1366
+ if (!existsSync4(srcSkill)) {
1262
1367
  continue;
1263
1368
  }
1264
- const targetDir = join3(projectDir, ".agents", "skills", id);
1265
- const targetSkill = join3(targetDir, "SKILL.md");
1266
- if (!writer.write(targetSkill, renderBundledSkill(readFileSync2(srcSkill, "utf8")))) {
1369
+ const targetDir = join5(projectDir, ".agents", "skills", id);
1370
+ const targetSkill = join5(targetDir, "SKILL.md");
1371
+ if (!writer.write(targetSkill, renderBundledSkill(readFileSync3(srcSkill, "utf8")))) {
1267
1372
  continue;
1268
1373
  }
1269
1374
  written.push(targetSkill);
@@ -1271,10 +1376,10 @@ function writeBundledSkillDirs(params) {
1271
1376
  if (rel === "SKILL.md") continue;
1272
1377
  if (rel.split("/").some((segment) => segment.startsWith("."))) continue;
1273
1378
  const segments = rel.split("/");
1274
- const raw = readFileSync2(join3(srcDir, ...segments), "utf8");
1379
+ const raw = readFileSync3(join5(srcDir, ...segments), "utf8");
1275
1380
  const ported = rel.endsWith(".md") || rel.endsWith(".sh") ? `${portBody(raw)}
1276
1381
  ` : raw;
1277
- const target = join3(targetDir, ...segments);
1382
+ const target = join5(targetDir, ...segments);
1278
1383
  if (!writer.write(target, ported, { createInRefresh: true })) continue;
1279
1384
  if (rel.endsWith(".sh")) chmodSync(target, 493);
1280
1385
  written.push(target);
@@ -1285,30 +1390,40 @@ function writeBundledSkillDirs(params) {
1285
1390
 
1286
1391
  // src/owned-write.ts
1287
1392
  init_esm_shims();
1288
- import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
1289
- import { dirname, relative, sep } from "path";
1393
+ import { existsSync as existsSync5, mkdirSync, readFileSync as readFileSync4, writeFileSync } from "fs";
1394
+ import { dirname, relative, sep as sep2 } from "path";
1290
1395
 
1291
1396
  // src/foreign-slot.ts
1292
1397
  init_esm_shims();
1293
- import { lstatSync } from "fs";
1294
- import { join as join4 } from "path";
1398
+ import { lstatSync, realpathSync, statSync } from "fs";
1399
+ import { join as join6 } from "path";
1295
1400
  var SKILL_SLOT = /^((?:\.claude|\.agents)\/skills\/[^/]+)(?:\/|$)/;
1296
1401
  function occupiedByNonDirectory(path) {
1297
1402
  const stat = lstatSync(path, { throwIfNoEntry: false });
1298
1403
  return stat !== void 0 && !stat.isDirectory();
1299
1404
  }
1405
+ function linksToProjectSharedSkill(projectDir, id) {
1406
+ const slot = join6(projectDir, ".claude/skills", id);
1407
+ if (lstatSync(slot, { throwIfNoEntry: false })?.isSymbolicLink() !== true) return false;
1408
+ try {
1409
+ const real = realpathSync(slot);
1410
+ return real === join6(realpathSync(projectDir), ".agents/skills", id) && statSync(real).isDirectory();
1411
+ } catch {
1412
+ return false;
1413
+ }
1414
+ }
1300
1415
  function foreignOwnedTarget(projectDir, rel) {
1301
1416
  const slot = SKILL_SLOT.exec(rel)?.[1];
1302
1417
  if (slot === void 0) return null;
1303
- if (occupiedByNonDirectory(join4(projectDir, slot))) return slot;
1418
+ if (occupiedByNonDirectory(join6(projectDir, slot))) return slot;
1304
1419
  if (rel !== slot) {
1305
1420
  const parts = rel.slice(slot.length + 1).split("/");
1306
1421
  let cursor = slot;
1307
1422
  for (const part of parts.slice(0, -1)) {
1308
1423
  cursor = `${cursor}/${part}`;
1309
- if (occupiedByNonDirectory(join4(projectDir, cursor))) return cursor;
1424
+ if (occupiedByNonDirectory(join6(projectDir, cursor))) return cursor;
1310
1425
  }
1311
- const leaf = lstatSync(join4(projectDir, rel), { throwIfNoEntry: false });
1426
+ const leaf = lstatSync(join6(projectDir, rel), { throwIfNoEntry: false });
1312
1427
  if (leaf !== void 0 && !leaf.isFile()) return rel;
1313
1428
  }
1314
1429
  return null;
@@ -1324,20 +1439,20 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
1324
1439
  let updated = 0;
1325
1440
  return {
1326
1441
  write(absPath, content, opts) {
1327
- const rel = relative(projectDir, absPath).split(sep).join("/");
1442
+ const rel = relative(projectDir, absPath).split(sep2).join("/");
1328
1443
  const foreign = foreignOwnedTarget(projectDir, rel);
1329
1444
  if (foreign !== null) {
1330
1445
  if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
1331
1446
  return false;
1332
1447
  }
1333
1448
  const digest = hashContent(content);
1334
- if (!existsSync4(absPath)) {
1449
+ if (!existsSync5(absPath)) {
1335
1450
  if (refreshOnly && !opts?.createInRefresh) return false;
1336
1451
  mkdirSync(dirname(absPath), { recursive: true });
1337
1452
  writeFileSync(absPath, content);
1338
1453
  updated++;
1339
1454
  } else {
1340
- const current = readFileSync3(absPath, "utf8");
1455
+ const current = readFileSync4(absPath, "utf8");
1341
1456
  if (current !== content) {
1342
1457
  if (!isHarnessOwned(baseline, rel, current)) {
1343
1458
  backupPaths.push(backupFile(absPath, now));
@@ -1364,8 +1479,8 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
1364
1479
 
1365
1480
  // src/rules-port.ts
1366
1481
  init_esm_shims();
1367
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1368
- import { join as join5 } from "path";
1482
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
1483
+ import { join as join7 } from "path";
1369
1484
  function stripClaudeFrontmatter(source) {
1370
1485
  if (!source.startsWith("---\n")) return source.trim();
1371
1486
  const end = source.indexOf("\n---", 4);
@@ -1375,9 +1490,9 @@ function stripClaudeFrontmatter(source) {
1375
1490
  function portRules(harnessRoot, ruleNames) {
1376
1491
  const ported = [];
1377
1492
  for (const name of ruleNames) {
1378
- const src = join5(harnessRoot, "templates/rules", `${name}.md`);
1379
- if (!existsSync5(src)) continue;
1380
- ported.push({ name, body: stripClaudeFrontmatter(readFileSync4(src, "utf8")) });
1493
+ const src = join7(harnessRoot, "templates/rules", `${name}.md`);
1494
+ if (!existsSync6(src)) continue;
1495
+ ported.push({ name, body: stripClaudeFrontmatter(readFileSync5(src, "utf8")) });
1381
1496
  }
1382
1497
  return ported;
1383
1498
  }
@@ -1397,11 +1512,17 @@ function runAntigravityTransform(params) {
1397
1512
  refreshOnly
1398
1513
  } = params;
1399
1514
  const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
1400
- const rulesFile = writeRules(harnessRoot, projectDir, writer, selectedInternalSkills);
1515
+ const { skillIds, skillWriter } = agentsSkillSlot({
1516
+ projectDir,
1517
+ selectedInternalSkills,
1518
+ refreshOnly: refreshOnly ?? false,
1519
+ writer
1520
+ });
1521
+ const rulesFile = writeRules(harnessRoot, projectDir, writer, skillIds);
1401
1522
  const antigravityInstalled = rulesFile !== null;
1402
1523
  const harnessRuleFiles = [];
1403
1524
  for (const rule of portRules(harnessRoot, rules)) {
1404
- const target = join6(projectDir, ".agents", "rules", `${rule.name}.md`);
1525
+ const target = join8(projectDir, ".agents", "rules", `${rule.name}.md`);
1405
1526
  if (!writer.write(target, `${rule.body}
1406
1527
  `, { createInRefresh: antigravityInstalled })) {
1407
1528
  continue;
@@ -1411,8 +1532,8 @@ function runAntigravityTransform(params) {
1411
1532
  const skillFiles = writeBundledSkillDirs({
1412
1533
  harnessRoot,
1413
1534
  projectDir,
1414
- skillIds: selectedInternalSkills,
1415
- writer
1535
+ skillIds,
1536
+ writer: skillWriter
1416
1537
  });
1417
1538
  return {
1418
1539
  rulesFile,
@@ -1422,14 +1543,14 @@ function runAntigravityTransform(params) {
1422
1543
  };
1423
1544
  }
1424
1545
  function writeRules(harnessRoot, projectDir, writer, selectedInternalSkills) {
1425
- const claudeMdPath = join6(harnessRoot, "templates/CLAUDE.md");
1426
- const templatePath = join6(harnessRoot, "templates/antigravity/AGENTS.md.template");
1427
- if (!existsSync6(claudeMdPath) || !existsSync6(templatePath)) {
1546
+ const claudeMdPath = join8(harnessRoot, "templates/CLAUDE.md");
1547
+ const templatePath = join8(harnessRoot, "templates/antigravity/AGENTS.md.template");
1548
+ if (!existsSync7(claudeMdPath) || !existsSync7(templatePath)) {
1428
1549
  return null;
1429
1550
  }
1430
- const claudeMd = readFileSync5(claudeMdPath, "utf8");
1431
- const template = readFileSync5(templatePath, "utf8");
1432
- const target = join6(projectDir, ".agents", "rules", "uzys-harness.md");
1551
+ const claudeMd = readFileSync6(claudeMdPath, "utf8");
1552
+ const template = readFileSync6(templatePath, "utf8");
1553
+ const target = join8(projectDir, ".agents", "rules", "uzys-harness.md");
1433
1554
  const rulesOut = renderAgentsMd({
1434
1555
  template,
1435
1556
  claudeMd,
@@ -1447,18 +1568,18 @@ function writeRules(harnessRoot, projectDir, writer, selectedInternalSkills) {
1447
1568
  // src/codex/opt-in.ts
1448
1569
  init_esm_shims();
1449
1570
  import { homedir } from "os";
1450
- import { join as join7 } from "path";
1571
+ import { join as join9 } from "path";
1451
1572
 
1452
1573
  // src/codex/trust-entry.ts
1453
1574
  init_esm_shims();
1454
- import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
1575
+ import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
1455
1576
  import { dirname as dirname2 } from "path";
1456
1577
  var TRUST_BLOCK_REGEX = /\[projects\."([^"]+)"\]/g;
1457
1578
  function registerTrustEntry(opts) {
1458
1579
  const { configPath, projectDir } = opts;
1459
1580
  try {
1460
1581
  mkdirSync2(dirname2(configPath), { recursive: true });
1461
- const existing = existsSync7(configPath) ? readFileSync6(configPath, "utf8") : "";
1582
+ const existing = existsSync8(configPath) ? readFileSync7(configPath, "utf8") : "";
1462
1583
  if (hasTrustEntry(existing, projectDir)) {
1463
1584
  return { status: "already-present" };
1464
1585
  }
@@ -1482,8 +1603,8 @@ function hasTrustEntry(configContent, projectDir) {
1482
1603
 
1483
1604
  // src/codex/opt-in.ts
1484
1605
  function runCodexOptIn(ctx) {
1485
- const codexHome = ctx.codexHome ?? join7(homedir(), ".codex");
1486
- const configPath = join7(codexHome, "config.toml");
1606
+ const codexHome = ctx.codexHome ?? join9(homedir(), ".codex");
1607
+ const configPath = join9(codexHome, "config.toml");
1487
1608
  const result = registerTrustEntry({ configPath, projectDir: ctx.projectDir });
1488
1609
  return {
1489
1610
  trustEntry: {
@@ -1496,8 +1617,8 @@ function runCodexOptIn(ctx) {
1496
1617
 
1497
1618
  // src/codex/transform.ts
1498
1619
  init_esm_shims();
1499
- import { chmodSync as chmodSync2, existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
1500
- import { basename as basename2, join as join8 } from "path";
1620
+ import { chmodSync as chmodSync2, existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
1621
+ import { basename as basename2, join as join10 } from "path";
1501
1622
 
1502
1623
  // src/codex/config-toml.ts
1503
1624
  init_esm_shims();
@@ -1571,38 +1692,49 @@ function runCodexTransform(params) {
1571
1692
  selectedInternalSkills = [],
1572
1693
  rules = [],
1573
1694
  baseline,
1574
- refreshOnly
1695
+ refreshOnly,
1696
+ writeAgentsMd = true
1575
1697
  } = params;
1576
1698
  const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
1577
- const claudeMd = readRequired(join8(harnessRoot, "templates/CLAUDE.md"));
1578
- const agentsTemplate = readRequired(join8(harnessRoot, "templates/codex/AGENTS.md.template"));
1579
- const configTemplate = readRequired(join8(harnessRoot, "templates/codex/config.toml.template"));
1699
+ const claudeMd = readRequired(join10(harnessRoot, "templates/CLAUDE.md"));
1700
+ const agentsTemplate = readRequired(join10(harnessRoot, "templates/codex/AGENTS.md.template"));
1701
+ const configTemplate = readRequired(join10(harnessRoot, "templates/codex/config.toml.template"));
1580
1702
  const projectName = basename2(projectDir);
1581
- const mcp = readOptionalJson(join8(harnessRoot, ".mcp.json"));
1582
- const agentsMdPath = join8(projectDir, "AGENTS.md");
1583
- ensureDir(projectDir);
1584
- const agentsMdOut = renderAgentsMd({
1585
- template: agentsTemplate,
1586
- claudeMd,
1587
- projectName,
1588
- // ADR-085 — 상시 스킬 안내는 앵커가 아니라 여기(프로젝트 맥락)에, 깔린 것만.
1589
- // #503 — 그 조각은 설치자 소유 절 안에 사니 마커로 감싼다.
1590
- projectContext: withMarkedContinuousSkillsNote(
1591
- renderFillScaffold("agents-md"),
1592
- selectedInternalSkills
1593
- ),
1594
- // Codex 는 룰 디렉터리가 없다 — 룰이 AGENTS.md 본문에 들어가야 도달한다(§Harness Rules).
1595
- harnessRules: renderRulesBlock(portRules(harnessRoot, rules))
1703
+ const mcp = readOptionalJson(join10(harnessRoot, ".mcp.json"));
1704
+ const { skillIds, skillWriter } = agentsSkillSlot({
1705
+ projectDir,
1706
+ selectedInternalSkills,
1707
+ refreshOnly: refreshOnly ?? false,
1708
+ writer
1596
1709
  });
1597
- writer.write(
1598
- agentsMdPath,
1599
- mergeAgentsMd({
1600
- rendered: agentsMdOut,
1601
- existing: existsSync8(agentsMdPath) ? readFileSync7(agentsMdPath, "utf8") : null,
1602
- template: agentsTemplate
1603
- })
1604
- );
1605
- const configTomlPath = join8(projectDir, ".codex/config.toml");
1710
+ const agentsMdPath = join10(projectDir, "AGENTS.md");
1711
+ const seededContext = !writeAgentsMd || refreshOnly || existsSync9(agentsMdPath) ? null : seedAgentsMdProjectContext(projectDir);
1712
+ ensureDir(projectDir);
1713
+ if (writeAgentsMd) {
1714
+ const agentsMdOut = renderAgentsMd({
1715
+ template: agentsTemplate,
1716
+ claudeMd,
1717
+ projectName,
1718
+ // ADR-085 — 상시 스킬 안내는 앵커가 아니라 여기(프로젝트 맥락)에, 깔린 것만.
1719
+ // #503 — 그 조각은 설치자 소유 절 안에 사니 마커로 감싼다.
1720
+ // #530 — `skillIds` 를 쓴다(= 깔린 것 + **이번 실행이 만들 것**).
1721
+ projectContext: withMarkedContinuousSkillsNote(
1722
+ seededContext ?? renderFillScaffold("agents-md"),
1723
+ skillIds
1724
+ ),
1725
+ // Codex 는 룰 디렉터리가 없다 — 룰이 AGENTS.md 본문에 들어가야 도달한다(§Harness Rules).
1726
+ harnessRules: renderRulesBlock(portRules(harnessRoot, rules))
1727
+ });
1728
+ writer.write(
1729
+ agentsMdPath,
1730
+ mergeAgentsMd({
1731
+ rendered: agentsMdOut,
1732
+ existing: existsSync9(agentsMdPath) ? readFileSync8(agentsMdPath, "utf8") : null,
1733
+ template: agentsTemplate
1734
+ })
1735
+ );
1736
+ }
1737
+ const configTomlPath = join10(projectDir, ".codex/config.toml");
1606
1738
  writer.write(
1607
1739
  configTomlPath,
1608
1740
  renderConfigToml({
@@ -1612,15 +1744,15 @@ function runCodexTransform(params) {
1612
1744
  mcp
1613
1745
  })
1614
1746
  );
1615
- const hookDir = join8(projectDir, ".codex/hooks");
1747
+ const hookDir = join10(projectDir, ".codex/hooks");
1616
1748
  const hookFiles = [];
1617
1749
  for (const hook of HOOK_NAMES) {
1618
- const src = join8(harnessRoot, "templates/hooks", `${hook}.sh`);
1619
- if (!existsSync8(src)) {
1750
+ const src = join10(harnessRoot, "templates/hooks", `${hook}.sh`);
1751
+ if (!existsSync9(src)) {
1620
1752
  continue;
1621
1753
  }
1622
- const ported = readFileSync7(src, "utf8").replace(ENV_VAR_RENAME, "CODEX_PROJECT_DIR");
1623
- const target = join8(hookDir, `${hook}.sh`);
1754
+ const ported = readFileSync8(src, "utf8").replace(ENV_VAR_RENAME, "CODEX_PROJECT_DIR");
1755
+ const target = join10(hookDir, `${hook}.sh`);
1624
1756
  if (!writer.write(target, ported)) continue;
1625
1757
  chmodSync2(target, 493);
1626
1758
  hookFiles.push(target);
@@ -1628,11 +1760,12 @@ function runCodexTransform(params) {
1628
1760
  const skillFiles = writeBundledSkillDirs({
1629
1761
  harnessRoot,
1630
1762
  projectDir,
1631
- skillIds: selectedInternalSkills,
1632
- writer
1763
+ skillIds,
1764
+ writer: skillWriter
1633
1765
  });
1634
1766
  return {
1635
1767
  agentsMdPath,
1768
+ agentsMdSeededFrom: seededContext === null ? null : "CLAUDE.md",
1636
1769
  configTomlPath,
1637
1770
  hookFiles,
1638
1771
  skillFiles,
@@ -1640,17 +1773,17 @@ function runCodexTransform(params) {
1640
1773
  };
1641
1774
  }
1642
1775
  function readRequired(path) {
1643
- if (!existsSync8(path)) {
1776
+ if (!existsSync9(path)) {
1644
1777
  throw new Error(`Codex transform: required source missing: ${path}`);
1645
1778
  }
1646
- return readFileSync7(path, "utf8");
1779
+ return readFileSync8(path, "utf8");
1647
1780
  }
1648
1781
  function readOptionalJson(path) {
1649
- if (!existsSync8(path)) {
1782
+ if (!existsSync9(path)) {
1650
1783
  return null;
1651
1784
  }
1652
1785
  try {
1653
- return JSON.parse(readFileSync7(path, "utf8"));
1786
+ return JSON.parse(readFileSync8(path, "utf8"));
1654
1787
  } catch {
1655
1788
  return null;
1656
1789
  }
@@ -1658,8 +1791,8 @@ function readOptionalJson(path) {
1658
1791
 
1659
1792
  // src/opencode/transform.ts
1660
1793
  init_esm_shims();
1661
- import { existsSync as existsSync9, readdirSync, readFileSync as readFileSync8, unlinkSync } from "fs";
1662
- import { basename as basename3, join as join9 } from "path";
1794
+ import { existsSync as existsSync10, readdirSync, readFileSync as readFileSync9, unlinkSync } from "fs";
1795
+ import { basename as basename3, join as join11 } from "path";
1663
1796
 
1664
1797
  // src/opencode/agents-md.ts
1665
1798
  init_esm_shims();
@@ -1702,24 +1835,33 @@ function runOpencodeTransform(params) {
1702
1835
  refreshOnly
1703
1836
  } = params;
1704
1837
  const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
1705
- const claudeMd = readRequired2(join9(harnessRoot, "templates/CLAUDE.md"));
1706
- const agentsTemplate = readRequired2(join9(harnessRoot, "templates/opencode/AGENTS.md.template"));
1838
+ const claudeMd = readRequired2(join11(harnessRoot, "templates/CLAUDE.md"));
1839
+ const agentsTemplate = readRequired2(join11(harnessRoot, "templates/opencode/AGENTS.md.template"));
1707
1840
  const opencodeTemplate = readRequired2(
1708
- join9(harnessRoot, "templates/opencode/opencode.json.template")
1841
+ join11(harnessRoot, "templates/opencode/opencode.json.template")
1709
1842
  );
1710
1843
  const projectName = basename3(projectDir);
1711
- const mcp = readOptionalJson2(join9(harnessRoot, ".mcp.json"));
1844
+ const mcp = readOptionalJson2(join11(harnessRoot, ".mcp.json"));
1845
+ const { skillIds, skillWriter } = agentsSkillSlot({
1846
+ projectDir,
1847
+ selectedInternalSkills,
1848
+ refreshOnly: refreshOnly ?? false,
1849
+ writer
1850
+ });
1712
1851
  ensureDir(projectDir);
1713
- const agentsMdPath = join9(projectDir, "AGENTS.md");
1852
+ const agentsMdPath = join11(projectDir, "AGENTS.md");
1853
+ const seededContext = refreshOnly || existsSync10(agentsMdPath) ? null : seedAgentsMdProjectContext(projectDir);
1714
1854
  const agentsMdOut = renderAgentsMd2({
1715
1855
  template: agentsTemplate,
1716
1856
  claudeMd,
1717
1857
  projectName,
1718
1858
  // ADR-085 — 상시 스킬 안내는 앵커가 아니라 여기(프로젝트 맥락)에, 깔린 것만.
1719
1859
  // #503 — 그 조각은 설치자 소유 절 안에 사니 마커로 감싼다 (codex 와 같은 마커).
1860
+ // #531 — `skillIds` 를 쓴다(= 깔린 것 + **이번 실행이 만들 것**). 안 그러면 이번에 새로
1861
+ // 깐 상시 스킬이 다음 update 까지 안내에서 빠져 설치자 에이전트가 영영 안 연다.
1720
1862
  projectContext: withMarkedContinuousSkillsNote(
1721
- renderFillScaffold("agents-md"),
1722
- selectedInternalSkills
1863
+ seededContext ?? renderFillScaffold("agents-md"),
1864
+ skillIds
1723
1865
  ),
1724
1866
  // codex 와 **같은 파일**(프로젝트 루트 `AGENTS.md`)이다. 두 transform 이 서로 다른 본문을
1725
1867
  // 쓰면 나중에 도는 쪽이 앞선 쪽을 덮어써, codex+opencode 조합에서 룰이 통째로 사라진다
@@ -1730,26 +1872,26 @@ function runOpencodeTransform(params) {
1730
1872
  agentsMdPath,
1731
1873
  mergeAgentsMd({
1732
1874
  rendered: agentsMdOut,
1733
- existing: existsSync9(agentsMdPath) ? readFileSync8(agentsMdPath, "utf8") : null,
1875
+ existing: existsSync10(agentsMdPath) ? readFileSync9(agentsMdPath, "utf8") : null,
1734
1876
  template: agentsTemplate
1735
1877
  })
1736
1878
  );
1737
- const opencodeJsonPath = join9(projectDir, "opencode.json");
1879
+ const opencodeJsonPath = join11(projectDir, "opencode.json");
1738
1880
  writer.write(opencodeJsonPath, renderOpencodeJson({ template: opencodeTemplate, mcp }));
1739
1881
  const skillFiles = writeBundledSkillDirs({
1740
1882
  harnessRoot,
1741
1883
  projectDir,
1742
- skillIds: selectedInternalSkills,
1743
- writer
1884
+ skillIds,
1885
+ writer: skillWriter
1744
1886
  });
1745
1887
  const retiredCommands = [];
1746
- const cmdDir = join9(projectDir, ".opencode/commands");
1747
- if (existsSync9(cmdDir)) {
1888
+ const cmdDir = join11(projectDir, ".opencode/commands");
1889
+ if (existsSync10(cmdDir)) {
1748
1890
  for (const entry of readdirSync(cmdDir)) {
1749
1891
  if (!entry.endsWith(".md")) continue;
1750
1892
  const id = entry.slice(0, -3);
1751
- if (!existsSync9(join9(harnessRoot, "templates/skills", id, "SKILL.md"))) continue;
1752
- const victim = join9(cmdDir, entry);
1893
+ if (!existsSync10(join11(harnessRoot, "templates/skills", id, "SKILL.md"))) continue;
1894
+ const victim = join11(cmdDir, entry);
1753
1895
  try {
1754
1896
  backupFile(victim);
1755
1897
  unlinkSync(victim);
@@ -1760,6 +1902,7 @@ function runOpencodeTransform(params) {
1760
1902
  }
1761
1903
  return {
1762
1904
  agentsMdPath,
1905
+ agentsMdSeededFrom: seededContext === null ? null : "CLAUDE.md",
1763
1906
  opencodeJsonPath,
1764
1907
  skillFiles,
1765
1908
  retiredCommands,
@@ -1767,17 +1910,17 @@ function runOpencodeTransform(params) {
1767
1910
  };
1768
1911
  }
1769
1912
  function readRequired2(path) {
1770
- if (!existsSync9(path)) {
1913
+ if (!existsSync10(path)) {
1771
1914
  throw new Error(`OpenCode transform: required source missing: ${path}`);
1772
1915
  }
1773
- return readFileSync8(path, "utf8");
1916
+ return readFileSync9(path, "utf8");
1774
1917
  }
1775
1918
  function readOptionalJson2(path) {
1776
- if (!existsSync9(path)) {
1919
+ if (!existsSync10(path)) {
1777
1920
  return null;
1778
1921
  }
1779
1922
  try {
1780
- return JSON.parse(readFileSync8(path, "utf8"));
1923
+ return JSON.parse(readFileSync9(path, "utf8"));
1781
1924
  } catch {
1782
1925
  return null;
1783
1926
  }
@@ -1823,7 +1966,11 @@ function runCliTransforms(params) {
1823
1966
  selectedInternalSkills,
1824
1967
  rules,
1825
1968
  baseline,
1826
- refreshOnly
1969
+ refreshOnly,
1970
+ // #550 — 같은 `AGENTS.md` 를 한 실행에서 쓰는 쪽은 하나다. opencode 가 뒤에서 자기 템플릿으로
1971
+ // 쓰면 codex 판은 어차피 같은 실행 안에서 덮였고(최종 파일 = opencode 판), 매 실행 codex 가
1972
+ // opencode 판을 자기 판으로 뒤집는 한 번의 쓰기가 설치자 편집분을 백업으로 쌓았다.
1973
+ writeAgentsMd: !cli2.includes("opencode")
1827
1974
  });
1828
1975
  absorb(codex);
1829
1976
  if (codexTrust) {
@@ -1869,8 +2016,8 @@ function runCliTransforms(params) {
1869
2016
 
1870
2017
  // src/env-files.ts
1871
2018
  init_esm_shims();
1872
- import { appendFileSync, existsSync as existsSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync3 } from "fs";
1873
- import { join as join10 } from "path";
2019
+ import { appendFileSync, existsSync as existsSync11, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "fs";
2020
+ import { join as join12 } from "path";
1874
2021
  var ENV_EXAMPLE_BODY = `# .env.example \u2014 csr-supabase Track
1875
2022
  # Copy to .env (gitignored) and fill in values: cp .env.example .env
1876
2023
 
@@ -1907,24 +2054,24 @@ function writeEnvExample(projectDir, tracks) {
1907
2054
  if (!tracks.some((t) => ENV_EXAMPLE_TRACKS.includes(t))) {
1908
2055
  return false;
1909
2056
  }
1910
- const path = join10(projectDir, ".env.example");
1911
- if (existsSync10(path)) {
2057
+ const path = join12(projectDir, ".env.example");
2058
+ if (existsSync11(path)) {
1912
2059
  return false;
1913
2060
  }
1914
2061
  writeFileSync3(path, ENV_EXAMPLE_BODY);
1915
2062
  return true;
1916
2063
  }
1917
2064
  function addGitignoreEnv(projectDir) {
1918
- const path = join10(projectDir, ".gitignore");
1919
- if (!existsSync10(path)) {
2065
+ const path = join12(projectDir, ".gitignore");
2066
+ if (!existsSync11(path)) {
1920
2067
  return false;
1921
2068
  }
1922
- const content = readFileSync9(path, "utf8");
2069
+ const content = readFileSync10(path, "utf8");
1923
2070
  if (GITIGNORE_ENV_PATTERN.test(content)) {
1924
2071
  return false;
1925
2072
  }
1926
- const sep3 = content.endsWith("\n") ? "" : "\n";
1927
- appendFileSync(path, `${sep3}
2073
+ const sep4 = content.endsWith("\n") ? "" : "\n";
2074
+ appendFileSync(path, `${sep4}
1928
2075
  # Secret env (auto-added by agent-harness install)
1929
2076
  .env
1930
2077
  `);
@@ -1933,11 +2080,11 @@ function addGitignoreEnv(projectDir) {
1933
2080
  var AGENT_ARTIFACT_DIRS = [".factory/", ".goose/", ".uzys-agent-harness/"];
1934
2081
  var GITIGNORE_AGENT_ARTIFACT_HEADER = "# agent CLI / harness \uC790\uB3D9 \uC0DD\uC131\uBB3C (auto-added by agent-harness)";
1935
2082
  function addGitignoreAgentArtifacts(projectDir) {
1936
- const path = join10(projectDir, ".gitignore");
1937
- if (!existsSync10(path)) {
2083
+ const path = join12(projectDir, ".gitignore");
2084
+ if (!existsSync11(path)) {
1938
2085
  return [];
1939
2086
  }
1940
- const content = readFileSync9(path, "utf8");
2087
+ const content = readFileSync10(path, "utf8");
1941
2088
  const missing = AGENT_ARTIFACT_DIRS.filter((pattern) => {
1942
2089
  const lineRegex = new RegExp(`^${pattern.replace(/\./g, "\\.").replace(/\//g, "/")}\\s*$`, "m");
1943
2090
  return !lineRegex.test(content);
@@ -1945,9 +2092,9 @@ function addGitignoreAgentArtifacts(projectDir) {
1945
2092
  if (missing.length === 0) {
1946
2093
  return [];
1947
2094
  }
1948
- const sep3 = content.endsWith("\n") ? "" : "\n";
2095
+ const sep4 = content.endsWith("\n") ? "" : "\n";
1949
2096
  const block = [GITIGNORE_AGENT_ARTIFACT_HEADER, ...missing].join("\n");
1950
- appendFileSync(path, `${sep3}
2097
+ appendFileSync(path, `${sep4}
1951
2098
  ${block}
1952
2099
  `);
1953
2100
  return [...missing];
@@ -1956,9 +2103,9 @@ ${block}
1956
2103
  // src/external-installer.ts
1957
2104
  init_esm_shims();
1958
2105
  import { spawnSync } from "child_process";
1959
- import { existsSync as existsSync11, readdirSync as readdirSync2, readFileSync as readFileSync10 } from "fs";
2106
+ import { existsSync as existsSync12, readdirSync as readdirSync2, readFileSync as readFileSync11 } from "fs";
1960
2107
  import { homedir as homedir2 } from "os";
1961
- import { join as join11 } from "path";
2108
+ import { join as join13 } from "path";
1962
2109
  var DEFAULT_SPAWN_TIMEOUT_MS = 12e4;
1963
2110
  var NPM_SPAWN_TIMEOUT_MS = 6e5;
1964
2111
  function spawnTimeoutFor(kind) {
@@ -2070,7 +2217,10 @@ function refreshExternalSkills(projectDir, deps = {}) {
2070
2217
  tracks: log.spec.tracks,
2071
2218
  // `forceInclude` 가 조건 판정을 앞지르므로 옵션 값은 결과에 영향을 주지 않는다.
2072
2219
  options: DEFAULT_OPTIONS,
2073
- cli: log.spec.cli,
2220
+ // #528 — 대상 CLI 는 **깔린 집합**(`installedClis`)이다. `spec.cli` 는 마지막 install 의
2221
+ // 요청이라 `uninstall --cli` 뒤에도 뺀 CLI 를 말하고, 반대로 그걸 비우면 `buildSkillArgs` 가
2222
+ // `--agent`·`--copy` 를 못 붙여 Claude 몫이 조용히 빠진다(재리뷰 BLOCKER-3).
2223
+ cli: [...installedClis(log)],
2074
2224
  userOverride: { forceInclude: [...installedIds], forceExclude: [] },
2075
2225
  scope: log.scope,
2076
2226
  projectDir
@@ -2166,17 +2316,17 @@ function detectVersion(method, spawn) {
2166
2316
  if (at2 <= 0) return void 0;
2167
2317
  const plugin = method.pluginId.slice(0, at2);
2168
2318
  const marketplaceShort = method.pluginId.slice(at2 + 1);
2169
- const cacheBase = join11(homedir2(), ".claude/plugins/cache", marketplaceShort, plugin);
2170
- if (!existsSync11(cacheBase)) return void 0;
2319
+ const cacheBase = join13(homedir2(), ".claude/plugins/cache", marketplaceShort, plugin);
2320
+ if (!existsSync12(cacheBase)) return void 0;
2171
2321
  const versions = readdirSync2(cacheBase).filter((v2) => /^\d/.test(v2)).sort();
2172
2322
  return versions.at(-1);
2173
2323
  }
2174
2324
  case "npm": {
2175
2325
  const npmRoot = getNpmGlobalRoot(spawn);
2176
2326
  if (!npmRoot) return void 0;
2177
- const pkgJson = join11(npmRoot, method.pkg, "package.json");
2178
- if (!existsSync11(pkgJson)) return void 0;
2179
- const parsed = JSON.parse(readFileSync10(pkgJson, "utf8"));
2327
+ const pkgJson = join13(npmRoot, method.pkg, "package.json");
2328
+ if (!existsSync12(pkgJson)) return void 0;
2329
+ const parsed = JSON.parse(readFileSync11(pkgJson, "utf8"));
2180
2330
  return parsed.version;
2181
2331
  }
2182
2332
  default:
@@ -2201,9 +2351,30 @@ function getNpmGlobalRoot(spawn) {
2201
2351
  return void 0;
2202
2352
  }
2203
2353
 
2354
+ // src/linked-skill-bodies.ts
2355
+ init_esm_shims();
2356
+ import { join as join14 } from "path";
2357
+ function refreshLinkedSkillBodies(params) {
2358
+ const { harnessRoot, projectDir, ids, baseline } = params;
2359
+ const bodyKey = (id) => `.agents/skills/${id}/SKILL.md`;
2360
+ const ours = ids.filter((id) => baseline.has(bodyKey(id)));
2361
+ const writer = createOwnedWriter(projectDir, baseline);
2362
+ const written = new Set(
2363
+ writeBundledSkillDirs({ harnessRoot, projectDir, skillIds: ours, writer })
2364
+ );
2365
+ const result = writer.result();
2366
+ return {
2367
+ updated: ours.filter((id) => written.has(join14(projectDir, bodyKey(id)))),
2368
+ notOurs: ids.filter((id) => !baseline.has(bodyKey(id))),
2369
+ files: result.files,
2370
+ backupPaths: result.backupPaths,
2371
+ foreignOwned: result.foreignOwned
2372
+ };
2373
+ }
2374
+
2204
2375
  // src/mcp-merge.ts
2205
2376
  init_esm_shims();
2206
- import { existsSync as existsSync12, readFileSync as readFileSync11, writeFileSync as writeFileSync4 } from "fs";
2377
+ import { existsSync as existsSync13, readFileSync as readFileSync12, writeFileSync as writeFileSync4 } from "fs";
2207
2378
  function parseTrackMcpMap(raw) {
2208
2379
  const rows = [];
2209
2380
  for (const line of raw.split(/\r?\n/)) {
@@ -2254,15 +2425,15 @@ function mergeMcpServers(base, rows, tracks) {
2254
2425
  return out;
2255
2426
  }
2256
2427
  function composeMcpJson(opts) {
2257
- const base = JSON.parse(readFileSync11(opts.templateMcpPath, "utf8"));
2258
- const merged = opts.existingPath && existsSync12(opts.existingPath) ? mergeUserBase(base, opts.existingPath) : base;
2259
- const mapRaw = existsSync12(opts.trackMapPath) ? readFileSync11(opts.trackMapPath, "utf8") : "";
2428
+ const base = JSON.parse(readFileSync12(opts.templateMcpPath, "utf8"));
2429
+ const merged = opts.existingPath && existsSync13(opts.existingPath) ? mergeUserBase(base, opts.existingPath) : base;
2430
+ const mapRaw = existsSync13(opts.trackMapPath) ? readFileSync12(opts.trackMapPath, "utf8") : "";
2260
2431
  const rows = parseTrackMcpMap(mapRaw);
2261
2432
  return mergeMcpServers(merged, rows, opts.tracks);
2262
2433
  }
2263
2434
  function mergeUserBase(base, existingPath) {
2264
2435
  try {
2265
- const existing = JSON.parse(readFileSync11(existingPath, "utf8"));
2436
+ const existing = JSON.parse(readFileSync12(existingPath, "utf8"));
2266
2437
  return {
2267
2438
  ...base,
2268
2439
  mcpServers: { ...base.mcpServers, ...existing.mcpServers }
@@ -2280,24 +2451,26 @@ function writeMcpJson(path, mcp) {
2280
2451
  init_esm_shims();
2281
2452
  import {
2282
2453
  copyFileSync,
2283
- existsSync as existsSync13,
2454
+ existsSync as existsSync14,
2284
2455
  mkdirSync as mkdirSync3,
2285
2456
  readdirSync as readdirSync3,
2286
- readFileSync as readFileSync12,
2457
+ readFileSync as readFileSync13,
2287
2458
  rmdirSync,
2288
- statSync,
2459
+ statSync as statSync2,
2289
2460
  unlinkSync as unlinkSync2,
2290
2461
  writeFileSync as writeFileSync5
2291
2462
  } from "fs";
2292
- import { basename as basename4, dirname as dirname3, join as join12 } from "path";
2463
+ import { basename as basename4, dirname as dirname3, join as join15 } from "path";
2293
2464
  var LEGACY_ANCHOR_FILE = ".claude/CLAUDE.md";
2294
2465
  function buildUpdateSpec(projectDir, tracks, only) {
2466
+ const log = readInstallLog(projectDir);
2467
+ const clis = log === null ? [] : installedClis(log);
2295
2468
  return {
2296
2469
  tracks: [...tracks],
2297
2470
  options: DEFAULT_OPTIONS,
2298
- cli: ["claude"],
2471
+ cli: clis.length > 0 ? [...clis] : ["claude"],
2299
2472
  projectDir,
2300
- scope: readInstallLog(projectDir)?.scope ?? "project",
2473
+ scope: log?.scope ?? "project",
2301
2474
  // 전부 골랐으면 "제한 없음"과 같다 — 화면·기록에 제한이 있었던 것처럼 남기지 않는다.
2302
2475
  ...only !== void 0 && only.length > 0 && only.length < UPDATE_GROUPS.length ? { updateOnly: [...only] } : {}
2303
2476
  };
@@ -2308,7 +2481,7 @@ function parseUpdateOnly(raw) {
2308
2481
  return { ok: true, groups: [...new Set(raw)] };
2309
2482
  }
2310
2483
  function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
2311
- const claudeDir = join12(projectDir, ".claude");
2484
+ const claudeDir = join15(projectDir, ".claude");
2312
2485
  const startedAt = Date.now();
2313
2486
  const wants = (g) => only === void 0 || only.includes(g);
2314
2487
  const report = {
@@ -2323,6 +2496,7 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
2323
2496
  rootImportAdded: false,
2324
2497
  rootBlockRefreshed: false,
2325
2498
  legacyAnchor: null,
2499
+ claudeUnrecorded: null,
2326
2500
  skillsBackedUp: [],
2327
2501
  skillsSkippedLinks: [],
2328
2502
  skillsPruned: [],
@@ -2345,11 +2519,14 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
2345
2519
  report.installedNew = fresh.installed;
2346
2520
  report.restored = fresh.restored;
2347
2521
  report.needsReinstall = fresh.needsReinstall;
2522
+ const logAtStart = readInstallLog(projectDir);
2523
+ const claudeManaged = logAtStart === null || installedClis(logAtStart).includes("claude");
2524
+ report.claudeUnrecorded = !claudeManaged && logAtStart !== null && existsSync14(claudeDir) ? recordClaudeCommand(logAtStart, installedTracks(projectDir)) : null;
2348
2525
  const policyBase = policyBaseline(projectDir);
2349
2526
  for (const { dir, ext } of POLICY_DIRS) {
2350
- if (!wants(dir === "hooks" ? "hooks" : "rules")) continue;
2351
- const target = join12(claudeDir, dir);
2352
- const source = join12(templatesDir, dir);
2527
+ if (!claudeManaged || !wants(dir === "hooks" ? "hooks" : "rules")) continue;
2528
+ const target = join15(claudeDir, dir);
2529
+ const source = join15(templatesDir, dir);
2353
2530
  const label = `.claude/${dir}`;
2354
2531
  const ctx = { prefix: dir, baseline: policyBase };
2355
2532
  const synced = updateDir(target, source, ext, ctx);
@@ -2360,10 +2537,11 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
2360
2537
  const syncedDirs = POLICY_DIRS.filter((d2) => wants(d2.dir === "hooks" ? "hooks" : "rules")).map(
2361
2538
  (d2) => d2.dir
2362
2539
  );
2363
- if (syncedDirs.length > 0) refreshPolicyBaseline(projectDir, templatesDir, syncedDirs);
2364
- const skillSync = wants("skills") ? syncSkills(
2365
- join12(claudeDir, "skills"),
2366
- join12(templatesDir, "skills"),
2540
+ if (claudeManaged && syncedDirs.length > 0)
2541
+ refreshPolicyBaseline(projectDir, templatesDir, syncedDirs);
2542
+ const skillSync = claudeManaged && wants("skills") ? syncSkills(
2543
+ join15(claudeDir, "skills"),
2544
+ join15(templatesDir, "skills"),
2367
2545
  skillBaseline(projectDir),
2368
2546
  /* @__PURE__ */ new Date(),
2369
2547
  (relInSkills) => foreignOwnedTarget(projectDir, `.claude/skills/${relInSkills}`)
@@ -2372,25 +2550,36 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
2372
2550
  report.skillsBackedUp = skillSync.backedUp;
2373
2551
  report.skillsSkippedLinks = skillSync.skippedLinks;
2374
2552
  report.skillsPruned = skillSync.pruned;
2375
- const newSkills = wants("new-skills") ? installNewSkillDirs(projectDir, templatesDir, installedTracks(projectDir)) : { installed: [], foreignOwned: [] };
2553
+ const newSkills = claudeManaged && wants("new-skills") ? installNewSkillDirs(projectDir, templatesDir, installedTracks(projectDir)) : { installed: [], restored: [], foreignOwned: [] };
2376
2554
  report.installedNew.push(...newSkills.installed);
2377
- if (wants("skills")) refreshSkillBaseline(projectDir, templatesDir);
2378
- else if (newSkills.installed.length > 0)
2379
- recordNewSkillBaseline(projectDir, templatesDir, newSkills.installed);
2380
- const installLog = readInstallLog(projectDir);
2381
- if (existsSync13(claudeDir) && (installLog === null || installLog.spec.cli.includes("claude"))) {
2555
+ report.restored.push(...newSkills.restored);
2556
+ const copiedSkills = [...newSkills.installed, ...newSkills.restored];
2557
+ if (claudeManaged && wants("skills")) refreshSkillBaseline(projectDir, templatesDir);
2558
+ else if (claudeManaged && copiedSkills.length > 0)
2559
+ recordNewSkillBaseline(projectDir, templatesDir, copiedSkills);
2560
+ if (existsSync14(claudeDir) && claudeManaged) {
2382
2561
  if (wants("anchor")) syncHarnessAnchor(projectDir, templatesDir, report);
2383
2562
  }
2384
- const settingsPath = join12(claudeDir, "settings.json");
2385
- if (wants("hooks") && existsSync13(settingsPath)) {
2563
+ const settingsPath = join15(claudeDir, "settings.json");
2564
+ if (claudeManaged && wants("hooks") && existsSync14(settingsPath)) {
2386
2565
  report.staleHookRefs = cleanStaleHookRefs(settingsPath, claudeDir);
2387
2566
  }
2388
2567
  if (wants("hooks")) report.mcpAllowlistRetired = retireMcpAllowlist(projectDir);
2389
2568
  report.retiredAgents = staleAgentFiles(claudeDir);
2390
2569
  report.demotedAgents = demotedAgentFiles(claudeDir, installedTracks(projectDir));
2391
- const external = wants("external") ? refreshExternalCli(projectDir, harnessRoot) : { externalUpdated: 0, externalBackedUp: [], externalForeignOwned: [] };
2570
+ const external = wants("external") ? refreshExternalCli(projectDir, harnessRoot) : {
2571
+ externalUpdated: 0,
2572
+ externalBackedUp: [],
2573
+ externalForeignOwned: [],
2574
+ written: [],
2575
+ skillsInstalled: [],
2576
+ skillsRestored: []
2577
+ };
2392
2578
  report.externalUpdated = external.externalUpdated;
2393
2579
  report.externalBackedUp = external.externalBackedUp;
2580
+ report.installedNew.push(...external.skillsInstalled);
2581
+ report.restored.push(...external.skillsRestored);
2582
+ report.restoredWithout = restoredWithoutArgs(report.restored, installedTracks(projectDir));
2394
2583
  const saidBySlotRow = new Set(report.skillsSkippedLinks.map((id) => `.claude/skills/${id}`));
2395
2584
  const merged = [];
2396
2585
  for (const f of [
@@ -2402,6 +2591,17 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
2402
2591
  if (!saidBySlotRow.has(f) && !merged.includes(f)) merged.push(f);
2403
2592
  }
2404
2593
  report.foreignOwned = merged;
2594
+ if (wants("external")) {
2595
+ const linked = report.skillsSkippedLinks.filter(
2596
+ (id) => linksToProjectSharedSkill(projectDir, id)
2597
+ );
2598
+ const refreshed = linked.filter(
2599
+ (id) => external.written.includes(`.agents/skills/${id}/SKILL.md`)
2600
+ );
2601
+ report.skillsLinked = refreshed;
2602
+ report.skillsLinkedNotOurs = linked.filter((id) => !refreshed.includes(id));
2603
+ report.skillsSkippedLinks = report.skillsSkippedLinks.filter((id) => !linked.includes(id));
2604
+ }
2405
2605
  const skillRefresh = wants("external") ? (deps.refreshSkills ?? refreshExternalSkills)(projectDir) : { attempted: 0, refreshed: 0, failed: [], notInCatalog: [], unknown: false };
2406
2606
  report.externalSkillsRefreshed = skillRefresh.refreshed;
2407
2607
  report.externalSkillsFailed = skillRefresh.failed;
@@ -2415,20 +2615,20 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
2415
2615
  return report;
2416
2616
  }
2417
2617
  function staleSkillDirs(claudeDir) {
2418
- const dir = join12(claudeDir, "skills");
2419
- if (!existsSync13(dir)) return [];
2618
+ const dir = join15(claudeDir, "skills");
2619
+ if (!existsSync14(dir)) return [];
2420
2620
  const stale = (id) => RENAMED_SKILL_IDS[id] !== void 0 || RETIRED_SKILL_IDS.includes(id);
2421
2621
  return readdirSync3(dir, { withFileTypes: true }).filter((e2) => e2.isDirectory() && stale(e2.name)).map((e2) => e2.name);
2422
2622
  }
2423
2623
  function staleAgentFiles(claudeDir) {
2424
- const dir = join12(claudeDir, "agents");
2425
- if (!existsSync13(dir)) return [];
2624
+ const dir = join15(claudeDir, "agents");
2625
+ if (!existsSync14(dir)) return [];
2426
2626
  return readdirSync3(dir, { withFileTypes: true }).filter((e2) => e2.isFile() && e2.name.endsWith(".md")).map((e2) => basename4(e2.name, ".md")).filter((id) => RETIRED_AGENT_IDS.includes(id));
2427
2627
  }
2428
2628
  function demotedAgentFiles(claudeDir, tracks) {
2429
2629
  if (tracks.length === 0) return [];
2430
- const dir = join12(claudeDir, "agents");
2431
- if (!existsSync13(dir)) return [];
2630
+ const dir = join15(claudeDir, "agents");
2631
+ if (!existsSync14(dir)) return [];
2432
2632
  const demoted = TRACK_AGENTS.filter(([, pattern]) => !anyTrack(tracks, pattern)).map(
2433
2633
  ([id]) => id
2434
2634
  );
@@ -2440,7 +2640,7 @@ function installNewAssets(projectDir, templatesDir, wants = () => true) {
2440
2640
  const needsReinstall = [];
2441
2641
  const foreignOwned = [];
2442
2642
  const log = readInstallLog(projectDir);
2443
- const claudeSelected = log?.spec.cli.includes("claude") ?? false;
2643
+ const claudeSelected = installedClis(log).includes("claude");
2444
2644
  const baselineExcluded = new Set(log?.spec.baselineExclude ?? []);
2445
2645
  const priorBaseline = policyBaseline(projectDir);
2446
2646
  for (const entry of trackOnlyFileAssets(installedTracks(projectDir))) {
@@ -2448,15 +2648,15 @@ function installNewAssets(projectDir, templatesDir, wants = () => true) {
2448
2648
  if (!wants(group)) continue;
2449
2649
  if (entry.target === HARNESS_ANCHOR_FILE) continue;
2450
2650
  if (isBaselineExcluded(entry.target, baselineExcluded)) continue;
2451
- const target = join12(projectDir, entry.target);
2651
+ const target = join15(projectDir, entry.target);
2452
2652
  const foreign = foreignOwnedTarget(projectDir, entry.target);
2453
2653
  if (foreign !== null) {
2454
2654
  if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
2455
2655
  continue;
2456
2656
  }
2457
- if (existsSync13(target)) continue;
2458
- const source = join12(templatesDir, entry.source);
2459
- if (!existsSync13(source)) continue;
2657
+ if (existsSync14(target)) continue;
2658
+ const source = join15(templatesDir, entry.source);
2659
+ if (!existsSync14(source)) continue;
2460
2660
  if (entry.target.startsWith(".claude/")) {
2461
2661
  if (!claudeSelected) continue;
2462
2662
  if (entry.target.startsWith(".claude/hooks/") || entry.target === ".claude/settings.json") {
@@ -2478,26 +2678,26 @@ function collectRunBackups(projectDir, startedAt) {
2478
2678
  const out = [];
2479
2679
  const consider = (rel) => {
2480
2680
  if (!BACKUP_SUFFIX.test(rel)) return;
2481
- const abs = join12(projectDir, rel);
2482
- if (statSync(abs).mtimeMs < startedAt - 1e3) return;
2681
+ const abs = join15(projectDir, rel);
2682
+ if (statSync2(abs).mtimeMs < startedAt - 1e3) return;
2483
2683
  out.push({ path: rel.replace(BACKUP_SUFFIX, ""), backup: rel });
2484
2684
  };
2485
2685
  for (const e2 of readdirSync3(projectDir, { withFileTypes: true }))
2486
2686
  if (e2.isFile()) consider(e2.name);
2487
2687
  for (const dir of BACKUP_SCAN_DIRS) {
2488
- for (const rel of listFilesRecursive(join12(projectDir, dir))) consider(`${dir}/${rel}`);
2688
+ for (const rel of listFilesRecursive(join15(projectDir, dir))) consider(`${dir}/${rel}`);
2489
2689
  }
2490
2690
  return out.sort((a, b2) => a.path.localeCompare(b2.path));
2491
2691
  }
2492
2692
  function writeBackupList(projectDir, backups) {
2493
- const dir = join12(projectDir, ".uzys-agent-harness");
2494
- const file = join12(dir, "update-backups.json");
2693
+ const dir = join15(projectDir, ".uzys-agent-harness");
2694
+ const file = join15(dir, "update-backups.json");
2495
2695
  try {
2496
2696
  if (backups.length === 0) {
2497
- if (existsSync13(file)) unlinkSync2(file);
2697
+ if (existsSync14(file)) unlinkSync2(file);
2498
2698
  return;
2499
2699
  }
2500
- if (!existsSync13(dir)) return;
2700
+ if (!existsSync14(dir)) return;
2501
2701
  writeFileSync5(
2502
2702
  file,
2503
2703
  `${JSON.stringify({ updatedAt: (/* @__PURE__ */ new Date()).toISOString(), note: "update \uAC00 \uB0A8\uAE34 \uBC31\uC5C5 \uC30D. \uC6D0\uBCF8(path)\uC740 \uCD5C\uC2E0 \uBC88\uB4E4\uD310, \uBC31\uC5C5(backup)\uC740 \uB2F9\uC2E0\uC758 \uD3B8\uC9D1\uBD84. \uD3B8\uC9D1\uC744 \uC0C8 \uD310\uC5D0 \uB2E4\uC2DC \uC5B9\uC73C\uB824\uBA74 \uB450 \uD30C\uC77C\uC758 diff \uB97C \uBCF4\uB77C.", backups }, null, 2)}
@@ -2508,29 +2708,33 @@ function writeBackupList(projectDir, backups) {
2508
2708
  }
2509
2709
  function installNewSkillDirs(projectDir, templatesDir, tracks) {
2510
2710
  const installed = [];
2711
+ const restored = [];
2511
2712
  const foreignOwned = [];
2512
2713
  const log = readInstallLog(projectDir);
2714
+ const priorIds = new Set((log?.skillFiles ?? []).map((f) => f.path.split("/")[0] ?? ""));
2513
2715
  const excluded = new Set(log?.spec.baselineExclude ?? []);
2514
2716
  const skillExcluded = new Set(log?.spec.skillExclude ?? []);
2515
- if (!(log?.spec.cli ?? ["claude"]).includes("claude")) return { installed, foreignOwned };
2717
+ if (log !== null && !installedClis(log).includes("claude"))
2718
+ return { installed, restored, foreignOwned };
2516
2719
  const spec = buildAssetSpec({ tracks, options: DEFAULT_OPTIONS });
2517
2720
  for (const entry of buildManifest(spec)) {
2518
2721
  if (entry.type !== "dir" || !entry.target.startsWith(".claude/skills/")) continue;
2519
2722
  if (!entry.applies(spec) || isBaselineExcluded(entry.target, excluded)) continue;
2520
2723
  if (skillExcluded.has(entry.target.slice(".claude/skills/".length))) continue;
2521
- const target = join12(projectDir, entry.target);
2724
+ const target = join15(projectDir, entry.target);
2522
2725
  const foreign = foreignOwnedTarget(projectDir, entry.target);
2523
2726
  if (foreign !== null) {
2524
2727
  if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
2525
2728
  continue;
2526
2729
  }
2527
- if (existsSync13(target)) continue;
2528
- const source = join12(templatesDir, entry.source);
2529
- if (!existsSync13(source)) continue;
2730
+ if (existsSync14(target)) continue;
2731
+ const source = join15(templatesDir, entry.source);
2732
+ if (!existsSync14(source)) continue;
2530
2733
  copyDir(source, target, (rel) => foreignOwnedTarget(projectDir, `${entry.target}/${rel}`));
2531
- installed.push(entry.target);
2734
+ if (priorIds.has(entry.target.slice(".claude/skills/".length))) restored.push(entry.target);
2735
+ else installed.push(entry.target);
2532
2736
  }
2533
- return { installed, foreignOwned };
2737
+ return { installed, restored, foreignOwned };
2534
2738
  }
2535
2739
  function recordNewSkillBaseline(projectDir, templatesDir, targets) {
2536
2740
  const log = readInstallLog(projectDir);
@@ -2554,16 +2758,16 @@ function trackOnlyFileAssets(tracks) {
2554
2758
  );
2555
2759
  }
2556
2760
  function syncHarnessAnchor(projectDir, templatesDir, report, now = /* @__PURE__ */ new Date()) {
2557
- if (existsSync13(join12(projectDir, LEGACY_ANCHOR_FILE))) {
2761
+ if (existsSync14(join15(projectDir, LEGACY_ANCHOR_FILE))) {
2558
2762
  report.legacyAnchor = LEGACY_ANCHOR_FILE;
2559
2763
  }
2560
- const templateMd = join12(templatesDir, "CLAUDE.md");
2561
- if (!existsSync13(templateMd)) return;
2562
- const anchor = join12(projectDir, HARNESS_ANCHOR_FILE);
2563
- const existed = existsSync13(anchor);
2764
+ const templateMd = join15(templatesDir, "CLAUDE.md");
2765
+ if (!existsSync14(templateMd)) return;
2766
+ const anchor = join15(projectDir, HARNESS_ANCHOR_FILE);
2767
+ const existed = existsSync14(anchor);
2564
2768
  if (existed) {
2565
- const current = readFileSync12(anchor, "utf8");
2566
- if (current !== readFileSync12(templateMd, "utf8")) {
2769
+ const current = readFileSync13(anchor, "utf8");
2770
+ if (current !== readFileSync13(templateMd, "utf8")) {
2567
2771
  const recorded = readInstallLog(projectDir)?.templates.rootClaudeMd;
2568
2772
  const owned = recorded?.path === HARNESS_ANCHOR_FILE && hashContent(current) === recorded.sha256;
2569
2773
  if (!owned) {
@@ -2583,15 +2787,15 @@ function syncHarnessAnchor(projectDir, templatesDir, report, now = /* @__PURE__
2583
2787
  recordAnchorBaseline(projectDir, anchor);
2584
2788
  }
2585
2789
  function upsertRootImport(projectDir) {
2586
- const target = join12(projectDir, "CLAUDE.md");
2587
- const existing = existsSync13(target) ? readFileSync12(target, "utf8") : null;
2790
+ const target = join15(projectDir, "CLAUDE.md");
2791
+ const existing = existsSync14(target) ? readFileSync13(target, "utf8") : null;
2588
2792
  const next = upsertHarnessImport(existing, {
2589
2793
  projectName: basename4(projectDir),
2590
2794
  tracks: installedTracks(projectDir),
2591
2795
  // ADR-085 — update 는 선택 목록을 인자로 받지 않는다. `syncSkills` 와 같은 판정을 쓴다:
2592
2796
  // `.claude/skills/<id>` 가 있으면 그 스킬은 이 설치가 고른 것이다.
2593
2797
  continuousSkills: CONTINUOUS_SKILLS.map((s) => s.id).filter(
2594
- (id) => existsSync13(join12(projectDir, ".claude/skills", id))
2798
+ (id) => existsSync14(join15(projectDir, ".claude/skills", id))
2595
2799
  )
2596
2800
  });
2597
2801
  if (next === existing) return false;
@@ -2611,7 +2815,7 @@ function recordAnchorBaseline(projectDir, anchor) {
2611
2815
  ...log.templates,
2612
2816
  rootClaudeMd: {
2613
2817
  path: HARNESS_ANCHOR_FILE,
2614
- sha256: hashContent(readFileSync12(anchor, "utf8"))
2818
+ sha256: hashContent(readFileSync13(anchor, "utf8"))
2615
2819
  }
2616
2820
  }
2617
2821
  };
@@ -2622,16 +2826,45 @@ function recordAnchorBaseline(projectDir, anchor) {
2622
2826
  }
2623
2827
  function installedBundledSkills(projectDir) {
2624
2828
  return INTERNAL_BUNDLED_SKILL_IDS.filter(
2625
- (id) => existsSync13(join12(projectDir, ".claude/skills", id)) || existsSync13(join12(projectDir, ".agents/skills", id))
2829
+ (id) => existsSync14(join15(projectDir, ".claude/skills", id)) || existsSync14(join15(projectDir, ".agents/skills", id))
2626
2830
  );
2627
2831
  }
2832
+ function installedCliTargets(log) {
2833
+ if (log === null) return ALL_CLI_TARGETS;
2834
+ return installedClis(log);
2835
+ }
2836
+ var RESTORED_SKILL_SLOT = /^\.(?:claude|agents)\/skills\/([^/]+)$/;
2837
+ function restoredWithoutArgs(paths, tracks) {
2838
+ const catalog = new Set(EXTERNAL_ASSETS.map((a) => a.id));
2839
+ const baselineIds = new Set(listBaselineTargets({ tracks }).map((t) => t.id));
2840
+ const out = {};
2841
+ for (const path of paths) {
2842
+ const skill = RESTORED_SKILL_SLOT.exec(path)?.[1];
2843
+ if (skill !== void 0 && catalog.has(skill) && INTERNAL_BUNDLED_SKILL_IDS.includes(skill)) {
2844
+ out[path] = skill;
2845
+ continue;
2846
+ }
2847
+ const target = classifyBaselineTarget(path);
2848
+ if (target !== null && baselineIds.has(target.id)) out[path] = target.id;
2849
+ }
2850
+ return out;
2851
+ }
2852
+ var SHARED_SKILLS_DIR = ".agents/skills";
2853
+ var SHARED_SKILL_FILE = /^\.agents\/skills\/([^/]+)\//;
2854
+ var SHARED_SKILL_MD = /^\.agents\/skills\/([^/]+)\/SKILL\.md$/;
2855
+ function sharedSkillIdsOnDisk(projectDir) {
2856
+ const root = join15(projectDir, SHARED_SKILLS_DIR);
2857
+ if (!existsSync14(root)) return /* @__PURE__ */ new Set();
2858
+ return new Set(readdirSync3(root).filter((id) => existsSync14(join15(root, id, "SKILL.md"))));
2859
+ }
2628
2860
  function refreshExternalCli(projectDir, harnessRoot) {
2629
2861
  const log = readInstallLog(projectDir);
2630
2862
  const baselineExcluded = new Set(log?.spec.baselineExclude ?? []);
2863
+ const presentBefore = sharedSkillIdsOnDisk(projectDir);
2631
2864
  const result = runCliTransforms({
2632
2865
  harnessRoot,
2633
2866
  projectDir,
2634
- cli: ALL_CLI_TARGETS,
2867
+ cli: installedCliTargets(log),
2635
2868
  // 스킬은 **디스크에 있는 것만** 넘긴다. 스킬 *파일*은 전체 목록을 넘겨도 refreshOnly 가
2636
2869
  // 없는 것을 건너뛰지만, `AGENTS.md` 의 상시 스킬 안내(ADR-085)는 이 목록 그대로 렌더돼
2637
2870
  // `--without` 으로 뺀 스킬(#505)·opt-in 스킬을 "열어라"고 적었다(실측 2026-09-21). 판정은
@@ -2651,26 +2884,41 @@ function refreshExternalCli(projectDir, harnessRoot) {
2651
2884
  } catch {
2652
2885
  }
2653
2886
  }
2887
+ const priorIds = new Set(
2888
+ (log?.externalFiles ?? []).map((f) => SHARED_SKILL_FILE.exec(f.path)?.[1]).filter(Boolean)
2889
+ );
2890
+ const skillsInstalled = [];
2891
+ const skillsRestored = [];
2892
+ for (const f of result.externalFiles) {
2893
+ const id = SHARED_SKILL_MD.exec(f.path)?.[1];
2894
+ if (id === void 0 || presentBefore.has(id)) continue;
2895
+ const slot = `${SHARED_SKILLS_DIR}/${id}`;
2896
+ const into = priorIds.has(id) ? skillsRestored : skillsInstalled;
2897
+ if (!into.includes(slot)) into.push(slot);
2898
+ }
2654
2899
  return {
2655
2900
  externalUpdated: result.externalUpdated,
2656
2901
  externalBackedUp: result.externalBackedUp,
2657
- externalForeignOwned: result.externalForeignOwned
2902
+ externalForeignOwned: result.externalForeignOwned,
2903
+ written: result.externalFiles.map((f) => f.path),
2904
+ skillsInstalled,
2905
+ skillsRestored
2658
2906
  };
2659
2907
  }
2660
2908
  function isHarnessOwned2(ctx, rel, current) {
2661
2909
  return isHarnessOwned(ctx.baseline, `${ctx.prefix}/${rel}`, current);
2662
2910
  }
2663
2911
  function updateDir(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
2664
- if (!existsSync13(target) || !existsSync13(source)) return { updated: 0, backedUp: [] };
2912
+ if (!existsSync14(target) || !existsSync14(source)) return { updated: 0, backedUp: [] };
2665
2913
  let updated = 0;
2666
2914
  const backedUp = [];
2667
2915
  for (const file of readdirSync3(target)) {
2668
2916
  if (!file.endsWith(ext)) continue;
2669
- const targetFile = join12(target, file);
2670
- const sourceFile = join12(source, file);
2671
- if (!existsSync13(sourceFile)) continue;
2672
- const next = readFileSync12(sourceFile, "utf8");
2673
- const current = readFileSync12(targetFile, "utf8");
2917
+ const targetFile = join15(target, file);
2918
+ const sourceFile = join15(source, file);
2919
+ if (!existsSync14(sourceFile)) continue;
2920
+ const next = readFileSync13(sourceFile, "utf8");
2921
+ const current = readFileSync13(targetFile, "utf8");
2674
2922
  if (current === next) continue;
2675
2923
  if (!isHarnessOwned2(ctx, file, current)) {
2676
2924
  backupFile(targetFile, now);
@@ -2682,7 +2930,7 @@ function updateDir(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
2682
2930
  return { updated, backedUp };
2683
2931
  }
2684
2932
  function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Date(), foreignOf) {
2685
- if (!existsSync13(targetDir) || !existsSync13(sourceDir))
2933
+ if (!existsSync14(targetDir) || !existsSync14(sourceDir))
2686
2934
  return { updated: 0, backedUp: [], skippedLinks: [], foreignOwned: [], pruned: [] };
2687
2935
  let updated = 0;
2688
2936
  const backedUp = [];
@@ -2691,27 +2939,27 @@ function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Da
2691
2939
  const pruned = [];
2692
2940
  for (const skill of readdirSync3(sourceDir, { withFileTypes: true })) {
2693
2941
  if (!skill.isDirectory()) continue;
2694
- const targetSkill = join12(targetDir, skill.name);
2942
+ const targetSkill = join15(targetDir, skill.name);
2695
2943
  if (occupiedByNonDirectory(targetSkill)) {
2696
2944
  skippedLinks.push(skill.name);
2697
2945
  continue;
2698
2946
  }
2699
- if (!existsSync13(targetSkill)) continue;
2700
- for (const rel of listFilesRecursive(join12(sourceDir, skill.name))) {
2701
- const targetFile = join12(targetSkill, rel);
2947
+ if (!existsSync14(targetSkill)) continue;
2948
+ for (const rel of listFilesRecursive(join15(sourceDir, skill.name))) {
2949
+ const targetFile = join15(targetSkill, rel);
2702
2950
  const foreign = foreignOf(`${skill.name}/${rel}`);
2703
2951
  if (foreign !== null) {
2704
2952
  if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
2705
2953
  continue;
2706
2954
  }
2707
- const next = readFileSync12(join12(sourceDir, skill.name, rel), "utf8");
2708
- if (!existsSync13(targetFile)) {
2955
+ const next = readFileSync13(join15(sourceDir, skill.name, rel), "utf8");
2956
+ if (!existsSync14(targetFile)) {
2709
2957
  mkdirSync3(dirname3(targetFile), { recursive: true });
2710
2958
  writeFileSync5(targetFile, next);
2711
2959
  updated++;
2712
2960
  continue;
2713
2961
  }
2714
- const current = readFileSync12(targetFile, "utf8");
2962
+ const current = readFileSync13(targetFile, "utf8");
2715
2963
  if (current === next) continue;
2716
2964
  const recorded = baseline.get(`${skill.name}/${rel}`);
2717
2965
  if (recorded === void 0 || hashContent(current) !== recorded) {
@@ -2721,13 +2969,13 @@ function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Da
2721
2969
  writeFileSync5(targetFile, next);
2722
2970
  updated++;
2723
2971
  }
2724
- const bundled = new Set(listFilesRecursive(join12(sourceDir, skill.name)));
2972
+ const bundled = new Set(listFilesRecursive(join15(sourceDir, skill.name)));
2725
2973
  for (const rel of listFilesRecursive(targetSkill)) {
2726
2974
  if (bundled.has(rel) || basename4(rel).includes(".backup-")) continue;
2727
2975
  if (foreignOf(`${skill.name}/${rel}`) !== null) continue;
2728
- const targetFile = join12(targetSkill, rel);
2976
+ const targetFile = join15(targetSkill, rel);
2729
2977
  const recorded = baseline.get(`${skill.name}/${rel}`);
2730
- if (recorded === void 0 || hashContent(readFileSync12(targetFile, "utf8")) !== recorded) {
2978
+ if (recorded === void 0 || hashContent(readFileSync13(targetFile, "utf8")) !== recorded) {
2731
2979
  backupFile(targetFile, now);
2732
2980
  backedUp.push(`${skill.name}/${rel}`);
2733
2981
  }
@@ -2776,16 +3024,16 @@ function refreshSkillBaseline(projectDir, templatesDir) {
2776
3024
  }
2777
3025
  }
2778
3026
  function pruneOrphans(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
2779
- if (!existsSync13(target) || !existsSync13(source)) return [];
3027
+ if (!existsSync14(target) || !existsSync14(source)) return [];
2780
3028
  const removed = [];
2781
3029
  for (const file of readdirSync3(target)) {
2782
3030
  if (!file.endsWith(ext)) continue;
2783
- const sourceFile = join12(source, file);
2784
- if (!existsSync13(sourceFile)) {
2785
- const targetFile = join12(target, file);
3031
+ const sourceFile = join15(source, file);
3032
+ if (!existsSync14(sourceFile)) {
3033
+ const targetFile = join15(target, file);
2786
3034
  if (!ctx.baseline.has(`${ctx.prefix}/${file}`)) continue;
2787
3035
  try {
2788
- const current = readFileSync12(targetFile, "utf8");
3036
+ const current = readFileSync13(targetFile, "utf8");
2789
3037
  if (!isHarnessOwned2(ctx, file, current)) backupFile(targetFile, now);
2790
3038
  unlinkSync2(targetFile);
2791
3039
  removed.push(file);
@@ -2796,8 +3044,8 @@ function pruneOrphans(target, source, ext, ctx, now = /* @__PURE__ */ new Date()
2796
3044
  return removed;
2797
3045
  }
2798
3046
  function retireMcpAllowlist(projectDir, now = /* @__PURE__ */ new Date()) {
2799
- const target = join12(projectDir, ".mcp-allowlist");
2800
- if (!existsSync13(target)) return null;
3047
+ const target = join15(projectDir, ".mcp-allowlist");
3048
+ if (!existsSync14(target)) return null;
2801
3049
  try {
2802
3050
  const backup = backupFile(target, now);
2803
3051
  unlinkSync2(target);
@@ -2809,7 +3057,7 @@ function retireMcpAllowlist(projectDir, now = /* @__PURE__ */ new Date()) {
2809
3057
  function cleanStaleHookRefs(settingsPath, claudeDir) {
2810
3058
  let settings;
2811
3059
  try {
2812
- settings = JSON.parse(readFileSync12(settingsPath, "utf8"));
3060
+ settings = JSON.parse(readFileSync13(settingsPath, "utf8"));
2813
3061
  } catch {
2814
3062
  return [];
2815
3063
  }
@@ -2836,7 +3084,7 @@ function cleanStaleHookRefs(settingsPath, claudeDir) {
2836
3084
  function keepHookRef(hook, claudeDir, removed) {
2837
3085
  const relPath = projectAnchoredRef(hook?.command ?? "", claudeDir);
2838
3086
  if (relPath === void 0) return true;
2839
- const exists = existsSync13(join12(claudeDir, relPath));
3087
+ const exists = existsSync14(join15(claudeDir, relPath));
2840
3088
  if (!exists && !removed.includes(relPath)) removed.push(relPath);
2841
3089
  return exists;
2842
3090
  }
@@ -2858,31 +3106,47 @@ function projectAnchoredRef(command, claudeDir) {
2858
3106
  }
2859
3107
  return void 0;
2860
3108
  }
3109
+ function recordClaudeCommand(log, tracks) {
3110
+ const trackArgs = (tracks.length > 0 ? tracks : log.spec.tracks).map((t) => `--track ${t}`);
3111
+ return `agent-harness install ${trackArgs.join(" ")} --cli claude --scope ${log.scope}`.replace(
3112
+ /\s+/g,
3113
+ " "
3114
+ );
3115
+ }
2861
3116
 
2862
3117
  // src/installer.ts
2863
3118
  function runInstall(ctx) {
2864
3119
  const { harnessRoot, projectDir, spec } = ctx;
2865
3120
  const mode = ctx.mode ?? "fresh";
2866
- const templatesDir = join13(harnessRoot, "templates");
2867
- if (!existsSync14(templatesDir)) {
3121
+ const templatesDir = join16(harnessRoot, "templates");
3122
+ if (!existsSync15(templatesDir)) {
2868
3123
  throw new Error(`Templates dir not found: ${templatesDir}`);
2869
3124
  }
2870
- const claudeDir = join13(projectDir, ".claude");
3125
+ const claudeDir = join16(projectDir, ".claude");
2871
3126
  const previousLog = readInstallLog(projectDir);
2872
- const claudeWasSelected = previousLog?.spec.cli.includes("claude") ?? false;
2873
- if (mode === "update" && !existsSync14(claudeDir) && (previousLog === null || claudeWasSelected)) {
3127
+ const claudeWasSelected = previousLog !== null && installedClis(previousLog).includes("claude");
3128
+ if (mode === "update" && !existsSync15(claudeDir) && (previousLog === null || claudeWasSelected)) {
2874
3129
  throw new Error(
2875
3130
  claudeWasSelected ? `Update mode found a broken install at ${projectDir} \u2014 this project installed Claude Code assets but \`.claude/\` is gone. Reinstall instead: agent-harness install --track <name>` : `Update mode requires an existing install at ${projectDir}`
2876
3131
  );
2877
3132
  }
2878
- const backupPath = resolveBackupPath(ctx, mode, claudeDir);
3133
+ const backupPath = resolveBackupPath(ctx, mode, claudeDir, previousLog);
2879
3134
  if (mode === "update") {
2880
3135
  return runUpdateInstall(ctx, templatesDir, backupPath);
2881
3136
  }
2882
3137
  const manifestSpec = buildManifestSpec(spec);
2883
3138
  const policyBase = new Map((previousLog?.policyFiles ?? []).map((f) => [f.path, f.sha256]));
3139
+ const skillBase = new Map((previousLog?.skillFiles ?? []).map((f) => [f.path, f.sha256]));
2884
3140
  const baselineExcluded = new Set(spec.baselineExclude ?? []);
2885
- const base = spec.cli.includes("claude") ? installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBase, baselineExcluded) : (
3141
+ const base = spec.cli.includes("claude") ? installClaudeBaseline(
3142
+ manifestSpec,
3143
+ projectDir,
3144
+ templatesDir,
3145
+ policyBase,
3146
+ skillBase,
3147
+ baselineExcluded,
3148
+ harnessRoot
3149
+ ) : (
2886
3150
  // claude 미선택이어도 CLI 중립 자산은 깔린다. manifest 전체가 `.claude/` baseline 안에서만
2887
3151
  // 돌던 탓에 이 자산들이 claude 설치에만 도달했는데, **배포 룰 본문이 이 스크립트들을
2888
3152
  // 호출 지점으로 지목한다** — 즉 없는 도구를 있다고 안내하고 있었다(#300 과 같은 형태).
@@ -2914,6 +3178,17 @@ function runInstall(ctx) {
2914
3178
  previousExternal: previousLog?.externalFiles ?? [],
2915
3179
  codexTrust: (spec.scope ?? "project") === "global" && spec.options.withCodexTrust
2916
3180
  });
3181
+ const linked = refreshLinkedSkillBodies({
3182
+ harnessRoot,
3183
+ projectDir,
3184
+ ids: base.linkedSkills,
3185
+ baseline: new Map(
3186
+ mergeExternalFiles(projectDir, previousLog?.externalFiles, externalFiles).map((f) => [
3187
+ f.path,
3188
+ f.sha256
3189
+ ])
3190
+ )
3191
+ });
2917
3192
  const baseline = {
2918
3193
  filesCopied: base.filesCopied,
2919
3194
  dirsCopied: base.dirsCopied,
@@ -2930,21 +3205,23 @@ function runInstall(ctx) {
2930
3205
  rootClaudeMd: base.rootClaudeMd,
2931
3206
  // 외부 CLI 백업도 같은 줄에 노출한다 — 백업이 화면에 안 보이면 사용자는 자기 편집분이
2932
3207
  // 어디 갔는지 알 수 없고, 그러면 백업은 있어도 없는 것과 같다 (ADR-046/047 과 같은 이유).
2933
- backups: [...base.backups, ...externalBackups],
3208
+ backups: [...base.backups, ...externalBackups, ...linked.backupPaths],
2934
3209
  baselineExcluded: base.excluded,
2935
3210
  baselineExcludedOnDisk: base.excludedOnDisk,
2936
3211
  // `.claude/` baseline 과 외부 CLI 산출물의 같은 판정을 **한 목록으로** 낸다.
2937
3212
  baselineForeignOwned: [
2938
- ...base.foreignOwned,
2939
- ...externalForeignOwned.filter((f) => !base.foreignOwned.includes(f))
2940
- ]
3213
+ .../* @__PURE__ */ new Set([...base.foreignOwned, ...externalForeignOwned, ...linked.foreignOwned])
3214
+ ],
3215
+ baselineLinked: linked.updated,
3216
+ baselineLinkedNotOurs: linked.notOurs
2941
3217
  };
2942
3218
  ctx.onProgress?.({ type: "baseline-complete", baseline });
2943
3219
  const external = runExternalPhase(ctx);
2944
3220
  const staleHookRefs = healStaleHookRefs(spec, projectDir);
2945
3221
  writeInstallLogSafe(
2946
3222
  ctx,
2947
- externalFiles,
3223
+ // 링크 본문의 기준선도 같은 필드다 — 같은 경로면 뒤(이번에 쓴 값)가 이긴다.
3224
+ [...externalFiles, ...linked.files],
2948
3225
  external,
2949
3226
  base.rootClaudeMdLog,
2950
3227
  previousLog,
@@ -2955,12 +3232,13 @@ function runInstall(ctx) {
2955
3232
  }
2956
3233
  function healStaleHookRefs(spec, projectDir) {
2957
3234
  if (!spec.cli.includes("claude")) return [];
2958
- const settingsPath = join13(projectDir, ".claude/settings.json");
2959
- if (!existsSync14(settingsPath)) return [];
2960
- return cleanStaleHookRefs(settingsPath, join13(projectDir, ".claude"));
3235
+ const settingsPath = join16(projectDir, ".claude/settings.json");
3236
+ if (!existsSync15(settingsPath)) return [];
3237
+ return cleanStaleHookRefs(settingsPath, join16(projectDir, ".claude"));
2961
3238
  }
2962
- function resolveBackupPath(ctx, mode, claudeDir) {
2963
- const wantBackup = ctx.backup ?? (mode === "update" || mode === "reinstall");
3239
+ function resolveBackupPath(ctx, mode, claudeDir, previousLog) {
3240
+ const claudeUntouched = previousLog !== null && !installedClis(previousLog).includes("claude");
3241
+ const wantBackup = ctx.backup ?? (mode === "reinstall" || mode === "update" && !claudeUntouched);
2964
3242
  if (!wantBackup) return null;
2965
3243
  return mode === "update" ? copyBackupDir(claudeDir) : backupDir(claudeDir);
2966
3244
  }
@@ -3017,7 +3295,8 @@ function emptyClaudeBaseline() {
3017
3295
  backups: [],
3018
3296
  excluded: [],
3019
3297
  excludedOnDisk: [],
3020
- foreignOwned: []
3298
+ foreignOwned: [],
3299
+ linkedSkills: []
3021
3300
  };
3022
3301
  }
3023
3302
  function installCliNeutralAssets(manifestSpec, projectDir, templatesDir, baselineExcluded) {
@@ -3029,12 +3308,12 @@ function installCliNeutralAssets(manifestSpec, projectDir, templatesDir, baselin
3029
3308
  continue;
3030
3309
  }
3031
3310
  if (!isCliNeutralTarget(entry.target)) continue;
3032
- const source = join13(templatesDir, entry.source);
3033
- if (!existsSync14(source)) {
3311
+ const source = join16(templatesDir, entry.source);
3312
+ if (!existsSync15(source)) {
3034
3313
  result.skipped += 1;
3035
3314
  continue;
3036
3315
  }
3037
- copyFile(source, join13(projectDir, entry.target));
3316
+ copyFile(source, join16(projectDir, entry.target));
3038
3317
  result.filesCopied += 1;
3039
3318
  }
3040
3319
  return result;
@@ -3046,14 +3325,28 @@ function backupEditedPolicyFile(entryTarget, target, source, baseline) {
3046
3325
  if (!POLICY_DIRS.some(({ dir, ext }) => rel.startsWith(`${dir}/`) && rel.endsWith(ext))) {
3047
3326
  return null;
3048
3327
  }
3049
- if (!existsSync14(target)) return null;
3050
- const current = readFileSync13(target, "utf-8");
3051
- if (current === readFileSync13(source, "utf-8")) return null;
3328
+ if (!existsSync15(target)) return null;
3329
+ const current = readFileSync14(target, "utf-8");
3330
+ if (current === readFileSync14(source, "utf-8")) return null;
3052
3331
  const recorded = baseline.get(rel);
3053
3332
  if (recorded !== void 0 && recorded === hashContent(current)) return null;
3054
3333
  return backupFile(target);
3055
3334
  }
3056
- function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBase, baselineExcluded) {
3335
+ function backupEditedSkillFiles(projectDir, entryTarget, source, baseline) {
3336
+ const id = entryTarget.slice(".claude/skills/".length);
3337
+ const backups = [];
3338
+ for (const rel of listFilesRecursive(source)) {
3339
+ if (foreignOwnedTarget(projectDir, `${entryTarget}/${rel}`) !== null) continue;
3340
+ const target = join16(projectDir, entryTarget, rel);
3341
+ if (!existsSync15(target)) continue;
3342
+ const current = readFileSync14(target, "utf-8");
3343
+ if (current === readFileSync14(join16(source, rel), "utf-8")) continue;
3344
+ if (isHarnessOwned(baseline, `${id}/${rel}`, current)) continue;
3345
+ backups.push(backupFile(target));
3346
+ }
3347
+ return backups;
3348
+ }
3349
+ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBase, skillBase, baselineExcluded, harnessRoot) {
3057
3350
  ensureProjectSkeleton(projectDir);
3058
3351
  const result = emptyClaudeBaseline();
3059
3352
  const manifest = buildManifest(manifestSpec);
@@ -3061,17 +3354,22 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
3061
3354
  if (!entry.applies(manifestSpec)) {
3062
3355
  continue;
3063
3356
  }
3064
- const target = join13(projectDir, entry.target);
3357
+ const target = join16(projectDir, entry.target);
3065
3358
  if (isBaselineExcluded(entry.target, baselineExcluded)) {
3066
3359
  result.excluded.push(entry.target);
3067
- if (existsSync14(target)) result.excludedOnDisk.push(entry.target);
3360
+ if (existsSync15(target)) result.excludedOnDisk.push(entry.target);
3068
3361
  continue;
3069
3362
  }
3070
- const source = join13(templatesDir, entry.source);
3071
- if (!existsSync14(source)) {
3363
+ const source = join16(templatesDir, entry.source);
3364
+ if (!existsSync15(source)) {
3072
3365
  result.skipped += 1;
3073
3366
  continue;
3074
3367
  }
3368
+ const skillId = entry.target.slice(".claude/skills/".length);
3369
+ if (entry.type === "dir" && linksToProjectSharedSkill(projectDir, skillId)) {
3370
+ result.linkedSkills.push(skillId);
3371
+ continue;
3372
+ }
3075
3373
  const foreign = foreignOwnedTarget(projectDir, entry.target);
3076
3374
  if (foreign !== null) {
3077
3375
  if (!result.foreignOwned.includes(foreign)) result.foreignOwned.push(foreign);
@@ -3079,7 +3377,7 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
3079
3377
  }
3080
3378
  if (entry.type === "file") {
3081
3379
  if (entry.target === ".claude/settings.json") {
3082
- const backup = backupFileIfChanged(target, readFileSync13(source, "utf-8"));
3380
+ const backup = backupFileIfChanged(target, readFileSync14(source, "utf-8"));
3083
3381
  if (backup) {
3084
3382
  result.backups.push(backup);
3085
3383
  }
@@ -3092,6 +3390,7 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
3092
3390
  copyFile(source, target);
3093
3391
  result.filesCopied += 1;
3094
3392
  } else {
3393
+ result.backups.push(...backupEditedSkillFiles(projectDir, entry.target, source, skillBase));
3095
3394
  for (const foreign2 of copyDir(
3096
3395
  source,
3097
3396
  target,
@@ -3103,24 +3402,29 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
3103
3402
  }
3104
3403
  accumulateCategory(result.categories, entry);
3105
3404
  }
3106
- const hookDir = join13(projectDir, ".claude/hooks");
3107
- if (existsSync14(hookDir)) {
3405
+ const hookDir = join16(projectDir, ".claude/hooks");
3406
+ if (existsSync15(hookDir)) {
3108
3407
  chmodHooksSync(hookDir);
3109
3408
  }
3110
3409
  writeInstalledTracks(projectDir, manifestSpec.tracks);
3111
3410
  const rootClaudeMd = writeRootClaudeMd(
3112
3411
  projectDir,
3113
3412
  manifestSpec.tracks,
3114
- manifestSpec.selectedInternalSkills ?? []
3413
+ manifestSpec.selectedInternalSkills ?? [],
3414
+ harnessRoot
3115
3415
  );
3116
- result.rootClaudeMd = { tracks: manifestSpec.tracks, created: rootClaudeMd.created };
3416
+ result.rootClaudeMd = {
3417
+ tracks: manifestSpec.tracks,
3418
+ created: rootClaudeMd.created,
3419
+ seededFrom: rootClaudeMd.seededFrom
3420
+ };
3117
3421
  result.rootClaudeMdLog = harnessAnchorLog(projectDir);
3118
3422
  return result;
3119
3423
  }
3120
3424
  function harnessAnchorLog(projectDir) {
3121
- const anchor = join13(projectDir, HARNESS_ANCHOR_FILE);
3122
- if (!existsSync14(anchor)) return null;
3123
- return { path: HARNESS_ANCHOR_FILE, sha256: hashContent(readFileSync13(anchor, "utf-8")) };
3425
+ const anchor = join16(projectDir, HARNESS_ANCHOR_FILE);
3426
+ if (!existsSync15(anchor)) return null;
3427
+ return { path: HARNESS_ANCHOR_FILE, sha256: hashContent(readFileSync14(anchor, "utf-8")) };
3124
3428
  }
3125
3429
  function writeEnvironmentFiles(projectDir, tracks) {
3126
3430
  return {
@@ -3177,8 +3481,9 @@ function writeInstallLogSafe(ctx, externalFiles, external, rootClaudeMdLog, prev
3177
3481
  claudeDirMovedAside,
3178
3482
  rootFiles
3179
3483
  );
3180
- const skillFiles = collectSkillHashes(ctx.projectDir, join13(ctx.harnessRoot, "templates"));
3181
- const policyFiles = collectPolicyHashes(ctx.projectDir, join13(ctx.harnessRoot, "templates"));
3484
+ const claudeInstalled = (log.spec.clis ?? []).includes("claude");
3485
+ const skillFiles = claudeInstalled ? collectSkillHashes(ctx.projectDir, join16(ctx.harnessRoot, "templates")) : [];
3486
+ const policyFiles = claudeInstalled ? collectPolicyHashes(ctx.projectDir, join16(ctx.harnessRoot, "templates")) : [];
3182
3487
  const merged = mergeExternalFiles(ctx.projectDir, previousLog?.externalFiles, externalFiles);
3183
3488
  writeInstallLog(ctx.projectDir, {
3184
3489
  ...log,
@@ -3194,11 +3499,11 @@ function writeInstallLogSafe(ctx, externalFiles, external, rootClaudeMdLog, prev
3194
3499
  }
3195
3500
  }
3196
3501
  function composeAndWriteMcp(harnessRoot, projectDir, spec) {
3197
- const mcpPath = join13(projectDir, ".mcp.json");
3198
- const created = !existsSync14(mcpPath);
3502
+ const mcpPath = join16(projectDir, ".mcp.json");
3503
+ const created = !existsSync15(mcpPath);
3199
3504
  const composed = composeMcpJson({
3200
- templateMcpPath: join13(harnessRoot, "templates/mcp.json"),
3201
- trackMapPath: join13(harnessRoot, "templates/track-mcp-map.tsv"),
3505
+ templateMcpPath: join16(harnessRoot, "templates/mcp.json"),
3506
+ trackMapPath: join16(harnessRoot, "templates/track-mcp-map.tsv"),
3202
3507
  existingPath: mcpPath,
3203
3508
  tracks: spec.tracks
3204
3509
  });
@@ -3251,24 +3556,26 @@ function accumulateCategory(cats, entry) {
3251
3556
  }
3252
3557
  }
3253
3558
  function writeInstalledTracks(projectDir, tracks) {
3254
- const path = join13(projectDir, ".claude/.installed-tracks");
3559
+ const path = join16(projectDir, ".claude/.installed-tracks");
3255
3560
  mkdirSync4(dirname4(path), { recursive: true });
3256
3561
  const sorted = [...new Set(tracks)].sort().join("\n");
3257
3562
  writeFileSync6(path, `${sorted}
3258
3563
  `);
3259
3564
  }
3260
- function writeRootClaudeMd(projectDir, tracks, continuousSkills) {
3261
- const target = join13(projectDir, "CLAUDE.md");
3262
- const existing = existsSync14(target) ? readFileSync13(target, "utf-8") : null;
3565
+ function writeRootClaudeMd(projectDir, tracks, continuousSkills, harnessRoot) {
3566
+ const target = join16(projectDir, "CLAUDE.md");
3567
+ const existing = existsSync15(target) ? readFileSync14(target, "utf-8") : null;
3568
+ const seeded = existing === null ? seedRootClaudeProjectContext(projectDir, harnessRoot) : null;
3263
3569
  const content = upsertHarnessImport(existing, {
3264
3570
  projectName: basename5(projectDir),
3265
3571
  tracks,
3266
- continuousSkills
3572
+ continuousSkills,
3573
+ ...seeded === null ? {} : { projectContext: seeded }
3267
3574
  });
3268
3575
  if (content !== existing) {
3269
3576
  writeFileSync6(target, content);
3270
3577
  }
3271
- return { created: existing === null };
3578
+ return { created: existing === null, seededFrom: seeded === null ? null : "AGENTS.md" };
3272
3579
  }
3273
3580
  function chmodHooksSync(hookDir) {
3274
3581
  for (const file of listHookFiles(hookDir)) {
@@ -3284,8 +3591,8 @@ function listHookFiles(hookDir) {
3284
3591
 
3285
3592
  // src/commands/install-render.ts
3286
3593
  init_esm_shims();
3287
- import { existsSync as existsSync15, readdirSync as readdirSync5 } from "fs";
3288
- import { join as join14 } from "path";
3594
+ import { existsSync as existsSync16, readdirSync as readdirSync5 } from "fs";
3595
+ import { join as join17 } from "path";
3289
3596
 
3290
3597
  // src/preset-recommend.ts
3291
3598
  init_esm_shims();
@@ -3453,13 +3760,13 @@ function renderUpdateSummary(log, spec, report) {
3453
3760
  function residentEntriesOnDisk(spec) {
3454
3761
  const assetSpec = buildManifestSpec(spec);
3455
3762
  const planned = buildManifest(assetSpec).filter((e2) => e2.applies(assetSpec));
3456
- const agentsDir = join14(spec.projectDir, ".claude", "agents");
3457
- const onDisk = existsSync15(agentsDir) ? readdirSync5(agentsDir, { withFileTypes: true }).filter((e2) => e2.isFile() && e2.name.endsWith(".md")).map((e2) => ({
3763
+ const agentsDir = join17(spec.projectDir, ".claude", "agents");
3764
+ const onDisk = existsSync16(agentsDir) ? readdirSync5(agentsDir, { withFileTypes: true }).filter((e2) => e2.isFile() && e2.name.endsWith(".md")).map((e2) => ({
3458
3765
  source: `agents/${e2.name}`,
3459
3766
  target: `.claude/agents/${e2.name}`,
3460
3767
  // 배포판이 아니라 **이 프로젝트의 파일**을 잰다 — 사용자가 고친 descriptor 도,
3461
3768
  // 배포판에 더는 없는 에이전트도 실제로 상주하는 것은 이쪽이다.
3462
- file: join14(agentsDir, e2.name)
3769
+ file: join17(agentsDir, e2.name)
3463
3770
  })) : [];
3464
3771
  return [...planned.filter((e2) => !e2.target.startsWith(".claude/agents/")), ...onDisk];
3465
3772
  }
@@ -3476,6 +3783,16 @@ function renderCliArtifacts(log, spec, report) {
3476
3783
  } else if (report.codex || report.opencode) {
3477
3784
  log(assetRow("success", "AGENTS.md", `from ${HARNESS_ANCHOR_FILE}`));
3478
3785
  }
3786
+ const agentsSeededFrom = report.codex?.agentsMdSeededFrom ?? report.opencode?.agentsMdSeededFrom ?? null;
3787
+ if (agentsSeededFrom) {
3788
+ log(
3789
+ assetRow(
3790
+ "success",
3791
+ "AGENTS.md",
3792
+ `Project Context seeded from ${agentsSeededFrom} \xB7 CLI \uACE0\uC720 \uD45C\uD604\uC740 audit-harness-fit \uC73C\uB85C \uB9DE\uCD98\uB2E4`
3793
+ )
3794
+ );
3795
+ }
3479
3796
  if (report.codex) {
3480
3797
  log(assetRow("success", ".codex/config.toml", "settings + [mcp_servers.*]"));
3481
3798
  log(assetRow("success", ".codex/hooks/", `${report.codex.hookFiles.length} files`));
@@ -3637,6 +3954,20 @@ function formatAssetMeta(asset, version) {
3637
3954
  return `internal \xB7 templates (${m.key})`;
3638
3955
  }
3639
3956
  }
3957
+ function linkedSkillRows(log, updated, notOurs) {
3958
+ for (const id of updated) {
3959
+ log(assetRow("success", `.claude/skills/${id}`, `linked \xB7 updated via .agents/skills/${id}`));
3960
+ }
3961
+ for (const id of notOurs) {
3962
+ log(
3963
+ assetRow(
3964
+ "skip",
3965
+ `.claude/skills/${id}`,
3966
+ `linked \xB7 not ours \u2014 .agents/skills/${id} \uB294 \uC774 \uD558\uB124\uC2A4\uAC00 \uC4F4 \uAE30\uB85D\uC774 \uC5C6\uC5B4 \uAC74\uB4DC\uB9AC\uC9C0 \uC54A\uC558\uB2E4`
3967
+ )
3968
+ );
3969
+ }
3970
+ }
3640
3971
  function renderPhase1Rows(log, baseline, verbose = false) {
3641
3972
  if (baseline.updateMode) {
3642
3973
  if (baseline.backup) {
@@ -3648,8 +3979,30 @@ function renderPhase1Rows(log, baseline, verbose = false) {
3648
3979
  for (const path of baseline.updateMode.installedNew) {
3649
3980
  log(assetRow("success", path, "added by this release"));
3650
3981
  }
3982
+ const withoutArgs = [];
3651
3983
  for (const path of baseline.updateMode.restored) {
3652
- log(assetRow("success", path, "was missing \u2014 reinstalled (delete it again if intentional)"));
3984
+ const arg = baseline.updateMode.restoredWithout?.[path];
3985
+ if (arg === void 0) {
3986
+ log(
3987
+ assetRow(
3988
+ "success",
3989
+ path,
3990
+ "was missing \u2014 reinstalled (harness-managed \u2014 update always restores it)"
3991
+ )
3992
+ );
3993
+ continue;
3994
+ }
3995
+ log(assetRow("success", path, "was missing \u2014 reinstalled"));
3996
+ if (!withoutArgs.includes(arg)) withoutArgs.push(arg);
3997
+ }
3998
+ if (withoutArgs.length > 0) {
3999
+ log(
4000
+ assetRow(
4001
+ "skip",
4002
+ "to keep it out",
4003
+ `deleting is undone by the next update \u2014 re-run \`agent-harness install\` with your usual flags plus ${withoutArgs.map((a) => `--without ${a}`).join(" ")}, then delete it again`
4004
+ )
4005
+ );
3653
4006
  }
3654
4007
  if (baseline.updateMode.needsReinstall.length > 0) {
3655
4008
  log(
@@ -3701,6 +4054,15 @@ function renderPhase1Rows(log, baseline, verbose = false) {
3701
4054
  )
3702
4055
  );
3703
4056
  }
4057
+ if (baseline.updateMode.claudeUnrecorded) {
4058
+ log(
4059
+ assetRow(
4060
+ "skip",
4061
+ ".claude/",
4062
+ `present but not recorded as installed \u2014 left untouched. If its rules/ \xB7 agents/ \xB7 hooks/ \xB7 skills/ came from this tool (you did not write them), run \`${baseline.updateMode.claudeUnrecorded}\` once to re-record them and updates resume; otherwise this directory is yours`
4063
+ )
4064
+ );
4065
+ }
3704
4066
  if (baseline.updateMode.legacyAnchor) {
3705
4067
  log(
3706
4068
  assetRow(
@@ -3728,6 +4090,11 @@ function renderPhase1Rows(log, baseline, verbose = false) {
3728
4090
  )
3729
4091
  );
3730
4092
  }
4093
+ linkedSkillRows(
4094
+ log,
4095
+ baseline.updateMode.skillsLinked ?? [],
4096
+ baseline.updateMode.skillsLinkedNotOurs ?? []
4097
+ );
3731
4098
  if (baseline.updateMode.skillsSkippedLinks.length > 0) {
3732
4099
  log(
3733
4100
  assetRow(
@@ -3934,6 +4301,7 @@ function renderPhase1Rows(log, baseline, verbose = false) {
3934
4301
  )
3935
4302
  );
3936
4303
  }
4304
+ linkedSkillRows(log, baseline.baselineLinked ?? [], baseline.baselineLinkedNotOurs ?? []);
3937
4305
  if (baseline.baselineForeignOwned.length > 0) {
3938
4306
  log(
3939
4307
  assetRow(
@@ -3950,7 +4318,7 @@ function renderPhase1Rows(log, baseline, verbose = false) {
3950
4318
  assetRow(
3951
4319
  "success",
3952
4320
  "CLAUDE.md (root)",
3953
- baseline.rootClaudeMd.created ? `fill-in scaffold + @import \xB7 ${n} track${n > 1 ? "s" : ""} noted` : `@import ${HARNESS_ANCHOR_FILE} (body preserved)`,
4321
+ rootClaudeMdMeta(baseline.rootClaudeMd, n),
3954
4322
  TEMPLATES_COL
3955
4323
  )
3956
4324
  );
@@ -4017,6 +4385,14 @@ function shortenPath(p2) {
4017
4385
  }
4018
4386
  return p2;
4019
4387
  }
4388
+ function rootClaudeMdMeta(root, trackCount) {
4389
+ const tracks = `${trackCount} track${trackCount > 1 ? "s" : ""} noted`;
4390
+ if (!root.created) return `@import ${HARNESS_ANCHOR_FILE} (body preserved)`;
4391
+ if (root.seededFrom) {
4392
+ return `Project Context seeded from ${root.seededFrom} + @import \xB7 ${tracks} \xB7 CLI \uACE0\uC720 \uD45C\uD604\uC740 audit-harness-fit \uC73C\uB85C \uB9DE\uCD98\uB2E4`;
4393
+ }
4394
+ return `fill-in scaffold + @import \xB7 ${tracks}`;
4395
+ }
4020
4396
  function formatCliPhaseTitle(targets) {
4021
4397
  const labels = [];
4022
4398
  if (targets.includes("codex")) labels.push("Codex");
@@ -4078,6 +4454,18 @@ function installAction(options, deps = {}) {
4078
4454
  exit(1);
4079
4455
  return;
4080
4456
  }
4457
+ const spec = installSpecFromOptions(options, validated.cli, err);
4458
+ executeSpec(spec, {
4459
+ log,
4460
+ err,
4461
+ exit,
4462
+ runPipeline,
4463
+ resolveHarnessRoot,
4464
+ verbose: options.verbose === true,
4465
+ ...options.reinstall === true ? { mode: "reinstall" } : {}
4466
+ });
4467
+ }
4468
+ function installSpecFromOptions(options, cli2, err) {
4081
4469
  const forceInclude = normalizeRepeatable(options.with);
4082
4470
  const forceExclude = normalizeRepeatable(options.without);
4083
4471
  const baselineIds = new Set(
@@ -4104,7 +4492,7 @@ function installAction(options, deps = {}) {
4104
4492
  const filteredInclude = forceInclude.filter((id) => validIds.has(id));
4105
4493
  const filteredExclude = forceExclude.filter((id) => validIds.has(id));
4106
4494
  const userOverride = filteredInclude.length > 0 || filteredExclude.length > 0 ? { forceInclude: filteredInclude, forceExclude: filteredExclude } : void 0;
4107
- const spec = {
4495
+ return {
4108
4496
  tracks: options.track ?? [],
4109
4497
  ...userOverride ? { userOverride } : {},
4110
4498
  ...baselineExclude.length > 0 ? { baselineExclude } : {},
@@ -4113,18 +4501,21 @@ function installAction(options, deps = {}) {
4113
4501
  options: {
4114
4502
  withCodexTrust: options.withCodexTrust === true
4115
4503
  },
4116
- cli: validated.cli,
4504
+ cli: cli2,
4117
4505
  projectDir: resolve2(options.projectDir ?? process.cwd()),
4118
4506
  scope: resolveScopeOption(options.scope, err)
4119
4507
  };
4120
- executeSpec(spec, {
4121
- log,
4122
- err,
4123
- exit,
4124
- runPipeline,
4125
- resolveHarnessRoot,
4126
- verbose: options.verbose === true
4127
- });
4508
+ }
4509
+ function installCommandLine(options) {
4510
+ const repeat = (flag, value) => normalizeRepeatable(value).map((v2) => `${flag} ${v2}`);
4511
+ return [
4512
+ "agent-harness install",
4513
+ ...repeat("--track", options.track),
4514
+ ...repeat("--cli", options.cli),
4515
+ ...options.scope ? [`--scope ${options.scope}`] : [],
4516
+ ...repeat("--with", options.with),
4517
+ ...repeat("--without", options.without)
4518
+ ].join(" ");
4128
4519
  }
4129
4520
  function executeSpec(spec, deps = {}) {
4130
4521
  const log = deps.log ?? console.log;
@@ -4206,13 +4597,16 @@ function registerInstallCommand(cli2) {
4206
4597
  ).option(
4207
4598
  "--with-codex-trust",
4208
4599
  "[Codex] Codex global opt-in: register trust entry in ~/.codex/config.toml"
4600
+ ).option(
4601
+ "--reinstall",
4602
+ "[Mode] Move .claude/ aside as .claude.backup-<ts> and rebuild it \u2014 use when .claude/ is damaged or missing"
4209
4603
  ).option("--verbose", "[Misc] Show installed file lists per category (default: counts only)").example("install --track tooling --with marketingskills").example("install --track csr-supabase --cli claude --cli codex").example("install --track csr-supabase --without netlify-cli --with railway-skills").action((options) => installAction(options));
4210
4604
  }
4211
4605
 
4212
4606
  // src/commands/list.ts
4213
4607
  init_esm_shims();
4214
- import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
4215
- import { join as join15, resolve as resolve3 } from "path";
4608
+ import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
4609
+ import { join as join18, resolve as resolve3 } from "path";
4216
4610
  function listAction(options = {}, deps = {}) {
4217
4611
  const log = deps.log ?? console.log;
4218
4612
  const err = deps.err ?? console.error;
@@ -4231,7 +4625,7 @@ function listAction(options = {}, deps = {}) {
4231
4625
  log(c.dim(` installed: ${installLog.installedAt}`));
4232
4626
  log(c.dim(` scope: ${installLog.scope}`));
4233
4627
  log(c.dim(` tracks: ${installLog.spec.tracks.join(", ") || "(none)"}`));
4234
- log(c.dim(` cli: ${installLog.spec.cli.join(", ") || "(none)"}`));
4628
+ log(c.dim(` cli: ${installedClis(installLog).join(", ") || "(none)"}`));
4235
4629
  log("");
4236
4630
  log(c.bold(` Assets (${installLog.assets.length})`));
4237
4631
  if (installLog.assets.length === 0) {
@@ -4275,8 +4669,8 @@ function formatTemplateRows(log, projectDir) {
4275
4669
  const rows = [` ${c.dim(dirs.join(" "))}`];
4276
4670
  const rootMd = log.templates.rootClaudeMd;
4277
4671
  if (rootMd) {
4278
- const path = join15(projectDir, rootMd.path);
4279
- const modified = existsSync16(path) && hashContent(readFileSync14(path, "utf8")) !== rootMd.sha256;
4672
+ const path = join18(projectDir, rootMd.path);
4673
+ const modified = existsSync17(path) && hashContent(readFileSync15(path, "utf8")) !== rootMd.sha256;
4280
4674
  rows.push(
4281
4675
  modified ? ` ${c.dim(rootMd.path)} ${c.yellow("(\uC218\uC815\uB428 \u2014 uninstall \uC2DC \uBCF4\uC874)")}` : ` ${c.dim(rootMd.path)}`
4282
4676
  );
@@ -4284,7 +4678,7 @@ function formatTemplateRows(log, projectDir) {
4284
4678
  return rows;
4285
4679
  }
4286
4680
  function formatRootFileRows(rootFiles, projectDir) {
4287
- const present = rootFiles.filter((f) => existsSync16(join15(projectDir, f.path)));
4681
+ const present = rootFiles.filter((f) => existsSync17(join18(projectDir, f.path)));
4288
4682
  const width = Math.max(0, ...present.map((f) => f.path.length));
4289
4683
  return present.map(
4290
4684
  (f) => ` ${c.dim(padDisplay(f.path, width))} ${c.dim(f.change === "created" ? "\uC0DD\uC131" : "\uBCD1\uD569")} ${c.dim(f.notes.join(" / "))}`
@@ -4302,15 +4696,64 @@ function registerListCommand(cli2) {
4302
4696
  init_esm_shims();
4303
4697
  import { spawnSync as spawnSync2 } from "child_process";
4304
4698
  import {
4305
- existsSync as existsSync17,
4699
+ existsSync as existsSync18,
4306
4700
  lstatSync as lstatSync2,
4307
4701
  readdirSync as readdirSync6,
4308
- readFileSync as readFileSync15,
4702
+ readFileSync as readFileSync16,
4309
4703
  rmdirSync as rmdirSync2,
4310
4704
  rmSync,
4311
4705
  writeFileSync as writeFileSync7
4312
4706
  } from "fs";
4313
- import { dirname as dirname5, join as join16, relative as relative2, resolve as resolve4, sep as sep2 } from "path";
4707
+ import { dirname as dirname5, join as join19, relative as relative2, resolve as resolve4, sep as sep3 } from "path";
4708
+
4709
+ // src/cli-ownership.ts
4710
+ init_esm_shims();
4711
+ var SHARED_BY_ALL = (self) => [
4712
+ { path: ".mcp.json", kind: "keep", sharedWith: CLI_BASES.filter((c3) => c3 !== self) },
4713
+ {
4714
+ path: `${INSTALL_LOG_DIR}/`,
4715
+ kind: "keep",
4716
+ sharedWith: CLI_BASES.filter((c3) => c3 !== self)
4717
+ }
4718
+ ];
4719
+ var CLI_OWNERSHIP = {
4720
+ claude: [
4721
+ { path: ".claude/", kind: "dir", sharedWith: [] },
4722
+ { path: HARNESS_ANCHOR_FILE, kind: "anchor", sharedWith: [] },
4723
+ { path: "CLAUDE.md", kind: "import-block", sharedWith: [] },
4724
+ ...SHARED_BY_ALL("claude")
4725
+ ],
4726
+ codex: [
4727
+ { path: ".codex/", kind: "dir", sharedWith: [] },
4728
+ { path: "AGENTS.md", kind: "recorded", sharedWith: ["opencode"] },
4729
+ { path: ".agents/skills/", kind: "recorded", sharedWith: ["opencode", "antigravity"] },
4730
+ ...SHARED_BY_ALL("codex")
4731
+ ],
4732
+ opencode: [
4733
+ { path: ".opencode/", kind: "dir", sharedWith: [] },
4734
+ { path: "opencode.json", kind: "recorded", sharedWith: [] },
4735
+ { path: "AGENTS.md", kind: "recorded", sharedWith: ["codex"] },
4736
+ { path: ".agents/skills/", kind: "recorded", sharedWith: ["codex", "antigravity"] },
4737
+ ...SHARED_BY_ALL("opencode")
4738
+ ],
4739
+ antigravity: [
4740
+ { path: ".agents/rules/uzys-harness.md", kind: "recorded", sharedWith: [] },
4741
+ { path: ".agents/skills/", kind: "recorded", sharedWith: ["codex", "opencode"] },
4742
+ ...SHARED_BY_ALL("antigravity")
4743
+ ]
4744
+ };
4745
+ function removableFor(cli2, remaining) {
4746
+ const others = remaining.filter((c3) => c3 !== cli2);
4747
+ const stillUsed = new Set(others.flatMap((c3) => CLI_OWNERSHIP[c3].map((p2) => p2.path)));
4748
+ const exclusive = [];
4749
+ const sharedNowUnowned = [];
4750
+ for (const owned of CLI_OWNERSHIP[cli2]) {
4751
+ if (owned.kind === "keep") continue;
4752
+ if (owned.sharedWith.length === 0) exclusive.push(owned);
4753
+ else if (!stillUsed.has(owned.path)) sharedNowUnowned.push(owned);
4754
+ }
4755
+ return { exclusive, sharedNowUnowned };
4756
+ }
4314
4757
 
4315
4758
  // src/uninstall-interactive.ts
4316
4759
  init_esm_shims();
@@ -5285,109 +5728,605 @@ ${a}
5285
5728
  };
5286
5729
  var jt = `${e("gray", $)} `;
5287
5730
 
5288
- // src/uninstall-interactive.ts
5289
- function buildRemovableRows(assets) {
5290
- return assets.map((a) => ({
5291
- value: a.id,
5292
- label: `${a.id} [${a.method}]${a.version ? ` v${a.version}` : ""}`,
5293
- hint: hintFor(a)
5294
- }));
5731
+ // src/prompts.ts
5732
+ init_esm_shims();
5733
+
5734
+ // src/router.ts
5735
+ init_esm_shims();
5736
+ var NO_RECORD = { clis: null, scope: null, repair: null };
5737
+ function buildInstallRecordView(state, log, claudeDirExists) {
5738
+ if (log === null) return NO_RECORD;
5739
+ const clis = installedClis(log);
5740
+ const broken = clis.includes("claude") && !claudeDirExists;
5741
+ return {
5742
+ clis,
5743
+ scope: log.scope,
5744
+ repair: broken ? repairCommand(log, state.tracks) : null
5745
+ };
5295
5746
  }
5296
- function hintFor(asset) {
5297
- if (asset.scope === "global") return "global scope \u2014 \uC790\uB3D9 \uC0AD\uC81C \uC548 \uD568, \uC218\uAE30 \uC81C\uAC70 \uBA85\uB839\uC744 \uCD9C\uB825\uD55C\uB2E4";
5298
- switch (asset.method) {
5299
- case "plugin":
5300
- return "claude plugin uninstall --scope project";
5301
- case "skill":
5302
- return "npx skills remove";
5303
- case "npm":
5304
- return "npm uninstall --save-dev";
5305
- case "npx-run":
5306
- // shell-script = #492 에서 은퇴한 legacy kind. 옛 로그가 그대로 들어온다.
5307
- case "shell-script":
5308
- case "internal":
5309
- return "\uC790\uB3D9 \uB418\uB3CC\uB9AC\uAE30 \uACBD\uB85C \uC5C6\uC74C \u2014 \uC804\uB7C9 \uC81C\uAC70(`.claude/` \uC0AD\uC81C)\uB85C\uB9CC \uC0AC\uB77C\uC9C4\uB2E4";
5310
- }
5747
+ function repairCommand(log, detected) {
5748
+ const recorded = log.spec.tracks.filter(isTrack);
5749
+ const tracks = recorded.length > 0 ? recorded : detected;
5750
+ const trackArgs = tracks.map((t) => `--track ${t}`).join(" ");
5751
+ return `agent-harness install --reinstall ${trackArgs} --cli claude --scope ${log.scope}`;
5752
+ }
5753
+ function describeInstall(state, record = NO_RECORD) {
5754
+ const tracks = state.tracks.length > 0 ? state.tracks.join(", ") : "(none detected)";
5755
+ if (record.clis === null) return `Installed here (no record): tracks ${tracks} \xB7 CLI unknown`;
5756
+ const clis = record.clis.length > 0 ? record.clis.join(", ") : "(none)";
5757
+ const head = `Installed here: tracks ${tracks} \xB7 CLI ${clis} \xB7 scope ${record.scope} (record: ${INSTALL_LOG_DIR}/${INSTALL_LOG_FILENAME})`;
5758
+ if (record.repair === null) return head;
5759
+ return [
5760
+ head,
5761
+ "\u26A0 .claude/ is missing but the record says Claude Code is installed. Update cannot rebuild it.",
5762
+ ` Repair: ${record.repair}`
5763
+ ].join("\n");
5311
5764
  }
5312
- async function runInteractiveUninstall(projectDir, deps = {}) {
5313
- const prompts = deps.prompts ?? defaultUninstallPrompts();
5314
- const isTty = deps.isTty ?? (() => Boolean(process.stdin.isTTY));
5315
- const readLog = deps.readLog ?? readInstallLog;
5316
- if (!isTty()) return { ok: false, reason: "no-tty" };
5317
- const log = readLog(projectDir);
5318
- if (!log) return { ok: false, reason: "no-log" };
5319
- prompts.intro("uzys-agent-harness \xB7 uninstall");
5320
- const rows = buildRemovableRows(log.assets);
5321
- const mode = await prompts.selectMode(rows.length);
5322
- if (mode === null) {
5323
- prompts.cancel("Cancelled.");
5324
- return { ok: false, reason: "cancelled" };
5765
+ function buildRouterChoices(_state, record = NO_RECORD) {
5766
+ const broken = typeof record.repair === "string";
5767
+ return [
5768
+ {
5769
+ value: "update",
5770
+ label: "Update",
5771
+ // 이 hint 가 update 동작의 광고다 — 갱신 대상(rules/agents/commands/hooks/skills)과 편집분
5772
+ // 백업을 빠뜨리면 거짓출하가 된다(R-3a · `tests/render-hint-parity.test.ts` 가 derive 로 문다).
5773
+ hint: broken ? ".claude/ missing \u2014 run the repair command above" : "bring installed rules / agents / commands / hooks / skills to this release; add tracks, CLIs or assets on the way \u2014 nothing is removed here, your edits are backed up as *.backup-<time>",
5774
+ enabled: !broken
5775
+ },
5776
+ {
5777
+ value: "uninstall",
5778
+ label: "Uninstall",
5779
+ hint: "remove everything, one CLI, or single assets",
5780
+ enabled: true
5781
+ },
5782
+ {
5783
+ value: "exit",
5784
+ label: "Exit",
5785
+ enabled: true
5786
+ }
5787
+ ];
5788
+ }
5789
+
5790
+ // src/wizard-steps.ts
5791
+ init_esm_shims();
5792
+ var WIZARD_TOTAL = 6;
5793
+ var WIZARD = {
5794
+ TRACKS: { current: 1, total: WIZARD_TOTAL },
5795
+ CLI: { current: 2, total: WIZARD_TOTAL },
5796
+ TARGETS: { current: 3, total: WIZARD_TOTAL },
5797
+ SCOPE: { current: 4, total: WIZARD_TOTAL },
5798
+ CONFIRM: { current: 5, total: WIZARD_TOTAL },
5799
+ INSTALL: { current: 6, total: WIZARD_TOTAL }
5800
+ };
5801
+ var UPDATE_WIZARD_TOTAL = 5;
5802
+ var UPDATE_WIZARD = {
5803
+ TRACKS: { current: 1, total: UPDATE_WIZARD_TOTAL },
5804
+ CLI: { current: 2, total: UPDATE_WIZARD_TOTAL },
5805
+ TARGETS: { current: 3, total: UPDATE_WIZARD_TOTAL },
5806
+ CONFIRM: { current: 4, total: UPDATE_WIZARD_TOTAL },
5807
+ RUN: { current: 5, total: UPDATE_WIZARD_TOTAL }
5808
+ };
5809
+ function stepLabel(step, suffix) {
5810
+ if (!step) return suffix;
5811
+ return `Step ${step.current}/${step.total} \u2014 ${suffix}`;
5812
+ }
5813
+
5814
+ // src/prompts.ts
5815
+ var TRACK_LABELS = {
5816
+ // #533 — base 는 모든 dev 트랙에 이미 들어 있다(dev 트랙 8종 ∪ base = 추가 파일 0, 설계 §5 실측).
5817
+ // 함께 골라도 해는 없어 막지 않고(사용자 결정 2026-09-27), 그 사실만 라벨로 알린다.
5818
+ base: "base \u2014 no stack yet \u2014 every dev track already includes it",
5819
+ tooling: "tooling \u2014 Bash + Markdown meta-project",
5820
+ "csr-supabase": "csr-supabase \u2014 Vite + React + Supabase",
5821
+ "csr-fastify": "csr-fastify \u2014 Vite + React + Fastify",
5822
+ "csr-fastapi": "csr-fastapi \u2014 Vite + React + FastAPI",
5823
+ "ssr-htmx": "ssr-htmx \u2014 htmx + FastAPI",
5824
+ "ssr-nextjs": "ssr-nextjs \u2014 Next.js (App Router)",
5825
+ data: "data \u2014 Python data / DuckDB / PySide6",
5826
+ executive: "executive \u2014 proposals / DD / pitch (no agent-skills)",
5827
+ full: "full \u2014 all dev capabilities",
5828
+ "project-management": "project-management \u2014 PM / Scrum / Jira / Confluence",
5829
+ "growth-marketing": "growth-marketing \u2014 Growth / Marketing / Content"
5830
+ };
5831
+ var VISIBLE_OPTION_DEFS = [];
5832
+ var CLI_BASE_LABELS = {
5833
+ claude: "Claude Code",
5834
+ codex: "Codex (OpenAI)",
5835
+ opencode: "OpenCode (anomalyco)",
5836
+ antigravity: "Antigravity (Google)"
5837
+ };
5838
+ var INSTALL_TARGET_PAGES = [
5839
+ // 2026-08-16 — 트랙이 고르는 자산을 **맨 앞에서** 전부 보여준다. 그전까지 이 항목들은 화면에
5840
+ // 한 번도 안 나온 채 깔렸고, 사용자는 설치가 끝난 뒤 요약에서 처음 봤다.
5841
+ //
5842
+ // 2페이지로 나눈 근거는 실측이다: 최대 트랙(`full`)이 32항목(당시 실측 — rules 6 · agents 9 · hooks 3 ·
5843
+ // skills 14)이라 clack groupMultiselect 의 "페이지당 ≤ ~30행" 제약을 한 페이지로는 못 지킨다.
5844
+ // 가르는 축은 역할이다 — 정책·가드가 한쪽, 실행 능력이 다른 쪽.
5845
+ { label: "Track baseline \u2014 Rules & Hooks (\uC815\uCC45\xB7\uAC00\uB4DC)", cats: [], baseline: ["rules", "hooks"] },
5846
+ {
5847
+ label: "Track baseline \u2014 Agents & Skills (\uC5D0\uC774\uC804\uD2B8\xB7\uC2A4\uD0AC)",
5848
+ cats: [],
5849
+ baseline: ["agents", "skills"]
5850
+ },
5851
+ { label: "Dev Core (Frontend \xB7 Backend \xB7 Data)", cats: ["frontend", "backend", "data"] },
5852
+ { label: "Dev Tools (Security \xB7 Quality \xB7 Understanding)", cats: ["dev-tools", "understanding"] },
5853
+ { label: "Business (PM \xB7 Executive \xB7 Documents)", cats: ["business"] },
5854
+ // v26.85.0 — 코드-퍼스트 비주얼/미디어 제작 (용도별 섹션, 전부 opt-in).
5855
+ { label: "Visual & Media (Slides \xB7 Diagrams \xB7 Motion \xB7 Video)", cats: ["visual-media"] },
5856
+ { label: "Workflow", cats: ["workflow"] }
5857
+ ];
5858
+ var BASELINE_TITLES = {
5859
+ rules: "Rules \u2014 \uC0C1\uC2DC \uC0C1\uC8FC\uD558\uB294 \uB300\uC6D0\uCE59 (4 CLI \uC804\uBD80\uC5D0 \uC124\uCE58)",
5860
+ hooks: "Hooks \u2014 \uACB0\uC815\uB860\uC801 \uAC00\uB4DC (\uD574\uC81C\uD558\uBA74 settings.json \uBC30\uC120\uB3C4 \uD568\uAED8 \uBE60\uC9C4\uB2E4)",
5861
+ agents: "Agents \u2014 \uB3C5\uB9BD \uB808\uC778 (\uB9AC\uBDF0\xB7\uAC80\uC99D)",
5862
+ skills: "Skills \u2014 \uD544\uC694\uD560 \uB54C \uC5F4\uB9AC\uB294 \uC791\uC5C5 \uC808\uCC28"
5863
+ };
5864
+ function buildPageGroups(cats, initialSet, installedSet = /* @__PURE__ */ new Set(), baselineKinds = [], baselineTargets = []) {
5865
+ const installedMark = (value) => installedSet.has(value) ? " \u25CF installed" : "";
5866
+ const groups = {};
5867
+ const flatItems = [];
5868
+ for (const kind of baselineKinds) {
5869
+ const items = baselineTargets.filter((t) => t.kind === kind).map((t) => ({
5870
+ value: t.id,
5871
+ label: ` ${t.name}${installedMark(t.id)}`,
5872
+ ...t.hint ? { hint: t.hint } : {}
5873
+ }));
5874
+ if (items.length === 0) continue;
5875
+ groups[BASELINE_TITLES[kind]] = items;
5876
+ flatItems.push(...items);
5325
5877
  }
5326
- if (mode === "all") {
5327
- const ok2 = await prompts.confirm(
5328
- [
5329
- "\uC804\uB7C9 \uC81C\uAC70 \u2014 \uB418\uB3CC\uB9B4 \uC218 \uC5C6\uB2E4:",
5330
- ` \xB7 \uC790\uC0B0 ${log.assets.length}\uAC1C (\uC790\uB3D9 \uACBD\uB85C\uAC00 \uC788\uB294 \uAC83\uB9CC \uC2E4\uC81C \uC81C\uAC70)`,
5331
- " \xB7 templates \uC0AD\uC81C: `.claude/` \uB4F1 (\uC124\uCE58 \uAE30\uB85D\uB3C4 \uD568\uAED8 \uC0AC\uB77C\uC9C4\uB2E4)",
5332
- " \xB7 `.claude/` \uBC16 \uD30C\uC77C(`.mcp.json` \uB4F1)\uC740 \uC0AD\uC81C\uD558\uC9C0 \uC54A\uACE0 \uC548\uB0B4\uB9CC \uD55C\uB2E4"
5333
- ].join("\n")
5878
+ for (const cat of cats) {
5879
+ const items = [];
5880
+ for (const o of VISIBLE_OPTION_DEFS.filter((d2) => d2.category === cat)) {
5881
+ items.push({
5882
+ value: `option:${o.key}`,
5883
+ // v26.62.3 — group header 와 옵션 사이 시각 hierarchy 강화. label prefix 4 space.
5884
+ label: ` ${o.label} [${o.source}]${installedMark(`option:${o.key}`)}`,
5885
+ hint: o.hint
5886
+ });
5887
+ }
5888
+ const tierOrder = { official: 0, vetted: 1, experimental: 2 };
5889
+ const catAssets = [...EXTERNAL_ASSETS.filter((x) => x.category === cat)].sort(
5890
+ (a, b2) => tierOrder[assetTrustTier(a.id)] - tierOrder[assetTrustTier(b2.id)]
5334
5891
  );
5335
- if (!ok2) {
5336
- prompts.cancel("Cancelled.");
5337
- return { ok: false, reason: "cancelled" };
5892
+ for (const a of catAssets) {
5893
+ const tier = assetTrustTier(a.id);
5894
+ const badge = tier === "official" ? " \u2605 official" : tier === "experimental" ? " \u26A0 experimental (opt-in)" : "";
5895
+ items.push({
5896
+ value: `asset:${a.id}`,
5897
+ label: ` ${a.id} [${a.source}]${badge}${installedMark(`asset:${a.id}`)}`,
5898
+ hint: a.description
5899
+ });
5338
5900
  }
5339
- return { ok: true, options: { projectDir } };
5340
- }
5341
- const picked = await prompts.selectAssets(rows);
5342
- if (picked === null) {
5343
- prompts.cancel("Cancelled.");
5344
- return { ok: false, reason: "cancelled" };
5901
+ if (items.length === 0) continue;
5902
+ const selectedInCat = items.filter((it3) => initialSet.has(it3.value)).length;
5903
+ const header = `${CATEGORY_TITLES[cat]} [${selectedInCat}/${items.length} \u2713 default]`;
5904
+ groups[header] = items;
5905
+ flatItems.push(...items);
5345
5906
  }
5346
- if (picked.length === 0) {
5347
- prompts.outro("\uC544\uBB34\uAC83\uB3C4 \uC120\uD0DD\uD558\uC9C0 \uC54A\uC558\uB2E4 \u2014 \uBCC0\uACBD \uC5C6\uC74C.");
5348
- return { ok: false, reason: "nothing-selected" };
5907
+ return { groups, flatItems };
5908
+ }
5909
+ function assertPagesCoverAllCategories(pages) {
5910
+ const counts = /* @__PURE__ */ new Map();
5911
+ for (const page of pages) {
5912
+ for (const cat of page.cats) counts.set(cat, (counts.get(cat) ?? 0) + 1);
5349
5913
  }
5350
- const ok = await prompts.confirm(
5351
- [
5352
- `\uC120\uD0DD \uC81C\uAC70 (${picked.length}\uAC1C):`,
5353
- ...picked.map((id) => ` \xB7 ${id}`),
5354
- "",
5355
- "templates \uB294 \uADF8\uB300\uB85C \uB454\uB2E4."
5356
- ].join("\n")
5357
- );
5358
- if (!ok) {
5359
- prompts.cancel("Cancelled.");
5360
- return { ok: false, reason: "cancelled" };
5914
+ const missing = CATEGORIES.filter((c3) => !counts.has(c3));
5915
+ const duplicated = CATEGORIES.filter((c3) => (counts.get(c3) ?? 0) > 1);
5916
+ if (missing.length > 0 || duplicated.length > 0) {
5917
+ throw new Error(
5918
+ `INSTALL_TARGET_PAGES drift vs CATEGORIES \u2014 missing=[${missing.join(", ")}] duplicated=[${duplicated.join(", ")}]. \uBAA8\uB4E0 \uCE74\uD14C\uACE0\uB9AC\uB294 \uC815\uD655\uD788 \uD55C \uD398\uC774\uC9C0\uC5D0 \uBC30\uCE58\uD574\uC57C \uD55C\uB2E4 (no-false-ship: wizard \uBBF8\uB178\uCD9C = \uAC70\uC9D3 \uAD11\uACE0).`
5919
+ );
5361
5920
  }
5362
- return { ok: true, options: { projectDir, only: picked.join(",") } };
5363
5921
  }
5364
- function defaultUninstallPrompts() {
5365
- return {
5922
+ assertPagesCoverAllCategories(INSTALL_TARGET_PAGES);
5923
+ function viewportItems(itemCount) {
5924
+ const rows = process.stdout.rows ?? 24;
5925
+ return Math.max(8, Math.min(itemCount, rows - 10));
5926
+ }
5927
+ var defaultPrompts = {
5928
+ intro: (msg) => ge(msg),
5929
+ outro: (msg) => ye(msg),
5930
+ cancel: (msg) => me(msg),
5931
+ selectTracks: async (initial, step, installed = []) => {
5932
+ const locked = new Set(installed);
5933
+ const result = await ve({
5934
+ message: locked.size > 0 ? `${stepLabel(step, "Tracks")} (\u25CF installed = already here, locked \xB7 check a new one to add its rules \xB7 agents \xB7 skills)` : stepLabel(step, "Select Track(s)"),
5935
+ options: TRACKS.map((t) => ({
5936
+ value: t,
5937
+ label: locked.has(t) ? `${TRACK_LABELS[t]} \u25CF installed` : TRACK_LABELS[t]
5938
+ })),
5939
+ ...initial ? { initialValues: initial } : {},
5940
+ maxItems: viewportItems(11),
5941
+ required: true
5942
+ });
5943
+ return q(result) ? null : result;
5944
+ },
5945
+ selectCli: async (initial, step, installed = []) => {
5946
+ const initialValues = initial && initial.length > 0 ? [...initial] : ["claude"];
5947
+ const locked = new Set(installed);
5948
+ const label = (c3) => locked.has(c3) ? `${CLI_BASE_LABELS[c3]} \u25CF installed` : CLI_BASE_LABELS[c3];
5949
+ const result = await ve({
5950
+ message: locked.size > 0 ? `${stepLabel(step, "CLIs")} (\u25CF installed = locked \u2014 remove one with Uninstall \xB7 check another to install its files)` : stepLabel(step, "Target CLI(s)"),
5951
+ options: [
5952
+ { value: "claude", label: label("claude") },
5953
+ { value: "codex", label: label("codex") },
5954
+ { value: "opencode", label: label("opencode") },
5955
+ { value: "antigravity", label: label("antigravity") }
5956
+ ],
5957
+ initialValues,
5958
+ required: true
5959
+ });
5960
+ if (q(result)) return null;
5961
+ return [...result].sort(
5962
+ (a, b2) => CLI_BASE_SORT_ORDER[a] - CLI_BASE_SORT_ORDER[b2]
5963
+ );
5964
+ },
5965
+ selectAction: async (state, record) => {
5966
+ const result = await Ee({
5967
+ message: describeInstall(state, record),
5968
+ options: buildRouterChoices(state, record).map((c3) => {
5969
+ const label = c3.enabled ? c3.label : `${c3.label} [disabled]`;
5970
+ return {
5971
+ value: c3.value,
5972
+ label,
5973
+ ...c3.hint ? { hint: c3.hint } : {},
5974
+ ...c3.enabled ? {} : { disabled: true }
5975
+ };
5976
+ })
5977
+ });
5978
+ return q(result) ? null : result;
5979
+ },
5980
+ /**
5981
+ * v26.64.0 (ADR-020) — Installation scope select. Default Project (D16 — no global write).
5982
+ * Global 은 사용자 명시 opt-in 시에만.
5983
+ */
5984
+ selectScope: async (initial = "project", step) => {
5985
+ const result = await Ee({
5986
+ message: stepLabel(step, "Installation scope"),
5987
+ initialValue: initial,
5988
+ options: [
5989
+ {
5990
+ value: "project",
5991
+ label: "Project",
5992
+ hint: "Install in current directory (committed with your project)"
5993
+ },
5994
+ {
5995
+ value: "global",
5996
+ label: "Global",
5997
+ hint: "Write to ~/.claude/, ~/.codex/, npm -g (shared across all projects)"
5998
+ }
5999
+ ]
6000
+ });
6001
+ return q(result) ? null : result;
6002
+ },
6003
+ selectUpdateGroups: async () => {
6004
+ const result = await ve({
6005
+ message: "What to update (space = toggle \xB7 your edits are always backed up)",
6006
+ options: [
6007
+ { value: "skills", label: "Skills", hint: "installed bundled skills \u2192 latest" },
6008
+ {
6009
+ value: "new-skills",
6010
+ label: "New skills",
6011
+ hint: "bundled skills added since your install"
6012
+ },
6013
+ {
6014
+ value: "rules",
6015
+ label: "Rules \xB7 agents \xB7 commands",
6016
+ hint: "your edited files \u2192 *.backup-<time>"
6017
+ },
6018
+ {
6019
+ value: "anchor",
6020
+ label: "CLAUDE-uzys-harness.md anchor",
6021
+ hint: "your edited anchor \u2192 *.backup-<time>"
6022
+ },
6023
+ {
6024
+ value: "hooks",
6025
+ label: "Hooks \xB7 settings.json",
6026
+ hint: "stale hook refs cleaned"
6027
+ },
6028
+ {
6029
+ value: "external",
6030
+ label: "Codex / OpenCode / Antigravity + external skills",
6031
+ hint: "rendered artifacts + npx skills"
6032
+ }
6033
+ ],
6034
+ initialValues: [...UPDATE_GROUPS],
6035
+ required: true
6036
+ });
6037
+ return q(result) ? null : result;
6038
+ },
6039
+ confirmInstall: async (summary) => {
6040
+ const result = await ue({
6041
+ message: `${summary}
6042
+
6043
+ Proceed?`,
6044
+ initialValue: true
6045
+ });
6046
+ return q(result) ? null : result;
6047
+ },
6048
+ selectInstallTargets: async (initialChecked, step, recap) => {
6049
+ const pages = INSTALL_TARGET_PAGES;
6050
+ const baselineTargets = withBaselineHints(listBaselineTargets({ tracks: recap?.tracks ?? [] }));
6051
+ const initialSet = new Set(initialChecked);
6052
+ const collected = new Set(initialChecked);
6053
+ const recapLine = recap ? `Tracks: ${recap.tracks.join(", ")} \xB7 CLIs: ${recap.cli.join(", ")}` : "";
6054
+ const installedSet = new Set(recap?.installed ?? []);
6055
+ process.stdout.write("\x1B[?1049h");
6056
+ let resultIds = null;
6057
+ let aborted = false;
6058
+ try {
6059
+ let pageIdx = 0;
6060
+ let dir = 1;
6061
+ while (pageIdx < pages.length) {
6062
+ const page = pages[pageIdx];
6063
+ if (!page) break;
6064
+ const { groups, flatItems } = buildPageGroups(
6065
+ page.cats,
6066
+ initialSet,
6067
+ installedSet,
6068
+ page.baseline ?? [],
6069
+ baselineTargets
6070
+ );
6071
+ if (flatItems.length === 0) {
6072
+ pageIdx += dir;
6073
+ if (pageIdx < 0) {
6074
+ aborted = true;
6075
+ break;
6076
+ }
6077
+ continue;
6078
+ }
6079
+ const selectedNow = flatItems.filter((it3) => collected.has(it3.value)).map((it3) => it3.value);
6080
+ const pageDefault = flatItems.filter((it3) => initialSet.has(it3.value)).length;
6081
+ const totalSelected = collected.size;
6082
+ const message = [
6083
+ `Step ${step.current}/${step.total} \xB7 Page ${pageIdx + 1}/${pages.length} \xB7 ${page.label}`,
6084
+ recapLine ? ` ${recapLine}` : "",
6085
+ ` Selected so far: ${totalSelected} items \xB7 This page default \u2713 ${pageDefault}/${flatItems.length}`,
6086
+ // 마커의 뜻을 화면에서 바로 알려준다 — 체크 해제를 제거로 오해하는 것이 이 화면의
6087
+ // 원래 문제였으므로, 마커가 보일 때는 제거 경로를 같은 자리에서 말한다.
6088
+ installedSet.size > 0 ? " \u25CF installed = \uC774\uBBF8 \uC124\uCE58\uB428 \xB7 \uCCB4\uD06C \uD574\uC81C\uD574\uB3C4 \uC81C\uAC70\uB418\uC9C0 \uC54A\uB294\uB2E4 (\uC81C\uAC70: Uninstall \uBA54\uB274 \uB610\uB294 agent-harness uninstall)" : "",
6089
+ " Space toggle \xB7 Enter \u2192 next \xB7 ESC \u2192 prev"
6090
+ ].filter(Boolean).join("\n");
6091
+ const groupOpts = {
6092
+ message,
6093
+ options: groups,
6094
+ initialValues: selectedNow,
6095
+ required: false,
6096
+ selectableGroups: false
6097
+ };
6098
+ const result = await pe(groupOpts);
6099
+ if (q(result)) {
6100
+ if (pageIdx === 0) {
6101
+ aborted = true;
6102
+ break;
6103
+ }
6104
+ dir = -1;
6105
+ pageIdx--;
6106
+ continue;
6107
+ }
6108
+ for (const it3 of flatItems) collected.delete(it3.value);
6109
+ for (const v2 of result) collected.add(v2);
6110
+ dir = 1;
6111
+ pageIdx++;
6112
+ }
6113
+ if (!aborted) {
6114
+ resultIds = [...collected];
6115
+ }
6116
+ } finally {
6117
+ process.stdout.write("\x1B[?1049l");
6118
+ }
6119
+ if (resultIds !== null) {
6120
+ process.stdout.write(
6121
+ `\u25C7 Step ${step.current}/${step.total} \u2014 Install targets \xB7 ${resultIds.length} selected
6122
+ \u2502
6123
+ `
6124
+ );
6125
+ }
6126
+ return resultIds;
6127
+ }
6128
+ };
6129
+
6130
+ // src/uninstall-interactive.ts
6131
+ function buildRemovableRows(assets) {
6132
+ return assets.map((a) => ({
6133
+ value: a.id,
6134
+ label: `${a.id} [${a.method}]${a.version ? ` v${a.version}` : ""}`,
6135
+ hint: hintFor(a)
6136
+ }));
6137
+ }
6138
+ function hintFor(asset) {
6139
+ if (asset.scope === "global") {
6140
+ return "global scope \u2014 not removed automatically; prints the manual command to run";
6141
+ }
6142
+ switch (asset.method) {
6143
+ case "plugin":
6144
+ return "claude plugin uninstall --scope project";
6145
+ case "skill":
6146
+ return "npx skills remove";
6147
+ case "npm":
6148
+ return "npm uninstall --save-dev";
6149
+ case "npx-run":
6150
+ // shell-script = #492 에서 은퇴한 legacy kind. 옛 로그가 그대로 들어온다.
6151
+ case "shell-script":
6152
+ case "internal":
6153
+ return 'no automatic reverse path \u2014 only "Remove everything" clears it';
6154
+ }
6155
+ }
6156
+ function buildUninstallModeChoices(clis, assetCount) {
6157
+ return [
6158
+ {
6159
+ value: "cli",
6160
+ label: "Remove one CLI",
6161
+ 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"',
6162
+ enabled: clis.length > 1
6163
+ },
6164
+ {
6165
+ value: "selected",
6166
+ label: "Remove selected assets",
6167
+ hint: assetCount > 0 ? `pick from the ${assetCount} external assets; templates (.claude/ etc.) stay` : "no external assets recorded",
6168
+ enabled: assetCount > 0
6169
+ },
6170
+ {
6171
+ value: "all",
6172
+ label: "Remove everything",
6173
+ hint: "assets + templates + the install record \u2014 CLI folders (.claude/ \xB7 .codex/ \xB7 .opencode/, whichever exist) are moved aside as <dir>.backup-<time>",
6174
+ enabled: true
6175
+ }
6176
+ ];
6177
+ }
6178
+ function describeOwned(p2) {
6179
+ if (p2.kind === "dir") {
6180
+ return `${p2.path} \u2192 moved aside as ${p2.path.replace(/\/+$/, "")}.backup-<time> (your own files there stay in it)`;
6181
+ }
6182
+ if (p2.kind === "import-block") return `the import block in ${p2.path} (your text stays)`;
6183
+ if (p2.path === "AGENTS.md") return "AGENTS.md harness sections (your ## Project Context stays)";
6184
+ if (p2.path === ".agents/skills/") return ".agents/skills/<harness skills>";
6185
+ return p2.path;
6186
+ }
6187
+ function removalSummary(cli2, installed) {
6188
+ const remaining = installed.filter((c3) => c3 !== cli2);
6189
+ const { exclusive, sharedNowUnowned } = removableFor(cli2, remaining);
6190
+ const lines = [];
6191
+ if (exclusive.length > 0) {
6192
+ lines.push(
6193
+ ` \xB7 ${exclusive.map(describeOwned).join(" \xB7 ")} (only ${CLI_BASE_LABELS[cli2]} uses them)`
6194
+ );
6195
+ }
6196
+ if (sharedNowUnowned.length > 0) {
6197
+ lines.push(
6198
+ ` \xB7 ${sharedNowUnowned.map(describeOwned).join(" \xB7 ")} (${CLI_BASE_LABELS[cli2]} was the last user)`
6199
+ );
6200
+ }
6201
+ const kept = [
6202
+ ...new Set(
6203
+ remaining.flatMap(
6204
+ (c3) => CLI_OWNERSHIP[c3].filter((p2) => p2.kind !== "keep").map((p2) => p2.path)
6205
+ )
6206
+ )
6207
+ ];
6208
+ lines.push(
6209
+ ` \xB7 kept: ${[...kept, ".mcp.json", `install record (clis: ${remaining.join(", ")})`].join(" \xB7 ")}`
6210
+ );
6211
+ return lines;
6212
+ }
6213
+ function buildCliRows(installed) {
6214
+ return installed.map((cli2) => {
6215
+ const { exclusive, sharedNowUnowned } = removableFor(
6216
+ cli2,
6217
+ installed.filter((c3) => c3 !== cli2)
6218
+ );
6219
+ return {
6220
+ value: cli2,
6221
+ label: CLI_BASE_LABELS[cli2],
6222
+ hint: `removes ${[...exclusive, ...sharedNowUnowned].map(describeOwned).join(" \xB7 ")}`
6223
+ };
6224
+ });
6225
+ }
6226
+ async function runInteractiveUninstall(projectDir, deps = {}) {
6227
+ const prompts = deps.prompts ?? defaultUninstallPrompts();
6228
+ const isTty = deps.isTty ?? (() => Boolean(process.stdin.isTTY));
6229
+ const readLog = deps.readLog ?? readInstallLog;
6230
+ if (!isTty()) return { ok: false, reason: "no-tty" };
6231
+ const log = readLog(projectDir);
6232
+ if (!log) return { ok: false, reason: "no-log" };
6233
+ if (!deps.embedded) prompts.intro("uzys-agent-harness \xB7 uninstall");
6234
+ const rows = buildRemovableRows(log.assets);
6235
+ const clis = installedClis(log);
6236
+ const mode = await prompts.selectMode(
6237
+ buildUninstallModeChoices(clis, rows.length),
6238
+ `uzys-agent-harness \xB7 uninstall installed: ${clis.join(", ") || "(none)"} \xB7 assets ${rows.length}`
6239
+ );
6240
+ if (mode === null) {
6241
+ prompts.cancel("Cancelled.");
6242
+ return { ok: false, reason: "cancelled" };
6243
+ }
6244
+ if (mode === "cli") return pickCli(projectDir, clis, prompts);
6245
+ if (mode === "all") {
6246
+ const ok2 = await prompts.confirm(
6247
+ [
6248
+ "Remove everything? This is the same as: agent-harness uninstall --yes",
6249
+ ` \xB7 ${log.assets.length} recorded asset(s) \u2014 only those with an automatic reverse path are removed`,
6250
+ " \xB7 templates: CLI folders (.claude/ \xB7 .codex/ \xB7 .opencode/, whichever exist) are moved aside as <dir>.backup-<time> (your own files there stay in them)",
6251
+ " \xB7 the install record goes too",
6252
+ " \xB7 files outside (.mcp.json etc.) are not deleted \u2014 you get instructions instead"
6253
+ ].join("\n")
6254
+ );
6255
+ if (!ok2) {
6256
+ prompts.cancel("Cancelled.");
6257
+ return { ok: false, reason: "cancelled" };
6258
+ }
6259
+ return { ok: true, options: { projectDir } };
6260
+ }
6261
+ const picked = await prompts.selectAssets(rows);
6262
+ if (picked === null) {
6263
+ prompts.cancel("Cancelled.");
6264
+ return { ok: false, reason: "cancelled" };
6265
+ }
6266
+ if (picked.length === 0) {
6267
+ prompts.outro("Nothing selected \u2014 no changes.");
6268
+ return { ok: false, reason: "nothing-selected" };
6269
+ }
6270
+ const ok = await prompts.confirm(
6271
+ [
6272
+ `Remove ${picked.length} selected asset(s)? This is the same as: agent-harness uninstall --only ${picked.join(",")}`,
6273
+ ...picked.map((id) => ` \xB7 ${id}`),
6274
+ "",
6275
+ "Templates (.claude/ etc.) stay."
6276
+ ].join("\n")
6277
+ );
6278
+ if (!ok) {
6279
+ prompts.cancel("Cancelled.");
6280
+ return { ok: false, reason: "cancelled" };
6281
+ }
6282
+ return { ok: true, options: { projectDir, only: picked.join(",") } };
6283
+ }
6284
+ async function pickCli(projectDir, clis, prompts) {
6285
+ const cli2 = await prompts.selectCli(buildCliRows(clis));
6286
+ if (cli2 === null) {
6287
+ prompts.cancel("Cancelled.");
6288
+ return { ok: false, reason: "cancelled" };
6289
+ }
6290
+ const ok = await prompts.confirm(
6291
+ [
6292
+ `Remove ${cli2}? This is the same as: agent-harness uninstall --cli ${cli2}`,
6293
+ ...removalSummary(cli2, clis)
6294
+ ].join("\n")
6295
+ );
6296
+ if (!ok) {
6297
+ prompts.cancel("Cancelled.");
6298
+ return { ok: false, reason: "cancelled" };
6299
+ }
6300
+ return { ok: true, options: { projectDir, cli: cli2 } };
6301
+ }
6302
+ function defaultUninstallPrompts() {
6303
+ return {
5366
6304
  intro: (m) => ge(m),
5367
6305
  outro: (m) => ye(m),
5368
6306
  cancel: (m) => me(m),
5369
- selectMode: async (rowCount) => {
6307
+ selectMode: async (choices, message) => {
5370
6308
  const r = await Ee({
5371
- message: `\uBB34\uC5C7\uC744 \uC81C\uAC70\uD560\uAE4C? (\uC124\uCE58\uB41C \uC790\uC0B0 ${rowCount}\uAC1C)`,
5372
- options: [
5373
- {
5374
- value: "selected",
5375
- label: "\uD56D\uBAA9 \uC120\uD0DD\uD574\uC11C \uC81C\uAC70",
5376
- hint: "templates(`.claude/` \uB4F1)\uB294 \uADF8\uB300\uB85C \uB454\uB2E4"
5377
- },
5378
- {
5379
- value: "all",
5380
- label: "\uC804\uBD80 \uC81C\uAC70",
5381
- hint: "\uC790\uC0B0 + templates. \uC124\uCE58 \uAE30\uB85D\uB3C4 \uC0AC\uB77C\uC9C4\uB2E4"
5382
- }
5383
- ]
6309
+ message,
6310
+ options: choices.map((c3) => ({
6311
+ value: c3.value,
6312
+ label: c3.enabled ? c3.label : `${c3.label} [disabled]`,
6313
+ hint: c3.hint,
6314
+ ...c3.enabled ? {} : { disabled: true }
6315
+ }))
6316
+ });
6317
+ return q(r) ? null : r;
6318
+ },
6319
+ selectCli: async (rows) => {
6320
+ const r = await Ee({
6321
+ message: 'Which CLI? (the last remaining CLI cannot be removed this way \u2014 use "Remove everything")',
6322
+ options: rows.map((x) => ({ value: x.value, label: x.label, hint: x.hint }))
5384
6323
  });
5385
6324
  return q(r) ? null : r;
5386
6325
  },
5387
6326
  selectAssets: async (rows) => {
5388
6327
  if (rows.length === 0) return [];
5389
6328
  const r = await ve({
5390
- message: "\uC81C\uAC70\uD560 \uD56D\uBAA9 (Space \uD1A0\uAE00 \xB7 Enter \uD655\uC815 \xB7 ESC \uCDE8\uC18C)",
6329
+ message: "Assets to remove (Space toggle \xB7 Enter confirm \xB7 ESC cancel)",
5391
6330
  options: rows.map((x) => ({ value: x.value, label: x.label, hint: x.hint })),
5392
6331
  required: false
5393
6332
  });
@@ -5396,13 +6335,25 @@ function defaultUninstallPrompts() {
5396
6335
  confirm: async (summary) => {
5397
6336
  const r = await ue({ message: `${summary}
5398
6337
 
5399
- \uC9C4\uD589\uD560\uAE4C?`, initialValue: false });
6338
+ Proceed?`, initialValue: false });
5400
6339
  return q(r) ? null : r;
5401
6340
  }
5402
6341
  };
5403
6342
  }
5404
6343
 
5405
6344
  // src/commands/uninstall.ts
6345
+ function templateDirPath(projectDir, rel) {
6346
+ return join19(projectDir, rel.replace(/\/+$/, ""));
6347
+ }
6348
+ function movedAsideLine(projectDir, rel, backup) {
6349
+ return ` ${status.success(`${rel} moved aside \u2192 ${relative2(projectDir, backup)} (your own files there are kept)`)}`;
6350
+ }
6351
+ var MOVE_ASIDE_PREVIEW = (rel) => ` \u25CB move ${rel} aside \u2192 ${rel.replace(/\/+$/, "")}.backup-<time> (your own files there stay in the backup)`;
6352
+ function recordedTemplateDirs(log) {
6353
+ return [log.templates.claudeDir, log.templates.codexDir, log.templates.opencodeDir].filter(
6354
+ (d2) => d2 !== void 0
6355
+ );
6356
+ }
5406
6357
  function uninstallAction(options, deps = {}) {
5407
6358
  const log = deps.log ?? console.log;
5408
6359
  const err = deps.err ?? console.error;
@@ -5410,6 +6361,8 @@ function uninstallAction(options, deps = {}) {
5410
6361
  const spawn = deps.spawn ?? defaultSpawn2;
5411
6362
  const rm = deps.rm ?? defaultRm;
5412
6363
  const writeLog = deps.writeLog ?? writeInstallLog;
6364
+ const moveAside = deps.moveAside ?? backupDir;
6365
+ const harnessRoot = (deps.resolveHarnessRoot ?? defaultHarnessRoot)();
5413
6366
  const projectDir = resolve4(options.projectDir ?? process.cwd());
5414
6367
  const installLog = readInstallLog(projectDir);
5415
6368
  if (!installLog) {
@@ -5418,6 +6371,13 @@ function uninstallAction(options, deps = {}) {
5418
6371
  exit(1);
5419
6372
  return;
5420
6373
  }
6374
+ if (options.cli !== void 0) {
6375
+ removeCliAction(
6376
+ { options, installLog, projectDir, harnessRoot },
6377
+ { log, err, exit, rm, writeLog, moveAside }
6378
+ );
6379
+ return;
6380
+ }
5421
6381
  const selectedIds = parseOnly(options.only);
5422
6382
  if (options.only !== void 0 && selectedIds === null) {
5423
6383
  err(status.failure(c.red("ERROR: --only \uC5D0 \uC790\uC0B0 id \uAC00 \uC5C6\uB2E4")));
@@ -5438,7 +6398,14 @@ function uninstallAction(options, deps = {}) {
5438
6398
  const plan = planReverse(targetAssets, spawn);
5439
6399
  for (const line of headerLines(installLog, selectedIds, targetAssets.length)) log(line);
5440
6400
  if (options.dryRun) {
5441
- for (const line of dryRunLines(plan, installLog, projectDir, keepTemplates, rootFiles)) {
6401
+ for (const line of dryRunLines(
6402
+ plan,
6403
+ installLog,
6404
+ projectDir,
6405
+ keepTemplates,
6406
+ rootFiles,
6407
+ harnessRoot
6408
+ )) {
5442
6409
  log(line);
5443
6410
  }
5444
6411
  exit(0);
@@ -5447,12 +6414,16 @@ function uninstallAction(options, deps = {}) {
5447
6414
  const logSurvives = selectedIds !== null;
5448
6415
  const { succeeded, failed, removedIds } = executeReverse(plan, log, logSurvives);
5449
6416
  if (!keepTemplates) {
5450
- const { rootClaudeMdKept, importStripped, external } = removeTemplates(
6417
+ const { rootClaudeMdKept, importStripped, external, moved } = removeTemplates(
5451
6418
  installLog,
5452
6419
  projectDir,
5453
- rm
6420
+ { rm, moveAside },
6421
+ harnessRoot
5454
6422
  );
5455
- log(` ${status.success(`templates removed: ${formatTemplateList(installLog)}`)}`);
6423
+ for (const m of moved) log(movedAsideLine(projectDir, m.rel, m.backup));
6424
+ if (recordedTemplateDirs(installLog).length === 0) {
6425
+ log(` ${status.success("templates removed: (none)")}`);
6426
+ }
5456
6427
  for (const line of externalRemovalLines(external)) log(line);
5457
6428
  if (importStripped) {
5458
6429
  log(` ${status.success("CLAUDE.md \u2014 harness @import removed (\uBCF8\uBB38 \uBCF4\uC874)")}`);
@@ -5489,863 +6460,715 @@ function summarize(r) {
5489
6460
  if (r.failed > 0)
5490
6461
  return {
5491
6462
  line: c.yellow(`uninstall finished with ${r.failed} skip(s) (${r.succeeded} ok)`),
5492
- code: 1
5493
- };
5494
- if (r.nothingDone)
5495
- return {
5496
- line: c.yellow("\uC544\uBB34\uAC83\uB3C4 \uC790\uB3D9 \uC81C\uAC70\uB418\uC9C0 \uC54A\uC558\uB2E4 \u2014 \uC704 \uC548\uB0B4\uB97C \uB530\uB77C \uC9C1\uC811 \uCC98\uB9AC\uD574\uC57C \uD55C\uB2E4"),
5497
- code: 1
5498
- };
5499
- if (r.logWriteFailed)
5500
- return {
5501
- line: c.yellow("\uC790\uC0B0\uC740 \uC81C\uAC70\uB410\uC73C\uB098 install log \uB97C \uAC31\uC2E0\uD558\uC9C0 \uBABB\uD588\uB2E4 (\uAE30\uB85D\uC774 \uC2E4\uC81C\uC640 \uB2E4\uB974\uB2E4)"),
5502
- code: 1
5503
- };
5504
- return { line: status.success(c.green(`uninstall complete (${r.succeeded} asset(s))`)), code: 0 };
5505
- }
5506
- function headerLines(installLog, selectedIds, targetCount) {
5507
- return [
5508
- "",
5509
- c.bold("uzys-agent-harness \xB7 uninstall"),
5510
- "",
5511
- c.dim(` installed: ${installLog.installedAt}`),
5512
- c.dim(` scope: ${installLog.scope}`),
5513
- c.dim(
5514
- selectedIds ? ` assets: ${targetCount} selected of ${installLog.assets.length} (--only)` : ` assets: ${installLog.assets.length}`
5515
- ),
5516
- ""
5517
- ];
5518
- }
5519
- function executeReverse(plan, log, logSurvives) {
5520
- let succeeded = 0;
5521
- let failed = 0;
5522
- const removedIds = [];
5523
- for (const step of plan.reverseSteps) {
5524
- const result = step.execute();
5525
- if (result.ok) {
5526
- log(` ${status.success(step.label)}`);
5527
- removedIds.push(step.assetId);
5528
- succeeded++;
5529
- } else {
5530
- log(` ${c.yellow("\u2298")} ${step.label} (${result.message ?? "failed"})`);
5531
- failed++;
5532
- }
5533
- }
5534
- const tail = logSurvives ? "\uC790\uB3D9 \uB418\uB3CC\uB9AC\uAE30 \uACBD\uB85C \uC5C6\uC74C, \uAE30\uB85D \uC720\uC9C0" : "\uC790\uB3D9 \uB418\uB3CC\uB9AC\uAE30 \uACBD\uB85C \uC5C6\uC74C";
5535
- for (const asset of plan.noReversePath) {
5536
- log(` ${c.yellow("\u2298")} ${asset.id} (${asset.method}) \u2014 ${tail}`);
5537
- }
5538
- return { succeeded, failed, removedIds };
5539
- }
5540
- function settleLog(ctx, io) {
5541
- const { installLog, projectDir, selectedIds, removedIds } = ctx;
5542
- if (selectedIds) {
5543
- const remaining = installLog.assets.filter((a) => !removedIds.includes(a.id));
5544
- try {
5545
- io.writeLog(projectDir, { ...installLog, assets: remaining });
5546
- io.log(` ${status.success(`install log updated (${remaining.length} asset(s) remain)`)}`);
5547
- } catch (e2) {
5548
- io.err(status.failure(c.red(`ERROR: install log \uAC31\uC2E0 \uC2E4\uD328 \u2014 ${installLogPath(projectDir)}`)));
5549
- io.err(c.dim(` ${e2 instanceof Error ? e2.message : String(e2)}`));
5550
- io.err(c.dim(` \uC2E4\uC81C\uB85C \uC81C\uAC70\uB41C \uC790\uC0B0: ${removedIds.join(", ") || "(\uC5C6\uC74C)"}`));
5551
- return true;
5552
- }
5553
- } else {
5554
- io.rm(join16(projectDir, INSTALL_LOG_DIR));
5555
- io.rm(legacyInstallLogPath(projectDir));
5556
- io.log(` ${status.success("install log removed")}`);
5557
- }
5558
- return false;
5559
- }
5560
- function dryRunLines(plan, installLog, projectDir, keepTemplates, rootFiles) {
5561
- const lines = [c.yellow("[DRY RUN] reverse list (\uC2E4\uC81C \uBCC0\uACBD \uC5C6\uC74C):"), ""];
5562
- if (plan.reverseSteps.length === 0) {
5563
- lines.push(c.dim(" (no project-scope assets to reverse)"));
5564
- }
5565
- lines.push(...plan.reverseSteps.map((s) => ` \u25CB ${s.label}`));
5566
- lines.push(
5567
- ...plan.noReversePath.map(
5568
- (a) => c.dim(` \u2298 ${a.id} (${a.method}) \u2014 \uC790\uB3D9 \uB418\uB3CC\uB9AC\uAE30 \uACBD\uB85C \uC5C6\uC74C, \uAE30\uB85D \uC720\uC9C0`)
5569
- )
5570
- );
5571
- if (!keepTemplates) {
5572
- lines.push(` \u25CB remove templates: ${formatTemplateList(installLog)}`);
5573
- const rootMd = installLog.templates.rootClaudeMd;
5574
- if (rootMd) {
5575
- lines.push(
5576
- rootClaudeMdModified(installLog, projectDir) ? ` \u25CB keep ${rootMd.path} (modified since install \u2014 preserved)` : ` \u25CB remove ${rootMd.path}`
5577
- );
5578
- }
5579
- if (hasRootImport(projectDir)) {
5580
- lines.push(" \u25CB strip harness @import from CLAUDE.md (\uBCF8\uBB38 \uBCF4\uC874)");
5581
- }
5582
- lines.push(...previewExternalLines(installLog, projectDir));
5583
- }
5584
- lines.push(...advisoryLines(plan, projectDir, rootFiles), "");
5585
- return lines;
5586
- }
5587
- function advisoryLines(plan, projectDir, rootFiles) {
5588
- const lines = [];
5589
- if (plan.globalAdvisories.length > 0) {
5590
- lines.push(
5591
- "",
5592
- c.yellow(
5593
- `[GLOBAL] ${plan.globalAdvisories.length} asset(s) at scope=global \u2014 manual removal required (D16):`
5594
- )
5595
- );
5596
- for (const adv of plan.globalAdvisories) {
5597
- lines.push(c.dim(` \xB7 ${adv.asset.id} (${adv.asset.method})`), c.dim(` ${adv.command}`));
5598
- }
5599
- }
5600
- lines.push(...rootFileAdvisoryLines(rootFiles, projectDir));
5601
- return lines;
5602
- }
5603
- function rootFileAdvisoryLines(rootFiles, projectDir) {
5604
- const present = rootFiles.filter((f) => existsSync17(join16(projectDir, f.path)));
5605
- if (present.length === 0) return [];
5606
- return [
5607
- "",
5608
- c.yellow("[ROOT] `.claude/` \uBC16\uC5D0 \uB0A8\uB294 \uAC83 (\uC790\uB3D9\uC73C\uB85C \uC9C0\uC6B0\uC9C0 \uC54A\uB294\uB2E4):"),
5609
- ...present.flatMap((f) => [
5610
- c.dim(
5611
- ` \xB7 ${f.path} \u2014 ${f.change === "created" ? "\uD558\uB124\uC2A4\uAC00 \uC0DD\uC131 (\uC218\uC815\uD55C \uC801 \uC5C6\uC73C\uBA74 \uC0AD\uC81C\uD574\uB3C4 \uC548\uC804)" : "\uAE30\uC874 \uC0AC\uC6A9\uC790 \uD30C\uC77C\uC5D0 \uBCD1\uD569 (\uC9C1\uC811 \uD655\uC778 \uD544\uC694)"}`
5612
- ),
5613
- c.dim(` ${f.notes.join(" / ")}`)
5614
- ])
5615
- ];
5616
- }
5617
- function planReverse(assets, spawn) {
5618
- const reverseSteps = [];
5619
- const globalAdvisories = [];
5620
- const noReversePath = [];
5621
- for (const asset of assets) {
5622
- if (asset.scope === "global") {
5623
- globalAdvisories.push({ asset, command: buildGlobalAdvisoryCmd(asset) });
5624
- continue;
5625
- }
5626
- const step = buildProjectReverseStep(asset, spawn);
5627
- if (step) reverseSteps.push(step);
5628
- else noReversePath.push(asset);
5629
- }
5630
- return { reverseSteps, globalAdvisories, noReversePath };
5631
- }
5632
- function buildProjectReverseStep(asset, spawn) {
5633
- switch (asset.method) {
5634
- case "plugin": {
5635
- const pluginId = asset.detail.pluginId ?? asset.id;
5636
- return {
5637
- assetId: asset.id,
5638
- label: `claude plugin uninstall --scope project ${pluginId}`,
5639
- execute: () => {
5640
- const r = spawn("claude", ["plugin", "uninstall", "--scope", "project", pluginId]);
5641
- return r.status === 0 ? { ok: true } : { ok: false, message: (r.stderr || "").trim() };
5642
- }
5643
- };
5644
- }
5645
- case "skill": {
5646
- const source = asset.detail.source ?? asset.id;
5647
- return {
5648
- assetId: asset.id,
5649
- label: `npx skills remove ${source}`,
5650
- execute: () => {
5651
- const r = spawn("npx", [skillsCliSpec(), "remove", source, "--yes"]);
5652
- return r.status === 0 ? { ok: true } : { ok: false, message: (r.stderr || "").trim() };
5653
- }
5654
- };
5655
- }
5656
- case "npm": {
5657
- const pkg = asset.detail.pkg ?? asset.id;
5658
- return {
5659
- assetId: asset.id,
5660
- label: `npm uninstall --save-dev ${pkg}`,
5661
- execute: () => {
5662
- const r = spawn("npm", ["uninstall", "--save-dev", pkg]);
5663
- return r.status === 0 ? { ok: true } : { ok: false, message: (r.stderr || "").trim() };
5664
- }
5665
- };
5666
- }
5667
- case "npx-run":
5668
- return null;
5669
- case "shell-script":
5670
- return null;
5671
- case "internal":
5672
- return null;
5673
- }
5674
- }
5675
- function unknownIds(installLog, selectedIds) {
5676
- const known = new Set(installLog.assets.map((a) => a.id));
5677
- return selectedIds.filter((id) => !known.has(id));
5678
- }
5679
- function parseOnly(only) {
5680
- if (!only) return null;
5681
- const ids = only.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
5682
- return ids.length > 0 ? ids : null;
5683
- }
5684
- function buildGlobalAdvisoryCmd(asset) {
5685
- switch (asset.method) {
5686
- case "plugin": {
5687
- const pid = asset.detail.pluginId ?? asset.id;
5688
- return `claude plugin uninstall --scope user ${pid}`;
5689
- }
5690
- case "skill": {
5691
- const s = asset.detail.source ?? asset.id;
5692
- return `npx skills remove -g ${s}`;
5693
- }
5694
- case "npm": {
5695
- const pkg = asset.detail.pkg ?? asset.id;
5696
- return `npm uninstall -g ${pkg}`;
5697
- }
5698
- case "npx-run":
5699
- case "shell-script":
5700
- case "internal":
5701
- return "(no standard reverse \u2014 manual)";
5702
- }
6463
+ code: 1
6464
+ };
6465
+ if (r.nothingDone)
6466
+ return {
6467
+ line: c.yellow("\uC544\uBB34\uAC83\uB3C4 \uC790\uB3D9 \uC81C\uAC70\uB418\uC9C0 \uC54A\uC558\uB2E4 \u2014 \uC704 \uC548\uB0B4\uB97C \uB530\uB77C \uC9C1\uC811 \uCC98\uB9AC\uD574\uC57C \uD55C\uB2E4"),
6468
+ code: 1
6469
+ };
6470
+ if (r.logWriteFailed)
6471
+ return {
6472
+ line: c.yellow("\uC790\uC0B0\uC740 \uC81C\uAC70\uB410\uC73C\uB098 install log \uB97C \uAC31\uC2E0\uD558\uC9C0 \uBABB\uD588\uB2E4 (\uAE30\uB85D\uC774 \uC2E4\uC81C\uC640 \uB2E4\uB974\uB2E4)"),
6473
+ code: 1
6474
+ };
6475
+ return { line: status.success(c.green(`uninstall complete (${r.succeeded} asset(s))`)), code: 0 };
5703
6476
  }
5704
- function removeTemplates(log, projectDir, rm) {
5705
- rm(join16(projectDir, log.templates.claudeDir));
5706
- if (log.templates.codexDir) rm(join16(projectDir, log.templates.codexDir));
5707
- if (log.templates.opencodeDir) rm(join16(projectDir, log.templates.opencodeDir));
5708
- const external = removeExternalFiles(log, projectDir, rm);
5709
- const importStripped = stripRootImport(projectDir);
5710
- const rootMd = log.templates.rootClaudeMd;
5711
- if (rootMd) {
5712
- if (rootClaudeMdModified(log, projectDir))
5713
- return { rootClaudeMdKept: true, importStripped, external };
5714
- rm(join16(projectDir, rootMd.path));
5715
- }
5716
- return { rootClaudeMdKept: false, importStripped, external };
6477
+ function headerLines(installLog, selectedIds, targetCount) {
6478
+ return [
6479
+ "",
6480
+ c.bold("uzys-agent-harness \xB7 uninstall"),
6481
+ "",
6482
+ c.dim(` installed: ${installLog.installedAt}`),
6483
+ c.dim(` scope: ${installLog.scope}`),
6484
+ c.dim(
6485
+ selectedIds ? ` assets: ${targetCount} selected of ${installLog.assets.length} (--only)` : ` assets: ${installLog.assets.length}`
6486
+ ),
6487
+ ""
6488
+ ];
5717
6489
  }
5718
- function removeExternalFiles(log, projectDir, rm) {
5719
- const removed = [];
5720
- const kept = [];
5721
- for (const { path, sha256 } of log.externalFiles ?? []) {
5722
- const abs = join16(projectDir, path);
5723
- if (!existsSync17(abs)) continue;
5724
- if (!lstatSync2(abs).isFile()) continue;
5725
- if (hashContent(readFileSync15(abs, "utf8")) !== sha256) {
5726
- kept.push(path);
5727
- continue;
6490
+ function executeReverse(plan, log, logSurvives) {
6491
+ let succeeded = 0;
6492
+ let failed = 0;
6493
+ const removedIds = [];
6494
+ for (const step of plan.reverseSteps) {
6495
+ const result = step.execute();
6496
+ if (result.ok) {
6497
+ log(` ${status.success(step.label)}`);
6498
+ removedIds.push(step.assetId);
6499
+ succeeded++;
6500
+ } else {
6501
+ log(` ${c.yellow("\u2298")} ${step.label} (${result.message ?? "failed"})`);
6502
+ failed++;
5728
6503
  }
5729
- rm(abs);
5730
- removed.push(path);
5731
6504
  }
5732
- for (const path of removed) pruneEmptyDirsUpward(projectDir, dirname5(join16(projectDir, path)));
5733
- return { removed, kept };
6505
+ const tail = logSurvives ? "\uC790\uB3D9 \uB418\uB3CC\uB9AC\uAE30 \uACBD\uB85C \uC5C6\uC74C, \uAE30\uB85D \uC720\uC9C0" : "\uC790\uB3D9 \uB418\uB3CC\uB9AC\uAE30 \uACBD\uB85C \uC5C6\uC74C";
6506
+ for (const asset of plan.noReversePath) {
6507
+ log(` ${c.yellow("\u2298")} ${asset.id} (${asset.method}) \u2014 ${tail}`);
6508
+ }
6509
+ return { succeeded, failed, removedIds };
5734
6510
  }
5735
- function pruneEmptyDirsUpward(projectDir, startDir) {
5736
- let cursor = startDir;
5737
- while (true) {
5738
- const rel = relative2(projectDir, cursor);
5739
- if (rel === "" || rel.startsWith(`..${sep2}`) || rel === "..") return;
6511
+ function settleLog(ctx, io) {
6512
+ const { installLog, projectDir, selectedIds, removedIds } = ctx;
6513
+ if (selectedIds) {
6514
+ const remaining = installLog.assets.filter((a) => !removedIds.includes(a.id));
5740
6515
  try {
5741
- if (readdirSync6(cursor).length > 0) return;
5742
- rmdirSync2(cursor);
5743
- } catch {
5744
- return;
6516
+ io.writeLog(projectDir, { ...installLog, assets: remaining });
6517
+ io.log(` ${status.success(`install log updated (${remaining.length} asset(s) remain)`)}`);
6518
+ } catch (e2) {
6519
+ io.err(status.failure(c.red(`ERROR: install log \uAC31\uC2E0 \uC2E4\uD328 \u2014 ${installLogPath(projectDir)}`)));
6520
+ io.err(c.dim(` ${e2 instanceof Error ? e2.message : String(e2)}`));
6521
+ io.err(c.dim(` \uC2E4\uC81C\uB85C \uC81C\uAC70\uB41C \uC790\uC0B0: ${removedIds.join(", ") || "(\uC5C6\uC74C)"}`));
6522
+ return true;
5745
6523
  }
5746
- cursor = dirname5(cursor);
6524
+ } else {
6525
+ io.rm(join19(projectDir, INSTALL_LOG_DIR));
6526
+ io.rm(legacyInstallLogPath(projectDir));
6527
+ io.log(` ${status.success("install log removed")}`);
5747
6528
  }
6529
+ return false;
5748
6530
  }
5749
- function stripRootImport(projectDir) {
5750
- const current = readRootClaudeMd(projectDir);
5751
- const stripped = current === null ? null : stripHarnessImport(current);
5752
- if (stripped === null) return false;
5753
- try {
5754
- writeFileSync7(join16(projectDir, "CLAUDE.md"), stripped, "utf8");
5755
- return true;
5756
- } catch {
5757
- return false;
6531
+ function dryRunLines(plan, installLog, projectDir, keepTemplates, rootFiles, harnessRoot) {
6532
+ const lines = [c.yellow("[DRY RUN] reverse list (\uC2E4\uC81C \uBCC0\uACBD \uC5C6\uC74C):"), ""];
6533
+ if (plan.reverseSteps.length === 0) {
6534
+ lines.push(c.dim(" (no project-scope assets to reverse)"));
5758
6535
  }
5759
- }
5760
- function previewExternalLines(installLog, projectDir) {
5761
- const lines = [];
5762
- let removable = 0;
5763
- for (const { path, sha256 } of installLog.externalFiles ?? []) {
5764
- const abs = join16(projectDir, path);
5765
- if (!existsSync17(abs)) continue;
5766
- if (hashContent(readFileSync15(abs, "utf8")) !== sha256) {
5767
- lines.push(` \u25CB keep ${path} (modified since install \u2014 preserved)`);
5768
- continue;
6536
+ lines.push(...plan.reverseSteps.map((s) => ` \u25CB ${s.label}`));
6537
+ lines.push(
6538
+ ...plan.noReversePath.map(
6539
+ (a) => c.dim(` \u2298 ${a.id} (${a.method}) \u2014 \uC790\uB3D9 \uB418\uB3CC\uB9AC\uAE30 \uACBD\uB85C \uC5C6\uC74C, \uAE30\uB85D \uC720\uC9C0`)
6540
+ )
6541
+ );
6542
+ if (!keepTemplates) {
6543
+ const dirs = recordedTemplateDirs(installLog);
6544
+ for (const rel of dirs) {
6545
+ if (existsSync18(join19(projectDir, rel))) lines.push(MOVE_ASIDE_PREVIEW(rel));
5769
6546
  }
5770
- removable += 1;
6547
+ if (dirs.length === 0) lines.push(" \u25CB remove templates: (none)");
6548
+ const rootMd = installLog.templates.rootClaudeMd;
6549
+ if (rootMd) {
6550
+ lines.push(
6551
+ rootClaudeMdModified(installLog, projectDir) ? ` \u25CB keep ${rootMd.path} (modified since install \u2014 preserved)` : ` \u25CB remove ${rootMd.path}`
6552
+ );
6553
+ }
6554
+ if (hasRootImport(projectDir)) {
6555
+ lines.push(" \u25CB strip harness @import from CLAUDE.md (\uBCF8\uBB38 \uBCF4\uC874)");
6556
+ }
6557
+ lines.push(...previewExternalLines(installLog, projectDir, harnessRoot, dirs));
5771
6558
  }
5772
- if (removable > 0) lines.unshift(` \u25CB remove ${removable} CLI output file(s)`);
6559
+ lines.push(...advisoryLines(plan, projectDir, rootFiles), "");
5773
6560
  return lines;
5774
6561
  }
5775
- function externalRemovalLines(external) {
6562
+ function advisoryLines(plan, projectDir, rootFiles) {
5776
6563
  const lines = [];
5777
- if (external.removed.length > 0) {
5778
- lines.push(` ${status.success(`CLI outputs removed: ${external.removed.length} file(s)`)}`);
5779
- }
5780
- for (const path of external.kept) {
6564
+ if (plan.globalAdvisories.length > 0) {
5781
6565
  lines.push(
5782
- ` ${c.yellow("\u2298")} ${path} kept \u2014 modified since install. Remove manually if intended.`
6566
+ "",
6567
+ c.yellow(
6568
+ `[GLOBAL] ${plan.globalAdvisories.length} asset(s) at scope=global \u2014 manual removal required (D16):`
6569
+ )
5783
6570
  );
6571
+ for (const adv of plan.globalAdvisories) {
6572
+ lines.push(c.dim(` \xB7 ${adv.asset.id} (${adv.asset.method})`), c.dim(` ${adv.command}`));
6573
+ }
5784
6574
  }
6575
+ lines.push(...rootFileAdvisoryLines(rootFiles, projectDir));
5785
6576
  return lines;
5786
6577
  }
5787
- function hasRootImport(projectDir) {
5788
- const current = readRootClaudeMd(projectDir);
5789
- return current !== null && stripHarnessImport(current) !== null;
6578
+ function rootFileAdvisoryLines(rootFiles, projectDir) {
6579
+ const present = rootFiles.filter((f) => existsSync18(join19(projectDir, f.path)));
6580
+ if (present.length === 0) return [];
6581
+ return [
6582
+ "",
6583
+ c.yellow("[ROOT] `.claude/` \uBC16\uC5D0 \uB0A8\uB294 \uAC83 (\uC790\uB3D9\uC73C\uB85C \uC9C0\uC6B0\uC9C0 \uC54A\uB294\uB2E4):"),
6584
+ ...present.flatMap((f) => [
6585
+ c.dim(
6586
+ ` \xB7 ${f.path} \u2014 ${f.change === "created" ? "\uD558\uB124\uC2A4\uAC00 \uC0DD\uC131 (\uC218\uC815\uD55C \uC801 \uC5C6\uC73C\uBA74 \uC0AD\uC81C\uD574\uB3C4 \uC548\uC804)" : "\uAE30\uC874 \uC0AC\uC6A9\uC790 \uD30C\uC77C\uC5D0 \uBCD1\uD569 (\uC9C1\uC811 \uD655\uC778 \uD544\uC694)"}`
6587
+ ),
6588
+ c.dim(` ${f.notes.join(" / ")}`)
6589
+ ])
6590
+ ];
5790
6591
  }
5791
- function readRootClaudeMd(projectDir) {
5792
- const path = join16(projectDir, "CLAUDE.md");
5793
- if (!existsSync17(path)) return null;
5794
- try {
5795
- return readFileSync15(path, "utf8");
5796
- } catch {
6592
+ var TEMPLATE_DIR_FIELD = {
6593
+ claude: "claudeDir",
6594
+ codex: "codexDir",
6595
+ opencode: "opencodeDir"
6596
+ };
6597
+ function resolveCliTarget(ctx, io) {
6598
+ const { options, installLog } = ctx;
6599
+ if (options.only !== void 0 || options.keepTemplates) {
6600
+ io.err(status.failure(c.red("ERROR: --cli \uB294 --only \xB7 --keep-templates \uC640 \uD568\uAED8 \uC4F8 \uC218 \uC5C6\uB2E4")));
6601
+ io.err(
6602
+ c.dim(
6603
+ " CLI \uD558\uB098 \uC81C\uAC70: uninstall --cli <name> / \uC790\uC0B0 \uD558\uB098 \uC81C\uAC70: uninstall --only <id>"
6604
+ )
6605
+ );
6606
+ io.exit(1);
5797
6607
  return null;
5798
6608
  }
5799
- }
5800
- function rootClaudeMdModified(log, projectDir) {
5801
- const rootMd = log.templates.rootClaudeMd;
5802
- if (!rootMd) return false;
5803
- const path = join16(projectDir, rootMd.path);
5804
- if (!existsSync17(path)) return false;
5805
- return hashContent(readFileSync15(path, "utf8")) !== rootMd.sha256;
5806
- }
5807
- function formatTemplateList(log) {
5808
- const items = [log.templates.claudeDir];
5809
- if (log.templates.codexDir) items.push(log.templates.codexDir);
5810
- if (log.templates.opencodeDir) items.push(log.templates.opencodeDir);
5811
- return items.join(", ");
5812
- }
5813
- function defaultSpawn2(cmd, args) {
5814
- return spawnSync2(cmd, [...args], { encoding: "utf8", stdio: "pipe", timeout: 12e4 });
5815
- }
5816
- function defaultRm(path) {
5817
- if (existsSync17(path)) {
5818
- rmSync(path, { recursive: true, force: true });
6609
+ const target = (options.cli ?? "").trim();
6610
+ if (!isCliBase(target)) {
6611
+ io.err(status.failure(c.red(`ERROR: Invalid --cli value: ${target || "(empty)"}`)));
6612
+ io.err(c.dim(` Must be one of: ${CLI_BASES.join(" | ")}`));
6613
+ io.exit(1);
6614
+ return null;
5819
6615
  }
5820
- }
5821
- function registerUninstallCommand(cli2) {
5822
- cli2.command("uninstall", "Uninstall harness assets (log-based reverse)").option("--project-dir <path>", "[Project] Target project directory", {
5823
- default: process.cwd()
5824
- }).option("--dry-run", "[Mode] List reverse steps without executing").option(
5825
- "--keep-templates",
5826
- "[Mode] Keep `.claude/`, `.codex/`, `.opencode/` templates (remove only external assets)"
5827
- ).option(
5828
- "--only <ids>",
5829
- "[Scope] Remove only these assets (comma-separated ids from `agent-harness list`). Templates untouched"
5830
- ).option("--yes", "[Mode] Skip the interactive picker and remove everything (non-interactive)").action(async (options) => {
5831
- await dispatchUninstall(options);
5832
- });
5833
- }
5834
- function shouldRunInteractive(options, isTty) {
5835
- if (!isTty) return false;
5836
- if (options.yes || options.dryRun) return false;
5837
- return options.only === void 0;
5838
- }
5839
- async function dispatchUninstall(options) {
5840
- if (!shouldRunInteractive(options, Boolean(process.stdin.isTTY))) {
5841
- uninstallAction(options);
5842
- return;
6616
+ const installed = installedClis(installLog);
6617
+ if (!installed.includes(target)) {
6618
+ io.err(status.failure(c.red(`ERROR: ${target} is not installed in this project`)));
6619
+ io.err(c.dim(` installed: ${installed.join(", ") || "(none)"}`));
6620
+ io.exit(1);
6621
+ return null;
5843
6622
  }
5844
- const projectDir = resolve4(options.projectDir ?? process.cwd());
5845
- const picked = await runInteractiveUninstall(projectDir);
5846
- if (!picked.ok || !picked.options) {
5847
- if (picked.reason === "no-log") {
5848
- console.error(
5849
- status.failure(c.red(`ERROR: install log not found at ${installLogPath(projectDir)}`))
5850
- );
5851
- console.error(c.dim(" Nothing installed here by agent-harness."));
5852
- process.exit(1);
5853
- }
5854
- return;
6623
+ if (installed.length <= 1) {
6624
+ io.err(status.failure(c.red(`ERROR: ${target} \uB294 \uC774 \uD504\uB85C\uC81D\uD2B8\uC758 \uB9C8\uC9C0\uB9C9 CLI \uB2E4`)));
6625
+ 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)"));
6626
+ io.exit(1);
6627
+ return null;
5855
6628
  }
5856
- uninstallAction(picked.options);
6629
+ return target;
5857
6630
  }
5858
-
5859
- // src/commands/update.ts
5860
- init_esm_shims();
5861
- import { resolve as resolve5 } from "path";
5862
-
5863
- // src/state.ts
5864
- init_esm_shims();
5865
- import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
5866
- import { join as join17 } from "path";
5867
- var META_FILE = ".claude/.installed-tracks";
5868
- var LEGACY_SIGNATURES = [
5869
- { rule: "htmx.md", track: "ssr-htmx" },
5870
- { rule: "nextjs.md", track: "ssr-nextjs" },
5871
- { rule: "data-analysis.md", track: "data" },
5872
- { rule: "pyside6.md", track: "data" },
5873
- { rule: "cli-development.md", track: "tooling" }
5874
- ];
5875
- function detectInstallState(projectDir) {
5876
- const claudeDir = join17(projectDir, ".claude");
5877
- const hasClaudeDir = existsSync18(claudeDir);
5878
- if (hasClaudeDir) {
5879
- const metaPath = join17(projectDir, META_FILE);
5880
- if (existsSync18(metaPath)) {
5881
- const tracks2 = readMetafile(metaPath);
5882
- return { state: "existing", tracks: tracks2, source: "metafile", hasClaudeDir: true };
5883
- }
5884
- const tracks = inferFromLegacySignatures(projectDir);
5885
- return { state: "existing", tracks, source: "legacy", hasClaudeDir: true };
5886
- }
5887
- const log = readInstallLog(projectDir);
5888
- if (log) {
5889
- return {
5890
- state: "existing",
5891
- tracks: tracksFromLog(log.spec.tracks),
5892
- source: "install-log",
5893
- hasClaudeDir: false
5894
- };
5895
- }
5896
- return { state: "new", tracks: [], source: "none", hasClaudeDir: false };
6631
+ function underAny(path, owned) {
6632
+ return owned.some((o) => o.endsWith("/") ? path.startsWith(o) : path === o);
5897
6633
  }
5898
- function tracksFromLog(tracks) {
5899
- return [...new Set(tracks.filter(isTrack))].sort();
6634
+ function planCliRemoval(target, remaining, installLog) {
6635
+ const { exclusive, sharedNowUnowned } = removableFor(target, remaining);
6636
+ const owned = [...exclusive, ...sharedNowUnowned];
6637
+ const recorded = owned.filter((o) => o.kind === "recorded").map((o) => o.path);
6638
+ const scopedFiles = (installLog.externalFiles ?? []).filter(
6639
+ (f) => underAny(f.path, recorded)
6640
+ );
6641
+ return {
6642
+ dirs: owned.filter((o) => o.kind === "dir").map((o) => o.path),
6643
+ scoped: { ...installLog, externalFiles: scopedFiles },
6644
+ anchor: owned.some((o) => o.kind === "anchor"),
6645
+ importBlock: owned.some((o) => o.kind === "import-block")
6646
+ };
5900
6647
  }
5901
- function readMetafile(path) {
5902
- const raw = readFileSync16(path, "utf8");
5903
- const seen = /* @__PURE__ */ new Set();
5904
- for (const line of raw.split(/\s+/)) {
5905
- const trimmed = line.trim();
5906
- if (isTrack(trimmed)) {
5907
- seen.add(trimmed);
5908
- }
6648
+ function removeCliHeader(target, installed, remaining) {
6649
+ return [
6650
+ "",
6651
+ c.bold(`uzys-agent-harness \xB7 uninstall --cli ${target}`),
6652
+ "",
6653
+ c.dim(` installed: ${installed.join(", ")}`),
6654
+ c.dim(` removing: ${target}`),
6655
+ c.dim(` remaining: ${remaining.join(", ")}`),
6656
+ ""
6657
+ ];
6658
+ }
6659
+ function removeCliDryRunLines(plan, ctx, remaining) {
6660
+ const { installLog, projectDir, harnessRoot } = ctx;
6661
+ const lines = [c.yellow("[DRY RUN] CLI \uC81C\uAC70 \uBBF8\uB9AC\uBCF4\uAE30 (\uC2E4\uC81C \uBCC0\uACBD \uC5C6\uC74C):"), ""];
6662
+ for (const dir of plan.dirs) {
6663
+ if (existsSync18(join19(projectDir, dir))) lines.push(MOVE_ASIDE_PREVIEW(dir));
6664
+ }
6665
+ lines.push(...previewExternalLines(plan.scoped, projectDir, harnessRoot));
6666
+ const rootMd = plan.anchor ? installLog.templates.rootClaudeMd : void 0;
6667
+ if (rootMd && existsSync18(join19(projectDir, rootMd.path))) {
6668
+ lines.push(
6669
+ rootClaudeMdModified(installLog, projectDir) ? ` \u25CB keep ${rootMd.path} (modified since install \u2014 preserved)` : ` \u25CB remove ${rootMd.path}`
6670
+ );
5909
6671
  }
5910
- return [...seen].sort();
5911
- }
5912
- function inferFromLegacySignatures(projectDir) {
5913
- const rulesDir = join17(projectDir, ".claude/rules");
5914
- if (!existsSync18(rulesDir)) {
5915
- return [];
6672
+ if (plan.importBlock && hasRootImport(projectDir)) {
6673
+ lines.push(" \u25CB strip harness @import from CLAUDE.md (\uBCF8\uBB38 \uBCF4\uC874)");
5916
6674
  }
5917
- const found = /* @__PURE__ */ new Set();
5918
- for (const sig of LEGACY_SIGNATURES) {
5919
- if (existsSync18(join17(rulesDir, sig.rule))) {
5920
- found.add(sig.track);
5921
- }
6675
+ lines.push(` \u25CB install log updated (clis: ${remaining.join(", ")})`, "");
6676
+ return lines;
6677
+ }
6678
+ function removeHarnessAnchor(ctx, io) {
6679
+ const { installLog, projectDir } = ctx;
6680
+ const rootMd = installLog.templates.rootClaudeMd;
6681
+ if (!rootMd || !existsSync18(join19(projectDir, rootMd.path))) return false;
6682
+ if (rootClaudeMdModified(installLog, projectDir)) {
6683
+ io.log(
6684
+ ` ${c.yellow("\u2298")} ${rootMd.path} kept \u2014 modified since install. Remove manually if intended.`
6685
+ );
6686
+ return false;
5922
6687
  }
5923
- return [...found].sort();
6688
+ io.rm(join19(projectDir, rootMd.path));
6689
+ io.log(` ${status.success(`${rootMd.path} removed`)}`);
6690
+ return true;
5924
6691
  }
5925
-
5926
- // src/commands/update.ts
5927
- function updateAction(options = {}, deps = {}) {
5928
- const log = deps.log ?? console.log;
5929
- const err = deps.err ?? console.error;
5930
- const exit = deps.exit ?? ((code) => process.exit(code));
5931
- const detect = deps.detect ?? detectInstallState;
5932
- const execute = deps.execute ?? executeSpec;
5933
- const projectDir = resolve5(options.projectDir ?? process.cwd());
5934
- const state = detect(projectDir);
5935
- if (state.state === "new") {
5936
- err(status.failure(c.red(`No harness install found at ${projectDir}`)));
5937
- err(c.dim(" Run `agent-harness install --track <name>` first."));
5938
- exit(1);
6692
+ function settleCliLog(ctx, target, remaining, recovered, anchorRemoved, removedDirs) {
6693
+ const { installLog } = ctx;
6694
+ const next = {
6695
+ ...installLog,
6696
+ spec: {
6697
+ ...installLog.spec,
6698
+ // `spec.cli` 는 손대지 않는다 — "마지막 install 이 요청한 CLI" 라는 뜻이고, 빼면 빈 배열이
6699
+ // 된다(재리뷰 BLOCKER-3: 외부 스킬 refresh 가 그 값으로 `--agent`·`--copy` 를 조립하다
6700
+ // 빈 배열이면 둘 다 빠져 Claude 몫이 조용히 빠졌다). 깔린 집합은 `clis` 하나가 말한다.
6701
+ clis: [...remaining]
6702
+ },
6703
+ templates: { ...installLog.templates }
6704
+ };
6705
+ const field = TEMPLATE_DIR_FIELD[target];
6706
+ if (field) delete next.templates[field];
6707
+ if (anchorRemoved) delete next.templates.rootClaudeMd;
6708
+ const survivors = (installLog.externalFiles ?? []).filter(
6709
+ (f) => !recovered.has(f.path) && !underAny(f.path, removedDirs)
6710
+ );
6711
+ if (survivors.length > 0) next.externalFiles = survivors;
6712
+ else delete next.externalFiles;
6713
+ return next;
6714
+ }
6715
+ function removeCliAction(ctx, io) {
6716
+ const target = resolveCliTarget(ctx, io);
6717
+ if (target === null) return;
6718
+ const { options, installLog, projectDir, harnessRoot } = ctx;
6719
+ const installed = installedClis(installLog);
6720
+ const remaining = installed.filter((cli2) => cli2 !== target);
6721
+ const plan = planCliRemoval(target, remaining, installLog);
6722
+ for (const line of removeCliHeader(target, installed, remaining)) io.log(line);
6723
+ if (options.dryRun) {
6724
+ for (const line of removeCliDryRunLines(plan, ctx, remaining)) io.log(line);
6725
+ io.exit(0);
5939
6726
  return;
5940
6727
  }
5941
- const rawOnly = options.only === void 0 ? [] : [options.only].flat().map((v2) => typeof v2 === "string" ? v2 : "(missing value)");
5942
- const parsed = parseUpdateOnly(rawOnly);
5943
- if (!parsed.ok) {
5944
- err(status.failure(c.red(`Unknown --only value: ${parsed.invalid.join(", ")}`)));
5945
- err(c.dim(` Valid groups: ${UPDATE_GROUPS.join(" | ")}`));
5946
- exit(1);
6728
+ for (const dir of plan.dirs) {
6729
+ if (!existsSync18(join19(projectDir, dir))) continue;
6730
+ const backup = io.moveAside(templateDirPath(projectDir, dir));
6731
+ if (backup) io.log(movedAsideLine(projectDir, dir, backup));
6732
+ }
6733
+ const external = removeExternalFiles(plan.scoped, projectDir, io.rm, harnessRoot);
6734
+ for (const line of externalRemovalLines(external)) io.log(line);
6735
+ if (plan.importBlock && stripRootImport(projectDir)) {
6736
+ io.log(` ${status.success("CLAUDE.md \u2014 harness @import removed (\uBCF8\uBB38 \uBCF4\uC874)")}`);
6737
+ }
6738
+ const anchorRemoved = plan.anchor ? removeHarnessAnchor(ctx, io) : false;
6739
+ const recovered = /* @__PURE__ */ new Set([...external.removed, ...external.stripped]);
6740
+ const next = settleCliLog(ctx, target, remaining, recovered, anchorRemoved, plan.dirs);
6741
+ try {
6742
+ io.writeLog(projectDir, next);
6743
+ io.log(` ${status.success(`install log updated (clis: ${remaining.join(", ")})`)}`);
6744
+ } catch (e2) {
6745
+ io.err(status.failure(c.red(`ERROR: install log \uAC31\uC2E0 \uC2E4\uD328 \u2014 ${installLogPath(projectDir)}`)));
6746
+ io.err(c.dim(` ${e2 instanceof Error ? e2.message : String(e2)}`));
6747
+ io.log("");
6748
+ io.log(
6749
+ 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)`)
6750
+ );
6751
+ io.exit(1);
5947
6752
  return;
5948
6753
  }
5949
- log(c.dim(`Updating installed harness files in ${projectDir}`));
5950
- execute(buildUpdateSpec(projectDir, state.tracks, parsed.groups), {
5951
- log,
5952
- err,
5953
- exit,
5954
- mode: "update"
5955
- });
5956
- }
5957
- function registerUpdateCommand(cli2) {
5958
- cli2.command(
5959
- "update",
5960
- "Refresh installed policy files (rules / agents / commands / hooks / skills)"
5961
- ).option("--project-dir <path>", "[Project] Target project directory", {
5962
- default: process.cwd()
5963
- }).option(
5964
- "--only <group>",
5965
- `[Scope] Update only this group (repeatable): ${UPDATE_GROUPS.join(" | ")}`
5966
- ).action((options) => {
5967
- updateAction(options);
5968
- });
6754
+ io.log("");
6755
+ io.log(status.success(c.green(`${target} removed (remaining: ${remaining.join(", ")})`)));
6756
+ io.exit(0);
5969
6757
  }
5970
-
5971
- // src/interactive.ts
5972
- init_esm_shims();
5973
-
5974
- // src/prompts.ts
5975
- init_esm_shims();
5976
-
5977
- // src/router.ts
5978
- init_esm_shims();
5979
- function buildRouterChoices(state) {
5980
- const detected = state.tracks.length > 0 ? state.tracks.join(", ") : "(none detected)";
5981
- return [
5982
- {
5983
- value: "add",
5984
- label: "Add a new Track",
5985
- hint: `Current: ${detected}`,
5986
- enabled: true
5987
- },
5988
- {
5989
- value: "update",
5990
- label: "Update policy files (auto-backup)",
5991
- // v26.126.0 (R-3a) — skills 가 목록에 들어왔다. 이 문구가 곧 update 의 광고이고
5992
- // (update 는 위저드로만 도달한다) 실동작과 어긋나면 그 자체로 거짓출하다.
5993
- hint: "Refresh rules / agents / commands / hooks / skills \u2014 your edits are backed up",
5994
- enabled: true
5995
- },
5996
- {
5997
- value: "remove",
5998
- label: "Remove a Track (unsupported)",
5999
- hint: "Manual edit of .claude/ required \u2014 not automated",
6000
- enabled: false
6001
- },
6002
- {
6003
- value: "reinstall",
6004
- label: "Reinstall (backs up current .claude/ first)",
6005
- hint: "Use when state is corrupted",
6006
- enabled: true
6007
- },
6008
- {
6009
- value: "exit",
6010
- label: "Exit",
6011
- enabled: true
6758
+ function planReverse(assets, spawn) {
6759
+ const reverseSteps = [];
6760
+ const globalAdvisories = [];
6761
+ const noReversePath = [];
6762
+ for (const asset of assets) {
6763
+ if (asset.scope === "global") {
6764
+ globalAdvisories.push({ asset, command: buildGlobalAdvisoryCmd(asset) });
6765
+ continue;
6012
6766
  }
6013
- ];
6767
+ const step = buildProjectReverseStep(asset, spawn);
6768
+ if (step) reverseSteps.push(step);
6769
+ else noReversePath.push(asset);
6770
+ }
6771
+ return { reverseSteps, globalAdvisories, noReversePath };
6014
6772
  }
6015
- function summarizeState(state) {
6016
- if (state.state === "new") {
6017
- return "No prior install detected \u2014 new install flow.";
6773
+ function buildProjectReverseStep(asset, spawn) {
6774
+ switch (asset.method) {
6775
+ case "plugin": {
6776
+ const pluginId = asset.detail.pluginId ?? asset.id;
6777
+ return {
6778
+ assetId: asset.id,
6779
+ label: `claude plugin uninstall --scope project ${pluginId}`,
6780
+ execute: () => {
6781
+ const r = spawn("claude", ["plugin", "uninstall", "--scope", "project", pluginId]);
6782
+ return r.status === 0 ? { ok: true } : { ok: false, message: (r.stderr || "").trim() };
6783
+ }
6784
+ };
6785
+ }
6786
+ case "skill": {
6787
+ const source = asset.detail.source ?? asset.id;
6788
+ return {
6789
+ assetId: asset.id,
6790
+ label: `npx skills remove ${source}`,
6791
+ execute: () => {
6792
+ const r = spawn("npx", [skillsCliSpec(), "remove", source, "--yes"]);
6793
+ return r.status === 0 ? { ok: true } : { ok: false, message: (r.stderr || "").trim() };
6794
+ }
6795
+ };
6796
+ }
6797
+ case "npm": {
6798
+ const pkg = asset.detail.pkg ?? asset.id;
6799
+ return {
6800
+ assetId: asset.id,
6801
+ label: `npm uninstall --save-dev ${pkg}`,
6802
+ execute: () => {
6803
+ const r = spawn("npm", ["uninstall", "--save-dev", pkg]);
6804
+ return r.status === 0 ? { ok: true } : { ok: false, message: (r.stderr || "").trim() };
6805
+ }
6806
+ };
6807
+ }
6808
+ case "npx-run":
6809
+ return null;
6810
+ case "shell-script":
6811
+ return null;
6812
+ case "internal":
6813
+ return null;
6018
6814
  }
6019
- const trackList = state.tracks.length > 0 ? state.tracks.join(", ") : "(no tracks resolved)";
6020
- const sourceLabel = state.source === "metafile" ? "via .claude/.installed-tracks" : state.source === "legacy" ? "via legacy rules/*.md heuristic" : state.source === "install-log" ? "via .uzys-agent-harness/ install log" : "via no source";
6021
- return `Existing install detected ${sourceLabel}. Tracks: ${trackList}.`;
6022
6815
  }
6023
-
6024
- // src/wizard-steps.ts
6025
- init_esm_shims();
6026
- var WIZARD_TOTAL = 6;
6027
- var WIZARD = {
6028
- TRACKS: { current: 1, total: WIZARD_TOTAL },
6029
- CLI: { current: 2, total: WIZARD_TOTAL },
6030
- TARGETS: { current: 3, total: WIZARD_TOTAL },
6031
- SCOPE: { current: 4, total: WIZARD_TOTAL },
6032
- CONFIRM: { current: 5, total: WIZARD_TOTAL },
6033
- INSTALL: { current: 6, total: WIZARD_TOTAL }
6034
- };
6035
- function stepLabel(step, suffix) {
6036
- if (!step) return suffix;
6037
- return `Step ${step.current}/${step.total} \u2014 ${suffix}`;
6816
+ function unknownIds(installLog, selectedIds) {
6817
+ const known = new Set(installLog.assets.map((a) => a.id));
6818
+ return selectedIds.filter((id) => !known.has(id));
6038
6819
  }
6039
-
6040
- // src/prompts.ts
6041
- var TRACK_LABELS = {
6042
- base: "base \u2014 principles \xB7 methodology \xB7 tests only (no stack)",
6043
- tooling: "tooling \u2014 Bash + Markdown meta-project",
6044
- "csr-supabase": "csr-supabase \u2014 Vite + React + Supabase",
6045
- "csr-fastify": "csr-fastify \u2014 Vite + React + Fastify",
6046
- "csr-fastapi": "csr-fastapi \u2014 Vite + React + FastAPI",
6047
- "ssr-htmx": "ssr-htmx \u2014 htmx + FastAPI",
6048
- "ssr-nextjs": "ssr-nextjs \u2014 Next.js (App Router)",
6049
- data: "data \u2014 Python data / DuckDB / PySide6",
6050
- executive: "executive \u2014 proposals / DD / pitch (no agent-skills)",
6051
- full: "full \u2014 all dev capabilities",
6052
- "project-management": "project-management \u2014 PM / Scrum / Jira / Confluence",
6053
- "growth-marketing": "growth-marketing \u2014 Growth / Marketing / Content"
6054
- };
6055
- var VISIBLE_OPTION_DEFS = [];
6056
- var CLI_BASE_LABELS = {
6057
- claude: "Claude Code",
6058
- codex: "Codex (OpenAI)",
6059
- opencode: "OpenCode (anomalyco)",
6060
- antigravity: "Antigravity (Google)"
6061
- };
6062
- var INSTALL_TARGET_PAGES = [
6063
- // 2026-08-16 — 트랙이 고르는 자산을 **맨 앞에서** 전부 보여준다. 그전까지 이 항목들은 화면에
6064
- // 한 번도 안 나온 채 깔렸고, 사용자는 설치가 끝난 뒤 요약에서 처음 봤다.
6065
- //
6066
- // 2페이지로 나눈 근거는 실측이다: 최대 트랙(`full`)이 32항목(당시 실측 — rules 6 · agents 9 · hooks 3 ·
6067
- // skills 14)이라 clack groupMultiselect 의 "페이지당 ≤ ~30행" 제약을 한 페이지로는 못 지킨다.
6068
- // 가르는 축은 역할이다 — 정책·가드가 한쪽, 실행 능력이 다른 쪽.
6069
- { label: "Track baseline \u2014 Rules & Hooks (\uC815\uCC45\xB7\uAC00\uB4DC)", cats: [], baseline: ["rules", "hooks"] },
6070
- {
6071
- label: "Track baseline \u2014 Agents & Skills (\uC5D0\uC774\uC804\uD2B8\xB7\uC2A4\uD0AC)",
6072
- cats: [],
6073
- baseline: ["agents", "skills"]
6074
- },
6075
- { label: "Dev Core (Frontend \xB7 Backend \xB7 Data)", cats: ["frontend", "backend", "data"] },
6076
- { label: "Dev Tools (Security \xB7 Quality \xB7 Understanding)", cats: ["dev-tools", "understanding"] },
6077
- { label: "Business (PM \xB7 Executive \xB7 Documents)", cats: ["business"] },
6078
- // v26.85.0 — 코드-퍼스트 비주얼/미디어 제작 (용도별 섹션, 전부 opt-in).
6079
- { label: "Visual & Media (Slides \xB7 Diagrams \xB7 Motion \xB7 Video)", cats: ["visual-media"] },
6080
- { label: "Workflow", cats: ["workflow"] }
6081
- ];
6082
- var BASELINE_TITLES = {
6083
- rules: "Rules \u2014 \uC0C1\uC2DC \uC0C1\uC8FC\uD558\uB294 \uB300\uC6D0\uCE59 (4 CLI \uC804\uBD80\uC5D0 \uC124\uCE58)",
6084
- hooks: "Hooks \u2014 \uACB0\uC815\uB860\uC801 \uAC00\uB4DC (\uD574\uC81C\uD558\uBA74 settings.json \uBC30\uC120\uB3C4 \uD568\uAED8 \uBE60\uC9C4\uB2E4)",
6085
- agents: "Agents \u2014 \uB3C5\uB9BD \uB808\uC778 (\uB9AC\uBDF0\xB7\uAC80\uC99D)",
6086
- skills: "Skills \u2014 \uD544\uC694\uD560 \uB54C \uC5F4\uB9AC\uB294 \uC791\uC5C5 \uC808\uCC28"
6087
- };
6088
- function buildPageGroups(cats, initialSet, installedSet = /* @__PURE__ */ new Set(), baselineKinds = [], baselineTargets = []) {
6089
- const installedMark = (value) => installedSet.has(value) ? " \u25CF installed" : "";
6090
- const groups = {};
6091
- const flatItems = [];
6092
- for (const kind of baselineKinds) {
6093
- const items = baselineTargets.filter((t) => t.kind === kind).map((t) => ({
6094
- value: t.id,
6095
- label: ` ${t.name}${installedMark(t.id)}`,
6096
- ...t.hint ? { hint: t.hint } : {}
6097
- }));
6098
- if (items.length === 0) continue;
6099
- groups[BASELINE_TITLES[kind]] = items;
6100
- flatItems.push(...items);
6101
- }
6102
- for (const cat of cats) {
6103
- const items = [];
6104
- for (const o of VISIBLE_OPTION_DEFS.filter((d2) => d2.category === cat)) {
6105
- items.push({
6106
- value: `option:${o.key}`,
6107
- // v26.62.3 — group header 와 옵션 사이 시각 hierarchy 강화. label prefix 4 space.
6108
- label: ` ${o.label} [${o.source}]${installedMark(`option:${o.key}`)}`,
6109
- hint: o.hint
6110
- });
6111
- }
6112
- const tierOrder = { official: 0, vetted: 1, experimental: 2 };
6113
- const catAssets = [...EXTERNAL_ASSETS.filter((x) => x.category === cat)].sort(
6114
- (a, b2) => tierOrder[assetTrustTier(a.id)] - tierOrder[assetTrustTier(b2.id)]
6115
- );
6116
- for (const a of catAssets) {
6117
- const tier = assetTrustTier(a.id);
6118
- const badge = tier === "official" ? " \u2605 official" : tier === "experimental" ? " \u26A0 experimental (opt-in)" : "";
6119
- items.push({
6120
- value: `asset:${a.id}`,
6121
- label: ` ${a.id} [${a.source}]${badge}${installedMark(`asset:${a.id}`)}`,
6122
- hint: a.description
6123
- });
6820
+ function parseOnly(only) {
6821
+ if (!only) return null;
6822
+ const ids = only.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
6823
+ return ids.length > 0 ? ids : null;
6824
+ }
6825
+ function buildGlobalAdvisoryCmd(asset) {
6826
+ switch (asset.method) {
6827
+ case "plugin": {
6828
+ const pid = asset.detail.pluginId ?? asset.id;
6829
+ return `claude plugin uninstall --scope user ${pid}`;
6124
6830
  }
6125
- if (items.length === 0) continue;
6126
- const selectedInCat = items.filter((it3) => initialSet.has(it3.value)).length;
6127
- const header = `${CATEGORY_TITLES[cat]} [${selectedInCat}/${items.length} \u2713 default]`;
6128
- groups[header] = items;
6129
- flatItems.push(...items);
6831
+ case "skill": {
6832
+ const s = asset.detail.source ?? asset.id;
6833
+ return `npx skills remove -g ${s}`;
6834
+ }
6835
+ case "npm": {
6836
+ const pkg = asset.detail.pkg ?? asset.id;
6837
+ return `npm uninstall -g ${pkg}`;
6838
+ }
6839
+ case "npx-run":
6840
+ case "shell-script":
6841
+ case "internal":
6842
+ return "(no standard reverse \u2014 manual)";
6130
6843
  }
6131
- return { groups, flatItems };
6132
6844
  }
6133
- function assertPagesCoverAllCategories(pages) {
6134
- const counts = /* @__PURE__ */ new Map();
6135
- for (const page of pages) {
6136
- for (const cat of page.cats) counts.set(cat, (counts.get(cat) ?? 0) + 1);
6845
+ function removeTemplates(log, projectDir, io, harnessRoot) {
6846
+ const { rm } = io;
6847
+ const moved = recordedTemplateDirs(log).flatMap((rel) => {
6848
+ const backup = io.moveAside(templateDirPath(projectDir, rel));
6849
+ return backup ? [{ rel, backup }] : [];
6850
+ });
6851
+ const external = removeExternalFiles(log, projectDir, rm, harnessRoot);
6852
+ const importStripped = stripRootImport(projectDir);
6853
+ const rootMd = log.templates.rootClaudeMd;
6854
+ if (rootMd) {
6855
+ if (rootClaudeMdModified(log, projectDir))
6856
+ return { rootClaudeMdKept: true, importStripped, external, moved };
6857
+ rm(join19(projectDir, rootMd.path));
6137
6858
  }
6138
- const missing = CATEGORIES.filter((c3) => !counts.has(c3));
6139
- const duplicated = CATEGORIES.filter((c3) => (counts.get(c3) ?? 0) > 1);
6140
- if (missing.length > 0 || duplicated.length > 0) {
6141
- throw new Error(
6142
- `INSTALL_TARGET_PAGES drift vs CATEGORIES \u2014 missing=[${missing.join(", ")}] duplicated=[${duplicated.join(", ")}]. \uBAA8\uB4E0 \uCE74\uD14C\uACE0\uB9AC\uB294 \uC815\uD655\uD788 \uD55C \uD398\uC774\uC9C0\uC5D0 \uBC30\uCE58\uD574\uC57C \uD55C\uB2E4 (no-false-ship: wizard \uBBF8\uB178\uCD9C = \uAC70\uC9D3 \uAD11\uACE0).`
6143
- );
6859
+ return { rootClaudeMdKept: false, importStripped, external, moved };
6860
+ }
6861
+ var AGENTS_MD = "AGENTS.md";
6862
+ function readAgentsMdTemplate(harnessRoot, log) {
6863
+ const flavor = log.templates.opencodeDir ? "opencode" : "codex";
6864
+ try {
6865
+ return readFileSync16(join19(harnessRoot, "templates", flavor, "AGENTS.md.template"), "utf8");
6866
+ } catch {
6867
+ return null;
6144
6868
  }
6145
6869
  }
6146
- assertPagesCoverAllCategories(INSTALL_TARGET_PAGES);
6147
- function viewportItems(itemCount) {
6148
- const rows = process.stdout.rows ?? 24;
6149
- return Math.max(8, Math.min(itemCount, rows - 10));
6870
+ function agentsMdRemainder(current, harnessRoot, log) {
6871
+ const template = readAgentsMdTemplate(harnessRoot, log);
6872
+ if (template === null) return { ok: false };
6873
+ return { ok: true, remainder: stripHarnessFromAgentsMd({ existing: current, template }) };
6150
6874
  }
6151
- var defaultPrompts = {
6152
- intro: (msg) => ge(msg),
6153
- outro: (msg) => ye(msg),
6154
- cancel: (msg) => me(msg),
6155
- selectTracks: async (initial, step) => {
6156
- const result = await ve({
6157
- message: stepLabel(step, "Select Track(s)"),
6158
- options: TRACKS.map((t) => ({ value: t, label: TRACK_LABELS[t] })),
6159
- ...initial ? { initialValues: initial } : {},
6160
- maxItems: viewportItems(11),
6161
- required: true
6162
- });
6163
- return q(result) ? null : result;
6164
- },
6165
- selectCli: async (initial, step) => {
6166
- const initialValues = initial && initial.length > 0 ? [...initial] : ["claude"];
6167
- const result = await ve({
6168
- message: stepLabel(step, "Target CLI(s)"),
6169
- options: [
6170
- { value: "claude", label: CLI_BASE_LABELS.claude },
6171
- { value: "codex", label: CLI_BASE_LABELS.codex },
6172
- { value: "opencode", label: CLI_BASE_LABELS.opencode },
6173
- { value: "antigravity", label: CLI_BASE_LABELS.antigravity }
6174
- ],
6175
- initialValues,
6176
- required: true
6177
- });
6178
- if (q(result)) return null;
6179
- return [...result].sort(
6180
- (a, b2) => CLI_BASE_SORT_ORDER[a] - CLI_BASE_SORT_ORDER[b2]
6181
- );
6182
- },
6183
- selectAction: async (state) => {
6184
- const result = await Ee({
6185
- message: summarizeState(state),
6186
- options: buildRouterChoices(state).map((c3) => {
6187
- const label = c3.enabled ? c3.label : `${c3.label} [disabled]`;
6188
- return {
6189
- value: c3.value,
6190
- label,
6191
- ...c3.hint ? { hint: c3.hint } : {},
6192
- ...c3.enabled ? {} : { disabled: true }
6193
- };
6194
- })
6195
- });
6196
- return q(result) ? null : result;
6197
- },
6198
- /**
6199
- * v26.64.0 (ADR-020) — Installation scope select. Default Project (D16 — no global write).
6200
- * Global 은 사용자 명시 opt-in 시에만.
6201
- */
6202
- selectScope: async (initial = "project", step) => {
6203
- const result = await Ee({
6204
- message: stepLabel(step, "Installation scope"),
6205
- initialValue: initial,
6206
- options: [
6207
- {
6208
- value: "project",
6209
- label: "Project",
6210
- hint: "Install in current directory (committed with your project)"
6211
- },
6212
- {
6213
- value: "global",
6214
- label: "Global",
6215
- hint: "Write to ~/.claude/, ~/.codex/, npm -g (shared across all projects)"
6216
- }
6217
- ]
6218
- });
6219
- return q(result) ? null : result;
6220
- },
6221
- selectUpdateGroups: async () => {
6222
- const result = await ve({
6223
- message: "What to update (space = toggle \xB7 your edits are always backed up)",
6224
- options: [
6225
- { value: "skills", label: "Skills", hint: "installed bundled skills \u2192 latest" },
6226
- {
6227
- value: "new-skills",
6228
- label: "New skills",
6229
- hint: "bundled skills added since your install"
6230
- },
6231
- {
6232
- value: "rules",
6233
- label: "Rules \xB7 agents \xB7 commands",
6234
- hint: "your edited files \u2192 *.backup-<time>"
6235
- },
6236
- {
6237
- value: "anchor",
6238
- label: "CLAUDE-uzys-harness.md anchor",
6239
- hint: "your edited anchor \u2192 *.backup-<time>"
6240
- },
6241
- {
6242
- value: "hooks",
6243
- label: "Hooks \xB7 settings.json",
6244
- hint: "stale hook refs cleaned"
6245
- },
6246
- {
6247
- value: "external",
6248
- label: "Codex / OpenCode / Antigravity + external skills",
6249
- hint: "rendered artifacts + npx skills"
6875
+ function removeExternalFiles(log, projectDir, rm, harnessRoot) {
6876
+ const removed = [];
6877
+ const kept = [];
6878
+ const stripped = [];
6879
+ const unjudged = [];
6880
+ for (const { path, sha256 } of log.externalFiles ?? []) {
6881
+ const abs = join19(projectDir, path);
6882
+ if (!existsSync18(abs)) continue;
6883
+ if (!lstatSync2(abs).isFile()) continue;
6884
+ const current = readFileSync16(abs, "utf8");
6885
+ if (hashContent(current) !== sha256) {
6886
+ kept.push(path);
6887
+ continue;
6888
+ }
6889
+ if (path === AGENTS_MD) {
6890
+ const verdict = agentsMdRemainder(current, harnessRoot, log);
6891
+ if (!verdict.ok) {
6892
+ unjudged.push(path);
6893
+ continue;
6894
+ }
6895
+ if (verdict.remainder !== null) {
6896
+ try {
6897
+ writeFileSync7(abs, verdict.remainder, "utf8");
6898
+ stripped.push(path);
6899
+ } catch {
6900
+ unjudged.push(path);
6250
6901
  }
6251
- ],
6252
- initialValues: [...UPDATE_GROUPS],
6253
- required: true
6254
- });
6255
- return q(result) ? null : result;
6256
- },
6257
- confirmInstall: async (summary) => {
6258
- const result = await ue({
6259
- message: `${summary}
6260
-
6261
- Proceed?`,
6262
- initialValue: true
6263
- });
6264
- return q(result) ? null : result;
6265
- },
6266
- selectInstallTargets: async (initialChecked, step, recap) => {
6267
- const pages = INSTALL_TARGET_PAGES;
6268
- const baselineTargets = withBaselineHints(listBaselineTargets({ tracks: recap?.tracks ?? [] }));
6269
- const initialSet = new Set(initialChecked);
6270
- const collected = new Set(initialChecked);
6271
- const recapLine = recap ? `Tracks: ${recap.tracks.join(", ")} \xB7 CLIs: ${recap.cli.join(", ")}` : "";
6272
- const installedSet = new Set(recap?.installed ?? []);
6273
- process.stdout.write("\x1B[?1049h");
6274
- let resultIds = null;
6275
- let aborted = false;
6902
+ continue;
6903
+ }
6904
+ }
6905
+ rm(abs);
6906
+ removed.push(path);
6907
+ }
6908
+ for (const path of removed) pruneEmptyDirsUpward(projectDir, dirname5(join19(projectDir, path)));
6909
+ return { removed, kept, stripped, unjudged };
6910
+ }
6911
+ function pruneEmptyDirsUpward(projectDir, startDir) {
6912
+ let cursor = startDir;
6913
+ while (true) {
6914
+ const rel = relative2(projectDir, cursor);
6915
+ if (rel === "" || rel.startsWith(`..${sep3}`) || rel === "..") return;
6276
6916
  try {
6277
- let pageIdx = 0;
6278
- let dir = 1;
6279
- while (pageIdx < pages.length) {
6280
- const page = pages[pageIdx];
6281
- if (!page) break;
6282
- const { groups, flatItems } = buildPageGroups(
6283
- page.cats,
6284
- initialSet,
6285
- installedSet,
6286
- page.baseline ?? [],
6287
- baselineTargets
6288
- );
6289
- if (flatItems.length === 0) {
6290
- pageIdx += dir;
6291
- if (pageIdx < 0) {
6292
- aborted = true;
6293
- break;
6294
- }
6295
- continue;
6296
- }
6297
- const selectedNow = flatItems.filter((it3) => collected.has(it3.value)).map((it3) => it3.value);
6298
- const pageDefault = flatItems.filter((it3) => initialSet.has(it3.value)).length;
6299
- const totalSelected = collected.size;
6300
- const message = [
6301
- `Step ${step.current}/${step.total} \xB7 Page ${pageIdx + 1}/${pages.length} \xB7 ${page.label}`,
6302
- recapLine ? ` ${recapLine}` : "",
6303
- ` Selected so far: ${totalSelected} items \xB7 This page default \u2713 ${pageDefault}/${flatItems.length}`,
6304
- // 마커의 뜻을 화면에서 바로 알려준다 — 체크 해제를 제거로 오해하는 것이 이 화면의
6305
- // 원래 문제였으므로, 마커가 보일 때는 제거 경로를 같은 자리에서 말한다.
6306
- installedSet.size > 0 ? " \u25CF installed = \uC774\uBBF8 \uC124\uCE58\uB428 \xB7 \uCCB4\uD06C \uD574\uC81C\uD574\uB3C4 \uC81C\uAC70\uB418\uC9C0 \uC54A\uB294\uB2E4 (\uC81C\uAC70: agent-harness uninstall)" : "",
6307
- " Space toggle \xB7 Enter \u2192 next \xB7 ESC \u2192 prev"
6308
- ].filter(Boolean).join("\n");
6309
- const groupOpts = {
6310
- message,
6311
- options: groups,
6312
- initialValues: selectedNow,
6313
- required: false,
6314
- selectableGroups: false
6315
- };
6316
- const result = await pe(groupOpts);
6317
- if (q(result)) {
6318
- if (pageIdx === 0) {
6319
- aborted = true;
6320
- break;
6321
- }
6322
- dir = -1;
6323
- pageIdx--;
6324
- continue;
6325
- }
6326
- for (const it3 of flatItems) collected.delete(it3.value);
6327
- for (const v2 of result) collected.add(v2);
6328
- dir = 1;
6329
- pageIdx++;
6917
+ if (readdirSync6(cursor).length > 0) return;
6918
+ rmdirSync2(cursor);
6919
+ } catch {
6920
+ return;
6921
+ }
6922
+ cursor = dirname5(cursor);
6923
+ }
6924
+ }
6925
+ function stripRootImport(projectDir) {
6926
+ const current = readRootClaudeMd(projectDir);
6927
+ const stripped = current === null ? null : stripHarnessImport(current);
6928
+ if (stripped === null) return false;
6929
+ try {
6930
+ writeFileSync7(join19(projectDir, "CLAUDE.md"), stripped, "utf8");
6931
+ return true;
6932
+ } catch {
6933
+ return false;
6934
+ }
6935
+ }
6936
+ function previewExternalLines(installLog, projectDir, harnessRoot, movedDirs = []) {
6937
+ const lines = [];
6938
+ let removable = 0;
6939
+ for (const { path, sha256 } of installLog.externalFiles ?? []) {
6940
+ const abs = join19(projectDir, path);
6941
+ if (underAny(path, movedDirs)) continue;
6942
+ if (!existsSync18(abs)) continue;
6943
+ if (!lstatSync2(abs).isFile()) continue;
6944
+ const current = readFileSync16(abs, "utf8");
6945
+ if (hashContent(current) !== sha256) {
6946
+ lines.push(` \u25CB keep ${path} (modified since install \u2014 preserved)`);
6947
+ continue;
6948
+ }
6949
+ if (path === AGENTS_MD) {
6950
+ const verdict = agentsMdRemainder(current, harnessRoot, installLog);
6951
+ if (!verdict.ok) {
6952
+ lines.push(` \u25CB keep ${path} (harness sections could not be separated \u2014 preserved)`);
6953
+ continue;
6330
6954
  }
6331
- if (!aborted) {
6332
- resultIds = [...collected];
6955
+ if (verdict.remainder !== null) {
6956
+ lines.push(` \u25CB strip harness sections from ${path} (\uBCF8\uBB38 \uBCF4\uC874)`);
6957
+ continue;
6333
6958
  }
6334
- } finally {
6335
- process.stdout.write("\x1B[?1049l");
6336
6959
  }
6337
- if (resultIds !== null) {
6338
- process.stdout.write(
6339
- `\u25C7 Step ${step.current}/${step.total} \u2014 Install targets \xB7 ${resultIds.length} selected
6340
- \u2502
6341
- `
6960
+ removable += 1;
6961
+ }
6962
+ if (removable > 0) lines.unshift(` \u25CB remove ${removable} CLI output file(s)`);
6963
+ return lines;
6964
+ }
6965
+ function externalRemovalLines(external) {
6966
+ const lines = [];
6967
+ if (external.removed.length > 0) {
6968
+ lines.push(` ${status.success(`CLI outputs removed: ${external.removed.length} file(s)`)}`);
6969
+ }
6970
+ for (const path of external.stripped) {
6971
+ lines.push(` ${status.success(`${path} \u2014 harness sections removed (\uBCF8\uBB38 \uBCF4\uC874)`)}`);
6972
+ }
6973
+ for (const path of external.unjudged) {
6974
+ lines.push(
6975
+ ` ${c.yellow("\u2298")} ${path} kept \u2014 harness sections could not be separated (template unreadable or write failed). Remove manually if intended.`
6976
+ );
6977
+ }
6978
+ for (const path of external.kept) {
6979
+ lines.push(
6980
+ ` ${c.yellow("\u2298")} ${path} kept \u2014 modified since install. Remove manually if intended.`
6981
+ );
6982
+ }
6983
+ return lines;
6984
+ }
6985
+ function hasRootImport(projectDir) {
6986
+ const current = readRootClaudeMd(projectDir);
6987
+ return current !== null && stripHarnessImport(current) !== null;
6988
+ }
6989
+ function readRootClaudeMd(projectDir) {
6990
+ const path = join19(projectDir, "CLAUDE.md");
6991
+ if (!existsSync18(path)) return null;
6992
+ try {
6993
+ return readFileSync16(path, "utf8");
6994
+ } catch {
6995
+ return null;
6996
+ }
6997
+ }
6998
+ function rootClaudeMdModified(log, projectDir) {
6999
+ const rootMd = log.templates.rootClaudeMd;
7000
+ if (!rootMd) return false;
7001
+ const path = join19(projectDir, rootMd.path);
7002
+ if (!existsSync18(path)) return false;
7003
+ return hashContent(readFileSync16(path, "utf8")) !== rootMd.sha256;
7004
+ }
7005
+ function defaultSpawn2(cmd, args) {
7006
+ return spawnSync2(cmd, [...args], { encoding: "utf8", stdio: "pipe", timeout: 12e4 });
7007
+ }
7008
+ function defaultRm(path) {
7009
+ if (existsSync18(path)) {
7010
+ rmSync(path, { recursive: true, force: true });
7011
+ }
7012
+ }
7013
+ function registerUninstallCommand(cli2) {
7014
+ cli2.command("uninstall", "Uninstall harness assets (log-based reverse)").option("--project-dir <path>", "[Project] Target project directory", {
7015
+ default: process.cwd()
7016
+ }).option("--dry-run", "[Mode] List reverse steps without executing").option(
7017
+ "--keep-templates",
7018
+ "[Mode] Keep `.claude/`, `.codex/`, `.opencode/` templates (remove only external assets)"
7019
+ ).option(
7020
+ "--only <ids>",
7021
+ "[Scope] Remove only these assets (comma-separated ids from `agent-harness list`). Templates untouched"
7022
+ ).option(
7023
+ "--cli <name>",
7024
+ `[Scope] Remove one CLI only (${CLI_BASES.join(" | ")}). Shared files stay until the last user leaves`
7025
+ ).option("--yes", "[Mode] Skip the interactive picker and remove everything (non-interactive)").action(async (options) => {
7026
+ await dispatchUninstall(options);
7027
+ });
7028
+ }
7029
+ function shouldRunInteractive(options, isTty) {
7030
+ if (!isTty) return false;
7031
+ if (options.yes || options.dryRun) return false;
7032
+ return options.only === void 0 && options.cli === void 0;
7033
+ }
7034
+ async function dispatchUninstall(options) {
7035
+ if (!shouldRunInteractive(options, Boolean(process.stdin.isTTY))) {
7036
+ uninstallAction(options);
7037
+ return;
7038
+ }
7039
+ await runUninstallScreen(resolve4(options.projectDir ?? process.cwd()));
7040
+ }
7041
+ async function runUninstallScreen(projectDir, opts = {}) {
7042
+ const picked = await runInteractiveUninstall(projectDir, opts);
7043
+ if (!picked.ok || !picked.options) {
7044
+ if (picked.reason === "no-log") {
7045
+ console.error(
7046
+ status.failure(c.red(`ERROR: install log not found at ${installLogPath(projectDir)}`))
6342
7047
  );
7048
+ console.error(c.dim(" Nothing installed here by agent-harness."));
7049
+ process.exit(1);
6343
7050
  }
6344
- return resultIds;
7051
+ return;
6345
7052
  }
6346
- };
7053
+ uninstallAction(picked.options);
7054
+ }
7055
+
7056
+ // src/commands/update.ts
7057
+ init_esm_shims();
7058
+ import { resolve as resolve5 } from "path";
7059
+
7060
+ // src/state.ts
7061
+ init_esm_shims();
7062
+ import { existsSync as existsSync19, readFileSync as readFileSync17 } from "fs";
7063
+ import { join as join20 } from "path";
7064
+ var META_FILE = ".claude/.installed-tracks";
7065
+ var LEGACY_SIGNATURES = [
7066
+ { rule: "htmx.md", track: "ssr-htmx" },
7067
+ { rule: "nextjs.md", track: "ssr-nextjs" },
7068
+ { rule: "data-analysis.md", track: "data" },
7069
+ { rule: "pyside6.md", track: "data" },
7070
+ { rule: "cli-development.md", track: "tooling" }
7071
+ ];
7072
+ function detectInstallState(projectDir) {
7073
+ const claudeDir = join20(projectDir, ".claude");
7074
+ const hasClaudeDir = existsSync19(claudeDir);
7075
+ if (hasClaudeDir) {
7076
+ const metaPath = join20(projectDir, META_FILE);
7077
+ if (existsSync19(metaPath)) {
7078
+ const tracks2 = readMetafile(metaPath);
7079
+ return { state: "existing", tracks: tracks2, source: "metafile", hasClaudeDir: true };
7080
+ }
7081
+ const tracks = inferFromLegacySignatures(projectDir);
7082
+ return { state: "existing", tracks, source: "legacy", hasClaudeDir: true };
7083
+ }
7084
+ const log = readInstallLog(projectDir);
7085
+ if (log) {
7086
+ return {
7087
+ state: "existing",
7088
+ tracks: tracksFromLog(log.spec.tracks),
7089
+ source: "install-log",
7090
+ hasClaudeDir: false
7091
+ };
7092
+ }
7093
+ return { state: "new", tracks: [], source: "none", hasClaudeDir: false };
7094
+ }
7095
+ function tracksFromLog(tracks) {
7096
+ return [...new Set(tracks.filter(isTrack))].sort();
7097
+ }
7098
+ function readMetafile(path) {
7099
+ const raw = readFileSync17(path, "utf8");
7100
+ const seen = /* @__PURE__ */ new Set();
7101
+ for (const line of raw.split(/\s+/)) {
7102
+ const trimmed = line.trim();
7103
+ if (isTrack(trimmed)) {
7104
+ seen.add(trimmed);
7105
+ }
7106
+ }
7107
+ return [...seen].sort();
7108
+ }
7109
+ function inferFromLegacySignatures(projectDir) {
7110
+ const rulesDir = join20(projectDir, ".claude/rules");
7111
+ if (!existsSync19(rulesDir)) {
7112
+ return [];
7113
+ }
7114
+ const found = /* @__PURE__ */ new Set();
7115
+ for (const sig of LEGACY_SIGNATURES) {
7116
+ if (existsSync19(join20(rulesDir, sig.rule))) {
7117
+ found.add(sig.track);
7118
+ }
7119
+ }
7120
+ return [...found].sort();
7121
+ }
7122
+
7123
+ // src/commands/update.ts
7124
+ function updateAction(options = {}, deps = {}) {
7125
+ const log = deps.log ?? console.log;
7126
+ const err = deps.err ?? console.error;
7127
+ const exit = deps.exit ?? ((code) => process.exit(code));
7128
+ const detect = deps.detect ?? detectInstallState;
7129
+ const execute = deps.execute ?? executeSpec;
7130
+ const projectDir = resolve5(options.projectDir ?? process.cwd());
7131
+ const state = detect(projectDir);
7132
+ if (state.state === "new") {
7133
+ err(status.failure(c.red(`No harness install found at ${projectDir}`)));
7134
+ err(c.dim(" Run `agent-harness install --track <name>` first."));
7135
+ exit(1);
7136
+ return;
7137
+ }
7138
+ const rawOnly = options.only === void 0 ? [] : [options.only].flat().map((v2) => typeof v2 === "string" ? v2 : "(missing value)");
7139
+ const parsed = parseUpdateOnly(rawOnly);
7140
+ if (!parsed.ok) {
7141
+ err(status.failure(c.red(`Unknown --only value: ${parsed.invalid.join(", ")}`)));
7142
+ err(c.dim(` Valid groups: ${UPDATE_GROUPS.join(" | ")}`));
7143
+ exit(1);
7144
+ return;
7145
+ }
7146
+ log(c.dim(`Updating installed harness files in ${projectDir}`));
7147
+ execute(buildUpdateSpec(projectDir, state.tracks, parsed.groups), {
7148
+ log,
7149
+ err,
7150
+ exit,
7151
+ mode: "update"
7152
+ });
7153
+ }
7154
+ function registerUpdateCommand(cli2) {
7155
+ cli2.command(
7156
+ "update",
7157
+ "Refresh installed policy files (rules / agents / commands / hooks / skills)"
7158
+ ).option("--project-dir <path>", "[Project] Target project directory", {
7159
+ default: process.cwd()
7160
+ }).option(
7161
+ "--only <group>",
7162
+ `[Scope] Update only this group (repeatable): ${UPDATE_GROUPS.join(" | ")}`
7163
+ ).action((options) => {
7164
+ updateAction(options);
7165
+ });
7166
+ }
6347
7167
 
6348
7168
  // src/interactive.ts
7169
+ init_esm_shims();
7170
+ import { existsSync as existsSync20 } from "fs";
7171
+ import { join as join21 } from "path";
6349
7172
  function splitInstallTargets(targets) {
6350
7173
  const optionKeys = [];
6351
7174
  const assetIds = [];
@@ -6386,6 +7209,59 @@ function initialTargetSelection(tracks, installedProjectAssetIds) {
6386
7209
  const baseline = listBaselineTargets({ tracks }).map((t) => t.id);
6387
7210
  return [...assets, ...baseline];
6388
7211
  }
7212
+ function lockTracks(picked, installed) {
7213
+ return [.../* @__PURE__ */ new Set([...installed, ...picked])].sort();
7214
+ }
7215
+ function lockClis(picked, installed) {
7216
+ return [.../* @__PURE__ */ new Set([...installed, ...picked])].sort(
7217
+ (a, b2) => CLI_BASE_SORT_ORDER[a] - CLI_BASE_SORT_ORDER[b2]
7218
+ );
7219
+ }
7220
+ var BUNDLED_SKILLS = new Set(INTERNAL_BUNDLED_SKILL_IDS);
7221
+ function classifyUpdateIntent(log, confirmed, legacyTracks = []) {
7222
+ const recordTracks = log ? log.spec.tracks : legacyTracks;
7223
+ const recordClis = recordedClis(log);
7224
+ if (!sameSet(confirmed.tracks, recordTracks)) return "add";
7225
+ if (!sameSet(confirmed.cli, recordClis)) return "add";
7226
+ const recorded = log?.assets ?? [];
7227
+ const covered = coveredByRecord(log, legacyTracks);
7228
+ const picked = new Set(confirmed.assetIds);
7229
+ if (confirmed.assetIds.some((id) => !covered.has(id))) return "add";
7230
+ if (recorded.some((a) => a.scope !== "global" && !picked.has(a.id))) return "add";
7231
+ if (!sameSet(confirmed.baselineExclude, log?.spec.baselineExclude ?? [])) return "add";
7232
+ if (!sameSet(confirmed.skillExclude, log?.spec.skillExclude ?? [])) return "add";
7233
+ return "refresh";
7234
+ }
7235
+ function recordedClis(log) {
7236
+ return log ? [...installedClis(log)] : ["claude"];
7237
+ }
7238
+ function coveredByRecord(log, legacyTracks) {
7239
+ const recordTracks = (log ? log.spec.tracks : legacyTracks).filter(isTrack);
7240
+ const clis = recordedClis(log);
7241
+ const covered = new Set((log?.assets ?? []).map((a) => a.id));
7242
+ for (const id of recommendedExternalAssets(recordTracks)) {
7243
+ const asset = EXTERNAL_ASSETS.find((a) => a.id === id);
7244
+ if (!asset || asset.method.kind === "internal" || !assetReachesCli(asset, clis)) {
7245
+ covered.add(id);
7246
+ }
7247
+ }
7248
+ return covered;
7249
+ }
7250
+ function sameSet(a, b2) {
7251
+ const left = new Set(a);
7252
+ const right = new Set(b2);
7253
+ return left.size === right.size && [...left].every((x) => right.has(x));
7254
+ }
7255
+ function updateInitialSelection(tracks, installedProjectAssetIds, log, legacyTracks = []) {
7256
+ const covered = coveredByRecord(log, legacyTracks);
7257
+ const recordTracks = (log ? log.spec.tracks : legacyTracks).filter(isTrack);
7258
+ const excluded = /* @__PURE__ */ new Set([
7259
+ ...log?.spec.baselineExclude ?? [],
7260
+ ...(log?.spec.skillExclude ?? []).map((id) => `asset:${id}`),
7261
+ ...recommendedExternalAssets(recordTracks).filter((id) => !covered.has(id)).map((id) => `asset:${id}`)
7262
+ ]);
7263
+ return initialTargetSelection(tracks, installedProjectAssetIds).filter((id) => !excluded.has(id));
7264
+ }
6389
7265
  async function runInteractive(projectDir, deps = {}) {
6390
7266
  const prompts = deps.prompts ?? defaultPrompts;
6391
7267
  const detect = deps.detect ?? detectInstallState;
@@ -6400,10 +7276,10 @@ async function runInteractive(projectDir, deps = {}) {
6400
7276
  prompts.intro("uzys-agent-harness installer");
6401
7277
  const state = detect(projectDir);
6402
7278
  const installed = (deps.readInstalled ?? installedTargetState)(projectDir);
6403
- let initialTracks;
6404
- let mode = "fresh";
6405
7279
  if (state.state === "existing") {
6406
- const action = await prompts.selectAction(state);
7280
+ const log = readInstallLog(projectDir);
7281
+ const record = buildInstallRecordView(state, log, existsSync20(join21(projectDir, ".claude")));
7282
+ const action = await prompts.selectAction(state, record);
6407
7283
  if (action === null) {
6408
7284
  prompts.cancel("Cancelled.");
6409
7285
  return { ok: false, reason: "cancelled" };
@@ -6412,34 +7288,14 @@ async function runInteractive(projectDir, deps = {}) {
6412
7288
  prompts.outro("Exiting without changes.");
6413
7289
  return { ok: false, reason: "exit" };
6414
7290
  }
6415
- if (action === "remove") {
6416
- prompts.cancel("Track removal is not automated \u2014 manually edit `.claude/`. Aborting.");
6417
- return { ok: false, reason: "disabled-action" };
6418
- }
6419
- if (action === "update") {
6420
- mode = "update";
6421
- const groups = prompts.selectUpdateGroups ? await prompts.selectUpdateGroups() : null;
6422
- if (groups === null && prompts.selectUpdateGroups) {
6423
- prompts.cancel("Cancelled.");
6424
- return { ok: false, reason: "cancelled" };
6425
- }
6426
- const spec = buildUpdateSpec(projectDir, state.tracks, groups ?? void 0);
6427
- const scopeLine = spec.updateOnly ? `UPDATE ${spec.updateOnly.join(" \xB7 ")} only (untouched: ${UPDATE_GROUPS.filter((g) => !spec.updateOnly?.includes(g)).join(", ")}):` : "UPDATE installed harness files:";
6428
- const confirmed = await prompts.confirmInstall(`${scopeLine}
6429
- ${formatSummary(spec)}`);
6430
- if (!confirmed) {
6431
- prompts.outro("Cancelled.");
6432
- return { ok: false, reason: "cancelled" };
6433
- }
6434
- prompts.outro("Running update mode...");
6435
- return { ok: true, mode: "update", spec };
7291
+ if (action === "uninstall") {
7292
+ return { ok: true, uninstall: true };
6436
7293
  }
6437
- if (action === "add") {
6438
- mode = "add";
6439
- initialTracks = state.tracks;
6440
- } else if (action === "reinstall") {
6441
- mode = "reinstall";
7294
+ if (record.repair !== null) {
7295
+ prompts.cancel(`.claude/ is missing \u2014 repair first: ${record.repair}`);
7296
+ return { ok: false, reason: "disabled-action", message: record.repair };
6442
7297
  }
7298
+ return runUpdateFlow({ projectDir, state, log, prompts, installed });
6443
7299
  }
6444
7300
  let step = "tracks";
6445
7301
  let tracks = null;
@@ -6448,7 +7304,7 @@ ${formatSummary(spec)}`);
6448
7304
  let scope = "project";
6449
7305
  while (true) {
6450
7306
  if (step === "tracks") {
6451
- const result = await prompts.selectTracks(tracks ?? initialTracks, WIZARD.TRACKS);
7307
+ const result = await prompts.selectTracks(tracks ?? void 0, WIZARD.TRACKS);
6452
7308
  if (result === null) {
6453
7309
  prompts.cancel("Cancelled.");
6454
7310
  return { ok: false, reason: "cancelled" };
@@ -6502,7 +7358,7 @@ ${formatSummary(spec)}`);
6502
7358
  projectDir,
6503
7359
  ...userOverride ? { userOverride } : {},
6504
7360
  // 해제 목록을 안 넘기면 이 화면이 **제외 유무와 문자열이 완전히 같아진다** — 같은 화면이
6505
- // 외부 자산 제거는 `-User removed:` 로 이미 보고하므로, 없음은 "아무것도 안 빠졌다"로
7361
+ // 외부 자산 제거는 `-Unchecked by you:` 로 이미 보고하므로, 없음은 "아무것도 안 빠졌다"로
6506
7362
  // 읽힌다. 상주 비용을 줄이려고 20개를 푼 사용자가 그대로인 숫자를 보게 된다.
6507
7363
  ...baselineExclude.length > 0 ? { baselineExclude } : {}
6508
7364
  })}
@@ -6529,10 +7385,169 @@ ${summary}`
6529
7385
  ...baselineExclude.length > 0 ? { baselineExclude } : {}
6530
7386
  };
6531
7387
  prompts.outro(stepLabel(WIZARD.INSTALL, "Installing..."));
6532
- return { ok: true, mode, spec };
7388
+ return { ok: true, mode: "fresh", spec };
7389
+ }
7390
+ }
7391
+ }
7392
+ async function runUpdateFlow(ctx) {
7393
+ const { projectDir, state, log, prompts, installed } = ctx;
7394
+ const lockedTracks = lockTracks(log ? log.spec.tracks.filter(isTrack) : [], state.tracks);
7395
+ const lockedClis = installedClis(log);
7396
+ const scope = log?.scope ?? "project";
7397
+ let step = "tracks";
7398
+ let tracks = null;
7399
+ let cli2 = null;
7400
+ let targetSelections = null;
7401
+ while (true) {
7402
+ if (step === "tracks") {
7403
+ const result = await prompts.selectTracks(
7404
+ tracks ?? lockedTracks,
7405
+ UPDATE_WIZARD.TRACKS,
7406
+ lockedTracks
7407
+ );
7408
+ if (result === null) {
7409
+ prompts.cancel("Cancelled.");
7410
+ return { ok: false, reason: "cancelled" };
7411
+ }
7412
+ const next = lockTracks(result, lockedTracks);
7413
+ if (tracks !== null && !tracksEqual(tracks, next)) targetSelections = null;
7414
+ tracks = next;
7415
+ step = "cli";
7416
+ } else if (step === "cli") {
7417
+ const initial = cli2 ?? (lockedClis.length > 0 ? [...lockedClis] : ["claude"]);
7418
+ const result = await prompts.selectCli(initial, UPDATE_WIZARD.CLI, lockedClis);
7419
+ if (result === null) {
7420
+ step = "tracks";
7421
+ continue;
7422
+ }
7423
+ cli2 = lockClis(result, lockedClis);
7424
+ step = "targets";
7425
+ } else if (step === "targets") {
7426
+ const current = tracks ?? lockedTracks;
7427
+ const initial = targetSelections !== null ? [...targetSelections] : updateInitialSelection(current, installed.projectScoped, log, state.tracks);
7428
+ const result = await prompts.selectInstallTargets(initial, UPDATE_WIZARD.TARGETS, {
7429
+ tracks: current,
7430
+ cli: cli2 ?? [...lockedClis],
7431
+ installed: installed.installed.map((id) => `asset:${id}`)
7432
+ });
7433
+ if (result === null) {
7434
+ step = "cli";
7435
+ continue;
7436
+ }
7437
+ targetSelections = result;
7438
+ step = "confirm";
7439
+ } else {
7440
+ const outcome = await confirmUpdate({
7441
+ projectDir,
7442
+ state,
7443
+ log,
7444
+ prompts,
7445
+ scope,
7446
+ lockedClis,
7447
+ // biome-ignore lint/style/noNonNullAssertion: confirm 도달 = 이전 step 완료 보장
7448
+ tracks,
7449
+ // biome-ignore lint/style/noNonNullAssertion: same as above
7450
+ cli: cli2,
7451
+ targetSelections: targetSelections ?? []
7452
+ });
7453
+ if (outcome === "back") {
7454
+ step = "targets";
7455
+ continue;
7456
+ }
7457
+ return outcome;
6533
7458
  }
6534
7459
  }
6535
7460
  }
7461
+ async function confirmUpdate(input) {
7462
+ const { projectDir, state, log, prompts, scope, tracks, cli: cli2 } = input;
7463
+ const { assetIds, baselineIds } = splitInstallTargets(input.targetSelections);
7464
+ const userOverride = computeUserOverride(tracks, assetIds);
7465
+ const baselineExclude = baselineExcludeFrom(listBaselineTargets({ tracks }), baselineIds);
7466
+ const skillExclude = (userOverride?.forceExclude ?? []).filter((id) => BUNDLED_SKILLS.has(id));
7467
+ const intent = classifyUpdateIntent(
7468
+ log,
7469
+ { tracks, cli: cli2, assetIds, baselineExclude, skillExclude },
7470
+ state.tracks
7471
+ );
7472
+ const header = stepLabel(UPDATE_WIZARD.CONFIRM, "Confirm");
7473
+ const scopeLine = ` SCOPE ${scope === "global" ? "Global" : "Project"} (${log ? "from your install record \u2014 not changed here" : "no install record \u2014 default"})`;
7474
+ if (intent === "refresh") {
7475
+ const groups = prompts.selectUpdateGroups ? await prompts.selectUpdateGroups() : void 0;
7476
+ if (groups === null) return "back";
7477
+ const spec2 = buildUpdateSpec(projectDir, state.tracks, groups);
7478
+ const only = spec2.updateOnly ?? [];
7479
+ const summary2 = [
7480
+ header,
7481
+ only.length > 0 ? `UPDATE ${only.join(" \xB7 ")} only (untouched: ${UPDATE_GROUPS.filter((g) => !only.includes(g)).join(", ")}):` : "UPDATE installed harness files:",
7482
+ // 요약은 Step 3 에서 체크된 것을 센다 — spec(추천 전체)으로 세면 체크 안 된 채 둔 자산까지
7483
+ // "selected" 로 적는데 refresh 는 그것을 깔지 않는다(리뷰 B2). 실행 spec 은 그대로다.
7484
+ annotate(formatSummary({ ...spec2, ...userOverride ? { userOverride } : {} }), {
7485
+ Tracks: "no change",
7486
+ CLI: "no change",
7487
+ Assets: "no change"
7488
+ }),
7489
+ scopeLine,
7490
+ ` RUNS AS agent-harness update${only.map((g) => ` --only ${g}`).join("")}`
7491
+ ].join("\n");
7492
+ const confirmed2 = await prompts.confirmInstall(summary2);
7493
+ if (confirmed2 === null) return "back";
7494
+ if (!confirmed2) {
7495
+ prompts.outro("Cancelled.");
7496
+ return { ok: false, reason: "cancelled" };
7497
+ }
7498
+ prompts.outro("Running update mode...");
7499
+ return { ok: true, mode: "update", spec: spec2 };
7500
+ }
7501
+ const without = [...userOverride?.forceExclude ?? [], ...baselineExclude];
7502
+ const options = {
7503
+ track: [...tracks],
7504
+ cli: [...cli2],
7505
+ scope,
7506
+ projectDir,
7507
+ ...userOverride && userOverride.forceInclude.length > 0 ? { with: [...userOverride.forceInclude] } : {},
7508
+ ...without.length > 0 ? { without } : {}
7509
+ };
7510
+ const spec = installSpecFromOptions(options, [...cli2], () => {
7511
+ });
7512
+ const recordTracks = new Set(log ? log.spec.tracks : state.tracks);
7513
+ const addedTracks = tracks.filter((t) => !recordTracks.has(t));
7514
+ const addedClis = cli2.filter((c3) => !input.lockedClis.includes(c3));
7515
+ const covered = coveredByRecord(log, state.tracks);
7516
+ const addedAssets = assetIds.filter((id) => !covered.has(id));
7517
+ const locked = input.lockedClis.join(", ");
7518
+ const summary = [
7519
+ header,
7520
+ annotate(formatSummary(spec), {
7521
+ Tracks: addedTracks.length > 0 ? `+${addedTracks.join(", +")}` : "no change",
7522
+ CLI: [
7523
+ addedClis.length > 0 ? `+${addedClis.join(", +")}` : "",
7524
+ locked ? `${locked} ${input.lockedClis.length > 1 ? "stay" : "stays"} \u2014 locked` : ""
7525
+ ].filter(Boolean).join(" \xB7 "),
7526
+ Assets: addedAssets.length > 0 ? `+${addedAssets.length} new: ${addedAssets.join(", ")}` : "no new assets"
7527
+ }),
7528
+ scopeLine,
7529
+ ` RUNS AS ${installCommandLine(options)}`,
7530
+ " (adds the new track's files and the new CLI's files \xB7 refreshes what is installed \xB7 edited files \u2192 *.backup-<time>)",
7531
+ " (retired-file cleanup runs on your next `agent-harness update`)"
7532
+ ].join("\n");
7533
+ const confirmed = await prompts.confirmInstall(summary);
7534
+ if (confirmed === null) return "back";
7535
+ if (!confirmed) {
7536
+ prompts.outro("Cancelled by user.");
7537
+ return { ok: false, reason: "cancelled" };
7538
+ }
7539
+ prompts.outro(stepLabel(UPDATE_WIZARD.RUN, "Installing..."));
7540
+ return { ok: true, mode: "add", spec };
7541
+ }
7542
+ function annotate(summary, notes) {
7543
+ return summary.split("\n").map((line) => {
7544
+ const key = Object.keys(notes).find(
7545
+ (k2) => line.startsWith(`${k2}:`)
7546
+ );
7547
+ const note = key ? notes[key] : "";
7548
+ return note ? `${line.padEnd(40)} (${note})` : line;
7549
+ }).join("\n");
7550
+ }
6536
7551
  function tracksEqual(a, b2) {
6537
7552
  if (a.length !== b2.length) return false;
6538
7553
  const sortedA = [...a].sort();
@@ -6585,7 +7600,7 @@ function formatSummary(spec) {
6585
7600
  lines.push(` +User added: ${spec.userOverride.forceInclude.join(", ")}`);
6586
7601
  }
6587
7602
  if (spec.userOverride.forceExclude.length > 0) {
6588
- lines.push(` -User removed: ${spec.userOverride.forceExclude.join(", ")}`);
7603
+ lines.push(` -Unchecked by you: ${spec.userOverride.forceExclude.join(", ")}`);
6589
7604
  }
6590
7605
  }
6591
7606
  if (baselineExcluded.size > 0) {
@@ -6605,6 +7620,11 @@ async function defaultAction(deps = {}) {
6605
7620
  const run = deps.run ?? ((cwd) => runInteractive(cwd));
6606
7621
  const execute = deps.execute ?? executeSpec;
6607
7622
  const result = await run(process.cwd());
7623
+ if (result.ok && result.uninstall) {
7624
+ const uninstall = deps.uninstall ?? ((cwd) => runUninstallScreen(cwd, { embedded: true }));
7625
+ await uninstall(process.cwd());
7626
+ return;
7627
+ }
6608
7628
  if (!result.ok) {
6609
7629
  if (result.message) {
6610
7630
  err(result.message);