@uzysjung/agent-harness 26.160.1 → 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.1",
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();
@@ -1223,6 +1136,11 @@ function trimBlankEdges(lines) {
1223
1136
  while (out.length > 0 && (out[0] ?? "").trim() === "") out.shift();
1224
1137
  return out;
1225
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
+ }
1226
1144
  function stripHarnessFromAgentsMd(params) {
1227
1145
  const { existing, template } = params;
1228
1146
  const lines = existing.split("\n");
@@ -1244,7 +1162,161 @@ function stripHarnessFromAgentsMd(params) {
1244
1162
  return out.join("\n");
1245
1163
  }
1246
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
+ }
1317
+
1247
1318
  // src/codex/agents-md.ts
1319
+ init_esm_shims();
1248
1320
  function renameSlashes(text) {
1249
1321
  return text.replaceAll("/uzys:", "/uzys-");
1250
1322
  }
@@ -1256,8 +1328,8 @@ function renderAgentsMd(params) {
1256
1328
 
1257
1329
  // src/codex/skills.ts
1258
1330
  init_esm_shims();
1259
- import { chmodSync, existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1260
- import { join as join3 } from "path";
1331
+ import { chmodSync, existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
1332
+ import { join as join5 } from "path";
1261
1333
  function renderBundledSkill(source) {
1262
1334
  const trimmed = source.trimEnd();
1263
1335
  const lines = trimmed.split(/\r?\n/);
@@ -1289,14 +1361,14 @@ function writeBundledSkillDirs(params) {
1289
1361
  const { harnessRoot, projectDir, skillIds, writer } = params;
1290
1362
  const written = [];
1291
1363
  for (const id of skillIds) {
1292
- const srcDir = join3(harnessRoot, "templates/skills", id);
1293
- const srcSkill = join3(srcDir, "SKILL.md");
1294
- if (!existsSync3(srcSkill)) {
1364
+ const srcDir = join5(harnessRoot, "templates/skills", id);
1365
+ const srcSkill = join5(srcDir, "SKILL.md");
1366
+ if (!existsSync4(srcSkill)) {
1295
1367
  continue;
1296
1368
  }
1297
- const targetDir = join3(projectDir, ".agents", "skills", id);
1298
- const targetSkill = join3(targetDir, "SKILL.md");
1299
- 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")))) {
1300
1372
  continue;
1301
1373
  }
1302
1374
  written.push(targetSkill);
@@ -1304,10 +1376,10 @@ function writeBundledSkillDirs(params) {
1304
1376
  if (rel === "SKILL.md") continue;
1305
1377
  if (rel.split("/").some((segment) => segment.startsWith("."))) continue;
1306
1378
  const segments = rel.split("/");
1307
- const raw = readFileSync2(join3(srcDir, ...segments), "utf8");
1379
+ const raw = readFileSync3(join5(srcDir, ...segments), "utf8");
1308
1380
  const ported = rel.endsWith(".md") || rel.endsWith(".sh") ? `${portBody(raw)}
1309
1381
  ` : raw;
1310
- const target = join3(targetDir, ...segments);
1382
+ const target = join5(targetDir, ...segments);
1311
1383
  if (!writer.write(target, ported, { createInRefresh: true })) continue;
1312
1384
  if (rel.endsWith(".sh")) chmodSync(target, 493);
1313
1385
  written.push(target);
@@ -1318,30 +1390,40 @@ function writeBundledSkillDirs(params) {
1318
1390
 
1319
1391
  // src/owned-write.ts
1320
1392
  init_esm_shims();
1321
- import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
1322
- 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";
1323
1395
 
1324
1396
  // src/foreign-slot.ts
1325
1397
  init_esm_shims();
1326
- import { lstatSync } from "fs";
1327
- import { join as join4 } from "path";
1398
+ import { lstatSync, realpathSync, statSync } from "fs";
1399
+ import { join as join6 } from "path";
1328
1400
  var SKILL_SLOT = /^((?:\.claude|\.agents)\/skills\/[^/]+)(?:\/|$)/;
1329
1401
  function occupiedByNonDirectory(path) {
1330
1402
  const stat = lstatSync(path, { throwIfNoEntry: false });
1331
1403
  return stat !== void 0 && !stat.isDirectory();
1332
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
+ }
1333
1415
  function foreignOwnedTarget(projectDir, rel) {
1334
1416
  const slot = SKILL_SLOT.exec(rel)?.[1];
1335
1417
  if (slot === void 0) return null;
1336
- if (occupiedByNonDirectory(join4(projectDir, slot))) return slot;
1418
+ if (occupiedByNonDirectory(join6(projectDir, slot))) return slot;
1337
1419
  if (rel !== slot) {
1338
1420
  const parts = rel.slice(slot.length + 1).split("/");
1339
1421
  let cursor = slot;
1340
1422
  for (const part of parts.slice(0, -1)) {
1341
1423
  cursor = `${cursor}/${part}`;
1342
- if (occupiedByNonDirectory(join4(projectDir, cursor))) return cursor;
1424
+ if (occupiedByNonDirectory(join6(projectDir, cursor))) return cursor;
1343
1425
  }
1344
- const leaf = lstatSync(join4(projectDir, rel), { throwIfNoEntry: false });
1426
+ const leaf = lstatSync(join6(projectDir, rel), { throwIfNoEntry: false });
1345
1427
  if (leaf !== void 0 && !leaf.isFile()) return rel;
1346
1428
  }
1347
1429
  return null;
@@ -1357,20 +1439,20 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
1357
1439
  let updated = 0;
1358
1440
  return {
1359
1441
  write(absPath, content, opts) {
1360
- const rel = relative(projectDir, absPath).split(sep).join("/");
1442
+ const rel = relative(projectDir, absPath).split(sep2).join("/");
1361
1443
  const foreign = foreignOwnedTarget(projectDir, rel);
1362
1444
  if (foreign !== null) {
1363
1445
  if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
1364
1446
  return false;
1365
1447
  }
1366
1448
  const digest = hashContent(content);
1367
- if (!existsSync4(absPath)) {
1449
+ if (!existsSync5(absPath)) {
1368
1450
  if (refreshOnly && !opts?.createInRefresh) return false;
1369
1451
  mkdirSync(dirname(absPath), { recursive: true });
1370
1452
  writeFileSync(absPath, content);
1371
1453
  updated++;
1372
1454
  } else {
1373
- const current = readFileSync3(absPath, "utf8");
1455
+ const current = readFileSync4(absPath, "utf8");
1374
1456
  if (current !== content) {
1375
1457
  if (!isHarnessOwned(baseline, rel, current)) {
1376
1458
  backupPaths.push(backupFile(absPath, now));
@@ -1397,8 +1479,8 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
1397
1479
 
1398
1480
  // src/rules-port.ts
1399
1481
  init_esm_shims();
1400
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1401
- import { join as join5 } from "path";
1482
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
1483
+ import { join as join7 } from "path";
1402
1484
  function stripClaudeFrontmatter(source) {
1403
1485
  if (!source.startsWith("---\n")) return source.trim();
1404
1486
  const end = source.indexOf("\n---", 4);
@@ -1408,9 +1490,9 @@ function stripClaudeFrontmatter(source) {
1408
1490
  function portRules(harnessRoot, ruleNames) {
1409
1491
  const ported = [];
1410
1492
  for (const name of ruleNames) {
1411
- const src = join5(harnessRoot, "templates/rules", `${name}.md`);
1412
- if (!existsSync5(src)) continue;
1413
- 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")) });
1414
1496
  }
1415
1497
  return ported;
1416
1498
  }
@@ -1430,11 +1512,17 @@ function runAntigravityTransform(params) {
1430
1512
  refreshOnly
1431
1513
  } = params;
1432
1514
  const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
1433
- 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);
1434
1522
  const antigravityInstalled = rulesFile !== null;
1435
1523
  const harnessRuleFiles = [];
1436
1524
  for (const rule of portRules(harnessRoot, rules)) {
1437
- const target = join6(projectDir, ".agents", "rules", `${rule.name}.md`);
1525
+ const target = join8(projectDir, ".agents", "rules", `${rule.name}.md`);
1438
1526
  if (!writer.write(target, `${rule.body}
1439
1527
  `, { createInRefresh: antigravityInstalled })) {
1440
1528
  continue;
@@ -1444,8 +1532,8 @@ function runAntigravityTransform(params) {
1444
1532
  const skillFiles = writeBundledSkillDirs({
1445
1533
  harnessRoot,
1446
1534
  projectDir,
1447
- skillIds: selectedInternalSkills,
1448
- writer
1535
+ skillIds,
1536
+ writer: skillWriter
1449
1537
  });
1450
1538
  return {
1451
1539
  rulesFile,
@@ -1455,14 +1543,14 @@ function runAntigravityTransform(params) {
1455
1543
  };
1456
1544
  }
1457
1545
  function writeRules(harnessRoot, projectDir, writer, selectedInternalSkills) {
1458
- const claudeMdPath = join6(harnessRoot, "templates/CLAUDE.md");
1459
- const templatePath = join6(harnessRoot, "templates/antigravity/AGENTS.md.template");
1460
- 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)) {
1461
1549
  return null;
1462
1550
  }
1463
- const claudeMd = readFileSync5(claudeMdPath, "utf8");
1464
- const template = readFileSync5(templatePath, "utf8");
1465
- 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");
1466
1554
  const rulesOut = renderAgentsMd({
1467
1555
  template,
1468
1556
  claudeMd,
@@ -1480,18 +1568,18 @@ function writeRules(harnessRoot, projectDir, writer, selectedInternalSkills) {
1480
1568
  // src/codex/opt-in.ts
1481
1569
  init_esm_shims();
1482
1570
  import { homedir } from "os";
1483
- import { join as join7 } from "path";
1571
+ import { join as join9 } from "path";
1484
1572
 
1485
1573
  // src/codex/trust-entry.ts
1486
1574
  init_esm_shims();
1487
- 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";
1488
1576
  import { dirname as dirname2 } from "path";
1489
1577
  var TRUST_BLOCK_REGEX = /\[projects\."([^"]+)"\]/g;
1490
1578
  function registerTrustEntry(opts) {
1491
1579
  const { configPath, projectDir } = opts;
1492
1580
  try {
1493
1581
  mkdirSync2(dirname2(configPath), { recursive: true });
1494
- const existing = existsSync7(configPath) ? readFileSync6(configPath, "utf8") : "";
1582
+ const existing = existsSync8(configPath) ? readFileSync7(configPath, "utf8") : "";
1495
1583
  if (hasTrustEntry(existing, projectDir)) {
1496
1584
  return { status: "already-present" };
1497
1585
  }
@@ -1515,8 +1603,8 @@ function hasTrustEntry(configContent, projectDir) {
1515
1603
 
1516
1604
  // src/codex/opt-in.ts
1517
1605
  function runCodexOptIn(ctx) {
1518
- const codexHome = ctx.codexHome ?? join7(homedir(), ".codex");
1519
- const configPath = join7(codexHome, "config.toml");
1606
+ const codexHome = ctx.codexHome ?? join9(homedir(), ".codex");
1607
+ const configPath = join9(codexHome, "config.toml");
1520
1608
  const result = registerTrustEntry({ configPath, projectDir: ctx.projectDir });
1521
1609
  return {
1522
1610
  trustEntry: {
@@ -1529,8 +1617,8 @@ function runCodexOptIn(ctx) {
1529
1617
 
1530
1618
  // src/codex/transform.ts
1531
1619
  init_esm_shims();
1532
- import { chmodSync as chmodSync2, existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
1533
- 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";
1534
1622
 
1535
1623
  // src/codex/config-toml.ts
1536
1624
  init_esm_shims();
@@ -1604,38 +1692,49 @@ function runCodexTransform(params) {
1604
1692
  selectedInternalSkills = [],
1605
1693
  rules = [],
1606
1694
  baseline,
1607
- refreshOnly
1695
+ refreshOnly,
1696
+ writeAgentsMd = true
1608
1697
  } = params;
1609
1698
  const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
1610
- const claudeMd = readRequired(join8(harnessRoot, "templates/CLAUDE.md"));
1611
- const agentsTemplate = readRequired(join8(harnessRoot, "templates/codex/AGENTS.md.template"));
1612
- 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"));
1613
1702
  const projectName = basename2(projectDir);
1614
- const mcp = readOptionalJson(join8(harnessRoot, ".mcp.json"));
1615
- const agentsMdPath = join8(projectDir, "AGENTS.md");
1616
- ensureDir(projectDir);
1617
- const agentsMdOut = renderAgentsMd({
1618
- template: agentsTemplate,
1619
- claudeMd,
1620
- projectName,
1621
- // ADR-085 — 상시 스킬 안내는 앵커가 아니라 여기(프로젝트 맥락)에, 깔린 것만.
1622
- // #503 — 그 조각은 설치자 소유 절 안에 사니 마커로 감싼다.
1623
- projectContext: withMarkedContinuousSkillsNote(
1624
- renderFillScaffold("agents-md"),
1625
- selectedInternalSkills
1626
- ),
1627
- // Codex 는 룰 디렉터리가 없다 — 룰이 AGENTS.md 본문에 들어가야 도달한다(§Harness Rules).
1628
- 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
1629
1709
  });
1630
- writer.write(
1631
- agentsMdPath,
1632
- mergeAgentsMd({
1633
- rendered: agentsMdOut,
1634
- existing: existsSync8(agentsMdPath) ? readFileSync7(agentsMdPath, "utf8") : null,
1635
- template: agentsTemplate
1636
- })
1637
- );
1638
- 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");
1639
1738
  writer.write(
1640
1739
  configTomlPath,
1641
1740
  renderConfigToml({
@@ -1645,15 +1744,15 @@ function runCodexTransform(params) {
1645
1744
  mcp
1646
1745
  })
1647
1746
  );
1648
- const hookDir = join8(projectDir, ".codex/hooks");
1747
+ const hookDir = join10(projectDir, ".codex/hooks");
1649
1748
  const hookFiles = [];
1650
1749
  for (const hook of HOOK_NAMES) {
1651
- const src = join8(harnessRoot, "templates/hooks", `${hook}.sh`);
1652
- if (!existsSync8(src)) {
1750
+ const src = join10(harnessRoot, "templates/hooks", `${hook}.sh`);
1751
+ if (!existsSync9(src)) {
1653
1752
  continue;
1654
1753
  }
1655
- const ported = readFileSync7(src, "utf8").replace(ENV_VAR_RENAME, "CODEX_PROJECT_DIR");
1656
- 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`);
1657
1756
  if (!writer.write(target, ported)) continue;
1658
1757
  chmodSync2(target, 493);
1659
1758
  hookFiles.push(target);
@@ -1661,11 +1760,12 @@ function runCodexTransform(params) {
1661
1760
  const skillFiles = writeBundledSkillDirs({
1662
1761
  harnessRoot,
1663
1762
  projectDir,
1664
- skillIds: selectedInternalSkills,
1665
- writer
1763
+ skillIds,
1764
+ writer: skillWriter
1666
1765
  });
1667
1766
  return {
1668
1767
  agentsMdPath,
1768
+ agentsMdSeededFrom: seededContext === null ? null : "CLAUDE.md",
1669
1769
  configTomlPath,
1670
1770
  hookFiles,
1671
1771
  skillFiles,
@@ -1673,17 +1773,17 @@ function runCodexTransform(params) {
1673
1773
  };
1674
1774
  }
1675
1775
  function readRequired(path) {
1676
- if (!existsSync8(path)) {
1776
+ if (!existsSync9(path)) {
1677
1777
  throw new Error(`Codex transform: required source missing: ${path}`);
1678
1778
  }
1679
- return readFileSync7(path, "utf8");
1779
+ return readFileSync8(path, "utf8");
1680
1780
  }
1681
1781
  function readOptionalJson(path) {
1682
- if (!existsSync8(path)) {
1782
+ if (!existsSync9(path)) {
1683
1783
  return null;
1684
1784
  }
1685
1785
  try {
1686
- return JSON.parse(readFileSync7(path, "utf8"));
1786
+ return JSON.parse(readFileSync8(path, "utf8"));
1687
1787
  } catch {
1688
1788
  return null;
1689
1789
  }
@@ -1691,8 +1791,8 @@ function readOptionalJson(path) {
1691
1791
 
1692
1792
  // src/opencode/transform.ts
1693
1793
  init_esm_shims();
1694
- import { existsSync as existsSync9, readdirSync, readFileSync as readFileSync8, unlinkSync } from "fs";
1695
- 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";
1696
1796
 
1697
1797
  // src/opencode/agents-md.ts
1698
1798
  init_esm_shims();
@@ -1735,24 +1835,33 @@ function runOpencodeTransform(params) {
1735
1835
  refreshOnly
1736
1836
  } = params;
1737
1837
  const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
1738
- const claudeMd = readRequired2(join9(harnessRoot, "templates/CLAUDE.md"));
1739
- 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"));
1740
1840
  const opencodeTemplate = readRequired2(
1741
- join9(harnessRoot, "templates/opencode/opencode.json.template")
1841
+ join11(harnessRoot, "templates/opencode/opencode.json.template")
1742
1842
  );
1743
1843
  const projectName = basename3(projectDir);
1744
- 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
+ });
1745
1851
  ensureDir(projectDir);
1746
- const agentsMdPath = join9(projectDir, "AGENTS.md");
1852
+ const agentsMdPath = join11(projectDir, "AGENTS.md");
1853
+ const seededContext = refreshOnly || existsSync10(agentsMdPath) ? null : seedAgentsMdProjectContext(projectDir);
1747
1854
  const agentsMdOut = renderAgentsMd2({
1748
1855
  template: agentsTemplate,
1749
1856
  claudeMd,
1750
1857
  projectName,
1751
1858
  // ADR-085 — 상시 스킬 안내는 앵커가 아니라 여기(프로젝트 맥락)에, 깔린 것만.
1752
1859
  // #503 — 그 조각은 설치자 소유 절 안에 사니 마커로 감싼다 (codex 와 같은 마커).
1860
+ // #531 — `skillIds` 를 쓴다(= 깔린 것 + **이번 실행이 만들 것**). 안 그러면 이번에 새로
1861
+ // 깐 상시 스킬이 다음 update 까지 안내에서 빠져 설치자 에이전트가 영영 안 연다.
1753
1862
  projectContext: withMarkedContinuousSkillsNote(
1754
- renderFillScaffold("agents-md"),
1755
- selectedInternalSkills
1863
+ seededContext ?? renderFillScaffold("agents-md"),
1864
+ skillIds
1756
1865
  ),
1757
1866
  // codex 와 **같은 파일**(프로젝트 루트 `AGENTS.md`)이다. 두 transform 이 서로 다른 본문을
1758
1867
  // 쓰면 나중에 도는 쪽이 앞선 쪽을 덮어써, codex+opencode 조합에서 룰이 통째로 사라진다
@@ -1763,26 +1872,26 @@ function runOpencodeTransform(params) {
1763
1872
  agentsMdPath,
1764
1873
  mergeAgentsMd({
1765
1874
  rendered: agentsMdOut,
1766
- existing: existsSync9(agentsMdPath) ? readFileSync8(agentsMdPath, "utf8") : null,
1875
+ existing: existsSync10(agentsMdPath) ? readFileSync9(agentsMdPath, "utf8") : null,
1767
1876
  template: agentsTemplate
1768
1877
  })
1769
1878
  );
1770
- const opencodeJsonPath = join9(projectDir, "opencode.json");
1879
+ const opencodeJsonPath = join11(projectDir, "opencode.json");
1771
1880
  writer.write(opencodeJsonPath, renderOpencodeJson({ template: opencodeTemplate, mcp }));
1772
1881
  const skillFiles = writeBundledSkillDirs({
1773
1882
  harnessRoot,
1774
1883
  projectDir,
1775
- skillIds: selectedInternalSkills,
1776
- writer
1884
+ skillIds,
1885
+ writer: skillWriter
1777
1886
  });
1778
1887
  const retiredCommands = [];
1779
- const cmdDir = join9(projectDir, ".opencode/commands");
1780
- if (existsSync9(cmdDir)) {
1888
+ const cmdDir = join11(projectDir, ".opencode/commands");
1889
+ if (existsSync10(cmdDir)) {
1781
1890
  for (const entry of readdirSync(cmdDir)) {
1782
1891
  if (!entry.endsWith(".md")) continue;
1783
1892
  const id = entry.slice(0, -3);
1784
- if (!existsSync9(join9(harnessRoot, "templates/skills", id, "SKILL.md"))) continue;
1785
- const victim = join9(cmdDir, entry);
1893
+ if (!existsSync10(join11(harnessRoot, "templates/skills", id, "SKILL.md"))) continue;
1894
+ const victim = join11(cmdDir, entry);
1786
1895
  try {
1787
1896
  backupFile(victim);
1788
1897
  unlinkSync(victim);
@@ -1793,6 +1902,7 @@ function runOpencodeTransform(params) {
1793
1902
  }
1794
1903
  return {
1795
1904
  agentsMdPath,
1905
+ agentsMdSeededFrom: seededContext === null ? null : "CLAUDE.md",
1796
1906
  opencodeJsonPath,
1797
1907
  skillFiles,
1798
1908
  retiredCommands,
@@ -1800,17 +1910,17 @@ function runOpencodeTransform(params) {
1800
1910
  };
1801
1911
  }
1802
1912
  function readRequired2(path) {
1803
- if (!existsSync9(path)) {
1913
+ if (!existsSync10(path)) {
1804
1914
  throw new Error(`OpenCode transform: required source missing: ${path}`);
1805
1915
  }
1806
- return readFileSync8(path, "utf8");
1916
+ return readFileSync9(path, "utf8");
1807
1917
  }
1808
1918
  function readOptionalJson2(path) {
1809
- if (!existsSync9(path)) {
1919
+ if (!existsSync10(path)) {
1810
1920
  return null;
1811
1921
  }
1812
1922
  try {
1813
- return JSON.parse(readFileSync8(path, "utf8"));
1923
+ return JSON.parse(readFileSync9(path, "utf8"));
1814
1924
  } catch {
1815
1925
  return null;
1816
1926
  }
@@ -1856,7 +1966,11 @@ function runCliTransforms(params) {
1856
1966
  selectedInternalSkills,
1857
1967
  rules,
1858
1968
  baseline,
1859
- refreshOnly
1969
+ refreshOnly,
1970
+ // #550 — 같은 `AGENTS.md` 를 한 실행에서 쓰는 쪽은 하나다. opencode 가 뒤에서 자기 템플릿으로
1971
+ // 쓰면 codex 판은 어차피 같은 실행 안에서 덮였고(최종 파일 = opencode 판), 매 실행 codex 가
1972
+ // opencode 판을 자기 판으로 뒤집는 한 번의 쓰기가 설치자 편집분을 백업으로 쌓았다.
1973
+ writeAgentsMd: !cli2.includes("opencode")
1860
1974
  });
1861
1975
  absorb(codex);
1862
1976
  if (codexTrust) {
@@ -1902,8 +2016,8 @@ function runCliTransforms(params) {
1902
2016
 
1903
2017
  // src/env-files.ts
1904
2018
  init_esm_shims();
1905
- import { appendFileSync, existsSync as existsSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync3 } from "fs";
1906
- 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";
1907
2021
  var ENV_EXAMPLE_BODY = `# .env.example \u2014 csr-supabase Track
1908
2022
  # Copy to .env (gitignored) and fill in values: cp .env.example .env
1909
2023
 
@@ -1940,24 +2054,24 @@ function writeEnvExample(projectDir, tracks) {
1940
2054
  if (!tracks.some((t) => ENV_EXAMPLE_TRACKS.includes(t))) {
1941
2055
  return false;
1942
2056
  }
1943
- const path = join10(projectDir, ".env.example");
1944
- if (existsSync10(path)) {
2057
+ const path = join12(projectDir, ".env.example");
2058
+ if (existsSync11(path)) {
1945
2059
  return false;
1946
2060
  }
1947
2061
  writeFileSync3(path, ENV_EXAMPLE_BODY);
1948
2062
  return true;
1949
2063
  }
1950
2064
  function addGitignoreEnv(projectDir) {
1951
- const path = join10(projectDir, ".gitignore");
1952
- if (!existsSync10(path)) {
2065
+ const path = join12(projectDir, ".gitignore");
2066
+ if (!existsSync11(path)) {
1953
2067
  return false;
1954
2068
  }
1955
- const content = readFileSync9(path, "utf8");
2069
+ const content = readFileSync10(path, "utf8");
1956
2070
  if (GITIGNORE_ENV_PATTERN.test(content)) {
1957
2071
  return false;
1958
2072
  }
1959
- const sep3 = content.endsWith("\n") ? "" : "\n";
1960
- appendFileSync(path, `${sep3}
2073
+ const sep4 = content.endsWith("\n") ? "" : "\n";
2074
+ appendFileSync(path, `${sep4}
1961
2075
  # Secret env (auto-added by agent-harness install)
1962
2076
  .env
1963
2077
  `);
@@ -1966,11 +2080,11 @@ function addGitignoreEnv(projectDir) {
1966
2080
  var AGENT_ARTIFACT_DIRS = [".factory/", ".goose/", ".uzys-agent-harness/"];
1967
2081
  var GITIGNORE_AGENT_ARTIFACT_HEADER = "# agent CLI / harness \uC790\uB3D9 \uC0DD\uC131\uBB3C (auto-added by agent-harness)";
1968
2082
  function addGitignoreAgentArtifacts(projectDir) {
1969
- const path = join10(projectDir, ".gitignore");
1970
- if (!existsSync10(path)) {
2083
+ const path = join12(projectDir, ".gitignore");
2084
+ if (!existsSync11(path)) {
1971
2085
  return [];
1972
2086
  }
1973
- const content = readFileSync9(path, "utf8");
2087
+ const content = readFileSync10(path, "utf8");
1974
2088
  const missing = AGENT_ARTIFACT_DIRS.filter((pattern) => {
1975
2089
  const lineRegex = new RegExp(`^${pattern.replace(/\./g, "\\.").replace(/\//g, "/")}\\s*$`, "m");
1976
2090
  return !lineRegex.test(content);
@@ -1978,9 +2092,9 @@ function addGitignoreAgentArtifacts(projectDir) {
1978
2092
  if (missing.length === 0) {
1979
2093
  return [];
1980
2094
  }
1981
- const sep3 = content.endsWith("\n") ? "" : "\n";
2095
+ const sep4 = content.endsWith("\n") ? "" : "\n";
1982
2096
  const block = [GITIGNORE_AGENT_ARTIFACT_HEADER, ...missing].join("\n");
1983
- appendFileSync(path, `${sep3}
2097
+ appendFileSync(path, `${sep4}
1984
2098
  ${block}
1985
2099
  `);
1986
2100
  return [...missing];
@@ -1989,9 +2103,9 @@ ${block}
1989
2103
  // src/external-installer.ts
1990
2104
  init_esm_shims();
1991
2105
  import { spawnSync } from "child_process";
1992
- 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";
1993
2107
  import { homedir as homedir2 } from "os";
1994
- import { join as join11 } from "path";
2108
+ import { join as join13 } from "path";
1995
2109
  var DEFAULT_SPAWN_TIMEOUT_MS = 12e4;
1996
2110
  var NPM_SPAWN_TIMEOUT_MS = 6e5;
1997
2111
  function spawnTimeoutFor(kind) {
@@ -2103,7 +2217,10 @@ function refreshExternalSkills(projectDir, deps = {}) {
2103
2217
  tracks: log.spec.tracks,
2104
2218
  // `forceInclude` 가 조건 판정을 앞지르므로 옵션 값은 결과에 영향을 주지 않는다.
2105
2219
  options: DEFAULT_OPTIONS,
2106
- 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)],
2107
2224
  userOverride: { forceInclude: [...installedIds], forceExclude: [] },
2108
2225
  scope: log.scope,
2109
2226
  projectDir
@@ -2199,17 +2316,17 @@ function detectVersion(method, spawn) {
2199
2316
  if (at2 <= 0) return void 0;
2200
2317
  const plugin = method.pluginId.slice(0, at2);
2201
2318
  const marketplaceShort = method.pluginId.slice(at2 + 1);
2202
- const cacheBase = join11(homedir2(), ".claude/plugins/cache", marketplaceShort, plugin);
2203
- if (!existsSync11(cacheBase)) return void 0;
2319
+ const cacheBase = join13(homedir2(), ".claude/plugins/cache", marketplaceShort, plugin);
2320
+ if (!existsSync12(cacheBase)) return void 0;
2204
2321
  const versions = readdirSync2(cacheBase).filter((v2) => /^\d/.test(v2)).sort();
2205
2322
  return versions.at(-1);
2206
2323
  }
2207
2324
  case "npm": {
2208
2325
  const npmRoot = getNpmGlobalRoot(spawn);
2209
2326
  if (!npmRoot) return void 0;
2210
- const pkgJson = join11(npmRoot, method.pkg, "package.json");
2211
- if (!existsSync11(pkgJson)) return void 0;
2212
- 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"));
2213
2330
  return parsed.version;
2214
2331
  }
2215
2332
  default:
@@ -2234,9 +2351,30 @@ function getNpmGlobalRoot(spawn) {
2234
2351
  return void 0;
2235
2352
  }
2236
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
+
2237
2375
  // src/mcp-merge.ts
2238
2376
  init_esm_shims();
2239
- 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";
2240
2378
  function parseTrackMcpMap(raw) {
2241
2379
  const rows = [];
2242
2380
  for (const line of raw.split(/\r?\n/)) {
@@ -2287,15 +2425,15 @@ function mergeMcpServers(base, rows, tracks) {
2287
2425
  return out;
2288
2426
  }
2289
2427
  function composeMcpJson(opts) {
2290
- const base = JSON.parse(readFileSync11(opts.templateMcpPath, "utf8"));
2291
- const merged = opts.existingPath && existsSync12(opts.existingPath) ? mergeUserBase(base, opts.existingPath) : base;
2292
- 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") : "";
2293
2431
  const rows = parseTrackMcpMap(mapRaw);
2294
2432
  return mergeMcpServers(merged, rows, opts.tracks);
2295
2433
  }
2296
2434
  function mergeUserBase(base, existingPath) {
2297
2435
  try {
2298
- const existing = JSON.parse(readFileSync11(existingPath, "utf8"));
2436
+ const existing = JSON.parse(readFileSync12(existingPath, "utf8"));
2299
2437
  return {
2300
2438
  ...base,
2301
2439
  mcpServers: { ...base.mcpServers, ...existing.mcpServers }
@@ -2313,24 +2451,26 @@ function writeMcpJson(path, mcp) {
2313
2451
  init_esm_shims();
2314
2452
  import {
2315
2453
  copyFileSync,
2316
- existsSync as existsSync13,
2454
+ existsSync as existsSync14,
2317
2455
  mkdirSync as mkdirSync3,
2318
2456
  readdirSync as readdirSync3,
2319
- readFileSync as readFileSync12,
2457
+ readFileSync as readFileSync13,
2320
2458
  rmdirSync,
2321
- statSync,
2459
+ statSync as statSync2,
2322
2460
  unlinkSync as unlinkSync2,
2323
2461
  writeFileSync as writeFileSync5
2324
2462
  } from "fs";
2325
- 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";
2326
2464
  var LEGACY_ANCHOR_FILE = ".claude/CLAUDE.md";
2327
2465
  function buildUpdateSpec(projectDir, tracks, only) {
2466
+ const log = readInstallLog(projectDir);
2467
+ const clis = log === null ? [] : installedClis(log);
2328
2468
  return {
2329
2469
  tracks: [...tracks],
2330
2470
  options: DEFAULT_OPTIONS,
2331
- cli: ["claude"],
2471
+ cli: clis.length > 0 ? [...clis] : ["claude"],
2332
2472
  projectDir,
2333
- scope: readInstallLog(projectDir)?.scope ?? "project",
2473
+ scope: log?.scope ?? "project",
2334
2474
  // 전부 골랐으면 "제한 없음"과 같다 — 화면·기록에 제한이 있었던 것처럼 남기지 않는다.
2335
2475
  ...only !== void 0 && only.length > 0 && only.length < UPDATE_GROUPS.length ? { updateOnly: [...only] } : {}
2336
2476
  };
@@ -2341,7 +2481,7 @@ function parseUpdateOnly(raw) {
2341
2481
  return { ok: true, groups: [...new Set(raw)] };
2342
2482
  }
2343
2483
  function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
2344
- const claudeDir = join12(projectDir, ".claude");
2484
+ const claudeDir = join15(projectDir, ".claude");
2345
2485
  const startedAt = Date.now();
2346
2486
  const wants = (g) => only === void 0 || only.includes(g);
2347
2487
  const report = {
@@ -2356,6 +2496,7 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
2356
2496
  rootImportAdded: false,
2357
2497
  rootBlockRefreshed: false,
2358
2498
  legacyAnchor: null,
2499
+ claudeUnrecorded: null,
2359
2500
  skillsBackedUp: [],
2360
2501
  skillsSkippedLinks: [],
2361
2502
  skillsPruned: [],
@@ -2378,11 +2519,14 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
2378
2519
  report.installedNew = fresh.installed;
2379
2520
  report.restored = fresh.restored;
2380
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;
2381
2525
  const policyBase = policyBaseline(projectDir);
2382
2526
  for (const { dir, ext } of POLICY_DIRS) {
2383
- if (!wants(dir === "hooks" ? "hooks" : "rules")) continue;
2384
- const target = join12(claudeDir, dir);
2385
- 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);
2386
2530
  const label = `.claude/${dir}`;
2387
2531
  const ctx = { prefix: dir, baseline: policyBase };
2388
2532
  const synced = updateDir(target, source, ext, ctx);
@@ -2393,10 +2537,11 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
2393
2537
  const syncedDirs = POLICY_DIRS.filter((d2) => wants(d2.dir === "hooks" ? "hooks" : "rules")).map(
2394
2538
  (d2) => d2.dir
2395
2539
  );
2396
- if (syncedDirs.length > 0) refreshPolicyBaseline(projectDir, templatesDir, syncedDirs);
2397
- const skillSync = wants("skills") ? syncSkills(
2398
- join12(claudeDir, "skills"),
2399
- 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"),
2400
2545
  skillBaseline(projectDir),
2401
2546
  /* @__PURE__ */ new Date(),
2402
2547
  (relInSkills) => foreignOwnedTarget(projectDir, `.claude/skills/${relInSkills}`)
@@ -2405,25 +2550,36 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
2405
2550
  report.skillsBackedUp = skillSync.backedUp;
2406
2551
  report.skillsSkippedLinks = skillSync.skippedLinks;
2407
2552
  report.skillsPruned = skillSync.pruned;
2408
- 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: [] };
2409
2554
  report.installedNew.push(...newSkills.installed);
2410
- if (wants("skills")) refreshSkillBaseline(projectDir, templatesDir);
2411
- else if (newSkills.installed.length > 0)
2412
- recordNewSkillBaseline(projectDir, templatesDir, newSkills.installed);
2413
- const installLog = readInstallLog(projectDir);
2414
- 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) {
2415
2561
  if (wants("anchor")) syncHarnessAnchor(projectDir, templatesDir, report);
2416
2562
  }
2417
- const settingsPath = join12(claudeDir, "settings.json");
2418
- if (wants("hooks") && existsSync13(settingsPath)) {
2563
+ const settingsPath = join15(claudeDir, "settings.json");
2564
+ if (claudeManaged && wants("hooks") && existsSync14(settingsPath)) {
2419
2565
  report.staleHookRefs = cleanStaleHookRefs(settingsPath, claudeDir);
2420
2566
  }
2421
2567
  if (wants("hooks")) report.mcpAllowlistRetired = retireMcpAllowlist(projectDir);
2422
2568
  report.retiredAgents = staleAgentFiles(claudeDir);
2423
2569
  report.demotedAgents = demotedAgentFiles(claudeDir, installedTracks(projectDir));
2424
- 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
+ };
2425
2578
  report.externalUpdated = external.externalUpdated;
2426
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));
2427
2583
  const saidBySlotRow = new Set(report.skillsSkippedLinks.map((id) => `.claude/skills/${id}`));
2428
2584
  const merged = [];
2429
2585
  for (const f of [
@@ -2435,6 +2591,17 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
2435
2591
  if (!saidBySlotRow.has(f) && !merged.includes(f)) merged.push(f);
2436
2592
  }
2437
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
+ }
2438
2605
  const skillRefresh = wants("external") ? (deps.refreshSkills ?? refreshExternalSkills)(projectDir) : { attempted: 0, refreshed: 0, failed: [], notInCatalog: [], unknown: false };
2439
2606
  report.externalSkillsRefreshed = skillRefresh.refreshed;
2440
2607
  report.externalSkillsFailed = skillRefresh.failed;
@@ -2448,20 +2615,20 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
2448
2615
  return report;
2449
2616
  }
2450
2617
  function staleSkillDirs(claudeDir) {
2451
- const dir = join12(claudeDir, "skills");
2452
- if (!existsSync13(dir)) return [];
2618
+ const dir = join15(claudeDir, "skills");
2619
+ if (!existsSync14(dir)) return [];
2453
2620
  const stale = (id) => RENAMED_SKILL_IDS[id] !== void 0 || RETIRED_SKILL_IDS.includes(id);
2454
2621
  return readdirSync3(dir, { withFileTypes: true }).filter((e2) => e2.isDirectory() && stale(e2.name)).map((e2) => e2.name);
2455
2622
  }
2456
2623
  function staleAgentFiles(claudeDir) {
2457
- const dir = join12(claudeDir, "agents");
2458
- if (!existsSync13(dir)) return [];
2624
+ const dir = join15(claudeDir, "agents");
2625
+ if (!existsSync14(dir)) return [];
2459
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));
2460
2627
  }
2461
2628
  function demotedAgentFiles(claudeDir, tracks) {
2462
2629
  if (tracks.length === 0) return [];
2463
- const dir = join12(claudeDir, "agents");
2464
- if (!existsSync13(dir)) return [];
2630
+ const dir = join15(claudeDir, "agents");
2631
+ if (!existsSync14(dir)) return [];
2465
2632
  const demoted = TRACK_AGENTS.filter(([, pattern]) => !anyTrack(tracks, pattern)).map(
2466
2633
  ([id]) => id
2467
2634
  );
@@ -2473,7 +2640,7 @@ function installNewAssets(projectDir, templatesDir, wants = () => true) {
2473
2640
  const needsReinstall = [];
2474
2641
  const foreignOwned = [];
2475
2642
  const log = readInstallLog(projectDir);
2476
- const claudeSelected = log?.spec.cli.includes("claude") ?? false;
2643
+ const claudeSelected = installedClis(log).includes("claude");
2477
2644
  const baselineExcluded = new Set(log?.spec.baselineExclude ?? []);
2478
2645
  const priorBaseline = policyBaseline(projectDir);
2479
2646
  for (const entry of trackOnlyFileAssets(installedTracks(projectDir))) {
@@ -2481,15 +2648,15 @@ function installNewAssets(projectDir, templatesDir, wants = () => true) {
2481
2648
  if (!wants(group)) continue;
2482
2649
  if (entry.target === HARNESS_ANCHOR_FILE) continue;
2483
2650
  if (isBaselineExcluded(entry.target, baselineExcluded)) continue;
2484
- const target = join12(projectDir, entry.target);
2651
+ const target = join15(projectDir, entry.target);
2485
2652
  const foreign = foreignOwnedTarget(projectDir, entry.target);
2486
2653
  if (foreign !== null) {
2487
2654
  if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
2488
2655
  continue;
2489
2656
  }
2490
- if (existsSync13(target)) continue;
2491
- const source = join12(templatesDir, entry.source);
2492
- if (!existsSync13(source)) continue;
2657
+ if (existsSync14(target)) continue;
2658
+ const source = join15(templatesDir, entry.source);
2659
+ if (!existsSync14(source)) continue;
2493
2660
  if (entry.target.startsWith(".claude/")) {
2494
2661
  if (!claudeSelected) continue;
2495
2662
  if (entry.target.startsWith(".claude/hooks/") || entry.target === ".claude/settings.json") {
@@ -2511,26 +2678,26 @@ function collectRunBackups(projectDir, startedAt) {
2511
2678
  const out = [];
2512
2679
  const consider = (rel) => {
2513
2680
  if (!BACKUP_SUFFIX.test(rel)) return;
2514
- const abs = join12(projectDir, rel);
2515
- if (statSync(abs).mtimeMs < startedAt - 1e3) return;
2681
+ const abs = join15(projectDir, rel);
2682
+ if (statSync2(abs).mtimeMs < startedAt - 1e3) return;
2516
2683
  out.push({ path: rel.replace(BACKUP_SUFFIX, ""), backup: rel });
2517
2684
  };
2518
2685
  for (const e2 of readdirSync3(projectDir, { withFileTypes: true }))
2519
2686
  if (e2.isFile()) consider(e2.name);
2520
2687
  for (const dir of BACKUP_SCAN_DIRS) {
2521
- for (const rel of listFilesRecursive(join12(projectDir, dir))) consider(`${dir}/${rel}`);
2688
+ for (const rel of listFilesRecursive(join15(projectDir, dir))) consider(`${dir}/${rel}`);
2522
2689
  }
2523
2690
  return out.sort((a, b2) => a.path.localeCompare(b2.path));
2524
2691
  }
2525
2692
  function writeBackupList(projectDir, backups) {
2526
- const dir = join12(projectDir, ".uzys-agent-harness");
2527
- const file = join12(dir, "update-backups.json");
2693
+ const dir = join15(projectDir, ".uzys-agent-harness");
2694
+ const file = join15(dir, "update-backups.json");
2528
2695
  try {
2529
2696
  if (backups.length === 0) {
2530
- if (existsSync13(file)) unlinkSync2(file);
2697
+ if (existsSync14(file)) unlinkSync2(file);
2531
2698
  return;
2532
2699
  }
2533
- if (!existsSync13(dir)) return;
2700
+ if (!existsSync14(dir)) return;
2534
2701
  writeFileSync5(
2535
2702
  file,
2536
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)}
@@ -2541,29 +2708,33 @@ function writeBackupList(projectDir, backups) {
2541
2708
  }
2542
2709
  function installNewSkillDirs(projectDir, templatesDir, tracks) {
2543
2710
  const installed = [];
2711
+ const restored = [];
2544
2712
  const foreignOwned = [];
2545
2713
  const log = readInstallLog(projectDir);
2714
+ const priorIds = new Set((log?.skillFiles ?? []).map((f) => f.path.split("/")[0] ?? ""));
2546
2715
  const excluded = new Set(log?.spec.baselineExclude ?? []);
2547
2716
  const skillExcluded = new Set(log?.spec.skillExclude ?? []);
2548
- if (!(log?.spec.cli ?? ["claude"]).includes("claude")) return { installed, foreignOwned };
2717
+ if (log !== null && !installedClis(log).includes("claude"))
2718
+ return { installed, restored, foreignOwned };
2549
2719
  const spec = buildAssetSpec({ tracks, options: DEFAULT_OPTIONS });
2550
2720
  for (const entry of buildManifest(spec)) {
2551
2721
  if (entry.type !== "dir" || !entry.target.startsWith(".claude/skills/")) continue;
2552
2722
  if (!entry.applies(spec) || isBaselineExcluded(entry.target, excluded)) continue;
2553
2723
  if (skillExcluded.has(entry.target.slice(".claude/skills/".length))) continue;
2554
- const target = join12(projectDir, entry.target);
2724
+ const target = join15(projectDir, entry.target);
2555
2725
  const foreign = foreignOwnedTarget(projectDir, entry.target);
2556
2726
  if (foreign !== null) {
2557
2727
  if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
2558
2728
  continue;
2559
2729
  }
2560
- if (existsSync13(target)) continue;
2561
- const source = join12(templatesDir, entry.source);
2562
- if (!existsSync13(source)) continue;
2730
+ if (existsSync14(target)) continue;
2731
+ const source = join15(templatesDir, entry.source);
2732
+ if (!existsSync14(source)) continue;
2563
2733
  copyDir(source, target, (rel) => foreignOwnedTarget(projectDir, `${entry.target}/${rel}`));
2564
- installed.push(entry.target);
2734
+ if (priorIds.has(entry.target.slice(".claude/skills/".length))) restored.push(entry.target);
2735
+ else installed.push(entry.target);
2565
2736
  }
2566
- return { installed, foreignOwned };
2737
+ return { installed, restored, foreignOwned };
2567
2738
  }
2568
2739
  function recordNewSkillBaseline(projectDir, templatesDir, targets) {
2569
2740
  const log = readInstallLog(projectDir);
@@ -2587,16 +2758,16 @@ function trackOnlyFileAssets(tracks) {
2587
2758
  );
2588
2759
  }
2589
2760
  function syncHarnessAnchor(projectDir, templatesDir, report, now = /* @__PURE__ */ new Date()) {
2590
- if (existsSync13(join12(projectDir, LEGACY_ANCHOR_FILE))) {
2761
+ if (existsSync14(join15(projectDir, LEGACY_ANCHOR_FILE))) {
2591
2762
  report.legacyAnchor = LEGACY_ANCHOR_FILE;
2592
2763
  }
2593
- const templateMd = join12(templatesDir, "CLAUDE.md");
2594
- if (!existsSync13(templateMd)) return;
2595
- const anchor = join12(projectDir, HARNESS_ANCHOR_FILE);
2596
- 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);
2597
2768
  if (existed) {
2598
- const current = readFileSync12(anchor, "utf8");
2599
- if (current !== readFileSync12(templateMd, "utf8")) {
2769
+ const current = readFileSync13(anchor, "utf8");
2770
+ if (current !== readFileSync13(templateMd, "utf8")) {
2600
2771
  const recorded = readInstallLog(projectDir)?.templates.rootClaudeMd;
2601
2772
  const owned = recorded?.path === HARNESS_ANCHOR_FILE && hashContent(current) === recorded.sha256;
2602
2773
  if (!owned) {
@@ -2616,15 +2787,15 @@ function syncHarnessAnchor(projectDir, templatesDir, report, now = /* @__PURE__
2616
2787
  recordAnchorBaseline(projectDir, anchor);
2617
2788
  }
2618
2789
  function upsertRootImport(projectDir) {
2619
- const target = join12(projectDir, "CLAUDE.md");
2620
- const existing = existsSync13(target) ? readFileSync12(target, "utf8") : null;
2790
+ const target = join15(projectDir, "CLAUDE.md");
2791
+ const existing = existsSync14(target) ? readFileSync13(target, "utf8") : null;
2621
2792
  const next = upsertHarnessImport(existing, {
2622
2793
  projectName: basename4(projectDir),
2623
2794
  tracks: installedTracks(projectDir),
2624
2795
  // ADR-085 — update 는 선택 목록을 인자로 받지 않는다. `syncSkills` 와 같은 판정을 쓴다:
2625
2796
  // `.claude/skills/<id>` 가 있으면 그 스킬은 이 설치가 고른 것이다.
2626
2797
  continuousSkills: CONTINUOUS_SKILLS.map((s) => s.id).filter(
2627
- (id) => existsSync13(join12(projectDir, ".claude/skills", id))
2798
+ (id) => existsSync14(join15(projectDir, ".claude/skills", id))
2628
2799
  )
2629
2800
  });
2630
2801
  if (next === existing) return false;
@@ -2644,7 +2815,7 @@ function recordAnchorBaseline(projectDir, anchor) {
2644
2815
  ...log.templates,
2645
2816
  rootClaudeMd: {
2646
2817
  path: HARNESS_ANCHOR_FILE,
2647
- sha256: hashContent(readFileSync12(anchor, "utf8"))
2818
+ sha256: hashContent(readFileSync13(anchor, "utf8"))
2648
2819
  }
2649
2820
  }
2650
2821
  };
@@ -2655,20 +2826,41 @@ function recordAnchorBaseline(projectDir, anchor) {
2655
2826
  }
2656
2827
  function installedBundledSkills(projectDir) {
2657
2828
  return INTERNAL_BUNDLED_SKILL_IDS.filter(
2658
- (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))
2659
2830
  );
2660
2831
  }
2661
2832
  function installedCliTargets(log) {
2662
2833
  if (log === null) return ALL_CLI_TARGETS;
2663
- return ALL_CLI_TARGETS.filter((cli2) => {
2664
- if (cli2 === "codex") return log.templates.codexDir !== void 0;
2665
- if (cli2 === "opencode") return log.templates.opencodeDir !== void 0;
2666
- return true;
2667
- });
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"))));
2668
2859
  }
2669
2860
  function refreshExternalCli(projectDir, harnessRoot) {
2670
2861
  const log = readInstallLog(projectDir);
2671
2862
  const baselineExcluded = new Set(log?.spec.baselineExclude ?? []);
2863
+ const presentBefore = sharedSkillIdsOnDisk(projectDir);
2672
2864
  const result = runCliTransforms({
2673
2865
  harnessRoot,
2674
2866
  projectDir,
@@ -2692,26 +2884,41 @@ function refreshExternalCli(projectDir, harnessRoot) {
2692
2884
  } catch {
2693
2885
  }
2694
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
+ }
2695
2899
  return {
2696
2900
  externalUpdated: result.externalUpdated,
2697
2901
  externalBackedUp: result.externalBackedUp,
2698
- externalForeignOwned: result.externalForeignOwned
2902
+ externalForeignOwned: result.externalForeignOwned,
2903
+ written: result.externalFiles.map((f) => f.path),
2904
+ skillsInstalled,
2905
+ skillsRestored
2699
2906
  };
2700
2907
  }
2701
2908
  function isHarnessOwned2(ctx, rel, current) {
2702
2909
  return isHarnessOwned(ctx.baseline, `${ctx.prefix}/${rel}`, current);
2703
2910
  }
2704
2911
  function updateDir(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
2705
- if (!existsSync13(target) || !existsSync13(source)) return { updated: 0, backedUp: [] };
2912
+ if (!existsSync14(target) || !existsSync14(source)) return { updated: 0, backedUp: [] };
2706
2913
  let updated = 0;
2707
2914
  const backedUp = [];
2708
2915
  for (const file of readdirSync3(target)) {
2709
2916
  if (!file.endsWith(ext)) continue;
2710
- const targetFile = join12(target, file);
2711
- const sourceFile = join12(source, file);
2712
- if (!existsSync13(sourceFile)) continue;
2713
- const next = readFileSync12(sourceFile, "utf8");
2714
- 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");
2715
2922
  if (current === next) continue;
2716
2923
  if (!isHarnessOwned2(ctx, file, current)) {
2717
2924
  backupFile(targetFile, now);
@@ -2723,7 +2930,7 @@ function updateDir(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
2723
2930
  return { updated, backedUp };
2724
2931
  }
2725
2932
  function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Date(), foreignOf) {
2726
- if (!existsSync13(targetDir) || !existsSync13(sourceDir))
2933
+ if (!existsSync14(targetDir) || !existsSync14(sourceDir))
2727
2934
  return { updated: 0, backedUp: [], skippedLinks: [], foreignOwned: [], pruned: [] };
2728
2935
  let updated = 0;
2729
2936
  const backedUp = [];
@@ -2732,27 +2939,27 @@ function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Da
2732
2939
  const pruned = [];
2733
2940
  for (const skill of readdirSync3(sourceDir, { withFileTypes: true })) {
2734
2941
  if (!skill.isDirectory()) continue;
2735
- const targetSkill = join12(targetDir, skill.name);
2942
+ const targetSkill = join15(targetDir, skill.name);
2736
2943
  if (occupiedByNonDirectory(targetSkill)) {
2737
2944
  skippedLinks.push(skill.name);
2738
2945
  continue;
2739
2946
  }
2740
- if (!existsSync13(targetSkill)) continue;
2741
- for (const rel of listFilesRecursive(join12(sourceDir, skill.name))) {
2742
- 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);
2743
2950
  const foreign = foreignOf(`${skill.name}/${rel}`);
2744
2951
  if (foreign !== null) {
2745
2952
  if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
2746
2953
  continue;
2747
2954
  }
2748
- const next = readFileSync12(join12(sourceDir, skill.name, rel), "utf8");
2749
- if (!existsSync13(targetFile)) {
2955
+ const next = readFileSync13(join15(sourceDir, skill.name, rel), "utf8");
2956
+ if (!existsSync14(targetFile)) {
2750
2957
  mkdirSync3(dirname3(targetFile), { recursive: true });
2751
2958
  writeFileSync5(targetFile, next);
2752
2959
  updated++;
2753
2960
  continue;
2754
2961
  }
2755
- const current = readFileSync12(targetFile, "utf8");
2962
+ const current = readFileSync13(targetFile, "utf8");
2756
2963
  if (current === next) continue;
2757
2964
  const recorded = baseline.get(`${skill.name}/${rel}`);
2758
2965
  if (recorded === void 0 || hashContent(current) !== recorded) {
@@ -2762,13 +2969,13 @@ function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Da
2762
2969
  writeFileSync5(targetFile, next);
2763
2970
  updated++;
2764
2971
  }
2765
- const bundled = new Set(listFilesRecursive(join12(sourceDir, skill.name)));
2972
+ const bundled = new Set(listFilesRecursive(join15(sourceDir, skill.name)));
2766
2973
  for (const rel of listFilesRecursive(targetSkill)) {
2767
2974
  if (bundled.has(rel) || basename4(rel).includes(".backup-")) continue;
2768
2975
  if (foreignOf(`${skill.name}/${rel}`) !== null) continue;
2769
- const targetFile = join12(targetSkill, rel);
2976
+ const targetFile = join15(targetSkill, rel);
2770
2977
  const recorded = baseline.get(`${skill.name}/${rel}`);
2771
- if (recorded === void 0 || hashContent(readFileSync12(targetFile, "utf8")) !== recorded) {
2978
+ if (recorded === void 0 || hashContent(readFileSync13(targetFile, "utf8")) !== recorded) {
2772
2979
  backupFile(targetFile, now);
2773
2980
  backedUp.push(`${skill.name}/${rel}`);
2774
2981
  }
@@ -2817,16 +3024,16 @@ function refreshSkillBaseline(projectDir, templatesDir) {
2817
3024
  }
2818
3025
  }
2819
3026
  function pruneOrphans(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
2820
- if (!existsSync13(target) || !existsSync13(source)) return [];
3027
+ if (!existsSync14(target) || !existsSync14(source)) return [];
2821
3028
  const removed = [];
2822
3029
  for (const file of readdirSync3(target)) {
2823
3030
  if (!file.endsWith(ext)) continue;
2824
- const sourceFile = join12(source, file);
2825
- if (!existsSync13(sourceFile)) {
2826
- const targetFile = join12(target, file);
3031
+ const sourceFile = join15(source, file);
3032
+ if (!existsSync14(sourceFile)) {
3033
+ const targetFile = join15(target, file);
2827
3034
  if (!ctx.baseline.has(`${ctx.prefix}/${file}`)) continue;
2828
3035
  try {
2829
- const current = readFileSync12(targetFile, "utf8");
3036
+ const current = readFileSync13(targetFile, "utf8");
2830
3037
  if (!isHarnessOwned2(ctx, file, current)) backupFile(targetFile, now);
2831
3038
  unlinkSync2(targetFile);
2832
3039
  removed.push(file);
@@ -2837,8 +3044,8 @@ function pruneOrphans(target, source, ext, ctx, now = /* @__PURE__ */ new Date()
2837
3044
  return removed;
2838
3045
  }
2839
3046
  function retireMcpAllowlist(projectDir, now = /* @__PURE__ */ new Date()) {
2840
- const target = join12(projectDir, ".mcp-allowlist");
2841
- if (!existsSync13(target)) return null;
3047
+ const target = join15(projectDir, ".mcp-allowlist");
3048
+ if (!existsSync14(target)) return null;
2842
3049
  try {
2843
3050
  const backup = backupFile(target, now);
2844
3051
  unlinkSync2(target);
@@ -2850,7 +3057,7 @@ function retireMcpAllowlist(projectDir, now = /* @__PURE__ */ new Date()) {
2850
3057
  function cleanStaleHookRefs(settingsPath, claudeDir) {
2851
3058
  let settings;
2852
3059
  try {
2853
- settings = JSON.parse(readFileSync12(settingsPath, "utf8"));
3060
+ settings = JSON.parse(readFileSync13(settingsPath, "utf8"));
2854
3061
  } catch {
2855
3062
  return [];
2856
3063
  }
@@ -2877,7 +3084,7 @@ function cleanStaleHookRefs(settingsPath, claudeDir) {
2877
3084
  function keepHookRef(hook, claudeDir, removed) {
2878
3085
  const relPath = projectAnchoredRef(hook?.command ?? "", claudeDir);
2879
3086
  if (relPath === void 0) return true;
2880
- const exists = existsSync13(join12(claudeDir, relPath));
3087
+ const exists = existsSync14(join15(claudeDir, relPath));
2881
3088
  if (!exists && !removed.includes(relPath)) removed.push(relPath);
2882
3089
  return exists;
2883
3090
  }
@@ -2899,31 +3106,47 @@ function projectAnchoredRef(command, claudeDir) {
2899
3106
  }
2900
3107
  return void 0;
2901
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
+ }
2902
3116
 
2903
3117
  // src/installer.ts
2904
3118
  function runInstall(ctx) {
2905
3119
  const { harnessRoot, projectDir, spec } = ctx;
2906
3120
  const mode = ctx.mode ?? "fresh";
2907
- const templatesDir = join13(harnessRoot, "templates");
2908
- if (!existsSync14(templatesDir)) {
3121
+ const templatesDir = join16(harnessRoot, "templates");
3122
+ if (!existsSync15(templatesDir)) {
2909
3123
  throw new Error(`Templates dir not found: ${templatesDir}`);
2910
3124
  }
2911
- const claudeDir = join13(projectDir, ".claude");
3125
+ const claudeDir = join16(projectDir, ".claude");
2912
3126
  const previousLog = readInstallLog(projectDir);
2913
- const claudeWasSelected = previousLog?.spec.cli.includes("claude") ?? false;
2914
- 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)) {
2915
3129
  throw new Error(
2916
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}`
2917
3131
  );
2918
3132
  }
2919
- const backupPath = resolveBackupPath(ctx, mode, claudeDir);
3133
+ const backupPath = resolveBackupPath(ctx, mode, claudeDir, previousLog);
2920
3134
  if (mode === "update") {
2921
3135
  return runUpdateInstall(ctx, templatesDir, backupPath);
2922
3136
  }
2923
3137
  const manifestSpec = buildManifestSpec(spec);
2924
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]));
2925
3140
  const baselineExcluded = new Set(spec.baselineExclude ?? []);
2926
- 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
+ ) : (
2927
3150
  // claude 미선택이어도 CLI 중립 자산은 깔린다. manifest 전체가 `.claude/` baseline 안에서만
2928
3151
  // 돌던 탓에 이 자산들이 claude 설치에만 도달했는데, **배포 룰 본문이 이 스크립트들을
2929
3152
  // 호출 지점으로 지목한다** — 즉 없는 도구를 있다고 안내하고 있었다(#300 과 같은 형태).
@@ -2955,6 +3178,17 @@ function runInstall(ctx) {
2955
3178
  previousExternal: previousLog?.externalFiles ?? [],
2956
3179
  codexTrust: (spec.scope ?? "project") === "global" && spec.options.withCodexTrust
2957
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
+ });
2958
3192
  const baseline = {
2959
3193
  filesCopied: base.filesCopied,
2960
3194
  dirsCopied: base.dirsCopied,
@@ -2971,21 +3205,23 @@ function runInstall(ctx) {
2971
3205
  rootClaudeMd: base.rootClaudeMd,
2972
3206
  // 외부 CLI 백업도 같은 줄에 노출한다 — 백업이 화면에 안 보이면 사용자는 자기 편집분이
2973
3207
  // 어디 갔는지 알 수 없고, 그러면 백업은 있어도 없는 것과 같다 (ADR-046/047 과 같은 이유).
2974
- backups: [...base.backups, ...externalBackups],
3208
+ backups: [...base.backups, ...externalBackups, ...linked.backupPaths],
2975
3209
  baselineExcluded: base.excluded,
2976
3210
  baselineExcludedOnDisk: base.excludedOnDisk,
2977
3211
  // `.claude/` baseline 과 외부 CLI 산출물의 같은 판정을 **한 목록으로** 낸다.
2978
3212
  baselineForeignOwned: [
2979
- ...base.foreignOwned,
2980
- ...externalForeignOwned.filter((f) => !base.foreignOwned.includes(f))
2981
- ]
3213
+ .../* @__PURE__ */ new Set([...base.foreignOwned, ...externalForeignOwned, ...linked.foreignOwned])
3214
+ ],
3215
+ baselineLinked: linked.updated,
3216
+ baselineLinkedNotOurs: linked.notOurs
2982
3217
  };
2983
3218
  ctx.onProgress?.({ type: "baseline-complete", baseline });
2984
3219
  const external = runExternalPhase(ctx);
2985
3220
  const staleHookRefs = healStaleHookRefs(spec, projectDir);
2986
3221
  writeInstallLogSafe(
2987
3222
  ctx,
2988
- externalFiles,
3223
+ // 링크 본문의 기준선도 같은 필드다 — 같은 경로면 뒤(이번에 쓴 값)가 이긴다.
3224
+ [...externalFiles, ...linked.files],
2989
3225
  external,
2990
3226
  base.rootClaudeMdLog,
2991
3227
  previousLog,
@@ -2996,12 +3232,13 @@ function runInstall(ctx) {
2996
3232
  }
2997
3233
  function healStaleHookRefs(spec, projectDir) {
2998
3234
  if (!spec.cli.includes("claude")) return [];
2999
- const settingsPath = join13(projectDir, ".claude/settings.json");
3000
- if (!existsSync14(settingsPath)) return [];
3001
- 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"));
3002
3238
  }
3003
- function resolveBackupPath(ctx, mode, claudeDir) {
3004
- 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);
3005
3242
  if (!wantBackup) return null;
3006
3243
  return mode === "update" ? copyBackupDir(claudeDir) : backupDir(claudeDir);
3007
3244
  }
@@ -3058,7 +3295,8 @@ function emptyClaudeBaseline() {
3058
3295
  backups: [],
3059
3296
  excluded: [],
3060
3297
  excludedOnDisk: [],
3061
- foreignOwned: []
3298
+ foreignOwned: [],
3299
+ linkedSkills: []
3062
3300
  };
3063
3301
  }
3064
3302
  function installCliNeutralAssets(manifestSpec, projectDir, templatesDir, baselineExcluded) {
@@ -3070,12 +3308,12 @@ function installCliNeutralAssets(manifestSpec, projectDir, templatesDir, baselin
3070
3308
  continue;
3071
3309
  }
3072
3310
  if (!isCliNeutralTarget(entry.target)) continue;
3073
- const source = join13(templatesDir, entry.source);
3074
- if (!existsSync14(source)) {
3311
+ const source = join16(templatesDir, entry.source);
3312
+ if (!existsSync15(source)) {
3075
3313
  result.skipped += 1;
3076
3314
  continue;
3077
3315
  }
3078
- copyFile(source, join13(projectDir, entry.target));
3316
+ copyFile(source, join16(projectDir, entry.target));
3079
3317
  result.filesCopied += 1;
3080
3318
  }
3081
3319
  return result;
@@ -3087,14 +3325,28 @@ function backupEditedPolicyFile(entryTarget, target, source, baseline) {
3087
3325
  if (!POLICY_DIRS.some(({ dir, ext }) => rel.startsWith(`${dir}/`) && rel.endsWith(ext))) {
3088
3326
  return null;
3089
3327
  }
3090
- if (!existsSync14(target)) return null;
3091
- const current = readFileSync13(target, "utf-8");
3092
- 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;
3093
3331
  const recorded = baseline.get(rel);
3094
3332
  if (recorded !== void 0 && recorded === hashContent(current)) return null;
3095
3333
  return backupFile(target);
3096
3334
  }
3097
- 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) {
3098
3350
  ensureProjectSkeleton(projectDir);
3099
3351
  const result = emptyClaudeBaseline();
3100
3352
  const manifest = buildManifest(manifestSpec);
@@ -3102,17 +3354,22 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
3102
3354
  if (!entry.applies(manifestSpec)) {
3103
3355
  continue;
3104
3356
  }
3105
- const target = join13(projectDir, entry.target);
3357
+ const target = join16(projectDir, entry.target);
3106
3358
  if (isBaselineExcluded(entry.target, baselineExcluded)) {
3107
3359
  result.excluded.push(entry.target);
3108
- if (existsSync14(target)) result.excludedOnDisk.push(entry.target);
3360
+ if (existsSync15(target)) result.excludedOnDisk.push(entry.target);
3109
3361
  continue;
3110
3362
  }
3111
- const source = join13(templatesDir, entry.source);
3112
- if (!existsSync14(source)) {
3363
+ const source = join16(templatesDir, entry.source);
3364
+ if (!existsSync15(source)) {
3113
3365
  result.skipped += 1;
3114
3366
  continue;
3115
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
+ }
3116
3373
  const foreign = foreignOwnedTarget(projectDir, entry.target);
3117
3374
  if (foreign !== null) {
3118
3375
  if (!result.foreignOwned.includes(foreign)) result.foreignOwned.push(foreign);
@@ -3120,7 +3377,7 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
3120
3377
  }
3121
3378
  if (entry.type === "file") {
3122
3379
  if (entry.target === ".claude/settings.json") {
3123
- const backup = backupFileIfChanged(target, readFileSync13(source, "utf-8"));
3380
+ const backup = backupFileIfChanged(target, readFileSync14(source, "utf-8"));
3124
3381
  if (backup) {
3125
3382
  result.backups.push(backup);
3126
3383
  }
@@ -3133,6 +3390,7 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
3133
3390
  copyFile(source, target);
3134
3391
  result.filesCopied += 1;
3135
3392
  } else {
3393
+ result.backups.push(...backupEditedSkillFiles(projectDir, entry.target, source, skillBase));
3136
3394
  for (const foreign2 of copyDir(
3137
3395
  source,
3138
3396
  target,
@@ -3144,24 +3402,29 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
3144
3402
  }
3145
3403
  accumulateCategory(result.categories, entry);
3146
3404
  }
3147
- const hookDir = join13(projectDir, ".claude/hooks");
3148
- if (existsSync14(hookDir)) {
3405
+ const hookDir = join16(projectDir, ".claude/hooks");
3406
+ if (existsSync15(hookDir)) {
3149
3407
  chmodHooksSync(hookDir);
3150
3408
  }
3151
3409
  writeInstalledTracks(projectDir, manifestSpec.tracks);
3152
3410
  const rootClaudeMd = writeRootClaudeMd(
3153
3411
  projectDir,
3154
3412
  manifestSpec.tracks,
3155
- manifestSpec.selectedInternalSkills ?? []
3413
+ manifestSpec.selectedInternalSkills ?? [],
3414
+ harnessRoot
3156
3415
  );
3157
- result.rootClaudeMd = { tracks: manifestSpec.tracks, created: rootClaudeMd.created };
3416
+ result.rootClaudeMd = {
3417
+ tracks: manifestSpec.tracks,
3418
+ created: rootClaudeMd.created,
3419
+ seededFrom: rootClaudeMd.seededFrom
3420
+ };
3158
3421
  result.rootClaudeMdLog = harnessAnchorLog(projectDir);
3159
3422
  return result;
3160
3423
  }
3161
3424
  function harnessAnchorLog(projectDir) {
3162
- const anchor = join13(projectDir, HARNESS_ANCHOR_FILE);
3163
- if (!existsSync14(anchor)) return null;
3164
- 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")) };
3165
3428
  }
3166
3429
  function writeEnvironmentFiles(projectDir, tracks) {
3167
3430
  return {
@@ -3218,8 +3481,9 @@ function writeInstallLogSafe(ctx, externalFiles, external, rootClaudeMdLog, prev
3218
3481
  claudeDirMovedAside,
3219
3482
  rootFiles
3220
3483
  );
3221
- const skillFiles = collectSkillHashes(ctx.projectDir, join13(ctx.harnessRoot, "templates"));
3222
- 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")) : [];
3223
3487
  const merged = mergeExternalFiles(ctx.projectDir, previousLog?.externalFiles, externalFiles);
3224
3488
  writeInstallLog(ctx.projectDir, {
3225
3489
  ...log,
@@ -3235,11 +3499,11 @@ function writeInstallLogSafe(ctx, externalFiles, external, rootClaudeMdLog, prev
3235
3499
  }
3236
3500
  }
3237
3501
  function composeAndWriteMcp(harnessRoot, projectDir, spec) {
3238
- const mcpPath = join13(projectDir, ".mcp.json");
3239
- const created = !existsSync14(mcpPath);
3502
+ const mcpPath = join16(projectDir, ".mcp.json");
3503
+ const created = !existsSync15(mcpPath);
3240
3504
  const composed = composeMcpJson({
3241
- templateMcpPath: join13(harnessRoot, "templates/mcp.json"),
3242
- trackMapPath: join13(harnessRoot, "templates/track-mcp-map.tsv"),
3505
+ templateMcpPath: join16(harnessRoot, "templates/mcp.json"),
3506
+ trackMapPath: join16(harnessRoot, "templates/track-mcp-map.tsv"),
3243
3507
  existingPath: mcpPath,
3244
3508
  tracks: spec.tracks
3245
3509
  });
@@ -3292,24 +3556,26 @@ function accumulateCategory(cats, entry) {
3292
3556
  }
3293
3557
  }
3294
3558
  function writeInstalledTracks(projectDir, tracks) {
3295
- const path = join13(projectDir, ".claude/.installed-tracks");
3559
+ const path = join16(projectDir, ".claude/.installed-tracks");
3296
3560
  mkdirSync4(dirname4(path), { recursive: true });
3297
3561
  const sorted = [...new Set(tracks)].sort().join("\n");
3298
3562
  writeFileSync6(path, `${sorted}
3299
3563
  `);
3300
3564
  }
3301
- function writeRootClaudeMd(projectDir, tracks, continuousSkills) {
3302
- const target = join13(projectDir, "CLAUDE.md");
3303
- 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;
3304
3569
  const content = upsertHarnessImport(existing, {
3305
3570
  projectName: basename5(projectDir),
3306
3571
  tracks,
3307
- continuousSkills
3572
+ continuousSkills,
3573
+ ...seeded === null ? {} : { projectContext: seeded }
3308
3574
  });
3309
3575
  if (content !== existing) {
3310
3576
  writeFileSync6(target, content);
3311
3577
  }
3312
- return { created: existing === null };
3578
+ return { created: existing === null, seededFrom: seeded === null ? null : "AGENTS.md" };
3313
3579
  }
3314
3580
  function chmodHooksSync(hookDir) {
3315
3581
  for (const file of listHookFiles(hookDir)) {
@@ -3325,8 +3591,8 @@ function listHookFiles(hookDir) {
3325
3591
 
3326
3592
  // src/commands/install-render.ts
3327
3593
  init_esm_shims();
3328
- import { existsSync as existsSync15, readdirSync as readdirSync5 } from "fs";
3329
- import { join as join14 } from "path";
3594
+ import { existsSync as existsSync16, readdirSync as readdirSync5 } from "fs";
3595
+ import { join as join17 } from "path";
3330
3596
 
3331
3597
  // src/preset-recommend.ts
3332
3598
  init_esm_shims();
@@ -3494,13 +3760,13 @@ function renderUpdateSummary(log, spec, report) {
3494
3760
  function residentEntriesOnDisk(spec) {
3495
3761
  const assetSpec = buildManifestSpec(spec);
3496
3762
  const planned = buildManifest(assetSpec).filter((e2) => e2.applies(assetSpec));
3497
- const agentsDir = join14(spec.projectDir, ".claude", "agents");
3498
- 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) => ({
3499
3765
  source: `agents/${e2.name}`,
3500
3766
  target: `.claude/agents/${e2.name}`,
3501
3767
  // 배포판이 아니라 **이 프로젝트의 파일**을 잰다 — 사용자가 고친 descriptor 도,
3502
3768
  // 배포판에 더는 없는 에이전트도 실제로 상주하는 것은 이쪽이다.
3503
- file: join14(agentsDir, e2.name)
3769
+ file: join17(agentsDir, e2.name)
3504
3770
  })) : [];
3505
3771
  return [...planned.filter((e2) => !e2.target.startsWith(".claude/agents/")), ...onDisk];
3506
3772
  }
@@ -3517,6 +3783,16 @@ function renderCliArtifacts(log, spec, report) {
3517
3783
  } else if (report.codex || report.opencode) {
3518
3784
  log(assetRow("success", "AGENTS.md", `from ${HARNESS_ANCHOR_FILE}`));
3519
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
+ }
3520
3796
  if (report.codex) {
3521
3797
  log(assetRow("success", ".codex/config.toml", "settings + [mcp_servers.*]"));
3522
3798
  log(assetRow("success", ".codex/hooks/", `${report.codex.hookFiles.length} files`));
@@ -3678,19 +3954,55 @@ function formatAssetMeta(asset, version) {
3678
3954
  return `internal \xB7 templates (${m.key})`;
3679
3955
  }
3680
3956
  }
3681
- function renderPhase1Rows(log, baseline, verbose = false) {
3682
- if (baseline.updateMode) {
3683
- if (baseline.backup) {
3684
- log(assetRow("success", "backup", shortenPath(baseline.backup)));
3685
- }
3686
- for (const [dir, count] of Object.entries(baseline.updateMode.updated)) {
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
+ }
3971
+ function renderPhase1Rows(log, baseline, verbose = false) {
3972
+ if (baseline.updateMode) {
3973
+ if (baseline.backup) {
3974
+ log(assetRow("success", "backup", shortenPath(baseline.backup)));
3975
+ }
3976
+ for (const [dir, count] of Object.entries(baseline.updateMode.updated)) {
3687
3977
  if (count > 0) log(assetRow("success", dir, `${count} files updated`));
3688
3978
  }
3689
3979
  for (const path of baseline.updateMode.installedNew) {
3690
3980
  log(assetRow("success", path, "added by this release"));
3691
3981
  }
3982
+ const withoutArgs = [];
3692
3983
  for (const path of baseline.updateMode.restored) {
3693
- 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
+ );
3694
4006
  }
3695
4007
  if (baseline.updateMode.needsReinstall.length > 0) {
3696
4008
  log(
@@ -3742,6 +4054,15 @@ function renderPhase1Rows(log, baseline, verbose = false) {
3742
4054
  )
3743
4055
  );
3744
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
+ }
3745
4066
  if (baseline.updateMode.legacyAnchor) {
3746
4067
  log(
3747
4068
  assetRow(
@@ -3769,6 +4090,11 @@ function renderPhase1Rows(log, baseline, verbose = false) {
3769
4090
  )
3770
4091
  );
3771
4092
  }
4093
+ linkedSkillRows(
4094
+ log,
4095
+ baseline.updateMode.skillsLinked ?? [],
4096
+ baseline.updateMode.skillsLinkedNotOurs ?? []
4097
+ );
3772
4098
  if (baseline.updateMode.skillsSkippedLinks.length > 0) {
3773
4099
  log(
3774
4100
  assetRow(
@@ -3975,6 +4301,7 @@ function renderPhase1Rows(log, baseline, verbose = false) {
3975
4301
  )
3976
4302
  );
3977
4303
  }
4304
+ linkedSkillRows(log, baseline.baselineLinked ?? [], baseline.baselineLinkedNotOurs ?? []);
3978
4305
  if (baseline.baselineForeignOwned.length > 0) {
3979
4306
  log(
3980
4307
  assetRow(
@@ -3991,7 +4318,7 @@ function renderPhase1Rows(log, baseline, verbose = false) {
3991
4318
  assetRow(
3992
4319
  "success",
3993
4320
  "CLAUDE.md (root)",
3994
- 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),
3995
4322
  TEMPLATES_COL
3996
4323
  )
3997
4324
  );
@@ -4058,6 +4385,14 @@ function shortenPath(p2) {
4058
4385
  }
4059
4386
  return p2;
4060
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
+ }
4061
4396
  function formatCliPhaseTitle(targets) {
4062
4397
  const labels = [];
4063
4398
  if (targets.includes("codex")) labels.push("Codex");
@@ -4119,6 +4454,18 @@ function installAction(options, deps = {}) {
4119
4454
  exit(1);
4120
4455
  return;
4121
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) {
4122
4469
  const forceInclude = normalizeRepeatable(options.with);
4123
4470
  const forceExclude = normalizeRepeatable(options.without);
4124
4471
  const baselineIds = new Set(
@@ -4145,7 +4492,7 @@ function installAction(options, deps = {}) {
4145
4492
  const filteredInclude = forceInclude.filter((id) => validIds.has(id));
4146
4493
  const filteredExclude = forceExclude.filter((id) => validIds.has(id));
4147
4494
  const userOverride = filteredInclude.length > 0 || filteredExclude.length > 0 ? { forceInclude: filteredInclude, forceExclude: filteredExclude } : void 0;
4148
- const spec = {
4495
+ return {
4149
4496
  tracks: options.track ?? [],
4150
4497
  ...userOverride ? { userOverride } : {},
4151
4498
  ...baselineExclude.length > 0 ? { baselineExclude } : {},
@@ -4154,18 +4501,21 @@ function installAction(options, deps = {}) {
4154
4501
  options: {
4155
4502
  withCodexTrust: options.withCodexTrust === true
4156
4503
  },
4157
- cli: validated.cli,
4504
+ cli: cli2,
4158
4505
  projectDir: resolve2(options.projectDir ?? process.cwd()),
4159
4506
  scope: resolveScopeOption(options.scope, err)
4160
4507
  };
4161
- executeSpec(spec, {
4162
- log,
4163
- err,
4164
- exit,
4165
- runPipeline,
4166
- resolveHarnessRoot,
4167
- verbose: options.verbose === true
4168
- });
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(" ");
4169
4519
  }
4170
4520
  function executeSpec(spec, deps = {}) {
4171
4521
  const log = deps.log ?? console.log;
@@ -4247,13 +4597,16 @@ function registerInstallCommand(cli2) {
4247
4597
  ).option(
4248
4598
  "--with-codex-trust",
4249
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"
4250
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));
4251
4604
  }
4252
4605
 
4253
4606
  // src/commands/list.ts
4254
4607
  init_esm_shims();
4255
- import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
4256
- 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";
4257
4610
  function listAction(options = {}, deps = {}) {
4258
4611
  const log = deps.log ?? console.log;
4259
4612
  const err = deps.err ?? console.error;
@@ -4272,7 +4625,7 @@ function listAction(options = {}, deps = {}) {
4272
4625
  log(c.dim(` installed: ${installLog.installedAt}`));
4273
4626
  log(c.dim(` scope: ${installLog.scope}`));
4274
4627
  log(c.dim(` tracks: ${installLog.spec.tracks.join(", ") || "(none)"}`));
4275
- log(c.dim(` cli: ${installLog.spec.cli.join(", ") || "(none)"}`));
4628
+ log(c.dim(` cli: ${installedClis(installLog).join(", ") || "(none)"}`));
4276
4629
  log("");
4277
4630
  log(c.bold(` Assets (${installLog.assets.length})`));
4278
4631
  if (installLog.assets.length === 0) {
@@ -4316,8 +4669,8 @@ function formatTemplateRows(log, projectDir) {
4316
4669
  const rows = [` ${c.dim(dirs.join(" "))}`];
4317
4670
  const rootMd = log.templates.rootClaudeMd;
4318
4671
  if (rootMd) {
4319
- const path = join15(projectDir, rootMd.path);
4320
- 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;
4321
4674
  rows.push(
4322
4675
  modified ? ` ${c.dim(rootMd.path)} ${c.yellow("(\uC218\uC815\uB428 \u2014 uninstall \uC2DC \uBCF4\uC874)")}` : ` ${c.dim(rootMd.path)}`
4323
4676
  );
@@ -4325,7 +4678,7 @@ function formatTemplateRows(log, projectDir) {
4325
4678
  return rows;
4326
4679
  }
4327
4680
  function formatRootFileRows(rootFiles, projectDir) {
4328
- const present = rootFiles.filter((f) => existsSync16(join15(projectDir, f.path)));
4681
+ const present = rootFiles.filter((f) => existsSync17(join18(projectDir, f.path)));
4329
4682
  const width = Math.max(0, ...present.map((f) => f.path.length));
4330
4683
  return present.map(
4331
4684
  (f) => ` ${c.dim(padDisplay(f.path, width))} ${c.dim(f.change === "created" ? "\uC0DD\uC131" : "\uBCD1\uD569")} ${c.dim(f.notes.join(" / "))}`
@@ -4343,15 +4696,64 @@ function registerListCommand(cli2) {
4343
4696
  init_esm_shims();
4344
4697
  import { spawnSync as spawnSync2 } from "child_process";
4345
4698
  import {
4346
- existsSync as existsSync17,
4699
+ existsSync as existsSync18,
4347
4700
  lstatSync as lstatSync2,
4348
4701
  readdirSync as readdirSync6,
4349
- readFileSync as readFileSync15,
4702
+ readFileSync as readFileSync16,
4350
4703
  rmdirSync as rmdirSync2,
4351
4704
  rmSync,
4352
4705
  writeFileSync as writeFileSync7
4353
4706
  } from "fs";
4354
- 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
+ }
4355
4757
 
4356
4758
  // src/uninstall-interactive.ts
4357
4759
  init_esm_shims();
@@ -5326,109 +5728,605 @@ ${a}
5326
5728
  };
5327
5729
  var jt = `${e("gray", $)} `;
5328
5730
 
5329
- // src/uninstall-interactive.ts
5330
- function buildRemovableRows(assets) {
5331
- return assets.map((a) => ({
5332
- value: a.id,
5333
- label: `${a.id} [${a.method}]${a.version ? ` v${a.version}` : ""}`,
5334
- hint: hintFor(a)
5335
- }));
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
+ };
5336
5746
  }
5337
- function hintFor(asset) {
5338
- 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";
5339
- switch (asset.method) {
5340
- case "plugin":
5341
- return "claude plugin uninstall --scope project";
5342
- case "skill":
5343
- return "npx skills remove";
5344
- case "npm":
5345
- return "npm uninstall --save-dev";
5346
- case "npx-run":
5347
- // shell-script = #492 에서 은퇴한 legacy kind. 옛 로그가 그대로 들어온다.
5348
- case "shell-script":
5349
- case "internal":
5350
- 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";
5351
- }
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");
5352
5764
  }
5353
- async function runInteractiveUninstall(projectDir, deps = {}) {
5354
- const prompts = deps.prompts ?? defaultUninstallPrompts();
5355
- const isTty = deps.isTty ?? (() => Boolean(process.stdin.isTTY));
5356
- const readLog = deps.readLog ?? readInstallLog;
5357
- if (!isTty()) return { ok: false, reason: "no-tty" };
5358
- const log = readLog(projectDir);
5359
- if (!log) return { ok: false, reason: "no-log" };
5360
- prompts.intro("uzys-agent-harness \xB7 uninstall");
5361
- const rows = buildRemovableRows(log.assets);
5362
- const mode = await prompts.selectMode(rows.length);
5363
- if (mode === null) {
5364
- prompts.cancel("Cancelled.");
5365
- 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);
5366
5877
  }
5367
- if (mode === "all") {
5368
- const ok2 = await prompts.confirm(
5369
- [
5370
- "\uC804\uB7C9 \uC81C\uAC70 \u2014 \uB418\uB3CC\uB9B4 \uC218 \uC5C6\uB2E4:",
5371
- ` \xB7 \uC790\uC0B0 ${log.assets.length}\uAC1C (\uC790\uB3D9 \uACBD\uB85C\uAC00 \uC788\uB294 \uAC83\uB9CC \uC2E4\uC81C \uC81C\uAC70)`,
5372
- " \xB7 templates \uC0AD\uC81C: `.claude/` \uB4F1 (\uC124\uCE58 \uAE30\uB85D\uB3C4 \uD568\uAED8 \uC0AC\uB77C\uC9C4\uB2E4)",
5373
- " \xB7 `.claude/` \uBC16 \uD30C\uC77C(`.mcp.json` \uB4F1)\uC740 \uC0AD\uC81C\uD558\uC9C0 \uC54A\uACE0 \uC548\uB0B4\uB9CC \uD55C\uB2E4"
5374
- ].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)]
5375
5891
  );
5376
- if (!ok2) {
5377
- prompts.cancel("Cancelled.");
5378
- 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
+ });
5379
5900
  }
5380
- return { ok: true, options: { projectDir } };
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);
5381
5906
  }
5382
- const picked = await prompts.selectAssets(rows);
5383
- if (picked === null) {
5384
- prompts.cancel("Cancelled.");
5385
- return { ok: false, reason: "cancelled" };
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);
5386
5913
  }
5387
- if (picked.length === 0) {
5388
- prompts.outro("\uC544\uBB34\uAC83\uB3C4 \uC120\uD0DD\uD558\uC9C0 \uC54A\uC558\uB2E4 \u2014 \uBCC0\uACBD \uC5C6\uC74C.");
5389
- return { ok: false, reason: "nothing-selected" };
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
+ );
5390
5920
  }
5391
- const ok = await prompts.confirm(
5392
- [
5393
- `\uC120\uD0DD \uC81C\uAC70 (${picked.length}\uAC1C):`,
5394
- ...picked.map((id) => ` \xB7 ${id}`),
5395
- "",
5396
- "templates \uB294 \uADF8\uB300\uB85C \uB454\uB2E4."
5397
- ].join("\n")
5398
- );
5399
- if (!ok) {
5921
+ }
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) {
5400
6279
  prompts.cancel("Cancelled.");
5401
6280
  return { ok: false, reason: "cancelled" };
5402
6281
  }
5403
6282
  return { ok: true, options: { projectDir, only: picked.join(",") } };
5404
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
+ }
5405
6302
  function defaultUninstallPrompts() {
5406
6303
  return {
5407
6304
  intro: (m) => ge(m),
5408
6305
  outro: (m) => ye(m),
5409
6306
  cancel: (m) => me(m),
5410
- selectMode: async (rowCount) => {
6307
+ selectMode: async (choices, message) => {
5411
6308
  const r = await Ee({
5412
- message: `\uBB34\uC5C7\uC744 \uC81C\uAC70\uD560\uAE4C? (\uC124\uCE58\uB41C \uC790\uC0B0 ${rowCount}\uAC1C)`,
5413
- options: [
5414
- {
5415
- value: "selected",
5416
- label: "\uD56D\uBAA9 \uC120\uD0DD\uD574\uC11C \uC81C\uAC70",
5417
- hint: "templates(`.claude/` \uB4F1)\uB294 \uADF8\uB300\uB85C \uB454\uB2E4"
5418
- },
5419
- {
5420
- value: "all",
5421
- label: "\uC804\uBD80 \uC81C\uAC70",
5422
- hint: "\uC790\uC0B0 + templates. \uC124\uCE58 \uAE30\uB85D\uB3C4 \uC0AC\uB77C\uC9C4\uB2E4"
5423
- }
5424
- ]
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 }))
5425
6323
  });
5426
6324
  return q(r) ? null : r;
5427
6325
  },
5428
6326
  selectAssets: async (rows) => {
5429
6327
  if (rows.length === 0) return [];
5430
6328
  const r = await ve({
5431
- 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)",
5432
6330
  options: rows.map((x) => ({ value: x.value, label: x.label, hint: x.hint })),
5433
6331
  required: false
5434
6332
  });
@@ -5437,13 +6335,25 @@ function defaultUninstallPrompts() {
5437
6335
  confirm: async (summary) => {
5438
6336
  const r = await ue({ message: `${summary}
5439
6337
 
5440
- \uC9C4\uD589\uD560\uAE4C?`, initialValue: false });
6338
+ Proceed?`, initialValue: false });
5441
6339
  return q(r) ? null : r;
5442
6340
  }
5443
6341
  };
5444
6342
  }
5445
6343
 
5446
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
+ }
5447
6357
  function uninstallAction(options, deps = {}) {
5448
6358
  const log = deps.log ?? console.log;
5449
6359
  const err = deps.err ?? console.error;
@@ -5451,6 +6361,7 @@ function uninstallAction(options, deps = {}) {
5451
6361
  const spawn = deps.spawn ?? defaultSpawn2;
5452
6362
  const rm = deps.rm ?? defaultRm;
5453
6363
  const writeLog = deps.writeLog ?? writeInstallLog;
6364
+ const moveAside = deps.moveAside ?? backupDir;
5454
6365
  const harnessRoot = (deps.resolveHarnessRoot ?? defaultHarnessRoot)();
5455
6366
  const projectDir = resolve4(options.projectDir ?? process.cwd());
5456
6367
  const installLog = readInstallLog(projectDir);
@@ -5460,6 +6371,13 @@ function uninstallAction(options, deps = {}) {
5460
6371
  exit(1);
5461
6372
  return;
5462
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
+ }
5463
6381
  const selectedIds = parseOnly(options.only);
5464
6382
  if (options.only !== void 0 && selectedIds === null) {
5465
6383
  err(status.failure(c.red("ERROR: --only \uC5D0 \uC790\uC0B0 id \uAC00 \uC5C6\uB2E4")));
@@ -5496,13 +6414,16 @@ function uninstallAction(options, deps = {}) {
5496
6414
  const logSurvives = selectedIds !== null;
5497
6415
  const { succeeded, failed, removedIds } = executeReverse(plan, log, logSurvives);
5498
6416
  if (!keepTemplates) {
5499
- const { rootClaudeMdKept, importStripped, external } = removeTemplates(
6417
+ const { rootClaudeMdKept, importStripped, external, moved } = removeTemplates(
5500
6418
  installLog,
5501
6419
  projectDir,
5502
- rm,
6420
+ { rm, moveAside },
5503
6421
  harnessRoot
5504
6422
  );
5505
- 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
+ }
5506
6427
  for (const line of externalRemovalLines(external)) log(line);
5507
6428
  if (importStripped) {
5508
6429
  log(` ${status.success("CLAUDE.md \u2014 harness @import removed (\uBCF8\uBB38 \uBCF4\uC874)")}`);
@@ -5601,7 +6522,7 @@ function settleLog(ctx, io) {
5601
6522
  return true;
5602
6523
  }
5603
6524
  } else {
5604
- io.rm(join16(projectDir, INSTALL_LOG_DIR));
6525
+ io.rm(join19(projectDir, INSTALL_LOG_DIR));
5605
6526
  io.rm(legacyInstallLogPath(projectDir));
5606
6527
  io.log(` ${status.success("install log removed")}`);
5607
6528
  }
@@ -5619,7 +6540,11 @@ function dryRunLines(plan, installLog, projectDir, keepTemplates, rootFiles, har
5619
6540
  )
5620
6541
  );
5621
6542
  if (!keepTemplates) {
5622
- lines.push(` \u25CB remove templates: ${formatTemplateList(installLog)}`);
6543
+ const dirs = recordedTemplateDirs(installLog);
6544
+ for (const rel of dirs) {
6545
+ if (existsSync18(join19(projectDir, rel))) lines.push(MOVE_ASIDE_PREVIEW(rel));
6546
+ }
6547
+ if (dirs.length === 0) lines.push(" \u25CB remove templates: (none)");
5623
6548
  const rootMd = installLog.templates.rootClaudeMd;
5624
6549
  if (rootMd) {
5625
6550
  lines.push(
@@ -5629,7 +6554,7 @@ function dryRunLines(plan, installLog, projectDir, keepTemplates, rootFiles, har
5629
6554
  if (hasRootImport(projectDir)) {
5630
6555
  lines.push(" \u25CB strip harness @import from CLAUDE.md (\uBCF8\uBB38 \uBCF4\uC874)");
5631
6556
  }
5632
- lines.push(...previewExternalLines(installLog, projectDir, harnessRoot));
6557
+ lines.push(...previewExternalLines(installLog, projectDir, harnessRoot, dirs));
5633
6558
  }
5634
6559
  lines.push(...advisoryLines(plan, projectDir, rootFiles), "");
5635
6560
  return lines;
@@ -5651,7 +6576,7 @@ function advisoryLines(plan, projectDir, rootFiles) {
5651
6576
  return lines;
5652
6577
  }
5653
6578
  function rootFileAdvisoryLines(rootFiles, projectDir) {
5654
- const present = rootFiles.filter((f) => existsSync17(join16(projectDir, f.path)));
6579
+ const present = rootFiles.filter((f) => existsSync18(join19(projectDir, f.path)));
5655
6580
  if (present.length === 0) return [];
5656
6581
  return [
5657
6582
  "",
@@ -5664,6 +6589,172 @@ function rootFileAdvisoryLines(rootFiles, projectDir) {
5664
6589
  ])
5665
6590
  ];
5666
6591
  }
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);
6607
+ return null;
6608
+ }
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;
6615
+ }
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;
6622
+ }
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;
6628
+ }
6629
+ return target;
6630
+ }
6631
+ function underAny(path, owned) {
6632
+ return owned.some((o) => o.endsWith("/") ? path.startsWith(o) : path === o);
6633
+ }
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
+ };
6647
+ }
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
+ );
6671
+ }
6672
+ if (plan.importBlock && hasRootImport(projectDir)) {
6673
+ lines.push(" \u25CB strip harness @import from CLAUDE.md (\uBCF8\uBB38 \uBCF4\uC874)");
6674
+ }
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;
6687
+ }
6688
+ io.rm(join19(projectDir, rootMd.path));
6689
+ io.log(` ${status.success(`${rootMd.path} removed`)}`);
6690
+ return true;
6691
+ }
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);
6726
+ return;
6727
+ }
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);
6752
+ return;
6753
+ }
6754
+ io.log("");
6755
+ io.log(status.success(c.green(`${target} removed (remaining: ${remaining.join(", ")})`)));
6756
+ io.exit(0);
6757
+ }
5667
6758
  function planReverse(assets, spawn) {
5668
6759
  const reverseSteps = [];
5669
6760
  const globalAdvisories = [];
@@ -5703,752 +6794,381 @@ function buildProjectReverseStep(asset, spawn) {
5703
6794
  }
5704
6795
  };
5705
6796
  }
5706
- case "npm": {
5707
- const pkg = asset.detail.pkg ?? asset.id;
5708
- return {
5709
- assetId: asset.id,
5710
- label: `npm uninstall --save-dev ${pkg}`,
5711
- execute: () => {
5712
- const r = spawn("npm", ["uninstall", "--save-dev", pkg]);
5713
- return r.status === 0 ? { ok: true } : { ok: false, message: (r.stderr || "").trim() };
5714
- }
5715
- };
5716
- }
5717
- case "npx-run":
5718
- return null;
5719
- case "shell-script":
5720
- return null;
5721
- case "internal":
5722
- return null;
5723
- }
5724
- }
5725
- function unknownIds(installLog, selectedIds) {
5726
- const known = new Set(installLog.assets.map((a) => a.id));
5727
- return selectedIds.filter((id) => !known.has(id));
5728
- }
5729
- function parseOnly(only) {
5730
- if (!only) return null;
5731
- const ids = only.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
5732
- return ids.length > 0 ? ids : null;
5733
- }
5734
- function buildGlobalAdvisoryCmd(asset) {
5735
- switch (asset.method) {
5736
- case "plugin": {
5737
- const pid = asset.detail.pluginId ?? asset.id;
5738
- return `claude plugin uninstall --scope user ${pid}`;
5739
- }
5740
- case "skill": {
5741
- const s = asset.detail.source ?? asset.id;
5742
- return `npx skills remove -g ${s}`;
5743
- }
5744
- case "npm": {
5745
- const pkg = asset.detail.pkg ?? asset.id;
5746
- return `npm uninstall -g ${pkg}`;
5747
- }
5748
- case "npx-run":
5749
- case "shell-script":
5750
- case "internal":
5751
- return "(no standard reverse \u2014 manual)";
5752
- }
5753
- }
5754
- function removeTemplates(log, projectDir, rm, harnessRoot) {
5755
- rm(join16(projectDir, log.templates.claudeDir));
5756
- if (log.templates.codexDir) rm(join16(projectDir, log.templates.codexDir));
5757
- if (log.templates.opencodeDir) rm(join16(projectDir, log.templates.opencodeDir));
5758
- const external = removeExternalFiles(log, projectDir, rm, harnessRoot);
5759
- const importStripped = stripRootImport(projectDir);
5760
- const rootMd = log.templates.rootClaudeMd;
5761
- if (rootMd) {
5762
- if (rootClaudeMdModified(log, projectDir))
5763
- return { rootClaudeMdKept: true, importStripped, external };
5764
- rm(join16(projectDir, rootMd.path));
5765
- }
5766
- return { rootClaudeMdKept: false, importStripped, external };
5767
- }
5768
- var AGENTS_MD = "AGENTS.md";
5769
- function readAgentsMdTemplate(harnessRoot, log) {
5770
- const flavor = log.templates.opencodeDir ? "opencode" : "codex";
5771
- try {
5772
- return readFileSync15(join16(harnessRoot, "templates", flavor, "AGENTS.md.template"), "utf8");
5773
- } catch {
5774
- return null;
5775
- }
5776
- }
5777
- function agentsMdRemainder(current, harnessRoot, log) {
5778
- const template = readAgentsMdTemplate(harnessRoot, log);
5779
- if (template === null) return { ok: false };
5780
- return { ok: true, remainder: stripHarnessFromAgentsMd({ existing: current, template }) };
5781
- }
5782
- function removeExternalFiles(log, projectDir, rm, harnessRoot) {
5783
- const removed = [];
5784
- const kept = [];
5785
- const stripped = [];
5786
- const unjudged = [];
5787
- for (const { path, sha256 } of log.externalFiles ?? []) {
5788
- const abs = join16(projectDir, path);
5789
- if (!existsSync17(abs)) continue;
5790
- if (!lstatSync2(abs).isFile()) continue;
5791
- const current = readFileSync15(abs, "utf8");
5792
- if (hashContent(current) !== sha256) {
5793
- kept.push(path);
5794
- continue;
5795
- }
5796
- if (path === AGENTS_MD) {
5797
- const verdict = agentsMdRemainder(current, harnessRoot, log);
5798
- if (!verdict.ok) {
5799
- unjudged.push(path);
5800
- continue;
5801
- }
5802
- if (verdict.remainder !== null) {
5803
- try {
5804
- writeFileSync7(abs, verdict.remainder, "utf8");
5805
- stripped.push(path);
5806
- } catch {
5807
- unjudged.push(path);
5808
- }
5809
- continue;
5810
- }
5811
- }
5812
- rm(abs);
5813
- removed.push(path);
5814
- }
5815
- for (const path of removed) pruneEmptyDirsUpward(projectDir, dirname5(join16(projectDir, path)));
5816
- return { removed, kept, stripped, unjudged };
5817
- }
5818
- function pruneEmptyDirsUpward(projectDir, startDir) {
5819
- let cursor = startDir;
5820
- while (true) {
5821
- const rel = relative2(projectDir, cursor);
5822
- if (rel === "" || rel.startsWith(`..${sep2}`) || rel === "..") return;
5823
- try {
5824
- if (readdirSync6(cursor).length > 0) return;
5825
- rmdirSync2(cursor);
5826
- } catch {
5827
- return;
5828
- }
5829
- cursor = dirname5(cursor);
5830
- }
5831
- }
5832
- function stripRootImport(projectDir) {
5833
- const current = readRootClaudeMd(projectDir);
5834
- const stripped = current === null ? null : stripHarnessImport(current);
5835
- if (stripped === null) return false;
5836
- try {
5837
- writeFileSync7(join16(projectDir, "CLAUDE.md"), stripped, "utf8");
5838
- return true;
5839
- } catch {
5840
- return false;
5841
- }
5842
- }
5843
- function previewExternalLines(installLog, projectDir, harnessRoot) {
5844
- const lines = [];
5845
- let removable = 0;
5846
- for (const { path, sha256 } of installLog.externalFiles ?? []) {
5847
- const abs = join16(projectDir, path);
5848
- if (!existsSync17(abs)) continue;
5849
- const current = readFileSync15(abs, "utf8");
5850
- if (hashContent(current) !== sha256) {
5851
- lines.push(` \u25CB keep ${path} (modified since install \u2014 preserved)`);
5852
- continue;
5853
- }
5854
- if (path === AGENTS_MD) {
5855
- const verdict = agentsMdRemainder(current, harnessRoot, installLog);
5856
- if (!verdict.ok) {
5857
- lines.push(` \u25CB keep ${path} (harness sections could not be separated \u2014 preserved)`);
5858
- continue;
5859
- }
5860
- if (verdict.remainder !== null) {
5861
- lines.push(` \u25CB strip harness sections from ${path} (\uBCF8\uBB38 \uBCF4\uC874)`);
5862
- continue;
5863
- }
5864
- }
5865
- removable += 1;
5866
- }
5867
- if (removable > 0) lines.unshift(` \u25CB remove ${removable} CLI output file(s)`);
5868
- return lines;
5869
- }
5870
- function externalRemovalLines(external) {
5871
- const lines = [];
5872
- if (external.removed.length > 0) {
5873
- lines.push(` ${status.success(`CLI outputs removed: ${external.removed.length} file(s)`)}`);
5874
- }
5875
- for (const path of external.stripped) {
5876
- lines.push(` ${status.success(`${path} \u2014 harness sections removed (\uBCF8\uBB38 \uBCF4\uC874)`)}`);
5877
- }
5878
- for (const path of external.unjudged) {
5879
- lines.push(
5880
- ` ${c.yellow("\u2298")} ${path} kept \u2014 harness sections could not be separated (template unreadable or write failed). Remove manually if intended.`
5881
- );
5882
- }
5883
- for (const path of external.kept) {
5884
- lines.push(
5885
- ` ${c.yellow("\u2298")} ${path} kept \u2014 modified since install. Remove manually if intended.`
5886
- );
5887
- }
5888
- return lines;
5889
- }
5890
- function hasRootImport(projectDir) {
5891
- const current = readRootClaudeMd(projectDir);
5892
- return current !== null && stripHarnessImport(current) !== null;
5893
- }
5894
- function readRootClaudeMd(projectDir) {
5895
- const path = join16(projectDir, "CLAUDE.md");
5896
- if (!existsSync17(path)) return null;
5897
- try {
5898
- return readFileSync15(path, "utf8");
5899
- } catch {
5900
- return null;
5901
- }
5902
- }
5903
- function rootClaudeMdModified(log, projectDir) {
5904
- const rootMd = log.templates.rootClaudeMd;
5905
- if (!rootMd) return false;
5906
- const path = join16(projectDir, rootMd.path);
5907
- if (!existsSync17(path)) return false;
5908
- return hashContent(readFileSync15(path, "utf8")) !== rootMd.sha256;
5909
- }
5910
- function formatTemplateList(log) {
5911
- const items = [log.templates.claudeDir];
5912
- if (log.templates.codexDir) items.push(log.templates.codexDir);
5913
- if (log.templates.opencodeDir) items.push(log.templates.opencodeDir);
5914
- return items.join(", ");
5915
- }
5916
- function defaultSpawn2(cmd, args) {
5917
- return spawnSync2(cmd, [...args], { encoding: "utf8", stdio: "pipe", timeout: 12e4 });
5918
- }
5919
- function defaultRm(path) {
5920
- if (existsSync17(path)) {
5921
- rmSync(path, { recursive: true, force: true });
5922
- }
5923
- }
5924
- function registerUninstallCommand(cli2) {
5925
- cli2.command("uninstall", "Uninstall harness assets (log-based reverse)").option("--project-dir <path>", "[Project] Target project directory", {
5926
- default: process.cwd()
5927
- }).option("--dry-run", "[Mode] List reverse steps without executing").option(
5928
- "--keep-templates",
5929
- "[Mode] Keep `.claude/`, `.codex/`, `.opencode/` templates (remove only external assets)"
5930
- ).option(
5931
- "--only <ids>",
5932
- "[Scope] Remove only these assets (comma-separated ids from `agent-harness list`). Templates untouched"
5933
- ).option("--yes", "[Mode] Skip the interactive picker and remove everything (non-interactive)").action(async (options) => {
5934
- await dispatchUninstall(options);
5935
- });
5936
- }
5937
- function shouldRunInteractive(options, isTty) {
5938
- if (!isTty) return false;
5939
- if (options.yes || options.dryRun) return false;
5940
- return options.only === void 0;
5941
- }
5942
- async function dispatchUninstall(options) {
5943
- if (!shouldRunInteractive(options, Boolean(process.stdin.isTTY))) {
5944
- uninstallAction(options);
5945
- return;
5946
- }
5947
- const projectDir = resolve4(options.projectDir ?? process.cwd());
5948
- const picked = await runInteractiveUninstall(projectDir);
5949
- if (!picked.ok || !picked.options) {
5950
- if (picked.reason === "no-log") {
5951
- console.error(
5952
- status.failure(c.red(`ERROR: install log not found at ${installLogPath(projectDir)}`))
5953
- );
5954
- console.error(c.dim(" Nothing installed here by agent-harness."));
5955
- process.exit(1);
5956
- }
5957
- return;
5958
- }
5959
- uninstallAction(picked.options);
5960
- }
5961
-
5962
- // src/commands/update.ts
5963
- init_esm_shims();
5964
- import { resolve as resolve5 } from "path";
5965
-
5966
- // src/state.ts
5967
- init_esm_shims();
5968
- import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
5969
- import { join as join17 } from "path";
5970
- var META_FILE = ".claude/.installed-tracks";
5971
- var LEGACY_SIGNATURES = [
5972
- { rule: "htmx.md", track: "ssr-htmx" },
5973
- { rule: "nextjs.md", track: "ssr-nextjs" },
5974
- { rule: "data-analysis.md", track: "data" },
5975
- { rule: "pyside6.md", track: "data" },
5976
- { rule: "cli-development.md", track: "tooling" }
5977
- ];
5978
- function detectInstallState(projectDir) {
5979
- const claudeDir = join17(projectDir, ".claude");
5980
- const hasClaudeDir = existsSync18(claudeDir);
5981
- if (hasClaudeDir) {
5982
- const metaPath = join17(projectDir, META_FILE);
5983
- if (existsSync18(metaPath)) {
5984
- const tracks2 = readMetafile(metaPath);
5985
- return { state: "existing", tracks: tracks2, source: "metafile", hasClaudeDir: true };
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
+ };
5986
6807
  }
5987
- const tracks = inferFromLegacySignatures(projectDir);
5988
- return { state: "existing", tracks, source: "legacy", hasClaudeDir: true };
5989
- }
5990
- const log = readInstallLog(projectDir);
5991
- if (log) {
5992
- return {
5993
- state: "existing",
5994
- tracks: tracksFromLog(log.spec.tracks),
5995
- source: "install-log",
5996
- hasClaudeDir: false
5997
- };
6808
+ case "npx-run":
6809
+ return null;
6810
+ case "shell-script":
6811
+ return null;
6812
+ case "internal":
6813
+ return null;
5998
6814
  }
5999
- return { state: "new", tracks: [], source: "none", hasClaudeDir: false };
6000
6815
  }
6001
- function tracksFromLog(tracks) {
6002
- return [...new Set(tracks.filter(isTrack))].sort();
6816
+ function unknownIds(installLog, selectedIds) {
6817
+ const known = new Set(installLog.assets.map((a) => a.id));
6818
+ return selectedIds.filter((id) => !known.has(id));
6003
6819
  }
6004
- function readMetafile(path) {
6005
- const raw = readFileSync16(path, "utf8");
6006
- const seen = /* @__PURE__ */ new Set();
6007
- for (const line of raw.split(/\s+/)) {
6008
- const trimmed = line.trim();
6009
- if (isTrack(trimmed)) {
6010
- seen.add(trimmed);
6011
- }
6012
- }
6013
- return [...seen].sort();
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;
6014
6824
  }
6015
- function inferFromLegacySignatures(projectDir) {
6016
- const rulesDir = join17(projectDir, ".claude/rules");
6017
- if (!existsSync18(rulesDir)) {
6018
- return [];
6019
- }
6020
- const found = /* @__PURE__ */ new Set();
6021
- for (const sig of LEGACY_SIGNATURES) {
6022
- if (existsSync18(join17(rulesDir, sig.rule))) {
6023
- found.add(sig.track);
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}`;
6024
6830
  }
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)";
6025
6843
  }
6026
- return [...found].sort();
6027
- }
6028
-
6029
- // src/commands/update.ts
6030
- function updateAction(options = {}, deps = {}) {
6031
- const log = deps.log ?? console.log;
6032
- const err = deps.err ?? console.error;
6033
- const exit = deps.exit ?? ((code) => process.exit(code));
6034
- const detect = deps.detect ?? detectInstallState;
6035
- const execute = deps.execute ?? executeSpec;
6036
- const projectDir = resolve5(options.projectDir ?? process.cwd());
6037
- const state = detect(projectDir);
6038
- if (state.state === "new") {
6039
- err(status.failure(c.red(`No harness install found at ${projectDir}`)));
6040
- err(c.dim(" Run `agent-harness install --track <name>` first."));
6041
- exit(1);
6042
- return;
6043
- }
6044
- const rawOnly = options.only === void 0 ? [] : [options.only].flat().map((v2) => typeof v2 === "string" ? v2 : "(missing value)");
6045
- const parsed = parseUpdateOnly(rawOnly);
6046
- if (!parsed.ok) {
6047
- err(status.failure(c.red(`Unknown --only value: ${parsed.invalid.join(", ")}`)));
6048
- err(c.dim(` Valid groups: ${UPDATE_GROUPS.join(" | ")}`));
6049
- exit(1);
6050
- return;
6051
- }
6052
- log(c.dim(`Updating installed harness files in ${projectDir}`));
6053
- execute(buildUpdateSpec(projectDir, state.tracks, parsed.groups), {
6054
- log,
6055
- err,
6056
- exit,
6057
- mode: "update"
6058
- });
6059
6844
  }
6060
- function registerUpdateCommand(cli2) {
6061
- cli2.command(
6062
- "update",
6063
- "Refresh installed policy files (rules / agents / commands / hooks / skills)"
6064
- ).option("--project-dir <path>", "[Project] Target project directory", {
6065
- default: process.cwd()
6066
- }).option(
6067
- "--only <group>",
6068
- `[Scope] Update only this group (repeatable): ${UPDATE_GROUPS.join(" | ")}`
6069
- ).action((options) => {
6070
- updateAction(options);
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 }] : [];
6071
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));
6858
+ }
6859
+ return { rootClaudeMdKept: false, importStripped, external, moved };
6072
6860
  }
6073
-
6074
- // src/interactive.ts
6075
- init_esm_shims();
6076
-
6077
- // src/prompts.ts
6078
- init_esm_shims();
6079
-
6080
- // src/router.ts
6081
- init_esm_shims();
6082
- function buildRouterChoices(state) {
6083
- const detected = state.tracks.length > 0 ? state.tracks.join(", ") : "(none detected)";
6084
- return [
6085
- {
6086
- value: "add",
6087
- label: "Add a new Track",
6088
- hint: `Current: ${detected}`,
6089
- enabled: true
6090
- },
6091
- {
6092
- value: "update",
6093
- label: "Update policy files (auto-backup)",
6094
- // v26.126.0 (R-3a) — skills 가 목록에 들어왔다. 이 문구가 곧 update 의 광고이고
6095
- // (update 는 위저드로만 도달한다) 실동작과 어긋나면 그 자체로 거짓출하다.
6096
- hint: "Refresh rules / agents / commands / hooks / skills \u2014 your edits are backed up",
6097
- enabled: true
6098
- },
6099
- {
6100
- value: "remove",
6101
- label: "Remove a Track (unsupported)",
6102
- hint: "Manual edit of .claude/ required \u2014 not automated",
6103
- enabled: false
6104
- },
6105
- {
6106
- value: "reinstall",
6107
- label: "Reinstall (backs up current .claude/ first)",
6108
- hint: "Use when state is corrupted",
6109
- enabled: true
6110
- },
6111
- {
6112
- value: "exit",
6113
- label: "Exit",
6114
- enabled: true
6115
- }
6116
- ];
6117
- }
6118
- function summarizeState(state) {
6119
- if (state.state === "new") {
6120
- return "No prior install detected \u2014 new install flow.";
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;
6121
6868
  }
6122
- const trackList = state.tracks.length > 0 ? state.tracks.join(", ") : "(no tracks resolved)";
6123
- 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";
6124
- return `Existing install detected ${sourceLabel}. Tracks: ${trackList}.`;
6125
6869
  }
6126
-
6127
- // src/wizard-steps.ts
6128
- init_esm_shims();
6129
- var WIZARD_TOTAL = 6;
6130
- var WIZARD = {
6131
- TRACKS: { current: 1, total: WIZARD_TOTAL },
6132
- CLI: { current: 2, total: WIZARD_TOTAL },
6133
- TARGETS: { current: 3, total: WIZARD_TOTAL },
6134
- SCOPE: { current: 4, total: WIZARD_TOTAL },
6135
- CONFIRM: { current: 5, total: WIZARD_TOTAL },
6136
- INSTALL: { current: 6, total: WIZARD_TOTAL }
6137
- };
6138
- function stepLabel(step, suffix) {
6139
- if (!step) return suffix;
6140
- return `Step ${step.current}/${step.total} \u2014 ${suffix}`;
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 }) };
6141
6874
  }
6142
-
6143
- // src/prompts.ts
6144
- var TRACK_LABELS = {
6145
- base: "base \u2014 principles \xB7 methodology \xB7 tests only (no stack)",
6146
- tooling: "tooling \u2014 Bash + Markdown meta-project",
6147
- "csr-supabase": "csr-supabase \u2014 Vite + React + Supabase",
6148
- "csr-fastify": "csr-fastify \u2014 Vite + React + Fastify",
6149
- "csr-fastapi": "csr-fastapi \u2014 Vite + React + FastAPI",
6150
- "ssr-htmx": "ssr-htmx \u2014 htmx + FastAPI",
6151
- "ssr-nextjs": "ssr-nextjs \u2014 Next.js (App Router)",
6152
- data: "data \u2014 Python data / DuckDB / PySide6",
6153
- executive: "executive \u2014 proposals / DD / pitch (no agent-skills)",
6154
- full: "full \u2014 all dev capabilities",
6155
- "project-management": "project-management \u2014 PM / Scrum / Jira / Confluence",
6156
- "growth-marketing": "growth-marketing \u2014 Growth / Marketing / Content"
6157
- };
6158
- var VISIBLE_OPTION_DEFS = [];
6159
- var CLI_BASE_LABELS = {
6160
- claude: "Claude Code",
6161
- codex: "Codex (OpenAI)",
6162
- opencode: "OpenCode (anomalyco)",
6163
- antigravity: "Antigravity (Google)"
6164
- };
6165
- var INSTALL_TARGET_PAGES = [
6166
- // 2026-08-16 — 트랙이 고르는 자산을 **맨 앞에서** 전부 보여준다. 그전까지 이 항목들은 화면에
6167
- // 한 번도 안 나온 채 깔렸고, 사용자는 설치가 끝난 뒤 요약에서 처음 봤다.
6168
- //
6169
- // 2페이지로 나눈 근거는 실측이다: 최대 트랙(`full`)이 32항목(당시 실측 — rules 6 · agents 9 · hooks 3 ·
6170
- // skills 14)이라 clack groupMultiselect 의 "페이지당 ≤ ~30행" 제약을 한 페이지로는 못 지킨다.
6171
- // 가르는 축은 역할이다 — 정책·가드가 한쪽, 실행 능력이 다른 쪽.
6172
- { label: "Track baseline \u2014 Rules & Hooks (\uC815\uCC45\xB7\uAC00\uB4DC)", cats: [], baseline: ["rules", "hooks"] },
6173
- {
6174
- label: "Track baseline \u2014 Agents & Skills (\uC5D0\uC774\uC804\uD2B8\xB7\uC2A4\uD0AC)",
6175
- cats: [],
6176
- baseline: ["agents", "skills"]
6177
- },
6178
- { label: "Dev Core (Frontend \xB7 Backend \xB7 Data)", cats: ["frontend", "backend", "data"] },
6179
- { label: "Dev Tools (Security \xB7 Quality \xB7 Understanding)", cats: ["dev-tools", "understanding"] },
6180
- { label: "Business (PM \xB7 Executive \xB7 Documents)", cats: ["business"] },
6181
- // v26.85.0 — 코드-퍼스트 비주얼/미디어 제작 (용도별 섹션, 전부 opt-in).
6182
- { label: "Visual & Media (Slides \xB7 Diagrams \xB7 Motion \xB7 Video)", cats: ["visual-media"] },
6183
- { label: "Workflow", cats: ["workflow"] }
6184
- ];
6185
- var BASELINE_TITLES = {
6186
- rules: "Rules \u2014 \uC0C1\uC2DC \uC0C1\uC8FC\uD558\uB294 \uB300\uC6D0\uCE59 (4 CLI \uC804\uBD80\uC5D0 \uC124\uCE58)",
6187
- hooks: "Hooks \u2014 \uACB0\uC815\uB860\uC801 \uAC00\uB4DC (\uD574\uC81C\uD558\uBA74 settings.json \uBC30\uC120\uB3C4 \uD568\uAED8 \uBE60\uC9C4\uB2E4)",
6188
- agents: "Agents \u2014 \uB3C5\uB9BD \uB808\uC778 (\uB9AC\uBDF0\xB7\uAC80\uC99D)",
6189
- skills: "Skills \u2014 \uD544\uC694\uD560 \uB54C \uC5F4\uB9AC\uB294 \uC791\uC5C5 \uC808\uCC28"
6190
- };
6191
- function buildPageGroups(cats, initialSet, installedSet = /* @__PURE__ */ new Set(), baselineKinds = [], baselineTargets = []) {
6192
- const installedMark = (value) => installedSet.has(value) ? " \u25CF installed" : "";
6193
- const groups = {};
6194
- const flatItems = [];
6195
- for (const kind of baselineKinds) {
6196
- const items = baselineTargets.filter((t) => t.kind === kind).map((t) => ({
6197
- value: t.id,
6198
- label: ` ${t.name}${installedMark(t.id)}`,
6199
- ...t.hint ? { hint: t.hint } : {}
6200
- }));
6201
- if (items.length === 0) continue;
6202
- groups[BASELINE_TITLES[kind]] = items;
6203
- flatItems.push(...items);
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);
6901
+ }
6902
+ continue;
6903
+ }
6904
+ }
6905
+ rm(abs);
6906
+ removed.push(path);
6204
6907
  }
6205
- for (const cat of cats) {
6206
- const items = [];
6207
- for (const o of VISIBLE_OPTION_DEFS.filter((d2) => d2.category === cat)) {
6208
- items.push({
6209
- value: `option:${o.key}`,
6210
- // v26.62.3 — group header 와 옵션 사이 시각 hierarchy 강화. label prefix 4 space.
6211
- label: ` ${o.label} [${o.source}]${installedMark(`option:${o.key}`)}`,
6212
- hint: o.hint
6213
- });
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;
6916
+ try {
6917
+ if (readdirSync6(cursor).length > 0) return;
6918
+ rmdirSync2(cursor);
6919
+ } catch {
6920
+ return;
6214
6921
  }
6215
- const tierOrder = { official: 0, vetted: 1, experimental: 2 };
6216
- const catAssets = [...EXTERNAL_ASSETS.filter((x) => x.category === cat)].sort(
6217
- (a, b2) => tierOrder[assetTrustTier(a.id)] - tierOrder[assetTrustTier(b2.id)]
6218
- );
6219
- for (const a of catAssets) {
6220
- const tier = assetTrustTier(a.id);
6221
- const badge = tier === "official" ? " \u2605 official" : tier === "experimental" ? " \u26A0 experimental (opt-in)" : "";
6222
- items.push({
6223
- value: `asset:${a.id}`,
6224
- label: ` ${a.id} [${a.source}]${badge}${installedMark(`asset:${a.id}`)}`,
6225
- hint: a.description
6226
- });
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;
6227
6948
  }
6228
- if (items.length === 0) continue;
6229
- const selectedInCat = items.filter((it3) => initialSet.has(it3.value)).length;
6230
- const header = `${CATEGORY_TITLES[cat]} [${selectedInCat}/${items.length} \u2713 default]`;
6231
- groups[header] = items;
6232
- flatItems.push(...items);
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;
6954
+ }
6955
+ if (verdict.remainder !== null) {
6956
+ lines.push(` \u25CB strip harness sections from ${path} (\uBCF8\uBB38 \uBCF4\uC874)`);
6957
+ continue;
6958
+ }
6959
+ }
6960
+ removable += 1;
6233
6961
  }
6234
- return { groups, flatItems };
6962
+ if (removable > 0) lines.unshift(` \u25CB remove ${removable} CLI output file(s)`);
6963
+ return lines;
6235
6964
  }
6236
- function assertPagesCoverAllCategories(pages) {
6237
- const counts = /* @__PURE__ */ new Map();
6238
- for (const page of pages) {
6239
- for (const cat of page.cats) counts.set(cat, (counts.get(cat) ?? 0) + 1);
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)`)}`);
6240
6969
  }
6241
- const missing = CATEGORIES.filter((c3) => !counts.has(c3));
6242
- const duplicated = CATEGORIES.filter((c3) => (counts.get(c3) ?? 0) > 1);
6243
- if (missing.length > 0 || duplicated.length > 0) {
6244
- throw new Error(
6245
- `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).`
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.`
6246
6981
  );
6247
6982
  }
6983
+ return lines;
6248
6984
  }
6249
- assertPagesCoverAllCategories(INSTALL_TARGET_PAGES);
6250
- function viewportItems(itemCount) {
6251
- const rows = process.stdout.rows ?? 24;
6252
- return Math.max(8, Math.min(itemCount, rows - 10));
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)}`))
7047
+ );
7048
+ console.error(c.dim(" Nothing installed here by agent-harness."));
7049
+ process.exit(1);
7050
+ }
7051
+ return;
7052
+ }
7053
+ uninstallAction(picked.options);
6253
7054
  }
6254
- var defaultPrompts = {
6255
- intro: (msg) => ge(msg),
6256
- outro: (msg) => ye(msg),
6257
- cancel: (msg) => me(msg),
6258
- selectTracks: async (initial, step) => {
6259
- const result = await ve({
6260
- message: stepLabel(step, "Select Track(s)"),
6261
- options: TRACKS.map((t) => ({ value: t, label: TRACK_LABELS[t] })),
6262
- ...initial ? { initialValues: initial } : {},
6263
- maxItems: viewportItems(11),
6264
- required: true
6265
- });
6266
- return q(result) ? null : result;
6267
- },
6268
- selectCli: async (initial, step) => {
6269
- const initialValues = initial && initial.length > 0 ? [...initial] : ["claude"];
6270
- const result = await ve({
6271
- message: stepLabel(step, "Target CLI(s)"),
6272
- options: [
6273
- { value: "claude", label: CLI_BASE_LABELS.claude },
6274
- { value: "codex", label: CLI_BASE_LABELS.codex },
6275
- { value: "opencode", label: CLI_BASE_LABELS.opencode },
6276
- { value: "antigravity", label: CLI_BASE_LABELS.antigravity }
6277
- ],
6278
- initialValues,
6279
- required: true
6280
- });
6281
- if (q(result)) return null;
6282
- return [...result].sort(
6283
- (a, b2) => CLI_BASE_SORT_ORDER[a] - CLI_BASE_SORT_ORDER[b2]
6284
- );
6285
- },
6286
- selectAction: async (state) => {
6287
- const result = await Ee({
6288
- message: summarizeState(state),
6289
- options: buildRouterChoices(state).map((c3) => {
6290
- const label = c3.enabled ? c3.label : `${c3.label} [disabled]`;
6291
- return {
6292
- value: c3.value,
6293
- label,
6294
- ...c3.hint ? { hint: c3.hint } : {},
6295
- ...c3.enabled ? {} : { disabled: true }
6296
- };
6297
- })
6298
- });
6299
- return q(result) ? null : result;
6300
- },
6301
- /**
6302
- * v26.64.0 (ADR-020) — Installation scope select. Default Project (D16 — no global write).
6303
- * Global 은 사용자 명시 opt-in 시에만.
6304
- */
6305
- selectScope: async (initial = "project", step) => {
6306
- const result = await Ee({
6307
- message: stepLabel(step, "Installation scope"),
6308
- initialValue: initial,
6309
- options: [
6310
- {
6311
- value: "project",
6312
- label: "Project",
6313
- hint: "Install in current directory (committed with your project)"
6314
- },
6315
- {
6316
- value: "global",
6317
- label: "Global",
6318
- hint: "Write to ~/.claude/, ~/.codex/, npm -g (shared across all projects)"
6319
- }
6320
- ]
6321
- });
6322
- return q(result) ? null : result;
6323
- },
6324
- selectUpdateGroups: async () => {
6325
- const result = await ve({
6326
- message: "What to update (space = toggle \xB7 your edits are always backed up)",
6327
- options: [
6328
- { value: "skills", label: "Skills", hint: "installed bundled skills \u2192 latest" },
6329
- {
6330
- value: "new-skills",
6331
- label: "New skills",
6332
- hint: "bundled skills added since your install"
6333
- },
6334
- {
6335
- value: "rules",
6336
- label: "Rules \xB7 agents \xB7 commands",
6337
- hint: "your edited files \u2192 *.backup-<time>"
6338
- },
6339
- {
6340
- value: "anchor",
6341
- label: "CLAUDE-uzys-harness.md anchor",
6342
- hint: "your edited anchor \u2192 *.backup-<time>"
6343
- },
6344
- {
6345
- value: "hooks",
6346
- label: "Hooks \xB7 settings.json",
6347
- hint: "stale hook refs cleaned"
6348
- },
6349
- {
6350
- value: "external",
6351
- label: "Codex / OpenCode / Antigravity + external skills",
6352
- hint: "rendered artifacts + npx skills"
6353
- }
6354
- ],
6355
- initialValues: [...UPDATE_GROUPS],
6356
- required: true
6357
- });
6358
- return q(result) ? null : result;
6359
- },
6360
- confirmInstall: async (summary) => {
6361
- const result = await ue({
6362
- message: `${summary}
6363
7055
 
6364
- Proceed?`,
6365
- initialValue: true
6366
- });
6367
- return q(result) ? null : result;
6368
- },
6369
- selectInstallTargets: async (initialChecked, step, recap) => {
6370
- const pages = INSTALL_TARGET_PAGES;
6371
- const baselineTargets = withBaselineHints(listBaselineTargets({ tracks: recap?.tracks ?? [] }));
6372
- const initialSet = new Set(initialChecked);
6373
- const collected = new Set(initialChecked);
6374
- const recapLine = recap ? `Tracks: ${recap.tracks.join(", ")} \xB7 CLIs: ${recap.cli.join(", ")}` : "";
6375
- const installedSet = new Set(recap?.installed ?? []);
6376
- process.stdout.write("\x1B[?1049h");
6377
- let resultIds = null;
6378
- let aborted = false;
6379
- try {
6380
- let pageIdx = 0;
6381
- let dir = 1;
6382
- while (pageIdx < pages.length) {
6383
- const page = pages[pageIdx];
6384
- if (!page) break;
6385
- const { groups, flatItems } = buildPageGroups(
6386
- page.cats,
6387
- initialSet,
6388
- installedSet,
6389
- page.baseline ?? [],
6390
- baselineTargets
6391
- );
6392
- if (flatItems.length === 0) {
6393
- pageIdx += dir;
6394
- if (pageIdx < 0) {
6395
- aborted = true;
6396
- break;
6397
- }
6398
- continue;
6399
- }
6400
- const selectedNow = flatItems.filter((it3) => collected.has(it3.value)).map((it3) => it3.value);
6401
- const pageDefault = flatItems.filter((it3) => initialSet.has(it3.value)).length;
6402
- const totalSelected = collected.size;
6403
- const message = [
6404
- `Step ${step.current}/${step.total} \xB7 Page ${pageIdx + 1}/${pages.length} \xB7 ${page.label}`,
6405
- recapLine ? ` ${recapLine}` : "",
6406
- ` Selected so far: ${totalSelected} items \xB7 This page default \u2713 ${pageDefault}/${flatItems.length}`,
6407
- // 마커의 뜻을 화면에서 바로 알려준다 — 체크 해제를 제거로 오해하는 것이 이 화면의
6408
- // 원래 문제였으므로, 마커가 보일 때는 제거 경로를 같은 자리에서 말한다.
6409
- 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)" : "",
6410
- " Space toggle \xB7 Enter \u2192 next \xB7 ESC \u2192 prev"
6411
- ].filter(Boolean).join("\n");
6412
- const groupOpts = {
6413
- message,
6414
- options: groups,
6415
- initialValues: selectedNow,
6416
- required: false,
6417
- selectableGroups: false
6418
- };
6419
- const result = await pe(groupOpts);
6420
- if (q(result)) {
6421
- if (pageIdx === 0) {
6422
- aborted = true;
6423
- break;
6424
- }
6425
- dir = -1;
6426
- pageIdx--;
6427
- continue;
6428
- }
6429
- for (const it3 of flatItems) collected.delete(it3.value);
6430
- for (const v2 of result) collected.add(v2);
6431
- dir = 1;
6432
- pageIdx++;
6433
- }
6434
- if (!aborted) {
6435
- resultIds = [...collected];
6436
- }
6437
- } finally {
6438
- process.stdout.write("\x1B[?1049l");
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 };
6439
7080
  }
6440
- if (resultIds !== null) {
6441
- process.stdout.write(
6442
- `\u25C7 Step ${step.current}/${step.total} \u2014 Install targets \xB7 ${resultIds.length} selected
6443
- \u2502
6444
- `
6445
- );
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);
6446
7105
  }
6447
- return resultIds;
6448
7106
  }
6449
- };
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
+ }
6450
7167
 
6451
7168
  // src/interactive.ts
7169
+ init_esm_shims();
7170
+ import { existsSync as existsSync20 } from "fs";
7171
+ import { join as join21 } from "path";
6452
7172
  function splitInstallTargets(targets) {
6453
7173
  const optionKeys = [];
6454
7174
  const assetIds = [];
@@ -6489,6 +7209,59 @@ function initialTargetSelection(tracks, installedProjectAssetIds) {
6489
7209
  const baseline = listBaselineTargets({ tracks }).map((t) => t.id);
6490
7210
  return [...assets, ...baseline];
6491
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
+ }
6492
7265
  async function runInteractive(projectDir, deps = {}) {
6493
7266
  const prompts = deps.prompts ?? defaultPrompts;
6494
7267
  const detect = deps.detect ?? detectInstallState;
@@ -6503,10 +7276,10 @@ async function runInteractive(projectDir, deps = {}) {
6503
7276
  prompts.intro("uzys-agent-harness installer");
6504
7277
  const state = detect(projectDir);
6505
7278
  const installed = (deps.readInstalled ?? installedTargetState)(projectDir);
6506
- let initialTracks;
6507
- let mode = "fresh";
6508
7279
  if (state.state === "existing") {
6509
- 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);
6510
7283
  if (action === null) {
6511
7284
  prompts.cancel("Cancelled.");
6512
7285
  return { ok: false, reason: "cancelled" };
@@ -6515,34 +7288,14 @@ async function runInteractive(projectDir, deps = {}) {
6515
7288
  prompts.outro("Exiting without changes.");
6516
7289
  return { ok: false, reason: "exit" };
6517
7290
  }
6518
- if (action === "remove") {
6519
- prompts.cancel("Track removal is not automated \u2014 manually edit `.claude/`. Aborting.");
6520
- return { ok: false, reason: "disabled-action" };
6521
- }
6522
- if (action === "update") {
6523
- mode = "update";
6524
- const groups = prompts.selectUpdateGroups ? await prompts.selectUpdateGroups() : null;
6525
- if (groups === null && prompts.selectUpdateGroups) {
6526
- prompts.cancel("Cancelled.");
6527
- return { ok: false, reason: "cancelled" };
6528
- }
6529
- const spec = buildUpdateSpec(projectDir, state.tracks, groups ?? void 0);
6530
- const scopeLine = spec.updateOnly ? `UPDATE ${spec.updateOnly.join(" \xB7 ")} only (untouched: ${UPDATE_GROUPS.filter((g) => !spec.updateOnly?.includes(g)).join(", ")}):` : "UPDATE installed harness files:";
6531
- const confirmed = await prompts.confirmInstall(`${scopeLine}
6532
- ${formatSummary(spec)}`);
6533
- if (!confirmed) {
6534
- prompts.outro("Cancelled.");
6535
- return { ok: false, reason: "cancelled" };
6536
- }
6537
- prompts.outro("Running update mode...");
6538
- return { ok: true, mode: "update", spec };
7291
+ if (action === "uninstall") {
7292
+ return { ok: true, uninstall: true };
6539
7293
  }
6540
- if (action === "add") {
6541
- mode = "add";
6542
- initialTracks = state.tracks;
6543
- } else if (action === "reinstall") {
6544
- 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 };
6545
7297
  }
7298
+ return runUpdateFlow({ projectDir, state, log, prompts, installed });
6546
7299
  }
6547
7300
  let step = "tracks";
6548
7301
  let tracks = null;
@@ -6551,7 +7304,7 @@ ${formatSummary(spec)}`);
6551
7304
  let scope = "project";
6552
7305
  while (true) {
6553
7306
  if (step === "tracks") {
6554
- const result = await prompts.selectTracks(tracks ?? initialTracks, WIZARD.TRACKS);
7307
+ const result = await prompts.selectTracks(tracks ?? void 0, WIZARD.TRACKS);
6555
7308
  if (result === null) {
6556
7309
  prompts.cancel("Cancelled.");
6557
7310
  return { ok: false, reason: "cancelled" };
@@ -6605,7 +7358,7 @@ ${formatSummary(spec)}`);
6605
7358
  projectDir,
6606
7359
  ...userOverride ? { userOverride } : {},
6607
7360
  // 해제 목록을 안 넘기면 이 화면이 **제외 유무와 문자열이 완전히 같아진다** — 같은 화면이
6608
- // 외부 자산 제거는 `-User removed:` 로 이미 보고하므로, 없음은 "아무것도 안 빠졌다"로
7361
+ // 외부 자산 제거는 `-Unchecked by you:` 로 이미 보고하므로, 없음은 "아무것도 안 빠졌다"로
6609
7362
  // 읽힌다. 상주 비용을 줄이려고 20개를 푼 사용자가 그대로인 숫자를 보게 된다.
6610
7363
  ...baselineExclude.length > 0 ? { baselineExclude } : {}
6611
7364
  })}
@@ -6632,10 +7385,169 @@ ${summary}`
6632
7385
  ...baselineExclude.length > 0 ? { baselineExclude } : {}
6633
7386
  };
6634
7387
  prompts.outro(stepLabel(WIZARD.INSTALL, "Installing..."));
6635
- 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;
6636
7458
  }
6637
7459
  }
6638
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
+ }
6639
7551
  function tracksEqual(a, b2) {
6640
7552
  if (a.length !== b2.length) return false;
6641
7553
  const sortedA = [...a].sort();
@@ -6688,7 +7600,7 @@ function formatSummary(spec) {
6688
7600
  lines.push(` +User added: ${spec.userOverride.forceInclude.join(", ")}`);
6689
7601
  }
6690
7602
  if (spec.userOverride.forceExclude.length > 0) {
6691
- lines.push(` -User removed: ${spec.userOverride.forceExclude.join(", ")}`);
7603
+ lines.push(` -Unchecked by you: ${spec.userOverride.forceExclude.join(", ")}`);
6692
7604
  }
6693
7605
  }
6694
7606
  if (baselineExcluded.size > 0) {
@@ -6708,6 +7620,11 @@ async function defaultAction(deps = {}) {
6708
7620
  const run = deps.run ?? ((cwd) => runInteractive(cwd));
6709
7621
  const execute = deps.execute ?? executeSpec;
6710
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
+ }
6711
7628
  if (!result.ok) {
6712
7629
  if (result.message) {
6713
7630
  err(result.message);